diff --git a/components/bt/esp_ble_audio/CMakeLists.txt b/components/bt/esp_ble_audio/CMakeLists.txt index 55487047587..7ace6e00e13 100644 --- a/components/bt/esp_ble_audio/CMakeLists.txt +++ b/components/bt/esp_ble_audio/CMakeLists.txt @@ -67,8 +67,8 @@ if(CONFIG_BT_ISO) list(APPEND ble_audio_include_dirs "${CMAKE_CURRENT_LIST_DIR}/api/iso/include" - "${CMAKE_CURRENT_LIST_DIR}/host" - "${CMAKE_CURRENT_LIST_DIR}/host/adapter" + "${CMAKE_CURRENT_LIST_DIR}/host/common/include" + "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/include" "${CMAKE_CURRENT_LIST_DIR}/include/subsys/bluetooth" "${CMAKE_CURRENT_LIST_DIR}/include/subsys/bluetooth/host" "${CMAKE_CURRENT_LIST_DIR}/include" diff --git a/components/bt/esp_ble_audio/api/audio/include/esp_ble_audio_bap_lc3_preset_defs.h b/components/bt/esp_ble_audio/api/audio/include/esp_ble_audio_bap_lc3_preset_defs.h index bc5ed17566e..e2d922ce9d3 100644 --- a/components/bt/esp_ble_audio/api/audio/include/esp_ble_audio_bap_lc3_preset_defs.h +++ b/components/bt/esp_ble_audio/api/audio/include/esp_ble_audio_bap_lc3_preset_defs.h @@ -57,6 +57,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 8_1_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -71,6 +72,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 8_2_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -85,6 +87,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 16_1_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -99,6 +102,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 16_2_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -113,6 +117,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 24_1_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -129,6 +134,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; * * Mandatory to support as unicast server. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -143,6 +149,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 32_1_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -157,6 +164,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 32_2_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -171,6 +179,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 441_1_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -185,6 +194,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 441_2_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -199,6 +209,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 48_1_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -213,6 +224,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 48_2_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -227,6 +239,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 48_3_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -241,6 +254,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 48_4_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -255,6 +269,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 48_5_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -269,6 +284,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 48_6_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -283,6 +299,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 8_1_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -297,6 +314,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 8_2_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -311,6 +329,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 16_1_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -325,6 +344,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 16_2_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -339,6 +359,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 24_1_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -353,6 +374,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 24_2_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -367,6 +389,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 32_1_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -381,6 +404,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 32_2_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -395,6 +419,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 441_1_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -409,6 +434,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 441_2_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -423,6 +449,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 48_1_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -437,6 +464,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 48_2_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -451,6 +479,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 48_3_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -465,6 +494,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 48_4_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -479,6 +509,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 48_5_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -493,6 +524,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Unicast 48_6_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -507,6 +539,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 8_1_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -521,6 +554,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 8_2_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -535,6 +569,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 16_1_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -551,6 +586,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; * * Mandatory to support as both broadcast source and sink. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -565,6 +601,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 24_1_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -581,6 +618,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; * * Mandatory to support as broadcast sink. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -595,6 +633,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 32_1_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -609,6 +648,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 32_2_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -623,6 +663,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 441_1_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -637,6 +678,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 441_2_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -651,6 +693,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 48_1_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -665,6 +708,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 48_2_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -679,6 +723,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 48_3_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -693,6 +738,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 48_4_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -707,6 +753,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 48_5_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -721,6 +768,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 48_6_1 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -735,6 +783,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 8_1_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -749,6 +798,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 8_2_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -763,6 +813,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 16_1_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -779,6 +830,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; * * Mandatory to support as both broadcast source and sink. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -793,6 +845,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 24_1_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -809,6 +862,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; * * Mandatory to support as broadcast sink. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -823,6 +877,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 32_1_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -837,6 +892,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 32_2_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -851,6 +907,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 441_1_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -865,6 +922,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 441_2_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -879,6 +937,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 48_1_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -893,6 +952,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 48_2_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -907,6 +967,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 48_3_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -921,6 +982,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 48_4_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -935,6 +997,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 48_5_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ @@ -949,6 +1012,7 @@ typedef struct bt_bap_lc3_preset esp_ble_audio_bap_lc3_preset_t; /** * @brief Helper to declare LC3 Broadcast 48_6_2 codec configuration. * + * @param _name Preset configuration name. * @param _loc Audio channel location bitfield. * @param _stream_context Stream context. */ diff --git a/components/bt/esp_ble_audio/api/audio/include/esp_ble_audio_mcs_defs.h b/components/bt/esp_ble_audio/api/audio/include/esp_ble_audio_mcs_defs.h index 3a75a162f40..1e65d7c21d0 100644 --- a/components/bt/esp_ble_audio/api/audio/include/esp_ble_audio_mcs_defs.h +++ b/components/bt/esp_ble_audio/api/audio/include/esp_ble_audio_mcs_defs.h @@ -257,6 +257,8 @@ extern "C" { /** * Check if an Object ID is valid for MCS. * This differs from BT_OTS_VALID_OBJ_ID as MCS does not use the directory list object. + * + * @param id Object ID to validate. */ #define ESP_BLE_AUDIO_MCS_VALID_OBJ_ID(id) \ (IN_RANGE((id), BT_OTS_OBJ_ID_MIN, BT_OTS_OBJ_ID_MAX)) diff --git a/components/bt/esp_ble_audio/api/iso/include/esp_ble_iso_common_api.h b/components/bt/esp_ble_audio/api/iso/include/esp_ble_iso_common_api.h index d46d6020f4c..6e9281b4fae 100644 --- a/components/bt/esp_ble_audio/api/iso/include/esp_ble_iso_common_api.h +++ b/components/bt/esp_ble_audio/api/iso/include/esp_ble_iso_common_api.h @@ -37,6 +37,8 @@ extern "C" { * BIT(0) that represents index 0x01. To synchronize to e.g. BIS * indexes 0x01 and 0x02, the bitfield value should be BIT(0) | BIT(1). * As a general notation, to sync to BIS index N use BIT(N - 1). + * + * @param x BIS index (1-based). */ #define ESP_BLE_ISO_BIS_INDEX_BIT(x) BT_ISO_BIS_INDEX_BIT(x) diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/gap.c b/components/bt/esp_ble_audio/host/adapter/nimble/gap.c index 079c98034be..867cd3238d5 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/gap.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/gap.c @@ -17,16 +17,66 @@ #include <../host/iso_internal.h> #include "host/ble_gap.h" +#include "host/ble_store.h" #include "services/gap/ble_svc_gap.h" #include "common/host.h" #include "common/app/gap.h" +#include "nimble/hs_error.h" + +/* Map a NimBLE connection's security state to Zephyr bt_security_t. + * + * To reach L3/L4 the app must configure NimBLE for authenticated pairing: + * ble_hs_cfg.sm_mitm = 1; + * ble_hs_cfg.sm_io_cap = ; + * (e.g. DISP_ONLY + KEYBOARD_ONLY -> Passkey Entry, + * DISP_YES_NO on both sides -> Numeric Compare) + * L4 additionally requires ble_hs_cfg.sm_sc = 1 (LE Secure Connections). + * + * ble_gap_sec_state does not expose an SC flag; without it we cannot tell + * legacy authenticated pairing apart from SC. Look up the peer's LTK record + * to read the persisted sc bit. If the lookup fails (not bonded, store + * unavailable) downgrade to L3 conservatively. + */ +static uint8_t nimble_desc_to_sec_level(const struct ble_gap_conn_desc *desc) +{ + const struct ble_gap_sec_state *sec_state = &desc->sec_state; + struct ble_store_value_sec value = {0}; + struct ble_store_key_sec key = {0}; + int rc; + + /* L1: !encrypted */ + if (!sec_state->encrypted) { + return BT_SECURITY_L1; + } + + /* L2: encrypted, unauthenticated (Just Works) */ + if (!sec_state->authenticated) { + return BT_SECURITY_L2; + } + + /* L3: encrypted, authenticated, key_size < 16 */ + if (sec_state->key_size < 16) { + return BT_SECURITY_L3; + } + + key.peer_addr = desc->peer_id_addr; + rc = ble_store_read_peer_sec(&key, &value); + if (rc != 0 || !value.sc) { + /* Cannot confirm SC -> conservatively L3 */ + return BT_SECURITY_L3; + } + + /* L4: encrypted, authenticated, 128-bit key, LE Secure Connections */ + return BT_SECURITY_L4; +} + void bt_le_nimble_gap_post_event(void *param) { - struct bt_le_gap_app_param *qev; - struct ble_gap_conn_desc desc; - struct ble_gap_event *ev; + struct bt_le_gap_app_param *qev = NULL; + struct ble_gap_conn_desc desc = {0}; + struct ble_gap_event *ev = NULL; int err; qev = calloc(1, sizeof(*qev)); @@ -139,7 +189,8 @@ void bt_le_nimble_gap_post_event(void *param) qev->security_change.conn_handle = ev->enc_change.conn_handle; qev->security_change.role = desc.role; - qev->security_change.sec_level = BT_SECURITY_L3; + qev->security_change.sec_level = nimble_desc_to_sec_level(&desc); + qev->security_change.bonded = desc.sec_state.bonded; qev->security_change.dst.type = desc.peer_id_addr.type; memcpy(qev->security_change.dst.val, desc.peer_id_addr.val, BT_ADDR_SIZE); } @@ -191,15 +242,16 @@ int bt_le_nimble_scan_start(const struct bt_le_scan_param *param, ble_gap_event_ scan_param.passive = !param->type; scan_param.filter_duplicates = 0; - return ble_gap_disc(BLE_OWN_ADDR_PUBLIC, BLE_HS_FOREVER, &scan_param, cb, NULL); + return nimble_err_to_errno(ble_gap_disc(BLE_OWN_ADDR_PUBLIC, BLE_HS_FOREVER, + &scan_param, cb, NULL)); } int bt_le_nimble_scan_stop(void) { - return ble_gap_disc_cancel(); + return nimble_err_to_errno(ble_gap_disc_cancel()); } int bt_le_nimble_iso_disconnect(uint16_t conn_handle, uint8_t reason) { - return ble_gap_iso_disconnect(conn_handle, reason); + return nimble_err_to_errno(ble_gap_iso_disconnect(conn_handle, reason)); } diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/gatt/gatt.c b/components/bt/esp_ble_audio/host/adapter/nimble/gatt/gatt.c index 6b6f52beea7..f29bb32ed50 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/gatt/gatt.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/gatt/gatt.c @@ -23,6 +23,8 @@ #include "host/ble_hs_mbuf.h" #include "common/host.h" + +#include "nimble/hs_error.h" #include "common/app/gatt.h" void bt_le_nimble_gatt_post_event(void *param) @@ -418,7 +420,7 @@ int bt_le_nimble_gatts_notify(struct bt_conn *conn, struct bt_gatt_notify_params { LOG_DBG("[N]GattsNtf[%u]", conn ? conn->handle : UINT16_MAX); - return gatts_notify(conn, params); + return nimble_err_to_errno(gatts_notify(conn, params)); } int bt_le_nimble_gatts_indicate(struct bt_conn *conn, struct bt_gatt_indicate_params *params) @@ -514,7 +516,7 @@ int bt_le_nimble_gattc_write_without_rsp(struct bt_conn *conn, uint16_t handle, { LOG_DBG("[N]GattcWrCmd[%u][%u][%u]", conn->handle, handle, length); - return ble_gattc_write_no_rsp_flat(conn->handle, handle, data, length); + return nimble_err_to_errno(ble_gattc_write_no_rsp_flat(conn->handle, handle, data, length)); } int bt_le_nimble_gattc_write_ccc(struct bt_conn *conn, struct bt_gatt_subscribe_params *params) diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/gatt/gatt.db.c b/components/bt/esp_ble_audio/host/adapter/nimble/gatt/gatt.db.c index acc3007fc31..b49ebf64a4a 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/gatt/gatt.db.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/gatt/gatt.db.c @@ -662,18 +662,33 @@ static int gattc_db_find_inc_svcs_cb_safe(uint16_t conn_handle, if (error->status) { if (error->status == BLE_HS_EDONE || error->status == BLE_HS_EINVAL) { - /* If a service of the remote GATT server: - * - Has an included service; - * - Only have 2 attributes, i.e. Service declaration and - * Include declaration; - * - Not the last service within the remote GATT database; + /* BLE_HS_EINVAL here is benign noise from NimBLE iteration, + * not a real failure. Root cause: * - * These will cause BLE_HS_EINVAL returned while finding - * included services, mark the included service discovery - * as completed to prevent the discovery state machine from - * stalling. + * In ble_gattc_find_inc_svcs_rx_complete (ble_gattc.c), the + * end-of-procedure check is `prev_handle == 0xffff` only. If + * the last include attribute lands on the parent service's + * end_handle (e.g. a service with just decl + include), then + * prev_handle is bumped to end_handle and the resume path + * calls ble_att_clt_tx_read_type with [end+1, end] -- which + * fails its `start > end` guard with BLE_HS_EINVAL and prints + * E NimBLE: ble_att_clt_tx_read_type rc=3 * - * Mark the included service discovery as completed. + * The proper upstream fix would be in ble_gattc.c: + * + * - if (proc->find_inc_svcs.prev_handle == 0xffff) { + * + if (proc->find_inc_svcs.prev_handle == 0xffff || + * + proc->find_inc_svcs.prev_handle >= + * + proc->find_inc_svcs.end_handle) { + * // Procedure complete. + * ble_gattc_find_inc_svcs_cb(proc, BLE_HS_EDONE, 0, NULL); + * return BLE_HS_EDONE; + * } + * + * We keep NimBLE untouched and absorb the EINVAL here. + * + * Mark the included service discovery as completed so the + * state machine moves on instead of stalling. */ asvc->inc_svc_disc = 1; @@ -1432,7 +1447,10 @@ void bt_le_nimble_gattc_db_remove(uint16_t conn_handle) adb = gattc_db_find(conn_handle); if (adb == NULL) { - LOG_WRN("[N]GattcDbNotFoundToRemove[%u]", conn_handle); + /* Legitimate when the conn never ran auto-disc (e.g. pairing failed + * or the app doesn't use GATT). Idempotent remove, not an error. + */ + LOG_DBG("[N]GattcDbNotFoundToRemove[%u]", conn_handle); return; } diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/gatt/gatt.nrp.c b/components/bt/esp_ble_audio/host/adapter/nimble/gatt/gatt.nrp.c index 347bff8cc20..470c2f8ba09 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/gatt/gatt.nrp.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/gatt/gatt.nrp.c @@ -24,6 +24,8 @@ #include "common/host.h" +#include "nimble/hs_error.h" + struct gatt_nrp_node { uint8_t type; @@ -703,7 +705,9 @@ static int gatt_nrp_insert(struct bt_conn *conn, uint8_t type, void *params) LOG_DBG("[N]GattNrpAppend[%p]", nrp_node); sys_slist_append(&nrp->list, &nrp_node->node); } else { - LOG_ERR("[N]GattNrpInsertFail[%d]", rc); + if (rc != BLE_HS_ENOTCONN) { + LOG_ERR("[N]GattNrpInsertFail[%d]", rc); + } free(nrp_node); } @@ -746,7 +750,8 @@ int bt_le_nimble_gatt_nrp_insert(struct bt_conn *conn, uint8_t type, void *param } else { rc = gatt_nrp_insert(conn, type, params); } - return rc; + + return nimble_err_to_errno(rc); } int bt_le_nimble_gatt_nrp_remove(struct bt_conn *conn, uint8_t type, void *params) diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/gap.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/gap.h similarity index 100% rename from components/bt/esp_ble_audio/host/adapter/nimble/gap.h rename to components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/gap.h diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/gatt/gatt.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/gatt.h similarity index 100% rename from components/bt/esp_ble_audio/host/adapter/nimble/gatt/gatt.h rename to components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/gatt.h diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/hs_error.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/hs_error.h new file mode 100644 index 00000000000..ae28fd9e5a3 --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/hs_error.h @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "host/ble_hs.h" + +/** + * Translate a NimBLE host error code (positive BLE_HS_* value from + * host/ble_hs.h) into a standard negative POSIX errno. + * + * Used at the adapter boundary to keep NimBLE-specific error codes out of + * common/ and lib code, which use standard Zephyr/POSIX errno conventions. + * + * Pass-through rules: + * - rc == 0: success, returned as-is + * - rc < 0: already in negative-errno form, returned as-is + * - rc >= 0x100: protocol-level error (ATT / HCI / L2CAP base) whose + * encoded class the caller may extract — returned as-is so + * the semantic information is preserved + * - 1..31: known NimBLE host codes, translated below + * - other: unknown NimBLE code, mapped to -EIO so it is not + * confused with a known POSIX errno + */ +static inline int nimble_err_to_errno(int rc) +{ + if (rc <= 0) { + return rc; + } + + if (rc >= 0x100) { + return rc; + } + + switch (rc) { + case BLE_HS_EAGAIN: return -EAGAIN; + case BLE_HS_EALREADY: return -EALREADY; + case BLE_HS_EINVAL: return -EINVAL; + case BLE_HS_EMSGSIZE: return -EMSGSIZE; + case BLE_HS_ENOENT: return -ENOENT; + case BLE_HS_ENOMEM: return -ENOMEM; + case BLE_HS_ENOTCONN: return -ENOTCONN; + case BLE_HS_ENOTSUP: return -ENOTSUP; + case BLE_HS_EAPP: return -EPROTO; + case BLE_HS_EBADDATA: return -EBADMSG; + case BLE_HS_EOS: return -EIO; + case BLE_HS_ECONTROLLER: return -EIO; + case BLE_HS_ETIMEOUT: return -ETIMEDOUT; + case BLE_HS_EDONE: return -EALREADY; + case BLE_HS_EBUSY: return -EBUSY; + case BLE_HS_EREJECT: return -EACCES; + case BLE_HS_EUNKNOWN: return -EIO; + case BLE_HS_EROLE: return -EPERM; + case BLE_HS_ETIMEOUT_HCI: return -ETIMEDOUT; + case BLE_HS_ENOMEM_EVT: return -ENOMEM; + case BLE_HS_ENOADDR: return -EADDRNOTAVAIL; + case BLE_HS_ENOTSYNCED: return -EAGAIN; + case BLE_HS_EAUTHEN: return -EPERM; + case BLE_HS_EAUTHOR: return -EACCES; + case BLE_HS_EENCRYPT: return -EPERM; + case BLE_HS_EENCRYPT_KEY_SZ: return -EINVAL; + case BLE_HS_ESTORE_CAP: return -ENOSPC; + case BLE_HS_ESTORE_FAIL: return -EIO; + case BLE_HS_EPREEMPTED: return -EINTR; + case BLE_HS_EDISABLED: return -EPERM; + case BLE_HS_ESTALLED: return -EAGAIN; /* flow-control stall */ + default: return -EIO; + } +} diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/init.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/init.h similarity index 100% rename from components/bt/esp_ble_audio/host/adapter/nimble/init.h rename to components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/init.h diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/iso.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/iso.h similarity index 100% rename from components/bt/esp_ble_audio/host/adapter/nimble/iso.h rename to components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/iso.h diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/l2cap.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/l2cap.h similarity index 100% rename from components/bt/esp_ble_audio/host/adapter/nimble/l2cap.h rename to components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/l2cap.h diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/ascs.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/ascs.h similarity index 100% rename from components/bt/esp_ble_audio/host/adapter/nimble/profiles/ascs.h rename to components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/ascs.h diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/bass.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/bass.h similarity index 100% rename from components/bt/esp_ble_audio/host/adapter/nimble/profiles/bass.h rename to components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/bass.h diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/cas.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/cas.h similarity index 100% rename from components/bt/esp_ble_audio/host/adapter/nimble/profiles/cas.h rename to components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/cas.h diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/csis.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/csis.h similarity index 100% rename from components/bt/esp_ble_audio/host/adapter/nimble/profiles/csis.h rename to components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/csis.h diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/has.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/has.h similarity index 100% rename from components/bt/esp_ble_audio/host/adapter/nimble/profiles/has.h rename to components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/has.h diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/mcs.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/mcs.h similarity index 100% rename from components/bt/esp_ble_audio/host/adapter/nimble/profiles/mcs.h rename to components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/mcs.h diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/mics.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/mics.h similarity index 100% rename from components/bt/esp_ble_audio/host/adapter/nimble/profiles/mics.h rename to components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/mics.h diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/pacs.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/pacs.h similarity index 100% rename from components/bt/esp_ble_audio/host/adapter/nimble/profiles/pacs.h rename to components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/pacs.h diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/server.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/server.h similarity index 100% rename from components/bt/esp_ble_audio/host/adapter/nimble/profiles/server.h rename to components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/server.h diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/tbs.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/tbs.h similarity index 100% rename from components/bt/esp_ble_audio/host/adapter/nimble/profiles/tbs.h rename to components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/tbs.h diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/tmas.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/tmas.h similarity index 100% rename from components/bt/esp_ble_audio/host/adapter/nimble/profiles/tmas.h rename to components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/tmas.h diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/vcs.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/vcs.h similarity index 100% rename from components/bt/esp_ble_audio/host/adapter/nimble/profiles/vcs.h rename to components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/vcs.h diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/iso.c b/components/bt/esp_ble_audio/host/adapter/nimble/iso.c index 3ad173f444e..6538a0d1ec6 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/iso.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/iso.c @@ -19,6 +19,8 @@ #include "common/host.h" +#include "nimble/hs_error.h" + extern int ble_hs_iso_evt_rx_cb_set(void *cb); #if CONFIG_BT_ISO_RX extern int ble_hs_iso_pkt_rx_cb_set(ble_hs_iso_pkt_rx_fn cb); @@ -705,54 +707,72 @@ int bt_le_nimble_iso_cmd_send_sync(uint16_t opcode, struct net_buf *buf, struct net_buf **rsp) { + int rc; + switch (opcode) { case BT_HCI_OP_LE_READ_ISO_TX_SYNC: - return hci_cmd_read_iso_tx_sync(buf, rsp); + rc = hci_cmd_read_iso_tx_sync(buf, rsp); + break; case BT_HCI_OP_LE_SET_CIG_PARAMS: - return hci_cmd_set_cig_params(buf, rsp); + rc = hci_cmd_set_cig_params(buf, rsp); + break; case BT_HCI_OP_LE_SET_CIG_PARAMS_TEST: - return hci_cmd_set_cig_params_test(buf, rsp); + rc = hci_cmd_set_cig_params_test(buf, rsp); + break; case BT_HCI_OP_LE_CREATE_CIS: - return hci_cmd_create_cis(buf, rsp); + rc = hci_cmd_create_cis(buf, rsp); + break; case BT_HCI_OP_LE_REMOVE_CIG: - return hci_cmd_remove_cig(buf, rsp); + rc = hci_cmd_remove_cig(buf, rsp); + break; case BT_HCI_OP_LE_ACCEPT_CIS: - return hci_cmd_accept_cis_req(buf, rsp); + rc = hci_cmd_accept_cis_req(buf, rsp); + break; case BT_HCI_OP_LE_REJECT_CIS: - return hci_cmd_reject_cis_req(buf, rsp); + rc = hci_cmd_reject_cis_req(buf, rsp); + break; case BT_HCI_OP_LE_CREATE_BIG: - return hci_cmd_create_big(buf, rsp); + rc = hci_cmd_create_big(buf, rsp); + break; case BT_HCI_OP_LE_CREATE_BIG_TEST: - return hci_cmd_create_big_test(buf, rsp); + rc = hci_cmd_create_big_test(buf, rsp); + break; case BT_HCI_OP_LE_TERMINATE_BIG: - return hci_cmd_terminate_big(buf, rsp); + rc = hci_cmd_terminate_big(buf, rsp); + break; case BT_HCI_OP_LE_BIG_CREATE_SYNC: - return hci_cmd_big_create_sync(buf, rsp); + rc = hci_cmd_big_create_sync(buf, rsp); + break; case BT_HCI_OP_LE_BIG_TERMINATE_SYNC: - return hci_cmd_big_terminate_sync(buf, rsp); + rc = hci_cmd_big_terminate_sync(buf, rsp); + break; case BT_HCI_OP_LE_SETUP_ISO_PATH: - return hci_cmd_setup_iso_data_path(buf, rsp); + rc = hci_cmd_setup_iso_data_path(buf, rsp); + break; case BT_HCI_OP_LE_REMOVE_ISO_PATH: - return hci_cmd_remove_iso_data_path(buf, rsp); + rc = hci_cmd_remove_iso_data_path(buf, rsp); + break; default: LOG_ERR("[N]IsoCmdNotSupp[%04x]", opcode); net_buf_unref(buf); return -ENOTSUP; } + + return nimble_err_to_errno(rc); } static void iso_evt_rx(uint8_t event, const void *data, diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/l2cap.c b/components/bt/esp_ble_audio/host/adapter/nimble/l2cap.c index 2f6e49c9ef1..c4d48396e48 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/l2cap.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/l2cap.c @@ -23,6 +23,8 @@ #include "common/host.h" +#include "nimble/hs_error.h" + _Static_assert(CONFIG_BT_NIMBLE_L2CAP_COC_MAX_NUM && "At least one L2CAP coc shall be supported"); #define OTS_L2CAP_BUF_COUNT (3 * CONFIG_BT_NIMBLE_L2CAP_COC_MAX_NUM) @@ -206,7 +208,7 @@ int bt_le_nimble_l2cap_chan_connect(uint16_t conn_handle) LOG_ERR("[N]L2capConnectFail[%d]", rc); os_mbuf_free_chain(sdu_rx); - return rc; + return nimble_err_to_errno(rc); } return 0; @@ -230,7 +232,7 @@ int bt_le_nimble_l2cap_chan_disconnect(struct bt_l2cap_chan *chan) rc = ble_l2cap_disconnect(ots_chan); if (rc) { LOG_ERR("[N]L2capDisconnectFail[%d]", rc); - return rc; + return nimble_err_to_errno(rc); } return 0; @@ -268,16 +270,20 @@ int bt_le_nimble_l2cap_chan_send(struct bt_l2cap_chan *chan, struct net_buf *buf rc = ble_l2cap_send(ots_chan, sdu_tx); if (rc) { if (rc == BLE_HS_ESTALLED) { - /* TODO: - * Wait for the BLE_L2CAP_EVENT_COC_TX_UNSTALLED event and continue. + /* sdu is queued in tx->sdus[0]; NimBLE will continue on + * BLE_L2CAP_EVENT_COC_TX_UNSTALLED. Do NOT free here. */ LOG_WRN("[N]MoreCreditsForL2capSend"); + } else if (rc == BLE_HS_EBADDATA || rc == BLE_HS_EBUSY) { + /* sdu was rejected before being queued; caller still owns it. */ + LOG_ERR("[N]L2capSendFail[%d]", rc); + os_mbuf_free_chain(sdu_tx); } else { + /* Internal error inside continue_tx; NimBLE already freed sdu. */ LOG_ERR("[N]L2capSendFail[%d]", rc); } - os_mbuf_free_chain(sdu_tx); - return rc; + return nimble_err_to_errno(rc); } return 0; diff --git a/components/bt/esp_ble_audio/host/common/conn.c b/components/bt/esp_ble_audio/host/common/conn.c index 662c3e692ee..e1d6a64ae2f 100644 --- a/components/bt/esp_ble_audio/host/common/conn.c +++ b/components/bt/esp_ble_audio/host/common/conn.c @@ -275,7 +275,7 @@ struct bt_conn *bt_conn_new(struct bt_conn *conns, size_t size) for (size_t i = 0; i < size; i++) { struct bt_conn *conn = &conns[i]; - if (conn->type == 0) { + if (conn->type == BT_CONN_TYPE_NONE) { (void)memset(conn, 0, sizeof(struct bt_conn)); return conn; } @@ -460,12 +460,6 @@ static void iso_conn_set_state(struct bt_conn *conn, bt_conn_state_t state) case BT_CONN_INITIATING: break; case BT_CONN_DISCONNECT_COMPLETE: - conn->type = 0; - /* Note: - * The bt_iso_disconnected is not called here, the ISO - * disconnected callback will be called by the ISO layer. - */ - /* bt_iso_disconnected(conn); */ break; default: break; diff --git a/components/bt/esp_ble_audio/host/common/gatt.c b/components/bt/esp_ble_audio/host/common/gatt.c index e04d8860119..4dc2e9ac344 100644 --- a/components/bt/esp_ble_audio/host/common/gatt.c +++ b/components/bt/esp_ble_audio/host/common/gatt.c @@ -279,7 +279,15 @@ static void foreach_attr_type_dyndb(uint16_t start_handle, SYS_SLIST_FOR_EACH_CONTAINER(&gatt_db, svc, node) { #if 0 - /* Note: this check is not suitable for our Hosts */ + /* Upstream Zephyr's "skip current service if next->attrs[0].handle + * <= start_handle" optimization assumes gatt_db is sorted by + * handle ascending. bt_gatt_service_register() here uses + * sys_slist_append() (registration order, not handle order), and + * NimBLE assigns handles internally, so that invariant doesn't + * hold -- this optimization would skip services that should be + * searched. Re-enable only after switching service registration + * to insert by handle. + */ struct bt_gatt_service *next; next = SYS_SLIST_PEEK_NEXT_CONTAINER(svc, node); @@ -1029,6 +1037,35 @@ static struct gattc_sub *gattc_sub_add(struct bt_conn *conn) return sub; } +static void gattc_sub_clear(struct bt_conn *conn) +{ + struct bt_gatt_subscribe_params *tmp = NULL; + struct gattc_sub *sub = NULL; + + sub = gattc_sub_find(conn); + if (sub == NULL) { + return; + } + + LOG_DBG("GattcSubClear[%s]", bt_addr_le_str(&sub->peer)); + + /* Reset each entry's handles and NOTIFY/INDICATE value so any state read + * from the params struct after disconnect reflects "not subscribed / no + * valid handles", then drop the nodes from the list. Dropping the nodes + * is what actually unblocks resubscription: bt_gatt_subscribe() rejects + * a params pointer that's still linked (tmp == params → -EALREADY), so + * without the list reset the CCCD write would never be reissued to the + * peer. + */ + SYS_SLIST_FOR_EACH_CONTAINER(&sub->list, tmp, node) { + tmp->value_handle = 0; + tmp->ccc_handle = 0; + tmp->end_handle = 0; + tmp->value = 0; + } + sys_slist_init(&sub->list); +} + _IDF_ONLY struct gattc_sub *bt_gattc_sub_find(struct bt_conn *conn) { @@ -1109,34 +1146,33 @@ int bt_gatt_subscribe(struct bt_conn *conn, struct bt_gatt_subscribe_params *par assert(params->ccc_handle == BT_GATT_AUTO_DISCOVER_CCC_HANDLE || (params->end_handle && params->disc_params)); - LOG_DBG("GattcSub[%04x][%u][%u][%u]", - params->value, params->value_handle, params->ccc_handle, params->end_handle); + LOG_DBG("GattcSub[%u][%u][%u][%04x]", + params->value_handle, params->ccc_handle, params->end_handle, params->value); + /* Failures that return before sys_slist_append() reset + * params->value_handle, mirroring gattc_sub_clear() on + * disconnect, so callers gating retries on `value_handle == 0` + * can resubscribe. + */ if (conn->state != BT_CONN_CONNECTED) { LOG_ERR("NotConn[%d]", __LINE__); + params->value_handle = 0; /* unlinked: clear retry guard */ return -ENOTCONN; } sub = gattc_sub_add(conn); if (sub == NULL) { + params->value_handle = 0; /* unlinked: clear retry guard */ return -ENOMEM; } -#if 0 - if (params->disc_params && - params->disc_params->func == gattc_ccc_discover_cb) { - /* Already in progress */ - LOG_ERR("GattcSubAlreadyInProgress[%u][%u][%u]", - params->ccc_handle, params->value_handle, params->end_handle); - return -EBUSY; - } -#endif - /* Lookup existing subscriptions */ SYS_SLIST_FOR_EACH_CONTAINER(&sub->list, tmp, node) { /* Fail if entry already exists */ if (tmp == params) { - LOG_WRN("GattcSubExist[%u][%04x]", params->ccc_handle, params->value); + LOG_WRN("GattcSubExist[%u][%u][%04x]", + params->value_handle, params->ccc_handle, params->value); + /* live on sub->list: keep value_handle intact */ return -EALREADY; } @@ -1150,12 +1186,16 @@ int bt_gatt_subscribe(struct bt_conn *conn, struct bt_gatt_subscribe_params *par } } + LOG_INF("GattcHasSub[%u][%u][%u]", + params->value_handle, params->ccc_handle, has_subscription); + /* Skip write if already subscribed */ if (has_subscription == false) { if (params->ccc_handle == BT_GATT_AUTO_DISCOVER_CCC_HANDLE) { int err = gattc_ccc_discover(conn, params); if (err) { LOG_ERR("DiscCccFail[%d]", err); + params->value_handle = 0; /* unlinked: clear retry guard */ return err; } } else { @@ -1238,7 +1278,13 @@ int bt_gatt_unsubscribe(struct bt_conn *conn, struct bt_gatt_subscribe_params *p err = gattc_write_ccc(conn, params); if (err) { - LOG_ERR("WrCccFail[%d]", err); + /* -ENOTCONN is expected on the disconnect race: upper layer saw the + * conn as still connected but the peer was already gone by the time + * the write was issued. Not a real failure. + */ + if (err != -ENOTCONN) { + LOG_ERR("WrCccFail[%d]", err); + } return err; } } @@ -1345,17 +1391,35 @@ int bt_gatt_write_without_response_cb(struct bt_conn *conn, uint16_t handle, _LIB_ONLY void bt_le_acl_conn_disconnected_gatt_listener(uint16_t conn_handle) { + struct bt_conn *conn; + LOG_DBG("AclConnDisconnectedGattListener[%u]", conn_handle); bt_le_nimble_gatt_nrp_clear(conn_handle); - /* Note: - * If the connection is disconnected, currently we will not delete - * the gatt client database. - * Hence after the connection is re-established, there is no need - * to re-discover the gatt client database which can save time. - * And the unsubscribe -> re-subscribe procedure is not needed. + /* Drop the cached GATT client database so the next connection runs a + * fresh discovery. This keeps the client resilient when the peer's + * bonding is lost (e.g., server reboot without persisted bonds) or its + * GATT structure changes across reconnects. + * + * TODO: preserve the cached DB across reconnects to the same bonded + * peer, and invalidate it only on a Service Changed indication. That + * saves the rediscovery round-trip but requires honoring CCCD caching + * and Service Changed handling end-to-end. */ + bt_le_nimble_gattc_db_remove(conn_handle); + + /* The cached DB is dropped above, so the app will rediscover and call + * bt_gatt_subscribe() again on reconnect with the BAP library's reused + * `bt_gatt_subscribe_params` pointers. Drop those nodes from the + * tracking list (and zero their value) so the duplicate check in + * bt_gatt_subscribe() doesn't short-circuit with -EALREADY, which + * would otherwise skip the CCCD write and starve notifications. + */ + conn = bt_le_acl_conn_find(conn_handle); + if (conn) { + gattc_sub_clear(conn); + } } void bt_le_gatt_handle_event(uint8_t *data, size_t data_len) diff --git a/components/bt/esp_ble_audio/host/common/adv.h b/components/bt/esp_ble_audio/host/common/include/common/adv.h similarity index 100% rename from components/bt/esp_ble_audio/host/common/adv.h rename to components/bt/esp_ble_audio/host/common/include/common/adv.h diff --git a/components/bt/esp_ble_audio/host/common/app/gap.h b/components/bt/esp_ble_audio/host/common/include/common/app/gap.h similarity index 100% rename from components/bt/esp_ble_audio/host/common/app/gap.h rename to components/bt/esp_ble_audio/host/common/include/common/app/gap.h diff --git a/components/bt/esp_ble_audio/host/common/app/gatt.h b/components/bt/esp_ble_audio/host/common/include/common/app/gatt.h similarity index 98% rename from components/bt/esp_ble_audio/host/common/app/gatt.h rename to components/bt/esp_ble_audio/host/common/include/common/app/gatt.h index 91350b9a4e8..22c2271a83c 100644 --- a/components/bt/esp_ble_audio/host/common/app/gatt.h +++ b/components/bt/esp_ble_audio/host/common/include/common/app/gatt.h @@ -11,7 +11,7 @@ #include "sdkconfig.h" -#include "nimble/gatt/gatt.h" +#include "nimble/gatt.h" #ifdef __cplusplus extern "C" { diff --git a/components/bt/esp_ble_audio/host/common/conn.h b/components/bt/esp_ble_audio/host/common/include/common/conn.h similarity index 100% rename from components/bt/esp_ble_audio/host/common/conn.h rename to components/bt/esp_ble_audio/host/common/include/common/conn.h diff --git a/components/bt/esp_ble_audio/host/common/gatt.h b/components/bt/esp_ble_audio/host/common/include/common/gatt.h similarity index 99% rename from components/bt/esp_ble_audio/host/common/gatt.h rename to components/bt/esp_ble_audio/host/common/include/common/gatt.h index 289f44d8017..55ac31b74c5 100644 --- a/components/bt/esp_ble_audio/host/common/gatt.h +++ b/components/bt/esp_ble_audio/host/common/include/common/gatt.h @@ -14,7 +14,7 @@ #include #include -#include "nimble/gatt/gatt.h" +#include "nimble/gatt.h" #ifdef __cplusplus extern "C" { diff --git a/components/bt/esp_ble_audio/host/common/host.h b/components/bt/esp_ble_audio/host/common/include/common/host.h similarity index 100% rename from components/bt/esp_ble_audio/host/common/host.h rename to components/bt/esp_ble_audio/host/common/include/common/host.h diff --git a/components/bt/esp_ble_audio/host/common/init.h b/components/bt/esp_ble_audio/host/common/include/common/init.h similarity index 100% rename from components/bt/esp_ble_audio/host/common/init.h rename to components/bt/esp_ble_audio/host/common/include/common/init.h diff --git a/components/bt/esp_ble_audio/host/common/iso.h b/components/bt/esp_ble_audio/host/common/include/common/iso.h similarity index 100% rename from components/bt/esp_ble_audio/host/common/iso.h rename to components/bt/esp_ble_audio/host/common/include/common/iso.h diff --git a/components/bt/esp_ble_audio/host/common/l2cap.h b/components/bt/esp_ble_audio/host/common/include/common/l2cap.h similarity index 100% rename from components/bt/esp_ble_audio/host/common/l2cap.h rename to components/bt/esp_ble_audio/host/common/include/common/l2cap.h diff --git a/components/bt/esp_ble_audio/host/common/scan.h b/components/bt/esp_ble_audio/host/common/include/common/scan.h similarity index 100% rename from components/bt/esp_ble_audio/host/common/scan.h rename to components/bt/esp_ble_audio/host/common/include/common/scan.h diff --git a/components/bt/esp_ble_audio/host/common/task.h b/components/bt/esp_ble_audio/host/common/include/common/task.h similarity index 100% rename from components/bt/esp_ble_audio/host/common/task.h rename to components/bt/esp_ble_audio/host/common/include/common/task.h diff --git a/components/bt/esp_ble_audio/host/common/init.c b/components/bt/esp_ble_audio/host/common/init.c index c2e550e4bbe..e4ee6f4b1eb 100644 --- a/components/bt/esp_ble_audio/host/common/init.c +++ b/components/bt/esp_ble_audio/host/common/init.c @@ -170,7 +170,7 @@ static const uint16_t ext_structs[] = { sizeof(struct bt_bond_info), }; -#define LEA_VERSION (0x20260412) +#define LEA_VERSION (0x20260430) struct lib_ext_cfgs { /* BLE */ @@ -2049,10 +2049,6 @@ int bt_le_audio_init(void) return err; } -#if 0 - lib_dymem_size_init(); -#endif - printf(BT_ISO_LOG_COLOR_I "BLE Audio lib commit: [%s]" \ BT_ISO_LOG_RESET_COLOR "\n", lib_ext_commit_get()); diff --git a/components/bt/esp_ble_audio/host/iso/iso.c b/components/bt/esp_ble_audio/host/iso/iso.c index 256fce4344f..4aaad599cfc 100644 --- a/components/bt/esp_ble_audio/host/iso/iso.c +++ b/components/bt/esp_ble_audio/host/iso/iso.c @@ -454,6 +454,8 @@ static void bt_iso_chan_disconnected(struct bt_iso_chan *chan, uint8_t reason) bt_iso_cleanup_acl(chan->iso); if (conn_type == BT_ISO_CHAN_TYPE_PERIPHERAL) { + /* Release iso conn slot back to the pool. */ + chan->iso->type = BT_CONN_TYPE_NONE; bt_conn_unref(chan->iso); chan->iso = NULL; #if defined(CONFIG_BT_ISO_CENTRAL) @@ -1745,6 +1747,8 @@ static void cleanup_cig(struct bt_iso_cig *cig) SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&cig->cis_channels, cis, tmp, node) { if (cis->iso != NULL) { + /* Release iso conn slot back to the pool. */ + cis->iso->type = BT_CONN_TYPE_NONE; bt_conn_unref(cis->iso); cis->iso = NULL; } @@ -2403,6 +2407,8 @@ static void cleanup_big(struct bt_iso_big *big) SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&big->bis_channels, bis, tmp, node) { if (bis->iso != NULL) { + /* Release iso conn slot back to the pool. */ + bis->iso->type = BT_CONN_TYPE_NONE; bt_conn_unref(bis->iso); bis->iso = NULL; } diff --git a/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/media_proxy.h b/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/media_proxy.h index 8626cfddda6..b841788a240 100644 --- a/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/media_proxy.h +++ b/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/media_proxy.h @@ -1261,7 +1261,7 @@ int bt_media_proxy_ctrl_get_search_results_id_safe(struct media_player *player); * * @return 0 if success, errno on failure. */ -uint8_t bt_media_proxy_ctrl_get_content_ctrl_id_safe(struct media_player *player); +int bt_media_proxy_ctrl_get_content_ctrl_id_safe(struct media_player *player); /** * @brief Available calls in a player, that the media proxy can call diff --git a/components/bt/esp_ble_audio/include/zephyr/bluetooth/conn.h b/components/bt/esp_ble_audio/include/zephyr/bluetooth/conn.h index b1415e90296..594b25509a7 100644 --- a/components/bt/esp_ble_audio/include/zephyr/bluetooth/conn.h +++ b/components/bt/esp_ble_audio/include/zephyr/bluetooth/conn.h @@ -35,6 +35,8 @@ struct bt_conn; /** Connection Type */ enum __packed bt_conn_type { + /** No Connection Type (free slot marker) */ + BT_CONN_TYPE_NONE = 0, /** LE Connection Type */ BT_CONN_TYPE_LE = BIT(0), /** ISO Connection Type */ diff --git a/components/bt/esp_ble_audio/lib/include/audio.h b/components/bt/esp_ble_audio/lib/include/audio.h index 74400982d72..091a7ad75fe 100644 --- a/components/bt/esp_ble_audio/lib/include/audio.h +++ b/components/bt/esp_ble_audio/lib/include/audio.h @@ -24,9 +24,6 @@ extern int lib_ext_structs_check(const uint16_t *ext_structs, size_t size_struct extern int lib_ext_cfgs_set(const struct lib_ext_cfgs *ext_cfgs, size_t size_cfgs); extern int lib_ext_funcs_set(const struct lib_ext_funcs *ext_funcs, size_t size_funcs); extern int lib_funcs_set(const struct lib_funcs *funcs, size_t size_funcs); -#if 0 -extern void lib_dymem_size_init(void); -#endif extern const char *lib_ext_commit_get(void); struct bt_aics; diff --git a/components/bt/esp_ble_audio/lib/lib b/components/bt/esp_ble_audio/lib/lib index 4d80f4ef905..ad9b161851a 160000 --- a/components/bt/esp_ble_audio/lib/lib +++ b/components/bt/esp_ble_audio/lib/lib @@ -1 +1 @@ -Subproject commit 4d80f4ef905e451af52d7d6401edc7f311a4c85b +Subproject commit ad9b161851a008f6cf60b3b693fb206883a5bc01 diff --git a/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/README.md b/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/README.md index 98ba194367e..a2580a5806b 100644 --- a/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/README.md +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/README.md @@ -5,68 +5,91 @@ (See the README.md file in the upper level `examples` directory for more information about examples.) -This example demonstrates the **Basic Audio Profile (BAP) Broadcast Sink** functionality. It scans for BAP Broadcast Sources (advertising with the Broadcast Audio service data), establishes periodic advertising synchronization with the first matching source, creates a BAP Broadcast Sink, and synchronizes to the BIG to receive broadcast audio streams. It listens until the source stops or sync is lost. The scan filters by the hardcoded source name (`BAP Broadcast Source`). Optionally, you can run in a mode where a Broadcast Assistant (e.g. a phone app) controls when to sync via the Scan Delegator. +## Overview -The implementation uses the NimBLE host stack with ISO and BAP support, ESP-BLE-ISO, and ESP-BLE-AUDIO APIs (PACS, BAP broadcast sink, scan delegator). It is intended for chips that support BLE 5.2 ISO and LE Audio (e.g. ESP32-H4). Sink capabilities are registered with LC3 (16 kHz and 24 kHz, 10 ms frame duration, 1 channel). For encrypted broadcasts, the broadcast code is hardcoded as `1234` in the source, or can be provided by a Broadcast Assistant. +This example acts as a **BAP Broadcast Sink**. It scans for extended advertisements that carry the Broadcast Audio Announcement Service UUID and whose complete-name AD matches the hard-coded `"BAP Broadcast Source"` string; on a hit, it creates a periodic-advertising sync, builds a BAP broadcast sink for that PA handle and broadcast ID, decodes the BASE / BIGInfo from the PA channel, and then synchronizes to the BIG to receive BIS streams. + +The build runs on top of the NimBLE host stack and the ESP BLE Audio component set. Sink-side APIs used: `esp_ble_audio_common_init` / `_start`, `esp_ble_audio_pacs_register` + `esp_ble_audio_pacs_cap_register` (sink PAC and sink location enabled), `esp_ble_audio_bap_scan_delegator_register` (so a Broadcast Assistant can drive PA-sync, broadcast-code, and BIS-sync requests via BASS), `esp_ble_audio_bap_broadcast_sink_register_cb`, `esp_ble_audio_bap_broadcast_sink_create` / `_sync` / `_stop` / `_delete`, and `esp_ble_audio_bap_base_get_subgroup_count` / `_get_bis_indexes`. PAC capabilities are LC3 with sample rates 16 kHz + 24 kHz, frame duration 10 ms, 1 channel, 40–60 octets/frame, 1 frame/SDU. The fallback broadcast code is `"1234"`; if a Broadcast Assistant has supplied one through BASS it is used instead. + +After PA sync is established, `ble_gap_disc_cancel()` stops the extended scanner — BASE/BIGInfo arrive over the PA channel — and `pa_sync_lost()` re-arms the scanner. ## Requirements -* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4) -* A BAP Broadcast Source (e.g. another device or sample running as broadcast source) that is advertising and sending broadcast audio +* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31) +* A peer running the [broadcast_source](../broadcast_source) example (or another BAP Broadcast Source advertising the matching name) -## How to Use Example +## Configuration -Before project configuration and build, set the correct chip target: - -```bash -idf.py set-target esp32h4 -``` - -### Configure the Project - -Open the project configuration menu: +Open menuconfig: ```bash idf.py menuconfig ``` -In the **Example: Broadcast Sink** menu: +No build-time options — runtime defaults are baked into source. The example always runs both: it scans for `"BAP Broadcast Source"` directly and registers a BASS scan delegator so a Broadcast Assistant could also drive sync (PAST is rejected — see `pa_sync_req_cb`). -* **Whether to wait for a Broadcast Assistant**: If enabled, the device advertises connectable and waits for a Broadcast Assistant to connect and send PA sync, broadcast code, and BIS sync requests via the Scan Delegator; the sink then syncs according to those requests. +### Security & Pairing -### Build and Flash +The example inherits a Just-Works pairing model (LE Secure Connections, no MITM, no I/O capability) with bonding enabled from the shared init at `../common_components/example_init/ble_audio_example_init.c`; change pairing/IO-cap there if needed. -Run the following to build, flash and monitor: +## Build & Flash ```bash +idf.py set-target esp32h4 # or esp32s31 idf.py -p PORT flash monitor ``` -(To exit the serial monitor, type ``Ctrl-]``.) - -See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF. +(Exit serial monitor with `Ctrl-]`.) ## Example Flow -1. **Initialization**: NVS, Bluetooth stack, and LE Audio common layer (`esp_ble_audio_common_init`). Register PACS with sink capabilities (LC3), register BAP scan delegator callbacks and broadcast sink callbacks, then start the audio stack. Set device name if needed. -2. **Scanning**: Start extended scanning. On scan report, look for Broadcast Audio service data (UUID + broadcast ID). Filter by the hardcoded target device name (`BAP Broadcast Source`). When a matching advertiser with periodic advertising is found and not already syncing, create periodic advertising synchronization. -3. **PA sync**: When periodic sync is established, create a BAP Broadcast Sink (`esp_ble_audio_bap_broadcast_sink_create`) for that sync handle and broadcast ID. -4. **BASE and syncable**: When the BASE is received, the sink gets subgroup count and BIS index bitfield. When BIGInfo indicates the sink is syncable, call `esp_ble_audio_bap_broadcast_sink_sync` with the chosen BIS bitfield and broadcast code (from menuconfig or from the scan delegator). Streams are bound to the registered stream array. -5. **Streams**: As each BIS stream starts, the stream started callback runs; when all requested streams have started, the sink is considered running. Incoming audio data is delivered in the stream receive callback; the example counts valid, error, and lost packets per stream and logs periodically. When streams stop or PA sync is lost, the sink is cleaned up. +1. `app_main` initializes NVS, calls `bluetooth_init()`, and calls `esp_ble_audio_common_init(&info)` with `info.gap_cb = iso_gap_app_cb`. +2. PACS is registered (`snk_pac` + `snk_loc`), each stream's `ops` field is wired to `stream_ops`, and the LC3 sink capability is registered via `esp_ble_audio_pacs_cap_register(ESP_BLE_AUDIO_DIR_SINK, ...)`. +3. The scan delegator (`scan_delegator_cbs`: `recv_state_updated`, `pa_sync_req`, `pa_sync_term_req`, `broadcast_code`, `bis_sync_req`) and broadcast-sink callbacks (`base_recv`, `syncable`) are registered, then `esp_ble_audio_common_start(NULL)` runs. +4. `ext_scan_start()` runs passive extended discovery (`itvl=window=160`). For each `ESP_BLE_AUDIO_GAP_EVENT_EXT_SCAN_RECV`, `data_cb` matches the complete/short/broadcast name AD type against `"BAP Broadcast Source"` and records the Broadcast ID from the Broadcast Audio Service Data. +5. On match (and only when not already PA-syncing and no scan-delegator state is pinned), `pa_sync_create()` calls `ble_gap_periodic_adv_sync_create` with `skip=0`, `sync_timeout=10s`. +6. `ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC` clears `pa_syncing`, cancels discovery, stores `sync_handle`, and calls `esp_ble_audio_bap_broadcast_sink_create()`. +7. `base_recv_cb` extracts the subgroup count and BIS index bitfield (masked by `bis_index_mask`); when no Broadcast Assistant is connected, `requested_bis_sync` defaults to `ESP_BLE_AUDIO_BAP_BIS_SYNC_NO_PREF`. +8. `syncable_cb` AND-masks the BASE bitfield with the requested mask, copies `TARGET_BROADCAST_CODE` if the BIG is encrypted (unless BASS already supplied one), and calls `esp_ble_audio_bap_broadcast_sink_sync()` with the chosen mask and `streams_p`. +9. `stream_started_cb` resets per-stream RX metrics and increments `stream_count_started`; `stream_recv_cb` updates metrics via `example_audio_rx_metrics_on_recv()`. When all streams have stopped, `stream_stopped_cb` deletes the broadcast sink. `pa_sync_lost()` clears the cached `req_recv_state`, deletes any sink, and restarts the scanner. -## Example Output +## Expected Log ``` +I (xxx) BAP_BSNK: Scanning for broadcast source... I (xxx) BAP_BSNK: Broadcast source PA synced, creating Broadcast Sink -I (xxx) BAP_BSNK: Received BASE with 1 subgroups from broadcast sink 0x... -I (xxx) BAP_BSNK: Broadcast sink (0x...) is syncable, BIG not encrypted -I (xxx) BAP_BSNK: Stream 0x... started (1/1) -I (xxx) BAP_BSNK: Received 1000(1000/0/0) ISO data packets (stream 0x...) -... +I (xxx) BAP_BSNK: BASE received (... subgroup(s)) +I (xxx) BAP_BSNK: bis_index_bitfield = 0x... +I (xxx) BAP_BSNK: Broadcast sink syncable, BIG encrypted +I (xxx) BAP_BSNK: Syncing to broadcast: BIS mask 0x... (... stream(s)) +I (xxx) BAP_BSNK: [SNK #0] Stream started (.../...) ``` -If PA sync is lost: +Per-stream RX metrics are then emitted under the `BAP_BSNK` tag, name `SNK #`. If a Broadcast Assistant connects via BASS, additional log lines appear: ``` -I (xxx) BAP_BSNK: PA sync lost, reason ... +I (xxx) BAP_BSNK: Receive state updated, pa_sync 0x... encrypt 0x... +I (xxx) BAP_BSNK: Received request to sync to PA (PAST {not }available): ... +I (xxx) BAP_BSNK: Broadcast code received +I (xxx) BAP_BSNK: BIS sync req: broadcast_id 0x... BIS mask 0x... subgroup mask 0x... (...) +I (xxx) BAP_BSNK: Received request to terminate PA sync ``` + +On teardown / sync loss: + +``` +I (xxx) BAP_BSNK: [SNK #0] Stream stopped, reason 0x... (.../...) +I (xxx) BAP_BSNK: PA sync lost: sync_handle ... reason 0x... +I (xxx) BAP_BSNK: PA sync terminated +``` + +## Peer Pairing + +Run [broadcast_source](../broadcast_source/) on a second board. Expected interaction sequence: + +1. Source advertises extended PDU containing the Broadcast Audio Announcement Service UUID, Broadcast ID `0x123456`, and complete name `"BAP Broadcast Source"`; sink scans and matches by name + UUID. +2. Source's periodic advertising carries the encoded BASE; sink establishes PA sync and parses the BASE to recover subgroup count and BIS index bitfield. +3. Source's BIGInfo advertises the BIG as encrypted (broadcast code `"1234"`); sink reports `BIG encrypted`. +4. Source starts the BIG and the two BIS streams (`FRONT_LEFT`, `FRONT_RIGHT`); sink calls `esp_ble_audio_bap_broadcast_sink_sync()` with the chosen BIS bitfield and the matching broadcast code. +5. Source's TX scheduler keeps pushing SDUs at `preset_active.qos.interval`; sink stream `recv` callbacks deliver the data and update RX metrics. +6. Stopping the source (or losing PA sync) triggers `stream_stopped_cb` on the sink, which deletes the broadcast sink and resumes scanning. diff --git a/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/Kconfig.projbuild b/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/Kconfig.projbuild index 2b4dbdac845..64a59cf0079 100644 --- a/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/Kconfig.projbuild +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/Kconfig.projbuild @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: 2022 Nordic Semiconductor ASA -# SPDX-FileContributor: 2025 Espressif Systems (Shanghai) CO LTD +# SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 menu "Example: Broadcast Sink" diff --git a/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/main.c b/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/main.c index fe4189df435..e8d76f2d332 100644 --- a/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/main.c +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/main.c @@ -113,7 +113,7 @@ static void ext_scan_start(void) return; } - ESP_LOGI(TAG, "Extended scan started"); + ESP_LOGI(TAG, "Scanning for broadcast source..."); } static int pa_sync_create(const bt_addr_le_t *addr, uint8_t adv_sid) @@ -213,7 +213,7 @@ 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 for %p", recv_state); + ESP_LOGI(TAG, "Broadcast code received"); req_recv_state = recv_state; @@ -253,10 +253,10 @@ static int bis_sync_req_cb(esp_ble_conn_t *conn, } } - ESP_LOGI(TAG, "BIS sync req for %p: BIS indexes 0x%08x (subgroup indexes 0x%08x), " - "broadcast id: 0x%06x, (%s)", recv_state, requested_bis_sync, - requested_subgroup_sync, recv_state->broadcast_id, - stream_started ? "Stream started" : "Stream not started"); + ESP_LOGI(TAG, "BIS sync req: broadcast_id 0x%06x BIS mask 0x%08x subgroup mask 0x%08x (%s)", + recv_state->broadcast_id, requested_bis_sync, + requested_subgroup_sync, + stream_started ? "streaming" : "not streaming"); if (stream_started && requested_bis_sync == 0) { /* The stream stopped callback will be called as part of this, and @@ -307,8 +307,7 @@ static void base_recv_cb(esp_ble_audio_bap_broadcast_sink_t *sink, return; } - ESP_LOGI(TAG, "Received BASE with %d subgroups from broadcast sink %p", - base_subgroup_count, sink); + ESP_LOGI(TAG, "BASE received (%d subgroup(s))", base_subgroup_count); err = esp_ble_audio_bap_base_get_bis_indexes(base, &base_bis_index_bitfield); if (err) { @@ -334,8 +333,8 @@ static void syncable_cb(esp_ble_audio_bap_broadcast_sink_t *sink, uint32_t sync_bitfield; esp_err_t err; - ESP_LOGI(TAG, "Broadcast sink (%p) is syncable, BIG %s", - sink, biginfo->encryption ? "encrypted" : "not encrypted"); + ESP_LOGI(TAG, "Broadcast sink syncable, BIG %s", + biginfo->encryption ? "encrypted" : "not encrypted"); sync_bitfield = (bis_index_bitfield & requested_bis_sync); if (sync_bitfield == 0) { @@ -353,9 +352,8 @@ static void syncable_cb(esp_ble_audio_bap_broadcast_sink_t *sink, } } - ESP_LOGI(TAG, "Syncing to broadcast with bitfield:"); - ESP_LOGI(TAG, "0x%08x = 0x%08x (bis_index) & 0x%08x (req_bis_sync), stream_count %u", - sync_bitfield, bis_index_bitfield, requested_bis_sync, stream_count); + ESP_LOGI(TAG, "Syncing to broadcast: BIS mask 0x%08x (%u stream(s))", + sync_bitfield, stream_count); if (biginfo->encryption) { memset(sink_broadcast_code, 0, ESP_BLE_ISO_BROADCAST_CODE_SIZE); @@ -376,13 +374,24 @@ static esp_ble_audio_bap_broadcast_sink_cb_t broadcast_sink_cbs = { .syncable = syncable_cb, }; +static int stream_index(const esp_ble_audio_bap_stream_t *stream) +{ + for (size_t i = 0; i < ARRAY_SIZE(streams); i++) { + if (&streams[i].stream == stream) { + return (int)i; + } + } + return -1; +} + static void stream_started_cb(esp_ble_audio_bap_stream_t *stream) { struct broadcast_sink_stream *sink_stream = CONTAINER_OF(stream, struct broadcast_sink_stream, stream); - ESP_LOGI(TAG, "Stream %p started (%u/%u)", stream, stream_count_started, stream_count); + ESP_LOGI(TAG, "[SNK #%d] Stream started (%u/%u)", + stream_index(stream), stream_count_started, stream_count); example_audio_rx_metrics_reset(&sink_stream->rx_metrics); @@ -395,8 +404,8 @@ static void stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason { esp_err_t err; - ESP_LOGI(TAG, "Stream %p stopped with reason 0x%02x (%u/%u)", - stream, reason, stream_count_stopped, stream_count); + ESP_LOGI(TAG, "[SNK #%d] Stream stopped, reason 0x%02x (%u/%u)", + stream_index(stream), reason, stream_count_stopped, stream_count); if (++stream_count_stopped == stream_count) { stream_started = false; @@ -418,10 +427,11 @@ static void stream_recv_cb(esp_ble_audio_bap_stream_t *stream, struct broadcast_sink_stream *sink_stream = CONTAINER_OF(stream, struct broadcast_sink_stream, stream); + char name[24]; + snprintf(name, sizeof(name), "SNK #%d", stream_index(stream)); sink_stream->rx_metrics.last_sdu_len = len; - example_audio_rx_metrics_on_recv(info, &sink_stream->rx_metrics, - TAG, "stream", stream); + example_audio_rx_metrics_on_recv(info, &sink_stream->rx_metrics, TAG, name); } static esp_ble_audio_bap_stream_ops_t stream_ops = { @@ -506,6 +516,7 @@ static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event) static void pa_sync(esp_ble_audio_gap_app_event_t *event) { esp_err_t err; + int rc; pa_syncing = false; @@ -518,6 +529,15 @@ static void pa_sync(esp_ble_audio_gap_app_event_t *event) ESP_LOGI(TAG, "Broadcast source PA synced, creating Broadcast Sink"); + /* PA sync is established; the BASE / BIGInfo reports will arrive + * via the PA sync channel, so the extended scanner is no longer + * needed. Stop it now — pa_sync_lost() will restart it on loss. + */ + rc = ble_gap_disc_cancel(); + if (rc) { + ESP_LOGW(TAG, "Failed to stop scanning, err %d", rc); + } + err = esp_ble_audio_bap_broadcast_sink_create(event->pa_sync.sync_handle, broadcaster_broadcast_id, &broadcast_sink); @@ -529,7 +549,7 @@ static void pa_sync(esp_ble_audio_gap_app_event_t *event) static void pa_sync_lost(esp_ble_audio_gap_app_event_t *event) { - ESP_LOGI(TAG, "PA sync lost: sync_handle 0x%04x reason 0x%02x", + ESP_LOGI(TAG, "PA sync lost: sync_handle %u reason 0x%02x", event->pa_sync_lost.sync_handle, event->pa_sync_lost.reason); if (sync_handle == event->pa_sync_lost.sync_handle) { @@ -540,6 +560,12 @@ static void pa_sync_lost(esp_ble_audio_gap_app_event_t *event) stream_count = 0; stream_count_started = 0; stream_count_stopped = 0; + /* Clear the stale scan-delegator state pointer captured by + * recv_state_updated_cb() while we were synced. Without this, + * ext_scan_recv() would keep rejecting new source ads because + * its gate is `pa_syncing == false && req_recv_state == NULL`. + */ + req_recv_state = NULL; if (broadcast_sink != NULL) { esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink); diff --git a/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/sdkconfig.defaults b/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/sdkconfig.defaults index b7ad3b2846a..8a2bff9147b 100644 --- a/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/sdkconfig.defaults @@ -6,6 +6,8 @@ CONFIG_BT_ENABLED=y CONFIG_BT_BLUEDROID_ENABLED=n CONFIG_BT_NIMBLE_ENABLED=y CONFIG_BT_NIMBLE_EXT_ADV=y +CONFIG_BT_NIMBLE_NVS_PERSIST=y +CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 CONFIG_BT_NIMBLE_MAX_CCCDS=20 CONFIG_BT_NIMBLE_ISO=y CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y diff --git a/examples/bluetooth/esp_ble_audio/bap/broadcast_source/README.md b/examples/bluetooth/esp_ble_audio/bap/broadcast_source/README.md index f20a746c808..d840b865d01 100644 --- a/examples/bluetooth/esp_ble_audio/bap/broadcast_source/README.md +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_source/README.md @@ -5,57 +5,77 @@ (See the README.md file in the upper level `examples` directory for more information about examples.) -This example demonstrates the **Basic Audio Profile (BAP) Broadcast Source** functionality. It starts extended advertising with the Broadcast Audio service data (UUID and Broadcast ID) and device name, periodic advertising with the Broadcast Audio Source Endpoint (BASE), then starts the BAP Broadcast Source so that BIGInfo and (mock) audio data are sent over the BIG. Sinks such as the [broadcast_sink](../broadcast_sink) example can discover this source, establish periodic sync, and synchronize to the BIG to receive the streams. +## Overview -The implementation uses the NimBLE host stack with ISO and BAP support, ESP-BLE-ISO, and ESP-BLE-AUDIO APIs (BAP broadcast source create/start, stream send, LC3 presets). It is intended for chips that support BLE 5.2 ISO and LE Audio (e.g. ESP32-H4). The source is configured with the LC3 16_2_1 broadcast preset, a hardcoded broadcast ID (`0x123456`), and a hardcoded broadcast code (`1234`) for encrypted broadcasts. These values can be changed by editing the source code constants. +This example acts as a **BAP Broadcast Source**. It creates a BAP broadcast source from the LC3 `16_2_1` broadcast preset, encodes the BASE into the periodic advertising payload, places the Broadcast Audio Announcement Service UUID and a fixed 24-bit Broadcast ID (`0x123456`) in the extended advertising payload, and then starts the BIG so that BIS streams transmit synthetic SDU data on a fixed cadence. + +The build runs on top of the NimBLE host stack and the ESP BLE Audio component set. Source-side APIs used: `esp_ble_audio_common_init` / `esp_ble_audio_common_start` (common layer), `esp_ble_audio_bap_broadcast_source_create` / `_get_base` / `_start` (BAP), `esp_ble_audio_bap_broadcast_adv_add` (BAP/ISO glue), and the BAP stream callbacks (`started`, `stopped`, `sent`, `disconnected`). PACS, GAP scanner, and the scan delegator are **not** used on this side. Encryption is enabled because the broadcast code `"1234"` is non-empty; packing is `ESP_BLE_ISO_PACKING_SEQUENTIAL`. Channel allocation per stream is hard-coded as `FRONT_LEFT` for stream 0 and `FRONT_RIGHT` for stream 1. + +The TX scheduler is built on `example_audio_tx_scheduler_*` helpers; the source comment notes that ESP timer resolution is not accurate enough for the SDU interval, so a `k_work_delayable`-based scheduler is used instead. ## Requirements -* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4) -* Optionally, a device running the [broadcast_sink](../broadcast_sink) example to receive and play the broadcast streams +* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31) +* A peer running the [broadcast_sink](../broadcast_sink) example to receive and decode the BIS streams -## How to Use Example +## Configuration -Before project configuration and build, set the correct chip target: +Open menuconfig: ```bash -idf.py set-target esp32h4 +idf.py menuconfig ``` -### Build and Flash +No build-time options — runtime defaults are baked into source. The device name (`"BAP Broadcast Source"`), broadcast ID (`0x123456`), broadcast code (`"1234"`), advertising/periodic intervals, stream and subgroup counts (via `CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT` / `_SUBGROUP_COUNT` from the BAP component), and LC3 preset (`16_2_1`) are compile-time constants in `main.c`. -Run the following to build, flash and monitor: +### Security & Pairing + +The example inherits a Just-Works pairing model (LE Secure Connections, no MITM, no I/O capability) with bonding enabled from the shared init at `../common_components/example_init/ble_audio_example_init.c`; change pairing/IO-cap there if needed. + +## Build & Flash ```bash +idf.py set-target esp32h4 # or esp32s31 idf.py -p PORT flash monitor ``` -(To exit the serial monitor, type ``Ctrl-]``.) - -See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF. +(Exit serial monitor with `Ctrl-]`.) ## Example Flow -1. **Initialization**: NVS, Bluetooth stack, and LE Audio common layer (`esp_ble_audio_common_init`). No PACS or GAP callback needed for the source role. -2. **Broadcast source setup**: Register broadcast source callbacks (started/stopped). Configure subgroups and streams using the selected LC3 preset (codec config, QoS, channel allocation). Create the BAP Broadcast Source (`esp_ble_audio_bap_broadcast_source_create`) with optional encryption and sequential packing. Register stream callbacks (started, stopped, sent) for each stream. -3. **Extended and periodic advertising**: Set extended advertising data: Broadcast Audio UUID + Broadcast ID (static or random) + complete device name. Set periodic advertising data: Broadcast Audio UUID + encoded BASE from the broadcast source. Start periodic advertising then extended advertising. -4. **Start BIG**: Add the advertising set for BIG and start the BAP Broadcast Source (`esp_ble_audio_bap_broadcast_source_start`) with the same adv handle. BIGInfo is sent in the periodic advertising; BIS streams are created. -5. **Stream and send**: When each BIS stream starts, the stream started callback allocates an SDU buffer and starts a periodic TX scheduler based on `k_work_delayable`. The scheduler posts work items to the ISO task at the stream QoS interval to send mock audio data; sent and drift are reported in the stream sent callback. When a stream stops, resources are freed and the scheduler is stopped. Note that the scheduler timer resolution is in milliseconds, which may not match the exact SDU interval for all configurations. +1. `app_main` initializes NVS, calls `bluetooth_init()`, and calls `esp_ble_audio_common_init(NULL)` (no GAP callback for this role). +2. `broadcast_source_setup()` registers `source_started` / `source_stopped` callbacks, populates per-stream channel-allocation BIS metadata (left/right), registers `stream_ops` on each stream, and calls `esp_ble_audio_bap_broadcast_source_create()` with the encrypted preset. +3. `esp_ble_audio_common_start(NULL)` starts the audio stack; each stream's `example_audio_tx_scheduler_init()` is wired with `tx_scheduler_cb`. +4. `ext_adv_start()` configures non-connectable extended adv (1M primary / 2M secondary, 200 ms interval), writes Broadcast Audio Service Data + Broadcast ID + complete name, configures periodic adv (100 ms), writes the BASE returned by `esp_ble_audio_bap_broadcast_source_get_base()`, then starts periodic and extended advertising. +5. `broadcast_start()` calls `esp_ble_audio_bap_broadcast_adv_add()` and `esp_ble_audio_bap_broadcast_source_start()` on the same `ADV_HANDLE`, which kicks off BIGInfo + BIS creation. +6. When each BIS stream goes streaming, `stream_started_cb` allocates an SDU-sized buffer and calls `example_audio_tx_scheduler_start()` at `preset_active.qos.interval`; the scheduler invokes `broadcast_source_tx()`, which fills the buffer with the low byte of `seq_num` and calls `esp_ble_audio_bap_stream_send()`. +7. `stream_sent_cb` forwards completions to `example_audio_tx_scheduler_on_sent()` for drift accounting; `stream_stopped_cb` and `stream_disconnected_cb` stop the scheduler; `source_stopped_cb` frees all per-stream buffers. -## Example Output +## Expected Log ``` -I (xxx) BAP_BSRC: Creating broadcast source with 1 subgroups & 2 streams per subgroup -I (xxx) BAP_BSRC: Extended adv instance 0 started -I (xxx) BAP_BSRC: Broadcast source 0x... started -I (xxx) BAP_BSRC: Stream 0x... started -I (xxx) BAP_BSRC: Transmitted 1000 ISO data packets (stream 0x...) -... +I (xxx) BAP_BSRC: Creating broadcast source: ... subgroup(s), ... stream(s)/subgroup +I (xxx) BAP_BSRC: Advertising started (handle 0) +I (xxx) BAP_BSRC: Broadcast source started +I (xxx) BAP_BSRC: [SRC #0] Stream started +I (xxx) BAP_BSRC: [SRC #1] Stream started ``` -If the broadcast source stops (e.g. stream stopped): +Periodic TX accounting (emitted by `example_audio_tx_scheduler_on_sent` under the `BAP_BSRC` tag, name `SRC #`) follows. On teardown: ``` -I (xxx) BAP_BSRC: Stream 0x... stopped, reason 0x... -I (xxx) BAP_BSRC: Broadcast source 0x... stopped, reason 0x... +I (xxx) BAP_BSRC: [SRC #0] Stream stopped, reason 0x... +I (xxx) BAP_BSRC: [SRC #0] ISO disconnected, reason 0x... +I (xxx) BAP_BSRC: Broadcast source stopped, reason 0x... ``` + +## Peer Pairing + +Run [broadcast_sink](../broadcast_sink/) on a second board. Expected interaction sequence: + +1. Source advertises extended PDU containing the Broadcast Audio Announcement Service UUID, Broadcast ID `0x123456`, and complete name `"BAP Broadcast Source"`; sink scans and matches by name + UUID. +2. Source's periodic advertising carries the encoded BASE; sink establishes PA sync and parses the BASE to recover subgroup count and BIS index bitfield. +3. Source's BIGInfo advertises the BIG as encrypted (broadcast code `"1234"`); sink reports `BIG encrypted`. +4. Source starts the BIG and the two BIS streams (`FRONT_LEFT`, `FRONT_RIGHT`); sink calls `esp_ble_audio_bap_broadcast_sink_sync()` with the chosen BIS bitfield and the matching broadcast code. +5. Source's TX scheduler keeps pushing SDUs at `preset_active.qos.interval`; sink stream `recv` callbacks deliver the data and update RX metrics. +6. Stopping the source (or losing PA sync) triggers `stream_stopped_cb` on the sink, which deletes the broadcast sink and resumes scanning. diff --git a/examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/main.c b/examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/main.c index 3b31ad45bc5..f2260ce9821 100644 --- a/examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/main.c +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/main.c @@ -66,24 +66,36 @@ static esp_ble_audio_bap_broadcast_source_t *broadcast_source; static void broadcast_source_tx(struct broadcast_source_stream *source_stream); +static int stream_index(const esp_ble_audio_bap_stream_t *stream) +{ + for (size_t i = 0; i < ARRAY_SIZE(streams); i++) { + if (&streams[i].stream == stream) { + return (int)i; + } + } + return -1; +} + static void stream_started_cb(esp_ble_audio_bap_stream_t *stream) { struct broadcast_source_stream *source_stream = CONTAINER_OF(stream, struct broadcast_source_stream, stream); + int idx = stream_index(stream); esp_err_t err; - ESP_LOGI(TAG, "Stream %p started", stream); + ESP_LOGI(TAG, "[SRC #%d] Stream started", idx); if (source_stream->stream.qos == NULL || source_stream->stream.qos->sdu == 0) { - ESP_LOGE(TAG, "Invalid stream qos"); + ESP_LOGE(TAG, "[SRC #%d] Invalid QoS", idx); return; } if (source_stream->data == NULL) { source_stream->data = calloc(1, source_stream->stream.qos->sdu); if (source_stream->data == NULL) { - ESP_LOGE(TAG, "Failed to alloc tx buffer, sdu %u", source_stream->stream.qos->sdu); + ESP_LOGE(TAG, "[SRC #%d] Failed to alloc TX buffer (sdu %u)", + idx, source_stream->stream.qos->sdu); return; } } @@ -94,7 +106,7 @@ static void stream_started_cb(esp_ble_audio_bap_stream_t *stream) /* Note: esp timer is not accurate enough */ err = example_audio_tx_scheduler_start(&source_stream->scheduler, preset_active.qos.interval); if (err) { - ESP_LOGE(TAG, "Failed to start tx scheduler, err %d", err); + ESP_LOGE(TAG, "[SRC #%d] Scheduler start failed, err %d", idx, err); return; } @@ -106,13 +118,14 @@ static void stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason struct broadcast_source_stream *source_stream = CONTAINER_OF(stream, struct broadcast_source_stream, stream); + int idx = stream_index(stream); esp_err_t err; - ESP_LOGI(TAG, "Stream %p stopped, reason 0x%02x", stream, reason); + ESP_LOGI(TAG, "[SRC #%d] Stream stopped, reason 0x%02x", idx, reason); err = example_audio_tx_scheduler_stop(&source_stream->scheduler); if (err) { - ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err); + ESP_LOGE(TAG, "[SRC #%d] Scheduler stop failed, err %d", idx, err); } } @@ -121,13 +134,14 @@ static void stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, uint8_t r struct broadcast_source_stream *source_stream = CONTAINER_OF(stream, struct broadcast_source_stream, stream); + int idx = stream_index(stream); esp_err_t err; - ESP_LOGI(TAG, "Stream %p disconnected, reason 0x%02x", stream, reason); + ESP_LOGI(TAG, "[SRC #%d] ISO disconnected, reason 0x%02x", idx, reason); err = example_audio_tx_scheduler_stop(&source_stream->scheduler); if (err) { - ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err); + ESP_LOGE(TAG, "[SRC #%d] Scheduler stop failed, err %d", idx, err); } } @@ -136,8 +150,10 @@ static void stream_sent_cb(esp_ble_audio_bap_stream_t *stream, void *user_data) struct broadcast_source_stream *source_stream = CONTAINER_OF(stream, struct broadcast_source_stream, stream); + char name[24]; - example_audio_tx_scheduler_on_sent(&source_stream->scheduler, user_data, TAG, "stream", stream); + snprintf(name, sizeof(name), "SRC #%d", stream_index(stream)); + example_audio_tx_scheduler_on_sent(&source_stream->scheduler, user_data, TAG, name); } static esp_ble_audio_bap_stream_ops_t stream_ops = { @@ -170,13 +186,16 @@ static void broadcast_source_tx(struct broadcast_source_stream *source_stream) return; } + int idx = stream_index(&source_stream->stream); + if (source_stream->stream.qos == NULL || source_stream->stream.qos->sdu == 0) { - ESP_LOGE(TAG, "Invalid stream qos"); + ESP_LOGE(TAG, "[SRC #%d] Invalid QoS", idx); return; } if (source_stream->data == NULL) { - ESP_LOGE(TAG, "Tx buffer unavailable, sdu %u", source_stream->stream.qos->sdu); + ESP_LOGE(TAG, "[SRC #%d] TX buffer unavailable (sdu %u)", + idx, source_stream->stream.qos->sdu); return; } @@ -187,8 +206,7 @@ static void broadcast_source_tx(struct broadcast_source_stream *source_stream) source_stream->stream.qos->sdu, source_stream->seq_num); if (err) { - ESP_LOGD(TAG, "Failed to broadcast data on stream %p, err %d", - &source_stream->stream, err); + ESP_LOGD(TAG, "[SRC #%d] send failed, err %d", idx, err); return; } @@ -208,12 +226,12 @@ static void tx_scheduler_cb(void *arg) static void source_started_cb(esp_ble_audio_bap_broadcast_source_t *source) { - ESP_LOGI(TAG, "Broadcast source %p started", source); + ESP_LOGI(TAG, "Broadcast source started"); } static void source_stopped_cb(esp_ble_audio_bap_broadcast_source_t *source, uint8_t reason) { - ESP_LOGI(TAG, "Broadcast source %p stopped, reason 0x%02x", source, reason); + ESP_LOGI(TAG, "Broadcast source stopped, reason 0x%02x", reason); for (size_t i = 0; i < ARRAY_SIZE(streams); i++) { if (streams[i].data != NULL) { @@ -275,7 +293,7 @@ static esp_err_t broadcast_source_setup(void) memcpy(create_param.broadcast_code, LOCAL_BROADCAST_CODE, strlen(LOCAL_BROADCAST_CODE)); } - ESP_LOGI(TAG, "Creating broadcast source with %u subgroups & %u streams per subgroup", + ESP_LOGI(TAG, "Creating broadcast source: %u subgroup(s), %u stream(s)/subgroup", ARRAY_SIZE(subgroup_param), streams_per_subgroup); err = esp_ble_audio_bap_broadcast_source_create(&create_param, &broadcast_source); @@ -453,7 +471,7 @@ static int ext_adv_start(void) goto end; } - ESP_LOGI(TAG, "Extended adv instance %u started", ADV_HANDLE); + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); end: if (ext_data) { @@ -525,7 +543,7 @@ void app_main(void) tx_scheduler_cb, &streams[i]); if (err) { - ESP_LOGE(TAG, "Failed to initialize tx scheduler[%u], err %d", i, err); + ESP_LOGE(TAG, "[SRC #%zu] Scheduler init failed, err %d", i, err); return; } } diff --git a/examples/bluetooth/esp_ble_audio/bap/unicast_client/README.md b/examples/bluetooth/esp_ble_audio/bap/unicast_client/README.md index 74bfa915a2c..84b0d7922e2 100644 --- a/examples/bluetooth/esp_ble_audio/bap/unicast_client/README.md +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_client/README.md @@ -5,60 +5,119 @@ (See the README.md file in the upper level `examples` directory for more information about examples.) -This example demonstrates the **Basic Audio Profile (BAP) Unicast Client** functionality. It scans for a BAP Unicast Server (advertising with the ASCS service data), connects to it, performs GATT service discovery and MTU exchange, then discovers sink and source ASEs, configures streams with an LC3 unicast preset, sets QoS, enables and connects the streams, and establishes unicast audio. For source (TX) streams the client starts the stream and can send audio data; sink streams are started by the server. Run it together with the [unicast_server](../unicast_server) example on another device as the peer. +## Overview -The implementation uses the NimBLE host stack with ISO and BAP support, ESP-BLE-ISO, and ESP-BLE-AUDIO APIs (BAP unicast client discover, config, QoS, enable, connect, start; stream send). It is intended for chips that support BLE 5.2 ISO and LE Audio (e.g. ESP32-H4). The client uses the LC3 16_2_1 unicast preset and initiates pairing after the ACL connection. +This example implements a **BAP Unicast Client** on top of the NimBLE host stack. It scans for a peer advertising the ASCS service UUID with a complete local name of `BAP Unicast Server`, opens an ACL connection, initiates pairing, exchanges MTU, and starts GATT service discovery. + +Once the peer is reachable, the client uses the BAP unicast client API (`esp_ble_audio_bap_unicast_client_discover`, `esp_ble_audio_bap_stream_config`, `esp_ble_audio_bap_unicast_group_create`, `esp_ble_audio_bap_stream_qos`, `esp_ble_audio_bap_stream_enable`, `esp_ble_audio_bap_stream_connect`, `esp_ble_audio_bap_stream_start`) to discover sink and source ASEs, configure each with the LC3 `16_2_1` unicast preset, build a unicast group, set QoS, enable, and ISO-connect the streams. Source (TX-from-server) streams are started by this client; sink (TX-to-server) streams are started by the server. + +The TX path in `stream_tx.c` registers each newly started sink stream with `example_audio_tx_scheduler` and feeds it SDU-sized buffers driven by `esp_ble_audio_bap_stream_send`. The PACS callbacks (`pac_record`, `endpoint`, `location`, `available_contexts`) print the discovered peer capabilities. ## Requirements -* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4) -* Another device running the [unicast_server](../unicast_server) example, which advertises with ASCS and acts as BAP Unicast Server +* A board with BLE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31) +* Peer device running the paired example -## How to Use Example +## Configuration -Before project configuration and build, set the correct chip target: +Open menuconfig: + +```bash +idf.py menuconfig +``` + +No build-time options — runtime defaults are baked into source. + +### Security & Pairing + +This example inherits a Just-Works pairing model (LE Secure Connections, no MITM) with bonding enabled from the shared init at `../common_components/example_init/ble_audio_example_init.c`. + +## Build & Flash ```bash idf.py set-target esp32h4 -``` - -### Build and Flash - -Run the following to build, flash and monitor: - -```bash idf.py -p PORT flash monitor ``` -(To exit the serial monitor, type ``Ctrl-]``.) - -See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF. +(Exit serial monitor with `Ctrl-]`.) ## Example Flow -1. **Initialization**: NVS, Bluetooth stack, and LE Audio common layer (`esp_ble_audio_common_init`) with GAP and GATT callbacks. Register stream ops for sink and source streams, register BAP unicast client callbacks, then start the audio stack. -2. **Scanning**: Start passive extended scanning. On scan report, parse advertising for connectable packets containing the ASCS (Audio Stream Control Service) service data; when found, initiate an ACL connection to that device. -3. **Connection and security**: On ACL connection, initiate pairing. After MTU exchange and GATT service discovery complete, start BAP discovery (sinks first, then sources). -4. **Discovery**: Discover sink ASEs then source ASEs on the server. For each discovered endpoint, the endpoint callback records the EP; when source discovery completes, start configuring streams. -5. **Configure, QoS, enable**: For each sink and source stream, configure with the LC3 16_2_1 codec config. When all streams are configured, create a unicast group, set QoS for the group, then enable each stream with metadata. When all streams are enabled, connect each stream. -6. **Connect and start**: When each stream is connected, connect the next; when all are connected, start the source (TX) streams. Sink streams are started by the server. When a TX stream is started, it is registered for sending; the example can then send audio on that stream. +1. `app_main` initializes NVS, NimBLE, and the LE Audio common layer, registers stream ops on every sink/source slot, registers the BAP unicast client callbacks, sets the device name `BAP Unicast Client`, and calls `ext_scan_start`. +2. Passive extended scanning runs forever; `scan_data_cb` matches the complete local name `BAP Unicast Server` and parses the ASCS service-data AD for announcement type plus a 32-bit available-contexts field. +3. On a connectable match `conn_create` cancels scanning and issues `ble_gap_connect`; the resulting `ESP_BLE_AUDIO_GAP_EVENT_ACL_CONNECT` saves the handle and calls `ble_gap_security_initiate`. +4. After the security-change event the client triggers `ble_gattc_exchange_mtu`; `gatt_mtu_change` then calls `esp_ble_audio_gattc_disc_start`, and once both MTU and discovery are reported `discover_sinks` runs. +5. `discover_cb` chains sink discovery into `discover_sources`; `endpoint_cb` records each EP into the next free `sinks[]`/`sources[]` slot, and source completion kicks `configure_stream`, which configures every EP with `unicast_preset.codec_cfg`. +6. After the last `config_cb`, `create_group` builds `pair_params` (sinks first, then sources) with `ESP_BLE_ISO_PACKING_SEQUENTIAL` and calls `esp_ble_audio_bap_unicast_group_create`, then `set_stream_qos`. +7. When `is_all_stream_qos_set` returns true `enable_stream` walks every stream; on the final `enable_cb`, `connect_stream` ISO-connects each pair (sink first if available); when all are up `start_stream` starts only source streams. Sink streams are started by the server, and `stream_started_cb` calls `stream_tx_register` to begin TX. -## Example Output +## Expected Log + +TAG: `BAP_UCL`. + +Startup and scanning: ``` -I (xxx) BAP_UCL: Unicast server found, type ... -I (xxx) BAP_UCL: connection established, status 0 -I (xxx) BAP_UCL: Sink #0: ep 0x... -I (xxx) BAP_UCL: Source #0: ep 0x... -I (xxx) BAP_UCL: Discover sinks complete -I (xxx) BAP_UCL: Discover sources complete -I (xxx) BAP_UCL: Sink stream[0] configured -I (xxx) BAP_UCL: Source stream[0] configured -I (xxx) BAP_UCL: Stream 0x... QoS set -I (xxx) BAP_UCL: Stream 0x... enabled -I (xxx) BAP_UCL: Stream 0x... connected -I (xxx) BAP_UCL: Stream 0x... started -... +I (xxx) BAP_UCL: Scanning for unicast server... +I (xxx) BAP_UCL: Unicast server found: type ... contexts 0x... ``` -If connection or discovery fails, relevant error messages are logged. +ACL, security, MTU, discovery: + +``` +I (xxx) BAP_UCL: Connected: handle ... role ... peer ... +I (xxx) BAP_UCL: Security: handle ... level ... bonded ... +I (xxx) BAP_UCL: MTU updated: handle ... mtu ... +I (xxx) BAP_UCL: Service discovery started: handle ... +I (xxx) BAP_UCL: Service discovery complete: handle ... +``` + +ASE discovery: + +``` +I (xxx) BAP_UCL: [SNK] Discovering... +I (xxx) BAP_UCL: [SNK #...] Endpoint discovered +I (xxx) BAP_UCL: [SNK] Discovery complete +I (xxx) BAP_UCL: [SRC] Discovering... +I (xxx) BAP_UCL: [SRC #...] Endpoint discovered +I (xxx) BAP_UCL: [SRC] Discovery complete +``` + +Config / group / QoS / enable / connect / start: + +``` +I (xxx) BAP_UCL: [SNK #...] Config ACK'd +I (xxx) BAP_UCL: [SNK #...] Stream configured, QoS preference: +I (xxx) BAP_UCL: [SNK #...] Configured +I (xxx) BAP_UCL: Pair[...]: SNK=... SRC=... +I (xxx) BAP_UCL: Unicast group created (... pair(s)) +I (xxx) BAP_UCL: [SNK #...] QoS ACK'd +I (xxx) BAP_UCL: [SNK #...] QoS set +I (xxx) BAP_UCL: [SNK #...] Enable ACK'd +I (xxx) BAP_UCL: [SNK #...] Enabled +I (xxx) BAP_UCL: Pair[...] connecting (SNK first) +I (xxx) BAP_UCL: [SNK #...] ISO connected (pair[...]) +I (xxx) BAP_UCL: [SNK #...] Connected +I (xxx) BAP_UCL: [SRC] Starting all source streams +I (xxx) BAP_UCL: [SRC #...] Stream started +I (xxx) BAP_UCL: [SNK #...] Stream started +I (xxx) BAP_UCL: [SNK #...] Started (SDU ..., interval ... us) +``` + +Disconnect: + +``` +I (xxx) BAP_UCL: [SNK #...] ISO disconnected, reason 0x... +I (xxx) BAP_UCL: Disconnected: handle ... reason 0x... +``` + +## Peer Pairing + +Run [unicast_server](../unicast_server/) on a second board. + +1. Server boots and starts connectable extended advertising with the ASCS UUID and `BAP Unicast Server` name. +2. Client boots and scans; on matching the name and ASCS service data it cancels scanning and issues `ble_gap_connect`. +3. After ACL is up the client initiates pairing/security; both sides log `Connected:` and the client logs `Security:`. +4. Client exchanges MTU and runs GATT discovery, then discovers sink ASEs followed by source ASEs on the server. +5. Client drives ASCS Config -> QoS -> Enable -> ISO Connect on every ASE; the server returns its QoS preference and accepts each operation. +6. Client starts source streams; server auto-starts each sink stream from `stream_enabled_cb`, after which the client's TX scheduler feeds SDUs and the server's `stream_recv_cb` accumulates RX metrics. 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 eb314e35d95..5686eeac57f 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 @@ -8,6 +8,7 @@ #include #include #include +#include #include "nvs_flash.h" #include "esp_system.h" @@ -168,6 +169,11 @@ static void reset_stream_state(void) reset_stream_pair_state(); for (size_t i = 0; i < ARRAY_SIZE(sinks); i++) { + esp_ble_audio_bap_stream_ops_t *ops = sinks[i].stream.ops; + + memset(&sinks[i].stream, 0, sizeof(sinks[i].stream)); + sinks[i].stream.ops = ops; + sinks[i].configured = ASCS_RSP_NONE; sinks[i].qos_set = ASCS_RSP_NONE; sinks[i].enabled = ASCS_RSP_NONE; @@ -176,6 +182,11 @@ static void reset_stream_state(void) } for (size_t i = 0; i < ARRAY_SIZE(sources); i++) { + esp_ble_audio_bap_stream_ops_t *ops = sources[i].stream.ops; + + memset(&sources[i].stream, 0, sizeof(sources[i].stream)); + sources[i].stream.ops = ops; + sources[i].configured = ASCS_RSP_NONE; sources[i].qos_set = ASCS_RSP_NONE; sources[i].enabled = ASCS_RSP_NONE; @@ -199,17 +210,42 @@ static bool stream_is_sink(const esp_ble_audio_bap_stream_t *stream) return false; } +static const char *dir_str(esp_ble_audio_dir_t dir) +{ + return dir == ESP_BLE_AUDIO_DIR_SINK ? "SNK" : "SRC"; +} + +static const char *stream_dir_str(const esp_ble_audio_bap_stream_t *stream) +{ + return stream_is_sink(stream) ? "SNK" : "SRC"; +} + +static int stream_index(const esp_ble_audio_bap_stream_t *stream) +{ + for (size_t i = 0; i < ARRAY_SIZE(sinks); i++) { + if (&sinks[i].stream == stream) { + return (int)i; + } + } + for (size_t i = 0; i < ARRAY_SIZE(sources); i++) { + if (&sources[i].stream == stream) { + return (int)i; + } + } + return -1; +} + static int discover_sinks(void) { int err; err = esp_ble_audio_bap_unicast_client_discover(default_conn_handle, ESP_BLE_AUDIO_DIR_SINK); if (err) { - ESP_LOGE(TAG, "Failed to discover sinks, err %d", err); + ESP_LOGE(TAG, "[SNK] Discover failed, err %d", err); return err; } - ESP_LOGI(TAG, "Discovering sinks"); + ESP_LOGI(TAG, "[SNK] Discovering..."); return 0; } @@ -220,10 +256,12 @@ static int discover_sources(void) err = esp_ble_audio_bap_unicast_client_discover(default_conn_handle, ESP_BLE_AUDIO_DIR_SOURCE); if (err) { - ESP_LOGE(TAG, "Failed to discover sources, err %d", err); + ESP_LOGE(TAG, "[SRC] Discover failed, err %d", err); return err; } + ESP_LOGI(TAG, "[SRC] Discovering..."); + return 0; } @@ -240,7 +278,7 @@ static bool configure_stream(void) &sinks[i].stream, sinks[i].ep, &unicast_preset.codec_cfg); if (err) { - ESP_LOGE(TAG, "Failed to configure sink stream[%u], err %d", i, err); + ESP_LOGE(TAG, "[SNK #%zu] Config request failed, err %d", i, err); sinks[i].configured = ASCS_RSP_FAILURE; /* Try to configure the next sink stream */ @@ -259,7 +297,7 @@ static bool configure_stream(void) &sources[i].stream, sources[i].ep, &unicast_preset.codec_cfg); if (err) { - ESP_LOGE(TAG, "Failed to configure source stream[%u], err %d", i, err); + ESP_LOGE(TAG, "[SRC #%zu] Config request failed, err %d", i, err); sources[i].configured = ASCS_RSP_FAILURE; /* Try to configure the next source stream */ @@ -278,11 +316,11 @@ static void stream_configured(esp_ble_audio_bap_stream_t *stream, bool success) if (sinks[i].configured == ASCS_RSP_NONE && &sinks[i].stream == stream) { if (success) { - ESP_LOGI(TAG, "Sink stream[%u] configure succeeded", i); + ESP_LOGI(TAG, "[SNK #%zu] Configured", i); sinks[i].configured = ASCS_RSP_SUCCESS; configured_sink_stream_count++; } else { - ESP_LOGE(TAG, "Sink stream[%u] configure failed", i); + ESP_LOGE(TAG, "[SNK #%zu] Configure failed", i); sinks[i].configured = ASCS_RSP_FAILURE; } return; @@ -293,18 +331,18 @@ static void stream_configured(esp_ble_audio_bap_stream_t *stream, bool success) if (sources[i].configured == ASCS_RSP_NONE && &sources[i].stream == stream) { if (success) { - ESP_LOGI(TAG, "Source stream[%u] configure succeeded", i); + ESP_LOGI(TAG, "[SRC #%zu] Configured", i); sources[i].configured = ASCS_RSP_SUCCESS; configured_source_stream_count++; } else { - ESP_LOGE(TAG, "Source stream[%u] configure failed", i); + ESP_LOGE(TAG, "[SRC #%zu] Configure failed", i); sources[i].configured = ASCS_RSP_FAILURE; } return; } } - ESP_LOGW(TAG, "No matching stream found for configured stream %p", stream); + ESP_LOGW(TAG, "No matching stream for configured callback"); } static int create_group(void) @@ -335,8 +373,9 @@ static int create_group(void) pair_params[i].rx_param = NULL; } - ESP_LOGI(TAG, "Stream pair[%u]: sink %p source %p", i, - stream_pairs[i].sink_stream, stream_pairs[i].source_stream); + ESP_LOGI(TAG, "Pair[%zu]: SNK=%s SRC=%s", i, + stream_pairs[i].sink_stream ? "yes" : "-", + stream_pairs[i].source_stream ? "yes" : "-"); } group_param.params = pair_params; @@ -349,7 +388,7 @@ static int create_group(void) return err; } - ESP_LOGI(TAG, "Created unicast group"); + ESP_LOGI(TAG, "Unicast group created (%zu pair(s))", params_count); return 0; } @@ -374,10 +413,10 @@ static void stream_qos_set(esp_ble_audio_bap_stream_t *stream, bool success) sinks[i].qos_set == ASCS_RSP_NONE && &sinks[i].stream == stream) { if (success) { - ESP_LOGI(TAG, "Sink stream[%u] QoS set", i); + ESP_LOGI(TAG, "[SNK #%zu] QoS set", i); sinks[i].qos_set = ASCS_RSP_SUCCESS; } else { - ESP_LOGE(TAG, "Sink stream[%u] QoS set failed", i); + ESP_LOGE(TAG, "[SNK #%zu] QoS set failed", i); sinks[i].qos_set = ASCS_RSP_FAILURE; } return; @@ -389,17 +428,17 @@ static void stream_qos_set(esp_ble_audio_bap_stream_t *stream, bool success) sources[i].qos_set == ASCS_RSP_NONE && &sources[i].stream == stream) { if (success) { - ESP_LOGI(TAG, "Source stream[%u] QoS set", i); + ESP_LOGI(TAG, "[SRC #%zu] QoS set", i); sources[i].qos_set = ASCS_RSP_SUCCESS; } else { - ESP_LOGE(TAG, "Source stream[%u] QoS set failed", i); + ESP_LOGE(TAG, "[SRC #%zu] QoS set failed", i); sources[i].qos_set = ASCS_RSP_FAILURE; } return; } } - ESP_LOGW(TAG, "No matching stream found for QoS set stream %p", stream); + ESP_LOGW(TAG, "No matching stream for QoS callback"); } static bool is_all_stream_qos_set(void) @@ -433,7 +472,7 @@ static bool enable_stream(void) unicast_preset.codec_cfg.meta, unicast_preset.codec_cfg.meta_len); if (err) { - ESP_LOGE(TAG, "Failed to enable sink stream[%u], err %d", i, err); + ESP_LOGE(TAG, "[SNK #%zu] Enable request failed, err %d", i, err); sinks[i].enabled = ASCS_RSP_FAILURE; /* Try to enable the next sink stream */ @@ -452,7 +491,7 @@ static bool enable_stream(void) unicast_preset.codec_cfg.meta, unicast_preset.codec_cfg.meta_len); if (err) { - ESP_LOGE(TAG, "Failed to enable source stream[%u], err %d", i, err); + ESP_LOGE(TAG, "[SRC #%zu] Enable request failed, err %d", i, err); sources[i].enabled = ASCS_RSP_FAILURE; /* Try to enable the next source stream */ @@ -474,10 +513,10 @@ static void stream_enabled(esp_ble_audio_bap_stream_t *stream, bool success) sinks[i].enabled == ASCS_RSP_NONE && &sinks[i].stream == stream) { if (success) { - ESP_LOGI(TAG, "Sink stream[%u] enabled", i); + ESP_LOGI(TAG, "[SNK #%zu] Enabled", i); sinks[i].enabled = ASCS_RSP_SUCCESS; } else { - ESP_LOGE(TAG, "Sink stream[%u] enabled failed", i); + ESP_LOGE(TAG, "[SNK #%zu] Enable failed", i); sinks[i].enabled = ASCS_RSP_FAILURE; } return; @@ -490,17 +529,17 @@ static void stream_enabled(esp_ble_audio_bap_stream_t *stream, bool success) sources[i].enabled == ASCS_RSP_NONE && &sources[i].stream == stream) { if (success) { - ESP_LOGI(TAG, "Source stream[%u] enabled", i); + ESP_LOGI(TAG, "[SRC #%zu] Enabled", i); sources[i].enabled = ASCS_RSP_SUCCESS; } else { - ESP_LOGE(TAG, "Source stream[%u] enabled failed", i); + ESP_LOGE(TAG, "[SRC #%zu] Enable failed", i); sources[i].enabled = ASCS_RSP_FAILURE; } return; } } - ESP_LOGW(TAG, "No matching stream found for enabled stream %p", stream); + ESP_LOGW(TAG, "No matching stream for enable callback"); } static int connect_stream(void) @@ -546,7 +585,7 @@ static int connect_stream(void) err = esp_ble_audio_bap_stream_connect(stream); if (err) { - ESP_LOGE(TAG, "Failed to connect stream pair[%u], err %d", i, err); + ESP_LOGE(TAG, "Pair[%zu] connect failed, err %d", i, err); if (sink_ready) { sinks[sink_index].connected = ASCS_RSP_FAILURE; @@ -564,7 +603,8 @@ static int connect_stream(void) sources[source_index].connected = ASCS_RSP_CONNECT; } - ESP_LOGI(TAG, "Connecting stream pair[%u], sink %p source %p", i, sink_stream, source_stream); + ESP_LOGI(TAG, "Pair[%zu] connecting (%s)", i, + sink_ready ? "SNK first" : "SRC first"); return false; } @@ -603,12 +643,11 @@ static void stream_connected(esp_ble_audio_bap_stream_t *stream, bool success) sinks[i].connected == ASCS_RSP_CONNECT && &sinks[i].stream == stream) { if (success) { - ESP_LOGI(TAG, "Sink stream[%u] connect succeeded", i); - sinks[i].connected = state; + ESP_LOGI(TAG, "[SNK #%zu] Connected", i); } else { - ESP_LOGE(TAG, "Sink stream[%u] connect failed", i); - sinks[i].connected = state; + ESP_LOGE(TAG, "[SNK #%zu] Connect failed", i); } + sinks[i].connected = state; return; } } @@ -620,17 +659,16 @@ static void stream_connected(esp_ble_audio_bap_stream_t *stream, bool success) sources[i].connected == ASCS_RSP_CONNECT && &sources[i].stream == stream) { if (success) { - ESP_LOGI(TAG, "Source stream[%u] connect succeeded", i); - sources[i].connected = state; + ESP_LOGI(TAG, "[SRC #%zu] Connected", i); } else { - ESP_LOGE(TAG, "Source stream[%u] connect failed", i); - sources[i].connected = state; + ESP_LOGE(TAG, "[SRC #%zu] Connect failed", i); } + sources[i].connected = state; return; } } - ESP_LOGW(TAG, "No matching stream found for connected stream %p", stream); + ESP_LOGW(TAG, "No matching stream for connected callback"); } static int start_stream(void) @@ -639,7 +677,7 @@ static int start_stream(void) /* Note: sink streams are started by the unicast server */ - ESP_LOGI(TAG, "Starting source streams"); + ESP_LOGI(TAG, "[SRC] Starting all source streams"); for (size_t i = 0; i < ARRAY_SIZE(sources); i++) { if (sources[i].configured == ASCS_RSP_SUCCESS && @@ -648,7 +686,7 @@ static int start_stream(void) sources[i].connected == ASCS_RSP_SUCCESS) { err = esp_ble_audio_bap_stream_start(&sources[i].stream); if (err) { - ESP_LOGE(TAG, "Failed to start stream[%u], err %d", i, err); + ESP_LOGE(TAG, "[SRC #%zu] Start failed, err %d", i, err); } } } @@ -659,19 +697,21 @@ static int start_stream(void) static void stream_configured_cb(esp_ble_audio_bap_stream_t *stream, const esp_ble_audio_bap_qos_cfg_pref_t *pref) { - ESP_LOGI(TAG, "Stream %p configured", stream); - - example_print_qos_pref(pref); + ESP_LOGI(TAG, "[%s #%d] Stream configured, QoS preference:", + stream_dir_str(stream), stream_index(stream)); + example_print_qos_pref(TAG, pref); } static void stream_qos_set_cb(esp_ble_audio_bap_stream_t *stream) { - ESP_LOGI(TAG, "Stream %p QoS set", stream); + /* QoS set is also reported by qos_cb; skip the duplicate log here. */ + (void)stream; } static void stream_enabled_cb(esp_ble_audio_bap_stream_t *stream) { - ESP_LOGI(TAG, "Stream %p enabled", stream); + /* Enabled is also reported by enable_cb; skip the duplicate log here. */ + (void)stream; } static void stream_connected_cb(esp_ble_audio_bap_stream_t *stream) @@ -680,23 +720,17 @@ static void stream_connected_cb(esp_ble_audio_bap_stream_t *stream) int pair_index; bool ret; - ESP_LOGI(TAG, "Stream %p connected", stream); - pair_index = get_stream_pair_index(stream); paired_stream = get_paired_stream(stream); - if (pair_index >= 0) { - ESP_LOGI(TAG, "Stream %p in pair[%d], paired stream %p, same_pair %u", - stream, pair_index, paired_stream, - streams_are_same_pair(stream, paired_stream)); - } else { - ESP_LOGW(TAG, "Failed to find stream pair for stream %p", stream); - } + + ESP_LOGI(TAG, "[%s #%d] ISO connected (pair[%d])", + stream_dir_str(stream), stream_index(stream), pair_index); stream_connected(stream, true); if (streams_are_same_pair(stream, paired_stream) && stream_is_connecting(paired_stream)) { - ESP_LOGI(TAG, "Waiting paired stream %p connected before next pair", paired_stream); + /* Wait for the paired stream before moving on */ return; } @@ -710,7 +744,8 @@ static void stream_connected_cb(esp_ble_audio_bap_stream_t *stream) static void stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason) { - ESP_LOGI(TAG, "Stream %p disconnected, reason 0x%02x", stream, reason); + ESP_LOGI(TAG, "[%s #%d] ISO disconnected, reason 0x%02x", + stream_dir_str(stream), stream_index(stream), reason); /* Reset the connected state to allow reconnection */ for (size_t i = 0; i < ARRAY_SIZE(sinks); i++) { @@ -728,36 +763,41 @@ static void stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, uint8_t r } } - ESP_LOGW(TAG, "No matching stream found for disconnected stream %p", stream); + ESP_LOGW(TAG, "No matching stream for disconnected callback"); } static void stream_started_cb(esp_ble_audio_bap_stream_t *stream) { int err; - ESP_LOGI(TAG, "Stream %p started", stream); + ESP_LOGI(TAG, "[%s #%d] Stream started", + stream_dir_str(stream), stream_index(stream)); if (stream_is_sink(stream)) { err = stream_tx_register(stream); if (err) { - ESP_LOGE(TAG, "Failed to register stream %p for TX, err %d", stream, err); + ESP_LOGE(TAG, "[SNK #%d] Failed to register TX, err %d", + stream_index(stream), err); } } } static void stream_metadata_updated_cb(esp_ble_audio_bap_stream_t *stream) { - ESP_LOGI(TAG, "Stream %p metadata updated", stream); + ESP_LOGI(TAG, "[%s #%d] Metadata updated", + stream_dir_str(stream), stream_index(stream)); } static void stream_disabled_cb(esp_ble_audio_bap_stream_t *stream) { - ESP_LOGI(TAG, "Stream %p disabled", stream); + ESP_LOGI(TAG, "[%s #%d] Stream disabled", + stream_dir_str(stream), stream_index(stream)); } static void stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason) { - ESP_LOGI(TAG, "Stream %p stopped, reason 0x%02x", stream, reason); + ESP_LOGI(TAG, "[%s #%d] Stream stopped, reason 0x%02x", + stream_dir_str(stream), stream_index(stream), reason); if (stream_is_sink(stream)) { (void)stream_tx_unregister(stream); @@ -766,7 +806,8 @@ static void stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason static void stream_released_cb(esp_ble_audio_bap_stream_t *stream) { - ESP_LOGI(TAG, "Stream %p released", stream); + ESP_LOGI(TAG, "[%s #%d] Stream released", + stream_dir_str(stream), stream_index(stream)); } static void stream_sent_cb(esp_ble_audio_bap_stream_t *stream, void *user_data) @@ -792,14 +833,28 @@ static void location_cb(esp_ble_conn_t *conn, esp_ble_audio_dir_t dir, esp_ble_audio_location_t loc) { - ESP_LOGI(TAG, "Location, dir %u loc 0x%08lx", dir, loc); + ESP_LOGI(TAG, "[%s] Location: 0x%08lx", dir_str(dir), loc); } static void available_contexts_cb(esp_ble_conn_t *conn, esp_ble_audio_context_t snk_ctx, esp_ble_audio_context_t src_ctx) { - ESP_LOGI(TAG, "Available contexts, sink 0x%04x source 0x%04x", snk_ctx, src_ctx); + ESP_LOGI(TAG, "Available contexts: SNK=0x%04x SRC=0x%04x", snk_ctx, src_ctx); +} + +static void log_rsp(const char *op, esp_ble_audio_bap_stream_t *stream, + esp_ble_audio_bap_ascs_rsp_code_t rsp_code, + esp_ble_audio_bap_ascs_reason_t reason) +{ + if (rsp_code == ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_SUCCESS) { + ESP_LOGI(TAG, "[%s #%d] %s ACK'd", + stream_dir_str(stream), stream_index(stream), op); + } else { + ESP_LOGW(TAG, "[%s #%d] %s rejected (rsp 0x%02x reason 0x%02x)", + stream_dir_str(stream), stream_index(stream), op, + rsp_code, reason); + } } static void config_cb(esp_ble_audio_bap_stream_t *stream, @@ -810,7 +865,7 @@ static void config_cb(esp_ble_audio_bap_stream_t *stream, bool ret; int err; - ESP_LOGI(TAG, "Config, stream %p rsp_code 0x%02x reason 0x%02x", stream, rsp_code, reason); + log_rsp("Config", stream, rsp_code, reason); stream_configured(stream, rsp_code == ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_SUCCESS); @@ -853,7 +908,7 @@ static void qos_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_code_t rsp_code, esp_ble_audio_bap_ascs_reason_t reason) { - ESP_LOGI(TAG, "Qos, stream %p rsp_code 0x%02x reason 0x%02x", stream, rsp_code, reason); + log_rsp("QoS", stream, rsp_code, reason); stream_qos_set(stream, rsp_code == ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_SUCCESS); @@ -868,7 +923,7 @@ static void enable_cb(esp_ble_audio_bap_stream_t *stream, { bool ret; - ESP_LOGI(TAG, "Enable, stream %p rsp_code 0x%02x reason 0x%02x", stream, rsp_code, reason); + log_rsp("Enable", stream, rsp_code, reason); stream_enabled(stream, rsp_code == ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_SUCCESS); @@ -884,35 +939,35 @@ static void start_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_code_t rsp_code, esp_ble_audio_bap_ascs_reason_t reason) { - ESP_LOGI(TAG, "Start, stream %p rsp_code 0x%02x reason 0x%02x", stream, rsp_code, reason); + log_rsp("Start", stream, rsp_code, reason); } static void stop_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_code_t rsp_code, esp_ble_audio_bap_ascs_reason_t reason) { - ESP_LOGI(TAG, "Stop, stream %p rsp_code 0x%02x reason 0x%02x", stream, rsp_code, reason); + log_rsp("Stop", stream, rsp_code, reason); } static void disable_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_code_t rsp_code, esp_ble_audio_bap_ascs_reason_t reason) { - ESP_LOGI(TAG, "Disable, stream %p rsp_code 0x%02x reason 0x%02x", stream, rsp_code, reason); + log_rsp("Disable", stream, rsp_code, reason); } static void metadata_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_code_t rsp_code, esp_ble_audio_bap_ascs_reason_t reason) { - ESP_LOGI(TAG, "Metadata, stream %p rsp_code 0x%02x reason 0x%02x", stream, rsp_code, reason); + log_rsp("Metadata", stream, rsp_code, reason); } static void release_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_code_t rsp_code, esp_ble_audio_bap_ascs_reason_t reason) { - ESP_LOGI(TAG, "Release, stream %p rsp_code 0x%02x reason 0x%02x", stream, rsp_code, reason); + log_rsp("Release", stream, rsp_code, reason); } static void pac_record_cb(esp_ble_conn_t *conn, @@ -923,7 +978,7 @@ static void pac_record_cb(esp_ble_conn_t *conn, return; } - example_print_codec_cap(codec_cap); + example_print_codec_cap(TAG, codec_cap); } static void endpoint_cb(esp_ble_conn_t *conn, @@ -933,23 +988,23 @@ static void endpoint_cb(esp_ble_conn_t *conn, if (dir == ESP_BLE_AUDIO_DIR_SOURCE) { for (size_t i = 0; i < ARRAY_SIZE(sources); i++) { if (sources[i].ep == NULL) { - ESP_LOGI(TAG, "Source #%u: ep %p", i, ep); + ESP_LOGI(TAG, "[SRC #%zu] Endpoint discovered", i); sources[i].ep = ep; return; } } - ESP_LOGW(TAG, "Could not add source ep"); + ESP_LOGW(TAG, "[SRC] Endpoint dropped (no free slot)"); } else if (dir == ESP_BLE_AUDIO_DIR_SINK) { for (size_t i = 0; i < ARRAY_SIZE(sinks); i++) { if (sinks[i].ep == NULL) { - ESP_LOGI(TAG, "Sink #%u: ep %p", i, ep); + ESP_LOGI(TAG, "[SNK #%zu] Endpoint discovered", i); sinks[i].ep = ep; return; } } - ESP_LOGW(TAG, "Could not add sink ep"); + ESP_LOGW(TAG, "[SNK] Endpoint dropped (no free slot)"); } } @@ -961,17 +1016,17 @@ static void discover_cb(esp_ble_conn_t *conn, int err, esp_ble_audio_dir_t dir) if (dir == ESP_BLE_AUDIO_DIR_SINK) { if (err) { - ESP_LOGE(TAG, "Discovery sinks failed, err %d", err); + ESP_LOGE(TAG, "[SNK] Discovery failed, err %d", err); } else { - ESP_LOGI(TAG, "Discover sinks complete"); + ESP_LOGI(TAG, "[SNK] Discovery complete"); discover_sources(); } } else if (dir == ESP_BLE_AUDIO_DIR_SOURCE) { if (err) { - ESP_LOGE(TAG, "Discovery sources failed, err %d", err); + ESP_LOGE(TAG, "[SRC] Discovery failed, err %d", err); } else { - ESP_LOGI(TAG, "Discover sources complete"); + ESP_LOGI(TAG, "[SRC] Discovery complete"); configure_stream(); } @@ -1120,8 +1175,8 @@ static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event) return; } - ESP_LOGI(TAG, "Unicast server found, type %u, contexts 0x%08x, meta_len %u", - adv.announcement_type, adv.audio_contexts, adv.meta_len); + ESP_LOGI(TAG, "Unicast server found: type %u contexts 0x%08x", + adv.announcement_type, adv.audio_contexts); dst.type = event->ext_scan_recv.addr.type; memcpy(dst.val, event->ext_scan_recv.addr.val, sizeof(dst.val)); @@ -1142,30 +1197,28 @@ static void acl_connect(esp_ble_audio_gap_app_event_t *event) int err; if (event->acl_connect.status) { - ESP_LOGE(TAG, "connection failed, status %d", event->acl_connect.status); + ESP_LOGE(TAG, "Connection failed, status %d", event->acl_connect.status); return; } - ESP_LOGI(TAG, "Conn established:"); - ESP_LOGI(TAG, "conn_handle 0x%04x status 0x%02x role %u peer %02x:%02x:%02x:%02x:%02x:%02x", - event->acl_connect.conn_handle, event->acl_connect.status, - event->acl_connect.role, event->acl_connect.dst.val[5], - event->acl_connect.dst.val[4], event->acl_connect.dst.val[3], - event->acl_connect.dst.val[2], event->acl_connect.dst.val[1], - event->acl_connect.dst.val[0]); + ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x", + event->acl_connect.conn_handle, event->acl_connect.role, + event->acl_connect.dst.val[5], event->acl_connect.dst.val[4], + event->acl_connect.dst.val[3], event->acl_connect.dst.val[2], + event->acl_connect.dst.val[1], event->acl_connect.dst.val[0]); + + default_conn_handle = event->acl_connect.conn_handle; err = pairing_start(event->acl_connect.conn_handle); if (err) { ESP_LOGE(TAG, "Failed to initiate security, err %d", err); return; } - - default_conn_handle = event->acl_connect.conn_handle; } static void acl_disconnect(esp_ble_audio_gap_app_event_t *event) { - ESP_LOGI(TAG, "Conn terminated: conn_handle 0x%04x reason 0x%02x", + ESP_LOGI(TAG, "Disconnected: handle %u reason 0x%02x", event->acl_disconnect.conn_handle, event->acl_disconnect.reason); default_conn_handle = CONN_HANDLE_INIT; @@ -1173,13 +1226,17 @@ static void acl_disconnect(esp_ble_audio_gap_app_event_t *event) disc_cancelled = false; mtu_exchanged = false; - reset_stream_state(); - + /* Delete the group before reset_stream_state() memsets the streams: + * the library walks stream->iso / stream->group to return ISO slots + * to the pool, and zeroing them first leaks the slot. + */ if (unicast_group != NULL) { esp_ble_audio_bap_unicast_group_delete(unicast_group); unicast_group = NULL; } + reset_stream_state(); + ext_scan_start(); } @@ -1188,19 +1245,14 @@ 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); + ESP_LOGE(TAG, "Security change failed, status %d", event->security_change.status); return; } - ESP_LOGI(TAG, "Security change:"); - ESP_LOGI(TAG, "conn_handle 0x%04x status 0x%02x role %u sec_level %u bonded %u " - "peer %02x:%02x:%02x:%02x:%02x:%02x", - event->security_change.conn_handle, event->security_change.status, - event->security_change.role, event->security_change.sec_level, - event->security_change.bonded, event->security_change.dst.val[5], - event->security_change.dst.val[4], event->security_change.dst.val[3], - event->security_change.dst.val[2], event->security_change.dst.val[1], - event->security_change.dst.val[0]); + ESP_LOGI(TAG, "Security: handle %u level %u bonded %u", + event->security_change.conn_handle, + event->security_change.sec_level, + event->security_change.bonded); err = exchange_mtu(event->security_change.conn_handle); if (err) { @@ -1233,22 +1285,23 @@ static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event) { int err; - ESP_LOGI(TAG, "gatt mtu change, conn_handle %u, mtu %u", + ESP_LOGI(TAG, "MTU updated: handle %u mtu %u", event->gatt_mtu_change.conn_handle, event->gatt_mtu_change.mtu); if (event->gatt_mtu_change.mtu < ESP_BLE_AUDIO_ATT_MTU_MIN) { - ESP_LOGW(TAG, "Invalid new mtu %u, shall be at least %u", + ESP_LOGW(TAG, "MTU %u below minimum %u", event->gatt_mtu_change.mtu, ESP_BLE_AUDIO_ATT_MTU_MIN); return; } err = esp_ble_audio_gattc_disc_start(event->gatt_mtu_change.conn_handle); if (err) { - ESP_LOGE(TAG, "Failed to start svc disc, err %d", err); + ESP_LOGE(TAG, "Failed to start service discovery, err %d", err); return; } - ESP_LOGI(TAG, "Start discovering gatt services"); + ESP_LOGI(TAG, "Service discovery started: handle %u", + event->gatt_mtu_change.conn_handle); /* Note: * MTU exchanged event may arrived after discover completed event. @@ -1262,14 +1315,15 @@ static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event) static void gattc_disc_cmpl(esp_ble_audio_gatt_app_event_t *event) { - ESP_LOGI(TAG, "gattc disc cmpl, status %u, conn_handle %u", - event->gattc_disc_cmpl.status, event->gattc_disc_cmpl.conn_handle); - if (event->gattc_disc_cmpl.status) { - ESP_LOGE(TAG, "gattc disc failed, status %u", event->gattc_disc_cmpl.status); + ESP_LOGE(TAG, "Service discovery failed, status %u", + event->gattc_disc_cmpl.status); return; } + ESP_LOGI(TAG, "Service discovery complete: handle %u", + event->gattc_disc_cmpl.conn_handle); + /* Note: * Discover completed event may arrived before MTU exchanged event. */ @@ -1317,7 +1371,7 @@ static int ext_scan_start(void) return err; } - ESP_LOGI(TAG, "Extended scan started"); + ESP_LOGI(TAG, "Scanning for unicast server..."); return 0; } diff --git a/examples/bluetooth/esp_ble_audio/bap/unicast_client/main/stream_tx.c b/examples/bluetooth/esp_ble_audio/bap/unicast_client/main/stream_tx.c index 347c4913411..caca8cb9515 100644 --- a/examples/bluetooth/esp_ble_audio/bap/unicast_client/main/stream_tx.c +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_client/main/stream_tx.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -46,13 +47,13 @@ static void stream_tx_send(struct tx_stream *tx_stream) } if (tx_stream->stream->qos == NULL || tx_stream->stream->qos->sdu == 0) { - ESP_LOGE(TAG, "Invalid stream qos"); + ESP_LOGE(TAG, "[SNK TX] Invalid QoS"); return; } if (tx_stream->data == NULL) { - ESP_LOGE(TAG, "TX buffer unavailable, SDU %u (stream %p)", - tx_stream->stream->qos->sdu, tx_stream->stream); + ESP_LOGE(TAG, "[SNK TX] Buffer unavailable (SDU %u)", + tx_stream->stream->qos->sdu); return; } @@ -64,8 +65,7 @@ static void stream_tx_send(struct tx_stream *tx_stream) tx_stream->stream->qos->sdu, tx_stream->seq_num); if (err) { - ESP_LOGD(TAG, "Failed to transmit data on stream %p, err %d", - tx_stream->stream, err); + ESP_LOGD(TAG, "[SNK TX] send failed, err %d", err); return; } @@ -76,8 +76,11 @@ void stream_tx_sent(esp_ble_audio_bap_stream_t *stream, void *user_data) { for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) { if (tx_streams[i].stream == stream) { + char name[24]; + + snprintf(name, sizeof(name), "SNK #%zu", i); example_audio_tx_scheduler_on_sent(&tx_streams[i].scheduler, user_data, - TAG, "stream", stream); + TAG, name); break; } } @@ -100,18 +103,16 @@ int stream_tx_register(esp_ble_audio_bap_stream_t *stream) for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) { if (tx_streams[i].stream == NULL) { - ESP_LOGI(TAG, "Registered stream %p for TX", stream); - if (stream->qos == NULL || stream->qos->sdu == 0) { - ESP_LOGE(TAG, "Invalid stream qos"); + ESP_LOGE(TAG, "[SNK #%zu] Invalid QoS", i); return -EINVAL; } if (tx_streams[i].data == NULL) { tx_streams[i].data = calloc(1, stream->qos->sdu); if (tx_streams[i].data == NULL) { - ESP_LOGE(TAG, "Failed to alloc TX buffer, SDU %u (stream %p)", - stream->qos->sdu, stream); + ESP_LOGE(TAG, "[SNK #%zu] Failed to alloc buffer (SDU %u)", + i, stream->qos->sdu); return -ENOMEM; } } @@ -123,17 +124,20 @@ int stream_tx_register(esp_ble_audio_bap_stream_t *stream) err = example_audio_tx_scheduler_start(&tx_streams[i].scheduler, stream->qos->interval); if (err) { - ESP_LOGE(TAG, "Failed to start tx scheduler, err %d", err); + ESP_LOGE(TAG, "[SNK #%zu] Scheduler start failed, err %d", i, err); tx_streams[i].stream = NULL; return err; } + ESP_LOGI(TAG, "[SNK #%zu] Started (SDU %u, interval %u us)", + i, stream->qos->sdu, stream->qos->interval); + stream_tx_send(&tx_streams[i]); return 0; } } - ESP_LOGE(TAG, "No free TX stream slot"); + ESP_LOGE(TAG, "[SNK] No free TX slot"); return -ENOMEM; } @@ -148,11 +152,9 @@ int stream_tx_unregister(esp_ble_audio_bap_stream_t *stream) for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) { if (tx_streams[i].stream == stream) { - ESP_LOGI(TAG, "Unregistered stream %p for TX", stream); - err = example_audio_tx_scheduler_stop(&tx_streams[i].scheduler); if (err) { - ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err); + ESP_LOGE(TAG, "[SNK #%zu] Scheduler stop failed, err %d", i, err); return err; } @@ -162,6 +164,7 @@ int stream_tx_unregister(esp_ble_audio_bap_stream_t *stream) tx_streams[i].data = NULL; } + ESP_LOGI(TAG, "[SNK #%zu] Stopped", i); return 0; } } @@ -178,7 +181,7 @@ void stream_tx_init(void) tx_scheduler_cb, &tx_streams[i]); if (err) { - ESP_LOGE(TAG, "Failed to init tx scheduler[%u], err %d", i, err); + ESP_LOGE(TAG, "[SNK #%zu] Scheduler init failed, err %d", i, err); return; } } diff --git a/examples/bluetooth/esp_ble_audio/bap/unicast_client/sdkconfig.defaults b/examples/bluetooth/esp_ble_audio/bap/unicast_client/sdkconfig.defaults index 358a3896ea1..c1c26bbc584 100644 --- a/examples/bluetooth/esp_ble_audio/bap/unicast_client/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_client/sdkconfig.defaults @@ -6,6 +6,7 @@ CONFIG_BT_ENABLED=y CONFIG_BT_BLUEDROID_ENABLED=n CONFIG_BT_NIMBLE_ENABLED=y CONFIG_BT_NIMBLE_EXT_ADV=y +CONFIG_BT_NIMBLE_NVS_PERSIST=y CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 CONFIG_BT_NIMBLE_MAX_CCCDS=20 CONFIG_BT_NIMBLE_ISO=y diff --git a/examples/bluetooth/esp_ble_audio/bap/unicast_server/README.md b/examples/bluetooth/esp_ble_audio/bap/unicast_server/README.md index b3c8abf7a36..3431689d4a3 100644 --- a/examples/bluetooth/esp_ble_audio/bap/unicast_server/README.md +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_server/README.md @@ -5,57 +5,105 @@ (See the README.md file in the upper level `examples` directory for more information about examples.) -This example demonstrates the **Basic Audio Profile (BAP) Unicast Server** functionality. It starts connectable extended advertising with the ASCS (Audio Stream Control Service) UUID and service data (targeted announcement, sink and source audio contexts), then waits for a BAP Unicast Client to connect. After connection, the server responds to client requests: codec configuration, QoS, enable, and start. The server starts sink (RX) streams when the client enables them; source (TX) streams are started by the client. The server receives audio on sink streams and can send on source streams. Run it together with the [unicast_client](../unicast_client) example on another device as the client. +## Overview -The implementation uses the NimBLE host stack with ISO and BAP support, ESP-BLE-ISO, and ESP-BLE-AUDIO APIs (PACS, BAP unicast server register and callbacks, stream start, stream recv). It is intended for chips that support BLE 5.2 ISO and LE Audio (e.g. ESP32-H4). PACS advertises sink and source capabilities with LC3 (any frequency, 10 ms frame, up to 2 channels, various contexts). The number of sink and source ASEs is set via `CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT` and `CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT`. +This example implements a **BAP Unicast Server** on top of the NimBLE host stack. It registers PACS sink and source capabilities (LC3, any frequency, 10 ms frame duration, 2-channel support, 40-120 octets per frame, up to 2 frames per SDU) and a hard-coded location/context set (front-left + front-right; UNSPECIFIED | CONVERSATIONAL | MEDIA), then starts connectable extended advertising on 1M/2M PHY whose payload carries the Flags AD, the ASCS UUID, the targeted unicast announcement service data with sink/source contexts, and the device name `BAP Unicast Server`. + +The unicast server callbacks (`config`, `reconfig`, `qos`, `enable`, `start`, `metadata`, `disable`, `stop`, `release`) react to ASCS PDUs from a peer client. `config_cb` allocates a free `sink_streams[]`/`source_streams[]` slot and returns a fixed `qos_pref` (unframed PDUs, 2M PHY preference, RTN 2, 10 ms max latency, 20-40 ms presentation delay). `enable_cb` decodes the codec config to sample rate, frame duration, and frame-blocks-per-SDU before accepting the request. Reconfig is rejected unconditionally. + +The stream ops handle ISO data: when the client enables a sink ASE, `stream_enabled_cb` calls `esp_ble_audio_bap_stream_start` so the server starts sink streams locally (the client starts source streams). Inbound ISO SDUs flow through `stream_recv_cb` into `example_audio_rx_metrics_on_recv`, which logs RX metrics periodically. ## Requirements -* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4) -* Another device running the [unicast_client](../unicast_client) example, which scans for ASCS and connects to this server +* A board with BLE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31) +* Peer device running the paired example -## How to Use Example +## Configuration -Before project configuration and build, set the correct chip target: +Open menuconfig: + +```bash +idf.py menuconfig +``` + +No build-time options — runtime defaults are baked into source. + +### Security & Pairing + +This example inherits a Just-Works pairing model (LE Secure Connections, no MITM) with bonding enabled from the shared init at `../common_components/example_init/ble_audio_example_init.c`. + +## Build & Flash ```bash idf.py set-target esp32h4 -``` - -### Build and Flash - -Run the following to build, flash and monitor: - -```bash idf.py -p PORT flash monitor ``` -(To exit the serial monitor, type ``Ctrl-]``.) - -See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF. +(Exit serial monitor with `Ctrl-]`.) ## Example Flow -1. **Initialization**: NVS, Bluetooth stack, and LE Audio common layer (`esp_ble_audio_common_init`) with GAP and GATT callbacks. Register PACS (sink and source PAC and location), register the BAP unicast server (sink and source ASE counts), register unicast server callbacks (config, reconfig, QoS, enable, start, metadata, disable, stop, release), register PACS capabilities (LC3 sink and source), register stream callbacks for sink and source streams, set PACS location and supported/available contexts, then start the audio stack and set the device name. -2. **Advertising**: Start connectable extended advertising with flags, ASCS UUID, ASCS service data (targeted, sink/source contexts), and device name `bap_unicast_server`. -3. **Client connection**: When a client connects, ACL and optional security change are handled. On MTU exchange the server may start GATT service discovery (as needed for the stack). -4. **ASCS operations**: The client discovers ASEs, then sends codec config, QoS, enable, and start. The server allocates a stream per config (sink or source), returns QoS preference, validates enable (codec params), and on start resets stream counters. For sink streams, when the client enables the stream the server calls `esp_ble_audio_bap_stream_start`; for source streams the client starts them. -5. **Streaming**: When a sink stream is started, received ISO SDUs are delivered in the stream recv callback; the example counts valid, error, and lost packets and logs periodically. Source streams can send audio when started by the client. +1. `app_main` initializes NVS, NimBLE, and the LE Audio common layer, then calls `esp_ble_audio_pacs_register` with `snk_pac/snk_loc/src_pac/src_loc` all true. +2. It registers the unicast server (`esp_ble_audio_bap_unicast_server_register` with `CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT` / `CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT`) and its callback table, registers sink and source PACS capabilities, and attaches `stream_ops` to every preallocated stream slot. +3. PACS location is set per direction (`SINK_LOCATION` / `SOURCE_LOCATION`) and supported plus available contexts are pushed for sink and source. +4. `esp_ble_audio_common_start` and `ble_svc_gap_device_name_set("BAP Unicast Server")` run, then `ext_adv_start` builds `ext_adv_data` with Flags, ASCS UUID, ASCS service data (targeted announcement + contexts), and the device name; it configures and starts the extended advertising set on `ADV_HANDLE` 0. +5. On `ESP_BLE_AUDIO_GAP_EVENT_ACL_CONNECT` the connection is logged; after the peer client triggers MTU exchange the server logs the new MTU and calls `esp_ble_audio_gattc_disc_start`. +6. ASCS callbacks fire as the client drives the state machine: `config_cb` allocates a stream and returns the QoS preference, `qos_cb` records `max_sdu`, `enable_cb` validates the codec, `metadata_cb` parses metadata, and `disable`/`stop`/`release` log their requests. +7. `stream_enabled_cb` auto-starts sink streams server-side; `stream_started_cb` resets TX or RX counters depending on direction; `stream_recv_cb` updates per-stream RX metrics on every ISO SDU. +8. On disconnect `acl_disconnect` logs the reason and restarts `ext_adv_start`. -## Example Output +## Expected Log + +TAG: `BAP_USR`. + +Startup and advertising: ``` -I (xxx) BAP_USR: Extended adv instance 0 started -I (xxx) BAP_USR: connection established, status 0 -I (xxx) BAP_USR: ASE Codec Config: conn 0x... ep 0x... dir ... -I (xxx) BAP_USR: Stream 0x... enabled -I (xxx) BAP_USR: Stream 0x... started -I (xxx) BAP_USR: Received 1000(1000/0/0) ISO data packets (stream 0x...) -... +I (xxx) BAP_USR: PACS: locations and contexts set +I (xxx) BAP_USR: Advertising started (handle 0) ``` -If the client disconnects: +Client connects: ``` -I (xxx) BAP_USR: connection disconnected, reason 0x... +I (xxx) BAP_USR: Connected: handle ... role ... peer ... +I (xxx) BAP_USR: MTU updated: handle ... mtu ... +I (xxx) BAP_USR: Service discovery started: handle ... +I (xxx) BAP_USR: Service discovery complete: handle ... status ... ``` + +ASCS operations: + +``` +I (xxx) BAP_USR: [SNK #...] Config request +I (xxx) BAP_USR: [SNK #...] QoS request: +I (xxx) BAP_USR: [SNK #...] Enable request: ... Hz, ... us, ... block(s)/SDU +I (xxx) BAP_USR: [SNK #...] Stream enabled +I (xxx) BAP_USR: [SNK #...] Stream started +I (xxx) BAP_USR: [SRC #...] Config request +I (xxx) BAP_USR: [SRC #...] QoS request: +I (xxx) BAP_USR: [SRC #...] Enable request: ... Hz, ... us, ... block(s)/SDU +I (xxx) BAP_USR: [SRC #...] Stream enabled +I (xxx) BAP_USR: [SRC #...] Stream started +``` + +Teardown: + +``` +I (xxx) BAP_USR: [SNK #...] Disable request +I (xxx) BAP_USR: [SNK #...] Stop request +I (xxx) BAP_USR: [SNK #...] Stream stopped, reason 0x... +I (xxx) BAP_USR: [SNK #...] Release request +I (xxx) BAP_USR: Disconnected: handle ... reason 0x... +``` + +## Peer Pairing + +Run [unicast_client](../unicast_client/) on a second board. + +1. Server boots and starts connectable extended advertising with the ASCS UUID and `BAP Unicast Server` name. +2. Client boots and scans; on matching the name and ASCS service data it cancels scanning and issues `ble_gap_connect`. +3. After ACL is up the client initiates pairing/security; both sides log `Connected:` and the client logs `Security:`. +4. Client exchanges MTU and runs GATT discovery, then discovers sink ASEs followed by source ASEs on the server. +5. Client drives ASCS Config -> QoS -> Enable -> ISO Connect on every ASE; the server returns its QoS preference and accepts each operation. +6. Client starts source streams; server auto-starts each sink stream from `stream_enabled_cb`, after which the client's TX scheduler feeds SDUs and the server's `stream_recv_cb` accumulates RX metrics. diff --git a/examples/bluetooth/esp_ble_audio/bap/unicast_server/main/main.c b/examples/bluetooth/esp_ble_audio/bap/unicast_server/main/main.c index 03f37cc0387..c922bb4f521 100644 --- a/examples/bluetooth/esp_ble_audio/bap/unicast_server/main/main.c +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_server/main/main.c @@ -128,6 +128,31 @@ static esp_ble_audio_dir_t stream_dir(const esp_ble_audio_bap_stream_t *stream) assert(0 && "Invalid stream"); } +static const char *dir_str(esp_ble_audio_dir_t dir) +{ + return dir == ESP_BLE_AUDIO_DIR_SINK ? "SNK" : "SRC"; +} + +static const char *stream_dir_str(const esp_ble_audio_bap_stream_t *stream) +{ + return dir_str(stream_dir(stream)); +} + +static int stream_index(const esp_ble_audio_bap_stream_t *stream) +{ + for (size_t i = 0; i < ARRAY_SIZE(sink_streams); i++) { + if (stream == &sink_streams[i].stream) { + return (int)i; + } + } + for (size_t i = 0; i < ARRAY_SIZE(source_streams); i++) { + if (stream == &source_streams[i].stream) { + return (int)i; + } + } + return -1; +} + static esp_ble_audio_bap_stream_t *stream_alloc(esp_ble_audio_dir_t dir) { if (dir == ESP_BLE_AUDIO_DIR_SOURCE) { @@ -159,20 +184,18 @@ static int config_cb(esp_ble_conn_t *conn, esp_ble_audio_bap_qos_cfg_pref_t *const pref, esp_ble_audio_bap_ascs_rsp_t *rsp) { - ESP_LOGI(TAG, "Config: conn %p ep %p dir %u", conn, ep, dir); - - example_print_codec_cfg(codec_cfg); + example_print_codec_cfg(TAG, codec_cfg); *stream = stream_alloc(dir); if (*stream == NULL) { - ESP_LOGI(TAG, "No streams available"); + ESP_LOGW(TAG, "[%s] No streams available", dir_str(dir)); *rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_NO_MEM, ESP_BLE_AUDIO_BAP_ASCS_REASON_NONE); return -ENOMEM; } - ESP_LOGI(TAG, "Config stream %p", *stream); + ESP_LOGI(TAG, "[%s #%d] Config request", dir_str(dir), stream_index(*stream)); if (dir == ESP_BLE_AUDIO_DIR_SOURCE) { configured_source_stream_count++; @@ -189,9 +212,8 @@ static int reconfig_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_qos_cfg_pref_t *const pref, esp_ble_audio_bap_ascs_rsp_t *rsp) { - ESP_LOGI(TAG, "Reconfig: stream %p dir %u", stream, dir); - - example_print_codec_cfg(codec_cfg); + ESP_LOGW(TAG, "[%s #%d] Reconfig rejected (not supported)", + dir_str(dir), stream_index(stream)); *rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_CONF_UNSUPPORTED, ESP_BLE_AUDIO_BAP_ASCS_REASON_NONE); @@ -204,9 +226,9 @@ static int qos_cb(esp_ble_audio_bap_stream_t *stream, const esp_ble_audio_bap_qos_cfg_t *qos, esp_ble_audio_bap_ascs_rsp_t *rsp) { - ESP_LOGI(TAG, "QoS: stream %p qos %p", stream, qos); - - example_print_qos(qos); + ESP_LOGI(TAG, "[%s #%d] QoS request:", + stream_dir_str(stream), stream_index(stream)); + example_print_qos(TAG, qos); for (size_t i = 0; i < ARRAY_SIZE(source_streams); i++) { if (stream == &source_streams[i].stream) { @@ -274,8 +296,9 @@ static int enable_cb(esp_ble_audio_bap_stream_t *stream, return err; } - ESP_LOGI(TAG, "Enable: stream %p meta_len %u codec %u %u %u", - stream, meta_len, freq_hz, frame_dur_us, frame_blocks); + ESP_LOGI(TAG, "[%s #%d] Enable request: %lu Hz, %lu us, %u block(s)/SDU", + stream_dir_str(stream), stream_index(stream), + freq_hz, frame_dur_us, frame_blocks); *rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_SUCCESS, ESP_BLE_AUDIO_BAP_ASCS_REASON_CODEC_DATA); @@ -285,8 +308,6 @@ static int enable_cb(esp_ble_audio_bap_stream_t *stream, static int start_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_t *rsp) { - ESP_LOGI(TAG, "Start: stream %p", stream); - for (size_t i = 0; i < ARRAY_SIZE(source_streams); i++) { if (stream == &source_streams[i].stream) { source_streams[i].seq_num = 0; @@ -304,7 +325,7 @@ static bool data_cb(uint8_t type, const uint8_t *data, esp_ble_audio_bap_ascs_rsp_t *rsp = user_data; if (ESP_BLE_AUDIO_METADATA_TYPE_IS_KNOWN(type) == false) { - ESP_LOGW(TAG, "Invalid metadata type %u or length %u", type, data_len); + ESP_LOGW(TAG, "Invalid metadata type 0x%02x len %u", type, data_len); *rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_METADATA_REJECTED, type); @@ -320,7 +341,9 @@ static int metadata_cb(esp_ble_audio_bap_stream_t *stream, { esp_err_t err; - ESP_LOGI(TAG, "Metadata: stream %p meta_len %u", stream, meta_len); + ESP_LOGI(TAG, "[%s #%d] Metadata update (%u bytes)", + stream_dir_str(stream), stream_index(stream), meta_len); + example_print_meta(TAG, meta, meta_len); err = esp_ble_audio_data_parse(meta, meta_len, data_cb, rsp); if (err) { @@ -333,21 +356,24 @@ static int metadata_cb(esp_ble_audio_bap_stream_t *stream, static int disable_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_t *rsp) { - ESP_LOGI(TAG, "Disable: stream %p", stream); + ESP_LOGI(TAG, "[%s #%d] Disable request", + stream_dir_str(stream), stream_index(stream)); return 0; } static int stop_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_t *rsp) { - ESP_LOGI(TAG, "Stop: stream %p", stream); + ESP_LOGI(TAG, "[%s #%d] Stop request", + stream_dir_str(stream), stream_index(stream)); return 0; } static int release_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_t *rsp) { - ESP_LOGI(TAG, "Release: stream %p", stream); + ESP_LOGI(TAG, "[%s #%d] Release request", + stream_dir_str(stream), stream_index(stream)); if (stream_dir(stream) == ESP_BLE_AUDIO_DIR_SOURCE && configured_source_stream_count > 0) { @@ -373,7 +399,8 @@ static void stream_enabled_cb(esp_ble_audio_bap_stream_t *stream) { esp_err_t err; - ESP_LOGI(TAG, "Stream %p enabled", stream); + ESP_LOGI(TAG, "[%s #%d] Stream enabled", + stream_dir_str(stream), stream_index(stream)); /* The unicast server is responsible for starting sink ASEs * after the client has enabled them. @@ -381,14 +408,16 @@ static void stream_enabled_cb(esp_ble_audio_bap_stream_t *stream) if (stream_dir(stream) == ESP_BLE_AUDIO_DIR_SINK) { err = esp_ble_audio_bap_stream_start(stream); if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to start stream %p, err %d", stream, err); + ESP_LOGE(TAG, "[SNK #%d] Failed to start stream, err %d", + stream_index(stream), err); } } } static void stream_started_cb(esp_ble_audio_bap_stream_t *stream) { - ESP_LOGI(TAG, "Stream %p started", stream); + ESP_LOGI(TAG, "[%s #%d] Stream started", + stream_dir_str(stream), stream_index(stream)); if (stream_dir(stream) == ESP_BLE_AUDIO_DIR_SOURCE) { struct unicast_server_stream *source_stream = @@ -408,7 +437,8 @@ static void stream_started_cb(esp_ble_audio_bap_stream_t *stream) static void stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason) { - ESP_LOGI(TAG, "Stream %p stopped, reason 0x%02x", stream, reason); + ESP_LOGI(TAG, "[%s #%d] Stream stopped, reason 0x%02x", + stream_dir_str(stream), stream_index(stream), reason); } static void stream_recv_cb(esp_ble_audio_bap_stream_t *stream, @@ -418,10 +448,13 @@ static void stream_recv_cb(esp_ble_audio_bap_stream_t *stream, if (stream_dir(stream) == ESP_BLE_AUDIO_DIR_SINK) { struct unicast_server_sink_stream *sink_stream = CONTAINER_OF(stream, struct unicast_server_sink_stream, stream); + char name[24]; + size_t idx = sink_stream - sink_streams; + snprintf(name, sizeof(name), "SNK #%zu", idx); sink_stream->rx_metrics.last_sdu_len = len; example_audio_rx_metrics_on_recv(info, &sink_stream->rx_metrics, - TAG, "stream", stream); + TAG, name); } } @@ -452,8 +485,6 @@ static int set_pacs_location(void) } } - ESP_LOGI(TAG, "Location successfully set"); - return 0; } @@ -477,8 +508,6 @@ static int set_pacs_supported_contexts(void) } } - ESP_LOGI(TAG, "Supported contexts successfully set"); - return 0; } @@ -502,7 +531,7 @@ static int set_pacs_available_contexts(void) } } - ESP_LOGI(TAG, "Available contexts successfully set"); + ESP_LOGI(TAG, "PACS: locations and contexts set"); return 0; } @@ -591,28 +620,26 @@ static void ext_adv_start(void) return; } - ESP_LOGI(TAG, "Extended adv instance %u started", ADV_HANDLE); + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); } static void acl_connect(esp_ble_audio_gap_app_event_t *event) { if (event->acl_connect.status) { - ESP_LOGE(TAG, "connection failed, status %d", event->acl_connect.status); + ESP_LOGE(TAG, "Connection failed, status %d", event->acl_connect.status); return; } - ESP_LOGI(TAG, "Conn established:"); - ESP_LOGI(TAG, "conn_handle 0x%04x status 0x%02x role %u peer %02x:%02x:%02x:%02x:%02x:%02x", - event->acl_connect.conn_handle, event->acl_connect.status, - event->acl_connect.role, event->acl_connect.dst.val[5], - event->acl_connect.dst.val[4], event->acl_connect.dst.val[3], - event->acl_connect.dst.val[2], event->acl_connect.dst.val[1], - event->acl_connect.dst.val[0]); + ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x", + event->acl_connect.conn_handle, event->acl_connect.role, + event->acl_connect.dst.val[5], event->acl_connect.dst.val[4], + event->acl_connect.dst.val[3], event->acl_connect.dst.val[2], + event->acl_connect.dst.val[1], event->acl_connect.dst.val[0]); } static void acl_disconnect(esp_ble_audio_gap_app_event_t *event) { - ESP_LOGI(TAG, "Conn terminated: conn_handle 0x%04x reason 0x%02x", + ESP_LOGI(TAG, "Disconnected: handle %u reason 0x%02x", event->acl_disconnect.conn_handle, event->acl_disconnect.reason); ext_adv_start(); @@ -636,11 +663,11 @@ static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event) { int err; - ESP_LOGI(TAG, "gatt mtu change, conn_handle %u, mtu %u", + ESP_LOGI(TAG, "MTU updated: handle %u mtu %u", event->gatt_mtu_change.conn_handle, event->gatt_mtu_change.mtu); if (event->gatt_mtu_change.mtu < ESP_BLE_AUDIO_ATT_MTU_MIN) { - ESP_LOGW(TAG, "Invalid new mtu %u, shall be at least %u", + ESP_LOGW(TAG, "MTU %u below minimum %u", event->gatt_mtu_change.mtu, ESP_BLE_AUDIO_ATT_MTU_MIN); return; } @@ -651,17 +678,19 @@ static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event) */ err = esp_ble_audio_gattc_disc_start(event->gatt_mtu_change.conn_handle); if (err) { - ESP_LOGE(TAG, "Failed to start svc disc, err %d", err); + ESP_LOGE(TAG, "Failed to start service discovery, err %d", err); return; } - ESP_LOGI(TAG, "Start discovering gatt services"); + ESP_LOGI(TAG, "Service discovery started: handle %u", + event->gatt_mtu_change.conn_handle); } static void gattc_disc_cmpl(esp_ble_audio_gatt_app_event_t *event) { - ESP_LOGI(TAG, "gattc disc cmpl, status %u, conn_handle %u", - event->gattc_disc_cmpl.status, event->gattc_disc_cmpl.conn_handle); + ESP_LOGI(TAG, "Service discovery complete: handle %u status %u", + event->gattc_disc_cmpl.conn_handle, + event->gattc_disc_cmpl.status); } static void iso_gatt_app_cb(esp_ble_audio_gatt_app_event_t *event) diff --git a/examples/bluetooth/esp_ble_audio/bap/unicast_server/sdkconfig.defaults b/examples/bluetooth/esp_ble_audio/bap/unicast_server/sdkconfig.defaults index a81ac77b511..5859e2c3258 100644 --- a/examples/bluetooth/esp_ble_audio/bap/unicast_server/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_server/sdkconfig.defaults @@ -6,6 +6,7 @@ CONFIG_BT_ENABLED=y CONFIG_BT_BLUEDROID_ENABLED=n CONFIG_BT_NIMBLE_ENABLED=y CONFIG_BT_NIMBLE_EXT_ADV=y +CONFIG_BT_NIMBLE_NVS_PERSIST=y CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 CONFIG_BT_NIMBLE_MAX_CCCDS=20 CONFIG_BT_NIMBLE_ISO=y diff --git a/examples/bluetooth/esp_ble_audio/cap/acceptor/README.md b/examples/bluetooth/esp_ble_audio/cap/acceptor/README.md index b5f765735b0..254f1edcad6 100644 --- a/examples/bluetooth/esp_ble_audio/cap/acceptor/README.md +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/README.md @@ -5,72 +5,156 @@ (See the README.md file in the upper level `examples` directory for more information about examples.) -This example demonstrates the **Common Audio Profile (CAP) Acceptor** functionality. It advertises so that a CAP Initiator can connect and set up available audio streams. The acceptor operates in one of two mutually exclusive modes selected at build time: as a **BAP Unicast Server** (unicast mode, for use with a CAP Initiator) or as a **BAP Broadcast Sink** (broadcast mode). In broadcast mode, it can be configured to scan for broadcast sources by itself, or to wait for a Broadcast Assistant to connect and direct the sync. Run it together with the [initiator](../initiator) example on another device as the CAP Initiator. +## Overview -The implementation uses the NimBLE host stack with ISO and LE Audio support, ESP-BLE-AUDIO (PACS, CAP acceptor, BAP unicast server, BAP broadcast sink, BAP scan delegator). Advertising includes the CAS (Common Audio Service) UUID and service data; when unicast is enabled it also advertises ASCS with targeted announcement and sink/source contexts; when broadcast is enabled it can include BASS (Broadcast Assistant) service data. PACS advertises LC3 sink and source capabilities (e.g. 7.5 ms / 10 ms frame, stereo, multiple contexts). The example supports optional Kconfig: unicast, broadcast, self-scan for broadcast sources, device name, and (when self-scan) target broadcast device name and broadcast code. +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). + +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. + +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`. ## Requirements -* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4) -* Optionally, another device running the [initiator](../initiator) example as CAP Initiator (for unicast and/or as Broadcast Assistant) +* A board with BLE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31) +* Peer device running the paired example -## How to Use Example +## Configuration -Before project configuration and build, set the correct chip target: - -```bash -idf.py set-target esp32h4 -``` - -### Configure the Project - -Open the configuration menu to select the mode: +Open menuconfig: ```bash idf.py menuconfig ``` -Under **Example: CAP Acceptor**, select one of: +Under **Example: CAP Acceptor** -> **CAP Acceptor mode**: -* **Unicast** — act as a BAP Unicast Server; start connectable advertising for a CAP Initiator -* **Broadcast** — act as a BAP Broadcast Sink; receive broadcast audio. When this mode is selected, you can additionally enable: - * **Scan for Broadcast Sources without Broadcast Assistant** — start scanning for broadcast sources independently, without waiting for a Broadcast Assistant to connect +* **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. -### Build and Flash +When **Broadcast** is selected, an additional option appears: -Run the following to build, flash and monitor: +* **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. + +### Security & Pairing + +Just-Works pairing (LE Secure Connections, no MITM, `BLE_SM_IO_CAP_NO_IO`) with bonding enabled, inherited from `../../common_components/example_init/ble_audio_example_init.c`. Change pairing or IO-cap there. + +## Build & Flash ```bash +idf.py set-target esp32h4 idf.py -p PORT flash monitor ``` -(To exit the serial monitor, type ``Ctrl-]``.) - -See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF. +(Exit serial monitor with `Ctrl-]`.) ## Example Flow -1. **Initialization**: NVS, Bluetooth stack, and LE Audio common layer (`esp_ble_audio_common_init`) with GAP and GATT callbacks. Register PACS (sink and source PAC and location), register sink and source capabilities (LC3), set PACS location and supported/available contexts (`init_cap_acceptor`). In unicast mode, initialize the CAP acceptor unicast part (BAP unicast server, stream alloc/release). In broadcast mode, initialize the CAP acceptor broadcast part (BAP broadcast sink, scan delegator). Start the audio stack and set the device name. -2. **Scan (optional)**: In broadcast mode with self-scan enabled, start scanning for broadcast sources before or in addition to advertising. -3. **Advertising**: Start connectable extended advertising with flags, CAS service data, and additionally ASCS service data in unicast mode or BASS service data in broadcast mode, plus device name (`cap_acceptor`). -4. **Initiator connection (unicast mode)**: When a CAP Initiator connects, the peer connection handle is stored. On MTU exchange the acceptor may start GATT service discovery. The initiator discovers ASEs and sets up unicast streams; the acceptor allocates streams and reports stream released when appropriate. -5. **Broadcast (broadcast mode)**: On PA sync events the acceptor syncs to the broadcast stream; on PA sync lost it handles disconnection. With self-scan, scan results are processed to sync to the chosen broadcast source. +1. `app_main` initializes NVS, the Bluetooth controller (`bluetooth_init`) and the LE Audio common layer (`esp_ble_audio_common_init`) with GAP and GATT callbacks. +2. `init_cap_acceptor` registers PACS, registers sink+source PAC capabilities, sets PAC location (`FRONT_LEFT | FRONT_RIGHT`) and supported/available contexts. +3. The selected sub-mode initializes itself: `cap_acceptor_unicast_init` registers BAP unicast-server callbacks, CAP stream ops on the sink and source streams, and a TX scheduler; or `cap_acceptor_broadcast_init` registers BAP scan-delegator and broadcast-sink callbacks plus broadcast stream ops. +4. `esp_ble_audio_common_start` starts the audio stack and the device name is set to `CAP Acceptor`. +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. +9. On ACL disconnect the connection handle is reset and `ext_adv_start` re-arms advertising. -## Example Output +## Expected Log + +TAG: `CAP_ACC`. + +### Unicast mode ``` -I (xxx) CAP_ACC: Extended adv instance 0 started -I (xxx) CAP_ACC: connection established, status 0 -I (xxx) CAP_ACC: gatt mtu change, conn_handle 1, mtu ... -... -I (xxx) CAP_ACC: PA synced, handle 0x... status 0x00 -... +I (xxx) CAP_ACC: Advertising started (handle 0) +I (xxx) CAP_ACC: Connected: handle ... role ... peer ... +I (xxx) CAP_ACC: MTU updated: handle ... mtu ... +I (xxx) CAP_ACC: Service discovery started: handle ... +I (xxx) CAP_ACC: Service discovery complete: handle ... +I (xxx) CAP_ACC: [SNK] Config request, codec cfg: +I (xxx) CAP_ACC: [SNK #0] Stream configured, QoS preference: +I (xxx) CAP_ACC: [SRC] Config request, codec cfg: +I (xxx) CAP_ACC: [SRC #0] Stream configured, QoS preference: +I (xxx) CAP_ACC: [SNK #0] QoS request: +I (xxx) CAP_ACC: [SNK #0] QoS set +I (xxx) CAP_ACC: [SRC #0] QoS request: +I (xxx) CAP_ACC: [SRC #0] QoS set +I (xxx) CAP_ACC: [SNK #0] Enable request (meta_len ...) +I (xxx) CAP_ACC: [SNK #0] Stream enabled +I (xxx) CAP_ACC: [SRC #0] Enable request (meta_len ...) +I (xxx) CAP_ACC: [SRC #0] Stream enabled +I (xxx) CAP_ACC: [SNK #0] Stream started +I (xxx) CAP_ACC: [SRC #0] Start request +I (xxx) CAP_ACC: [SRC #0] Stream started +``` + +On teardown / disconnect: + +``` +I (xxx) CAP_ACC: [SNK #0] Stop request +I (xxx) CAP_ACC: [SNK #0] Stream stopped, reason 0x... +I (xxx) CAP_ACC: [SNK #0] Release request +I (xxx) CAP_ACC: [SNK #0] Stream released I (xxx) CAP_ACC: Sink stream released +I (xxx) CAP_ACC: Disconnected: handle ... reason 0x... +I (xxx) CAP_ACC: Advertising started (handle 0) ``` -If the connection is lost: +### Broadcast mode (Broadcast Assistant) ``` -I (xxx) CAP_ACC: connection disconnected, reason 0x... -I (xxx) CAP_ACC: PA sync lost, handle 0x... reason ... +I (xxx) CAP_ACC: Advertising started (handle 0) +I (xxx) CAP_ACC: Connected: handle ... role ... peer ... +I (xxx) CAP_ACC: Receive state updated, pa_sync 0x... encrypt 0x... +I (xxx) CAP_ACC: Received request to sync to PA (PAST not available): ... +I (xxx) CAP_ACC: Syncing without PAST +I (xxx) CAP_ACC: PA sync ... synced for broadcast sink +I (xxx) CAP_ACC: Creating broadcast sink for broadcast ID 0x...... +I (xxx) CAP_ACC: BASE received +I (xxx) CAP_ACC: BIGInfo received +I (xxx) CAP_ACC: BIS sync request received (src_id ...): 0x........ +I (xxx) CAP_ACC: Syncing to broadcast with bitfield 0x........ +I (xxx) CAP_ACC: [SNK #0] Stream started ``` + +### Broadcast mode (self-scan) + +``` +I (xxx) CAP_ACC: Advertising started (handle 0) +I (xxx) CAP_ACC: Scanning for broadcast source... +I (xxx) CAP_ACC: Syncing without PAST from scan +I (xxx) CAP_ACC: PA sync ... synced for broadcast sink +I (xxx) CAP_ACC: Creating broadcast sink for broadcast ID 0x...... +I (xxx) CAP_ACC: BASE received +I (xxx) CAP_ACC: BIGInfo received +I (xxx) CAP_ACC: Syncing to broadcast with bitfield 0x........ +I (xxx) CAP_ACC: [SNK #0] Stream started +``` + +On PA sync loss: + +``` +I (xxx) CAP_ACC: [SNK #0] Stream stopped, reason 0x... +I (xxx) CAP_ACC: PA sync lost: sync_handle ... reason 0x... +I (xxx) CAP_ACC: Scanning for broadcast source... +``` + +## Peer Pairing + +Run the [initiator](../initiator/) on a second board. The initiator and acceptor must be configured for the **same sub-mode** — both `EXAMPLE_UNICAST`, or both `EXAMPLE_BROADCAST` — otherwise they will not pair. + +### Unicast + +1. Flash this acceptor with `EXAMPLE_UNICAST`; it advertises connectable with CAS + ASCS service data and PACS contexts. +2. Flash the initiator with `EXAMPLE_UNICAST`; it scans, finds CAS, connects and pairs. +3. The acceptor exchanges MTU, runs service discovery, then handles ASCS config / qos / enable for both sink and source ASEs. +4. The sink ASE auto-starts on enable (Receiver Start Ready); the source ASE starts on the initiator's `start` and the acceptor's TX scheduler begins. +5. Bidirectional audio runs until either side disconnects. + +### Broadcast + +1. Flash the initiator with `EXAMPLE_BROADCAST`; it advertises as `CAP Broadcast Source` (broadcast ID `0x123456`) and starts the BIG. +2. Flash this acceptor with `EXAMPLE_BROADCAST`. Either enable `EXAMPLE_SCAN_SELF` to self-scan for the source by name (broadcast code `1234`), or leave it disabled and use a separate Broadcast Assistant that connects via BASS to drive PA / BIS sync. +3. The acceptor PA-syncs, receives BASE and BIGInfo, syncs the first BIS, and `[SNK #0] Stream started`. +4. On PA sync loss the acceptor cleans up; in self-scan mode it restarts scanning. 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 599bcbb96f6..e34aa7fe7fd 100644 --- a/examples/bluetooth/esp_ble_audio/cap/acceptor/main/Kconfig.projbuild +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/Kconfig.projbuild @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: 2022 Nordic Semiconductor ASA -# SPDX-FileContributor: 2025 Espressif Systems (Shanghai) CO LTD +# SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 menu "Example: CAP Acceptor" 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 05aa7ef3e54..4f03d36c2ba 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 @@ -106,7 +106,7 @@ static int ext_scan_start(void) return err; } - ESP_LOGI(TAG, "Extended scan started"); + ESP_LOGI(TAG, "Scanning for broadcast source..."); return 0; } @@ -147,7 +147,7 @@ int check_start_scan(void) static void broadcast_stream_started_cb(esp_ble_audio_bap_stream_t *stream) { - ESP_LOGI(TAG, "Broadcast stream %p started", stream); + ESP_LOGI(TAG, "[SNK #0] Stream started"); example_audio_rx_metrics_reset(&rx_metrics); @@ -157,13 +157,18 @@ static void broadcast_stream_started_cb(esp_ble_audio_bap_stream_t *stream) static void broadcast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason) { - ESP_LOGI(TAG, "Broadcast stream %p stopped, reason 0x%02x", stream, 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 - check_start_scan(); + /* 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 */ } @@ -171,9 +176,8 @@ 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, "stream", stream); + example_audio_rx_metrics_on_recv(info, &rx_metrics, TAG, "SNK #0"); } static int create_broadcast_sink(void) @@ -355,7 +359,7 @@ static int pa_sync_req_cb(esp_ble_conn_t *conn, { int err; - ESP_LOGI(TAG, "Received request to sync to PA (PAST %savailble): %u", + 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; @@ -409,7 +413,7 @@ 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 for %p", recv_state); + ESP_LOGI(TAG, "Broadcast code received (src_id %u)", recv_state->src_id); broadcast_sink.recv_state = recv_state; @@ -437,7 +441,8 @@ static int bis_sync_req_cb(esp_ble_conn_t *conn, 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 for %p: 0x%08lx", recv_state, bis_sync_req[0]); + 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) { @@ -703,5 +708,7 @@ int cap_acceptor_broadcast_init(void) cbs_registered = true; } + ESP_LOGI(TAG, "CAP acceptor broadcast initialized"); + return 0; } diff --git a/examples/bluetooth/esp_ble_audio/cap/acceptor/main/cap_acceptor_unicast.c b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/cap_acceptor_unicast.c index c1453cecd5b..2acba3efa2c 100644 --- a/examples/bluetooth/esp_ble_audio/cap/acceptor/main/cap_acceptor_unicast.c +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/cap_acceptor_unicast.c @@ -31,8 +31,52 @@ static example_audio_tx_scheduler_t tx_scheduler; static uint16_t tx_seq_num; static uint8_t *iso_data; +static const struct peer_config *s_peer; + static void unicast_server_tx(void); +static const char *dir_str(esp_ble_audio_dir_t dir) +{ + return dir == ESP_BLE_AUDIO_DIR_SINK ? "SNK" : "SRC"; +} + +static const char *stream_dir_str(const esp_ble_audio_bap_stream_t *stream) +{ + esp_ble_audio_bap_ep_info_t ep_info = {0}; + + if (stream == NULL) { + return "???"; + } + + /* When the stream is released, stream->ep is already detached by the lib. + * Fall back to identifying by the stream's address. + */ + if (stream->ep == NULL) { + if (s_peer != NULL) { + if (stream == &s_peer->sink_stream.bap_stream) { + return "SNK"; + } + if (stream == &s_peer->source_stream.bap_stream) { + return "SRC"; + } + } + return "???"; + } + + if (esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info) != 0) { + return "???"; + } + + return dir_str(ep_info.dir); +} + +static int stream_index(const esp_ble_audio_bap_stream_t *stream) +{ + /* Only one sink and one source per peer in this example. */ + (void)stream; + return 0; +} + static void tx_scheduler_cb(void *arg) { (void)arg; @@ -49,13 +93,13 @@ static int config_cb(esp_ble_conn_t *conn, { esp_ble_audio_cap_stream_t *cap_stream; - ESP_LOGI(TAG, "Config: conn %p ep %p dir %u", conn, ep, dir); + ESP_LOGI(TAG, "[%s] Config request, codec cfg:", dir_str(dir)); - example_print_codec_cfg(codec_cfg); + example_print_codec_cfg(TAG, codec_cfg); cap_stream = stream_alloc(dir); if (cap_stream == NULL) { - ESP_LOGE(TAG, "No streams available"); + ESP_LOGE(TAG, "[%s] No streams available", dir_str(dir)); *rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_NO_MEM, ESP_BLE_AUDIO_BAP_ASCS_REASON_NONE); @@ -64,8 +108,6 @@ static int config_cb(esp_ble_conn_t *conn, *stream = &cap_stream->bap_stream; - ESP_LOGI(TAG, "Config stream %p", *stream); - *pref = qos_pref; return 0; @@ -77,9 +119,10 @@ static int reconfig_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_qos_cfg_pref_t *const pref, esp_ble_audio_bap_ascs_rsp_t *rsp) { - ESP_LOGI(TAG, "Reconfig: stream %p dir %u", stream, dir); + ESP_LOGI(TAG, "[%s #%d] Reconfig request, codec cfg:", + dir_str(dir), stream_index(stream)); - example_print_codec_cfg(codec_cfg); + example_print_codec_cfg(TAG, codec_cfg); *pref = qos_pref; @@ -90,9 +133,10 @@ static int qos_cb(esp_ble_audio_bap_stream_t *stream, const esp_ble_audio_bap_qos_cfg_t *qos, esp_ble_audio_bap_ascs_rsp_t *rsp) { - ESP_LOGI(TAG, "QoS: stream %p qos %p", stream, qos); + ESP_LOGI(TAG, "[%s #%d] QoS request:", + stream_dir_str(stream), stream_index(stream)); - example_print_qos(qos); + example_print_qos(TAG, qos); return 0; } @@ -101,14 +145,16 @@ static int enable_cb(esp_ble_audio_bap_stream_t *stream, const uint8_t meta[], size_t meta_len, esp_ble_audio_bap_ascs_rsp_t *rsp) { - ESP_LOGI(TAG, "Enable: stream %p meta_len %u", stream, meta_len); + ESP_LOGI(TAG, "[%s #%d] Enable request (meta_len %u)", + stream_dir_str(stream), stream_index(stream), meta_len); return 0; } static int start_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_t *rsp) { - ESP_LOGI(TAG, "Start: stream %p", stream); + ESP_LOGI(TAG, "[%s #%d] Start request", + stream_dir_str(stream), stream_index(stream)); return 0; } @@ -139,7 +185,8 @@ static int metadata_cb(esp_ble_audio_bap_stream_t *stream, }; esp_err_t err; - ESP_LOGI(TAG, "Metadata: stream %p meta_len %u", stream, meta_len); + ESP_LOGI(TAG, "[%s #%d] Metadata request (meta_len %u)", + stream_dir_str(stream), stream_index(stream), meta_len); err = esp_ble_audio_data_parse(meta, meta_len, data_func_cb, &func_param); if (err) { @@ -162,21 +209,24 @@ static int metadata_cb(esp_ble_audio_bap_stream_t *stream, static int disable_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_t *rsp) { - ESP_LOGI(TAG, "Disable: stream %p", stream); + ESP_LOGI(TAG, "[%s #%d] Disable request", + stream_dir_str(stream), stream_index(stream)); return 0; } static int stop_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_t *rsp) { - ESP_LOGI(TAG, "Stop: stream %p", stream); + ESP_LOGI(TAG, "[%s #%d] Stop request", + stream_dir_str(stream), stream_index(stream)); return 0; } static int release_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_t *rsp) { - ESP_LOGI(TAG, "Release: stream %p", stream); + ESP_LOGI(TAG, "[%s #%d] Release request", + stream_dir_str(stream), stream_index(stream)); return 0; } @@ -195,14 +245,16 @@ static const esp_ble_audio_bap_unicast_server_cb_t unicast_server_cb = { static void unicast_stream_configured_cb(esp_ble_audio_bap_stream_t *stream, const esp_ble_audio_bap_qos_cfg_pref_t *pref) { - ESP_LOGI(TAG, "Unicast stream %p configured", stream); + ESP_LOGI(TAG, "[%s #%d] Stream configured, QoS preference:", + stream_dir_str(stream), stream_index(stream)); - example_print_qos_pref(pref); + example_print_qos_pref(TAG, pref); } static void unicast_stream_qos_set_cb(esp_ble_audio_bap_stream_t *stream) { - ESP_LOGI(TAG, "Unicast stream %p QoS set", stream); + ESP_LOGI(TAG, "[%s #%d] QoS set", + stream_dir_str(stream), stream_index(stream)); } static void unicast_stream_enabled_cb(esp_ble_audio_bap_stream_t *stream) @@ -210,7 +262,8 @@ static void unicast_stream_enabled_cb(esp_ble_audio_bap_stream_t *stream) esp_ble_audio_bap_ep_info_t ep_info = {0}; esp_err_t err; - ESP_LOGI(TAG, "Unicast stream %p enabled", stream); + ESP_LOGI(TAG, "[%s #%d] Stream enabled", + stream_dir_str(stream), stream_index(stream)); err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info); if (err) { @@ -218,9 +271,6 @@ static void unicast_stream_enabled_cb(esp_ble_audio_bap_stream_t *stream) return; } - ESP_LOGI(TAG, "id 0x%02x dir 0x%02x can_send %u can_recv %u", - ep_info.id, ep_info.dir, ep_info.can_send, ep_info.can_recv); - if (ep_info.dir == ESP_BLE_AUDIO_DIR_SINK) { /* Automatically do the receiver start ready operation */ err = esp_ble_audio_bap_stream_start(stream); @@ -236,7 +286,8 @@ static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream) esp_ble_audio_bap_ep_info_t ep_info = {0}; esp_err_t err; - ESP_LOGI(TAG, "Unicast stream %p started", stream); + ESP_LOGI(TAG, "[%s #%d] Stream started", + stream_dir_str(stream), stream_index(stream)); example_audio_rx_metrics_reset(&rx_metrics); @@ -246,9 +297,6 @@ static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream) return; } - ESP_LOGI(TAG, "id 0x%02x dir 0x%02x can_send %u can_recv %u", - ep_info.id, ep_info.dir, ep_info.can_send, ep_info.can_recv); - if (ep_info.dir == ESP_BLE_AUDIO_DIR_SOURCE) { if (stream->qos == NULL || stream->qos->sdu == 0) { ESP_LOGE(TAG, "Invalid stream qos"); @@ -278,12 +326,14 @@ static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream) static void unicast_stream_metadata_updated_cb(esp_ble_audio_bap_stream_t *stream) { - ESP_LOGI(TAG, "Unicast stream %p metadata updated", stream); + ESP_LOGI(TAG, "[%s #%d] Metadata updated", + stream_dir_str(stream), stream_index(stream)); } static void unicast_stream_disabled_cb(esp_ble_audio_bap_stream_t *stream) { - ESP_LOGI(TAG, "Unicast stream %p disabled", stream); + ESP_LOGI(TAG, "[%s #%d] Stream disabled", + stream_dir_str(stream), stream_index(stream)); } static void unicast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason) @@ -291,7 +341,8 @@ static void unicast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_ esp_ble_audio_bap_ep_info_t ep_info = {0}; esp_err_t err; - ESP_LOGI(TAG, "Unicast stream %p stopped, reason 0x%02x", stream, reason); + ESP_LOGI(TAG, "[%s #%d] Stream stopped, reason 0x%02x", + stream_dir_str(stream), stream_index(stream), reason); err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info); if (err) { @@ -299,9 +350,6 @@ static void unicast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_ return; } - ESP_LOGI(TAG, "id 0x%02x dir 0x%02x can_send %u can_recv %u", - ep_info.id, ep_info.dir, ep_info.can_send, ep_info.can_recv); - if (ep_info.dir == ESP_BLE_AUDIO_DIR_SOURCE) { err = example_audio_tx_scheduler_stop(&tx_scheduler); if (err) { @@ -320,7 +368,8 @@ static void unicast_stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, u esp_ble_audio_bap_ep_info_t ep_info = {0}; esp_err_t err; - ESP_LOGI(TAG, "Unicast stream %p disconnected, reason 0x%02x", stream, reason); + ESP_LOGI(TAG, "[%s #%d] ISO disconnected, reason 0x%02x", + stream_dir_str(stream), stream_index(stream), reason); err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info); if (err) { @@ -347,7 +396,8 @@ static void unicast_stream_released_cb(esp_ble_audio_bap_stream_t *stream) esp_ble_audio_cap_stream_t, bap_stream); - ESP_LOGI(TAG, "Unicast stream %p released", stream); + ESP_LOGI(TAG, "[%s #%d] Stream released", + stream_dir_str(stream), stream_index(stream)); stream_released(cap_stream); } @@ -356,13 +406,21 @@ static void unicast_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 name[24]; + + snprintf(name, sizeof(name), "%s #%d", + stream_dir_str(stream), stream_index(stream)); rx_metrics.last_sdu_len = len; - example_audio_rx_metrics_on_recv(info, &rx_metrics, TAG, "stream", stream); + example_audio_rx_metrics_on_recv(info, &rx_metrics, TAG, name); } static void unicast_stream_sent_cb(esp_ble_audio_bap_stream_t *stream, void *user_data) { - example_audio_tx_scheduler_on_sent(&tx_scheduler, user_data, TAG, "stream", stream); + char name[24]; + + snprintf(name, sizeof(name), "%s #%d", + stream_dir_str(stream), stream_index(stream)); + example_audio_tx_scheduler_on_sent(&tx_scheduler, user_data, TAG, name); } static esp_ble_audio_bap_stream_ops_t unicast_stream_ops = { @@ -431,6 +489,8 @@ int cap_acceptor_unicast_init(struct peer_config *peer) static bool cbs_registered; esp_err_t err; + s_peer = peer; + if (cbs_registered == false) { esp_ble_audio_bap_unicast_server_register_param_t param = { CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT, @@ -472,5 +532,7 @@ int cap_acceptor_unicast_init(struct peer_config *peer) return -1; } + ESP_LOGI(TAG, "CAP acceptor unicast initialized"); + return 0; } 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 312ccc37323..6ba3f290fe0 100644 --- a/examples/bluetooth/esp_ble_audio/cap/acceptor/main/main.c +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/main.c @@ -134,7 +134,7 @@ static void ext_adv_start(void) return; } - ESP_LOGI(TAG, "Extended adv instance %u started", ADV_HANDLE); + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); } esp_ble_audio_cap_stream_t *stream_alloc(esp_ble_audio_dir_t dir) @@ -248,7 +248,7 @@ static void pa_sync(esp_ble_audio_gap_app_event_t *event) static void pa_sync_lost(esp_ble_audio_gap_app_event_t *event) { - ESP_LOGI(TAG, "PA sync lost: sync_handle 0x%04x reason 0x%02x", + ESP_LOGI(TAG, "PA sync lost: sync_handle %u reason 0x%02x", event->pa_sync_lost.sync_handle, event->pa_sync_lost.reason); broadcast_pa_lost(event); @@ -258,24 +258,22 @@ static void pa_sync_lost(esp_ble_audio_gap_app_event_t *event) static void acl_connect(esp_ble_audio_gap_app_event_t *event) { if (event->acl_connect.status) { - ESP_LOGE(TAG, "connection failed, status %d", event->acl_connect.status); + ESP_LOGE(TAG, "Connection failed, status %d", event->acl_connect.status); return; } - ESP_LOGI(TAG, "Conn established:"); - ESP_LOGI(TAG, "conn_handle 0x%04x status 0x%02x role %u peer %02x:%02x:%02x:%02x:%02x:%02x", - event->acl_connect.conn_handle, event->acl_connect.status, - event->acl_connect.role, event->acl_connect.dst.val[5], - event->acl_connect.dst.val[4], event->acl_connect.dst.val[3], - event->acl_connect.dst.val[2], event->acl_connect.dst.val[1], - event->acl_connect.dst.val[0]); + ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x", + event->acl_connect.conn_handle, event->acl_connect.role, + event->acl_connect.dst.val[5], event->acl_connect.dst.val[4], + event->acl_connect.dst.val[3], event->acl_connect.dst.val[2], + event->acl_connect.dst.val[1], event->acl_connect.dst.val[0]); peer.conn_handle = event->acl_connect.conn_handle; } static void acl_disconnect(esp_ble_audio_gap_app_event_t *event) { - ESP_LOGI(TAG, "Conn terminated: conn_handle 0x%04x reason 0x%02x", + ESP_LOGI(TAG, "Disconnected: handle %u reason 0x%02x", event->acl_disconnect.conn_handle, event->acl_disconnect.reason); peer.conn_handle = CONN_HANDLE_INIT; @@ -312,10 +310,11 @@ static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event) static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event) { + uint16_t conn_handle = event->gatt_mtu_change.conn_handle; int err; - ESP_LOGI(TAG, "gatt mtu change, conn_handle %u, mtu %u", - event->gatt_mtu_change.conn_handle, event->gatt_mtu_change.mtu); + ESP_LOGI(TAG, "MTU updated: handle %u mtu %u", + conn_handle, event->gatt_mtu_change.mtu); if (event->gatt_mtu_change.mtu < ESP_BLE_AUDIO_ATT_MTU_MIN) { ESP_LOGW(TAG, "Invalid new mtu %u, shall be at least %u", @@ -327,19 +326,19 @@ static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event) * - The device is a Peripheral (Link Layer role); * - The device works as a GATT client. */ - err = esp_ble_audio_gattc_disc_start(event->gatt_mtu_change.conn_handle); + err = esp_ble_audio_gattc_disc_start(conn_handle); if (err) { ESP_LOGE(TAG, "Failed to start svc disc, err %d", err); return; } - ESP_LOGI(TAG, "Start discovering gatt services"); + ESP_LOGI(TAG, "Service discovery started: handle %u", conn_handle); } static void gattc_disc_cmpl(esp_ble_audio_gatt_app_event_t *event) { - ESP_LOGI(TAG, "gattc disc cmpl, status %u, conn_handle %u", - event->gattc_disc_cmpl.status, event->gattc_disc_cmpl.conn_handle); + ESP_LOGI(TAG, "Service discovery complete: handle %u", + event->gattc_disc_cmpl.conn_handle); } static void iso_gatt_app_cb(esp_ble_audio_gatt_app_event_t *event) diff --git a/examples/bluetooth/esp_ble_audio/cap/acceptor/sdkconfig.defaults b/examples/bluetooth/esp_ble_audio/cap/acceptor/sdkconfig.defaults index 269d8a99643..f80986640a4 100644 --- a/examples/bluetooth/esp_ble_audio/cap/acceptor/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/sdkconfig.defaults @@ -6,6 +6,7 @@ CONFIG_BT_ENABLED=y CONFIG_BT_BLUEDROID_ENABLED=n CONFIG_BT_NIMBLE_ENABLED=y CONFIG_BT_NIMBLE_EXT_ADV=y +CONFIG_BT_NIMBLE_NVS_PERSIST=y CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 CONFIG_BT_NIMBLE_MAX_CCCDS=20 CONFIG_BT_NIMBLE_ISO=y diff --git a/examples/bluetooth/esp_ble_audio/cap/initiator/README.md b/examples/bluetooth/esp_ble_audio/cap/initiator/README.md index 6f3708d19e1..2f708dfd627 100644 --- a/examples/bluetooth/esp_ble_audio/cap/initiator/README.md +++ b/examples/bluetooth/esp_ble_audio/cap/initiator/README.md @@ -5,69 +5,127 @@ (See the README.md file in the upper level `examples` directory for more information about examples.) -This example demonstrates the **Common Audio Profile (CAP) Initiator** functionality. It operates in one of two mutually exclusive modes selected at build time: **unicast** or **broadcast**. In unicast mode, the initiator scans for connectable advertising that includes the CAS/ASCS service data, connects to the acceptor, performs service discovery and MTU exchange, discovers sink and source ASEs, configures streams with an LC3 16_2_1 unicast preset, sets QoS, enables and connects the streams, then starts the streams and can send or receive audio. In broadcast mode, the initiator starts extended and periodic advertising with the Broadcast Audio Announcement service data and BASE, so that Broadcast Sinks (e.g. the [acceptor](../acceptor) in broadcast mode) can sync. Run it together with the [acceptor](../acceptor) example on another device as the CAP Acceptor. +## Overview -The implementation uses the NimBLE host stack with ISO and BAP support, ESP-BLE-AUDIO (CAP initiator, BAP unicast client, BAP broadcast source). Unicast uses the LC3 16_2_1 preset and initiates pairing after connection. Broadcast uses an LC3 16_2_1 broadcast preset with a hardcoded broadcast ID. The device name is hardcoded as `CAP Initiator`. +This example implements the **Common Audio Profile (CAP) Initiator** role on top of the NimBLE host stack with ISO and LE Audio support. It is built in one of two mutually exclusive sub-modes selected at build time: a **CAP Unicast Initiator / BAP Unicast Client**, or a **CAP Broadcast Source**. A small TX module (`cap_initiator_tx.c`) drives audio packets onto every started stream using the LE Audio example TX scheduler. + +In **unicast** mode, the initiator scans for connectable extended advertising that carries CAS service data, connects, initiates pairing, exchanges MTU, performs GATT service discovery, then discovers CAS, sink ASEs and source ASEs, configures the codec with the LC3 16_2_1 unicast preset, creates an ad-hoc unicast group, and starts the streams. In **broadcast** mode, the initiator creates a CAP broadcast source with the LC3 16_2_1 broadcast preset, starts non-connectable extended advertising plus periodic advertising carrying the Broadcast Audio Announcement UUID, a hardcoded broadcast ID (`0x123456`), the local device name (`CAP Broadcast Source`), and the BASE, then starts the broadcast stream. + +The GAP/GATT device name follows the build-time mode: `CAP Initiator` in unicast mode, `CAP Broadcast Source` in broadcast mode (matching the local-name field embedded in extended advertising, which is what the acceptor's self-scan path matches against). Stream location and context are hardcoded to `MONO_AUDIO` / `UNSPECIFIED` in the LC3 preset. ## Requirements -* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4) -* For unicast or as Broadcast Assistant: another device running the [acceptor](../acceptor) example (advertising as CAP Acceptor) +* A board with BLE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31) +* Peer device running the paired example -## How to Use Example +## Configuration -Before project configuration and build, set the correct chip target: - -```bash -idf.py set-target esp32h4 -``` - -### Configure the Project - -Open the configuration menu to select the mode: +Open menuconfig: ```bash idf.py menuconfig ``` -Under **Example: CAP Initiator**, select one of: +Under **Example: CAP Initiator** -> **CAP Initiator mode**: -* **Unicast** — scan for a CAP Acceptor and set up unicast audio streams after connection -* **Broadcast** — act as a Broadcast Source; start extended and periodic advertising so that Broadcast Sinks can sync +* **Unicast** (`EXAMPLE_UNICAST`, default) — scan for a CAP Acceptor and set up unicast streams as BAP Unicast Client. Mutually exclusive with Broadcast. +* **Broadcast** (`EXAMPLE_BROADCAST`) — act as a CAP Broadcast Source; start extended + periodic advertising and a BIG so Broadcast Sinks can sync. Mutually exclusive with Unicast. -### Build and Flash +### Security & Pairing -Run the following to build, flash and monitor: +Just-Works pairing (LE Secure Connections, no MITM, `BLE_SM_IO_CAP_NO_IO`) with bonding enabled, inherited from `../../common_components/example_init/ble_audio_example_init.c`. Change pairing or IO-cap there. + +## Build & Flash ```bash +idf.py set-target esp32h4 idf.py -p PORT flash monitor ``` -(To exit the serial monitor, type ``Ctrl-]``.) - -See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF. +(Exit serial monitor with `Ctrl-]`.) ## Example Flow -1. **Initialization**: NVS, Bluetooth stack, and LE Audio common layer (`esp_ble_audio_common_init`) with GAP and GATT callbacks (unicast mode). In unicast mode, initialize the CAP initiator unicast part (BAP unicast client, CAP unicast group, stream callbacks). In broadcast mode, initialize the CAP initiator broadcast part (broadcast source, stream ops). Register the TX module for sending audio. Start the audio stack and set the device name (`CAP Initiator`). -2. **Unicast mode**: Start extended scanning. On scan result with connectable advertising and CAS/ASCS service data, connect to the acceptor. On connection, start pairing. On MTU exchange, start GATT service discovery. After discovery, discover ASEs, configure codec (LC3 16_2_1), set QoS, enable and connect streams, then start streams; the initiator registers the TX stream for sending and can receive on sink streams. -3. **Broadcast mode**: Start extended and periodic advertising with Broadcast Audio Announcement UUID, hardcoded broadcast ID, and BASE. When the broadcast stream is started, register it for TX and send audio. Sinks (e.g. acceptor in broadcast mode) can sync to this broadcast. -4. **Stream lifecycle**: Stream started/stopped callbacks register/unregister streams with the TX module; sent callbacks log periodic packet counts. +1. `app_main` initializes NVS, the Bluetooth controller (`bluetooth_init`) and the LE Audio common layer (`esp_ble_audio_common_init`); GAP/GATT callbacks are registered only in unicast mode. +2. The selected sub-mode initializes itself: `cap_initiator_unicast_init` registers CAP and BAP unicast-client callbacks, or `cap_initiator_broadcast_init` registers the broadcast stream ops. +3. `cap_initiator_tx_init` initializes the TX scheduler slot(s); `esp_ble_audio_common_start` starts the audio stack and the GAP device name is set per build-time mode (`CAP Initiator` for unicast, `CAP Broadcast Source` for broadcast). +4. **Unicast**: `cap_initiator_unicast_start` begins extended scanning. On a connectable advertising report containing CAS service data, it cancels scanning and calls `ble_gap_connect`. +5. **Unicast**: After ACL connect the initiator starts pairing, then on `SECURITY_CHANGE` exchanges MTU; on MTU change it starts GATT service discovery. Once both MTU and discovery are done, it calls `esp_ble_audio_cap_initiator_unicast_discover` (CAS), then BAP `discover` for SINK and SOURCE. +6. **Unicast**: It creates an ad-hoc unicast group with the LC3 16_2_1 QoS, then starts unicast audio. Stream callbacks log configured / qos_set / enabled / started; on start the sink (TX) stream is registered with the TX module. +7. **Broadcast**: `cap_initiator_broadcast_start` creates the broadcast source, configures and starts extended + periodic advertising (handle 0), adds the adv to the broadcast source, then starts the broadcast audio. On stream started the source stream is registered with the TX module. +8. On stream stopped/disconnected the TX module is unregistered; in unicast mode an ACL disconnect deletes the unicast group and re-arms scanning. -## Example Output +## Expected Log + +TAG: `CAP_INI`. + +### Unicast mode ``` -I (xxx) CAP_INI: connection established, status 0 -I (xxx) CAP_INI: Configured stream 0x... -I (xxx) CAP_INI: Started stream 0x... -... -I (xxx) CAP_INI: Started broadcast stream 0x... -I (xxx) CAP_INI: Sent 1000 HCI ISO data packets -... +I (xxx) CAP_INI: CAP initiator unicast initialized +I (xxx) CAP_INI: Scanning for CAP Acceptor... +I (xxx) CAP_INI: Found CAS in peer adv data! +I (xxx) CAP_INI: Connected: handle ... role ... peer ... +I (xxx) CAP_INI: Security: handle ... level ... bonded ... +I (xxx) CAP_INI: MTU updated: handle ... mtu ... +I (xxx) CAP_INI: Service discovery started: handle ... +I (xxx) CAP_INI: Service discovery complete: handle ... +I (xxx) CAP_INI: Discovering CAS +I (xxx) CAP_INI: Found CAS +I (xxx) CAP_INI: Discovering sinks +I (xxx) CAP_INI: [SNK] Endpoint discovered +I (xxx) CAP_INI: Discover sinks complete +I (xxx) CAP_INI: Discovering source ASEs +I (xxx) CAP_INI: [SRC] Endpoint discovered +I (xxx) CAP_INI: Discover sources complete +I (xxx) CAP_INI: Created unicast group +I (xxx) CAP_INI: Starting unicast streams +I (xxx) CAP_INI: [SNK #0] Stream configured, QoS preference: +I (xxx) CAP_INI: [SRC #0] Stream configured, QoS preference: +I (xxx) CAP_INI: [SNK #0] QoS set +I (xxx) CAP_INI: [SRC #0] QoS set +I (xxx) CAP_INI: [SNK #0] Stream enabled +I (xxx) CAP_INI: [SRC #0] Stream enabled +I (xxx) CAP_INI: Unicast start completed +I (xxx) CAP_INI: [SNK #0] Stream started +I (xxx) CAP_INI: [SNK #0] Started (SDU ... interval ... us) +I (xxx) CAP_INI: [SRC #0] Stream started ``` -If the connection is lost: +On disconnect: ``` -I (xxx) CAP_INI: connection disconnected, reason 0x... +I (xxx) CAP_INI: Disconnected: handle ... reason 0x... +I (xxx) CAP_INI: Deleted unicast group +I (xxx) CAP_INI: Scanning for CAP Acceptor... ``` + +### Broadcast mode + +``` +I (xxx) CAP_INI: CAP initiator broadcast initialized +I (xxx) CAP_INI: Creating broadcast source +I (xxx) CAP_INI: Advertising started (handle 0) +I (xxx) CAP_INI: [SRC #0] Stream started +I (xxx) CAP_INI: [SRC #0] Started (SDU ... interval ... us) +``` + +## Peer Pairing + +Run the [acceptor](../acceptor/) on a second board. The initiator and acceptor must be configured for the **same sub-mode** — both `EXAMPLE_UNICAST`, or both `EXAMPLE_BROADCAST` — otherwise they will not pair. + +### Unicast + +1. Flash the acceptor with `EXAMPLE_UNICAST`; it advertises with CAS + ASCS service data. +2. Flash this initiator with `EXAMPLE_UNICAST`; it scans, finds CAS in the peer's adv data and connects. +3. Pairing, MTU exchange and GATT service discovery complete. +4. CAS, sink and source discovery complete; the initiator creates the unicast group with the LC3 16_2_1 preset. +5. Streams are configured, QoS-set, enabled and started; bidirectional audio begins. +6. On ACL disconnect the initiator deletes the unicast group and resumes scanning. + +### Broadcast + +1. Flash this initiator with `EXAMPLE_BROADCAST`; it advertises as `CAP Broadcast Source` (broadcast ID `0x123456`) and starts the BIG. +2. Flash the acceptor with `EXAMPLE_BROADCAST`. With `EXAMPLE_SCAN_SELF` set the acceptor scans for the broadcast source by name and broadcast ID; without it, a separate Broadcast Assistant must drive PA sync via BASS. +3. The acceptor PA-syncs, receives the BASE/BIGInfo and syncs the BIG. +4. The acceptor's `[SNK #0] Stream started` and the initiator's TX flow are now active. diff --git a/examples/bluetooth/esp_ble_audio/cap/initiator/main/Kconfig.projbuild b/examples/bluetooth/esp_ble_audio/cap/initiator/main/Kconfig.projbuild index 472a311c6ea..c9c7448def2 100644 --- a/examples/bluetooth/esp_ble_audio/cap/initiator/main/Kconfig.projbuild +++ b/examples/bluetooth/esp_ble_audio/cap/initiator/main/Kconfig.projbuild @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: 2022 Nordic Semiconductor ASA -# SPDX-FileContributor: 2025 Espressif Systems (Shanghai) CO LTD +# SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 menu "Example: CAP Initiator" diff --git a/examples/bluetooth/esp_ble_audio/cap/initiator/main/cap_initiator.h b/examples/bluetooth/esp_ble_audio/cap/initiator/main/cap_initiator.h index fc2220d274d..f8b2201290d 100644 --- a/examples/bluetooth/esp_ble_audio/cap/initiator/main/cap_initiator.h +++ b/examples/bluetooth/esp_ble_audio/cap/initiator/main/cap_initiator.h @@ -30,6 +30,7 @@ struct tx_stream { uint16_t seq_num; uint8_t *data; example_audio_tx_scheduler_t scheduler; + bool is_broadcast; }; void cap_initiator_unicast_gap_cb(esp_ble_audio_gap_app_event_t *event); @@ -46,7 +47,7 @@ int cap_initiator_broadcast_init(void); void cap_initiator_tx_stream_sent(esp_ble_audio_bap_stream_t *stream, void *user_data); -int cap_initiator_tx_register_stream(esp_ble_audio_cap_stream_t *cap_stream); +int cap_initiator_tx_register_stream(esp_ble_audio_cap_stream_t *cap_stream, bool is_broadcast); int cap_initiator_tx_unregister_stream(esp_ble_audio_cap_stream_t *cap_stream); diff --git a/examples/bluetooth/esp_ble_audio/cap/initiator/main/cap_initiator_broadcast.c b/examples/bluetooth/esp_ble_audio/cap/initiator/main/cap_initiator_broadcast.c index a5ed29034f2..4010d691377 100644 --- a/examples/bluetooth/esp_ble_audio/cap/initiator/main/cap_initiator_broadcast.c +++ b/examples/bluetooth/esp_ble_audio/cap/initiator/main/cap_initiator_broadcast.c @@ -38,13 +38,13 @@ static void broadcast_stream_started_cb(esp_ble_audio_bap_stream_t *stream) esp_ble_audio_cap_stream_t *cap_stream; int err; - ESP_LOGI(TAG, "Broadcast stream %p started", stream); + ESP_LOGI(TAG, "[SRC #0] Stream started"); cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream); - err = cap_initiator_tx_register_stream(cap_stream); + err = cap_initiator_tx_register_stream(cap_stream, true); if (err) { - ESP_LOGE(TAG, "Failed to register stream %p for TX, err %d", stream, err); + ESP_LOGE(TAG, "[SRC #0] Failed to register TX, err %d", err); } } @@ -52,7 +52,7 @@ static void broadcast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint { esp_ble_audio_cap_stream_t *cap_stream; - ESP_LOGI(TAG, "Broadcast stream %p stopped, reason 0x%02x", stream, reason); + ESP_LOGI(TAG, "[SRC #0] Stream stopped, reason 0x%02x", reason); cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream); @@ -63,7 +63,7 @@ static void broadcast_stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, { esp_ble_audio_cap_stream_t *cap_stream; - ESP_LOGI(TAG, "Broadcast stream %p disconnected, reason 0x%02x", stream, reason); + ESP_LOGI(TAG, "[SRC #0] ISO disconnected, reason 0x%02x", reason); cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream); @@ -248,7 +248,7 @@ static int ext_adv_start(void) goto end; } - ESP_LOGI(TAG, "Extended adv instance %u started", ADV_HANDLE); + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); end: if (ext_data) { diff --git a/examples/bluetooth/esp_ble_audio/cap/initiator/main/cap_initiator_tx.c b/examples/bluetooth/esp_ble_audio/cap/initiator/main/cap_initiator_tx.c index 8ae26d69b50..79e24f17b05 100644 --- a/examples/bluetooth/esp_ble_audio/cap/initiator/main/cap_initiator_tx.c +++ b/examples/bluetooth/esp_ble_audio/cap/initiator/main/cap_initiator_tx.c @@ -16,20 +16,41 @@ static struct tx_stream tx_streams[IS_ENABLED(CONFIG_EXAMPLE_UNICAST) + IS_ENABLED(CONFIG_EXAMPLE_BROADCAST)]; -static bool cap_stream_is_streaming(const esp_ble_audio_cap_stream_t *cap_stream) +static const char *cap_stream_tx_label(const esp_ble_audio_cap_stream_t *cap_stream) +{ + esp_ble_audio_bap_ep_info_t ep_info = {0}; + + if (cap_stream == NULL || cap_stream->bap_stream.ep == NULL) { + /* Broadcast source streams do not expose an ep; always TX from SRC. */ + return "SRC"; + } + + if (esp_ble_audio_bap_ep_get_info(cap_stream->bap_stream.ep, &ep_info) != 0) { + return "SRC"; + } + + return (ep_info.dir == ESP_BLE_AUDIO_DIR_SINK) ? "SNK" : "SRC"; +} + +static bool tx_stream_is_streaming(const struct tx_stream *tx_stream) { esp_ble_audio_bap_ep_info_t ep_info = {0}; int err; - if (cap_stream == NULL) { + if (tx_stream == NULL || tx_stream->stream == NULL) { return false; } - if (cap_stream->bap_stream.ep == NULL) { + /* Broadcast streams have no ep; registration from started_cb implies streaming. */ + if (tx_stream->is_broadcast) { + return true; + } + + if (tx_stream->stream->bap_stream.ep == NULL) { return false; } - err = esp_ble_audio_bap_ep_get_info(cap_stream->bap_stream.ep, &ep_info); + err = esp_ble_audio_bap_ep_get_info(tx_stream->stream->bap_stream.ep, &ep_info); if (err) { return false; } @@ -39,25 +60,28 @@ static bool cap_stream_is_streaming(const esp_ble_audio_cap_stream_t *cap_stream static void cap_initiator_tx_send(struct tx_stream *tx_stream) { + const char *label; int err; if (tx_stream == NULL || tx_stream->stream == NULL) { return; } - if (cap_stream_is_streaming(tx_stream->stream) == false) { + if (tx_stream_is_streaming(tx_stream) == false) { return; } + label = cap_stream_tx_label(tx_stream->stream); + if (tx_stream->stream->bap_stream.qos == NULL || tx_stream->stream->bap_stream.qos->sdu == 0) { - ESP_LOGE(TAG, "Invalid stream qos"); + ESP_LOGE(TAG, "[%s] Invalid QoS", label); return; } if (tx_stream->data == NULL) { - ESP_LOGE(TAG, "TX buffer unavailable, SDU %u (stream %p)", - tx_stream->stream->bap_stream.qos->sdu, &tx_stream->stream->bap_stream); + ESP_LOGE(TAG, "[%s] Buffer unavailable (SDU %u)", + label, tx_stream->stream->bap_stream.qos->sdu); return; } @@ -67,8 +91,7 @@ static void cap_initiator_tx_send(struct tx_stream *tx_stream) tx_stream->stream->bap_stream.qos->sdu, tx_stream->seq_num); if (err) { - ESP_LOGD(TAG, "Failed to transmit data on stream %p, err %d", - &tx_stream->stream->bap_stream, err); + ESP_LOGD(TAG, "[%s] send failed, err %d", label, err); return; } @@ -79,7 +102,11 @@ void cap_initiator_tx_stream_sent(esp_ble_audio_bap_stream_t *stream, void *user { for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) { if (tx_streams[i].stream && &tx_streams[i].stream->bap_stream == stream) { - example_audio_tx_scheduler_on_sent(&tx_streams[i].scheduler, user_data, TAG, "stream", stream); + char name[24]; + + snprintf(name, sizeof(name), "%s #%zu", + cap_stream_tx_label(tx_streams[i].stream), i); + example_audio_tx_scheduler_on_sent(&tx_streams[i].scheduler, user_data, TAG, name); break; } } @@ -92,7 +119,7 @@ static void tx_scheduler_cb(void *arg) cap_initiator_tx_send(tx_stream); } -int cap_initiator_tx_register_stream(esp_ble_audio_cap_stream_t *cap_stream) +int cap_initiator_tx_register_stream(esp_ble_audio_cap_stream_t *cap_stream, bool is_broadcast) { int err; @@ -102,31 +129,41 @@ int cap_initiator_tx_register_stream(esp_ble_audio_cap_stream_t *cap_stream) for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) { if (tx_streams[i].stream == NULL) { + const char *label = cap_stream_tx_label(cap_stream); + if (cap_stream->bap_stream.qos == NULL || cap_stream->bap_stream.qos->sdu == 0) { - ESP_LOGE(TAG, "Invalid stream qos"); + ESP_LOGE(TAG, "[%s #%zu] Invalid QoS", label, i); return -EINVAL; } if (tx_streams[i].data == NULL) { tx_streams[i].data = calloc(1, cap_stream->bap_stream.qos->sdu); if (tx_streams[i].data == NULL) { - ESP_LOGE(TAG, "Failed to alloc TX buffer, SDU %u (stream %p)", - cap_stream->bap_stream.qos->sdu, &cap_stream->bap_stream); + ESP_LOGE(TAG, "[%s #%zu] Failed to alloc buffer (SDU %u)", + label, i, cap_stream->bap_stream.qos->sdu); return -ENOMEM; } } tx_streams[i].stream = cap_stream; + tx_streams[i].is_broadcast = is_broadcast; tx_streams[i].seq_num = 0; example_audio_tx_scheduler_reset(&tx_streams[i].scheduler); err = example_audio_tx_scheduler_start(&tx_streams[i].scheduler, cap_stream->bap_stream.qos->interval); if (err) { - ESP_LOGE(TAG, "Failed to start tx scheduler, err %d", err); + ESP_LOGE(TAG, "[%s #%zu] Scheduler start failed, err %d", + label, i, err); tx_streams[i].stream = NULL; + tx_streams[i].is_broadcast = false; return err; } + ESP_LOGI(TAG, "[%s #%zu] Started (SDU %u, interval %u us)", + label, i, + cap_stream->bap_stream.qos->sdu, + cap_stream->bap_stream.qos->interval); + cap_initiator_tx_send(&tx_streams[i]); return 0; } @@ -147,18 +184,23 @@ int cap_initiator_tx_unregister_stream(esp_ble_audio_cap_stream_t *cap_stream) for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) { if (tx_streams[i].stream == cap_stream) { + const char *label = cap_stream_tx_label(tx_streams[i].stream); + err = example_audio_tx_scheduler_stop(&tx_streams[i].scheduler); if (err) { - ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err); + ESP_LOGE(TAG, "[%s #%zu] Scheduler stop failed, err %d", + label, i, err); return err; } tx_streams[i].stream = NULL; + tx_streams[i].is_broadcast = false; if (tx_streams[i].data != NULL) { free(tx_streams[i].data); tx_streams[i].data = NULL; } + ESP_LOGI(TAG, "[%s #%zu] Stopped", label, i); return 0; } } @@ -177,7 +219,7 @@ void cap_initiator_tx_init(void) tx_scheduler_cb, &tx_streams[i]); if (err) { - ESP_LOGE(TAG, "Failed to initialize tx scheduler[%u], err %d", i, err); + ESP_LOGE(TAG, "Failed to initialize tx scheduler[%zu], err %d", i, err); return; } } 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 5b41001ae22..eaa72ed5e65 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 @@ -47,6 +47,28 @@ static struct peer_config { static example_audio_rx_metrics_t rx_metrics; +static const char *dir_str(esp_ble_audio_dir_t dir) +{ + return dir == ESP_BLE_AUDIO_DIR_SINK ? "SNK" : "SRC"; +} + +static const char *stream_dir_str(const esp_ble_audio_bap_stream_t *stream) +{ + if (stream == &peer.sink_stream.bap_stream) { + return "SNK"; + } else if (stream == &peer.source_stream.bap_stream) { + return "SRC"; + } + return "???"; +} + +static int stream_index(const esp_ble_audio_bap_stream_t *stream) +{ + /* Only one sink and one source per peer in this example. */ + (void)stream; + return 0; +} + static bool is_tx_stream(esp_ble_audio_bap_stream_t *stream) { esp_ble_audio_bap_ep_info_t ep_info = {0}; @@ -64,19 +86,22 @@ static bool is_tx_stream(esp_ble_audio_bap_stream_t *stream) static void unicast_stream_configured_cb(esp_ble_audio_bap_stream_t *stream, const esp_ble_audio_bap_qos_cfg_pref_t *pref) { - ESP_LOGI(TAG, "Unicast stream %p configured", stream); + ESP_LOGI(TAG, "[%s #%d] Stream configured, QoS preference:", + stream_dir_str(stream), stream_index(stream)); - example_print_qos_pref(pref); + example_print_qos_pref(TAG, pref); } static void unicast_stream_qos_set_cb(esp_ble_audio_bap_stream_t *stream) { - ESP_LOGI(TAG, "Unicast stream %p QoS set", stream); + ESP_LOGI(TAG, "[%s #%d] QoS set", + stream_dir_str(stream), stream_index(stream)); } static void unicast_stream_enabled_cb(esp_ble_audio_bap_stream_t *stream) { - ESP_LOGI(TAG, "Unicast stream %p enabled", stream); + ESP_LOGI(TAG, "[%s #%d] Stream enabled", + stream_dir_str(stream), stream_index(stream)); } static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream) @@ -84,35 +109,40 @@ static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream) esp_ble_audio_cap_stream_t *cap_stream; int err; - ESP_LOGI(TAG, "Unicast stream %p started", stream); + ESP_LOGI(TAG, "[%s #%d] Stream started", + stream_dir_str(stream), stream_index(stream)); example_audio_rx_metrics_reset(&rx_metrics); if (is_tx_stream(stream)) { cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream); - err = cap_initiator_tx_register_stream(cap_stream); + err = cap_initiator_tx_register_stream(cap_stream, false); if (err) { - ESP_LOGE(TAG, "Failed to register stream %p for TX, err %d", stream, err); + ESP_LOGE(TAG, "[%s #%d] Failed to register TX, err %d", + stream_dir_str(stream), stream_index(stream), err); } } } static void unicast_stream_metadata_updated_cb(esp_ble_audio_bap_stream_t *stream) { - ESP_LOGI(TAG, "Unicast stream %p metadata updated", stream); + ESP_LOGI(TAG, "[%s #%d] Metadata updated", + stream_dir_str(stream), stream_index(stream)); } static void unicast_stream_disabled_cb(esp_ble_audio_bap_stream_t *stream) { - ESP_LOGI(TAG, "Unicast stream %p disabled", stream); + ESP_LOGI(TAG, "[%s #%d] Stream disabled", + stream_dir_str(stream), stream_index(stream)); } static void unicast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason) { esp_ble_audio_cap_stream_t *cap_stream; - ESP_LOGI(TAG, "Unicast stream %p stopped, reason 0x%02x", stream, reason); + ESP_LOGI(TAG, "[%s #%d] Stream stopped, reason 0x%02x", + stream_dir_str(stream), stream_index(stream), reason); if (is_tx_stream(stream)) { cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream); @@ -125,7 +155,8 @@ static void unicast_stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, u { esp_ble_audio_cap_stream_t *cap_stream; - ESP_LOGI(TAG, "Unicast stream %p disconnected, reason 0x%02x", stream, reason); + ESP_LOGI(TAG, "[%s #%d] ISO disconnected, reason 0x%02x", + stream_dir_str(stream), stream_index(stream), reason); if (is_tx_stream(stream)) { cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream); @@ -136,19 +167,20 @@ static void unicast_stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, u static void unicast_stream_released_cb(esp_ble_audio_bap_stream_t *stream) { - if (stream == &peer.source_stream.bap_stream) { - ESP_LOGI(TAG, "Unicast source stream %p released", stream); - } else if (stream == &peer.sink_stream.bap_stream) { - ESP_LOGI(TAG, "Unicast sink stream %p released", stream); - } + ESP_LOGI(TAG, "[%s #%d] Stream released", + stream_dir_str(stream), stream_index(stream)); } static void unicast_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 name[24]; + + snprintf(name, sizeof(name), "%s #%d", + stream_dir_str(stream), stream_index(stream)); rx_metrics.last_sdu_len = len; - example_audio_rx_metrics_on_recv(info, &rx_metrics, TAG, "stream", stream); + example_audio_rx_metrics_on_recv(info, &rx_metrics, TAG, name); } static void unicast_stream_sent_cb(esp_ble_audio_bap_stream_t *stream, void *user_data) @@ -357,7 +389,7 @@ static void pac_record_cb(esp_ble_conn_t *conn, esp_ble_audio_dir_t dir, const esp_ble_audio_codec_cap_t *codec_cap) { - example_print_codec_cap(codec_cap); + example_print_codec_cap(TAG, codec_cap); } static void endpoint_cb(esp_ble_conn_t *conn, @@ -366,12 +398,12 @@ static void endpoint_cb(esp_ble_conn_t *conn, { if (dir == ESP_BLE_AUDIO_DIR_SOURCE) { if (peer.source_ep == NULL) { - ESP_LOGI(TAG, "Source ep: %p", ep); + ESP_LOGI(TAG, "[%s] Endpoint discovered", dir_str(dir)); peer.source_ep = ep; } } else if (dir == ESP_BLE_AUDIO_DIR_SINK) { if (peer.sink_ep == NULL) { - ESP_LOGI(TAG, "Sink ep: %p", ep); + ESP_LOGI(TAG, "[%s] Endpoint discovered", dir_str(dir)); peer.sink_ep = ep; } } @@ -398,7 +430,7 @@ static void unicast_discovery_complete_cb(esp_ble_conn_t *conn, int err, return; } - ESP_LOGI(TAG, "Found CAS with CSIS %p", csis_inst); + ESP_LOGI(TAG, "Found CAS with CSIS"); /* TODO: Do set member discovery */ } else { @@ -529,30 +561,28 @@ static void acl_connect(esp_ble_audio_gap_app_event_t *event) int err; if (event->acl_connect.status) { - ESP_LOGE(TAG, "connection failed, status %d", event->acl_connect.status); + ESP_LOGE(TAG, "Connection failed, status %d", event->acl_connect.status); return; } - ESP_LOGI(TAG, "Conn established:"); - ESP_LOGI(TAG, "conn_handle 0x%04x status 0x%02x role %u peer %02x:%02x:%02x:%02x:%02x:%02x", - event->acl_connect.conn_handle, event->acl_connect.status, - event->acl_connect.role, event->acl_connect.dst.val[5], - event->acl_connect.dst.val[4], event->acl_connect.dst.val[3], - event->acl_connect.dst.val[2], event->acl_connect.dst.val[1], - event->acl_connect.dst.val[0]); + ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x", + event->acl_connect.conn_handle, event->acl_connect.role, + event->acl_connect.dst.val[5], event->acl_connect.dst.val[4], + event->acl_connect.dst.val[3], event->acl_connect.dst.val[2], + event->acl_connect.dst.val[1], event->acl_connect.dst.val[0]); + + peer.conn_handle = event->acl_connect.conn_handle; err = pairing_start(event->acl_connect.conn_handle); if (err) { ESP_LOGE(TAG, "Failed to initiate security, err %d", err); return; } - - peer.conn_handle = event->acl_connect.conn_handle; } static void acl_disconnect(esp_ble_audio_gap_app_event_t *event) { - ESP_LOGI(TAG, "Conn terminated: conn_handle 0x%04x reason 0x%02x", + ESP_LOGI(TAG, "Disconnected: handle %u reason 0x%02x", event->acl_disconnect.conn_handle, event->acl_disconnect.reason); peer.conn_handle = CONN_HANDLE_INIT; @@ -573,19 +603,13 @@ 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); + ESP_LOGE(TAG, "Security change failed, status %d", event->security_change.status); return; } - ESP_LOGI(TAG, "Security change:"); - ESP_LOGI(TAG, "conn_handle 0x%04x status 0x%02x role %u sec_level %u bonded %u " - "peer %02x:%02x:%02x:%02x:%02x:%02x", - event->security_change.conn_handle, event->security_change.status, - event->security_change.role, event->security_change.sec_level, - event->security_change.bonded, event->security_change.dst.val[5], - event->security_change.dst.val[4], event->security_change.dst.val[3], - event->security_change.dst.val[2], event->security_change.dst.val[1], - event->security_change.dst.val[0]); + ESP_LOGI(TAG, "Security: handle %u level %u bonded %u", + event->security_change.conn_handle, event->security_change.sec_level, + event->security_change.bonded); err = exchange_mtu(event->security_change.conn_handle); if (err) { @@ -616,10 +640,11 @@ void cap_initiator_unicast_gap_cb(esp_ble_audio_gap_app_event_t *event) static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event) { + uint16_t conn_handle = event->gatt_mtu_change.conn_handle; int err; - ESP_LOGI(TAG, "gatt mtu change, conn_handle %u, mtu %u", - event->gatt_mtu_change.conn_handle, event->gatt_mtu_change.mtu); + ESP_LOGI(TAG, "MTU updated: handle %u mtu %u", + conn_handle, event->gatt_mtu_change.mtu); if (event->gatt_mtu_change.mtu < ESP_BLE_AUDIO_ATT_MTU_MIN) { ESP_LOGW(TAG, "Invalid new mtu %u, shall be at least %u", @@ -627,13 +652,13 @@ static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event) return; } - err = esp_ble_audio_gattc_disc_start(event->gatt_mtu_change.conn_handle); + err = esp_ble_audio_gattc_disc_start(conn_handle); if (err) { ESP_LOGE(TAG, "Failed to start svc disc, err %d", err); return; } - ESP_LOGI(TAG, "Start discovering gatt services"); + ESP_LOGI(TAG, "Service discovery started: handle %u", conn_handle); /* Note: * MTU exchanged event may arrived after discover completed event. @@ -647,8 +672,8 @@ static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event) static void gattc_disc_cmpl(esp_ble_audio_gatt_app_event_t *event) { - ESP_LOGI(TAG, "gattc disc cmpl, status %u, conn_handle %u", - event->gattc_disc_cmpl.status, event->gattc_disc_cmpl.conn_handle); + ESP_LOGI(TAG, "Service discovery complete: handle %u", + event->gattc_disc_cmpl.conn_handle); if (event->gattc_disc_cmpl.status) { ESP_LOGE(TAG, "gattc disc failed, status %u", event->gattc_disc_cmpl.status); @@ -702,7 +727,7 @@ int cap_initiator_unicast_start(void) return err; } - ESP_LOGI(TAG, "Extended scan started"); + ESP_LOGI(TAG, "Scanning for CAP Acceptor..."); return 0; } diff --git a/examples/bluetooth/esp_ble_audio/cap/initiator/main/main.c b/examples/bluetooth/esp_ble_audio/cap/initiator/main/main.c index 676e2fb8199..a5cf2e9ad54 100644 --- a/examples/bluetooth/esp_ble_audio/cap/initiator/main/main.c +++ b/examples/bluetooth/esp_ble_audio/cap/initiator/main/main.c @@ -68,7 +68,16 @@ void app_main(void) return; } + /* Match the GAP device name to the role this build was compiled for, so + * the name advertised in GAP reads matches the local-name field embedded + * in extended advertising (broadcast mode uses "CAP Broadcast Source", + * which is what the acceptor's self-scan path matches against). + */ +#if CONFIG_EXAMPLE_BROADCAST + err = ble_svc_gap_device_name_set("CAP Broadcast Source"); +#else err = ble_svc_gap_device_name_set("CAP Initiator"); +#endif if (err) { ESP_LOGE(TAG, "Failed to set device name, err %d", err); return; diff --git a/examples/bluetooth/esp_ble_audio/cap/initiator/sdkconfig.defaults b/examples/bluetooth/esp_ble_audio/cap/initiator/sdkconfig.defaults index dee60d5a222..478a4cc3975 100644 --- a/examples/bluetooth/esp_ble_audio/cap/initiator/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/cap/initiator/sdkconfig.defaults @@ -6,6 +6,7 @@ CONFIG_BT_ENABLED=y CONFIG_BT_BLUEDROID_ENABLED=n CONFIG_BT_NIMBLE_ENABLED=y CONFIG_BT_NIMBLE_EXT_ADV=y +CONFIG_BT_NIMBLE_NVS_PERSIST=y CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 CONFIG_BT_NIMBLE_MAX_CCCDS=20 CONFIG_BT_NIMBLE_ISO=y diff --git a/examples/bluetooth/esp_ble_audio/common_components/example_init/ble_audio_example_init.c b/examples/bluetooth/esp_ble_audio/common_components/example_init/ble_audio_example_init.c index 1607ee4b62f..ff3a3a402a0 100644 --- a/examples/bluetooth/esp_ble_audio/common_components/example_init/ble_audio_example_init.c +++ b/examples/bluetooth/esp_ble_audio/common_components/example_init/ble_audio_example_init.c @@ -21,32 +21,169 @@ #include "services/gap/ble_svc_gap.h" #include "services/gatt/ble_svc_gatt.h" -#define TAG "INIT" +#define TAG "EXAMPLE_INIT" static SemaphoreHandle_t example_audio_sem; void ble_store_config_init(void); +/* Names follow BT SIG Assigned Numbers: + * https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/uuids/ + */ +static const char *example_audio_uuid16_name(const ble_uuid_t *uuid) +{ + if (uuid->type != BLE_UUID_TYPE_16) { + return NULL; + } + + switch (ble_uuid_u16(uuid)) { + /* GAP / GATT */ + case 0x1800: return "GAP"; + case 0x1801: return "GATT"; + case 0x2a00: return "Device Name"; + case 0x2a01: return "Appearance"; + case 0x2a05: return "Service Changed"; + case 0x2b29: return "Client Sup Features"; + case 0x2b3a: return "Server Sup Features"; + + /* LE Audio services */ + case 0x1843: return "AICS"; + case 0x1844: return "VCS"; + case 0x1845: return "VOCS"; + case 0x1846: return "CSIS"; + case 0x1848: return "MCS"; + case 0x1849: return "GMCS"; + case 0x184b: return "TBS"; + case 0x184c: return "GTBS"; + case 0x184d: return "MICS"; + case 0x184e: return "ASCS"; + case 0x184f: return "BASS"; + case 0x1850: return "PACS"; + case 0x1851: return "Basic Audio Ann"; + case 0x1852: return "Broadcast Audio Ann"; + case 0x1853: return "CAS"; + case 0x1854: return "HAS"; + case 0x1855: return "TMAS"; + case 0x1856: return "Public Broadcast Ann"; + case 0x1858: return "GMAS"; + + /* Common */ + case 0x2b51: return "TMAP Role"; + case 0x2bba: return "CCID"; + + /* AICS chars */ + case 0x2b77: return "AICS State"; + case 0x2b78: return "AICS Gain Settings"; + case 0x2b79: return "AICS Input Type"; + case 0x2b7a: return "AICS Input Status"; + case 0x2b7b: return "AICS Control"; + case 0x2b7c: return "AICS Description"; + + /* VCS chars */ + case 0x2b7d: return "VCS State"; + case 0x2b7e: return "VCS Control"; + case 0x2b7f: return "VCS Flags"; + + /* VOCS chars */ + case 0x2b80: return "VOCS State"; + case 0x2b81: return "VOCS Location"; + case 0x2b82: return "VOCS Control"; + case 0x2b83: return "VOCS Description"; + + /* CSIS chars */ + case 0x2b84: return "CSIS SIRK"; + case 0x2b85: return "CSIS Set Size"; + case 0x2b86: return "CSIS Set Lock"; + case 0x2b87: return "CSIS Rank"; + + /* MCS chars */ + case 0x2b93: return "MCS Player Name"; + case 0x2b94: return "MCS Icon Obj ID"; + case 0x2b95: return "MCS Icon URL"; + case 0x2b96: return "MCS Track Changed"; + case 0x2b97: return "MCS Track Title"; + case 0x2b98: return "MCS Track Duration"; + case 0x2b99: return "MCS Track Position"; + case 0x2b9a: return "MCS Playback Speed"; + case 0x2b9b: return "MCS Seeking Speed"; + case 0x2b9c: return "MCS Track Seg Obj ID"; + case 0x2b9d: return "MCS Cur Track Obj ID"; + case 0x2b9e: return "MCS Next Track Obj ID"; + case 0x2b9f: return "MCS Parent Grp Obj ID"; + case 0x2ba0: return "MCS Cur Grp Obj ID"; + case 0x2ba1: return "MCS Playing Order"; + case 0x2ba2: return "MCS Playing Orders"; + case 0x2ba3: return "MCS Media State"; + case 0x2ba4: return "MCS Media CP"; + case 0x2ba5: return "MCS Media CP Opcodes"; + case 0x2ba6: return "MCS Search Res Obj ID"; + case 0x2ba7: return "MCS Search CP"; + + /* TBS chars */ + case 0x2bb3: return "TBS Provider Name"; + case 0x2bb4: return "TBS UCI"; + case 0x2bb5: return "TBS Technology"; + case 0x2bb6: return "TBS URI List"; + case 0x2bb7: return "TBS Signal Strength"; + case 0x2bb8: return "TBS Signal Interval"; + case 0x2bb9: return "TBS List Cur Calls"; + case 0x2bbb: return "TBS Status Flags"; + case 0x2bbc: return "TBS Incoming URI"; + case 0x2bbd: return "TBS Call State"; + case 0x2bbe: return "TBS Call CP"; + case 0x2bbf: return "TBS Optional Opcodes"; + case 0x2bc0: return "TBS Term Reason"; + case 0x2bc1: return "TBS Incoming Call"; + case 0x2bc2: return "TBS Friendly Name"; + + /* MICS / ASCS / BASS chars */ + case 0x2bc3: return "MICS Mute"; + case 0x2bc4: return "ASCS ASE Snk"; + case 0x2bc5: return "ASCS ASE Src"; + case 0x2bc6: return "ASCS ASE CP"; + case 0x2bc7: return "BASS CP"; + case 0x2bc8: return "BASS Recv State"; + + /* PACS chars */ + case 0x2bc9: return "PACS Snk"; + case 0x2bca: return "PACS Snk Loc"; + case 0x2bcb: return "PACS Src"; + case 0x2bcc: return "PACS Src Loc"; + case 0x2bcd: return "PACS Avail Ctx"; + case 0x2bce: return "PACS Sup Ctx"; + + default: return NULL; + } +} + static void example_audio_gatt_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg) { char buf[BLE_UUID_STR_LEN]; + const char *name; switch (ctxt->op) { case BLE_GATT_REGISTER_OP_SVC: - ESP_LOGI(TAG, "Register svc %s, handle %u", + name = example_audio_uuid16_name(ctxt->svc.svc_def->uuid); + ESP_LOGI(TAG, "svc %-20s (%s), handle %u", + name ? name : "?", ble_uuid_to_str(ctxt->svc.svc_def->uuid, buf), ctxt->svc.handle); break; case BLE_GATT_REGISTER_OP_CHR: - ESP_LOGI(TAG, "Register chr %s, handles %u/%u", + name = example_audio_uuid16_name(ctxt->chr.chr_def->uuid); + ESP_LOGI(TAG, "chr %-20s (%s), handles %u/%u", + name ? name : "?", ble_uuid_to_str(ctxt->chr.chr_def->uuid, buf), ctxt->chr.def_handle, ctxt->chr.val_handle); break; case BLE_GATT_REGISTER_OP_DSC: - ESP_LOGI(TAG, "Register dsc %s, handle %u", - ble_uuid_to_str(ctxt->dsc.dsc_def->uuid, buf), ctxt->dsc.handle); + name = example_audio_uuid16_name(ctxt->dsc.dsc_def->uuid); + ESP_LOGI(TAG, "dsc %-20s (%s), handle %u", + name ? name : "?", + ble_uuid_to_str(ctxt->dsc.dsc_def->uuid, buf), + ctxt->dsc.handle); break; default: @@ -103,14 +240,12 @@ esp_err_t bluetooth_init(void) ble_hs_cfg.reset_cb = example_audio_on_reset; ble_hs_cfg.sync_cb = example_audio_on_sync; ble_hs_cfg.store_status_cb = ble_store_util_status_rr; -#if 0 - ble_hs_cfg.sm_our_key_dist = 1; - ble_hs_cfg.sm_their_key_dist = 1; + ble_hs_cfg.sm_our_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID; + ble_hs_cfg.sm_their_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID; ble_hs_cfg.sm_sc = 1; ble_hs_cfg.sm_mitm = 0; ble_hs_cfg.sm_bonding = 1; ble_hs_cfg.sm_io_cap = BLE_SM_IO_CAP_NO_IO; -#endif /* XXX Need to have template for store */ ble_store_config_init(); 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 993740d23fe..84fc1728e5e 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 @@ -18,11 +18,14 @@ #include "esp_ble_audio_common_api.h" -#define TAG "UTILS" - #define LOG_GREEN "\033[0;" "32" "m" #define LOG_RESET "\033[0m" +/* Packet count between throughput summary logs. + * At 10 ms SDU interval this is ~60 s; at 7.5 ms ~45 s. + */ +#define LOG_INTERVAL_PACKETS 6000 + int example_audio_gap_event_cb(struct ble_gap_event *event, void *arg) { if (event->type == BLE_GAP_EVENT_EXT_DISC || @@ -50,148 +53,363 @@ static void print_hex(const uint8_t *ptr, size_t len) } } -static bool print_cb(uint8_t type, const uint8_t *data, - uint8_t data_len, void *user_data) +static void append_audio_contexts(char *buf, size_t buf_size, uint16_t ctx_bitmap) { - const char *str = (const char *)user_data; + static const char *const names[] = { + "Unspecified", "Conversational", "Media", "Game", + "Instructional", "Voice Assistants", "Live", "Sound Effects", + "Notifications", "Ringtone", "Alerts", "Emergency Alarm", + }; + size_t pos = 0; - printf(LOG_GREEN "I (%lu) %s: %s, type 0x%02x len %u data " LOG_RESET, - esp_log_timestamp(), TAG, str, type, data_len); - print_hex(data, data_len); - printf("\n"); + buf[0] = '\0'; + if (ctx_bitmap == 0) { + snprintf(buf, buf_size, "None"); + return; + } + for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); i++) { + if (ctx_bitmap & (1U << i)) { + int n = snprintf(buf + pos, buf_size - pos, "%s%s", + pos == 0 ? "" : ", ", names[i]); + if (n < 0 || (size_t)n >= buf_size - pos) { + break; + } + pos += n; + } + } +} +static void copy_to_cstr(char *dst, size_t dst_size, const uint8_t *src, uint8_t src_len) +{ + size_t n = (src_len < dst_size - 1) ? src_len : dst_size - 1; + + memcpy(dst, src, n); + dst[n] = '\0'; +} + +static void phy_to_str(char *buf, size_t buf_size, uint8_t phy) +{ + size_t pos = 0; + + buf[0] = '\0'; + if (phy & ESP_BLE_ISO_PHY_1M) { + pos += snprintf(buf + pos, buf_size - pos, "1M"); + } + if (phy & ESP_BLE_ISO_PHY_2M) { + pos += snprintf(buf + pos, buf_size - pos, "%s2M", pos ? " " : ""); + } + if (phy & ESP_BLE_ISO_PHY_CODED) { + pos += snprintf(buf + pos, buf_size - pos, "%sCoded", pos ? " " : ""); + } + if (pos == 0) { + snprintf(buf, buf_size, "0x%02x", phy); + } +} + +static bool print_meta_cb(uint8_t type, const uint8_t *data, + uint8_t data_len, void *user_data) +{ + const char *tag = (const char *)user_data; + char buf[128]; + + switch (type) { + case ESP_BLE_AUDIO_METADATA_TYPE_PREF_CONTEXT: + case ESP_BLE_AUDIO_METADATA_TYPE_STREAM_CONTEXT: { + uint16_t ctx = (data_len >= 2) + ? (uint16_t)data[0] | ((uint16_t)data[1] << 8) + : 0; + append_audio_contexts(buf, sizeof(buf), ctx); + ESP_LOGI(tag, "| %-19s: %s", + type == ESP_BLE_AUDIO_METADATA_TYPE_PREF_CONTEXT + ? "Preferred Context" : "Streaming Context", + buf); + break; + } + case ESP_BLE_AUDIO_METADATA_TYPE_PROGRAM_INFO: + copy_to_cstr(buf, sizeof(buf), data, data_len); + ESP_LOGI(tag, "| %-19s: %s", "Program Info", buf); + break; + case ESP_BLE_AUDIO_METADATA_TYPE_LANG: + copy_to_cstr(buf, sizeof(buf), data, data_len); + ESP_LOGI(tag, "| %-19s: %s", "Language", buf); + break; + case ESP_BLE_AUDIO_METADATA_TYPE_CCID_LIST: { + size_t pos = 0; + buf[0] = '\0'; + for (uint8_t i = 0; i < data_len; i++) { + int n = snprintf(buf + pos, sizeof(buf) - pos, "%s%u", + pos == 0 ? "" : " ", data[i]); + if (n < 0 || (size_t)n >= sizeof(buf) - pos) { + break; + } + pos += n; + } + ESP_LOGI(tag, "| %-19s: %s", "CCID List", buf); + break; + } + case ESP_BLE_AUDIO_METADATA_TYPE_PARENTAL_RATING: + ESP_LOGI(tag, "| %-19s: %u", "Parental Rating", + data_len >= 1 ? data[0] : 0); + break; + case ESP_BLE_AUDIO_METADATA_TYPE_PROGRAM_INFO_URI: + copy_to_cstr(buf, sizeof(buf), data, data_len); + ESP_LOGI(tag, "| %-19s: %s", "Program Info URI", buf); + break; + case ESP_BLE_AUDIO_METADATA_TYPE_AUDIO_STATE: + ESP_LOGI(tag, "| %-19s: 0x%02x", "Audio Active State", + data_len >= 1 ? data[0] : 0); + break; + case ESP_BLE_AUDIO_METADATA_TYPE_BROADCAST_IMMEDIATE: + ESP_LOGI(tag, "| %-19s: (set)", "Broadcast Immediate"); + break; + case ESP_BLE_AUDIO_METADATA_TYPE_ASSISTED_LISTENING_STREAM: + ESP_LOGI(tag, "| %-19s: 0x%02x", "Assisted Listening", + data_len >= 1 ? data[0] : 0); + break; + case ESP_BLE_AUDIO_METADATA_TYPE_BROADCAST_NAME: + copy_to_cstr(buf, sizeof(buf), data, data_len); + ESP_LOGI(tag, "| %-19s: %s", "Broadcast Name", buf); + break; + case ESP_BLE_AUDIO_METADATA_TYPE_EXTENDED: + case ESP_BLE_AUDIO_METADATA_TYPE_VENDOR: + default: + printf(LOG_GREEN "I (%lu) %s: | meta[type 0x%02x len %u]: " LOG_RESET, + esp_log_timestamp(), tag, type, data_len); + print_hex(data, data_len); + printf("\n"); + break; + } return true; } -void example_print_codec_cfg(const esp_ble_audio_codec_cfg_t *codec_cfg) +void example_print_meta(const char *tag, const uint8_t *meta, size_t meta_len) { esp_err_t err; - ESP_LOGI(TAG, "codec_cfg, id 0x%02x cid 0x%04x vid 0x%04x count %u", + if (meta_len == 0) { + return; + } + + ESP_LOGI(tag, "| Metadata:"); + err = esp_ble_audio_data_parse(meta, meta_len, print_meta_cb, (void *)tag); + if (err) { + ESP_LOGE(tag, "Failed to parse meta"); + } +} + +static void print_raw_data(const char *tag, const uint8_t *data, size_t data_len) +{ + printf(LOG_GREEN "I (%lu) %s: | data: " LOG_RESET, esp_log_timestamp(), tag); + print_hex(data, data_len); + printf("\n"); +} + +static void print_codec_cfg_lc3(const char *tag, const esp_ble_audio_codec_cfg_t *codec_cfg) +{ + esp_ble_audio_codec_cfg_frame_dur_t frame_dur; + esp_ble_audio_location_t chan_allocation; + esp_ble_audio_codec_cfg_freq_t freq; + uint16_t octets_per_frame; + uint32_t frame_dur_us; + uint8_t frame_blocks; + uint32_t freq_hz; + esp_err_t err; + + err = esp_ble_audio_codec_cfg_get_freq(codec_cfg, &freq); + if (err) { + ESP_LOGE(tag, "Failed to get frequency"); + return; + } + err = esp_ble_audio_codec_cfg_freq_to_freq_hz(freq, &freq_hz); + if (err) { + ESP_LOGE(tag, "Failed to get frequency hz"); + return; + } + ESP_LOGI(tag, "| %-19s: %lu Hz", "Frequency", freq_hz); + + err = esp_ble_audio_codec_cfg_get_frame_dur(codec_cfg, &frame_dur); + if (err) { + ESP_LOGE(tag, "Failed to get frame duration"); + return; + } + err = esp_ble_audio_codec_cfg_frame_dur_to_frame_dur_us(frame_dur, &frame_dur_us); + if (err) { + ESP_LOGE(tag, "Failed to get frame duration us"); + return; + } + ESP_LOGI(tag, "| %-19s: %lu us", "Frame Duration", frame_dur_us); + + err = esp_ble_audio_codec_cfg_get_chan_allocation(codec_cfg, &chan_allocation, false); + if (err) { + ESP_LOGE(tag, "Failed to get channel allocation"); + return; + } + ESP_LOGI(tag, "| %-19s: 0x%08lx", "Channel allocation", chan_allocation); + + err = esp_ble_audio_codec_cfg_get_octets_per_frame(codec_cfg, &octets_per_frame); + if (err) { + ESP_LOGE(tag, "Failed to get octets per frame"); + return; + } + ESP_LOGI(tag, "| %-19s: %u bytes", "Octets per frame", octets_per_frame); + + err = esp_ble_audio_codec_cfg_get_frame_blocks_per_sdu(codec_cfg, &frame_blocks, true); + if (err) { + ESP_LOGE(tag, "Failed to get frame blocks per sdu"); + return; + } + ESP_LOGI(tag, "| %-19s: %u", "Frames per SDU", frame_blocks); +} + +static void print_codec_cap_lc3(const char *tag, const esp_ble_audio_codec_cap_t *codec_cap) +{ + static const uint16_t freq_khz[] = { + 8, 11, 16, 22, 24, 32, 44, 48, 88, 96, 176, 192, 384, + }; + esp_ble_audio_codec_cap_freq_t freq; + esp_ble_audio_codec_cap_frame_dur_t frame_dur; + esp_ble_audio_codec_cap_chan_count_t chan_count; + esp_ble_audio_codec_octets_per_codec_frame_t octets; + uint8_t max_frames; + char buf[64]; + int pos; + esp_err_t err; + + err = esp_ble_audio_codec_cap_get_freq(codec_cap, &freq); + if (err) { + ESP_LOGE(tag, "Failed to get supported frequencies"); + return; + } + pos = 0; + buf[0] = '\0'; + for (size_t i = 0; i < sizeof(freq_khz) / sizeof(freq_khz[0]); i++) { + if (freq & (1U << i)) { + int n = snprintf(buf + pos, sizeof(buf) - pos, "%s%u", + pos == 0 ? "" : " ", freq_khz[i]); + if (n < 0 || (size_t)n >= sizeof(buf) - pos) { + break; + } + pos += n; + } + } + ESP_LOGI(tag, "| %-19s: %s kHz", "Frequencies", buf); + + err = esp_ble_audio_codec_cap_get_frame_dur(codec_cap, &frame_dur); + if (err) { + ESP_LOGE(tag, "Failed to get supported frame durations"); + return; + } + pos = 0; + buf[0] = '\0'; + if (frame_dur & ESP_BLE_AUDIO_CODEC_CAP_DURATION_7_5) { + pos += snprintf(buf + pos, sizeof(buf) - pos, "7.5"); + } + if (frame_dur & ESP_BLE_AUDIO_CODEC_CAP_DURATION_10) { + pos += snprintf(buf + pos, sizeof(buf) - pos, "%s10", + pos == 0 ? "" : " "); + } + ESP_LOGI(tag, "| %-19s: %s ms", "Frame Durations", buf); + + err = esp_ble_audio_codec_cap_get_supported_audio_chan_counts(codec_cap, &chan_count, true); + if (err) { + ESP_LOGE(tag, "Failed to get supported channel counts"); + return; + } + pos = 0; + buf[0] = '\0'; + for (size_t i = 0; i < 8; i++) { + if (chan_count & (1U << i)) { + int n = snprintf(buf + pos, sizeof(buf) - pos, "%s%u", + pos == 0 ? "" : " ", (unsigned)(i + 1)); + if (n < 0 || (size_t)n >= sizeof(buf) - pos) { + break; + } + pos += n; + } + } + ESP_LOGI(tag, "| %-19s: %s", "Channel Counts", buf); + + err = esp_ble_audio_codec_cap_get_octets_per_frame(codec_cap, &octets); + if (err) { + ESP_LOGE(tag, "Failed to get supported octets per frame"); + return; + } + ESP_LOGI(tag, "| %-19s: %u-%u bytes", "Octets per Frame", octets.min, octets.max); + + err = esp_ble_audio_codec_cap_get_max_codec_frames_per_sdu(codec_cap, &max_frames, true); + if (err) { + ESP_LOGE(tag, "Failed to get max frames per SDU"); + return; + } + ESP_LOGI(tag, "| %-19s: %u", "Max Frames per SDU", max_frames); +} + +void example_print_codec_cfg(const char *tag, const esp_ble_audio_codec_cfg_t *codec_cfg) +{ + ESP_LOGI(tag, "Codec Configuration:"); + ESP_LOGI(tag, "| id 0x%02x cid 0x%04x vid 0x%04x len %u", codec_cfg->id, codec_cfg->cid, codec_cfg->vid, codec_cfg->data_len); if (codec_cfg->id == ESP_BLE_ISO_CODING_FORMAT_LC3) { - esp_ble_audio_codec_cfg_frame_dur_t frame_dur; - esp_ble_audio_location_t chan_allocation; - esp_ble_audio_codec_cfg_freq_t freq; - uint16_t octets_per_frame; - uint32_t frame_dur_us; - uint8_t frame_blocks; - uint32_t freq_hz; - - /* LC3 uses the generic LTV format - other codecs might do as well */ - - err = esp_ble_audio_data_parse(codec_cfg->data, codec_cfg->data_len, print_cb, "data"); - if (err) { - ESP_LOGE(TAG, "Failed to parse codec_cfg data"); - return; - } - - err = esp_ble_audio_codec_cfg_get_freq(codec_cfg, &freq); - if (err) { - ESP_LOGE(TAG, "Failed to get frequency"); - return; - } - - err = esp_ble_audio_codec_cfg_freq_to_freq_hz(freq, &freq_hz); - if (err) { - ESP_LOGE(TAG, "Failed to get frequency hz"); - return; - } - - ESP_LOGI(TAG, "Frequency: %lu Hz", freq_hz); - - err = esp_ble_audio_codec_cfg_get_frame_dur(codec_cfg, &frame_dur); - if (err) { - ESP_LOGE(TAG, "Failed to get frame duration"); - return; - } - - err = esp_ble_audio_codec_cfg_frame_dur_to_frame_dur_us(frame_dur, &frame_dur_us); - if (err) { - ESP_LOGE(TAG, "Failed to get frame duration us"); - return; - } - - ESP_LOGI(TAG, "Frame Duration: %lu us", frame_dur_us); - - err = esp_ble_audio_codec_cfg_get_chan_allocation(codec_cfg, &chan_allocation, false); - if (err) { - ESP_LOGE(TAG, "Failed to get channel allocation"); - return; - } - - ESP_LOGI(TAG, "Channel allocation: 0x%08lx", chan_allocation); - - err = esp_ble_audio_codec_cfg_get_octets_per_frame(codec_cfg, &octets_per_frame); - if (err) { - ESP_LOGE(TAG, "Failed to get octets per frame"); - return; - } - - ESP_LOGI(TAG, "Octets per frame: %u", octets_per_frame); - - err = esp_ble_audio_codec_cfg_get_frame_blocks_per_sdu(codec_cfg, &frame_blocks, true); - if (err) { - ESP_LOGE(TAG, "Failed to get frame blocks per sdu"); - return; - } - - ESP_LOGI(TAG, "Frames per SDU: %u", frame_blocks); + print_codec_cfg_lc3(tag, codec_cfg); } else { - print_hex(codec_cfg->data, codec_cfg->data_len); + print_raw_data(tag, codec_cfg->data, codec_cfg->data_len); } - err = esp_ble_audio_data_parse(codec_cfg->meta, codec_cfg->meta_len, print_cb, "meta"); - if (err) { - ESP_LOGE(TAG, "Failed to parse codec_cfg meta"); - return; - } + example_print_meta(tag, codec_cfg->meta, codec_cfg->meta_len); } -void example_print_codec_cap(const esp_ble_audio_codec_cap_t *codec_cap) +void example_print_codec_cap(const char *tag, const esp_ble_audio_codec_cap_t *codec_cap) { - esp_err_t err; - - ESP_LOGI(TAG, "codec_cap, id 0x%02x cid 0x%04x vid 0x%04x count %u", + ESP_LOGI(tag, "Codec Capability:"); + ESP_LOGI(tag, "| id 0x%02x cid 0x%04x vid 0x%04x len %u", codec_cap->id, codec_cap->cid, codec_cap->vid, codec_cap->data_len); if (codec_cap->id == ESP_BLE_ISO_CODING_FORMAT_LC3) { - err = esp_ble_audio_data_parse(codec_cap->data, codec_cap->data_len, print_cb, "data"); - if (err) { - ESP_LOGE(TAG, "Failed to parse codec_cap data"); - return; - } + print_codec_cap_lc3(tag, codec_cap); } else { - printf(LOG_GREEN "I (%lu) %s: data: " LOG_RESET, esp_log_timestamp(), TAG); - print_hex(codec_cap->data, codec_cap->data_len); - printf("\n"); + print_raw_data(tag, codec_cap->data, codec_cap->data_len); } - err = esp_ble_audio_data_parse(codec_cap->meta, codec_cap->meta_len, print_cb, "meta"); - if (err) { - ESP_LOGE(TAG, "Failed to parse codec_cap meta"); - return; - } + example_print_meta(tag, codec_cap->meta, codec_cap->meta_len); } -void example_print_qos(const esp_ble_audio_bap_qos_cfg_t *qos) +/* Prints QoS fields only. Caller is expected to print a context header + * ending with ':' so the '| ' lines below naturally attach to that header. + */ +void example_print_qos(const char *tag, const esp_ble_audio_bap_qos_cfg_t *qos) { - ESP_LOGI(TAG, "QoS: interval %u framing 0x%02x phy 0x%02x", qos->interval, qos->framing, qos->phy); - ESP_LOGI(TAG, " sdu %u rtn %u latency %u pd %u", qos->sdu, qos->rtn, qos->latency, qos->pd); + char phy_str[16]; + + phy_to_str(phy_str, sizeof(phy_str), qos->phy); + + ESP_LOGI(tag, "| %-8s: %u us", "interval", qos->interval); + ESP_LOGI(tag, "| %-8s: %s", "framing", qos->framing ? "framed" : "unframed"); + ESP_LOGI(tag, "| %-8s: %s", "phy", phy_str); + ESP_LOGI(tag, "| %-8s: %u bytes", "sdu", qos->sdu); + ESP_LOGI(tag, "| %-8s: %u retransmission(s)", "rtn", qos->rtn); + ESP_LOGI(tag, "| %-8s: %u ms", "latency", qos->latency); + ESP_LOGI(tag, "| %-8s: %u us", "pd", qos->pd); } -void example_print_qos_pref(const esp_ble_audio_bap_qos_cfg_pref_t *pref) +/* Prints QoS Preference fields only. Same expectation as example_print_qos. */ +void example_print_qos_pref(const char *tag, const esp_ble_audio_bap_qos_cfg_pref_t *pref) { - ESP_LOGI(TAG, "QoS pref: unframed %s, phy %u, rtn %u, latency %u", - pref->unframed_supported ? "supported" : "not supported", - pref->phy, pref->rtn, pref->latency); - ESP_LOGI(TAG, " pd_min %u, pd_max %u", - pref->pd_min, pref->pd_max); - ESP_LOGI(TAG, " pref_pd_min %u, pref_pd_max %u", - pref->pref_pd_min, pref->pref_pd_max); + char phy_str[16]; + + phy_to_str(phy_str, sizeof(phy_str), pref->phy); + + ESP_LOGI(tag, "| %-11s: %s", "unframed", + pref->unframed_supported ? "supported" : "not supported"); + ESP_LOGI(tag, "| %-11s: %s", "phy", phy_str); + ESP_LOGI(tag, "| %-11s: %u retransmission(s)", "rtn", pref->rtn); + ESP_LOGI(tag, "| %-11s: %u ms", "latency", pref->latency); + ESP_LOGI(tag, "| %-11s: %u us", "pd_min", pref->pd_min); + ESP_LOGI(tag, "| %-11s: %u us", "pd_max", pref->pd_max); + ESP_LOGI(tag, "| %-11s: %u us", "pref_pd_min", pref->pref_pd_min); + ESP_LOGI(tag, "| %-11s: %u us", "pref_pd_max", pref->pref_pd_max); } bool example_is_substring(const char *substr, const char *str) @@ -276,8 +494,7 @@ int example_audio_tx_scheduler_stop(example_audio_tx_scheduler_t *scheduler) void example_audio_tx_scheduler_on_sent(example_audio_tx_scheduler_t *scheduler, const esp_ble_iso_tx_cb_info_t *info, const char *tag, - const char *obj_name, - const void *obj) + const char *obj_name) { size_t drift_count = 0; @@ -293,9 +510,9 @@ void example_audio_tx_scheduler_on_sent(example_audio_tx_scheduler_t *scheduler, scheduler->drift += drift_count; scheduler->count++; - if (scheduler->count % 1000 == 0) { - ESP_LOGI(tag, "Transmitted %u ISO data packets (%s %p)", - scheduler->count, (obj_name ? obj_name : ""), obj); + if (scheduler->count % LOG_INTERVAL_PACKETS == 0) { + ESP_LOGI(tag, "[%s] TX: %u packets", + obj_name ? obj_name : "ISO", scheduler->count); } } @@ -314,8 +531,7 @@ void example_audio_rx_metrics_reset(example_audio_rx_metrics_t *metrics) void example_audio_rx_metrics_on_recv(const esp_ble_iso_recv_info_t *info, example_audio_rx_metrics_t *metrics, const char *tag, - const char *obj_name, - const void *obj) + const char *obj_name) { assert(info); assert(metrics); @@ -337,8 +553,8 @@ void example_audio_rx_metrics_on_recv(const esp_ble_iso_recv_info_t *info, metrics->recv_count++; - if (metrics->recv_count % 1000 == 0) { - ESP_LOGI(tag, "Received %u ISO data packets (%s %p)", - metrics->recv_count, (obj_name ? obj_name : ""), obj); + if (metrics->recv_count % LOG_INTERVAL_PACKETS == 0) { + ESP_LOGI(tag, "[%s] RX: %u packets", + obj_name ? obj_name : "ISO", metrics->recv_count); } } 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 d083222ab71..04a457f5219 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 @@ -53,13 +53,15 @@ int example_audio_gap_event_cb(struct ble_gap_event *event, void *arg); -void example_print_codec_cfg(const esp_ble_audio_codec_cfg_t *codec_cfg); +void example_print_codec_cfg(const char *tag, const esp_ble_audio_codec_cfg_t *codec_cfg); -void example_print_codec_cap(const esp_ble_audio_codec_cap_t *codec_cap); +void example_print_codec_cap(const char *tag, const esp_ble_audio_codec_cap_t *codec_cap); -void example_print_qos(const esp_ble_audio_bap_qos_cfg_t *qos); +void example_print_meta(const char *tag, const uint8_t *meta, size_t meta_len); -void example_print_qos_pref(const esp_ble_audio_bap_qos_cfg_pref_t *pref); +void example_print_qos(const char *tag, const esp_ble_audio_bap_qos_cfg_t *qos); + +void example_print_qos_pref(const char *tag, const esp_ble_audio_bap_qos_cfg_pref_t *pref); bool example_is_substring(const char *substr, const char *str); @@ -104,15 +106,13 @@ int example_audio_tx_scheduler_stop(example_audio_tx_scheduler_t *scheduler); void example_audio_tx_scheduler_on_sent(example_audio_tx_scheduler_t *scheduler, const esp_ble_iso_tx_cb_info_t *info, const char *tag, - const char *obj_name, - const void *obj); + const char *obj_name); void example_audio_rx_metrics_reset(example_audio_rx_metrics_t *metrics); void example_audio_rx_metrics_on_recv(const esp_ble_iso_recv_info_t *info, example_audio_rx_metrics_t *metrics, const char *tag, - const char *obj_name, - const void *obj); + const char *obj_name); #endif /* BLE_AUDIO_EXAMPLE_UTILS_H_ */ diff --git a/examples/bluetooth/esp_ble_audio/tmap/bmr/README.md b/examples/bluetooth/esp_ble_audio/tmap/bmr/README.md index dd5c3eef9d0..3078f91144e 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/bmr/README.md +++ b/examples/bluetooth/esp_ble_audio/tmap/bmr/README.md @@ -5,58 +5,102 @@ (See the README.md file in the upper level `examples` directory for more information about examples.) -This example implements the **Telephone and Media Audio Profile (TMAP) Broadcast Media Receiver (BMR)** role. The BMR scans for broadcast sources that advertise both the Broadcast Audio service (broadcast ID) and the TMAP role TMAS with **BMS (Broadcast Media Sender)**. When such a source is found, it establishes periodic advertising sync, creates a BAP Broadcast Sink, and synchronizes to the BIG to receive broadcast audio. It also registers as a **VCP Volume Renderer** (Volume Control Profile) for local volume and mute. The BMR can work with a TMAP BMS or any BAP Broadcast Source that includes the TMAP BMS role in advertising (e.g. a device running the [broadcast_source](../../bap/broadcast_source) or [CAP initiator](../../cap/initiator) in broadcast mode, if configured as BMS). +## Overview -The implementation uses the NimBLE host stack with ISO and LE Audio support, ESP-BLE-AUDIO (TMAP BMR role, VCP volume renderer, PACS sink, BAP broadcast sink, BAP scan delegator). PACS advertises LC3 sink capability (e.g. 48 kHz, 10 ms, mono, media context). After startup the device starts scanning; on matching scan results it creates PA sync, then creates the broadcast sink and syncs to the BIS streams; received audio is counted in the stream recv callback. +This example implements the **Telephony and Media Audio Profile (TMAP) Broadcast Media Receiver (BMR)** role. The device scans for non-connectable extended advertisers that carry both the Broadcast Audio Announcement service (with a 24-bit broadcast ID) and the TMAS service with the **BMS** role bit set, then synchronizes to periodic advertising and the BIG to receive broadcast audio. + +The implementation runs on the **NimBLE** host stack and uses ESP-BLE-AUDIO for TMAP role registration, the BAP broadcast sink, the BAP scan delegator, the PACS sink (LC3 capability: 48 kHz, 10 ms, 1 channel, 100 octets/frame, media context — sized for the 48_2_1 preset that the paired BMS sends), and a VCP **Volume Renderer** (initial volume 10, unmuted, step 1). Up to `CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT` streams are pre-allocated and reused for every BIG sync; received SDUs are accounted for via `example_audio_rx_metrics_*`. ## Requirements -* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4) -* A broadcast source that advertises as TMAP BMS (Broadcast Media Sender) and sends broadcast audio (e.g. another board running a broadcast source example with TMAP BMS role) +* A board with BLE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31) +* Peer device running the paired example -## How to Use Example +## Configuration -Before project configuration and build, set the correct chip target: +```bash +idf.py menuconfig +``` + +No build-time options — runtime defaults are baked into source. + +### Security & Pairing + +NimBLE host security is inherited from `../../common_components/example_init/ble_audio_example_init.c`: LE Secure Connections with bonding enabled, no MITM, **Just-Works** pairing (`BLE_SM_IO_CAP_NO_IO`). The BMR receives broadcast (unencrypted) audio, so these settings only apply if a peer (e.g. a Broadcast Assistant) opens an ATT connection to the scan delegator / VCP renderer. + +## Build & Flash ```bash idf.py set-target esp32h4 -``` - -### Build and Flash - -Run the following to build, flash and monitor: - -```bash idf.py -p PORT flash monitor ``` -(To exit the serial monitor, type ``Ctrl-]``.) - -See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF. +(Exit serial monitor with `Ctrl-]`.) ## Example Flow -1. **Initialization**: NVS, Bluetooth stack, and LE Audio common layer (`esp_ble_audio_common_init`) with GAP callback. Register TMAP role BMR (`esp_ble_audio_tmap_register(ESP_BLE_AUDIO_TMAP_ROLE_BMR)`). Register VCP Volume Renderer (volume step, mute, volume, state/flags callbacks). Initialize BAP broadcast sink: register PACS (sink only), register broadcast sink callbacks (base_recv, syncable), register PACS sink capability (LC3), register scan delegator callbacks, set stream ops (started, stopped, recv). Start the audio stack. -2. **Scanning**: Start extended scanning. On scan result, parse for Broadcast Audio UUID (broadcast ID) and TMAS with BMS role; if both found and not already syncing, create periodic advertising sync with the source. -3. **PA sync**: When PA sync is established, stop scanning, create the BAP broadcast sink for the sync handle and broadcast ID. When BASE is received (base_recv), extract BIS indexes; when syncable callback is invoked, sync to the BIG with the chosen BIS and stream pointers. -4. **Streaming**: When streams start, reset packet counters. Received ISO SDUs are delivered in the stream recv callback; the example counts valid, error, lost, and zero-length SDU and logs periodically. -5. **VCP**: The Volume Renderer exposes volume and mute state; VCS state/flags callbacks log volume and mute changes when queried. +1. Initialize NVS, the Bluetooth controller/host, and the LE Audio common layer with a GAP app callback that handles `EXT_SCAN_RECV`, `PA_SYNC`, and `PA_SYNC_LOST`. +2. Register the TMAP role as `ESP_BLE_AUDIO_TMAP_ROLE_BMR`, then initialize the VCP Volume Renderer (`vcp_vol_renderer_init`) and the BAP broadcast sink (`bap_broadcast_sink_init`: PACS sink + sink location, broadcast sink callbacks, LC3 sink capability, scan delegator, stream ops on each pre-allocated stream). +3. Start the audio stack and begin passive extended scanning at 100 ms / 100 ms with `BLE_HS_FOREVER` duration. +4. On each scan result, parse service-data: capture broadcast ID from Broadcast Audio Announcement, set `tmap_bms_found` when TMAS carries the BMS role; if both are present and no PA sync is in progress, create the periodic-advertising sync. +5. On `PA_SYNC` success, cancel scanning, log the sync handle, and create the broadcast sink for the sync handle and broadcast ID. +6. The `base_recv` callback extracts the BIS index bitfield masked by the available stream count; the `syncable` callback then calls `esp_ble_audio_bap_broadcast_sink_sync` with the stream pointer array. +7. Per-stream `started` callback resets RX metrics; `recv` callback feeds each SDU into `example_audio_rx_metrics_on_recv` (tracking valid/error/lost/zero-length counts); `stopped` logs the reason. +8. On `PA_SYNC_LOST` matching the active sync handle, delete the broadcast sink and re-enter scanning. -## Example Output +## Expected Log +TAG is `TMAP_BMR`. + +Init: ``` -I (xxx) TMAP_BMR: Found TMAP BMS -I (xxx) TMAP_BMR: PA synced, handle 0x... status 0x00 -I (xxx) TMAP_BMR: PA sync ... synced with broadcast ID 0x... +I (xxx) TMAP_BMR: VCP volume renderer initialized +I (xxx) TMAP_BMR: BAP broadcast sink initialized +I (xxx) TMAP_BMR: Scanning for broadcaster... +``` + +Scan and PA sync: +``` +I (xxx) TMAP_BMR: Found TMAP BMS, starting PA sync (broadcast ID 0x......) +I (xxx) TMAP_BMR: PA synced: handle .. sid .. phy .. peer xx:xx:xx:xx:xx:xx +I (xxx) TMAP_BMR: PA sync .. synced with broadcast ID 0x...... I (xxx) TMAP_BMR: Broadcast source PA synced, waiting for BASE I (xxx) TMAP_BMR: BASE received, creating broadcast sink -I (xxx) TMAP_BMR: Stream 0x... started -I (xxx) TMAP_BMR: Received 1000(...) ISO data packets (stream 0x...) -... ``` -If PA sync is lost: +Streaming: +``` +I (xxx) TMAP_BMR: [SNK #0] Stream started +``` +(Subsequent per-packet metrics are emitted by `example_audio_rx_metrics_on_recv` under the `SNK #N` label.) +Stop / sync loss: ``` -I (xxx) TMAP_BMR: PA sync lost, handle 0x... reason ... +I (xxx) TMAP_BMR: [SNK #0] Stream stopped, reason 0x.. +I (xxx) TMAP_BMR: PA sync lost: sync_handle .. reason 0x.. +I (xxx) TMAP_BMR: PA sync .. lost with reason .. ``` + +VCP (when remote queries volume/flags): +``` +I (xxx) TMAP_BMR: VCS volume .., mute .. +I (xxx) TMAP_BMR: VCS flags 0x.. +``` + +Warnings (malformed adv): +``` +W (xxx) TMAP_BMR: Invalid ad size .. (uuid) +W (xxx) TMAP_BMR: Invalid ad size .. (Broadcast ID) +W (xxx) TMAP_BMR: Invalid ad size .. (tmap role) +``` + +## Peer Pairing + +Run [bms](../bms/) on a second board. + +1. Flash and boot this BMR; it initializes VCP and PACS, then enters extended scanning. +2. Boot the BMS peer; it advertises with TMAS BMS role and broadcast ID `0x123456` plus periodic advertising carrying the BASE. +3. This BMR matches both service-data entries on a single scan result and creates the PA sync; on success it cancels scanning and creates the broadcast sink. +4. `base_recv` derives the BIS index bitfield, then `syncable` triggers `esp_ble_audio_bap_broadcast_sink_sync` with the pre-allocated stream pointers. +5. Stream `started` fires, RX metrics reset, and `recv` accumulates valid/error/lost/zero-length SDU counters as the BMS sends. +6. If the BMS stops or moves out of range, PA sync loss tears down the sink and the BMR resumes scanning automatically. diff --git a/examples/bluetooth/esp_ble_audio/tmap/bmr/main/bap_broadcast_sink.c b/examples/bluetooth/esp_ble_audio/tmap/bmr/main/bap_broadcast_sink.c index 41b6c458731..532ab6c8ec9 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/bmr/main/bap_broadcast_sink.c +++ b/examples/bluetooth/esp_ble_audio/tmap/bmr/main/bap_broadcast_sink.c @@ -8,6 +8,7 @@ #include #include +#include #include #include "tmap_bmr.h" @@ -34,9 +35,11 @@ static uint8_t codec_data[] = ESP_BLE_AUDIO_CODEC_CAP_LC3_DATA( ESP_BLE_AUDIO_CODEC_CAP_FREQ_48KHZ, /* Sampling frequency 48kHz */ ESP_BLE_AUDIO_CODEC_CAP_DURATION_10, /* Frame duration 10ms */ - ESP_BLE_AUDIO_CODEC_CAP_CHAN_COUNT_SUPPORT(1), /* Supported channels 1 */ - 40, /* Minimum 40 octets per frame */ - 60, /* Maximum 60 octets per frame */ + ESP_BLE_AUDIO_CODEC_CAP_CHAN_COUNT_SUPPORT(1), /* Supported channels 1 (mono) */ + /* SDU size range covers the 48_2_1 LC3 broadcast preset (100 octets/frame) + * that the paired BMS sends. */ + 100, /* Minimum 100 octets per frame */ + 100, /* Maximum 100 octets per frame */ 1); /* Maximum 1 codec frame per SDU */ static uint8_t codec_meta[] = @@ -54,25 +57,38 @@ static uint32_t bis_index_bitfield; static example_audio_rx_metrics_t rx_metrics; +static int stream_index(const esp_ble_audio_bap_stream_t *stream) +{ + for (size_t i = 0; i < ARRAY_SIZE(streams); i++) { + if (&streams[i] == stream) { + return (int)i; + } + } + return -1; +} + static void stream_started_cb(esp_ble_audio_bap_stream_t *stream) { - ESP_LOGI(TAG, "Stream %p started", stream); + ESP_LOGI(TAG, "[SNK #%d] Stream started", stream_index(stream)); example_audio_rx_metrics_reset(&rx_metrics); } static void stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason) { - ESP_LOGI(TAG, "Stream %p stopped, reason 0x%02x", stream, reason); + ESP_LOGI(TAG, "[SNK #%d] Stream stopped, reason 0x%02x", + stream_index(stream), reason); } static void 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 name[24]; + snprintf(name, sizeof(name), "SNK #%d", stream_index(stream)); rx_metrics.last_sdu_len = len; - example_audio_rx_metrics_on_recv(info, &rx_metrics, TAG, "stream", stream); + example_audio_rx_metrics_on_recv(info, &rx_metrics, TAG, name); } static esp_ble_audio_bap_stream_ops_t stream_ops = { @@ -190,8 +206,6 @@ static bool scan_check_and_sync_broadcast(uint8_t type, const uint8_t *data, tmap_role = sys_get_le16(data + sizeof(uuid_val)); if (tmap_role & ESP_BLE_AUDIO_TMAP_ROLE_BMS) { - ESP_LOGI(TAG, "Found TMAP BMS"); - tmap_bms_found = true; return true; } @@ -221,6 +235,9 @@ void bap_broadcast_scan_recv(esp_ble_audio_gap_app_event_t *event) if (broadcast_id != ESP_BLE_AUDIO_BAP_INVALID_BROADCAST_ID && tmap_bms_found && pa_syncing == false) { + ESP_LOGI(TAG, "Found TMAP BMS, starting PA sync (broadcast ID 0x%06lx)", + broadcast_id); + addr.type = event->ext_scan_recv.addr.type; memcpy(addr.a.val, event->ext_scan_recv.addr.val, sizeof(addr.a.val)); @@ -300,7 +317,7 @@ int bap_broadcast_sink_scan(void) return err; } - ESP_LOGI(TAG, "Extended scan started"); + ESP_LOGI(TAG, "Scanning for broadcaster..."); return 0; } diff --git a/examples/bluetooth/esp_ble_audio/tmap/bmr/main/main.c b/examples/bluetooth/esp_ble_audio/tmap/bmr/main/main.c index 0f4bb8681be..7d14d92e8fd 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/bmr/main/main.c +++ b/examples/bluetooth/esp_ble_audio/tmap/bmr/main/main.c @@ -23,22 +23,19 @@ static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event) static void pa_sync(esp_ble_audio_gap_app_event_t *event) { - ESP_LOGI(TAG, "PA synced:"); - ESP_LOGI(TAG, "sync_handle 0x%04x status 0x%02x addr %02x:%02x:%02x:%02x:%02x:%02x " - "sid %u adv_phy %u per_adv_itvl 0x%04x adv_ca %u", - event->pa_sync.sync_handle, event->pa_sync.status, + ESP_LOGI(TAG, "PA synced: handle %u sid %u phy %u peer %02x:%02x:%02x:%02x:%02x:%02x", + event->pa_sync.sync_handle, event->pa_sync.sid, + event->pa_sync.adv_phy, event->pa_sync.addr.val[5], event->pa_sync.addr.val[4], event->pa_sync.addr.val[3], event->pa_sync.addr.val[2], - event->pa_sync.addr.val[1], event->pa_sync.addr.val[0], - event->pa_sync.sid, event->pa_sync.adv_phy, - event->pa_sync.per_adv_itvl, event->pa_sync.adv_ca); + event->pa_sync.addr.val[1], event->pa_sync.addr.val[0]); bap_broadcast_pa_sync(event); } static void pa_sync_lost(esp_ble_audio_gap_app_event_t *event) { - ESP_LOGI(TAG, "PA sync lost: sync_handle 0x%04x reason 0x%02x", + ESP_LOGI(TAG, "PA sync lost: sync_handle %u reason 0x%02x", event->pa_sync_lost.sync_handle, event->pa_sync_lost.reason); bap_broadcast_pa_lost(event); diff --git a/examples/bluetooth/esp_ble_audio/tmap/bmr/sdkconfig.defaults b/examples/bluetooth/esp_ble_audio/tmap/bmr/sdkconfig.defaults index c7a2951ee4c..0d9fb343304 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/bmr/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/tmap/bmr/sdkconfig.defaults @@ -6,6 +6,7 @@ CONFIG_BT_ENABLED=y CONFIG_BT_BLUEDROID_ENABLED=n CONFIG_BT_NIMBLE_ENABLED=y CONFIG_BT_NIMBLE_EXT_ADV=y +CONFIG_BT_NIMBLE_NVS_PERSIST=y CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 CONFIG_BT_NIMBLE_MAX_CCCDS=30 CONFIG_BT_NIMBLE_ISO=y diff --git a/examples/bluetooth/esp_ble_audio/tmap/bms/README.md b/examples/bluetooth/esp_ble_audio/tmap/bms/README.md index 5645de0935f..1efa6a495ef 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/bms/README.md +++ b/examples/bluetooth/esp_ble_audio/tmap/bms/README.md @@ -5,54 +5,92 @@ (See the README.md file in the upper level `examples` directory for more information about examples.) -This example implements the **Telephone and Media Audio Profile (TMAP) Broadcast Media Sender (BMS)** role. The BMS acts as a BAP Broadcast Source: it starts non-connectable extended advertising and periodic advertising. Extended advertising includes the **TMAS (Telephone and Media Audio Service)** UUID with the **BMS** role and the Broadcast Audio Announcement service (broadcast ID and device name); periodic advertising carries the BASE (Broadcast Audio Source Endpoint). After creating the broadcast source and starting the BIG, it sends broadcast audio on the BIS at the configured interval. TMAP BMR receivers (e.g. the [bmr](../bmr) example) that scan for both TMAS BMS and Broadcast Audio will discover and sync to this source. +## Overview -The implementation uses the NimBLE host stack with ISO and BAP support, ESP-BLE-AUDIO (TMAP BMS role, CAP initiator broadcast). It creates a single subgroup with one stream using the LC3 48_2_1 broadcast preset (48 kHz, stereo, media context). A periodic TX scheduler (based on `k_work_delayable`) drives ISO SDU transmission in the ISO task context; the stream sent callback logs packet counts and drift. All parameters (broadcast ID, broadcast code, device name) are hardcoded in the source and can be changed by editing the source code constants. +This example implements the **Telephony and Media Audio Profile (TMAP) Broadcast Media Sender (BMS)** role. The device acts as a non-connectable BAP Broadcast Source: extended advertising carries the TMAS service UUID with the BMS role flag, the Broadcast Audio Announcement service with a 24-bit broadcast ID (`0x123456`), and the complete local name (`"TMAP Broadcast Source"`); periodic advertising carries the encoded BASE. + +The implementation runs on the **NimBLE** host stack and uses the ESP-BLE-AUDIO library for the TMAP role registration and the CAP initiator broadcast pieces. A single subgroup with one stream is configured from the LC3 `48_2_1` broadcast preset (front-left location, media context). After the broadcast source is created and the BIG is started, a periodic TX scheduler (`example_audio_tx_scheduler_*`) drives ISO SDU transmission at the stream's QoS interval, and a sent callback tracks packet counts and drift. ## Requirements -* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4) -* Optionally, a TMAP BMR or BAP Broadcast Sink (e.g. the [bmr](../bmr) example) to receive the broadcast stream +* A board with BLE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31) +* Peer device running the paired example -## How to Use Example +## Configuration -Before project configuration and build, set the correct chip target: +```bash +idf.py menuconfig +``` + +No build-time options — runtime defaults are baked into source. + +### Security & Pairing + +NimBLE host security is inherited from `../../common_components/example_init/ble_audio_example_init.c`: LE Secure Connections with bonding enabled, no MITM, **Just-Works** pairing (`BLE_SM_IO_CAP_NO_IO`). The BMS broadcast itself is non-connectable and unencrypted, so these settings only apply if a peer ever opens an ATT connection. + +## Build & Flash ```bash idf.py set-target esp32h4 -``` - -### Build and Flash - -Run the following to build, flash and monitor: - -```bash idf.py -p PORT flash monitor ``` -(To exit the serial monitor, type ``Ctrl-]``.) - -See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF. +(Exit serial monitor with `Ctrl-]`.) ## Example Flow -1. **Initialization**: NVS, Bluetooth stack, and LE Audio common layer (`esp_ble_audio_common_init`). Register TMAP role BMS (`esp_ble_audio_tmap_register(ESP_BLE_AUDIO_TMAP_ROLE_BMS)`). Initialize CAP initiator broadcast: register stream ops (started, stopped, sent), create broadcast audio TX scheduler. -2. **Setup**: Create the broadcast source with one subgroup and one stream (LC3 48_2_1 preset), build extended adv data (TMAS + BMS role, Broadcast Audio UUID + broadcast ID, device name) and periodic adv data (BASE). Start periodic and extended advertising, add the ext adv to the BIG, then start the broadcast audio. The stream started callback starts the periodic TX scheduler and sends the first SDU. -3. **Streaming**: The TX scheduler, based on `k_work_delayable`, fires at the stream QoS interval in the ISO task context and sends ISO SDUs; the sent callback counts packets and logs periodically. Drift and untransmitted packets are reported in the callback. Note that the scheduler timer resolution is in milliseconds, which may not match the exact SDU interval for all configurations. -4. **Optional**: `cap_initiator_update` updates broadcast metadata; `cap_initiator_stop` stops and deletes the broadcast source. +1. Initialize NVS, the Bluetooth controller/host, and the LE Audio common layer (no GAP/GATT app callbacks). +2. Register the TMAP role as `ESP_BLE_AUDIO_TMAP_ROLE_BMS` and initialize the CAP initiator (register stream ops, init TX scheduler). +3. Start the audio stack, then build the create params (1 subgroup × 1 stream, LC3 48_2_1 preset, sequential packing, no encryption) and create the broadcast source. +4. Configure and start non-connectable extended advertising (TMAS+BMS role, Broadcast Audio + broadcast ID `0x123456`, device name) and periodic advertising carrying the BASE. +5. Add the advertising handle to the BIG and start broadcast audio; on stream-started, allocate the SDU buffer and arm the TX scheduler at `qos->interval`. +6. The scheduler callback fills SDUs with the rolling sequence number and calls `esp_ble_audio_cap_stream_send`; the sent callback updates RX-side metrics via `example_audio_tx_scheduler_on_sent`. +7. `cap_initiator_update` and `cap_initiator_stop` are exposed for runtime metadata refresh and teardown. -## Example Output +## Expected Log +TAG is `TMAP_BMS`. + +Init: +``` +I (xxx) TMAP_BMS: CAP initiator initialized +``` + +Setup: ``` I (xxx) TMAP_BMS: Creating broadcast source -I (xxx) TMAP_BMS: Extended adv instance 0 started -I (xxx) TMAP_BMS: Stream 0x... started -I (xxx) TMAP_BMS: Transmitted 1000 ISO data packets (stream 0x...) -... +I (xxx) TMAP_BMS: Advertising started (handle 0) +I (xxx) TMAP_BMS: CAP initiator setup ``` -If the controller reports drift or untransmitted packets: +Streaming: +``` +I (xxx) TMAP_BMS: [SRC #0] Stream started +``` +(Subsequent per-packet metrics are emitted by `example_audio_tx_scheduler_on_sent` under the `SRC #0` label.) +Stop / teardown: ``` -W (xxx) TMAP_BMS: ISO data packets ... drifted and ... not txd +I (xxx) TMAP_BMS: [SRC #0] Stream stopped, reason 0x.. +I (xxx) TMAP_BMS: [SRC #0] ISO disconnected, reason 0x.. +I (xxx) TMAP_BMS: Advertising stopped (handle 0) ``` + +Errors (representative): +``` +E (xxx) TMAP_BMS: Failed to create broadcast source, err .. +E (xxx) TMAP_BMS: Failed to start broadcast source, err .. +E (xxx) TMAP_BMS: [SRC #0] Invalid stream qos +E (xxx) TMAP_BMS: [SRC #0] Failed to alloc TX buffer, SDU .. +``` + +## Peer Pairing + +Run [bmr](../bmr/) on a second board. + +1. Flash and boot the BMR peer; it starts scanning for TMAP BMS advertisers. +2. Boot this BMS; it advertises with TMAS BMS role and broadcast ID `0x123456` and starts periodic advertising carrying the BASE. +3. The BMR matches the TMAS BMS role plus Broadcast Audio UUID, creates a PA sync, and waits for the BASE. +4. After BASE is received the BMR creates a broadcast sink and syncs to the BIG using the BIS index bitfield. +5. The BMS stream-started callback fires; its TX scheduler begins emitting SDUs that the BMR receives in its stream `recv` callback. +6. Stopping or power-cycling the BMS triggers PA sync loss on the BMR, which deletes the sink and resumes scanning. diff --git a/examples/bluetooth/esp_ble_audio/tmap/bms/main/cap_initiator.c b/examples/bluetooth/esp_ble_audio/tmap/bms/main/cap_initiator.c index 334f783dc77..d39c47626ef 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/bms/main/cap_initiator.c +++ b/examples/bluetooth/esp_ble_audio/tmap/bms/main/cap_initiator.c @@ -86,17 +86,17 @@ static void broadcast_started_cb(esp_ble_audio_bap_stream_t *stream) { esp_err_t err; - ESP_LOGI(TAG, "Stream %p started", stream); + ESP_LOGI(TAG, "[SRC #0] Stream started"); if (stream->qos == NULL || stream->qos->sdu == 0) { - ESP_LOGE(TAG, "Invalid stream qos"); + ESP_LOGE(TAG, "[SRC #0] Invalid stream qos"); return; } if (iso_data == NULL) { iso_data = calloc(1, stream->qos->sdu); if (iso_data == NULL) { - ESP_LOGE(TAG, "Failed to alloc TX buffer, SDU %u", stream->qos->sdu); + ESP_LOGE(TAG, "[SRC #0] Failed to alloc TX buffer, SDU %u", stream->qos->sdu); return; } } @@ -107,7 +107,7 @@ static void broadcast_started_cb(esp_ble_audio_bap_stream_t *stream) /* Note: esp timer is not accurate enough */ err = example_audio_tx_scheduler_start(&tx_scheduler, stream->qos->interval); if (err) { - ESP_LOGE(TAG, "Failed to start tx scheduler, err %d", err); + ESP_LOGE(TAG, "[SRC #0] Scheduler start failed, err %d", err); return; } @@ -118,11 +118,11 @@ static void broadcast_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t rea { esp_err_t err; - ESP_LOGI(TAG, "Stream %p stopped, reason 0x%02x", stream, reason); + ESP_LOGI(TAG, "[SRC #0] Stream stopped, reason 0x%02x", reason); err = example_audio_tx_scheduler_stop(&tx_scheduler); if (err) { - ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err); + ESP_LOGE(TAG, "[SRC #0] Scheduler stop failed, err %d", err); } if (iso_data != NULL) { @@ -135,11 +135,11 @@ static void broadcast_disconnected_cb(esp_ble_audio_bap_stream_t *stream, uint8_ { esp_err_t err; - ESP_LOGI(TAG, "Stream %p disconnected, reason 0x%02x", stream, reason); + ESP_LOGI(TAG, "[SRC #0] ISO disconnected, reason 0x%02x", reason); err = example_audio_tx_scheduler_stop(&tx_scheduler); if (err) { - ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err); + ESP_LOGE(TAG, "[SRC #0] Scheduler stop failed, err %d", err); } if (iso_data != NULL) { @@ -150,7 +150,7 @@ static void broadcast_disconnected_cb(esp_ble_audio_bap_stream_t *stream, uint8_ static void broadcast_sent_cb(esp_ble_audio_bap_stream_t *stream, void *user_data) { - example_audio_tx_scheduler_on_sent(&tx_scheduler, user_data, TAG, "stream", stream); + example_audio_tx_scheduler_on_sent(&tx_scheduler, user_data, TAG, "SRC #0"); } static esp_ble_audio_bap_stream_ops_t broadcast_stream_ops = { @@ -335,7 +335,7 @@ static int ext_adv_start(void) goto end; } - ESP_LOGI(TAG, "Extended adv instance %u started", ADV_HANDLE); + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); end: if (ext_data) { @@ -349,23 +349,26 @@ end: static int ext_adv_stop(void) { + int ret = 0; int err; err = ble_gap_periodic_adv_stop(ADV_HANDLE); if (err) { ESP_LOGE(TAG, "Failed to stop per advertising, err %d", err); + ret = err; } err = ble_gap_ext_adv_stop(ADV_HANDLE); if (err) { ESP_LOGE(TAG, "Failed to stop ext advertising, err %d", err); + ret = err; } - if (err == 0) { - ESP_LOGI(TAG, "Extended adv instance %u stopped", ADV_HANDLE); + if (ret == 0) { + ESP_LOGI(TAG, "Advertising stopped (handle %u)", ADV_HANDLE); } - return err; + return ret; } int cap_initiator_setup(void) diff --git a/examples/bluetooth/esp_ble_audio/tmap/central/README.md b/examples/bluetooth/esp_ble_audio/tmap/central/README.md index 7ea9b314a73..d57df2ecf95 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/central/README.md +++ b/examples/bluetooth/esp_ble_audio/tmap/central/README.md @@ -5,60 +5,122 @@ (See the README.md file in the upper level `examples` directory for more information about examples.) -This example implements the **Telephone and Media Audio Profile (TMAP) Central** roles: **Call Gateway (CG)** and **Unicast Media Sender (UMS)**. The central scans for devices that advertise the TMAS (Telephone and Media Audio Service) with the **UMR (Unicast Media Receiver)** role, connects to the first such device, performs pairing, MTU exchange, and GATT service discovery, then runs TMAP discovery and VCP discovery. After TMAP discovery completes, it sets up unicast audio streams (CAP initiator): discovers ASEs, configures codec (e.g. LC3 48_2_1), sets QoS, enables and connects streams, then starts them and sends audio to the peer’s sink. It also acts as **VCP Volume Controller** (volume/mute on the peer), **MCP server** (Media Control Profile / media proxy), and **CCP server** (Call Control Profile with TBS – Telephone Bearer Service) for call originate/terminate. Run it with a device that advertises TMAP UMR (e.g. a TMAP peripheral or CAP acceptor with unicast). +## Overview -The implementation uses the NimBLE host stack with ISO and LE Audio support, ESP-BLE-AUDIO (TMAP CG+UMS, CAP initiator unicast client, VCP volume controller, TBS, CSIP set coordinator, MCP/MCS/MCC, media proxy). Optional Kconfig: device name. +This example takes the **TMAP Call Gateway (CG)** and **Unicast Media Sender (UMS)** roles, registered together via `esp_ble_audio_tmap_register(ESP_BLE_AUDIO_TMAP_ROLE_CG | ESP_BLE_AUDIO_TMAP_ROLE_UMS)`. It scans for connectable extended advertising that carries TMAS service data with the **UMR** role bit set, connects to the first match, pairs, exchanges MTU, and then drives TMAP and VCP discovery before bringing up unicast audio. + +The host stack is NimBLE. The example uses the ESP-BLE-AUDIO library pieces for: CAP initiator with the BAP Unicast Client (LC3 preset 48_2_1, sink direction, FRONT_LEFT, MEDIA context), VCP Volume Controller, MCP server backed by the media proxy player, and CCP server registering a single GTBS bearer (`Generic TBS`, UCI `un000`, `tel,wechat` URI schemes, 5G technology). A periodic TX scheduler in the ISO task feeds dummy ISO SDUs filled with the sequence number. Device name is set to `TMAP Central`. ## Requirements -* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4) -* A device advertising TMAP UMR (Unicast Media Receiver), e.g. a CAP Acceptor or TMAP peripheral with unicast +* A board with BLE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31) +* Peer device running the paired example -## How to Use Example +## Configuration -Before project configuration and build, set the correct chip target: +```bash +idf.py menuconfig +``` + +No build-time options — runtime defaults are baked into source. + +### Security & Pairing + +Just-Works pairing (LE Secure Connections, no MITM, no I/O capability) with bonding enabled, inherited from `../../common_components/example_init/ble_audio_example_init.c`. + +## Build & Flash ```bash idf.py set-target esp32h4 -``` - -### Build and Flash - -Run the following to build, flash and monitor: - -```bash idf.py -p PORT flash monitor ``` -(To exit the serial monitor, type ``Ctrl-]``.) - -See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF. +(Exit serial monitor with `Ctrl-]`.) ## Example Flow -1. **Initialization**: NVS, Bluetooth stack, and LE Audio common layer (`esp_ble_audio_common_init`) with GAP and GATT callbacks. Register TMAP roles CG and UMS (`esp_ble_audio_tmap_register(ESP_BLE_AUDIO_TMAP_ROLE_CG | ESP_BLE_AUDIO_TMAP_ROLE_UMS)`). Initialize CAP initiator (unicast client, stream callbacks, TX timer). Initialize VCP volume controller. Initialize MCP server (media proxy). Initialize CCP server (TBS bearer, originate/terminate callbacks). Start the audio stack and set the device name. -2. **Scanning**: Start extended scanning. On scan result with connectable advertising, parse for TMAS with UMR role; when found, stop scan and connect to the peer. -3. **Connection**: On ACL connect, store connection handle and initiate pairing. On MTU change, start GATT service discovery. On discovery complete (and when MTU has been exchanged), start TMAP discovery and VCP discovery. -4. **TMAP discovery complete**: Call `cap_initiator_setup()` to create the unicast group, discover ASEs, configure codec, set QoS, enable and connect streams, then start streams. -5. **Unicast streaming**: When a sink-direction stream (TX from central to peer) is started, a periodic TX scheduler based on `k_work_delayable` runs in the ISO task context and sends ISO SDUs at the stream QoS interval; the sent callback logs packet counts. Source streams (RX) can be used to receive audio from the peer. Note that the scheduler timer resolution is in milliseconds, which may not match the exact SDU interval for all configurations. -6. **VCP**: After VCP discovery, the example can send mute/volume commands; state and flags callbacks log volume and mute. +1. `app_main` initializes NVS, Bluetooth, the audio common layer, and registers TMAP CG+UMS. +2. Sub-modules initialize in order: CAP initiator (with TX scheduler), VCP volume controller, MCP server (media proxy), CCP server (GTBS bearer). +3. The audio stack starts, the device name is set, and extended scanning begins. +4. Each connectable scan result is parsed for TMAS service data; if the peer's TMAP role contains UMR, scan is cancelled and a connection is created. +5. On ACL connect, security is initiated; on security change, an MTU exchange is requested. +6. After the MTU exchange and GATT discovery both complete, TMAP and VCP discovery start. +7. TMAP discovery completion triggers `cap_initiator_setup` → CAS discover → sink and source ASE discovery → unicast group create → unicast audio start. +8. When a sink stream reaches "started", the TX scheduler is started at the QoS interval and `unicast_audio_tx` sends ISO SDUs filled with the sequence number. +9. After VCP discovery, the volume state is read to sync the change counter, then a mute command is issued from the state callback. +10. On disconnect, the unicast group is deleted and scanning restarts. -## Example Output +## Expected Log + +Initialization phase: ``` -I (xxx) TMAP_CEN: Found TMAS in peer adv data! -I (xxx) TMAP_CEN: connection established, status 0 -I (xxx) TMAP_CEN: gatt mtu change, conn_handle 1, mtu ... -I (xxx) TMAP_CEN: gattc disc cmpl, status 0, conn_handle 1 -I (xxx) TMAP_CEN: TMAP discovery done -I (xxx) TMAP_CEN: Configured stream 0x... -I (xxx) TMAP_CEN: Unicast stream 0x... started -I (xxx) TMAP_CEN: Transmitted 1000 ISO data packets (stream 0x...) -... +TMAP_CEN: CAP initiator initialized +TMAP_CEN: VCP volume controller initialized +TMAP_CEN: MCP server initialized +TMAP_CEN: Registered gtbs bearer +TMAP_CEN: Scanning for peripheral... ``` -If the connection is lost: +Scan and connect: ``` -I (xxx) TMAP_CEN: connection disconnected, reason 0x... +TMAP_CEN: Found TMAS in peer adv data! +TMAP_CEN: Connected: handle role peer +TMAP_CEN: Security: handle level bonded +TMAP_CEN: MTU updated: handle mtu +TMAP_CEN: Service discovery started: handle +TMAP_CEN: Service discovery complete: handle status 0 ``` + +TMAP / VCP / CAP setup: + +``` +TMAP_CEN: TMAP discovery done +TMAP_CEN: CAP initiator setup +TMAP_CEN: Found CAS +TMAP_CEN: [SNK #0] Endpoint discovered +TMAP_CEN: Sink discover complete +TMAP_CEN: Source discover complete +TMAP_CEN: Created unicast group +TMAP_CEN: Started unicast audio +TMAP_CEN: VCP volume controller discovering +TMAP_CEN: VCP discovery done +TMAP_CEN: VCP state cb done, volume mute +``` + +Unicast streaming phase: + +``` +TMAP_CEN: [SNK #0] Stream configured, QoS preference: +TMAP_CEN: [SNK #0] QoS set +TMAP_CEN: [SNK #0] Stream enabled +TMAP_CEN: [SNK #0] Stream connected +TMAP_CEN: [SNK #0] Stream started +TMAP_CEN: Streaming, interval , length +TMAP_CEN: Unicast start completed +``` + +Call control / disconnect: + +``` +TMAP_CEN: CCP: Placing call to remote with id to +TMAP_CEN: CCP: Call terminated for id with reason +TMAP_CEN: [SNK #0] Stream stopped, reason 0x +TMAP_CEN: [SNK #0] ISO disconnected, reason 0x +TMAP_CEN: Disconnected: handle reason 0x +TMAP_CEN: Deleted unicast group +``` + +Tag is `TMAP_CEN`. + +## Peer Pairing + +Run [tmap_peripheral](../peripheral/) on a second board. + +1. Flash the peripheral and let it start extended advertising. +2. Flash and start this central; it scans for TMAS+UMR. +3. The central connects, pairs, and exchanges MTU with the peripheral. +4. TMAP discovery completes on both sides; the central runs CAP unicast setup against the peripheral's ASCS. +5. The central starts the sink stream and feeds ISO SDUs to the peripheral at the QoS interval. +6. The central can drive the peripheral's volume via VCP and accept TBS originate/terminate from the peripheral. diff --git a/examples/bluetooth/esp_ble_audio/tmap/central/main/cap_initiator.c b/examples/bluetooth/esp_ble_audio/tmap/central/main/cap_initiator.c index 24c265dfae8..5330b91a3f1 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/central/main/cap_initiator.c +++ b/examples/bluetooth/esp_ble_audio/tmap/central/main/cap_initiator.c @@ -8,6 +8,7 @@ #include #include +#include #include #include "esp_timer.h" @@ -33,20 +34,59 @@ ESP_BLE_AUDIO_BAP_LC3_UNICAST_PRESET_48_2_1_DEFINE(unicast_preset_48_2_1, ESP_BLE_AUDIO_LOCATION_FRONT_LEFT, ESP_BLE_AUDIO_CONTEXT_TYPE_MEDIA); +static const char *dir_str(esp_ble_audio_dir_t dir) +{ + return dir == ESP_BLE_AUDIO_DIR_SINK ? "SNK" : "SRC"; +} + +static bool stream_is_sink(const esp_ble_audio_bap_stream_t *stream) +{ + for (size_t i = 0; i < ARRAY_SIZE(unicast_sink_streams); i++) { + if (&unicast_sink_streams[i].bap_stream == stream) { + return true; + } + } + return false; +} + +static const char *stream_dir_str(const esp_ble_audio_bap_stream_t *stream) +{ + return stream_is_sink(stream) ? "SNK" : "SRC"; +} + +static int stream_index(const esp_ble_audio_bap_stream_t *stream) +{ + for (size_t i = 0; i < ARRAY_SIZE(unicast_sink_streams); i++) { + if (&unicast_sink_streams[i].bap_stream == stream) { + return (int)i; + } + } + for (size_t i = 0; i < ARRAY_SIZE(unicast_source_streams); i++) { + if (&unicast_source_streams[i].bap_stream == stream) { + return (int)i; + } + } + return -1; +} + static void unicast_stream_configured_cb(esp_ble_audio_bap_stream_t *stream, const esp_ble_audio_bap_qos_cfg_pref_t *pref) { - ESP_LOGI(TAG, "Unicast stream %p configured", stream); + ESP_LOGI(TAG, "[%s #%d] Stream configured, QoS preference:", + stream_dir_str(stream), stream_index(stream)); + example_print_qos_pref(TAG, pref); } static void unicast_stream_qos_set_cb(esp_ble_audio_bap_stream_t *stream) { - ESP_LOGI(TAG, "Unicast stream %p QoS set", stream); + ESP_LOGI(TAG, "[%s #%d] QoS set", + stream_dir_str(stream), stream_index(stream)); } static void unicast_stream_enabled_cb(esp_ble_audio_bap_stream_t *stream) { - ESP_LOGI(TAG, "Unicast stream %p enabled", stream); + ESP_LOGI(TAG, "[%s #%d] Stream enabled", + stream_dir_str(stream), stream_index(stream)); } static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream) @@ -54,7 +94,8 @@ static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream) esp_ble_audio_bap_ep_info_t ep_info = {0}; esp_err_t err; - ESP_LOGI(TAG, "Unicast stream %p started", stream); + ESP_LOGI(TAG, "[%s #%d] Stream started", + stream_dir_str(stream), stream_index(stream)); err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info); if (err) { @@ -99,12 +140,14 @@ static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream) static void unicast_stream_metadata_updated_cb(esp_ble_audio_bap_stream_t *stream) { - ESP_LOGI(TAG, "Unicast stream %p metadata updated", stream); + ESP_LOGI(TAG, "[%s #%d] Metadata updated", + stream_dir_str(stream), stream_index(stream)); } static void unicast_stream_disabled_cb(esp_ble_audio_bap_stream_t *stream) { - ESP_LOGI(TAG, "Unicast stream %p disabled", stream); + ESP_LOGI(TAG, "[%s #%d] Stream disabled", + stream_dir_str(stream), stream_index(stream)); } static void unicast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason) @@ -112,7 +155,8 @@ static void unicast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_ esp_ble_audio_bap_ep_info_t ep_info = {0}; esp_err_t err; - ESP_LOGI(TAG, "Unicast stream %p stopped, reason 0x%02x", stream, reason); + ESP_LOGI(TAG, "[%s #%d] Stream stopped, reason 0x%02x", + stream_dir_str(stream), stream_index(stream), reason); err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info); if (err) { @@ -135,17 +179,23 @@ static void unicast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_ static void unicast_stream_released_cb(esp_ble_audio_bap_stream_t *stream) { - ESP_LOGI(TAG, "Unicast stream %p released", stream); + ESP_LOGI(TAG, "[%s #%d] Stream released", + stream_dir_str(stream), stream_index(stream)); } static void unicast_stream_sent_cb(esp_ble_audio_bap_stream_t *stream, void *user_data) { - example_audio_tx_scheduler_on_sent(&tx_scheduler, user_data, TAG, "stream", stream); + char name[24]; + + snprintf(name, sizeof(name), "%s #%d", + stream_dir_str(stream), stream_index(stream)); + example_audio_tx_scheduler_on_sent(&tx_scheduler, user_data, TAG, name); } static void unicast_stream_connected_cb(esp_ble_audio_bap_stream_t *stream) { - ESP_LOGI(TAG, "Unicast stream %p connected", stream); + ESP_LOGI(TAG, "[%s #%d] Stream connected", + stream_dir_str(stream), stream_index(stream)); } static void unicast_stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason) @@ -153,7 +203,8 @@ static void unicast_stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, u esp_ble_audio_bap_ep_info_t ep_info = {0}; esp_err_t err; - ESP_LOGI(TAG, "Unicast stream %p disconnected, reason 0x%02x", stream, reason); + ESP_LOGI(TAG, "[%s #%d] ISO disconnected, reason 0x%02x", + stream_dir_str(stream), stream_index(stream), reason); err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info); if (err) { @@ -208,7 +259,7 @@ static void unicast_discovery_complete_cb(esp_ble_conn_t *conn, int err, return; } - ESP_LOGI(TAG, "Found CAS with CSIS %p", csis_inst); + ESP_LOGI(TAG, "Found CAS with CSIS"); } else { ESP_LOGI(TAG, "Found CAS"); } @@ -289,6 +340,17 @@ int unicast_group_delete(void) { int err; + /* The endpoint pointers become stale once the peer disconnects and are + * rediscovered on every reconnect. Clear them here so the next discovery + * does not find filled slots and warn "Unhandled endpoint". + */ + for (size_t i = 0; i < ARRAY_SIZE(unicast_sink_eps); i++) { + unicast_sink_eps[i] = NULL; + } + for (size_t i = 0; i < ARRAY_SIZE(unicast_source_eps); i++) { + unicast_source_eps[i] = NULL; + } + if (unicast_group == NULL) { return 0; } @@ -355,56 +417,64 @@ static void config_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_code_t rsp_code, esp_ble_audio_bap_ascs_reason_t reason) { - ESP_LOGI(TAG, "Config, stream %p rsp_code %u reason %u", stream, rsp_code, reason); + ESP_LOGI(TAG, "[%s #%d] Config response, rsp_code %u reason %u", + stream_dir_str(stream), stream_index(stream), rsp_code, reason); } static void qos_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_code_t rsp_code, esp_ble_audio_bap_ascs_reason_t reason) { - ESP_LOGI(TAG, "Qos, stream %p rsp_code %u reason %u", stream, rsp_code, reason); + ESP_LOGI(TAG, "[%s #%d] QoS response, rsp_code %u reason %u", + stream_dir_str(stream), stream_index(stream), rsp_code, reason); } static void enable_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_code_t rsp_code, esp_ble_audio_bap_ascs_reason_t reason) { - ESP_LOGI(TAG, "Enable, stream %p rsp_code %u reason %u", stream, rsp_code, reason); + ESP_LOGI(TAG, "[%s #%d] Enable response, rsp_code %u reason %u", + stream_dir_str(stream), stream_index(stream), rsp_code, reason); } static void start_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_code_t rsp_code, esp_ble_audio_bap_ascs_reason_t reason) { - ESP_LOGI(TAG, "Start, stream %p rsp_code %u reason %u", stream, rsp_code, reason); + ESP_LOGI(TAG, "[%s #%d] Start response, rsp_code %u reason %u", + stream_dir_str(stream), stream_index(stream), rsp_code, reason); } static void stop_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_code_t rsp_code, esp_ble_audio_bap_ascs_reason_t reason) { - ESP_LOGI(TAG, "Stop, stream %p rsp_code %u reason %u", stream, rsp_code, reason); + ESP_LOGI(TAG, "[%s #%d] Stop response, rsp_code %u reason %u", + stream_dir_str(stream), stream_index(stream), rsp_code, reason); } static void disable_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_code_t rsp_code, esp_ble_audio_bap_ascs_reason_t reason) { - ESP_LOGI(TAG, "Disable, stream %p rsp_code %u reason %u", stream, rsp_code, reason); + ESP_LOGI(TAG, "[%s #%d] Disable response, rsp_code %u reason %u", + stream_dir_str(stream), stream_index(stream), rsp_code, reason); } static void metadata_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_code_t rsp_code, esp_ble_audio_bap_ascs_reason_t reason) { - ESP_LOGI(TAG, "Metadata, stream %p rsp_code %u reason %u", stream, rsp_code, reason); + ESP_LOGI(TAG, "[%s #%d] Metadata response, rsp_code %u reason %u", + stream_dir_str(stream), stream_index(stream), rsp_code, reason); } static void release_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_code_t rsp_code, esp_ble_audio_bap_ascs_reason_t reason) { - ESP_LOGI(TAG, "Release, stream %p rsp_code %u reason %u", stream, rsp_code, reason); + ESP_LOGI(TAG, "[%s #%d] Release response, rsp_code %u reason %u", + stream_dir_str(stream), stream_index(stream), rsp_code, reason); } static void discover_cb(esp_ble_conn_t *conn, int err, esp_ble_audio_dir_t dir) @@ -446,7 +516,7 @@ static void discover_cb(esp_ble_conn_t *conn, int err, esp_ble_audio_dir_t dir) static void pac_record_cb(esp_ble_conn_t *conn, esp_ble_audio_dir_t dir, const esp_ble_audio_codec_cap_t *codec_cap) { - example_print_codec_cap(codec_cap); + example_print_codec_cap(TAG, codec_cap); } static void endpoint_cb(esp_ble_conn_t *conn, @@ -456,7 +526,7 @@ static void endpoint_cb(esp_ble_conn_t *conn, if (dir == ESP_BLE_AUDIO_DIR_SOURCE) { for (size_t i = 0; i < ARRAY_SIZE(unicast_source_eps); i++) { if (unicast_source_eps[i] == NULL) { - ESP_LOGI(TAG, "Source #%zu: ep %p", i, ep); + ESP_LOGI(TAG, "[SRC #%zu] Endpoint discovered", i); unicast_source_eps[i] = ep; return; } @@ -464,14 +534,14 @@ static void endpoint_cb(esp_ble_conn_t *conn, } else if (dir == ESP_BLE_AUDIO_DIR_SINK) { for (size_t i = 0; i < ARRAY_SIZE(unicast_sink_eps); i++) { if (unicast_sink_eps[i] == NULL) { - ESP_LOGI(TAG, "Sink #%zu: ep %p", i, ep); + ESP_LOGI(TAG, "[SNK #%zu] Endpoint discovered", i); unicast_sink_eps[i] = ep; return; } } } - ESP_LOGW(TAG, "Unhandled endpoint %p, dir %u", ep, dir); + ESP_LOGW(TAG, "[%s] Unhandled endpoint", dir_str(dir)); } static esp_ble_audio_bap_unicast_client_cb_t unicast_client_cbs = { 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 880848a0f2b..54a7c9d9f86 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/central/main/main.c +++ b/examples/bluetooth/esp_ble_audio/tmap/central/main/main.c @@ -91,7 +91,7 @@ static void ext_scan_start(void) return; } - ESP_LOGI(TAG, "Extended scan started"); + ESP_LOGI(TAG, "Scanning for peripheral..."); } static int conn_create(ble_addr_t *dst) @@ -214,30 +214,28 @@ static void acl_connect(esp_ble_audio_gap_app_event_t *event) int err; if (event->acl_connect.status) { - ESP_LOGE(TAG, "connection failed, status %d", event->acl_connect.status); + ESP_LOGE(TAG, "Connection failed, status %d", event->acl_connect.status); return; } - ESP_LOGI(TAG, "Conn established:"); - ESP_LOGI(TAG, "conn_handle 0x%04x status 0x%02x role %u peer %02x:%02x:%02x:%02x:%02x:%02x", - event->acl_connect.conn_handle, event->acl_connect.status, - event->acl_connect.role, event->acl_connect.dst.val[5], - event->acl_connect.dst.val[4], event->acl_connect.dst.val[3], - event->acl_connect.dst.val[2], event->acl_connect.dst.val[1], - event->acl_connect.dst.val[0]); + ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x", + event->acl_connect.conn_handle, event->acl_connect.role, + event->acl_connect.dst.val[5], event->acl_connect.dst.val[4], + event->acl_connect.dst.val[3], event->acl_connect.dst.val[2], + event->acl_connect.dst.val[1], event->acl_connect.dst.val[0]); + + default_conn_handle = event->acl_connect.conn_handle; err = pairing_start(event->acl_connect.conn_handle); if (err) { ESP_LOGE(TAG, "Failed to initiate security, err %d", err); return; } - - default_conn_handle = event->acl_connect.conn_handle; } static void acl_disconnect(esp_ble_audio_gap_app_event_t *event) { - ESP_LOGI(TAG, "Conn terminated: conn_handle 0x%04x reason 0x%02x", + ESP_LOGI(TAG, "Disconnected: handle %u reason 0x%02x", event->acl_disconnect.conn_handle, event->acl_disconnect.reason); default_conn_handle = CONN_HANDLE_INIT; @@ -255,19 +253,14 @@ 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); + ESP_LOGE(TAG, "Security change failed, status %d", event->security_change.status); return; } - ESP_LOGI(TAG, "Security change:"); - ESP_LOGI(TAG, "conn_handle 0x%04x status 0x%02x role %u sec_level %u bonded %u " - "peer %02x:%02x:%02x:%02x:%02x:%02x", - event->security_change.conn_handle, event->security_change.status, - event->security_change.role, event->security_change.sec_level, - event->security_change.bonded, event->security_change.dst.val[5], - event->security_change.dst.val[4], event->security_change.dst.val[3], - event->security_change.dst.val[2], event->security_change.dst.val[1], - event->security_change.dst.val[0]); + ESP_LOGI(TAG, "Security: handle %u level %u bonded %u", + event->security_change.conn_handle, + event->security_change.sec_level, + event->security_change.bonded); err = exchange_mtu(event->security_change.conn_handle); if (err) { @@ -300,22 +293,23 @@ static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event) { esp_err_t err; - ESP_LOGI(TAG, "gatt mtu change, conn_handle %u, mtu %u", + ESP_LOGI(TAG, "MTU updated: handle %u mtu %u", event->gatt_mtu_change.conn_handle, event->gatt_mtu_change.mtu); if (event->gatt_mtu_change.mtu < ESP_BLE_AUDIO_ATT_MTU_MIN) { - ESP_LOGW(TAG, "Invalid new mtu %u, shall be at least %u", + ESP_LOGW(TAG, "MTU %u below minimum %u", event->gatt_mtu_change.mtu, ESP_BLE_AUDIO_ATT_MTU_MIN); return; } err = esp_ble_audio_gattc_disc_start(event->gatt_mtu_change.conn_handle); if (err) { - ESP_LOGE(TAG, "Failed to start svc disc, err %d", err); + ESP_LOGE(TAG, "Failed to start service discovery, err %d", err); return; } - ESP_LOGI(TAG, "Start discovering gatt services"); + ESP_LOGI(TAG, "Service discovery started: handle %u", + event->gatt_mtu_change.conn_handle); /* Note: * MTU exchanged event may arrived after discover completed event. @@ -337,11 +331,11 @@ static void gattc_disc_cmpl(esp_ble_audio_gatt_app_event_t *event) { esp_err_t err; - ESP_LOGI(TAG, "gattc disc cmpl, status %u, conn_handle %u", - event->gattc_disc_cmpl.status, event->gattc_disc_cmpl.conn_handle); + ESP_LOGI(TAG, "Service discovery complete: handle %u status %u", + event->gattc_disc_cmpl.conn_handle, + event->gattc_disc_cmpl.status); if (event->gattc_disc_cmpl.status) { - ESP_LOGE(TAG, "gattc disc failed, status %u", event->gattc_disc_cmpl.status); return; } diff --git a/examples/bluetooth/esp_ble_audio/tmap/central/main/vcp_vol_ctlr.c b/examples/bluetooth/esp_ble_audio/tmap/central/main/vcp_vol_ctlr.c index 6ba7febb6f2..01116ba398e 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/central/main/vcp_vol_ctlr.c +++ b/examples/bluetooth/esp_ble_audio/tmap/central/main/vcp_vol_ctlr.c @@ -13,6 +13,12 @@ #include "tmap_central.h" static esp_ble_audio_vcp_vol_ctlr_t *vcp_vol_ctlr; +/* VCS control-point writes carry a change counter that must match the server's. + * After discovery the local counter is 0 while the server may have incremented it + * during a previous session (e.g. before the central rebooted). Read the state + * first to sync the counter, then issue the demo mute from the state callback. + */ +static bool initial_state_read_pending; static void vcs_discover_cb(esp_ble_audio_vcp_vol_ctlr_t *vol_ctlr, int err, uint8_t vocs_count, uint8_t aics_count) @@ -22,11 +28,13 @@ static void vcs_discover_cb(esp_ble_audio_vcp_vol_ctlr_t *vol_ctlr, int err, return; } - ESP_LOGI(TAG, "VCP discovery done, %p %p", vol_ctlr, vcp_vol_ctlr); + ESP_LOGI(TAG, "VCP discovery done"); - err = esp_ble_audio_vcp_vol_ctlr_mute(vol_ctlr); + initial_state_read_pending = true; + err = esp_ble_audio_vcp_vol_ctlr_read_state(vol_ctlr); if (err) { - ESP_LOGE(TAG, "Failed to send mute command, err %d", err); + ESP_LOGE(TAG, "Failed to read VCS state, err %d", err); + initial_state_read_pending = false; } } @@ -44,8 +52,19 @@ static void vcs_state_cb(esp_ble_audio_vcp_vol_ctlr_t *vol_ctlr, { if (err) { ESP_LOGE(TAG, "VCP state cb failed, err %d", err); - } else { - ESP_LOGI(TAG, "VCP state cb done, volume %u mute %u", volume, mute); + initial_state_read_pending = false; + return; + } + + ESP_LOGI(TAG, "VCP state cb done, volume %u mute %u", volume, mute); + + if (initial_state_read_pending) { + initial_state_read_pending = false; + + err = esp_ble_audio_vcp_vol_ctlr_mute(vol_ctlr); + if (err) { + ESP_LOGE(TAG, "Failed to send mute command, err %d", err); + } } } diff --git a/examples/bluetooth/esp_ble_audio/tmap/central/sdkconfig.defaults b/examples/bluetooth/esp_ble_audio/tmap/central/sdkconfig.defaults index 339e7447f3a..6658248e246 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/central/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/tmap/central/sdkconfig.defaults @@ -9,6 +9,7 @@ CONFIG_BT_ENABLED=y CONFIG_BT_BLUEDROID_ENABLED=n CONFIG_BT_NIMBLE_ENABLED=y CONFIG_BT_NIMBLE_EXT_ADV=y +CONFIG_BT_NIMBLE_NVS_PERSIST=y CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 CONFIG_BT_NIMBLE_MAX_CCCDS=40 CONFIG_BT_NIMBLE_ISO=y diff --git a/examples/bluetooth/esp_ble_audio/tmap/peripheral/README.md b/examples/bluetooth/esp_ble_audio/tmap/peripheral/README.md index eb226d30fc2..f402af71182 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/peripheral/README.md +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/README.md @@ -5,74 +5,123 @@ (See the README.md file in the upper level `examples` directory for more information about examples.) -This example implements the **Telephone and Media Audio Profile (TMAP) Peripheral** roles: **Call Terminal (CT)** and **Unicast Media Receiver (UMR)**. The peripheral advertises connectable extended advertising with ASCS, CAS, and **TMAS (Telephone and Media Audio Service)** with **UMR and CT** roles, plus targeted unicast announcement (sink/source contexts) and device name; optionally CSIS RSI when built as a set member (e.g. duo earbuds). A TMAP Central (e.g. the [central](../central) example) that scans for TMAS UMR will discover and connect to this device. After connection, the central performs TMAP discovery; the peripheral also runs TMAP discovery to detect peer roles (CG/UMS), then discovers TBS (Telephone Bearer Service) on the central for call control. The peripheral acts as **BAP Unicast Server** (responds to codec config, QoS, enable, start; receives/sends unicast audio), **VCP Volume Renderer** (local volume/mute), **TBS client** (originate/terminate calls via central), and **MCP controller** (Media Control). Run it together with the [central](../central) example on another device as the TMAP Central. +## Overview -The implementation uses the NimBLE host stack with ISO and LE Audio support, ESP-BLE-AUDIO (TMAP CT+UMR, CAP acceptor, BAP unicast server, VCP volume renderer, TBS client, optional CSIP set member, MCP/MCC). Optional Kconfig: earbuds type (single / duo with set member), device rank, earbud location, device name. +This example takes the **TMAP Call Terminal (CT)** and **Unicast Media Receiver (UMR)** roles, registered together via `esp_ble_audio_tmap_register(ESP_BLE_AUDIO_TMAP_ROLE_CT | ESP_BLE_AUDIO_TMAP_ROLE_UMR)`. It runs connectable extended advertising at a 200 ms interval that includes the GAP Earbud appearance, the ASCS/CAS/TMAS UUIDs, an ASCS targeted unicast announcement carrying sink and source contexts (UNSPECIFIED | CONVERSATIONAL | MEDIA | GAME | INSTRUCTIONAL), a CAS targeted announcement, the TMAS UMR|CT role payload, and the device name `tmap_peripheral`. When the Duo earbuds option is selected, a CSIS RSI is added to the advertisement. + +The host stack is NimBLE. The example uses the ESP-BLE-AUDIO library pieces for: BAP Unicast Server with PACS (LC3 cap with 16/32/48 kHz, 7.5/10 ms duration, 30–155 octets), VCP Volume Renderer (initial volume 10, unmuted, with VOCS+AICS instances built from Kconfig counts), TBS client (CCP Call Terminal — discovers GTBS, reads URI list, originates/terminates calls), MCC controller (reads player name, track title/duration/position, playback/seeking speed, playing order, media state, opcodes, CCID, plus a `mcp_send_cmd` for PLAY/PAUSE), and optionally CSIP Set Member with SIRK and rank from Kconfig. Sink streams are auto-started from the `enabled` callback. Device name is set to `TMAP Peripheral`. ## Requirements -* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4) -* Another device running the [central](../central) example as TMAP Central (CG + UMS) +* A board with BLE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31) +* Peer device running the paired example -## How to Use Example - -Before project configuration and build, set the correct chip target: - -```bash -idf.py set-target esp32h4 -``` - -### Configure the Project - -Open the configuration menu to set earbuds type, location, and device name: +## Configuration ```bash idf.py menuconfig ``` -Under **Example: TMAP Peripheral (CT & UMR)** you can set: +Under **Example: TMAP Peripheral (CT & UMR)**: -* **Earbuds type** — Single ear headset or Duo headset (duo enables CSIP set member and adds RSI to advertising) -* **Device rank in set** — Rank of this device in set (when duo is selected) -* **Earbud Location** — Left Ear or Right Ear +* **Earbuds type** — `Single ear headset` or `Duo headset`. Duo selects `BT_CSIP_SET_MEMBER` and `BT_CAP_ACCEPTOR_SET_MEMBER`, enabling CSIS and adding RSI to the advertisement. +* **Device rank in set** — integer 1–2, only when Duo is selected; written into the CSIS register parameters. +* **Earbud Location** — `Left Ear` or `Right Ear`. Adds `FRONT_LEFT` / `FRONT_RIGHT` to the PACS sink/source location bitmap. -### Build and Flash +### Security & Pairing -Run the following to build, flash and monitor: +Just-Works pairing (LE Secure Connections, no MITM, no I/O capability) with bonding enabled, inherited from `../../common_components/example_init/ble_audio_example_init.c`. + +## Build & Flash ```bash +idf.py set-target esp32h4 idf.py -p PORT flash monitor ``` -(To exit the serial monitor, type ``Ctrl-]``.) - -See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF. +(Exit serial monitor with `Ctrl-]`.) ## Example Flow -1. **Initialization**: NVS, Bluetooth stack, and LE Audio common layer (`esp_ble_audio_common_init`) with GAP and GATT callbacks. Register TMAP roles CT and UMR (`esp_ble_audio_tmap_register(ESP_BLE_AUDIO_TMAP_ROLE_CT | ESP_BLE_AUDIO_TMAP_ROLE_UMR)`). Optionally initialize CSIP set member and generate RSI for advertising. Initialize VCP volume renderer. Initialize BAP unicast server (PACS, ASCS callbacks, stream ops). Initialize CCP call control (TBS client). Initialize MCP controller. Start the audio stack (with optional CSIS instance when set member) and set the device name. -2. **Advertising**: Start connectable extended advertising with flags, appearance (earbud), ASCS/CAS/TMAS UUIDs, ASCS targeted + contexts, CAS targeted, TMAS with UMR|CT, device name, and optionally CSIS RSI. -3. **Central connection**: When the central connects, store the connection handle. On MTU change, start GATT service discovery. On discovery complete, start TMAP discovery (`tmap_discover_tmas`). -4. **TMAP discovery complete**: Store whether the peer is CG and/or UMS; start TBS discovery on the central (`ccp_discover_tbs`). The central will then run CAP initiator setup and configure unicast streams. -5. **Unicast**: The central configures and starts streams; the peripheral handles ASCS config/QoS/enable/start, starts sink streams when enabled, and receives/sends audio on stream callbacks. -6. **Call control**: The peripheral (TBS client) can originate or terminate calls via the central’s TBS (CCP server); discover and URI list read complete in callbacks. +1. `app_main` initializes NVS, Bluetooth, and the audio common layer, then registers TMAP CT+UMR. +2. If `BT_CSIP_SET_MEMBER` is on, the CSIP set member registers via the CAP acceptor, generates an RSI, and is plumbed into `start_info.csis_insts[0]`. +3. VCP volume renderer, BAP unicast server (PACS, ASCS callbacks, stream ops), CCP call control (TBS client), and MCP controller (MCC) are initialized. +4. The audio stack starts, device name is set, and extended advertising starts on handle 0. +5. On ACL connect the connection handle is stored; on MTU change GATT service discovery starts; on discovery complete `tmap_discover_tmas` runs. +6. TMAP discovery records whether the peer is CG and/or UMS, then `ccp_discover_tbs` triggers TBS discovery. +7. After GTBS is discovered the TBS URI list is read; `read_uri_schemes_string_cb` saves the first URI and chains into `mcp_discover_mcs`. +8. MCS discovery walks player name → track title → duration → position → playback speed → seeking speed → playing order → orders supported → media state → opcodes → CCID. +9. ASCS callbacks accept config/QoS/enable/start; the `enabled` stream op auto-starts sink streams (receiver-start-ready); RX metrics are reset on start and updated per received SDU. +10. Helpers `initiate_call`/`terminate_call` (gated on peer CG) and `play_media`/`pause_media` (gated on peer UMS) issue TBS and MCC commands. +11. On disconnect, the connection handle is cleared and advertising restarts. -## Example Output +## Expected Log + +Initialization phase: ``` -I (xxx) TMAP_PER: Extended adv instance 0 started -I (xxx) TMAP_PER: connection established, status 0 -I (xxx) TMAP_PER: gatt mtu change, conn_handle 1, mtu ... -I (xxx) TMAP_PER: gattc disc cmpl, status 0, conn_handle 1 -I (xxx) TMAP_PER: TMAP discovery done -I (xxx) TMAP_PER: CCP: Discovered GTBS -... -I (xxx) TMAP_PER: Stream 0x... started -... +TMAP_PER: TMAP CT UMR initialized +TMAP_PER: CSIP set member initialized +TMAP_PER: PRSI: 0x +TMAP_PER: vcp vol renderer, vocs_cnt aics_cnt +TMAP_PER: VCP volume renderer initialized +TMAP_PER: BAP unicast server initialized +TMAP_PER: CCP call controller initialized +TMAP_PER: MCP controller initialized +TMAP_PER: Advertising started (handle 0) ``` -If the connection is lost: +Connection / discovery: ``` -I (xxx) TMAP_PER: connection disconnected, reason 0x... +TMAP_PER: Connected: handle role peer +TMAP_PER: MTU updated: handle mtu +TMAP_PER: Service discovery started: handle +TMAP_PER: Service discovery complete: handle status 0 +TMAP_PER: TMAP discovery done +TMAP_PER: Discovered GTBS +TMAP_PER: Discovered remote URI +TMAP_PER: Discover MCS succeeded +TMAP_PER: Read player name succeeded, name +TMAP_PER: Read track title succeeded, title +TMAP_PER: Read media state succeeded, state <s> +TMAP_PER: Read content control id succeeded, ccid <c> ``` + +ASCS / unicast streaming: + +``` +TMAP_PER: [SNK] Config request: +TMAP_PER: [SNK #0] QoS request: +TMAP_PER: [SNK #0] Enable request (meta_len <n>) +TMAP_PER: [SNK #0] Stream enabled +TMAP_PER: [SNK #0] Start request +TMAP_PER: [SNK #0] Stream started +TMAP_PER: [SNK #0] Stop request +TMAP_PER: [SNK #0] Stream stopped, reason 0x<rr> +TMAP_PER: [SNK #0] Disable request +TMAP_PER: [SNK #0] Release request +``` + +VCP / call control / disconnect: + +``` +TMAP_PER: VCS volume <v>, mute <m> +TMAP_PER: VCS flags 0x<ff> +TMAP_PER: Call <i> originated +TMAP_PER: Call <i> terminated +TMAP_PER: Disconnected: handle <h> reason 0x<rr> +``` + +Tag is `TMAP_PER`. + +## Peer Pairing + +Run [tmap_central](../central/) on a second board. + +1. Flash this peripheral and start it; advertising begins on handle 0. +2. Flash and start the central; it scans for TMAS+UMR and connects to this device. +3. After pairing and MTU exchange, both sides complete TMAP discovery; this peripheral then discovers GTBS and reads the URI list. +4. The MCC chain reads the central's media proxy state; the central's CAP initiator configures and starts the sink stream. +5. The peripheral auto-starts the sink stream from the `enabled` callback and logs received SDU metrics. +6. Use `initiate_call`/`terminate_call` to drive the central's TBS, and `play_media`/`pause_media` to send PLAY/PAUSE via MCC. diff --git a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/Kconfig.projbuild b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/Kconfig.projbuild index 3da7e894c20..6c72a87e40f 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/Kconfig.projbuild +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/Kconfig.projbuild @@ -1,16 +1,9 @@ # SPDX-FileCopyrightText: 2022 Nordic Semiconductor ASA -# SPDX-FileContributor: 2025 Espressif Systems (Shanghai) CO LTD +# SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 menu "Example: TMAP Peripheral (CT & UMR)" - config EXAMPLE_TMAP_PER_SET_RANK - int "Device rank in set" - depends on EXAMPLE_TMAP_PER_DUO - range 1 2 - help - Rank of this device in set. - choice EXAMPLE_TMAP_PER_TYPE_CHOICE prompt "Earbuds type" @@ -27,6 +20,14 @@ menu "Example: TMAP Peripheral (CT & UMR)" endchoice + if EXAMPLE_TMAP_PER_DUO + config EXAMPLE_TMAP_PER_SET_RANK + int "Device rank in set" + range 1 2 + help + Rank of this device in set. + endif + choice EXAMPLE_TMAP_PER_LOCATION prompt "Earbud Location" diff --git a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/bap_unicast_sr.c b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/bap_unicast_sr.c index f0416f9a7fa..659920c05d6 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/bap_unicast_sr.c +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/bap_unicast_sr.c @@ -77,6 +77,41 @@ static esp_ble_audio_bap_stream_t *stream_alloc(void) return NULL; } +static const char *dir_str(esp_ble_audio_dir_t dir) +{ + return dir == ESP_BLE_AUDIO_DIR_SINK ? "SNK" : "SRC"; +} + +static bool stream_is_sink(const esp_ble_audio_bap_stream_t *stream) +{ + for (size_t i = 0; i < ARRAY_SIZE(sink_streams); i++) { + if (sink_streams[i].stream == stream) { + return true; + } + } + return false; +} + +static const char *stream_dir_str(const esp_ble_audio_bap_stream_t *stream) +{ + return stream_is_sink(stream) ? "SNK" : "SRC"; +} + +static int stream_index(const esp_ble_audio_bap_stream_t *stream) +{ + for (size_t i = 0; i < ARRAY_SIZE(sink_streams); i++) { + if (sink_streams[i].stream == stream) { + return (int)i; + } + } + for (size_t i = 0; i < ARRAY_SIZE(source_streams); i++) { + if (source_streams[i].stream == stream) { + return (int)i; + } + } + return -1; +} + static int config_cb(esp_ble_conn_t *conn, const esp_ble_audio_bap_ep_t *ep, esp_ble_audio_dir_t dir, @@ -87,21 +122,19 @@ static int config_cb(esp_ble_conn_t *conn, { bool stream_exist = false; - ESP_LOGI(TAG, "Config: conn %p ep %p dir %u", conn, ep, dir); + ESP_LOGI(TAG, "[%s] Config request:", dir_str(dir)); - example_print_codec_cfg(codec_cfg); + example_print_codec_cfg(TAG, codec_cfg); *stream = stream_alloc(); if (*stream == NULL) { - ESP_LOGE(TAG, "No streams available"); + ESP_LOGE(TAG, "[%s] No streams available", dir_str(dir)); *rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_NO_MEM, ESP_BLE_AUDIO_BAP_ASCS_REASON_NONE); return -ENOMEM; } - ESP_LOGI(TAG, "Config stream %p", *stream); - if (dir == ESP_BLE_AUDIO_DIR_SINK) { for (size_t i = 0; i < ARRAY_SIZE(sink_streams); i++) { if (sink_streams[i].stream == *stream) { @@ -112,7 +145,7 @@ static int config_cb(esp_ble_conn_t *conn, if (stream_exist == false) { if (configured_sink_stream_count >= ARRAY_SIZE(sink_streams)) { - ESP_LOGE(TAG, "No more sink stream available"); + ESP_LOGE(TAG, "[SNK] No more sink stream available"); *rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_NO_MEM, ESP_BLE_AUDIO_BAP_ASCS_REASON_NONE); @@ -131,7 +164,7 @@ static int config_cb(esp_ble_conn_t *conn, if (stream_exist == false) { if (configured_source_stream_count >= ARRAY_SIZE(source_streams)) { - ESP_LOGE(TAG, "No more source stream available"); + ESP_LOGE(TAG, "[SRC] No more source stream available"); *rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_NO_MEM, ESP_BLE_AUDIO_BAP_ASCS_REASON_NONE); @@ -155,9 +188,10 @@ static int reconfig_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_qos_cfg_pref_t *const pref, esp_ble_audio_bap_ascs_rsp_t *rsp) { - ESP_LOGI(TAG, "Reconfig: stream %p", stream); + ESP_LOGI(TAG, "[%s #%d] Reconfig request:", + dir_str(dir), stream_index(stream)); - example_print_codec_cfg(codec_cfg); + example_print_codec_cfg(TAG, codec_cfg); *pref = qos_pref; @@ -168,9 +202,10 @@ static int qos_cb(esp_ble_audio_bap_stream_t *stream, const esp_ble_audio_bap_qos_cfg_t *qos, esp_ble_audio_bap_ascs_rsp_t *rsp) { - ESP_LOGI(TAG, "QoS: stream %p qos %p", stream, qos); + ESP_LOGI(TAG, "[%s #%d] QoS request:", + stream_dir_str(stream), stream_index(stream)); - example_print_qos(qos); + example_print_qos(TAG, qos); return 0; } @@ -179,14 +214,16 @@ static int enable_cb(esp_ble_audio_bap_stream_t *stream, const uint8_t meta[], size_t meta_len, esp_ble_audio_bap_ascs_rsp_t *rsp) { - ESP_LOGI(TAG, "Enable: stream %p meta_len %u", stream, meta_len); + ESP_LOGI(TAG, "[%s #%d] Enable request (meta_len %u)", + stream_dir_str(stream), stream_index(stream), meta_len); return 0; } static int start_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_t *rsp) { - ESP_LOGI(TAG, "Start: stream %p", stream); + ESP_LOGI(TAG, "[%s #%d] Start request", + stream_dir_str(stream), stream_index(stream)); return 0; } @@ -265,7 +302,8 @@ static int metadata_cb(esp_ble_audio_bap_stream_t *stream, }; esp_err_t err; - ESP_LOGI(TAG, "Metadata: stream %p meta_len %u", stream, meta_len); + ESP_LOGI(TAG, "[%s #%d] Metadata request (meta_len %u)", + stream_dir_str(stream), stream_index(stream), meta_len); err = esp_ble_audio_data_parse(meta, meta_len, data_func_cb, &func_param); if (err) { @@ -291,21 +329,24 @@ static int metadata_cb(esp_ble_audio_bap_stream_t *stream, static int disable_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_t *rsp) { - ESP_LOGI(TAG, "Disable: stream %p", stream); + ESP_LOGI(TAG, "[%s #%d] Disable request", + stream_dir_str(stream), stream_index(stream)); return 0; } static int stop_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_t *rsp) { - ESP_LOGI(TAG, "Stop: stream %p", stream); + ESP_LOGI(TAG, "[%s #%d] Stop request", + stream_dir_str(stream), stream_index(stream)); return 0; } static int release_cb(esp_ble_audio_bap_stream_t *stream, esp_ble_audio_bap_ascs_rsp_t *rsp) { - ESP_LOGI(TAG, "Release: stream %p", stream); + ESP_LOGI(TAG, "[%s #%d] Release request", + stream_dir_str(stream), stream_index(stream)); for (size_t i = 0; i < ARRAY_SIZE(sink_streams); i++) { if (sink_streams[i].stream == stream) { @@ -327,7 +368,7 @@ static int release_cb(esp_ble_audio_bap_stream_t *stream, } } - ESP_LOGW(TAG, "Stream %p not found", stream); + ESP_LOGW(TAG, "Stream not found"); return -ENODEV; } @@ -348,7 +389,8 @@ static void stream_enabled(esp_ble_audio_bap_stream_t *stream) esp_ble_audio_bap_ep_info_t ep_info = {0}; int err; - ESP_LOGI(TAG, "Stream %p enabled", stream); + ESP_LOGI(TAG, "[%s #%d] Stream enabled", + stream_dir_str(stream), stream_index(stream)); err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info); if (err) { @@ -361,7 +403,8 @@ static void stream_enabled(esp_ble_audio_bap_stream_t *stream) /* Automatically do the receiver start ready operation */ err = esp_ble_audio_bap_stream_start(stream); if (err) { - ESP_LOGE(TAG, "Failed to start stream %p, err %d", stream, err); + ESP_LOGE(TAG, "[SNK #%d] Failed to start stream, err %d", + stream_index(stream), err); return; } } @@ -369,7 +412,8 @@ static void stream_enabled(esp_ble_audio_bap_stream_t *stream) static void stream_started(esp_ble_audio_bap_stream_t *stream) { - ESP_LOGI(TAG, "Stream %p started", stream); + ESP_LOGI(TAG, "[%s #%d] Stream started", + stream_dir_str(stream), stream_index(stream)); for (size_t i = 0; i < ARRAY_SIZE(sink_streams); i++) { if (sink_streams[i].stream == stream) { @@ -381,7 +425,8 @@ static void stream_started(esp_ble_audio_bap_stream_t *stream) static void stream_stopped(esp_ble_audio_bap_stream_t *stream, uint8_t reason) { - ESP_LOGI(TAG, "Stream %p stopped, reason 0x%02x", stream, reason); + ESP_LOGI(TAG, "[%s #%d] Stream stopped, reason 0x%02x", + stream_dir_str(stream), stream_index(stream), reason); } static void stream_recv(esp_ble_audio_bap_stream_t *stream, @@ -394,10 +439,12 @@ static void stream_recv(esp_ble_audio_bap_stream_t *stream, struct audio_sink *sink_stream = CONTAINER_OF(&sink_streams[i].stream, struct audio_sink, stream); + char name[24]; + snprintf(name, sizeof(name), "SNK #%zu", i); sink_stream->rx_metrics.last_sdu_len = len; example_audio_rx_metrics_on_recv(info, &sink_stream->rx_metrics, - TAG, "stream", stream); + TAG, name); break; } } diff --git a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/ccp_call_ctrl.c b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/ccp_call_ctrl.c index 6005ec69db9..cf7f6194e7e 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/ccp_call_ctrl.c +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/ccp_call_ctrl.c @@ -113,7 +113,7 @@ static void read_uri_schemes_string_cb(esp_ble_conn_t *conn, } if (i >= sizeof(remote_uri)) { - ESP_LOGW(TAG, "Cannot store URI of length %u: %s", i, value); + ESP_LOGW(TAG, "Cannot store URI of length %zu: %s", i, value); return; } diff --git a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/main.c b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/main.c index 0fce9a7b73e..0a04c073e88 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/main.c +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/main.c @@ -197,30 +197,28 @@ static void ext_adv_start(void) return; } - ESP_LOGI(TAG, "Extended adv instance %u started", ADV_HANDLE); + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); } static void acl_connect(esp_ble_audio_gap_app_event_t *event) { if (event->acl_connect.status) { - ESP_LOGE(TAG, "connection failed, status %d", event->acl_connect.status); + ESP_LOGE(TAG, "Connection failed, status %d", event->acl_connect.status); return; } - ESP_LOGI(TAG, "Conn established:"); - ESP_LOGI(TAG, "conn_handle 0x%04x status 0x%02x role %u peer %02x:%02x:%02x:%02x:%02x:%02x", - event->acl_connect.conn_handle, event->acl_connect.status, - event->acl_connect.role, event->acl_connect.dst.val[5], - event->acl_connect.dst.val[4], event->acl_connect.dst.val[3], - event->acl_connect.dst.val[2], event->acl_connect.dst.val[1], - event->acl_connect.dst.val[0]); + ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x", + event->acl_connect.conn_handle, event->acl_connect.role, + event->acl_connect.dst.val[5], event->acl_connect.dst.val[4], + event->acl_connect.dst.val[3], event->acl_connect.dst.val[2], + event->acl_connect.dst.val[1], event->acl_connect.dst.val[0]); default_conn_handle = event->acl_connect.conn_handle; } static void acl_disconnect(esp_ble_audio_gap_app_event_t *event) { - ESP_LOGI(TAG, "Conn terminated: conn_handle 0x%04x reason 0x%02x", + ESP_LOGI(TAG, "Disconnected: handle %u reason 0x%02x", event->acl_disconnect.conn_handle, event->acl_disconnect.reason); default_conn_handle = CONN_HANDLE_INIT; @@ -246,11 +244,11 @@ static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event) { int err; - ESP_LOGI(TAG, "gatt mtu change, conn_handle %u, mtu %u", + ESP_LOGI(TAG, "MTU updated: handle %u mtu %u", event->gatt_mtu_change.conn_handle, event->gatt_mtu_change.mtu); if (event->gatt_mtu_change.mtu < ESP_BLE_AUDIO_ATT_MTU_MIN) { - ESP_LOGW(TAG, "Invalid new mtu %u, shall be at least %u", + ESP_LOGW(TAG, "MTU %u below minimum %u", event->gatt_mtu_change.mtu, ESP_BLE_AUDIO_ATT_MTU_MIN); return; } @@ -261,20 +259,21 @@ static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event) */ err = esp_ble_audio_gattc_disc_start(event->gatt_mtu_change.conn_handle); if (err) { - ESP_LOGE(TAG, "Failed to start svc disc, err %d", err); + ESP_LOGE(TAG, "Failed to start service discovery, err %d", err); return; } - ESP_LOGI(TAG, "Start discovering gatt services"); + ESP_LOGI(TAG, "Service discovery started: handle %u", + event->gatt_mtu_change.conn_handle); } static void gattc_disc_cmpl(esp_ble_audio_gatt_app_event_t *event) { - ESP_LOGI(TAG, "gattc disc cmpl, status %u, conn_handle %u", - event->gattc_disc_cmpl.status, event->gattc_disc_cmpl.conn_handle); + ESP_LOGI(TAG, "Service discovery complete: handle %u status %u", + event->gattc_disc_cmpl.conn_handle, + event->gattc_disc_cmpl.status); if (event->gattc_disc_cmpl.status) { - ESP_LOGE(TAG, "gattc disc failed, status %u", event->gattc_disc_cmpl.status); return; } diff --git a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/vcp_vol_renderer.c b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/vcp_vol_renderer.c index 64d0884e006..ed1dc20238f 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/vcp_vol_renderer.c +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/vcp_vol_renderer.c @@ -190,10 +190,10 @@ int vcp_vol_renderer_init(void) ESP_LOGI(TAG, "vcp vol renderer, vocs_cnt %u aics_cnt %u", vcp_included.vocs_cnt, vcp_included.aics_cnt); for (size_t i = 0; i < vcp_included.vocs_cnt; i++) { - ESP_LOGI(TAG, "vcp vol renderer, vocs_%u %p", i, vcp_included.vocs[i]); + ESP_LOGI(TAG, "vcp vol renderer, vocs_%zu %p", i, vcp_included.vocs[i]); } for (size_t i = 0; i < vcp_included.aics_cnt; i++) { - ESP_LOGI(TAG, "vcp vol renderer, aics_%u %p", i, vcp_included.aics[i]); + ESP_LOGI(TAG, "vcp vol renderer, aics_%zu %p", i, vcp_included.aics[i]); } ESP_LOGI(TAG, "VCP volume renderer initialized"); diff --git a/examples/bluetooth/esp_ble_audio/tmap/peripheral/sdkconfig.defaults b/examples/bluetooth/esp_ble_audio/tmap/peripheral/sdkconfig.defaults index f6346788a9c..7307864a1ba 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/peripheral/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/sdkconfig.defaults @@ -9,6 +9,7 @@ CONFIG_BT_ENABLED=y CONFIG_BT_BLUEDROID_ENABLED=n CONFIG_BT_NIMBLE_ENABLED=y CONFIG_BT_NIMBLE_EXT_ADV=y +CONFIG_BT_NIMBLE_NVS_PERSIST=y CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 CONFIG_BT_NIMBLE_MAX_CCCDS=30 CONFIG_BT_NIMBLE_ISO=y diff --git a/examples/bluetooth/esp_ble_iso/big_broadcaster/README.md b/examples/bluetooth/esp_ble_iso/big_broadcaster/README.md index 81b548d5d19..9d4591517ab 100644 --- a/examples/bluetooth/esp_ble_iso/big_broadcaster/README.md +++ b/examples/bluetooth/esp_ble_iso/big_broadcaster/README.md @@ -1,52 +1,107 @@ | Supported Targets | ESP32-H4 | ESP32-S31 | | ----------------- | -------- | --------- | -# BLE BIG Broadcaster Example +# BIG Broadcaster (See the README.md file in the upper level `examples` directory for more information about examples.) -This example demonstrates the **Bluetooth LE Isochronous Broadcaster** functionality. It acts as a Broadcast Isochronous Stream (BIS) source: it starts extended advertising and periodic advertising, creates a Broadcast Isochronous Group (BIG), and sends isochronous data on the BIS channels. Receivers can synchronize to this BIG using the [big_receiver](../big_receiver) example. +## Overview -The implementation uses the NimBLE host stack with ISO support and the ESP-BLE-ISO APIs (CIG/BIG, data path, channel operations). It is intended for chips that support BLE 5.2 ISO (e.g. ESP32-H4). The advertised device name is hardcoded as `BIG Broadcaster` and the broadcast code is hardcoded as `1234`; these can be changed by editing the source code constants. +This example demonstrates a raw BLE Isochronous Broadcaster — it creates a Broadcast Isochronous Group (BIG) directly at the ISO transport layer over the NimBLE host, without any BLE Audio profile (BAP/CAP) on top. + +The device acts as the **broadcaster**: it starts extended + periodic advertising, creates a BIG carrying two BIS streams, sets up an HCI input data path on each BIS, and then transmits SDUs on a fixed 10 ms cadence using a software TX scheduler. The peer (`big_receiver`) discovers the broadcaster by name, syncs to the periodic advertising train, decodes BIGInfo, and joins the BIS sub-events. + +The audio data on the wire is **application-supplied dummy payload** (each SDU is filled with the byte value of its sequence number). The coding format is `ESP_BLE_ISO_CODING_FORMAT_TRANSPARENT`, so no LC3 / LE-Audio framing is involved. ## Requirements -* A board with Bluetooth LE 5.2 and ISO support (e.g. ESP32-H4) -* Optionally, a second device running the [big_receiver](../big_receiver) example to receive and sync to the BIG +* A board with BLE 5.2 and ISO support (e.g. ESP32-H4, ESP32-S31) +* Peer device running the paired example -## How to Use Example +## Configuration -Before project configuration and build, set the correct chip target: +```bash +idf.py menuconfig +``` + +No build-time options — runtime defaults are baked into source. + +Notable hard-coded parameters in `main/main.c`: + +* `LOCAL_DEVICE_NAME` — `"BIG Broadcaster"` (advertised in extended adv) +* `LOCAL_BROADCAST_CODE` — `"1234"` (BIG is encrypted) +* `BIG_SDU_INTERVAL_US` — 10 000 us +* `BIG_LATENCY_MS` — 10 ms +* `BIG_PHY` — 2M +* `BIG_RTN` — 2 (retransmissions) +* `BIS_ISO_CHAN_COUNT` — 2 +* `BIS_SDU_SIZE` — 120 bytes + +### Security & Pairing + +The shared init at `../common_components/example_init/ble_iso_example_init.c` configures Just-Works pairing (LE Secure Connections, no MITM, `BLE_SM_IO_CAP_NO_IO`) with bonding enabled, and leaves `gatts_register_cb = NULL` (no GATT services). These settings are not exercised by this example — BIG broadcast traffic is non-connectable and BIS payload encryption is driven by the broadcast code, independent of host SMP. + +## Build & Flash ```bash idf.py set-target esp32h4 -``` - -### Build and Flash - -Run the following to build, flash and monitor: - -```bash idf.py -p PORT flash monitor ``` -(To exit the serial monitor, type ``Ctrl-]``.) - -See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF. +(Exit serial monitor with `Ctrl-]`.) ## Example Flow -1. **Initialization**: NVS, Bluetooth stack (NimBLE), and ISO common layer (`esp_ble_iso_common_init`). -2. **Extended and periodic advertising**: Configure and start extended advertising with a fixed interval; attach periodic advertising with name `BIG Broadcaster` for receivers to discover. -3. **Create BIG**: Register the advertising set for BIG, then create a BIG with two BIS channels (hardcoded as `BIS_ISO_CHAN_COUNT` in the source; `CONFIG_BT_ISO_MAX_CHAN` must be >= 2), SDU interval 10 ms, latency 10 ms, sequential packing, unframed, and encryption with the hardcoded broadcast code `1234`. The [big_receiver](../big_receiver) must use the same broadcast code. -4. **Send ISO data**: Once all BIS channels are connected and their data paths are set up, a periodic TX scheduler based on `k_work_delayable` sends the same SDU on all BIS channels at the configured interval in the ISO task context; sequence numbers and drift handling are applied. Note that the scheduler timer resolution is in milliseconds, which may not match the exact SDU interval for all configurations. +1. NVS, NimBLE host, and the ISO common layer are initialised. +2. A TX scheduler is initialised for each of the two BIS channels. +3. Extended advertising parameters / data and periodic advertising parameters / data are configured for handle 0 (non-connectable, non-scannable, primary 1M / secondary 2M). +4. Periodic advertising and extended advertising are started. +5. `esp_ble_iso_big_ext_adv_add()` ties the adv handle to the BIG, then `esp_ble_iso_big_create()` issues the HCI `LE Create BIG` with two BIS, encryption on, and broadcast code `"1234"`. +6. As each BIS becomes ready, the connected callback installs an HCI input data path (`ESP_BLE_ISO_DATA_PATH_DIR_INPUT`, transparent coding). +7. Once both BIS are connected, the per-channel TX schedulers start firing every 10 ms, calling `esp_ble_iso_chan_send()` with a 120-byte dummy SDU and an incrementing sequence number. +8. On disconnect of all BIS the schedulers are stopped and the BIG handle is cleared. -## Example Output +## Expected Log + +Tag: `BIG_BRD`. + +Startup / advertising: ``` -I (xxx) BIG_BRD: Extended adv instance 0 started -I (xxx) BIG_BRD: ISO channel 0x0001 connected -I (xxx) BIG_BRD: ISO channel 0x0002 connected -I (xxx) BIG_BRD: Transmitted 1000 ISO data packets (chan 0x...) -... +I (xxx) BIG_BRD: Advertising started (handle 0) ``` + +BIS bring-up (first BIS, then second, then TX start): + +``` +I (xxx) BIG_BRD: [BIS #0] Connected +I (xxx) BIG_BRD: Waiting for remaining BIS channels (1/2) +I (xxx) BIG_BRD: [BIS #1] Connected +I (xxx) BIG_BRD: All 2 BIS channels connected, starting TX +``` + +Steady-state (logged every `LOG_INTERVAL_PACKETS` SDUs by the shared TX helper): + +``` +I (xxx) BIG_BRD: [BIS #0] TX: <N> packets +I (xxx) BIG_BRD: [BIS #1] TX: <N> packets +``` + +Teardown: + +``` +I (xxx) BIG_BRD: [BIS #0] Disconnected, reason 0x<rr> +I (xxx) BIG_BRD: [BIS #1] Disconnected, reason 0x<rr> +I (xxx) BIG_BRD: All BIS channels disconnected, TX stopped +``` + +## Peer Pairing + +Run [big_receiver](../big_receiver/) on a second board. + +1. Flash and run `big_broadcaster` on board A; it begins extended + periodic advertising and creates the BIG immediately. +2. Flash and run `big_receiver` on board B; it starts passive extended scanning. +3. Board B matches on the device name `"BIG Broadcaster"` and creates a periodic advertising sync to the broadcaster's PA train. +4. Once PA-synced, board B receives the BIGInfo report and calls `LE BIG Create Sync` with broadcast code `"1234"` and both BIS in the bitfield. +5. Board A logs `[BIS #0/1] Connected` as the controller reports the BIS as established; board B logs `PA synced` followed by per-BIS `Connected` and starts counting received SDUs. +6. Resetting either side causes the receiver to drop and (via `pa_sync_lost`) restart its scan, re-establishing the link automatically. diff --git a/examples/bluetooth/esp_ble_iso/big_broadcaster/main/main.c b/examples/bluetooth/esp_ble_iso/big_broadcaster/main/main.c index e74dd16544f..f4f6bb40e05 100644 --- a/examples/bluetooth/esp_ble_iso/big_broadcaster/main/main.c +++ b/examples/bluetooth/esp_ble_iso/big_broadcaster/main/main.c @@ -5,6 +5,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include <stdio.h> #include <string.h> #include <assert.h> @@ -73,20 +74,21 @@ static void iso_connected_cb(esp_ble_iso_chan_t *chan) .pid = ESP_BLE_ISO_DATA_PATH_HCI, .format = ESP_BLE_ISO_CODING_FORMAT_TRANSPARENT, }; + int chan_idx = bis_chan_index_get(chan); esp_err_t err; - ESP_LOGI(TAG, "ISO channel %p connected", chan); + ESP_LOGI(TAG, "[BIS #%d] Connected", chan_idx); err = esp_ble_iso_setup_data_path(chan, ESP_BLE_ISO_DATA_PATH_DIR_INPUT, &data_path); if (err) { - ESP_LOGE(TAG, "Failed to setup ISO data path, err %d", err); + ESP_LOGE(TAG, "[BIS #%d] Failed to setup data path, err %d", chan_idx, err); return; } connected_bis_count++; if (connected_bis_count < BIS_ISO_CHAN_COUNT) { - ESP_LOGI(TAG, "Waiting for all BIS channels (%u/%u)", + ESP_LOGI(TAG, "Waiting for remaining BIS channels (%u/%u)", connected_bis_count, BIS_ISO_CHAN_COUNT); return; } @@ -100,7 +102,7 @@ static void iso_connected_cb(esp_ble_iso_chan_t *chan) err = example_iso_tx_scheduler_start(&chan_tx[i].scheduler, BIG_SDU_INTERVAL_US); if (err) { - ESP_LOGE(TAG, "Failed to start tx scheduler[%u], err %d", i, err); + ESP_LOGE(TAG, "[BIS #%zu] Scheduler start failed, err %d", i, err); continue; } @@ -110,7 +112,8 @@ static void iso_connected_cb(esp_ble_iso_chan_t *chan) static void iso_disconnected_cb(esp_ble_iso_chan_t *chan, uint8_t reason) { - ESP_LOGI(TAG, "ISO channel %p disconnected, reason 0x%02x", chan, reason); + ESP_LOGI(TAG, "[BIS #%d] Disconnected, reason 0x%02x", + bis_chan_index_get(chan), reason); if (connected_bis_count > 0) { connected_bis_count--; @@ -124,7 +127,7 @@ static void iso_disconnected_cb(esp_ble_iso_chan_t *chan, uint8_t reason) for (size_t i = 0; i < BIS_ISO_CHAN_COUNT; i++) { err = example_iso_tx_scheduler_stop(&chan_tx[i].scheduler); if (err) { - ESP_LOGE(TAG, "Failed to stop tx scheduler[%u], err %d", i, err); + ESP_LOGE(TAG, "[BIS #%zu] Scheduler stop failed, err %d", i, err); } } @@ -135,14 +138,16 @@ static void iso_disconnected_cb(esp_ble_iso_chan_t *chan, uint8_t reason) static void iso_sent_cb(esp_ble_iso_chan_t *chan, void *user_data) { int chan_idx = bis_chan_index_get(chan); + char name[24]; if (chan_idx < 0) { - ESP_LOGW(TAG, "Unknown BIS channel %p", chan); + ESP_LOGW(TAG, "Unknown BIS channel"); return; } + snprintf(name, sizeof(name), "BIS #%d", chan_idx); example_iso_tx_scheduler_on_sent(&chan_tx[chan_idx].scheduler, - user_data, TAG, "chan", chan); + user_data, TAG, name); } static esp_ble_iso_chan_ops_t iso_ops = { @@ -281,7 +286,7 @@ static int ext_adv_start(void) return err; } - ESP_LOGI(TAG, "Extended adv instance %u started", ADV_HANDLE); + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); return 0; } @@ -334,7 +339,7 @@ static void iso_chan_send(int chan_idx) } if (bis_iso_chan[chan_idx].iso == NULL) { - ESP_LOGW(TAG, "No channel to transmit data, %u", chan_idx); + ESP_LOGW(TAG, "[BIS #%d] No channel to transmit", chan_idx); return; } @@ -347,8 +352,7 @@ static void iso_chan_send(int chan_idx) sizeof(chan_tx[chan_idx].data), chan_tx[chan_idx].seq_num); if (err) { - ESP_LOGD(TAG, "Failed to transmit data on channel 0x%04x", - bis_iso_chan[chan_idx].iso->handle); + ESP_LOGD(TAG, "[BIS #%d] send failed, err %d", chan_idx, err); return; } @@ -397,7 +401,7 @@ void app_main(void) tx_scheduler_cb, &chan_tx[i]); if (err) { - ESP_LOGE(TAG, "Failed to init tx scheduler[%u], err %d", i, err); + ESP_LOGE(TAG, "[BIS #%zu] Scheduler init failed, err %d", i, err); return; } } diff --git a/examples/bluetooth/esp_ble_iso/big_receiver/README.md b/examples/bluetooth/esp_ble_iso/big_receiver/README.md index 013f332d821..43d5b6a4edf 100644 --- a/examples/bluetooth/esp_ble_iso/big_receiver/README.md +++ b/examples/bluetooth/esp_ble_iso/big_receiver/README.md @@ -1,59 +1,105 @@ | Supported Targets | ESP32-H4 | ESP32-S31 | | ----------------- | -------- | --------- | -# BLE BIG Receiver Example +# BIG Receiver (See the README.md file in the upper level `examples` directory for more information about examples.) -This example demonstrates the **Bluetooth LE Synchronized Receiver** functionality. It acts as a BIS (Broadcast Isochronous Stream) sink: it scans for extended advertising, establishes periodic advertising synchronization with a broadcaster, receives BIGInfo from the periodic advertising, and synchronizes to the BIG to receive isochronous data on the BIS channels. Use it together with the [big_broadcaster](../big_broadcaster) example on another device as the source. +## Overview -The implementation uses the NimBLE host stack with ISO support and the ESP-BLE-ISO APIs (periodic sync, BIG sync, data path, channel operations). It is intended for chips that support BLE 5.2 ISO (e.g. ESP32-H4). The target broadcaster name is hardcoded as `BIG Broadcaster` and the broadcast code is hardcoded as `1234`; these must match the [big_broadcaster](../big_broadcaster) defaults. +This example demonstrates a raw BLE Isochronous Broadcast Receiver — it joins a Broadcast Isochronous Group (BIG) directly at the ISO transport layer over the NimBLE host, without any BLE Audio profile (BAP/CAP) on top. + +The device acts as the **receiver (BIG sync sink)**: it scans for an extended advertiser by device name, synchronises to the peer's periodic advertising train, parses the BIGInfo report, and then issues `LE BIG Create Sync` to join two BIS sub-events. An HCI output data path is installed on each BIS and incoming SDUs are accounted for by a small RX-metrics helper. + +The payloads received are **application-supplied dummy data** produced by `big_broadcaster`; this example does not decode LE-Audio / LC3 — the coding format on the data path is `ESP_BLE_ISO_CODING_FORMAT_TRANSPARENT`. ## Requirements -* A board with Bluetooth LE 5.2 and ISO support (e.g. ESP32-H4) -* Another device running the [big_broadcaster](../big_broadcaster) example, which performs periodic advertising and creates the BIG +* A board with BLE 5.2 and ISO support (e.g. ESP32-H4, ESP32-S31) +* Peer device running the paired example -## How to Use Example +## Configuration -Before project configuration and build, set the correct chip target: +```bash +idf.py menuconfig +``` + +No build-time options — runtime defaults are baked into source. + +Notable hard-coded parameters in `main/main.c`: + +* `TARGET_DEVICE_NAME` — `"BIG Broadcaster"` (matched against the AD complete-name field) +* `TARGET_BROADCAST_CODE` — `"1234"` (must match the broadcaster) +* `SCAN_INTERVAL` / `SCAN_WINDOW` — 100 ms / 100 ms (passive) +* `PA_SYNC_TIMEOUT` — 10 s +* `BIG_SYNC_TIMEOUT` — 1 s +* `BIS_ISO_CHAN_COUNT` — 2 + +### Security & Pairing + +The shared init at `../common_components/example_init/ble_iso_example_init.c` configures Just-Works pairing (LE Secure Connections, no MITM, `BLE_SM_IO_CAP_NO_IO`) with bonding enabled, and leaves `gatts_register_cb = NULL` (no GATT services). These settings are not exercised by this example — the receiver passively syncs to PA + BIS without ATT or pairing. + +## Build & Flash ```bash idf.py set-target esp32h4 -``` - -### Build and Flash - -Run the following to build, flash and monitor: - -```bash idf.py -p PORT flash monitor ``` -(To exit the serial monitor, type ``Ctrl-]``.) - -See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF. +(Exit serial monitor with `Ctrl-]`.) ## Example Flow -1. **Initialization**: NVS, Bluetooth stack (NimBLE), and ISO common layer (`esp_ble_iso_common_init`) with GAP callback for scan, periodic sync, and BIGInfo events. -2. **Extended scan**: Start passive extended scanning. On extended scan report, parse advertising data for the complete local name. -3. **Periodic advertising sync**: When the advertised name matches the hardcoded target name `BIG Broadcaster` and the advertiser has periodic advertising, create a periodic advertising synchronization to that advertiser. -4. **BIGInfo and BIG sync**: When BIGInfo is received in the periodic advertising, create a BIG sync (`esp_ble_iso_big_sync`) with two BIS channels (hardcoded in this example; must match the [big_broadcaster](../big_broadcaster)) and the hardcoded broadcast code `1234`. -5. **Receive ISO data**: When each BIS channel is connected, set up the output data path. Incoming ISO SDUs are reported in the receive callback; the example counts valid, error, and lost packets and logs periodically. +1. NVS, NimBLE host, and the ISO common layer are initialised; a GAP application callback is registered for ISO-related GAP events. +2. Passive extended scanning is started via `ble_gap_disc()`. +3. On each `EXT_SCAN_RECV` the AD payload is parsed; if the complete-name field equals `"BIG Broadcaster"` and the report carries a non-zero periodic-advertising interval, `ble_gap_periodic_adv_sync_create()` is called. +4. On `PA_SYNC` success the extended scan is cancelled (BIGInfo arrives over the PA channel anyway). +5. On the first `BIGINFO_RECV` event the receiver fills `esp_ble_iso_big_sync_param_t` (both BIS in `bis_bitfield`, broadcast code `"1234"`, MSE = `nse` from BIGInfo) and calls `esp_ble_iso_big_sync()` — the HCI `LE BIG Create Sync` command. +6. When each BIS becomes ready, the connected callback resets that BIS's RX metrics and installs an HCI output data path (`ESP_BLE_ISO_DATA_PATH_DIR_OUTPUT`, transparent coding). +7. Each `recv` callback updates the per-BIS counters; one log line is emitted every `LOG_INTERVAL_PACKETS` SDUs. +8. On BIS disconnect or PA-sync-lost the receiver clears its sync state and restarts extended scanning. -## Example Output +## Expected Log + +Tag: `BIG_SNC`. + +Discovery and PA sync: ``` -I (xxx) BIG_SNC: Extended scan started -I (xxx) BIG_SNC: ISO channel 0x0001 connected -I (xxx) BIG_SNC: ISO channel 0x0002 connected -I (xxx) BIG_SNC: Received 1000(1000/0/0) ISO data packets (chan 0x...) -... +I (xxx) BIG_SNC: Scanning for broadcaster... +I (xxx) BIG_SNC: PA synced: handle <h> sid <s> phy <p> peer xx:xx:xx:xx:xx:xx ``` -If periodic sync is lost: +BIS bring-up: ``` -I (xxx) BIG_SNC: PA sync lost, reason ... +I (xxx) BIG_SNC: [BIS #0] Connected +I (xxx) BIG_SNC: [BIS #1] Connected ``` + +Steady-state (logged every `LOG_INTERVAL_PACKETS` SDUs by the shared RX helper): + +``` +I (xxx) BIG_SNC: [BIS #0] RX: <N> packets +I (xxx) BIG_SNC: [BIS #1] RX: <N> packets +``` + +Teardown / re-sync (broadcaster disappears or restarts; reason `0x08` is the typical timeout, `0x3D` is the rare MIC-fail race documented in source): + +``` +I (xxx) BIG_SNC: [BIS #0] Disconnected, reason 0x08 +I (xxx) BIG_SNC: [BIS #1] Disconnected, reason 0x08 +I (xxx) BIG_SNC: PA sync lost: sync_handle <h> reason 0x<rr> +I (xxx) BIG_SNC: Scanning for broadcaster... +``` + +## Peer Pairing + +Run [big_broadcaster](../big_broadcaster/) on a second board. + +1. Flash and run `big_broadcaster` on board A; it begins extended + periodic advertising and creates the BIG. +2. Flash and run `big_receiver` on board B; it logs `Scanning for broadcaster...` and waits. +3. Board B matches the AD name `"BIG Broadcaster"`, creates a PA sync, logs `PA synced`, and stops the extended scan. +4. On the first BIGInfo report, board B issues `LE BIG Create Sync` with broadcast code `"1234"` and both BIS selected; both sides log `[BIS #0/1] Connected`. +5. Per-BIS RX-packet milestones are logged on B at the same cadence that A logs TX milestones. +6. Resetting board A causes B to log a BIS disconnect and `PA sync lost`, then automatically restart scanning to re-pair. diff --git a/examples/bluetooth/esp_ble_iso/big_receiver/main/main.c b/examples/bluetooth/esp_ble_iso/big_receiver/main/main.c index 72fe834ab9a..07a57949930 100644 --- a/examples/bluetooth/esp_ble_iso/big_receiver/main/main.c +++ b/examples/bluetooth/esp_ble_iso/big_receiver/main/main.c @@ -5,6 +5,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include <stdio.h> #include <string.h> #include <assert.h> @@ -55,27 +56,54 @@ static void iso_connected_cb(esp_ble_iso_chan_t *chan) .pid = ESP_BLE_ISO_DATA_PATH_HCI, .format = ESP_BLE_ISO_CODING_FORMAT_TRANSPARENT, }; + int chan_idx = bis_chan_index_get(chan); esp_err_t err; - ESP_LOGI(TAG, "ISO channel %p connected", chan); + ESP_LOGI(TAG, "[BIS #%d] Connected", chan_idx); + + /* New BIS session — reset RX counters so milestones reflect + * this session only. Matches the session-start reset pattern + * used by cis_peripheral and the audio examples. + */ + if (chan_idx >= 0) { + example_iso_rx_metrics_reset(&rx_metrics[chan_idx]); + } err = esp_ble_iso_setup_data_path(chan, ESP_BLE_ISO_DATA_PATH_DIR_OUTPUT, &data_path); if (err) { - ESP_LOGE(TAG, "Failed to setup ISO data path, err %d", err); + ESP_LOGE(TAG, "[BIS #%d] Failed to setup data path, err %d", chan_idx, err); return; } } static void iso_disconnected_cb(esp_ble_iso_chan_t *chan, uint8_t reason) { - ESP_LOGI(TAG, "ISO channel %p disconnected, reason 0x%02x", chan, reason); + /* Common BIS disconnect reasons during broadcaster restart: + * + * 0x08 CONN_TIMEOUT - no subevent received within + * BIG_Sync_Timeout (broadcaster is gone + * or out of range). This is the common + * case. + * + * 0x3D TERM_DUE_TO_MIC_FAIL - packets arrived but MIC check + * failed repeatedly. Only possible on + * encrypted BIGs; typically means the + * broadcaster restarted with a new + * session before we timed out, so the + * old session key no longer decrypts. + * + * !!! LOW PROBABILITY !!! + * Requires the broadcaster to come + * back on air within the BIG_Sync_ + * Timeout window — a narrow race. + * + * Both are normal: receiver will drop PA sync and re-discover. + */ + ESP_LOGI(TAG, "[BIS #%d] Disconnected, reason 0x%02x", + bis_chan_index_get(chan), reason); big_synced = false; out_big = NULL; - - for (size_t i = 0; i < BIS_ISO_CHAN_COUNT; i++) { - example_iso_rx_metrics_reset(&rx_metrics[i]); - } } static void iso_recv_cb(esp_ble_iso_chan_t *chan, @@ -83,14 +111,16 @@ static void iso_recv_cb(esp_ble_iso_chan_t *chan, const uint8_t *data, uint16_t len) { int chan_idx = bis_chan_index_get(chan); + char name[24]; if (chan_idx < 0) { - ESP_LOGW(TAG, "Unknown BIS channel %p", chan); + ESP_LOGW(TAG, "Unknown BIS channel"); return; } + snprintf(name, sizeof(name), "BIS #%d", chan_idx); rx_metrics[chan_idx].last_sdu_len = len; - example_iso_rx_metrics_on_recv(info, &rx_metrics[chan_idx], TAG, "chan", chan); + example_iso_rx_metrics_on_recv(info, &rx_metrics[chan_idx], TAG, name); } static esp_ble_iso_chan_ops_t iso_ops = { @@ -150,7 +180,7 @@ static void ext_scan_start(void) return; } - ESP_LOGI(TAG, "Extended scan started"); + ESP_LOGI(TAG, "Scanning for broadcaster..."); } static int pa_sync_create(uint8_t addr_type, uint8_t addr[6], uint8_t sid) @@ -213,26 +243,33 @@ static void ext_scan_recv(esp_ble_iso_gap_app_event_t *event) static void pa_sync(esp_ble_iso_gap_app_event_t *event) { + int err; + if (event->pa_sync.status) { ESP_LOGE(TAG, "PA sync failed, status %d", event->pa_sync.status); per_adv_synced = false; return; } - ESP_LOGI(TAG, "PA sync established:"); - ESP_LOGI(TAG, "sync_handle 0x%04x status 0x%02x addr %02x:%02x:%02x:%02x:%02x:%02x " - "sid %u adv_phy %u per_adv_itvl 0x%04x adv_ca %u", - event->pa_sync.sync_handle, event->pa_sync.status, + ESP_LOGI(TAG, "PA synced: handle %u sid %u phy %u peer %02x:%02x:%02x:%02x:%02x:%02x", + event->pa_sync.sync_handle, event->pa_sync.sid, event->pa_sync.adv_phy, event->pa_sync.addr.val[5], event->pa_sync.addr.val[4], event->pa_sync.addr.val[3], event->pa_sync.addr.val[2], - event->pa_sync.addr.val[1], event->pa_sync.addr.val[0], - event->pa_sync.sid, event->pa_sync.adv_phy, - event->pa_sync.per_adv_itvl, event->pa_sync.adv_ca); + event->pa_sync.addr.val[1], event->pa_sync.addr.val[0]); + + /* PA sync is established; the BIGInfo report will arrive via the + * PA sync channel, so the extended scanner is no longer needed. + * Stop it now — pa_sync_lost() will restart it on loss. + */ + err = ble_gap_disc_cancel(); + if (err) { + ESP_LOGW(TAG, "Failed to stop scanning, err %d", err); + } } static void pa_sync_lost(esp_ble_iso_gap_app_event_t *event) { - ESP_LOGI(TAG, "PA sync lost: sync_handle 0x%04x reason 0x%02x", + ESP_LOGI(TAG, "PA sync lost: sync_handle %u reason 0x%02x", event->pa_sync_lost.sync_handle, event->pa_sync_lost.reason); per_adv_synced = false; diff --git a/examples/bluetooth/esp_ble_iso/cis_central/README.md b/examples/bluetooth/esp_ble_iso/cis_central/README.md index 85ebcda9416..782a7a06770 100644 --- a/examples/bluetooth/esp_ble_iso/cis_central/README.md +++ b/examples/bluetooth/esp_ble_iso/cis_central/README.md @@ -5,51 +5,83 @@ (See the README.md file in the upper level `examples` directory for more information about examples.) -This example demonstrates how to use a **Connected Isochronous Stream (CIS)** as a central. It scans for a peripheral, establishes an ACL connection, creates a Connected Isochronous Group (CIG) and a CIS to the peer, and then sends isochronous data over the CIS every 10 ms. Run it together with the [cis_peripheral](../cis_peripheral) example on another device as the peer. +## Overview -The implementation uses the NimBLE host stack with ISO support and the ESP-BLE-ISO APIs (CIG create, CIS connect, data path, channel send). It is intended for chips that support BLE 5.2 ISO (e.g. ESP32-H4). The target peripheral name is hardcoded as `CIS Peripheral` and the connection security level is hardcoded as `ESP_BLE_ISO_SECURITY_MITM` (Level 3: encryption and authentication); the central initiates pairing after the ACL connection. +This is a raw BLE Connected Isochronous Stream (CIS) example operating directly at the ISO transport layer. It is **not** a BAP/CAP (BLE Audio profile) example — it does not implement Unicast Server/Client, ASCS, PACS, or any LC3 codec; it only exercises the underlying CIG/CIS plumbing. + +The central scans for a peer advertising the name `CIS Peripheral`, opens an ACL link, optionally pairs (security level `ESP_BLE_ISO_SECURITY_NO_MITM`), creates a single-CIS CIG (10 ms SDU interval, 2M PHY, RTN 2, 120-byte SDU, sequential/unframed), connects the CIS, configures the input data path to the HCI in transparent format, and then drives a software TX scheduler that submits one SDU every 10 ms. + +The transmitted payload is a dummy buffer filled with the current sequence number byte — there is no real audio data, the example just demonstrates the ISO transport mechanics on top of the NimBLE host with ISO support and the `esp_ble_iso_*` APIs. ## Requirements -* A board with Bluetooth LE 5.2 and ISO support (e.g. ESP32-H4) -* Another device running the [cis_peripheral](../cis_peripheral) example, which advertises and accepts the ACL and CIS +* A board with BLE 5.2 and ISO support (e.g. ESP32-H4, ESP32-S31) +* Peer device running the paired example -## How to Use Example +## Configuration -Before project configuration and build, set the correct chip target: +```bash +idf.py menuconfig +``` + +No build-time options — runtime defaults are baked into source. + +### Security & Pairing + +Just-Works pairing (LE Secure Connections, no MITM, `BLE_SM_IO_CAP_NO_IO`) with bonding enabled, inherited from the shared host init in `../common_components/example_init/ble_iso_example_init.c`. ISO examples do not register any GATT services (`gatts_register_cb = NULL`). + +## Build & Flash ```bash idf.py set-target esp32h4 -``` - -### Build and Flash - -Run the following to build, flash and monitor: - -```bash idf.py -p PORT flash monitor ``` -(To exit the serial monitor, type ``Ctrl-]``.) - -See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF. +(Exit serial monitor with `Ctrl-]`.) ## Example Flow -1. **Initialization**: NVS, Bluetooth stack (NimBLE), and ISO common layer (`esp_ble_iso_common_init`) with GAP callback for scan, ACL, and security events. -2. **Extended scan**: Start passive extended scanning. On scan report, parse the complete local name; when it matches the hardcoded target name `CIS Peripheral`, initiate an ACL connection to that device. -3. **ACL connection**: On connection established, initiate pairing (security level is hardcoded as `ESP_BLE_ISO_SECURITY_MITM`); the CIG and CIS are created after security is established. -4. **Security**: On security change (after pairing), create the CIG and CIS. -5. **CIG and CIS**: Create a CIG with one CIS (10 ms latency each direction, 10 ms SDU interval, sequential packing, unframed), then connect the CIS over the ACL handle. -6. **Send ISO data**: When the CIS is connected and the input data path is set up, a periodic TX scheduler based on `k_work_delayable` sends an SDU on the CIS every 10 ms in the ISO task context; sequence numbers and drift handling are applied. Note that the scheduler timer resolution is in milliseconds, which may not match the exact SDU interval for all configurations. +1. Initialize NVS, NimBLE host, and the ISO common layer with a GAP callback. +2. Start passive extended scanning for a device whose Complete Local Name is `CIS Peripheral`. +3. Cancel scan and create an ACL connection (interval 80 ms, supervision 5 s) once the target is matched. +4. On ACL connect, initiate pairing because the configured security level is `ESP_BLE_ISO_SECURITY_NO_MITM`. +5. After the security change, call `esp_ble_iso_cig_create` (one CIS, 10 ms latencies and SDU interval, sequential/unframed, SCA unknown) and `esp_ble_iso_chan_connect` over the ACL handle. +6. On CIS connect, set up the input data path (HCI / transparent) and start the periodic TX scheduler. +7. The scheduler invokes `esp_ble_iso_chan_send` every 10 ms with an incrementing sequence number on a 120-byte dummy SDU. -## Example Output +## Expected Log + +TAG: `CIS_CEN` + +Scan and connection phase: ``` -I (xxx) CIS_CEN: connection established, handle 0 status 0x00 -I (xxx) CIS_CEN: ISO channel 0x... connected -I (xxx) CIS_CEN: Transmitted 1000 ISO data packets (chan 0x...) -... +I CIS_CEN: Scanning for peripheral... +I CIS_CEN: Connected: handle <h> role <r> peer XX:XX:XX:XX:XX:XX +I CIS_CEN: Security: handle <h> level <l> bonded <b> ``` -If connection or security fails, relevant status or error messages are logged. +CIS setup and streaming phase (TX log emitted every `LOG_INTERVAL_PACKETS` SDUs by the shared utility): + +``` +I CIS_CEN: [CIS #0] Connected +I CIS_CEN: [CIS #0] TX: <count> packets +``` + +Disconnect path: + +``` +I CIS_CEN: [CIS #0] Disconnected, reason 0x<rr> +I CIS_CEN: Disconnected: handle <h> reason 0x<rr> +``` + +## Peer Pairing + +Run [cis_peripheral](../cis_peripheral/) on a second board. + +1. Flash and run `cis_peripheral` first; it begins extended advertising as `CIS Peripheral`. +2. Flash and run `cis_central` on the second board; it scans and matches that name. +3. The central creates the ACL connection and initiates pairing. +4. After the security change, the central creates the CIG and connects the CIS. +5. Both sides set up their data paths (input on central, output on peripheral). +6. The central streams 120-byte SDUs every 10 ms; the peripheral reports `RX: <count> packets` periodically. 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 e4cdc11dc31..9a24ae075b1 100644 --- a/examples/bluetooth/esp_ble_iso/cis_central/main/main.c +++ b/examples/bluetooth/esp_ble_iso/cis_central/main/main.c @@ -40,7 +40,7 @@ #define CONN_MIN_CE_LEN 0xFFFF #define CONN_DURATION 10000 /* 10s */ -#define SECURITY_LEVEL ESP_BLE_ISO_SECURITY_MITM +#define SECURITY_LEVEL ESP_BLE_ISO_SECURITY_NO_MITM #define CIG_LATENCY_MS 10 /* 10ms */ #define CIG_SDU_INTERVAL_US 10000 /* 10ms */ @@ -70,11 +70,11 @@ static void iso_connected_cb(esp_ble_iso_chan_t *chan) }; esp_err_t err; - ESP_LOGI(TAG, "ISO channel %p connected", chan); + ESP_LOGI(TAG, "[CIS #0] Connected"); err = esp_ble_iso_setup_data_path(chan, ESP_BLE_ISO_DATA_PATH_DIR_INPUT, &data_path); if (err) { - ESP_LOGE(TAG, "Failed to setup ISO data path, err %d", err); + ESP_LOGE(TAG, "[CIS #0] Failed to setup data path, err %d", err); return; } @@ -84,7 +84,7 @@ static void iso_connected_cb(esp_ble_iso_chan_t *chan) /* Note: esp timer is not accurate enough */ err = example_iso_tx_scheduler_start(&tx_scheduler, CIG_SDU_INTERVAL_US); if (err) { - ESP_LOGE(TAG, "Failed to start tx scheduler, err %d", err); + ESP_LOGE(TAG, "[CIS #0] Scheduler start failed, err %d", err); return; } @@ -95,17 +95,28 @@ static void iso_disconnected_cb(esp_ble_iso_chan_t *chan, uint8_t reason) { esp_err_t err; - ESP_LOGI(TAG, "ISO channel %p disconnected, reason 0x%02x", chan, reason); + ESP_LOGI(TAG, "[CIS #0] Disconnected, reason 0x%02x", reason); err = example_iso_tx_scheduler_stop(&tx_scheduler); if (err) { - ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err); + ESP_LOGE(TAG, "[CIS #0] Scheduler stop failed, err %d", err); + } + + /* Per BT Core 6.0 §7.7.5: a CIS on the Central retains its handle and + * data path across HCI_Disconnection_Complete (only the Peripheral side + * and other connection types get auto-deleted by the controller). So on + * the Central we must remove explicitly, otherwise the next setup on + * reconnect returns Command Disallowed. + */ + err = esp_ble_iso_remove_data_path(chan, ESP_BLE_ISO_DATA_PATH_DIR_INPUT); + if (err) { + ESP_LOGE(TAG, "[CIS #0] Failed to remove data path, err %d", err); } } static void iso_sent_cb(esp_ble_iso_chan_t *chan, void *user_data) { - example_iso_tx_scheduler_on_sent(&tx_scheduler, user_data, TAG, "chan", chan); + example_iso_tx_scheduler_on_sent(&tx_scheduler, user_data, TAG, "CIS #0"); } static esp_ble_iso_chan_ops_t iso_ops = { @@ -181,7 +192,7 @@ static void iso_chan_send(void) sizeof(iso_data), iso_seq_num); if (err) { - ESP_LOGD(TAG, "Failed to transmit data on channel %p", &iso_chan); + ESP_LOGD(TAG, "[CIS #0] Send failed, err %d", err); return; } @@ -217,7 +228,7 @@ static void ext_scan_start(void) return; } - ESP_LOGI(TAG, "Extended scan started"); + ESP_LOGI(TAG, "Scanning for peripheral..."); } static int conn_create(uint8_t addr_type, uint8_t addr[6]) @@ -305,18 +316,16 @@ static void acl_connect(esp_ble_iso_gap_app_event_t *event) int err; if (event->acl_connect.status) { - ESP_LOGE(TAG, "connection failed, status %d", event->acl_connect.status); + ESP_LOGE(TAG, "Connection failed, status %d", event->acl_connect.status); acl_connected = false; return; } - ESP_LOGI(TAG, "Conn established:"); - ESP_LOGI(TAG, "conn_handle 0x%04x status 0x%02x role %u peer %02x:%02x:%02x:%02x:%02x:%02x", - event->acl_connect.conn_handle, event->acl_connect.status, - event->acl_connect.role, event->acl_connect.dst.val[5], - event->acl_connect.dst.val[4], event->acl_connect.dst.val[3], - event->acl_connect.dst.val[2], event->acl_connect.dst.val[1], - event->acl_connect.dst.val[0]); + ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x", + event->acl_connect.conn_handle, event->acl_connect.role, + event->acl_connect.dst.val[5], event->acl_connect.dst.val[4], + event->acl_connect.dst.val[3], event->acl_connect.dst.val[2], + event->acl_connect.dst.val[1], event->acl_connect.dst.val[0]); if (iso_chan.required_sec_level == ESP_BLE_ISO_SECURITY_NO_MITM || iso_chan.required_sec_level == ESP_BLE_ISO_SECURITY_MITM) { @@ -332,7 +341,7 @@ static void acl_connect(esp_ble_iso_gap_app_event_t *event) static void acl_disconnect(esp_ble_iso_gap_app_event_t *event) { - ESP_LOGI(TAG, "Conn terminated: conn_handle 0x%04x reason 0x%02x", + ESP_LOGI(TAG, "Disconnected: handle %u reason 0x%02x", event->acl_disconnect.conn_handle, event->acl_disconnect.reason); acl_connected = false; @@ -343,19 +352,14 @@ 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); + ESP_LOGE(TAG, "Security change failed, status %d", event->security_change.status); return; } - ESP_LOGI(TAG, "Security change:"); - ESP_LOGI(TAG, "conn_handle 0x%04x status 0x%02x role %u sec_level %u bonded %u " - "peer %02x:%02x:%02x:%02x:%02x:%02x", - event->security_change.conn_handle, event->security_change.status, - event->security_change.role, event->security_change.sec_level, - event->security_change.bonded, event->security_change.dst.val[5], - event->security_change.dst.val[4], event->security_change.dst.val[3], - event->security_change.dst.val[2], event->security_change.dst.val[1], - event->security_change.dst.val[0]); + ESP_LOGI(TAG, "Security: handle %u level %u bonded %u", + event->security_change.conn_handle, + event->security_change.sec_level, + event->security_change.bonded); create_cig_and_cis(event->security_change.conn_handle); } diff --git a/examples/bluetooth/esp_ble_iso/cis_central/sdkconfig.defaults b/examples/bluetooth/esp_ble_iso/cis_central/sdkconfig.defaults index aa6d8e1bb0b..4a31aac41b7 100644 --- a/examples/bluetooth/esp_ble_iso/cis_central/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_iso/cis_central/sdkconfig.defaults @@ -6,6 +6,7 @@ CONFIG_BT_ENABLED=y CONFIG_BT_BLUEDROID_ENABLED=n CONFIG_BT_NIMBLE_ENABLED=y CONFIG_BT_NIMBLE_EXT_ADV=y +CONFIG_BT_NIMBLE_NVS_PERSIST=y CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 CONFIG_BT_NIMBLE_ISO=y CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y diff --git a/examples/bluetooth/esp_ble_iso/cis_peripheral/README.md b/examples/bluetooth/esp_ble_iso/cis_peripheral/README.md index 0d9fde3e536..a3816d29c08 100644 --- a/examples/bluetooth/esp_ble_iso/cis_peripheral/README.md +++ b/examples/bluetooth/esp_ble_iso/cis_peripheral/README.md @@ -5,52 +5,84 @@ (See the README.md file in the upper level `examples` directory for more information about examples.) -This example demonstrates how to use a **Connected Isochronous Stream (CIS)** as a peripheral. It starts connectable extended advertising with the name `CIS Peripheral`, waits for a central to connect and set up an isochronous channel, and then receives and counts isochronous data on the CIS. Run it together with the [cis_central](../cis_central) example on another device as the central. +## Overview -The implementation uses the NimBLE host stack with ISO support and the ESP-BLE-ISO APIs (ISO server register, accept callback, data path, channel receive). It is intended for chips that support BLE 5.2 ISO (e.g. ESP32-H4). The connection security level is hardcoded as `ESP_BLE_ISO_SECURITY_MITM` (Level 3: encryption and authentication); it must match the [cis_central](../cis_central) side for pairing to succeed. +This is a raw BLE Connected Isochronous Stream (CIS) example operating directly at the ISO transport layer. It is **not** a BAP/CAP (BLE Audio profile) example — it does not implement Unicast Server, ASCS, PACS, or any LC3 codec; it only exercises the underlying CIS accept/receive plumbing. + +The peripheral starts connectable extended advertising under the name `CIS Peripheral` (1M primary / 2M secondary PHY, 200 ms interval), registers an ISO server with security level `ESP_BLE_ISO_SECURITY_NO_MITM` and an accept callback that hands out the single CIS channel slot, sets up the output data path to the HCI in transparent format when the CIS connects, and tallies received SDUs through the shared RX-metrics helper. + +There is no audio decoding — incoming SDUs are simply counted (valid / error / lost / null), so any 120-byte dummy payload from the central is consumed as opaque data by the NimBLE host with ISO support and the `esp_ble_iso_*` APIs. ## Requirements -* A board with Bluetooth LE 5.2 and ISO support (e.g. ESP32-H4) -* Another device running the [cis_central](../cis_central) example, which scans, connects, and creates the CIS +* A board with BLE 5.2 and ISO support (e.g. ESP32-H4, ESP32-S31) +* Peer device running the paired example -## How to Use Example +## Configuration -Before project configuration and build, set the correct chip target: +```bash +idf.py menuconfig +``` + +No build-time options — runtime defaults are baked into source. + +### Security & Pairing + +Just-Works pairing (LE Secure Connections, no MITM, `BLE_SM_IO_CAP_NO_IO`) with bonding enabled, inherited from the shared host init in `../common_components/example_init/ble_iso_example_init.c`. ISO examples do not register any GATT services (`gatts_register_cb = NULL`). + +## Build & Flash ```bash idf.py set-target esp32h4 -``` - -### Build and Flash - -Run the following to build, flash and monitor: - -```bash idf.py -p PORT flash monitor ``` -(To exit the serial monitor, type ``Ctrl-]``.) - -See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF. +(Exit serial monitor with `Ctrl-]`.) ## Example Flow -1. **Initialization**: NVS, Bluetooth stack (NimBLE), and ISO common layer (`esp_ble_iso_common_init`) with GAP callback for ACL and security events. -2. **ISO server**: Register an ISO server with an accept callback and the configured security level. When a central requests a CIS, the accept callback returns the single CIS channel. -3. **Extended advertising**: Start connectable extended advertising with the local name `CIS Peripheral` so the central can discover and connect. -4. **ACL connection**: When the central connects, the connection and optional pairing/security change are handled; the central then creates the CIG and CIS. -5. **CIS connected**: When the CIS is connected, set up the output data path (receive direction). Incoming ISO SDUs are delivered in the receive callback; the example counts valid, error, and lost packets and logs periodically. +1. Initialize NVS, NimBLE host, and the ISO common layer with a GAP callback. +2. Register an ISO server (`esp_ble_iso_server_register`) whose accept callback returns the single static CIS channel. +3. Build flags + complete-local-name AD payload and start connectable extended advertising on handle 0 at a 200 ms interval. +4. On ACL connect, log the peer; the central drives subsequent pairing and CIG/CIS creation. +5. When the CIS request arrives, the accept callback supplies the channel; on CIS connect, set up the output data path (HCI / transparent) and reset RX metrics. +6. Each received SDU updates valid/error/lost counters; periodic RX summaries are logged by the shared utility. +7. On ACL disconnect, restart extended advertising. -## Example Output +## Expected Log + +TAG: `CIS_PER` + +Advertising and connection phase: ``` -I (xxx) CIS_PER: Extended adv instance 0 started -I (xxx) CIS_PER: connection established, handle 0 status 0x00 -I (xxx) CIS_PER: Incoming request from 0x0000 -I (xxx) CIS_PER: ISO channel 0x... connected -I (xxx) CIS_PER: Received 1000(1000/0/0) ISO data packets (chan 0x...) -... +I CIS_PER: Advertising started (handle 0) +I CIS_PER: Connected: handle <h> role <r> peer XX:XX:XX:XX:XX:XX +I CIS_PER: Security: handle <h> level <l> bonded <b> ``` -If connection or security fails, relevant status or error messages are logged. +CIS accept and streaming phase (RX log emitted every `LOG_INTERVAL_PACKETS` SDUs by the shared utility): + +``` +I CIS_PER: Incoming CIS request from handle <h> +I CIS_PER: [CIS #0] Connected +I CIS_PER: [CIS #0] RX: <count> packets +``` + +Disconnect path: + +``` +I CIS_PER: [CIS #0] Disconnected, reason 0x<rr> +I CIS_PER: Disconnected: handle <h> reason 0x<rr> +``` + +## Peer Pairing + +Run [cis_central](../cis_central/) on a second board. + +1. Flash and run `cis_peripheral` first; it advertises as `CIS Peripheral` on extended-advertising handle 0. +2. Flash and run `cis_central` on the second board; it scans and matches the name. +3. The central opens the ACL connection and initiates pairing; this peripheral handles the security change passively. +4. The central creates the CIG and CIS; the peripheral's accept callback returns the local channel. +5. The peripheral configures its output data path on CIS connection. +6. Incoming 120-byte SDUs are counted and `RX: <count> packets` is logged periodically. diff --git a/examples/bluetooth/esp_ble_iso/cis_peripheral/main/main.c b/examples/bluetooth/esp_ble_iso/cis_peripheral/main/main.c index 173f38de0f0..2e704aa9484 100644 --- a/examples/bluetooth/esp_ble_iso/cis_peripheral/main/main.c +++ b/examples/bluetooth/esp_ble_iso/cis_peripheral/main/main.c @@ -35,7 +35,7 @@ #define ADV_SECONDARY_PHY BLE_HCI_LE_PHY_2M #define ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(200) -#define SECURITY_LEVEL ESP_BLE_ISO_SECURITY_MITM +#define SECURITY_LEVEL ESP_BLE_ISO_SECURITY_NO_MITM #define CIS_SDU_SIZE 120 @@ -51,11 +51,11 @@ static void iso_connected_cb(esp_ble_iso_chan_t *chan) }; esp_err_t err; - ESP_LOGI(TAG, "ISO channel %p connected", chan); + ESP_LOGI(TAG, "[CIS #0] Connected"); err = esp_ble_iso_setup_data_path(chan, ESP_BLE_ISO_DATA_PATH_DIR_OUTPUT, &data_path); if (err) { - ESP_LOGE(TAG, "Failed to setup ISO data path, err %d", err); + ESP_LOGE(TAG, "[CIS #0] Failed to setup data path, err %d", err); return; } @@ -64,16 +64,15 @@ static void iso_connected_cb(esp_ble_iso_chan_t *chan) static void iso_disconnected_cb(esp_ble_iso_chan_t *chan, uint8_t reason) { - ESP_LOGI(TAG, "ISO channel %p disconnected, reason 0x%02x", chan, reason); + ESP_LOGI(TAG, "[CIS #0] Disconnected, reason 0x%02x", reason); } static void iso_recv_cb(esp_ble_iso_chan_t *chan, const esp_ble_iso_recv_info_t *info, const uint8_t *data, uint16_t len) { - rx_metrics.last_sdu_len = len; - example_iso_rx_metrics_on_recv(info, &rx_metrics, TAG, "chan", chan); + example_iso_rx_metrics_on_recv(info, &rx_metrics, TAG, "CIS #0"); } static esp_ble_iso_chan_ops_t iso_ops = { @@ -99,7 +98,7 @@ static esp_ble_iso_chan_t iso_chan = { static int iso_accept(const esp_ble_iso_accept_info_t *info, esp_ble_iso_chan_t **chan) { - ESP_LOGI(TAG, "Incoming request from 0x%04x", info->acl->handle); + ESP_LOGI(TAG, "Incoming CIS request from handle %u", info->acl->handle); if (iso_chan.iso) { ESP_LOGE(TAG, "No channels available"); @@ -177,23 +176,26 @@ static void ext_adv_start(void) return; } - ESP_LOGI(TAG, "Extended adv instance %u started", ADV_HANDLE); + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); } static void acl_connect(esp_ble_iso_gap_app_event_t *event) { - ESP_LOGI(TAG, "Conn established:"); - ESP_LOGI(TAG, "conn_handle 0x%04x status 0x%02x role %u peer %02x:%02x:%02x:%02x:%02x:%02x", - event->acl_connect.conn_handle, event->acl_connect.status, - event->acl_connect.role, event->acl_connect.dst.val[5], - event->acl_connect.dst.val[4], event->acl_connect.dst.val[3], - event->acl_connect.dst.val[2], event->acl_connect.dst.val[1], - event->acl_connect.dst.val[0]); + if (event->acl_connect.status) { + ESP_LOGE(TAG, "Connection failed, status %d", event->acl_connect.status); + return; + } + + ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x", + event->acl_connect.conn_handle, event->acl_connect.role, + event->acl_connect.dst.val[5], event->acl_connect.dst.val[4], + event->acl_connect.dst.val[3], event->acl_connect.dst.val[2], + event->acl_connect.dst.val[1], event->acl_connect.dst.val[0]); } static void acl_disconnect(esp_ble_iso_gap_app_event_t *event) { - ESP_LOGI(TAG, "Conn terminated: conn_handle 0x%04x reason 0x%02x", + ESP_LOGI(TAG, "Disconnected: handle %u reason 0x%02x", event->acl_disconnect.conn_handle, event->acl_disconnect.reason); ext_adv_start(); @@ -201,15 +203,15 @@ static void acl_disconnect(esp_ble_iso_gap_app_event_t *event) static void security_change(esp_ble_iso_gap_app_event_t *event) { - ESP_LOGI(TAG, "Security change:"); - ESP_LOGI(TAG, "conn_handle 0x%04x status 0x%02x role %u sec_level %u bonded %u " - "peer %02x:%02x:%02x:%02x:%02x:%02x", - event->security_change.conn_handle, event->security_change.status, - event->security_change.role, event->security_change.sec_level, - event->security_change.bonded, event->security_change.dst.val[5], - event->security_change.dst.val[4], event->security_change.dst.val[3], - event->security_change.dst.val[2], event->security_change.dst.val[1], - event->security_change.dst.val[0]); + if (event->security_change.status) { + ESP_LOGE(TAG, "Security change failed, status %d", event->security_change.status); + return; + } + + ESP_LOGI(TAG, "Security: handle %u level %u bonded %u", + event->security_change.conn_handle, + event->security_change.sec_level, + event->security_change.bonded); } static void iso_gap_app_cb(esp_ble_iso_gap_app_event_t *event) diff --git a/examples/bluetooth/esp_ble_iso/cis_peripheral/sdkconfig.defaults b/examples/bluetooth/esp_ble_iso/cis_peripheral/sdkconfig.defaults index 36afc5e930d..ada2e5c53eb 100644 --- a/examples/bluetooth/esp_ble_iso/cis_peripheral/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_iso/cis_peripheral/sdkconfig.defaults @@ -6,6 +6,7 @@ CONFIG_BT_ENABLED=y CONFIG_BT_BLUEDROID_ENABLED=n CONFIG_BT_NIMBLE_ENABLED=y CONFIG_BT_NIMBLE_EXT_ADV=y +CONFIG_BT_NIMBLE_NVS_PERSIST=y CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 CONFIG_BT_NIMBLE_ISO=y CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y diff --git a/examples/bluetooth/esp_ble_iso/common_components/example_init/ble_iso_example_init.c b/examples/bluetooth/esp_ble_iso/common_components/example_init/ble_iso_example_init.c index 5a81fce8d83..1e78f18d344 100644 --- a/examples/bluetooth/esp_ble_iso/common_components/example_init/ble_iso_example_init.c +++ b/examples/bluetooth/esp_ble_iso/common_components/example_init/ble_iso_example_init.c @@ -21,7 +21,7 @@ #include "services/gap/ble_svc_gap.h" #include "services/gatt/ble_svc_gatt.h" -#define TAG "INIT" +#define TAG "EXAMPLE_INIT" static SemaphoreHandle_t example_iso_sem; @@ -74,15 +74,13 @@ esp_err_t bluetooth_init(void) ble_hs_cfg.reset_cb = example_iso_on_reset; ble_hs_cfg.sync_cb = example_iso_on_sync; ble_hs_cfg.store_status_cb = ble_store_util_status_rr; -#if 0 ble_hs_cfg.gatts_register_cb = NULL; - ble_hs_cfg.sm_our_key_dist = 1; - ble_hs_cfg.sm_their_key_dist = 1; + ble_hs_cfg.sm_our_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID; + ble_hs_cfg.sm_their_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID; ble_hs_cfg.sm_sc = 1; ble_hs_cfg.sm_mitm = 0; ble_hs_cfg.sm_bonding = 1; ble_hs_cfg.sm_io_cap = BLE_SM_IO_CAP_NO_IO; -#endif /* XXX Need to have template for store */ ble_store_config_init(); 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 ecc8369a4a2..831d5410dd6 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 @@ -17,7 +17,10 @@ #include "esp_ble_iso_common_api.h" -#define TAG "UTILS" +/* Packet count between throughput summary logs. + * At 10 ms SDU interval this is ~60 s; at 7.5 ms ~45 s. + */ +#define LOG_INTERVAL_PACKETS 6000 int example_iso_gap_event_cb(struct ble_gap_event *event, void *arg) { @@ -94,8 +97,7 @@ int example_iso_tx_scheduler_stop(example_iso_tx_scheduler_t *scheduler) void example_iso_tx_scheduler_on_sent(example_iso_tx_scheduler_t *scheduler, const esp_ble_iso_tx_cb_info_t *info, const char *tag, - const char *obj_name, - const void *obj) + const char *obj_name) { size_t drift_count = 0; @@ -111,9 +113,9 @@ void example_iso_tx_scheduler_on_sent(example_iso_tx_scheduler_t *scheduler, scheduler->drift += drift_count; scheduler->count++; - if (scheduler->count % 1000 == 0) { - ESP_LOGI(tag, "Transmitted %u ISO data packets (%s %p)", - scheduler->count, (obj_name ? obj_name : ""), obj); + if (scheduler->count % LOG_INTERVAL_PACKETS == 0) { + ESP_LOGI(tag, "[%s] TX: %u packets", + obj_name ? obj_name : "ISO", scheduler->count); } } @@ -132,8 +134,7 @@ void example_iso_rx_metrics_reset(example_iso_rx_metrics_t *metrics) void example_iso_rx_metrics_on_recv(const esp_ble_iso_recv_info_t *info, example_iso_rx_metrics_t *metrics, const char *tag, - const char *obj_name, - const void *obj) + const char *obj_name) { assert(info); assert(metrics); @@ -155,8 +156,8 @@ void example_iso_rx_metrics_on_recv(const esp_ble_iso_recv_info_t *info, metrics->recv_count++; - if (metrics->recv_count % 1000 == 0) { - ESP_LOGI(tag, "Received %u ISO data packets (%s %p)", - metrics->recv_count, (obj_name ? obj_name : ""), obj); + if (metrics->recv_count % LOG_INTERVAL_PACKETS == 0) { + ESP_LOGI(tag, "[%s] RX: %u packets", + obj_name ? obj_name : "ISO", metrics->recv_count); } } 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 d62e05ab285..9a329483df4 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 @@ -93,15 +93,13 @@ int example_iso_tx_scheduler_stop(example_iso_tx_scheduler_t *scheduler); void example_iso_tx_scheduler_on_sent(example_iso_tx_scheduler_t *scheduler, const esp_ble_iso_tx_cb_info_t *info, const char *tag, - const char *obj_name, - const void *obj); + const char *obj_name); void example_iso_rx_metrics_reset(example_iso_rx_metrics_t *metrics); void example_iso_rx_metrics_on_recv(const esp_ble_iso_recv_info_t *info, example_iso_rx_metrics_t *metrics, const char *tag, - const char *obj_name, - const void *obj); + const char *obj_name); #endif /* BLE_ISO_EXAMPLE_UTILS_H_ */