diff --git a/components/bt/CMakeLists.txt b/components/bt/CMakeLists.txt index 4b292e45ceb..9fe97c472fe 100644 --- a/components/bt/CMakeLists.txt +++ b/components/bt/CMakeLists.txt @@ -70,6 +70,7 @@ if(CONFIG_IDF_DOC_BUILD OR CONFIG_BT_ENABLED) add_subdirectory(esp_ble_audio) list(APPEND srcs ${ble_audio_srcs}) list(APPEND include_dirs ${ble_audio_include_dirs}) + list(APPEND ldscripts ${ble_audio_ldscripts}) # BLE profiles (host-agnostic profile cores + shared profile infrastructure) add_subdirectory(ble_profiles) diff --git a/components/bt/common/Kconfig.in b/components/bt/common/Kconfig.in index 98c828220f7..e46e796c8e3 100644 --- a/components/bt/common/Kconfig.in +++ b/components/bt/common/Kconfig.in @@ -230,6 +230,16 @@ menu "BT Logs" Enable mesh log encoding via the async transport. The per-level compression options are configured in: Settings of BLE Log Compression → BLE Mesh log compression. + + config BT_LOG_CRITICAL_ONLY_ISO + bool "ISO: bandwidth-optimized logging" + depends on BT_ISO + select BLE_ISO_COMPRESSED_LOG_ENABLE + default y + help + Enable BLE Audio/ISO log encoding via the async transport. + The per-level compression options are configured in: + Settings of BLE Log Compression → BLE Audio/ISO log compression. endif # BT_LOG_CRITICAL_ONLY source "$IDF_PATH/components/bt/common/ble_log/Kconfig.in" diff --git a/components/bt/esp_ble_audio/CMakeLists.txt b/components/bt/esp_ble_audio/CMakeLists.txt index fcc052ee98d..607eed952da 100644 --- a/components/bt/esp_ble_audio/CMakeLists.txt +++ b/components/bt/esp_ble_audio/CMakeLists.txt @@ -4,13 +4,30 @@ function(register_ble_audio_libs) endif() if(CONFIG_SOC_BLE_AUDIO_SUPPORTED) - add_prebuilt_library(ble_audio "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/lib/lib/${idf_target}/libble_audio.a") + # Compressed-log builds link the log_compressed/ prebuilt lib (its logs + # call bt_le_audio_lib_compressed_out(), which is only functional under + # the same config); otherwise link the normal lib. + set(ble_audio_lib_dir "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/lib/lib/${idf_target}") + if(CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE) + set(ble_audio_lib "${ble_audio_lib_dir}/log_compressed/libble_audio.a") + if(NOT EXISTS "${ble_audio_lib}") + message(FATAL_ERROR + "BLE_ISO_COMPRESSED_LOG_ENABLE is set but the compressed lib is missing:\n" + " ${ble_audio_lib}\n" + "Build and copy it (in lib/ble_audio_development/tools/lib/):\n" + " ./compile.sh ${idf_target} -c && ./copy_lib.sh ${idf_target} -c") + endif() + else() + set(ble_audio_lib "${ble_audio_lib_dir}/libble_audio.a") + endif() + add_prebuilt_library(ble_audio "${ble_audio_lib}") target_link_libraries(${COMPONENT_LIB} PRIVATE ble_audio) endif() endfunction() set(ble_audio_srcs "" PARENT_SCOPE) set(ble_audio_include_dirs "" PARENT_SCOPE) +set(ble_audio_ldscripts "" PARENT_SCOPE) # API headers that are used in the docs are also compiled # even if CONFIG_BT_ENABLED=n as long as CONFIG_IDF_DOC_BUILD=y @@ -144,13 +161,25 @@ endif() if(CONFIG_BT_OTS OR CONFIG_BT_OTS_CLIENT) list(APPEND ble_audio_include_dirs + "${CMAKE_CURRENT_LIST_DIR}/host/services" "${CMAKE_CURRENT_LIST_DIR}/host/services/ots" ) list(APPEND ble_audio_srcs "${CMAKE_CURRENT_LIST_DIR}/host/services/ots/ots_l2cap.c" + "${CMAKE_CURRENT_LIST_DIR}/host/services/ots/adapter/l2cap.c" ) + if(CONFIG_BT_NIMBLE_ENABLED) + list(APPEND ble_audio_srcs + "${CMAKE_CURRENT_LIST_DIR}/host/services/ots/adapter/nimble/l2cap.c" + ) + else() + list(APPEND ble_audio_srcs + "${CMAKE_CURRENT_LIST_DIR}/host/services/ots/adapter/bluedroid/l2cap.c" + ) + endif() + if(CONFIG_BT_OTS) list(APPEND ble_audio_srcs "${CMAKE_CURRENT_LIST_DIR}/host/services/ots/ots_oacp.c" @@ -176,3 +205,4 @@ endif() # Export the variables to the parent scope set(ble_audio_srcs "${ble_audio_srcs}" PARENT_SCOPE) set(ble_audio_include_dirs "${ble_audio_include_dirs}" PARENT_SCOPE) +set(ble_audio_ldscripts "${CMAKE_CURRENT_LIST_DIR}/linker.lf" PARENT_SCOPE) diff --git a/components/bt/esp_ble_audio/Kconfig.in b/components/bt/esp_ble_audio/Kconfig.in index ab5be28b4cd..f4da9615a9f 100644 --- a/components/bt/esp_ble_audio/Kconfig.in +++ b/components/bt/esp_ble_audio/Kconfig.in @@ -6,6 +6,9 @@ config BT_AUDIO bool # hidden + # esp_ble_audio needs esp_ble_iso's headers () and symbols + # (bt_gatt_*/bt_conn_*/k_*), and both are gated on BT_ISO. + select BT_ISO help This option enables Bluetooth Audio support. The specific features that are available may depend on other features @@ -99,6 +102,40 @@ menu "OTS Options" rsource "host/services/ots/Kconfig.ots.in" endmenu +config BT_AUDIO_BSS_SEG_EXTERNAL_MEMORY + bool "Place BLE Audio .bss segment in external SPIRAM" + depends on BT_ISO_BSS_SEG_EXTERNAL_MEMORY + default n + help + Place the zero-initialized (.bss) static data of the BLE Audio + profiles in external SPIRAM instead of internal DRAM. Covers both the + esp_ble_audio IDF source files and the .bss of the prebuilt BLE Audio + library (libble_audio.a), the latter relocated via a linker fragment. + + Depends on BT_ISO_BSS_SEG_EXTERNAL_MEMORY: the audio layer sits on top + of the ISO/host layer, so its static data is moved to SPIRAM only when + the ISO layer's is, keeping the placement coherent. + + Requires SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY. Only .bss is moved; + initialized (.data) statistics stay internal. + +config BT_AUDIO_HEAP_EXTERNAL_MEMORY + bool "Place BLE Audio heap allocations in external SPIRAM" + depends on BT_ISO_HEAP_EXTERNAL_MEMORY + default n + help + Route BLE Audio runtime heap allocations to external SPIRAM instead of + internal DRAM. Covers both the esp_ble_audio profiles and the prebuilt + BLE Audio library (libble_audio.a): both allocate via idf_ext_calloc / + idf_ext_malloc, which this switch points at the ISO layer's SPIRAM + helper (heap_caps_*(MALLOC_CAP_SPIRAM)). + + Depends on BT_ISO_HEAP_EXTERNAL_MEMORY: the audio allocator hook reuses + the ISO layer's helper, available only when the ISO layer routes to + SPIRAM too. + + On SPIRAM exhaustion allocations return NULL (no internal fallback). + config BT_AUDIO_NO_LOG bool "Disable BLE Audio Lib Debug Log" default n @@ -117,6 +154,8 @@ menu "BLE Audio Lib Debug Log Level" choice BT_AUDIO_LOG_LEVEL prompt "BLE_AUDIO_LIB_LOG_LEVEL" + default BT_AUDIO_LOG_LEVEL_INFO if BT_LOG_CRITICAL_ONLY_ISO + default BT_AUDIO_LOG_LEVEL_DEBUG if BLE_COMPRESSED_LOG_ENABLE default BT_AUDIO_LOG_LEVEL_WARNING depends on !BT_AUDIO_NO_LOG help diff --git a/components/bt/esp_ble_audio/api/esp_ble_audio_bap_api.c b/components/bt/esp_ble_audio/api/esp_ble_audio_bap_api.c index d7a401a61b7..c8653f3e2d7 100644 --- a/components/bt/esp_ble_audio/api/esp_ble_audio_bap_api.c +++ b/components/bt/esp_ble_audio/api/esp_ble_audio_bap_api.c @@ -6,11 +6,14 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include + #include "esp_ble_audio_bap_api.h" #if CONFIG_BT_BAP_UNICAST_SERVER esp_err_t esp_ble_audio_bap_unicast_server_register(const esp_ble_audio_bap_unicast_server_register_param_t *param) { + esp_err_t ret = ESP_OK; int err; if (param == NULL || @@ -20,20 +23,26 @@ esp_err_t esp_ble_audio_bap_unicast_server_register(const esp_ble_audio_bap_unic return ESP_ERR_INVALID_ARG; } - err = bt_bap_unicast_server_register_safe(param); + bt_le_host_lock(); + + err = bt_bap_unicast_server_register(param); if (err) { - return ESP_FAIL; + ret = ESP_FAIL; + goto end; } #if BLE_AUDIO_SVC_DEFERRED_ADD err = bt_le_ascs_init(); if (err) { - bt_bap_unicast_server_unregister_safe(); - return ESP_FAIL; + bt_bap_unicast_server_unregister(); + ret = ESP_FAIL; + goto end; } #endif /* BLE_AUDIO_SVC_DEFERRED_ADD */ - return ESP_OK; +end: + bt_le_host_unlock(); + return ret; } esp_err_t esp_ble_audio_bap_unicast_server_unregister(void) @@ -45,6 +54,12 @@ esp_err_t esp_ble_audio_bap_unicast_server_unregister(void) return ESP_FAIL; } +#if BLE_AUDIO_SVC_DEFERRED_ADD + if (bt_le_ascs_deinit()) { + return ESP_FAIL; + } +#endif /* BLE_AUDIO_SVC_DEFERRED_ADD */ + return ESP_OK; } @@ -150,6 +165,8 @@ esp_err_t esp_ble_audio_bap_unicast_group_reconfig(esp_ble_audio_bap_unicast_gro int err; if (unicast_group == NULL || param == NULL || + param->params == NULL || + param->params_count == 0 || param->params_count > CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT) { return ESP_ERR_INVALID_ARG; } @@ -727,22 +744,29 @@ esp_err_t esp_ble_audio_bap_broadcast_sink_delete(esp_ble_audio_bap_broadcast_si #if CONFIG_BT_BAP_SCAN_DELEGATOR esp_err_t esp_ble_audio_bap_scan_delegator_register(esp_ble_audio_bap_scan_delegator_cb_t *cb) { + esp_err_t ret = ESP_OK; int err; - err = bt_bap_scan_delegator_register_safe(cb); + bt_le_host_lock(); + + err = bt_bap_scan_delegator_register(cb); if (err) { - return ESP_FAIL; + ret = ESP_FAIL; + goto end; } #if BLE_AUDIO_SVC_DEFERRED_ADD err = bt_le_bass_init(); if (err) { - bt_bap_scan_delegator_unregister_safe(); - return ESP_FAIL; + bt_bap_scan_delegator_unregister(); + ret = ESP_FAIL; + goto end; } #endif /* BLE_AUDIO_SVC_DEFERRED_ADD */ - return ESP_OK; +end: + bt_le_host_unlock(); + return ret; } esp_err_t esp_ble_audio_bap_scan_delegator_unregister(void) @@ -754,6 +778,12 @@ esp_err_t esp_ble_audio_bap_scan_delegator_unregister(void) return ESP_FAIL; } +#if BLE_AUDIO_SVC_DEFERRED_ADD + if (bt_le_bass_deinit()) { + return ESP_FAIL; + } +#endif /* BLE_AUDIO_SVC_DEFERRED_ADD */ + return ESP_OK; } @@ -798,7 +828,8 @@ esp_err_t esp_ble_audio_bap_scan_delegator_add_src(const esp_ble_audio_bap_scan_ param->sid > BT_GAP_SID_MAX || param->pa_state > ESP_BLE_AUDIO_BAP_PA_STATE_NO_PAST || param->encrypt_state > ESP_BLE_AUDIO_BAP_BIG_ENC_STATE_BAD_CODE || - param->num_subgroups > CONFIG_BT_BAP_BASS_MAX_SUBGROUPS) { + param->num_subgroups > CONFIG_BT_BAP_BASS_MAX_SUBGROUPS || + (param->num_subgroups > 0 && param->subgroups == NULL)) { return ESP_ERR_INVALID_ARG; } @@ -818,7 +849,8 @@ esp_err_t esp_ble_audio_bap_scan_delegator_mod_src(const esp_ble_audio_bap_scan_ if (param == NULL || param->broadcast_id > ESP_BLE_AUDIO_BROADCAST_ID_MAX || - param->num_subgroups > CONFIG_BT_BAP_BASS_MAX_SUBGROUPS) { + param->num_subgroups > CONFIG_BT_BAP_BASS_MAX_SUBGROUPS || + (param->num_subgroups > 0 && param->subgroups == NULL)) { return ESP_ERR_INVALID_ARG; } @@ -959,7 +991,8 @@ esp_err_t esp_ble_audio_bap_broadcast_assistant_add_src(uint16_t conn_handle, param->broadcast_id > ESP_BLE_AUDIO_BROADCAST_ID_MAX || param->addr.type > BT_ADDR_LE_RANDOM || param->adv_sid > BT_GAP_SID_MAX || - param->num_subgroups > CONFIG_BT_BAP_BASS_MAX_SUBGROUPS) { + param->num_subgroups > CONFIG_BT_BAP_BASS_MAX_SUBGROUPS || + (param->num_subgroups > 0 && param->subgroups == NULL)) { return ESP_ERR_INVALID_ARG; } @@ -989,7 +1022,8 @@ esp_err_t esp_ble_audio_bap_broadcast_assistant_mod_src(uint16_t conn_handle, int err; if (param == NULL || - param->num_subgroups > CONFIG_BT_BAP_BASS_MAX_SUBGROUPS) { + param->num_subgroups > CONFIG_BT_BAP_BASS_MAX_SUBGROUPS || + (param->num_subgroups > 0 && param->subgroups == NULL)) { return ESP_ERR_INVALID_ARG; } @@ -1288,7 +1322,7 @@ esp_err_t esp_ble_audio_bap_base_foreach_subgroup(const esp_ble_audio_bap_base_t } err = bt_bap_base_foreach_subgroup(base, func, user_data); - if (err) { + if (err && err != -ECANCELED) { return ESP_FAIL; } @@ -1411,7 +1445,7 @@ esp_err_t esp_ble_audio_bap_base_subgroup_foreach_bis(const esp_ble_audio_bap_ba } err = bt_bap_base_subgroup_foreach_bis(subgroup, func, user_data); - if (err) { + if (err && err != -ECANCELED) { return ESP_FAIL; } diff --git a/components/bt/esp_ble_audio/api/esp_ble_audio_cap_api.c b/components/bt/esp_ble_audio/api/esp_ble_audio_cap_api.c index a6959bac8da..7d8647efd10 100644 --- a/components/bt/esp_ble_audio/api/esp_ble_audio_cap_api.c +++ b/components/bt/esp_ble_audio/api/esp_ble_audio_cap_api.c @@ -128,7 +128,10 @@ esp_err_t esp_ble_audio_cap_unicast_group_create(const esp_ble_audio_cap_unicast { int err; - if (param == NULL || unicast_group == NULL) { + if (param == NULL || unicast_group == NULL || + param->params == NULL || + param->params_count == 0 || + param->params_count > CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT) { return ESP_ERR_INVALID_ARG; } @@ -145,7 +148,10 @@ esp_err_t esp_ble_audio_cap_unicast_group_reconfig(esp_ble_audio_cap_unicast_gro { int err; - if (unicast_group == NULL || param == NULL) { + if (unicast_group == NULL || param == NULL || + param->params == NULL || + param->params_count == 0 || + param->params_count > CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT) { return ESP_ERR_INVALID_ARG; } @@ -255,7 +261,7 @@ esp_err_t esp_ble_audio_cap_initiator_unicast_audio_start(const esp_ble_audio_ca int err; if (param == NULL || param->count == 0 || param->stream_params == NULL || - param->count > CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT) { + param->count > (2 * CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT)) { return ESP_ERR_INVALID_ARG; } @@ -272,7 +278,7 @@ esp_err_t esp_ble_audio_cap_initiator_unicast_audio_update(const esp_ble_audio_c int err; if (param == NULL || param->count == 0 || param->stream_params == NULL || - param->count > CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT) { + param->count > (2 * CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT)) { return ESP_ERR_INVALID_ARG; } @@ -289,7 +295,7 @@ esp_err_t esp_ble_audio_cap_initiator_unicast_audio_stop(const esp_ble_audio_cap int err; if (param == NULL || param->count == 0 || param->streams == NULL || - param->count > CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT) { + param->count > (2 * CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT)) { return ESP_ERR_INVALID_ARG; } diff --git a/components/bt/esp_ble_audio/api/esp_ble_audio_codec_api.c b/components/bt/esp_ble_audio/api/esp_ble_audio_codec_api.c index 3dec72fa948..59891a22401 100644 --- a/components/bt/esp_ble_audio/api/esp_ble_audio_codec_api.c +++ b/components/bt/esp_ble_audio/api/esp_ble_audio_codec_api.c @@ -299,7 +299,7 @@ esp_err_t esp_ble_audio_codec_cfg_set_val(esp_ble_audio_codec_cfg_t *codec_cfg, if (codec_cfg == NULL || codec_cfg->data == NULL || data == NULL || - data_len == 0 || data_len > UINT8_MAX) { + data_len == 0 || data_len > UINT8_MAX - 1) { return ESP_ERR_INVALID_ARG; } @@ -357,7 +357,7 @@ esp_err_t esp_ble_audio_codec_cfg_meta_set_val(esp_ble_audio_codec_cfg_t *codec_ if (codec_cfg == NULL || codec_cfg->meta == NULL || data == NULL || - data_len == 0 || data_len > UINT8_MAX) { + data_len == 0 || data_len > UINT8_MAX - 1) { return ESP_ERR_INVALID_ARG; } @@ -490,7 +490,7 @@ esp_err_t esp_ble_audio_codec_cfg_meta_set_program_info(esp_ble_audio_codec_cfg_ if (codec_cfg == NULL || codec_cfg->meta == NULL || program_info == NULL || - program_info_len == 0 || program_info_len > UINT8_MAX) { + program_info_len == 0 || program_info_len > UINT8_MAX - 1) { return ESP_ERR_INVALID_ARG; } @@ -565,7 +565,7 @@ esp_err_t esp_ble_audio_codec_cfg_meta_set_ccid_list(esp_ble_audio_codec_cfg_t * if (codec_cfg == NULL || codec_cfg->meta == NULL || ccid_list == NULL || - ccid_list_len == 0 || ccid_list_len > UINT8_MAX) { + ccid_list_len == 0 || ccid_list_len > UINT8_MAX - 1) { return ESP_ERR_INVALID_ARG; } @@ -643,7 +643,7 @@ esp_err_t esp_ble_audio_codec_cfg_meta_set_program_info_uri(esp_ble_audio_codec_ if (codec_cfg == NULL || codec_cfg->meta == NULL || program_info_uri == NULL || - program_info_uri_len == 0 || program_info_uri_len > UINT8_MAX) { + program_info_uri_len == 0 || program_info_uri_len > UINT8_MAX - 1) { return ESP_ERR_INVALID_ARG; } @@ -795,7 +795,7 @@ esp_err_t esp_ble_audio_codec_cfg_meta_set_broadcast_name(esp_ble_audio_codec_cf if (codec_cfg == NULL || codec_cfg->meta == NULL || broadcast_name == NULL || - broadcast_name_len == 0 || broadcast_name_len > UINT8_MAX) { + broadcast_name_len == 0 || broadcast_name_len > UINT8_MAX - 1) { return ESP_ERR_INVALID_ARG; } @@ -836,7 +836,7 @@ esp_err_t esp_ble_audio_codec_cfg_meta_set_extended(esp_ble_audio_codec_cfg_t *c if (codec_cfg == NULL || codec_cfg->meta == NULL || extended_meta == NULL || - extended_meta_len == 0 || extended_meta_len > UINT8_MAX) { + extended_meta_len == 0 || extended_meta_len > UINT8_MAX - 1) { return ESP_ERR_INVALID_ARG; } @@ -877,7 +877,7 @@ esp_err_t esp_ble_audio_codec_cfg_meta_set_vendor(esp_ble_audio_codec_cfg_t *cod if (codec_cfg == NULL || codec_cfg->meta == NULL || vendor_meta == NULL || - vendor_meta_len == 0 || vendor_meta_len > UINT8_MAX) { + vendor_meta_len == 0 || vendor_meta_len > UINT8_MAX - 1) { return ESP_ERR_INVALID_ARG; } @@ -918,7 +918,7 @@ esp_err_t esp_ble_audio_codec_cap_set_val(esp_ble_audio_codec_cap_t *codec_cap, if (codec_cap == NULL || codec_cap->data == NULL || data == NULL || - data_len == 0 || data_len > UINT8_MAX) { + data_len == 0 || data_len > UINT8_MAX - 1) { return ESP_ERR_INVALID_ARG; } @@ -1181,7 +1181,7 @@ esp_err_t esp_ble_audio_codec_cap_meta_set_val(esp_ble_audio_codec_cap_t *codec_ if (codec_cap == NULL || codec_cap->meta == NULL || data == NULL || - data_len == 0 || data_len > UINT8_MAX) { + data_len == 0 || data_len > UINT8_MAX - 1) { return ESP_ERR_INVALID_ARG; } @@ -1313,7 +1313,7 @@ esp_err_t esp_ble_audio_codec_cap_meta_set_program_info(esp_ble_audio_codec_cap_ if (codec_cap == NULL || codec_cap->meta == NULL || program_info == NULL || - program_info_len == 0 || program_info_len > UINT8_MAX) { + program_info_len == 0 || program_info_len > UINT8_MAX - 1) { return ESP_ERR_INVALID_ARG; } @@ -1388,7 +1388,7 @@ esp_err_t esp_ble_audio_codec_cap_meta_set_ccid_list(esp_ble_audio_codec_cap_t * if (codec_cap == NULL || codec_cap->meta == NULL || ccid_list == NULL || - ccid_list_len == 0 || ccid_list_len > UINT8_MAX) { + ccid_list_len == 0 || ccid_list_len > UINT8_MAX - 1) { return ESP_ERR_INVALID_ARG; } @@ -1466,7 +1466,7 @@ esp_err_t esp_ble_audio_codec_cap_meta_set_program_info_uri(esp_ble_audio_codec_ if (codec_cap == NULL || codec_cap->meta == NULL || program_info_uri == NULL || - program_info_uri_len == 0 || program_info_uri_len > UINT8_MAX) { + program_info_uri_len == 0 || program_info_uri_len > UINT8_MAX - 1) { return ESP_ERR_INVALID_ARG; } @@ -1618,7 +1618,7 @@ esp_err_t esp_ble_audio_codec_cap_meta_set_broadcast_name(esp_ble_audio_codec_ca if (codec_cap == NULL || codec_cap->meta == NULL || broadcast_name == NULL || - broadcast_name_len == 0 || broadcast_name_len > UINT8_MAX) { + broadcast_name_len == 0 || broadcast_name_len > UINT8_MAX - 1) { return ESP_ERR_INVALID_ARG; } @@ -1659,7 +1659,7 @@ esp_err_t esp_ble_audio_codec_cap_meta_set_extended(esp_ble_audio_codec_cap_t *c if (codec_cap == NULL || codec_cap->meta == NULL || extended_meta == NULL || - extended_meta_len == 0 || extended_meta_len > UINT8_MAX) { + extended_meta_len == 0 || extended_meta_len > UINT8_MAX - 1) { return ESP_ERR_INVALID_ARG; } @@ -1700,7 +1700,7 @@ esp_err_t esp_ble_audio_codec_cap_meta_set_vendor(esp_ble_audio_codec_cap_t *cod if (codec_cap == NULL || codec_cap->meta == NULL || vendor_meta == NULL || - vendor_meta_len == 0 || vendor_meta_len > UINT8_MAX) { + vendor_meta_len == 0 || vendor_meta_len > UINT8_MAX - 1) { return ESP_ERR_INVALID_ARG; } diff --git a/components/bt/esp_ble_audio/api/esp_ble_audio_common_api.c b/components/bt/esp_ble_audio/api/esp_ble_audio_common_api.c index 6fd5765848f..5551a89b798 100644 --- a/components/bt/esp_ble_audio/api/esp_ble_audio_common_api.c +++ b/components/bt/esp_ble_audio/api/esp_ble_audio_common_api.c @@ -11,8 +11,12 @@ #include #include +#include + #include "esp_ble_audio_common_api.h" +LOG_MODULE_REGISTER(LEA_API, CONFIG_BT_ISO_LOG_LEVEL); + esp_err_t esp_ble_audio_data_parse(const uint8_t ltv[], size_t size, bool (*func)(uint8_t type, const uint8_t *data, @@ -65,7 +69,7 @@ esp_err_t esp_ble_audio_gattc_disc_start(uint16_t conn_handle) return ESP_OK; } -void esp_ble_audio_gap_app_post_event(uint8_t type, void *param) +void esp_ble_audio_gap_app_post_event(uint16_t type, void *param) { bt_le_gap_app_post_event(type, param); } @@ -127,6 +131,52 @@ unregister_gap: return ESP_FAIL; } +esp_err_t esp_ble_audio_common_deinit(const esp_ble_iso_deinit_info_t *info) +{ + bool reset_ext_adv = true; + bool reset_pa_sync = true; + int err; + + if (info) { + if (!info->reset_acl_conn) { + LOG_WRN("ConnKeepUnsupported"); + return ESP_ERR_NOT_SUPPORTED; + } + + reset_ext_adv = info->reset_ext_adv; + reset_pa_sync = info->reset_pa_sync; + } + + if (bt_le_host_check_idle()) { + return ESP_ERR_INVALID_STATE; + } + + err = bt_le_iso_task_deinit(); + if (err) { + return ESP_ERR_TIMEOUT; + } + + bt_le_audio_deinit(); + + err = bt_le_host_deinit(); + if (err) { + return ESP_ERR_TIMEOUT; + } + + if (reset_ext_adv) { + bt_le_ext_adv_state_reset(); + } + + if (reset_pa_sync) { + bt_le_per_adv_sync_state_reset(); + } + + bt_le_gatt_app_cb_unregister(); + bt_le_gap_app_cb_unregister(); + + return ESP_OK; +} + esp_err_t esp_ble_audio_common_start(esp_ble_audio_start_info_t *info) { int err; diff --git a/components/bt/esp_ble_audio/api/esp_ble_audio_csip_api.c b/components/bt/esp_ble_audio/api/esp_ble_audio_csip_api.c index a0de9a5f417..5113de49269 100644 --- a/components/bt/esp_ble_audio/api/esp_ble_audio_csip_api.c +++ b/components/bt/esp_ble_audio/api/esp_ble_audio_csip_api.c @@ -59,6 +59,10 @@ esp_err_t esp_ble_audio_csip_set_member_unregister(esp_ble_audio_csip_set_member return ESP_ERR_INVALID_ARG; } + if (bt_le_csis_deinit(svc_inst)) { + return ESP_FAIL; + } + err = bt_csip_set_member_unregister_safe(svc_inst); if (err) { return ESP_FAIL; @@ -276,7 +280,7 @@ esp_err_t esp_ble_audio_csip_set_coordinator_lock(const esp_ble_audio_csip_set_c { int err; - if (members == NULL || count > CONFIG_BT_MAX_CONN || set_info == NULL) { + if (members == NULL || count == 0 || count > CONFIG_BT_MAX_CONN || set_info == NULL) { return ESP_ERR_INVALID_ARG; } @@ -294,7 +298,7 @@ esp_err_t esp_ble_audio_csip_set_coordinator_release(const esp_ble_audio_csip_se { int err; - if (members == NULL || count > CONFIG_BT_MAX_CONN || set_info == NULL) { + if (members == NULL || count == 0 || count > CONFIG_BT_MAX_CONN || set_info == NULL) { return ESP_ERR_INVALID_ARG; } diff --git a/components/bt/esp_ble_audio/api/esp_ble_audio_gmap_api.c b/components/bt/esp_ble_audio/api/esp_ble_audio_gmap_api.c index c6282f61073..2fc423fb131 100644 --- a/components/bt/esp_ble_audio/api/esp_ble_audio_gmap_api.c +++ b/components/bt/esp_ble_audio/api/esp_ble_audio_gmap_api.c @@ -94,27 +94,30 @@ static bool valid_gmap_features(esp_ble_audio_gmap_role_t role, if ((role & ESP_BLE_AUDIO_GMAP_ROLE_UGG) != 0) { esp_ble_audio_gmap_ugg_feat_t ugg_feat = features.ugg_feat; + /* UGG TX (multiplex / 96kbps source) configures peer Sink ASEs → ASE_SNK. + * UGG RX (multisink) configures peer Source ASEs → ASE_SRC. + * (BT_AUDIO_TX/RX: client ASE_SNK = TX, ASE_SRC = RX.) */ if ((ugg_feat & ESP_BLE_AUDIO_GMAP_UGG_FEAT_MULTIPLEX) != 0 && - CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT == 0) { + CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT == 0) { /* Cannot support ESP_BLE_AUDIO_GMAP_UGG_FEAT_MULTIPLEX with - * CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT == 0. + * CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT == 0. */ return false; } if ((ugg_feat & ESP_BLE_AUDIO_GMAP_UGG_FEAT_96KBPS_SOURCE) != 0 && - CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT == 0) { + CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT == 0) { /* Cannot support ESP_BLE_AUDIO_GMAP_UGG_FEAT_96KBPS_SOURCE with - * CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT == 0. + * CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT == 0. */ return false; } if ((ugg_feat & ESP_BLE_AUDIO_GMAP_UGG_FEAT_MULTISINK) != 0 && - (CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT < 2 || + (CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT < 2 || CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT < 2)) { /* Cannot support ESP_BLE_AUDIO_GMAP_UGG_FEAT_MULTISINK with - * CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT or + * CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT or * CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT < 2. */ return false; diff --git a/components/bt/esp_ble_audio/api/esp_ble_audio_has_api.c b/components/bt/esp_ble_audio/api/esp_ble_audio_has_api.c index 7bb847c555b..331be5b9703 100644 --- a/components/bt/esp_ble_audio/api/esp_ble_audio_has_api.c +++ b/components/bt/esp_ble_audio/api/esp_ble_audio_has_api.c @@ -144,28 +144,35 @@ esp_err_t esp_ble_audio_has_client_preset_prev(esp_ble_audio_has_t *has, bool sy #if CONFIG_BT_HAS esp_err_t esp_ble_audio_has_register(const esp_ble_audio_has_features_param_t *features) { + esp_err_t ret = ESP_OK; int err; if (features == NULL) { return ESP_ERR_INVALID_ARG; } - err = bt_has_register_safe(features); + bt_le_host_lock(); + + err = bt_has_register(features); if (err) { - return ESP_FAIL; + ret = ESP_FAIL; + goto end; } #if BLE_AUDIO_SVC_DEFERRED_ADD err = bt_le_has_init(); if (err) { - /* TODO: rollback register_safe once lib exposes an unregister API; + /* TODO: rollback register once lib exposes an unregister API; * retry will hit -EALREADY. Only reachable on GATT alloc failure. */ - return ESP_FAIL; + ret = ESP_FAIL; + goto end; } #endif /* BLE_AUDIO_SVC_DEFERRED_ADD */ - return ESP_OK; +end: + bt_le_host_unlock(); + return ret; } esp_err_t esp_ble_audio_has_preset_register(const esp_ble_audio_has_preset_register_param_t *param) diff --git a/components/bt/esp_ble_audio/api/esp_ble_audio_media_proxy_api.c b/components/bt/esp_ble_audio/api/esp_ble_audio_media_proxy_api.c index 6169851617e..c1629079fa0 100644 --- a/components/bt/esp_ble_audio/api/esp_ble_audio_media_proxy_api.c +++ b/components/bt/esp_ble_audio/api/esp_ble_audio_media_proxy_api.c @@ -288,7 +288,8 @@ esp_err_t esp_ble_audio_media_proxy_ctrl_set_next_track_id(esp_ble_audio_media_p { int err; - if (player == NULL || ESP_BLE_AUDIO_MCS_VALID_OBJ_ID(id) == false) { + if (player == NULL || (IS_ENABLED(CONFIG_BT_MCTL_LOCAL_PLAYER_LOCAL_CONTROL) && + ESP_BLE_AUDIO_MCS_VALID_OBJ_ID(id) == false)) { return ESP_ERR_INVALID_ARG; } @@ -415,12 +416,19 @@ esp_err_t esp_ble_audio_media_proxy_ctrl_get_media_state(esp_ble_audio_media_pla return ESP_OK; } +#define MCS_VALID_OP(opcode) \ + (IN_RANGE((opcode), ESP_BLE_AUDIO_MCS_OPC_PLAY, ESP_BLE_AUDIO_MCS_OPC_STOP) || \ + (opcode == ESP_BLE_AUDIO_MCS_OPC_MOVE_RELATIVE) || \ + IN_RANGE((opcode), ESP_BLE_AUDIO_MCS_OPC_PREV_SEGMENT, ESP_BLE_AUDIO_MCS_OPC_GOTO_SEGMENT) || \ + IN_RANGE((opcode), ESP_BLE_AUDIO_MCS_OPC_PREV_TRACK, ESP_BLE_AUDIO_MCS_OPC_GOTO_TRACK) || \ + IN_RANGE((opcode), ESP_BLE_AUDIO_MCS_OPC_PREV_GROUP, ESP_BLE_AUDIO_MCS_OPC_GOTO_GROUP)) + esp_err_t esp_ble_audio_media_proxy_ctrl_send_command(esp_ble_audio_media_player_t *player, const esp_ble_audio_mpl_cmd_t *command) { int err; - if (player == NULL || command == NULL) { + if (player == NULL || command == NULL || MCS_VALID_OP(command->opcode) == false) { return ESP_ERR_INVALID_ARG; } @@ -453,7 +461,9 @@ esp_err_t esp_ble_audio_media_proxy_ctrl_send_search(esp_ble_audio_media_player_ { int err; - if (player == NULL || search == NULL) { + if (player == NULL || search == NULL || + IN_RANGE(search->len, ESP_BLE_AUDIO_SEARCH_LEN_MIN, + ESP_BLE_AUDIO_SEARCH_LEN_MAX) == false) { return ESP_ERR_INVALID_ARG; } @@ -561,24 +571,31 @@ esp_err_t esp_ble_audio_media_proxy_pl_register(esp_ble_audio_media_proxy_pl_cal esp_err_t esp_ble_audio_media_proxy_pl_init(void) { + esp_err_t ret = ESP_OK; int err; - err = bt_media_proxy_pl_init_safe(); + bt_le_host_lock(); + + err = bt_media_proxy_pl_init(); if (err) { - return ESP_FAIL; + ret = ESP_FAIL; + goto end; } #if CONFIG_BT_MCS && BLE_AUDIO_SVC_DEFERRED_ADD err = bt_le_media_proxy_pl_init(); if (err) { - /* TODO: rollback pl_init_safe once lib exposes an undo API; + /* TODO: rollback pl_init once lib exposes an undo API; * retry will hit -EALREADY. Only reachable on GATT alloc failure. */ - return ESP_FAIL; + ret = ESP_FAIL; + goto end; } #endif /* CONFIG_BT_MCS && BLE_AUDIO_SVC_DEFERRED_ADD */ - return ESP_OK; +end: + bt_le_host_unlock(); + return ret; } esp_err_t esp_ble_audio_media_proxy_pl_set_player_name(char *name) diff --git a/components/bt/esp_ble_audio/api/esp_ble_audio_micp_api.c b/components/bt/esp_ble_audio/api/esp_ble_audio_micp_api.c index 4c836ef448e..b7b26162037 100644 --- a/components/bt/esp_ble_audio/api/esp_ble_audio_micp_api.c +++ b/components/bt/esp_ble_audio/api/esp_ble_audio_micp_api.c @@ -10,6 +10,7 @@ #if CONFIG_BT_MICP_MIC_DEV esp_err_t esp_ble_audio_micp_mic_dev_register(esp_ble_audio_micp_mic_dev_register_param_t *param) { + esp_err_t ret = ESP_OK; int err; if (param == NULL || @@ -17,22 +18,28 @@ esp_err_t esp_ble_audio_micp_mic_dev_register(esp_ble_audio_micp_mic_dev_registe return ESP_ERR_INVALID_ARG; } - err = bt_micp_mic_dev_register_safe(param); + bt_le_host_lock(); + + err = bt_micp_mic_dev_register(param); if (err) { - return ESP_FAIL; + ret = ESP_FAIL; + goto end; } #if BLE_AUDIO_SVC_DEFERRED_ADD err = bt_le_micp_mic_dev_init(); if (err) { - /* TODO: rollback register_safe once lib exposes an unregister API; + /* TODO: rollback register once lib exposes an unregister API; * retry will hit -EALREADY. Only reachable on GATT alloc failure. */ - return ESP_FAIL; + ret = ESP_FAIL; + goto end; } #endif /* BLE_AUDIO_SVC_DEFERRED_ADD */ - return ESP_OK; +end: + bt_le_host_unlock(); + return ret; } esp_err_t esp_ble_audio_micp_mic_dev_included_get(esp_ble_audio_micp_included_t *included) diff --git a/components/bt/esp_ble_audio/api/esp_ble_audio_pacs_api.c b/components/bt/esp_ble_audio/api/esp_ble_audio_pacs_api.c index 5050ccc73df..83efc238e96 100644 --- a/components/bt/esp_ble_audio/api/esp_ble_audio_pacs_api.c +++ b/components/bt/esp_ble_audio/api/esp_ble_audio_pacs_api.c @@ -34,6 +34,10 @@ esp_err_t esp_ble_audio_pacs_unregister(void) return ESP_FAIL; } + if (bt_le_pacs_deinit()) { + return ESP_FAIL; + } + return ESP_OK; } diff --git a/components/bt/esp_ble_audio/api/esp_ble_audio_pbp_api.c b/components/bt/esp_ble_audio/api/esp_ble_audio_pbp_api.c index be837df7ec0..3f7523fb281 100644 --- a/components/bt/esp_ble_audio/api/esp_ble_audio_pbp_api.c +++ b/components/bt/esp_ble_audio/api/esp_ble_audio_pbp_api.c @@ -16,7 +16,7 @@ esp_err_t esp_ble_audio_pbp_get_announcement(const uint8_t meta[], size_t meta_l int err; if (pba_data_buf == NULL || - pba_data_buf->size < (meta_len + ESP_BLE_AUDIO_PBP_MIN_PBA_SIZE)) { + net_buf_simple_tailroom(pba_data_buf) < (meta_len + ESP_BLE_AUDIO_PBP_MIN_PBA_SIZE)) { return ESP_ERR_INVALID_ARG; } diff --git a/components/bt/esp_ble_audio/api/esp_ble_audio_tbs_api.c b/components/bt/esp_ble_audio/api/esp_ble_audio_tbs_api.c index 8169c4bb679..94ef680d2c7 100644 --- a/components/bt/esp_ble_audio/api/esp_ble_audio_tbs_api.c +++ b/components/bt/esp_ble_audio/api/esp_ble_audio_tbs_api.c @@ -337,27 +337,34 @@ static bool valid_register_param(const esp_ble_audio_tbs_register_param_t *param esp_err_t esp_ble_audio_tbs_register_bearer(const esp_ble_audio_tbs_register_param_t *param, uint8_t *bearer_index) { - int ret; + esp_err_t ret = ESP_OK; + int err; if (param == NULL || valid_register_param(param) == false || bearer_index == NULL) { return ESP_ERR_INVALID_ARG; } - ret = bt_tbs_register_bearer_safe(param); - if (ret < 0) { - return ESP_FAIL; + bt_le_host_lock(); + + err = bt_tbs_register_bearer(param); + if (err < 0) { + ret = ESP_FAIL; + goto end; } - *bearer_index = ret; + *bearer_index = err; #if BLE_AUDIO_SVC_DEFERRED_ADD if (param->gtbs ? bt_le_gtbs_init() : bt_le_tbs_init()) { - bt_tbs_unregister_bearer_safe(*bearer_index); - return ESP_FAIL; + bt_tbs_unregister_bearer(*bearer_index); + ret = ESP_FAIL; + goto end; } #endif /* BLE_AUDIO_SVC_DEFERRED_ADD */ - return ESP_OK; +end: + bt_le_host_unlock(); + return ret; } esp_err_t esp_ble_audio_tbs_unregister_bearer(uint8_t bearer_index) @@ -369,6 +376,16 @@ esp_err_t esp_ble_audio_tbs_unregister_bearer(uint8_t bearer_index) return ESP_FAIL; } +#if BLE_AUDIO_SVC_DEFERRED_ADD + if (bearer_index == ESP_BLE_AUDIO_TBS_GTBS_INDEX) { + if (bt_le_gtbs_deinit()) { + return ESP_FAIL; + } + } else if (bt_le_tbs_deinit(bearer_index)) { + return ESP_FAIL; + } +#endif /* BLE_AUDIO_SVC_DEFERRED_ADD */ + return ESP_OK; } #endif /* CONFIG_BT_TBS */ diff --git a/components/bt/esp_ble_audio/api/esp_ble_audio_tmap_api.c b/components/bt/esp_ble_audio/api/esp_ble_audio_tmap_api.c index 14f3197dc88..e2330ea0743 100644 --- a/components/bt/esp_ble_audio/api/esp_ble_audio_tmap_api.c +++ b/components/bt/esp_ble_audio/api/esp_ble_audio_tmap_api.c @@ -11,24 +11,31 @@ #if CONFIG_BT_TMAP esp_err_t esp_ble_audio_tmap_register(esp_ble_audio_tmap_role_t role) { + esp_err_t ret = ESP_OK; int err; - err = bt_tmap_register_safe(role); + bt_le_host_lock(); + + err = bt_tmap_register(role); if (err) { - return ESP_FAIL; + ret = ESP_FAIL; + goto end; } #if BLE_AUDIO_SVC_DEFERRED_ADD err = bt_le_tmas_init(); if (err) { - /* TODO: rollback register_safe once lib exposes an unregister API; + /* TODO: rollback register once lib exposes an unregister API; * retry will hit -EALREADY. Only reachable on GATT alloc failure. */ - return ESP_FAIL; + ret = ESP_FAIL; + goto end; } #endif /* BLE_AUDIO_SVC_DEFERRED_ADD */ - return ESP_OK; +end: + bt_le_host_unlock(); + return ret; } esp_err_t esp_ble_audio_tmap_discover(uint16_t conn_handle, @@ -38,6 +45,10 @@ esp_err_t esp_ble_audio_tmap_discover(uint16_t conn_handle, void *conn; int err; + if (tmap_cb == NULL) { + return ESP_ERR_INVALID_ARG; + } + bt_le_host_lock(); conn = bt_le_acl_conn_find(conn_handle); diff --git a/components/bt/esp_ble_audio/api/esp_ble_audio_vcp_api.c b/components/bt/esp_ble_audio/api/esp_ble_audio_vcp_api.c index 5e90d3bce7e..7ff0ef48500 100644 --- a/components/bt/esp_ble_audio/api/esp_ble_audio_vcp_api.c +++ b/components/bt/esp_ble_audio/api/esp_ble_audio_vcp_api.c @@ -26,6 +26,7 @@ esp_err_t esp_ble_audio_vcp_vol_rend_included_get(esp_ble_audio_vcp_included_t * esp_err_t esp_ble_audio_vcp_vol_rend_register(esp_ble_audio_vcp_vol_rend_register_param_t *param) { + esp_err_t ret = ESP_OK; int err; if (param == NULL) { @@ -44,22 +45,28 @@ esp_err_t esp_ble_audio_vcp_vol_rend_register(esp_ble_audio_vcp_vol_rend_registe return ESP_ERR_INVALID_ARG; } - err = bt_vcp_vol_rend_register_safe(param); + bt_le_host_lock(); + + err = bt_vcp_vol_rend_register(param); if (err) { - return ESP_FAIL; + ret = ESP_FAIL; + goto end; } #if BLE_AUDIO_SVC_DEFERRED_ADD err = bt_le_vcp_vol_rend_init(); if (err) { - /* TODO: rollback register_safe once lib exposes an unregister API; + /* TODO: rollback register once lib exposes an unregister API; * retry will hit -EALREADY. Only reachable on GATT alloc failure. */ - return ESP_FAIL; + ret = ESP_FAIL; + goto end; } #endif /* BLE_AUDIO_SVC_DEFERRED_ADD */ - return ESP_OK; +end: + bt_le_host_unlock(); + return ret; } esp_err_t esp_ble_audio_vcp_vol_rend_set_step(uint8_t volume_step) diff --git a/components/bt/esp_ble_audio/api/include/esp_ble_audio_common_api.h b/components/bt/esp_ble_audio/api/include/esp_ble_audio_common_api.h index 6ff3cc7e64a..abc2efce177 100644 --- a/components/bt/esp_ble_audio/api/include/esp_ble_audio_common_api.h +++ b/components/bt/esp_ble_audio/api/include/esp_ble_audio_common_api.h @@ -102,7 +102,17 @@ esp_err_t esp_ble_audio_gattc_disc_start(uint16_t conn_handle); #define ESP_BLE_AUDIO_GAP_EVENT_ACL_DISCONNECT BT_LE_GAP_APP_EVENT_ACL_DISCONNECT /*!< Audio GAP Security Change event */ #define ESP_BLE_AUDIO_GAP_EVENT_SECURITY_CHANGE BT_LE_GAP_APP_EVENT_SECURITY_CHANGE -/** Audio GAP application event structure */ +/** + * @brief Audio GAP application event structure + * + * @note Addresses carried by these events are in the **active host's own byte + * order**: on-air/LSB-first under NimBLE, MSB-first under Bluedroid + * (the order every `esp_ble_gap_*` API takes). Feeding one straight back + * to a host API is therefore always correct; comparing one against an + * address the audio layer holds, such as a Broadcast Receive State, + * needs a reversal under Bluedroid. See `struct bt_le_addr` in + * common/app/gap.h for the full convention. + */ typedef struct bt_le_gap_app_event esp_ble_audio_gap_app_event_t; /*!< Audio GATT MTU exchange complete event */ @@ -121,24 +131,51 @@ typedef struct { /** * @brief Post an application-layer GAP event for audio internal usage. * - * @note This function is only needed while using NimBLE Host. + * @note NimBLE-only: NimBLE has no global GAP callback, so the layers below + * see only what the application forwards here. An event left out is + * dropped silently. Bluedroid's adapter hooks the stack directly. * - * @param type Event type. - * @param param Event parameters. + * @note Forward every event below that the application's flows can produce. + * Forwarding an unused one is harmless; omitting one leaves the + * matching state uncreated and fails later and elsewhere: + * - BLE_GAP_EVENT_EXT_DISC extended advertising report + * - BLE_GAP_EVENT_CONNECT ACL established or failed + * - BLE_GAP_EVENT_DISCONNECT ACL closed + * - BLE_GAP_EVENT_ENC_CHANGE encryption changed + * - BLE_GAP_EVENT_PERIODIC_SYNC PA sync established + * - BLE_GAP_EVENT_PERIODIC_SYNC_LOST PA sync lost + * - BLE_GAP_EVENT_PERIODIC_REPORT periodic advertising report + * - BLE_GAP_EVENT_PERIODIC_TRANSFER PA sync received over PAST + * - BLE_GAP_EVENT_PERIODIC_TRANSFER_V2 as above, PAwR + * + * GATT events go to esp_ble_audio_gatt_app_post_event() instead. + * + * @note Shares a sink with esp_ble_iso_gap_app_post_event(): forward a given + * event through exactly one of them or it is delivered twice. + * + * @param type Event type, i.e. ble_gap_event::type. + * @param param Event parameters, i.e. the struct ble_gap_event pointer. */ -void esp_ble_audio_gap_app_post_event(uint8_t type, void *param); +void esp_ble_audio_gap_app_post_event(uint16_t type, void *param); #if !CONFIG_BT_BLUEDROID_ENABLED /** * @brief Post an application-layer GATT event for audio internal usage. * - * @note NimBLE-only. Bluedroid dispatches GATT events directly inside the - * adapter (BTA callbacks), so no app-level post is needed. This - * declaration is hidden from Bluedroid builds to make misuse a - * compile-time error. + * @note NimBLE-only: Bluedroid dispatches GATT events inside the adapter, so + * no app-level post is needed. Hidden from Bluedroid builds to make + * misuse a compile-time error. * - * @param type Event type. - * @param param Event parameters. + * @note Forward all four below here rather than through + * esp_ble_audio_gap_app_post_event() - GATT has no ISO counterpart. + * Omitting one is silent; ASCS and PACS simply never react. + * - BLE_GAP_EVENT_MTU ATT MTU exchanged + * - BLE_GAP_EVENT_SUBSCRIBE peer wrote a CCC descriptor + * - BLE_GAP_EVENT_NOTIFY_RX notification or indication received + * - BLE_GAP_EVENT_NOTIFY_TX notification or indication sent + * + * @param type Event type, i.e. ble_gap_event::type. + * @param param Event parameters, i.e. the struct ble_gap_event pointer. */ void esp_ble_audio_gatt_app_post_event(uint8_t type, void *param); #endif /* !CONFIG_BT_BLUEDROID_ENABLED */ @@ -163,6 +200,30 @@ typedef struct { uint8_t dummy; /*!< Dummy field to avoid empty struct */ } esp_ble_audio_start_info_t; +/** + * @brief Deinitialize BLE Audio. + * + * The only precondition is that the application has stopped its streams: a live + * stream always has a CIS under it, so one still up makes this return + * ESP_ERR_INVALID_STATE with each offending item logged at ERROR level. + * + * @note Registered profiles need no prior unregister - this releases them + * itself, while the host lock and GATT application are still alive. + * The per-profile esp_ble_audio_*_unregister() calls are for dropping + * one profile while audio stays up. + * + * @note Also releases the ISO layer, so esp_ble_iso_common_deinit() must not + * be called as well. @p info has the same meaning as there. + * + * @param info Which ISO-layer records to clear, or NULL to clear all. + * + * @return ESP_OK on success, + * ESP_ERR_INVALID_STATE if something is still active, + * ESP_ERR_TIMEOUT if the ISO task could not be stopped. + * Nothing is released unless ESP_OK is returned. + */ +esp_err_t esp_ble_audio_common_deinit(const esp_ble_iso_deinit_info_t *info); + /** * @brief Start BLE Audio services. * diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/init.h b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/init.h index 4923388a935..2c1819c9641 100644 --- a/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/init.h +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/init.h @@ -29,6 +29,8 @@ extern "C" { int bt_le_bluedroid_audio_init(void); +void bt_le_bluedroid_audio_deinit(void); + int bt_le_bluedroid_media_proxy_pl_init(void); int bt_le_bluedroid_vcp_vol_rend_init(void); diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/ascs.h b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/ascs.h index 97f0996a21d..b7812aa7788 100644 --- a/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/ascs.h +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/ascs.h @@ -16,6 +16,8 @@ extern "C" { int bt_le_bluedroid_ascs_init(void); +int bt_le_bluedroid_ascs_deinit(void); + int bt_le_bluedroid_ascs_start(void); #ifdef __cplusplus diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/bass.h b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/bass.h index c4073352610..9f56d4ba29f 100644 --- a/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/bass.h +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/bass.h @@ -16,6 +16,8 @@ extern "C" { int bt_le_bluedroid_bass_init(void); +int bt_le_bluedroid_bass_deinit(void); + int bt_le_bluedroid_bass_start(void); #ifdef __cplusplus diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/csis.h b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/csis.h index 8038bd8a4d3..b0ed3268d96 100644 --- a/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/csis.h +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/csis.h @@ -14,8 +14,12 @@ extern "C" { #endif +void bt_le_bluedroid_csis_state_reset(void); + int bt_le_bluedroid_csis_init(void *csis_svc, uint8_t count); +int bt_le_bluedroid_csis_deinit(void *csis_svc); + int bt_le_bluedroid_csis_start(void); #ifdef __cplusplus diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/mcs.h b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/mcs.h index f9d9b289c09..ca28dcc40ad 100644 --- a/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/mcs.h +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/mcs.h @@ -14,6 +14,8 @@ extern "C" { #endif +void bt_le_bluedroid_mcs_state_reset(void); + int bt_le_bluedroid_gmcs_init(void); int bt_le_bluedroid_gmcs_start(void); diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/mics.h b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/mics.h index f5bcca9ce65..14dee7ce075 100644 --- a/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/mics.h +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/mics.h @@ -14,6 +14,8 @@ extern "C" { #endif +void bt_le_bluedroid_mics_state_reset(void); + int bt_le_bluedroid_mics_init(void *micp_inc); int bt_le_bluedroid_mics_start(void); diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/pacs.h b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/pacs.h index f36e8e76e80..0198b1dacc2 100644 --- a/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/pacs.h +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/pacs.h @@ -16,6 +16,8 @@ extern "C" { int bt_le_bluedroid_pacs_init(void); +int bt_le_bluedroid_pacs_deinit(void); + int bt_le_bluedroid_pacs_start(void); #ifdef __cplusplus diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/tbs.h b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/tbs.h index b866089ccae..be218c7537b 100644 --- a/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/tbs.h +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/tbs.h @@ -16,10 +16,14 @@ extern "C" { int bt_le_bluedroid_gtbs_init(void); +int bt_le_bluedroid_gtbs_deinit(void); + int bt_le_bluedroid_gtbs_start(void); int bt_le_bluedroid_tbs_init(void); +int bt_le_bluedroid_tbs_deinit(uint8_t bearer_index); + int bt_le_bluedroid_tbs_start(void); #ifdef __cplusplus diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/vcs.h b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/vcs.h index a7c797dd6e2..ee3ed3b4da6 100644 --- a/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/vcs.h +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/vcs.h @@ -14,6 +14,8 @@ extern "C" { #endif +void bt_le_bluedroid_vcs_state_reset(void); + int bt_le_bluedroid_vcs_init(void *vcp_inc); int bt_le_bluedroid_vcs_start(void); diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/server.h b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/server.h index ab5b69aa73b..833d8d2778c 100644 --- a/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/server.h +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/server.h @@ -162,10 +162,14 @@ enum { void bt_le_bluedroid_audio_gatts_init(void); +void bt_le_bluedroid_audio_gatts_deinit(void); + int bt_le_bluedroid_set_svc_in_progress(uint8_t value); int bt_le_bluedroid_svc_init(struct bt_gatt_service *svc); +int bt_le_bluedroid_svc_deinit(struct bt_gatt_service *svc); + int bt_le_bluedroid_svc_start(struct bt_gatt_service *svc); #ifdef __cplusplus diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/init.c b/components/bt/esp_ble_audio/host/adapter/bluedroid/init.c index 5383acb9abf..c2a0cafe95f 100644 --- a/components/bt/esp_ble_audio/host/adapter/bluedroid/init.c +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/init.c @@ -71,6 +71,19 @@ int bt_le_bluedroid_audio_init(void) bt_le_bluedroid_audio_gatts_init(); +#if CONFIG_BT_CSIP_SET_MEMBER + bt_le_bluedroid_csis_state_reset(); +#endif /* CONFIG_BT_CSIP_SET_MEMBER */ +#if CONFIG_BT_VCP_VOL_REND + bt_le_bluedroid_vcs_state_reset(); +#endif /* CONFIG_BT_VCP_VOL_REND */ +#if CONFIG_BT_MICP_MIC_DEV + bt_le_bluedroid_mics_state_reset(); +#endif /* CONFIG_BT_MICP_MIC_DEV */ +#if CONFIG_BT_MCS + bt_le_bluedroid_mcs_state_reset(); +#endif /* CONFIG_BT_MCS */ + #if CONFIG_BT_PACS err = bt_le_bluedroid_pacs_init(); if (err) { @@ -123,6 +136,16 @@ int bt_le_bluedroid_audio_init(void) return err; } +void bt_le_bluedroid_audio_deinit(void) +{ + LOG_DBG("[B]AudioDeinit"); + + /* The services are removed by the BTA_GATTS_AppDeregister() in the ISO + * layer's gatt deinit - GATT_Deregister() stops every service the app + * owns - so only this layer's own hook is dropped here. */ + bt_le_bluedroid_audio_gatts_deinit(); +} + #if CONFIG_BT_CSIP_SET_MEMBER static int bluedroid_gatt_csis_init(struct bt_le_audio_start_info *info, struct bt_gatt_service **inc_csis_svc) diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/ascs.c b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/ascs.c index 646ae4aeb9e..b8eb0201b99 100644 --- a/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/ascs.c +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/ascs.c @@ -38,6 +38,21 @@ int bt_le_bluedroid_ascs_init(void) return bt_le_bluedroid_svc_init(ascs_svc); } +int bt_le_bluedroid_ascs_deinit(void) +{ + struct bt_gatt_service *ascs_svc; + + LOG_DBG("[B]AscsDeinit"); + + ascs_svc = lib_ascs_svc_get(); + if (!ascs_svc) { + LOG_ERR("[B]AscsSvcGetFail"); + return -ENODEV; + } + + return bt_le_bluedroid_svc_deinit(ascs_svc); +} + int bt_le_bluedroid_ascs_start(void) { struct bt_gatt_service *ascs_svc; diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/bass.c b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/bass.c index 36726ddb237..5d99cde34ad 100644 --- a/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/bass.c +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/bass.c @@ -38,6 +38,21 @@ int bt_le_bluedroid_bass_init(void) return bt_le_bluedroid_svc_init(bass_svc); } +int bt_le_bluedroid_bass_deinit(void) +{ + struct bt_gatt_service *bass_svc; + + LOG_DBG("[B]BassDeinit"); + + bass_svc = lib_bap_bass_svc_get(); + if (!bass_svc) { + LOG_ERR("[B]BassSvcGetFail"); + return -ENODEV; + } + + return bt_le_bluedroid_svc_deinit(bass_svc); +} + int bt_le_bluedroid_bass_start(void) { struct bt_gatt_service *bass_svc; diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/cas.c b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/cas.c index 2a93afc5512..00ce6a82de2 100644 --- a/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/cas.c +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/cas.c @@ -16,13 +16,14 @@ #include "bluedroid/server.h" #include "common/host.h" +#include "common/audio_attr.h" #include "../../../lib/include/audio.h" LOG_MODULE_REGISTER(LEA_CAS, CONFIG_BT_ISO_LOG_LEVEL); #if CONFIG_BT_CAP_ACCEPTOR_SET_MEMBER -static struct inc_svc_inst inc_csis_inst; +static BT_AUDIO_EXT_RAM_BSS_ATTR struct inc_svc_inst inc_csis_inst; struct inc_svc_inst *cas_not_included_inst(void) { diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/csis.c b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/csis.c index 19e2be17869..5d9d9924a0d 100644 --- a/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/csis.c +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/csis.c @@ -16,6 +16,7 @@ #include "bluedroid/server.h" #include "common/host.h" +#include "common/audio_attr.h" #include "../../../lib/include/audio.h" @@ -23,11 +24,16 @@ LOG_MODULE_REGISTER(LEA_CSIS, CONFIG_BT_ISO_LOG_LEVEL); #define CSIS_SVC_COUNT CONFIG_BT_CSIP_SET_MEMBER_MAX_INSTANCE_COUNT -static struct csis_inst { +static BT_AUDIO_EXT_RAM_BSS_ATTR struct csis_inst { struct bt_gatt_service *svc_p; } csis_insts[CSIS_SVC_COUNT]; -static uint8_t csis_svc_count; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint8_t csis_svc_count; + +void bt_le_bluedroid_csis_state_reset(void) +{ + csis_svc_count = 0; +} int bt_le_bluedroid_csis_init(void *svc, uint8_t count) { @@ -57,6 +63,18 @@ int bt_le_bluedroid_csis_init(void *svc, uint8_t count) return 0; } +int bt_le_bluedroid_csis_deinit(void *csis_svc) +{ + LOG_DBG("[B]CsisDeinit"); + + if (csis_svc == NULL) { + LOG_ERR("[B]CsisSvcNull"); + return -EINVAL; + } + + return bt_le_bluedroid_svc_deinit(csis_svc); +} + int bt_le_bluedroid_csis_start(void) { int err; diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/mcs.c b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/mcs.c index 6869b474c04..0cce7e2cf82 100644 --- a/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/mcs.c +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/mcs.c @@ -16,6 +16,7 @@ #include "bluedroid/server.h" #include "common/host.h" +#include "common/audio_attr.h" #include "../../../lib/include/audio.h" @@ -29,7 +30,7 @@ LOG_MODULE_REGISTER(LEA_MCS, CONFIG_BT_ISO_LOG_LEVEL); /* GMCS includes a single secondary OTS instance. Mirrors the CAS->CSIS included- * secondary pattern: create the secondary before the primary so the include * declaration in the GMCS table resolves to OTS's live handle. */ -static struct inc_svc_inst inc_ots_inst; +static BT_AUDIO_EXT_RAM_BSS_ATTR struct inc_svc_inst inc_ots_inst; struct inc_svc_inst *gmcs_not_included_inst(void) { @@ -61,12 +62,28 @@ static int bluedroid_gmcs_ots_init(void) static int bluedroid_gmcs_ots_start(void) { + /* svc_p comes from bluedroid_gmcs_ots_init(), which only runs once the app + * calls media_proxy_pl_init(); NULL means MCS was never registered. The + * count-guarded loops in vcs.c / mics.c / csis.c skip for the same reason. */ + if (inc_ots_inst.svc_p == NULL) { + LOG_DBG("[B]GmcsOtsNotReg"); + return 0; + } + bt_le_bluedroid_set_svc_in_progress(OTS_IN_PROGRESS); return bt_le_bluedroid_svc_start(inc_ots_inst.svc_p); } #endif /* CONFIG_BT_OTS */ +void bt_le_bluedroid_mcs_state_reset(void) +{ +#if CONFIG_BT_OTS + inc_ots_inst.svc_p = NULL; + inc_ots_inst.included = false; +#endif /* CONFIG_BT_OTS */ +} + int bt_le_bluedroid_gmcs_init(void) { struct bt_gatt_service *gmcs_svc; @@ -150,6 +167,7 @@ int bt_le_bluedroid_mcs_init(void) * (created during gmcs_init). Reset the flag so it re-resolves per instance. */ inc_ots_inst.included = false; #endif /* CONFIG_BT_OTS */ + err = bt_le_bluedroid_svc_init(&list[i]); if (err) { return err; diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/mics.c b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/mics.c index b8941321600..5546da0707f 100644 --- a/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/mics.c +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/mics.c @@ -18,15 +18,17 @@ #include "common/host.h" +#include "common/audio_attr.h" + #include "../../../lib/include/audio.h" LOG_MODULE_REGISTER(LEA_MICS, CONFIG_BT_ISO_LOG_LEVEL); #define AICS_INST_COUNT CONFIG_BT_MICP_MIC_DEV_AICS_INSTANCE_COUNT -static uint8_t inc_aics_svc_count; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint8_t inc_aics_svc_count; -static struct inc_svc_inst inc_aics_insts[AICS_INST_COUNT]; +static BT_AUDIO_EXT_RAM_BSS_ATTR struct inc_svc_inst inc_aics_insts[AICS_INST_COUNT]; struct inc_svc_inst *mics_not_included_inst(void) { @@ -39,6 +41,11 @@ struct inc_svc_inst *mics_not_included_inst(void) return NULL; } +void bt_le_bluedroid_mics_state_reset(void) +{ + inc_aics_svc_count = 0; +} + int bt_le_bluedroid_mics_init(void *micp_inc) { struct bt_micp_included *micp_included; diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/pacs.c b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/pacs.c index 8dc151dbf67..a2e2280f632 100644 --- a/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/pacs.c +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/pacs.c @@ -38,6 +38,21 @@ int bt_le_bluedroid_pacs_init(void) return bt_le_bluedroid_svc_init(pacs_svc); } +int bt_le_bluedroid_pacs_deinit(void) +{ + struct bt_gatt_service *pacs_svc; + + LOG_DBG("[B]PacsDeinit"); + + pacs_svc = lib_pacs_svc_get(); + if (!pacs_svc) { + LOG_ERR("[B]PacsSvcGetFail"); + return -ENODEV; + } + + return bt_le_bluedroid_svc_deinit(pacs_svc); +} + int bt_le_bluedroid_pacs_start(void) { struct bt_gatt_service *pacs_svc; diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/tbs.c b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/tbs.c index 2055bb2e066..5f04e72c7fc 100644 --- a/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/tbs.c +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/tbs.c @@ -36,6 +36,21 @@ int bt_le_bluedroid_gtbs_init(void) return bt_le_bluedroid_svc_init(gtbs_svc); } +int bt_le_bluedroid_gtbs_deinit(void) +{ + struct bt_gatt_service *gtbs_svc; + + LOG_DBG("[B]GtbsDeinit"); + + gtbs_svc = lib_gtbs_svc_get(); + if (!gtbs_svc) { + LOG_ERR("[B]GtbsSvcGetFail"); + return -ENODEV; + } + + return bt_le_bluedroid_svc_deinit(gtbs_svc); +} + int bt_le_bluedroid_gtbs_start(void) { struct bt_gatt_service *gtbs_svc; @@ -83,6 +98,29 @@ int bt_le_bluedroid_tbs_init(void) return 0; } +int bt_le_bluedroid_tbs_deinit(uint8_t bearer_index) +{ +#if CONFIG_BT_TBS_BEARER_COUNT > 0 + struct bt_gatt_service *tbs_list; +#endif /* CONFIG_BT_TBS_BEARER_COUNT > 0 */ + + LOG_DBG("[B]TbsDeinit[%u]", bearer_index); + +#if CONFIG_BT_TBS_BEARER_COUNT > 0 + if (bearer_index < CONFIG_BT_TBS_BEARER_COUNT) { + tbs_list = lib_tbs_server_list_get(); + if (!tbs_list) { + return 0; + } + + return bt_le_bluedroid_svc_deinit(&tbs_list[bearer_index]); + } +#endif /* CONFIG_BT_TBS_BEARER_COUNT > 0 */ + + LOG_ERR("[B]TbsInvBearerIdx[%u]", bearer_index); + return -EINVAL; +} + int bt_le_bluedroid_tbs_start(void) { struct bt_gatt_service *tbs_list; diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/vcs.c b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/vcs.c index af5674646d6..6d4d904643b 100644 --- a/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/vcs.c +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/vcs.c @@ -17,6 +17,7 @@ #include "bluedroid/server.h" #include "common/host.h" +#include "common/audio_attr.h" #include "../../../lib/include/audio.h" @@ -25,11 +26,11 @@ LOG_MODULE_REGISTER(LEA_VCS, CONFIG_BT_ISO_LOG_LEVEL); #define VOCS_INST_COUNT CONFIG_BT_VCP_VOL_REND_VOCS_INSTANCE_COUNT #define AICS_INST_COUNT CONFIG_BT_VCP_VOL_REND_AICS_INSTANCE_COUNT -static uint8_t inc_vocs_svc_count; -static uint8_t inc_aics_svc_count; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint8_t inc_vocs_svc_count; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint8_t inc_aics_svc_count; -static struct inc_svc_inst inc_vocs_insts[VOCS_INST_COUNT]; -static struct inc_svc_inst inc_aics_insts[AICS_INST_COUNT]; +static BT_AUDIO_EXT_RAM_BSS_ATTR struct inc_svc_inst inc_vocs_insts[VOCS_INST_COUNT]; +static BT_AUDIO_EXT_RAM_BSS_ATTR struct inc_svc_inst inc_aics_insts[AICS_INST_COUNT]; struct inc_svc_inst *vcs_not_included_inst(void) { @@ -48,6 +49,12 @@ struct inc_svc_inst *vcs_not_included_inst(void) return NULL; } +void bt_le_bluedroid_vcs_state_reset(void) +{ + inc_vocs_svc_count = 0; + inc_aics_svc_count = 0; +} + int bt_le_bluedroid_vcs_init(void *vcp_inc) { struct bt_vcp_included *vcp_included; @@ -75,13 +82,10 @@ int bt_le_bluedroid_vcs_init(void *vcp_inc) return -EINVAL; } - inc_vocs_svc_count = vcp_included->vocs_cnt; - inc_aics_svc_count = vcp_included->aics_cnt; - bt_le_bluedroid_set_svc_in_progress(VOCS_IN_PROGRESS); /* VCS may include zero or more instances of VOCS */ - for (size_t i = 0; i < inc_vocs_svc_count; i++) { + for (size_t i = 0; i < vcp_included->vocs_cnt; i++) { inc_vocs_insts[i].svc_p = lib_vocs_svc_get(vcp_included->vocs[i]); if (!inc_vocs_insts[i].svc_p) { LOG_ERR("[B]VocsSvcGetFail[%u]", i); @@ -100,7 +104,7 @@ int bt_le_bluedroid_vcs_init(void *vcp_inc) bt_le_bluedroid_set_svc_in_progress(AICS_IN_PROGRESS); /* VCS may include zero or more instances of AICS */ - for (size_t i = 0; i < inc_aics_svc_count; i++) { + for (size_t i = 0; i < vcp_included->aics_cnt; i++) { inc_aics_insts[i].svc_p = lib_aics_svc_get(vcp_included->aics[i]); if (!inc_aics_insts[i].svc_p) { LOG_ERR("[B]AicsSvcGetFail[%u]", i); @@ -115,6 +119,12 @@ int bt_le_bluedroid_vcs_init(void *vcp_inc) return err; } } + + /* Commit counts only after every instance initialized — mid-loop + * failure leaves them at 0 (reset on entry) so vcs_start() never + * iterates partially-initialized entries. */ + inc_vocs_svc_count = vcp_included->vocs_cnt; + inc_aics_svc_count = vcp_included->aics_cnt; } bt_le_bluedroid_set_svc_in_progress(VCS_IN_PROGRESS); diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/server.c b/components/bt/esp_ble_audio/host/adapter/bluedroid/server.c index b7d306eb678..364bb3bae58 100644 --- a/components/bt/esp_ble_audio/host/adapter/bluedroid/server.c +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/server.c @@ -23,14 +23,48 @@ #include "bluedroid/server.h" #include "common/host.h" +#include "common/audio_attr.h" LOG_MODULE_REGISTER(LEA_GSRV, CONFIG_BT_ISO_LOG_LEVEL); -static uint8_t svc_in_progress; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint8_t svc_in_progress; -static uint16_t inc_svc_handle; -static uint16_t svc_handle; -static uint16_t chrc_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t inc_svc_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t svc_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t chrc_handle; + +static BT_AUDIO_EXT_RAM_BSS_ATTR uint8_t aics_count; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint8_t csis_count; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint8_t vocs_count; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint8_t mcs_count; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint8_t ots_count; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint8_t tbs_count; + +/* Every service svc_init() created, so the deinit can delete each one: + * BTA_GATTS_AppDeregister() clears BTA's srvc_cb[] but leaves gatt_cb.sr_reg[], + * which is the table GATTS_StartService() checks on the next init. */ +static BT_AUDIO_EXT_RAM_BSS_ATTR struct bt_gatt_service *created_svcs[CONFIG_BT_GATT_MAX_SR_PROFILES]; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint8_t created_svc_count; + +static void svc_created_record(struct bt_gatt_service *svc) +{ + for (uint8_t i = 0; i < created_svc_count; i++) { + if (created_svcs[i] == svc) { + /* Re-created in place: gtbs_init has no "already added" guard, and + * the tbs/mcs loops re-enter for each newly registered instance. */ + return; + } + } + + if (created_svc_count == ARRAY_SIZE(created_svcs)) { + /* Same ceiling as the stack's, so reaching it means the static_assert on + * TOTAL_SERVICE_COUNT is out of date rather than that we over-created. */ + LOG_ERR("[B]SvcRecordFull[%u]", svc_in_progress); + return; + } + + created_svcs[created_svc_count++] = svc; +} static bool is_primary_svc(void) { @@ -172,15 +206,14 @@ static void inc_svc_add_cb(uint16_t service_id, uint16_t attr_id, uint8_t status int result = status; if (service_id != svc_handle) { + /* Stale after timeout; take already failed — ignore without give. */ LOG_ERR("[B]IncSvcAddMismatch[%u][%u][%u]", svc_in_progress, service_id, svc_handle); - result = -1; - goto end; + return; } inc_svc_handle = attr_id; -end: bt_le_bluedroid_gatts_sem_give(result); } @@ -189,9 +222,8 @@ static void chrc_add_cb(uint16_t service_id, uint16_t attr_id, { int result = status; - if (service_id != svc_handle || ((tBT_UUID *)uuid)->len != 2) { - /* uuid16 is only meaningful when len == 2; for 32/128-bit UUIDs the - * union access would log the first 2 bytes of a wider value. */ + if (service_id != svc_handle) { + /* Stale after timeout; take already failed — ignore without give. */ if (((tBT_UUID *)uuid)->len == 2) { LOG_ERR("[B]ChrcAddMismatch[%u][%u][%u][2][0x%04x]", svc_in_progress, service_id, svc_handle, @@ -201,6 +233,13 @@ static void chrc_add_cb(uint16_t service_id, uint16_t attr_id, svc_in_progress, service_id, svc_handle, ((tBT_UUID *)uuid)->len); } + return; + } + + if (((tBT_UUID *)uuid)->len != 2) { + LOG_ERR("[B]ChrcAddMismatch[%u][%u][%u][%u][nonU16]", + svc_in_progress, service_id, svc_handle, + ((tBT_UUID *)uuid)->len); result = -1; goto end; } @@ -213,14 +252,13 @@ end: static void svc_start_cb(uint16_t service_id, uint8_t status) { - int result = status; - if (service_id != svc_handle) { + /* Stale after timeout; take already failed — ignore without give. */ LOG_ERR("[B]SvcStartMismatch[%u][%u]", service_id, svc_handle); - result = -1; + return; } - bt_le_bluedroid_gatts_sem_give(result); + bt_le_bluedroid_gatts_sem_give(status); } static struct gatts_svc_cb svc_cb = { @@ -232,9 +270,61 @@ static struct gatts_svc_cb svc_cb = { void bt_le_bluedroid_audio_gatts_init(void) { + svc_in_progress = 0; + inc_svc_handle = 0; + svc_handle = 0; + chrc_handle = 0; + + created_svc_count = 0; + + aics_count = 0; + csis_count = 0; + vocs_count = 0; + mcs_count = 0; + ots_count = 0; + tbs_count = 0; + bt_le_bluedroid_gatts_svc_cb_register(&svc_cb); } +int bt_le_bluedroid_svc_deinit(struct bt_gatt_service *svc) +{ + if (svc == NULL || svc->attrs == NULL) { + LOG_ERR("[B]SvcDeinitNoSvc"); + return -EINVAL; + } + + if (svc->attrs[0].handle == 0) { + LOG_DBG("[B]SvcDeinitNotCreated"); + return 0; + } + + LOG_DBG("[B]SvcDeinit[0x%04x]", svc->attrs[0].handle); + + BTA_GATTS_DeleteService(svc->attrs[0].handle); + svc->attrs[0].handle = 0; + + return 0; +} + +void bt_le_bluedroid_audio_gatts_deinit(void) +{ + /* Reverse creation order, so an included secondary outlives the primary that + * includes it. Each delete logs upstream's "Active Service Found" at ERROR; + * that is its successful lookup, not a failure. */ + + while (created_svc_count > 0) { + struct bt_gatt_service *svc = created_svcs[--created_svc_count]; + + created_svcs[created_svc_count] = NULL; + + /* Already-unregistered entries fall out as a no-op inside. */ + (void)bt_le_bluedroid_svc_deinit(svc); + } + + bt_le_bluedroid_gatts_svc_cb_register(NULL); +} + int bt_le_bluedroid_set_svc_in_progress(uint8_t value) { if (value >= MAX_IN_PROGRESS) { @@ -409,17 +499,6 @@ static struct inc_svc_inst *get_not_included_inst(void) static uint8_t get_svc_inst_id(uint16_t svc_uuid) { - /* Note: - * For LE Audio, some service could have multiple instances. - */ - - static uint8_t aics_count; - static uint8_t csis_count; - static uint8_t vocs_count; - static uint8_t mcs_count; - static uint8_t ots_count; - static uint8_t tbs_count; - switch (svc_uuid) { case BT_UUID_AICS_VAL: return aics_count++; @@ -455,7 +534,7 @@ int bt_le_bluedroid_svc_init(struct bt_gatt_service *svc) uint8_t inst_id; tBT_UUID uuid; - assert(svc); + BT_LE_ASSERT(svc); for (size_t i = 0; i < svc->attr_count; i++) { curr_attr = &svc->attrs[i]; @@ -469,7 +548,7 @@ int bt_le_bluedroid_svc_init(struct bt_gatt_service *svc) return -1; } - assert(curr_attr->user_data); + BT_LE_ASSERT(curr_attr->user_data); bt_le_bluedroid_gatt_uuid_convert(curr_attr->user_data, &uuid); inst_id = get_svc_inst_id(uuid.uu.uuid16); @@ -495,7 +574,7 @@ int bt_le_bluedroid_svc_init(struct bt_gatt_service *svc) return -1; } - assert(curr_attr->user_data); + BT_LE_ASSERT(curr_attr->user_data); bt_le_bluedroid_gatt_uuid_convert(curr_attr->user_data, &uuid); inst_id = get_svc_inst_id(uuid.uu.uuid16); @@ -560,7 +639,7 @@ int bt_le_bluedroid_svc_init(struct bt_gatt_service *svc) next_attr = &svc->attrs[i + 1]; perm = bt_le_bluedroid_gatt_perm_convert(next_attr->perm); - assert(curr_attr->user_data); + BT_LE_ASSERT(curr_attr->user_data); chrc = curr_attr->user_data; bt_le_bluedroid_gatt_uuid_convert(chrc->uuid, &uuid); @@ -606,18 +685,29 @@ int bt_le_bluedroid_svc_init(struct bt_gatt_service *svc) } } + /* Created, so the deinit has to delete it. attr_count 0 is a service the + * application never registered; svc_init walked nothing. */ + if (svc->attr_count > 0 && svc->attrs[0].handle != 0) { + svc_created_record(svc); + } + return 0; } int bt_le_bluedroid_svc_start(struct bt_gatt_service *svc) { - assert(svc); + BT_LE_ASSERT(svc); + + /* App may not register this svc; skip rather than fail audio_start. + * Unused CAP Acceptor capabilities stop at handle 0, GMAS at NULL + * attrs - only bt_gmap_register() allocates them. */ + if (svc->attrs == NULL) { + LOG_DBG("[B]SvcNotReg[%u]", svc_in_progress); + return 0; + } svc_handle = svc->attrs[0].handle; - /* App may not register this svc (e.g. CAP Acceptor single mode keeps - * unused capability built). Skip rather than fail audio_start. - */ if (svc_handle == 0) { LOG_DBG("[B]SvcNotInit[%u]", svc_in_progress); return 0; @@ -627,7 +717,9 @@ int bt_le_bluedroid_svc_start(struct bt_gatt_service *svc) BTA_GATTS_StartService(svc_handle, BTA_GATT_TRANSPORT_LE); if (bt_le_bluedroid_gatts_sem_take()) { - LOG_ERR("[B]SvcStartFail"); + /* Which service and which handle: BTA only says "already started" or + * "not created", neither of which names the caller. */ + LOG_ERR("[B]SvcStartFail[%u][0x%04x]", svc_in_progress, svc_handle); return -1; } diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/init.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/init.h index ce08a0b0db4..46c5b4f556e 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/init.h +++ b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/init.h @@ -28,6 +28,8 @@ extern "C" { int bt_le_nimble_audio_init(void); +void bt_le_nimble_audio_deinit(void); + int bt_le_nimble_media_proxy_pl_init(void); int bt_le_nimble_vcp_vol_rend_init(void); diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/ascs.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/ascs.h index 20ed621c08f..122c57d2946 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/ascs.h +++ b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/ascs.h @@ -18,6 +18,8 @@ int bt_le_nimble_ascs_attr_handle_set(void); int bt_le_nimble_ascs_init(void); +int bt_le_nimble_ascs_deinit(void); + #ifdef __cplusplus } #endif diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/bass.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/bass.h index 69ddc64c292..ed154da387f 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/bass.h +++ b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/bass.h @@ -18,6 +18,8 @@ int bt_le_nimble_bass_attr_handle_set(void); int bt_le_nimble_bass_init(void); +int bt_le_nimble_bass_deinit(void); + #ifdef __cplusplus } #endif diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/csis.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/csis.h index e2912395e21..ad382bd3d1f 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/csis.h +++ b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/csis.h @@ -19,8 +19,12 @@ extern "C" { int bt_le_nimble_csis_attr_handle_set(void); +void bt_le_nimble_csis_state_reset(void); + int bt_le_nimble_csis_init(void *csis_svc, uint8_t count); +int bt_le_nimble_csis_deinit(void *csis_svc); + #ifdef __cplusplus } #endif diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/mcs.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/mcs.h index d6f0db35290..6d25d87e4f2 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/mcs.h +++ b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/mcs.h @@ -19,6 +19,8 @@ extern "C" { int bt_le_nimble_gmcs_attr_handle_set(void); +void bt_le_nimble_mcs_state_reset(void); + int bt_le_nimble_gmcs_init(bool ots_included); int bt_le_nimble_mcs_attr_handle_set(void); diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/mics.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/mics.h index 73177597de7..1c5c41e0c10 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/mics.h +++ b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/mics.h @@ -19,6 +19,8 @@ extern "C" { int bt_le_nimble_mics_attr_handle_set(void); +void bt_le_nimble_mics_state_reset(void); + int bt_le_nimble_mics_init(void *micp_inc); #ifdef __cplusplus diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/pacs.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/pacs.h index e7d470fcb95..60185a5bb02 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/pacs.h +++ b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/pacs.h @@ -21,6 +21,8 @@ int bt_le_nimble_pacs_attr_handle_set(void); int bt_le_nimble_pacs_init(void); +int bt_le_nimble_pacs_deinit(void); + #ifdef __cplusplus } #endif diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/tbs.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/tbs.h index 9c14929e03b..e1690641325 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/tbs.h +++ b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/tbs.h @@ -18,10 +18,14 @@ int bt_le_nimble_gtbs_attr_handle_set(void); int bt_le_nimble_gtbs_init(void); +int bt_le_nimble_gtbs_deinit(void); + int bt_le_nimble_tbs_attr_handle_set(void); int bt_le_nimble_tbs_init(void); +int bt_le_nimble_tbs_deinit(uint8_t bearer_index); + #ifdef __cplusplus } #endif diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/vcs.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/vcs.h index db72dc6221e..6bdc0393a59 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/vcs.h +++ b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/vcs.h @@ -16,6 +16,8 @@ extern "C" { int bt_le_nimble_vcs_attr_handle_set(void); +void bt_le_nimble_vcs_state_reset(void); + int bt_le_nimble_vcs_init(void *vcp_inc); #ifdef __cplusplus diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/init.c b/components/bt/esp_ble_audio/host/adapter/nimble/init.c index ce338459170..f299d1c9567 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/init.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/init.c @@ -20,6 +20,7 @@ #include "nimble/init.h" #include "common/init.h" +#include "common/audio_attr.h" #include "../../../lib/include/audio.h" @@ -159,6 +160,9 @@ LOG_MODULE_REGISTER(LEA_NINIT, CONFIG_BT_ISO_LOG_LEVEL); _Static_assert(TOTAL_CCCDS_COUNT <= CONFIG_BT_NIMBLE_MAX_CCCDS, "Too small BT_NIMBLE_MAX_CCCDS"); +static BT_AUDIO_EXT_RAM_BSS_ATTR bool nimble_svcs_registered; +static BT_AUDIO_EXT_RAM_BSS_ATTR bool nimble_gatts_started; + int bt_le_nimble_audio_init(void) { int err = 0; @@ -169,6 +173,23 @@ int bt_le_nimble_audio_init(void) return err; } +#if CONFIG_BT_CSIP_SET_MEMBER + bt_le_nimble_csis_state_reset(); +#endif /* CONFIG_BT_CSIP_SET_MEMBER */ +#if CONFIG_BT_VCP_VOL_REND + bt_le_nimble_vcs_state_reset(); +#endif /* CONFIG_BT_VCP_VOL_REND */ +#if CONFIG_BT_MICP_MIC_DEV + bt_le_nimble_mics_state_reset(); +#endif /* CONFIG_BT_MICP_MIC_DEV */ +#if CONFIG_BT_MCS + bt_le_nimble_mcs_state_reset(); +#endif /* CONFIG_BT_MCS */ + + if (nimble_svcs_registered) { + return 0; + } + ble_svc_gap_init(); ble_svc_gatt_init(); @@ -222,6 +243,8 @@ int bt_le_nimble_audio_init(void) #endif /* CONFIG_BT_HAS */ #endif /* (BLE_AUDIO_SVC_DEFERRED_ADD == 0) */ + nimble_svcs_registered = true; + return err; } @@ -442,6 +465,14 @@ int bt_le_nimble_micp_mic_dev_init(void) } #endif /* CONFIG_BT_MICP_MIC_DEV */ +void bt_le_nimble_audio_deinit(void) +{ + LOG_DBG("[N]AudioDeinit"); + + /* Nothing to release: the services added here are boot-scoped and the defs + * NimBLE holds reference no lib memory, so resources_deinit() is safe. */ +} + int bt_le_nimble_audio_start(void *info) { struct bt_gatt_service *inc_csis_svc = NULL; @@ -499,10 +530,17 @@ int bt_le_nimble_audio_start(void *info) } #endif /* CONFIG_BT_GMAP */ - err = ble_gatts_start(); - if (err) { - LOG_ERR("[N]GattsStartFail[%d]", err); - return err; + /* Once per boot: ble_gatts_start() resets the whole ATT database and only + * re-registers defs added since the last start, so calling it again drops + * other owners' services. attr_handle_set() still runs every cycle. */ + if (!nimble_gatts_started) { + err = ble_gatts_start(); + if (err) { + LOG_ERR("[N]GattsStartFail[%d]", err); + return err; + } + + nimble_gatts_started = true; } err = nimble_gatt_attr_handle_set(); diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/ascs.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/ascs.c index 2b82fdd6145..52ed88764c4 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/ascs.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/ascs.c @@ -26,6 +26,7 @@ #include "nimble/server.h" #include "common/host.h" +#include "common/audio_attr.h" #include "../../../lib/include/audio.h" @@ -35,9 +36,9 @@ static const ble_uuid16_t ascs_uuid_ase_cp = BLE_UUID16_INIT(BT_UUID_ASCS_ASE_CP static const ble_uuid16_t ascs_uuid_ase_snk = BLE_UUID16_INIT(BT_UUID_ASCS_ASE_SNK_VAL); static const ble_uuid16_t ascs_uuid_ase_src = BLE_UUID16_INIT(BT_UUID_ASCS_ASE_SRC_VAL); -static uint16_t ase_control_point_handle; -static uint16_t ase_snk_handle[CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT]; -static uint16_t ase_src_handle[CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT]; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t ase_control_point_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t ase_snk_handle[CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT]; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t ase_src_handle[CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT]; static struct ble_gatt_svc_def gatt_svc_ascs[] = { { @@ -61,7 +62,7 @@ static void ascs_svc_add_ase_cp_chr(struct ble_gatt_chr_def *chr) chr->arg = NULL; chr->descriptors = NULL; /* NULL if no descriptors. Do not include CCCD */ chr->flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP | \ - BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_WRITE_ENC; + BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_WRITE_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC; chr->min_key_size = 16; chr->val_handle = &ase_control_point_handle; } @@ -78,7 +79,7 @@ static void ascs_svc_add_ase_snk_chr(struct ble_gatt_chr_def *chrs) chr->access_cb = bt_le_nimble_gatts_access_cb_safe; chr->arg = UINT_TO_POINTER(i); chr->descriptors = NULL; /* NULL if no descriptors. Do not include CCCD */ - chr->flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC; + chr->flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC; chr->min_key_size = 16; chr->val_handle = &ase_snk_handle[i]; } @@ -97,7 +98,7 @@ static void ascs_svc_add_ase_src_chr(struct ble_gatt_chr_def *chrs) chr->access_cb = bt_le_nimble_gatts_access_cb_safe; chr->arg = UINT_TO_POINTER(i); chr->descriptors = NULL; /* NULL if no descriptors. Do not include CCCD */ - chr->flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC; + chr->flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC; chr->min_key_size = 16; chr->val_handle = &ase_src_handle[i]; } @@ -127,7 +128,7 @@ int bt_le_nimble_ascs_attr_handle_set(void) return -ENODEV; } - assert(ase_control_point_handle >= 2); + BT_LE_ASSERT(ase_control_point_handle >= 2); start_handle = ase_control_point_handle - 2; /* server attr handle & char def handle */ #if CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT > 0 end_handle = ase_src_handle[CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT - 1] + 1; /* cccd attr handle */ @@ -201,6 +202,7 @@ static int ascs_svc_check(void) int bt_le_nimble_ascs_init(void) { + bool ascs_added = false; uint16_t chr_count; int rc; @@ -209,8 +211,8 @@ int bt_le_nimble_ascs_init(void) LOG_DBG("[N]AscsInit[%u]", chr_count); - gatt_svc_ascs[0].characteristics = calloc(chr_count, sizeof(struct ble_gatt_chr_def)); - assert(gatt_svc_ascs[0].characteristics); + gatt_svc_ascs[0].characteristics = bt_le_ext_calloc(chr_count, sizeof(struct ble_gatt_chr_def)); + BT_LE_ASSERT(gatt_svc_ascs[0].characteristics); ascs_svc_add_ase_cp_chr((void *)(gatt_svc_ascs[0].characteristics + 0)); @@ -233,6 +235,7 @@ int bt_le_nimble_ascs_init(void) LOG_ERR("[N]AscsAddSvcsFail[%d]", rc); goto free; } + ascs_added = true; rc = ascs_svc_check(); if (rc) { @@ -242,7 +245,18 @@ int bt_le_nimble_ascs_init(void) return 0; free: - free((void *)gatt_svc_ascs[0].characteristics); - gatt_svc_ascs[0].characteristics = NULL; + /* Once ble_gatts_add_svcs() succeeds NimBLE keeps the svc_def pointer and + * offers no per-service unregister, so an added service must be leaked + * rather than freed into a dangling entry of its global list. */ + if (!ascs_added) { + free((void *)gatt_svc_ascs[0].characteristics); + gatt_svc_ascs[0].characteristics = NULL; + } return rc; } + +int bt_le_nimble_ascs_deinit(void) +{ + LOG_DBG("[N]AscsDeinit"); + return 0; +} diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/bass.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/bass.c index e0499d5e2d0..f881c2aa05c 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/bass.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/bass.c @@ -26,6 +26,7 @@ #include "nimble/server.h" #include "common/host.h" +#include "common/audio_attr.h" #include "../../../lib/include/audio.h" @@ -34,8 +35,8 @@ LOG_MODULE_REGISTER(LEA_BASS, CONFIG_BT_ISO_LOG_LEVEL); static const ble_uuid16_t bass_uuid_control_point = BLE_UUID16_INIT(BT_UUID_BASS_CONTROL_POINT_VAL); static const ble_uuid16_t bass_uuid_recv_state = BLE_UUID16_INIT(BT_UUID_BASS_RECV_STATE_VAL); -static uint16_t bass_control_point_handle; -static uint16_t bass_recv_state_handle[CONFIG_BT_BAP_SCAN_DELEGATOR_RECV_STATE_COUNT]; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t bass_control_point_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t bass_recv_state_handle[CONFIG_BT_BAP_SCAN_DELEGATOR_RECV_STATE_COUNT]; static struct ble_gatt_svc_def gatt_svc_bass[] = { { @@ -74,7 +75,7 @@ static void bass_svc_add_recv_state_chr(struct ble_gatt_chr_def *chrs) chr->access_cb = bt_le_nimble_gatts_access_cb_safe; chr->arg = UINT_TO_POINTER(i); chr->descriptors = NULL; /* NULL if no descriptors. Do not include CCCD */ - chr->flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC; + chr->flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC; chr->min_key_size = 16; chr->val_handle = &bass_recv_state_handle[i]; } @@ -103,7 +104,7 @@ int bt_le_nimble_bass_attr_handle_set(void) return -ENODEV; } - assert(bass_control_point_handle >= 2); + BT_LE_ASSERT(bass_control_point_handle >= 2); start_handle = bass_control_point_handle - 2; /* server attr handle & char def handle */ end_handle = bass_recv_state_handle[CONFIG_BT_BAP_SCAN_DELEGATOR_RECV_STATE_COUNT - 1] + 1; /* cccd attr handle */ @@ -153,7 +154,7 @@ static int bass_svc_check(void) for (size_t i = 0; i < bass_svc->attr_count; i++) { uuid = (const struct bt_uuid_16 *)(bass_svc->attrs + i)->uuid; - if (uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && + if (uuid && uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && uuid->val == check->value) { chr_found = true; break; @@ -171,6 +172,7 @@ static int bass_svc_check(void) int bt_le_nimble_bass_init(void) { + bool bass_added = false; uint16_t chr_count; int rc; @@ -179,8 +181,8 @@ int bt_le_nimble_bass_init(void) LOG_DBG("[N]BassInit[%u]", chr_count); - gatt_svc_bass->characteristics = calloc(chr_count, sizeof(struct ble_gatt_chr_def)); - assert(gatt_svc_bass->characteristics); + gatt_svc_bass->characteristics = bt_le_ext_calloc(chr_count, sizeof(struct ble_gatt_chr_def)); + BT_LE_ASSERT(gatt_svc_bass->characteristics); bass_svc_add_control_point_chr((void *)(gatt_svc_bass->characteristics + 0)); @@ -197,6 +199,7 @@ int bt_le_nimble_bass_init(void) LOG_ERR("[N]BassAddSvcsFail[%d]", rc); goto free; } + bass_added = true; rc = bass_svc_check(); if (rc) { @@ -206,7 +209,18 @@ int bt_le_nimble_bass_init(void) return 0; free: - free((void *)gatt_svc_bass->characteristics); - gatt_svc_bass->characteristics = NULL; + /* Once ble_gatts_add_svcs() succeeds NimBLE keeps the svc_def pointer and + * offers no per-service unregister, so an added service must be leaked + * rather than freed into a dangling entry of its global list. */ + if (!bass_added) { + free((void *)gatt_svc_bass->characteristics); + gatt_svc_bass->characteristics = NULL; + } return rc; } + +int bt_le_nimble_bass_deinit(void) +{ + LOG_DBG("[N]BassDeinit"); + return 0; +} diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/cas.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/cas.c index db2f25527c7..d89ae99da8e 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/cas.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/cas.c @@ -18,6 +18,7 @@ #include "host/ble_gatt.h" #include "common/host.h" +#include "common/audio_attr.h" #include "../../../lib/include/audio.h" @@ -26,7 +27,7 @@ LOG_MODULE_REGISTER(LEA_CAS, CONFIG_BT_ISO_LOG_LEVEL); #if CONFIG_BT_CAP_ACCEPTOR_SET_MEMBER extern struct ble_gatt_svc_def *cas_get_included_csis(void *csis_svc_p); -static struct bt_gatt_service *inc_csis_svc; +static BT_AUDIO_EXT_RAM_BSS_ATTR struct bt_gatt_service *inc_csis_svc; static struct ble_gatt_svc_def *cas_inc_svcs[] = { NULL, diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/csis.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/csis.c index fa803b60c51..4d015430eb0 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/csis.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/csis.c @@ -28,6 +28,7 @@ #include "nimble/server.h" #include "common/host.h" +#include "common/audio_attr.h" #include "../../../lib/include/audio.h" @@ -39,7 +40,7 @@ LOG_MODULE_REGISTER(LEA_CSIS, CONFIG_BT_ISO_LOG_LEVEL); #if CONFIG_BT_CSIP_SET_MEMBER_SIRK_NOTIFIABLE #define CSIS_CHR_FLAGS_SIRK \ - (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC) + (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC) #else /* CONFIG_BT_CSIP_SET_MEMBER_SIRK_NOTIFIABLE */ #define CSIS_CHR_FLAGS_SIRK \ (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC) @@ -47,14 +48,14 @@ LOG_MODULE_REGISTER(LEA_CSIS, CONFIG_BT_ISO_LOG_LEVEL); #if CONFIG_BT_CSIP_SET_MEMBER_SIZE_NOTIFIABLE #define CSIS_CHR_FLAGS_SET_SIZE \ - (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC) + (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC) #else /* CONFIG_BT_CSIP_SET_MEMBER_SIZE_NOTIFIABLE */ #define CSIS_CHR_FLAGS_SET_SIZE \ (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC) #endif /* CONFIG_BT_CSIP_SET_MEMBER_SIZE_NOTIFIABLE */ #define CSIS_CHR_FLAGS_SET_LOCK \ - (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_WRITE | \ + (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC | \ BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_WRITE_ENC) #define CSIS_CHR_FLAGS_RANK \ @@ -62,7 +63,7 @@ LOG_MODULE_REGISTER(LEA_CSIS, CONFIG_BT_ISO_LOG_LEVEL); #if CONFIG_BT_CSIP_SET_MEMBER_SET_NAME_NOTIFIABLE #define CSIS_CHR_FLAGS_SET_NAME \ - (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC) + (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC) #else /* CONFIG_BT_CSIP_SET_MEMBER_SET_NAME_NOTIFIABLE */ #define CSIS_CHR_FLAGS_SET_NAME \ (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC) @@ -75,7 +76,7 @@ static const ble_uuid16_t csis_uuid_set_lock = BLE_UUID16_INIT(BT_UUID_CSIS_SET_ static const ble_uuid16_t csis_uuid_rank = BLE_UUID16_INIT(BT_UUID_CSIS_RANK_VAL); static const ble_uuid16_t csis_uuid_set_name = BLE_UUID16_INIT(BT_UUID_CSIS_SET_NAME_VAL); -static struct csis_inst { +static BT_AUDIO_EXT_RAM_BSS_ATTR struct csis_inst { struct bt_gatt_service *svc_p; uint16_t sirk_handle; uint16_t set_size_handle; @@ -84,10 +85,13 @@ static struct csis_inst { uint16_t set_name_handle; } csis_insts[CSIS_SVC_COUNT]; -static uint8_t csis_svc_count; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint8_t csis_svc_count; + +static BT_AUDIO_EXT_RAM_BSS_ATTR uint8_t csis_reg_count; +static BT_AUDIO_EXT_RAM_BSS_ATTR bool csis_registered; /* Extra one for terminating the CSIS service array */ -static struct ble_gatt_svc_def gatt_svc_csis[CSIS_SVC_COUNT + 1]; +static BT_AUDIO_EXT_RAM_BSS_ATTR struct ble_gatt_svc_def gatt_svc_csis[CSIS_SVC_COUNT + 1]; struct ble_gatt_svc_def *cas_get_included_csis(void *csis_svc_p) { @@ -123,7 +127,7 @@ static int csis_svc_check(void) struct ble_gatt_svc_def *csis = &gatt_svc_csis[i]; struct bt_gatt_service *svc = csis_insts[i].svc_p; - assert(svc); + BT_LE_ASSERT(svc); for (const struct ble_gatt_chr_def *chr = csis->characteristics; chr && chr->uuid; chr++) { @@ -134,7 +138,7 @@ static int csis_svc_check(void) for (size_t j = 0; j < svc->attr_count; j++) { uuid = (const struct bt_uuid_16 *)(svc->attrs + j)->uuid; - if (uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && + if (uuid && uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && uuid->val == check->value) { chr_found = true; break; @@ -160,8 +164,15 @@ int bt_le_nimble_csis_attr_handle_set(void) for (size_t i = 0; i < csis_svc_count; i++) { struct bt_gatt_service *zsvc = csis_insts[i].svc_p; - assert(zsvc); - assert(csis_insts[i].sirk_handle >= 2); + BT_LE_ASSERT(zsvc); + + /* Zero means no registration round ever covered this instance, so it is + * absent from the ATT database and there is no range to anchor. Happens + * when CSIS is first registered after the boot's ble_gatts_start(). */ + if (csis_insts[i].sirk_handle < 2) { + LOG_ERR("[N]CsisNoAttrHdl[%u]", i); + return -1; + } /* SIRK is always the first characteristic, so its value handle anchors the range. */ start_handle = csis_insts[i].sirk_handle - 2; /* server attr handle & char def handle */ @@ -180,7 +191,7 @@ int bt_le_nimble_csis_attr_handle_set(void) const struct bt_uuid_16 *uuid = (const struct bt_uuid_16 *)(zsvc->attrs + j)->uuid; uint16_t chr_handle = 0; - if (uuid->uuid.type != BT_UUID_TYPE_16) { + if (!uuid || uuid->uuid.type != BT_UUID_TYPE_16) { continue; } @@ -244,8 +255,8 @@ static void csis_svc_init(struct csis_inst *inst, svc->uuid = &csis_uuid_svc.u; svc->includes = NULL; - svc->characteristics = calloc(CSIS_CHR_COUNT, sizeof(struct ble_gatt_chr_def)); - assert(svc->characteristics); + svc->characteristics = bt_le_ext_calloc(CSIS_CHR_COUNT, sizeof(struct ble_gatt_chr_def)); + BT_LE_ASSERT(svc->characteristics); /* Build the NimBLE characteristics from the ones actually present in the Zephyr * service. Optional characteristics (set size, lock, rank) may be absent depending @@ -254,7 +265,7 @@ static void csis_svc_init(struct csis_inst *inst, for (size_t i = 0; i < zsvc->attr_count; i++) { const struct bt_uuid_16 *uuid = (const struct bt_uuid_16 *)zsvc->attrs[i].uuid; - if (uuid->uuid.type != BT_UUID_TYPE_16) { + if (!uuid || uuid->uuid.type != BT_UUID_TYPE_16) { continue; } @@ -291,11 +302,17 @@ static void csis_svc_init(struct csis_inst *inst, * the terminator slot. Trips if the switch matches a 6th char: a new CSIS case added * without bumping the (5 + 1), or a duplicate UUID in the Zephyr service table. */ - assert(chr_cnt < CSIS_CHR_COUNT); + BT_LE_ASSERT(chr_cnt < CSIS_CHR_COUNT); +} + +void bt_le_nimble_csis_state_reset(void) +{ + csis_svc_count = 0; } int bt_le_nimble_csis_init(void *svc, uint8_t count) { + bool csis_added = false; int rc; LOG_DBG("[N]CsisInit[%u]", count); @@ -305,11 +322,27 @@ int bt_le_nimble_csis_init(void *svc, uint8_t count) return -1; } + /* The attribute table is built by the boot's single ble_gatts_start(), so a + * different instance count now needs a table this boot cannot produce. Fail + * here, not later on a handle NimBLE never assigned. */ + if (csis_registered && count != csis_reg_count) { + LOG_ERR("[N]CsisCountChanged[%u][%u]", csis_reg_count, count); + return -1; + } + csis_svc_count = count; + /* Refreshed every cycle: the lib frees and reallocates its service objects, + * while the NimBLE defs below are built once and stay in the database. */ for (size_t i = 0; i < csis_svc_count; i++) { csis_insts[i].svc_p = ((struct bt_gatt_service **)svc)[i]; + } + if (csis_registered) { + return 0; + } + + for (size_t i = 0; i < csis_svc_count; i++) { csis_svc_init(&csis_insts[i], &gatt_svc_csis[i], csis_insts[i].svc_p); } @@ -324,19 +357,36 @@ int bt_le_nimble_csis_init(void *svc, uint8_t count) LOG_ERR("[N]CsisAddSvcsFail[%d]", rc); goto free; } + csis_added = true; rc = csis_svc_check(); if (rc) { goto free; } + csis_reg_count = count; + csis_registered = true; + return 0; free: - for (size_t i = 0; i < csis_svc_count; i++) { - free((void *)gatt_svc_csis[i].characteristics); - gatt_svc_csis[i].characteristics = NULL; + /* Once ble_gatts_add_svcs() succeeds NimBLE keeps the svc_def pointer and + * offers no per-service unregister, so an added service must be leaked + * rather than freed into a dangling entry of its global list. */ + if (!csis_added) { + for (size_t i = 0; i < csis_svc_count; i++) { + free((void *)gatt_svc_csis[i].characteristics); + gatt_svc_csis[i].characteristics = NULL; + } } csis_svc_count = 0; return rc; } + +int bt_le_nimble_csis_deinit(void *csis_svc) +{ + ARG_UNUSED(csis_svc); + + LOG_DBG("[N]CsisDeinit"); + return 0; +} diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/gmas.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/gmas.c index 525923fa7db..ad1176fbfff 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/gmas.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/gmas.c @@ -24,6 +24,7 @@ #include "nimble/server.h" #include "common/host.h" +#include "common/audio_attr.h" #include "../../../lib/include/audio.h" @@ -35,9 +36,9 @@ LOG_MODULE_REGISTER(LEA_GMAS, CONFIG_BT_ISO_LOG_LEVEL); #define GMAS_MAX_CHRS (1 + 4) /* GMAP Role + up to one feature char per role */ static const ble_uuid16_t gmas_svc_uuid = BLE_UUID16_INIT(BT_UUID_GMAS_VAL); -static ble_uuid16_t gmas_chr_uuids[GMAS_MAX_CHRS]; -static struct ble_gatt_chr_def gmas_chrs[GMAS_MAX_CHRS + 1]; -static struct ble_gatt_svc_def gatt_svc_gmas[2]; +static BT_AUDIO_EXT_RAM_BSS_ATTR ble_uuid16_t gmas_chr_uuids[GMAS_MAX_CHRS]; +static BT_AUDIO_EXT_RAM_BSS_ATTR struct ble_gatt_chr_def gmas_chrs[GMAS_MAX_CHRS + 1]; +static BT_AUDIO_EXT_RAM_BSS_ATTR struct ble_gatt_svc_def gatt_svc_gmas[2]; /* Mirror the lib's GMAS table (primary + role char + registered feature chars, each value attr following a 0x2803 declaration) into the NimBLE service def. */ @@ -49,7 +50,7 @@ static int gmas_build_svc(struct bt_gatt_service *gmas_svc) for (size_t i = 0; i < gmas_svc->attr_count; i++) { const struct bt_uuid_16 *u = (const struct bt_uuid_16 *)gmas_svc->attrs[i].uuid; - if (u->uuid.type != BT_UUID_TYPE_16) { + if (!u || u->uuid.type != BT_UUID_TYPE_16) { prev_decl = false; continue; } @@ -146,7 +147,7 @@ static int gmas_svc_check(void) for (size_t i = 0; i < gmas_svc->attr_count; i++) { uuid = (const struct bt_uuid_16 *)(gmas_svc->attrs + i)->uuid; - if (uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && + if (uuid && uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && uuid->val == check->value) { chr_found = true; break; diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/has.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/has.c index 4eeff2d4343..7a0e1b07803 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/has.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/has.c @@ -26,6 +26,7 @@ #include "nimble/server.h" #include "common/host.h" +#include "common/audio_attr.h" #include "../../../lib/include/audio.h" @@ -35,9 +36,9 @@ static const ble_uuid16_t has_uuid_features = BLE_UUID16_INIT(BT_UUID_HAS_HEARIN static const ble_uuid16_t has_uuid_control_point = BLE_UUID16_INIT(BT_UUID_HAS_PRESET_CONTROL_POINT_VAL); static const ble_uuid16_t has_uuid_preset_index = BLE_UUID16_INIT(BT_UUID_HAS_ACTIVE_PRESET_INDEX_VAL); -static uint16_t has_features_handle; -static uint16_t has_control_point_handle; -static uint16_t has_preset_index_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t has_features_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t has_control_point_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t has_preset_index_handle; static struct ble_gatt_svc_def gatt_svc_has[] = { { @@ -59,7 +60,7 @@ static void has_svc_add_features_chr(struct ble_gatt_chr_def *chr) chr->arg = NULL; chr->descriptors = NULL; /* NULL if no descriptors. Do not include CCCD */ #if CONFIG_BT_HAS_FEATURES_NOTIFIABLE - chr->flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC; + chr->flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC; #else /* CONFIG_BT_HAS_FEATURES_NOTIFIABLE */ chr->flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC; #endif /* CONFIG_BT_HAS_FEATURES_NOTIFIABLE */ @@ -77,9 +78,9 @@ static void has_svc_add_control_point_chr(struct ble_gatt_chr_def *chr) chr->descriptors = NULL; /* NULL if no descriptors. Do not include CCCD */ #if CONFIG_BT_HAS_PRESET_CONTROL_POINT_NOTIFIABLE chr->flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_INDICATE | - BLE_GATT_CHR_F_WRITE_ENC | BLE_GATT_CHR_F_NOTIFY; + BLE_GATT_CHR_F_WRITE_ENC | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC; #else /* CONFIG_BT_HAS_PRESET_CONTROL_POINT_NOTIFIABLE */ - chr->flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_INDICATE | BLE_GATT_CHR_F_WRITE_ENC; + chr->flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_INDICATE | BLE_GATT_CHR_F_WRITE_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC; #endif /* CONFIG_BT_HAS_PRESET_CONTROL_POINT_NOTIFIABLE */ chr->min_key_size = 16; chr->val_handle = &has_control_point_handle; @@ -96,7 +97,7 @@ static void has_svc_add_preset_index_chr(struct ble_gatt_chr_def *chr) chr->access_cb = bt_le_nimble_gatts_access_cb_safe; chr->arg = NULL; chr->descriptors = NULL; /* NULL if no descriptors. Do not include CCCD */ - chr->flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC; + chr->flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC; chr->min_key_size = 16; chr->val_handle = &has_preset_index_handle; } @@ -124,9 +125,15 @@ int bt_le_nimble_has_attr_handle_set(void) LOG_ERR("[N]HasSvcGetFail"); return -ENODEV; } - assert(has_svc->attr_count > 0); + BT_LE_ASSERT(has_svc->attr_count > 0); - end_handle = start_handle + has_svc->attr_count - 1; +#if CONFIG_BT_HAS_ACTIVE_PRESET_INDEX + BT_LE_ASSERT(has_preset_index_handle >= 2); + end_handle = has_preset_index_handle + 1; /* cccd attr handle */ +#else + BT_LE_ASSERT(has_control_point_handle >= 2); + end_handle = has_control_point_handle + 1; /* cccd attr handle */ +#endif LOG_DBG("[N]HasAttrHdlSet[%u][%u][%u]", start_handle, end_handle, has_svc->attr_count); @@ -174,7 +181,7 @@ static int has_svc_check(void) for (size_t i = 0; i < has_svc->attr_count; i++) { uuid = (const struct bt_uuid_16 *)(has_svc->attrs + i)->uuid; - if (uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && + if (uuid && uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && uuid->val == check->value) { chr_found = true; break; @@ -192,6 +199,7 @@ static int has_svc_check(void) int bt_le_nimble_has_init(void) { + bool has_added = false; uint8_t chr_count; int rc; @@ -204,8 +212,8 @@ int bt_le_nimble_has_init(void) LOG_DBG("[N]HasInit[%u]", chr_count); - gatt_svc_has->characteristics = calloc(chr_count, sizeof(struct ble_gatt_chr_def)); - assert(gatt_svc_has->characteristics); + gatt_svc_has->characteristics = bt_le_ext_calloc(chr_count, sizeof(struct ble_gatt_chr_def)); + BT_LE_ASSERT(gatt_svc_has->characteristics); has_svc_add_features_chr((void *)(gatt_svc_has->characteristics + 0)); @@ -226,6 +234,7 @@ int bt_le_nimble_has_init(void) LOG_ERR("[N]HasAddSvcsFail[%d]", rc); goto free; } + has_added = true; rc = has_svc_check(); if (rc) { @@ -235,7 +244,12 @@ int bt_le_nimble_has_init(void) return 0; free: - free((void *)gatt_svc_has->characteristics); - gatt_svc_has->characteristics = NULL; + /* Once ble_gatts_add_svcs() succeeds NimBLE keeps the svc_def pointer and + * offers no per-service unregister, so an added service must be leaked + * rather than freed into a dangling entry of its global list. */ + if (!has_added) { + free((void *)gatt_svc_has->characteristics); + gatt_svc_has->characteristics = NULL; + } return rc; } diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/mcs.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/mcs.c index c5e1e351517..895b2488ee5 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/mcs.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/mcs.c @@ -31,6 +31,7 @@ #include "nimble/server.h" #include "common/host.h" +#include "common/audio_attr.h" #include "../../../lib/include/audio.h" @@ -71,26 +72,26 @@ LOG_MODULE_REGISTER(LEA_MCS, CONFIG_BT_ISO_LOG_LEVEL); (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC) #define INC_OTS_CHR_FLAGS_ACTION_CP \ - (BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_INDICATE | BLE_GATT_CHR_F_WRITE_ENC) + (BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_INDICATE | BLE_GATT_CHR_F_WRITE_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC) #define INC_OTS_CHR_FLAGS_LIST_CP \ - (BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_INDICATE | BLE_GATT_CHR_F_WRITE_ENC) + (BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_INDICATE | BLE_GATT_CHR_F_WRITE_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC) -static uint8_t inc_ots_svc_count; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint8_t inc_ots_svc_count; -static struct bt_ots *ots; -static uint16_t inc_ots_chr_feature_handle; -static uint16_t inc_ots_chr_name_handle; -static uint16_t inc_ots_chr_type_handle; -static uint16_t inc_ots_chr_size_handle; -static uint16_t inc_ots_chr_id_handle; -static uint16_t inc_ots_chr_props_handle; -static uint16_t inc_ots_chr_action_cp_handle; -static uint16_t inc_ots_chr_list_cp_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR struct bt_ots *ots; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t inc_ots_chr_feature_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t inc_ots_chr_name_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t inc_ots_chr_type_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t inc_ots_chr_size_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t inc_ots_chr_id_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t inc_ots_chr_props_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t inc_ots_chr_action_cp_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t inc_ots_chr_list_cp_handle; -static struct ble_gatt_svc_def *gatt_svc_inc_ots; +static BT_AUDIO_EXT_RAM_BSS_ATTR struct ble_gatt_svc_def *gatt_svc_inc_ots; -static struct ble_gatt_svc_def **gmcs_inc_svcs; +static BT_AUDIO_EXT_RAM_BSS_ATTR struct ble_gatt_svc_def **gmcs_inc_svcs; static const ble_uuid16_t inc_ots_uuid_svc = BLE_UUID16_INIT(BT_UUID_OTS_VAL); static const ble_uuid16_t inc_ots_uuid_feature = BLE_UUID16_INIT(BT_UUID_OTS_FEATURE_VAL); @@ -103,34 +104,34 @@ static const ble_uuid16_t inc_ots_uuid_action_cp = BLE_UUID16_INIT(BT_UUID_OTS_A static const ble_uuid16_t inc_ots_uuid_list_cp = BLE_UUID16_INIT(BT_UUID_OTS_LIST_CP_VAL); #endif /* CONFIG_BT_OTS */ -static uint16_t mcs_player_name_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t mcs_player_name_handle; #if CONFIG_BT_OTS -static uint16_t mcs_icon_obj_id_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t mcs_icon_obj_id_handle; #endif /* CONFIG_BT_OTS */ -static uint16_t mcs_icon_url_handle; -static uint16_t mcs_track_changed_handle; -static uint16_t mcs_track_title_handle; -static uint16_t mcs_track_duration_handle; -static uint16_t mcs_track_position_handle; -static uint16_t mcs_playback_speed_handle; -static uint16_t mcs_seeking_speed_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t mcs_icon_url_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t mcs_track_changed_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t mcs_track_title_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t mcs_track_duration_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t mcs_track_position_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t mcs_playback_speed_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t mcs_seeking_speed_handle; #if CONFIG_BT_OTS -static uint16_t mcs_track_segments_obj_id_handle; -static uint16_t mcs_current_track_obj_id_handle; -static uint16_t mcs_next_track_obj_id_handle; -static uint16_t mcs_parent_group_obj_id_handle; -static uint16_t mcs_current_group_obj_id_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t mcs_track_segments_obj_id_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t mcs_current_track_obj_id_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t mcs_next_track_obj_id_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t mcs_parent_group_obj_id_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t mcs_current_group_obj_id_handle; #endif /* CONFIG_BT_OTS */ -static uint16_t mcs_playing_order_handle; -static uint16_t mcs_playing_orders_handle; -static uint16_t mcs_media_state_handle; -static uint16_t mcs_media_control_point_handle; -static uint16_t mcs_media_control_opcodes_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t mcs_playing_order_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t mcs_playing_orders_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t mcs_media_state_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t mcs_media_control_point_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t mcs_media_control_opcodes_handle; #if CONFIG_BT_OTS -static uint16_t mcs_search_control_point_handle; -static uint16_t mcs_search_results_obj_id_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t mcs_search_control_point_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t mcs_search_results_obj_id_handle; #endif /* CONFIG_BT_OTS */ -static uint16_t ccid_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t ccid_handle; static struct ble_gatt_svc_def gatt_svc_gmcs[] = { { @@ -145,7 +146,7 @@ static struct ble_gatt_svc_def gatt_svc_gmcs[] = { .access_cb = bt_le_nimble_gatts_access_cb_safe, .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, .val_handle = &mcs_player_name_handle, }, @@ -173,7 +174,7 @@ static struct ble_gatt_svc_def gatt_svc_gmcs[] = { .access_cb = bt_le_nimble_gatts_access_cb_safe, .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ - .flags = BLE_GATT_CHR_F_NOTIFY, + .flags = BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, .val_handle = &mcs_track_changed_handle, }, { @@ -181,7 +182,7 @@ static struct ble_gatt_svc_def gatt_svc_gmcs[] = { .access_cb = bt_le_nimble_gatts_access_cb_safe, .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, .val_handle = &mcs_track_title_handle, }, { @@ -189,7 +190,7 @@ static struct ble_gatt_svc_def gatt_svc_gmcs[] = { .access_cb = bt_le_nimble_gatts_access_cb_safe, .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, .val_handle = &mcs_track_duration_handle, }, { @@ -198,7 +199,7 @@ static struct ble_gatt_svc_def gatt_svc_gmcs[] = { .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | \ - BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_NOTIFY | \ + BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC | \ BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_WRITE_ENC, .min_key_size = 16, .val_handle = &mcs_track_position_handle, @@ -208,7 +209,7 @@ static struct ble_gatt_svc_def gatt_svc_gmcs[] = { .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | \ - BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_NOTIFY | \ + BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC | \ BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_WRITE_ENC, .min_key_size = 16, .val_handle = &mcs_playback_speed_handle, @@ -217,7 +218,7 @@ static struct ble_gatt_svc_def gatt_svc_gmcs[] = { .access_cb = bt_le_nimble_gatts_access_cb_safe, .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, .val_handle = &mcs_seeking_speed_handle, }, @@ -236,7 +237,7 @@ static struct ble_gatt_svc_def gatt_svc_gmcs[] = { .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | \ - BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_NOTIFY | \ + BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC | \ BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_WRITE_ENC, .min_key_size = 16, .val_handle = &mcs_current_track_obj_id_handle, @@ -246,7 +247,7 @@ static struct ble_gatt_svc_def gatt_svc_gmcs[] = { .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | \ - BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_NOTIFY | \ + BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC | \ BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_WRITE_ENC, .min_key_size = 16, .val_handle = &mcs_next_track_obj_id_handle, @@ -255,7 +256,7 @@ static struct ble_gatt_svc_def gatt_svc_gmcs[] = { .access_cb = bt_le_nimble_gatts_access_cb_safe, .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, .val_handle = &mcs_parent_group_obj_id_handle, }, { @@ -264,7 +265,7 @@ static struct ble_gatt_svc_def gatt_svc_gmcs[] = { .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | \ - BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_NOTIFY | \ + BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC | \ BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_WRITE_ENC, .min_key_size = 16, .val_handle = &mcs_current_group_obj_id_handle, @@ -276,7 +277,7 @@ static struct ble_gatt_svc_def gatt_svc_gmcs[] = { .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | \ - BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_NOTIFY | \ + BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC | \ BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_WRITE_ENC, .min_key_size = 16, .val_handle = &mcs_playing_order_handle, @@ -293,7 +294,7 @@ static struct ble_gatt_svc_def gatt_svc_gmcs[] = { .access_cb = bt_le_nimble_gatts_access_cb_safe, .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, .val_handle = &mcs_media_state_handle, }, { @@ -302,7 +303,7 @@ static struct ble_gatt_svc_def gatt_svc_gmcs[] = { .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ .flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP | \ - BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_WRITE_ENC, + BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_WRITE_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, .val_handle = &mcs_media_control_point_handle, }, { @@ -310,7 +311,7 @@ static struct ble_gatt_svc_def gatt_svc_gmcs[] = { .access_cb = bt_le_nimble_gatts_access_cb_safe, .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, .val_handle = &mcs_media_control_opcodes_handle, }, @@ -321,7 +322,7 @@ static struct ble_gatt_svc_def gatt_svc_gmcs[] = { .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ .flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP | \ - BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_WRITE_ENC, + BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_WRITE_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, .val_handle = &mcs_search_control_point_handle, }, { @@ -329,7 +330,7 @@ static struct ble_gatt_svc_def gatt_svc_gmcs[] = { .access_cb = bt_le_nimble_gatts_access_cb_safe, .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, .val_handle = &mcs_search_results_obj_id_handle, }, @@ -355,7 +356,7 @@ static struct ble_gatt_svc_def gatt_svc_gmcs[] = { /* Discrete MCS (0x1848): INSTANCE_COUNT instances sharing GMCS's characteristic table * and OTS include (val_handle vars are write-only here, so sharing is safe). Built in * bt_le_nimble_mcs_init as an N+1 NULL-terminated array for the GATT DB lifetime. */ -static struct ble_gatt_svc_def *gatt_svc_mcs; +static BT_AUDIO_EXT_RAM_BSS_ATTR struct ble_gatt_svc_def *gatt_svc_mcs; static const ble_uuid16_t mcs_uuid_svc = BLE_UUID16_INIT(BT_UUID_MCS_VAL); #if CONFIG_BT_OTS @@ -365,9 +366,9 @@ static int inc_ots_attr_handle_set(void) uint16_t start_handle; uint16_t end_handle; - assert(ots && ots->service); + BT_LE_ASSERT(ots && ots->service); - assert(inc_ots_chr_feature_handle >= 2); + BT_LE_ASSERT(inc_ots_chr_feature_handle >= 2); start_handle = inc_ots_chr_feature_handle - 2; /* server attr handle & char def handle */ end_handle = inc_ots_chr_list_cp_handle + 1; /* cccd for chr Object List Control Point */ @@ -471,7 +472,7 @@ static int gmcs_svc_check(void) for (size_t i = 0; i < gmcs_svc->attr_count; i++) { uuid = (const struct bt_uuid_16 *)(gmcs_svc->attrs + i)->uuid; - if (uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && + if (uuid && uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && uuid->val == check->value) { chr_found = true; break; @@ -497,8 +498,8 @@ static int inc_ots_svc_check(void) * the service exist in the service defined by Zephyr. */ - assert(gatt_svc_inc_ots); - assert(ots && ots->service); + BT_LE_ASSERT(gatt_svc_inc_ots); + BT_LE_ASSERT(ots && ots->service); LOG_DBG("[N]IncOtsSvcCheck"); @@ -511,7 +512,7 @@ static int inc_ots_svc_check(void) for (size_t i = 0; i < ots->service->attr_count; i++) { uuid = (const struct bt_uuid_16 *)(ots->service->attrs + i)->uuid; - if (uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && + if (uuid && uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && uuid->val == check->value) { chr_found = true; break; @@ -550,7 +551,7 @@ static int inc_ots_svc_init(void) uint8_t chr_count; attrs = bt_ots_svc_decl_get(ots); - assert(attrs); + BT_LE_ASSERT(attrs); chr_count = 0; @@ -574,8 +575,8 @@ static int inc_ots_svc_init(void) svc->includes = NULL; /* An additional characteristic consist of all 0s indicating end of characteristics */ - svc->characteristics = calloc(INC_OTS_CHR_COUNT + 1, sizeof(struct ble_gatt_chr_def)); - assert(svc->characteristics); + svc->characteristics = bt_le_ext_calloc(INC_OTS_CHR_COUNT + 1, sizeof(struct ble_gatt_chr_def)); + BT_LE_ASSERT(svc->characteristics); /* Characteristic - OTS Feature */ inc_ots_chr_init((void *)&svc->characteristics[0], @@ -629,8 +630,18 @@ static int inc_ots_svc_init(void) } #endif /* CONFIG_BT_OTS */ +void bt_le_nimble_mcs_state_reset(void) +{ +#if CONFIG_BT_OTS + inc_ots_svc_count = 0; +#endif /* CONFIG_BT_OTS */ +} + int bt_le_nimble_gmcs_init(bool ots_included) { +#if CONFIG_BT_OTS + bool inc_ots_added = false; +#endif /* CONFIG_BT_OTS */ int rc; LOG_DBG("[N]GmcsInit[%u]", ots_included); @@ -640,15 +651,15 @@ int bt_le_nimble_gmcs_init(bool ots_included) inc_ots_svc_count = 1; /* Extra one for terminating the included service array with NULL */ - gmcs_inc_svcs = calloc(2, sizeof(struct ble_gatt_svc_def *)); - assert(gmcs_inc_svcs); + gmcs_inc_svcs = bt_le_ext_calloc(2, sizeof(struct ble_gatt_svc_def *)); + BT_LE_ASSERT(gmcs_inc_svcs); /* Extra one for terminating the OTS service array */ - gatt_svc_inc_ots = calloc(2, sizeof(struct ble_gatt_svc_def)); - assert(gatt_svc_inc_ots); + gatt_svc_inc_ots = bt_le_ext_calloc(2, sizeof(struct ble_gatt_svc_def)); + BT_LE_ASSERT(gatt_svc_inc_ots); ots = lib_mcs_get_ots(); - assert(ots && ots->service); + BT_LE_ASSERT(ots && ots->service); rc = inc_ots_svc_init(); if (rc) { @@ -668,6 +679,7 @@ int bt_le_nimble_gmcs_init(bool ots_included) LOG_ERR("[N]IncOtsAddSvcsFail[%d]", rc); goto free; } + inc_ots_added = true; rc = inc_ots_svc_check(); if (rc) { @@ -702,18 +714,23 @@ int bt_le_nimble_gmcs_init(bool ots_included) free: #if CONFIG_BT_OTS + /* Once ble_gatts_add_svcs() succeeds NimBLE keeps the svc_def pointer and + * offers no per-service unregister, so an added service must be leaked + * rather than freed into a dangling entry of its global list. */ if (ots_included) { inc_ots_svc_count = 0; free(gmcs_inc_svcs); gmcs_inc_svcs = NULL; - if (gatt_svc_inc_ots[0].characteristics) { - free((void *)gatt_svc_inc_ots[0].characteristics); - gatt_svc_inc_ots[0].characteristics = NULL; + if (!inc_ots_added) { + if (gatt_svc_inc_ots[0].characteristics) { + free((void *)gatt_svc_inc_ots[0].characteristics); + gatt_svc_inc_ots[0].characteristics = NULL; + } + free(gatt_svc_inc_ots); + gatt_svc_inc_ots = NULL; } - free(gatt_svc_inc_ots); - gatt_svc_inc_ots = NULL; } gatt_svc_gmcs[0].includes = NULL; #endif /* CONFIG_BT_OTS */ @@ -778,7 +795,7 @@ static int mcs_svc_check(void) for (size_t i = 0; i < mcs_svc->attr_count; i++) { uuid = (const struct bt_uuid_16 *)(mcs_svc->attrs + i)->uuid; - if (uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && + if (uuid && uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && uuid->val == check->value) { chr_found = true; break; @@ -797,6 +814,7 @@ static int mcs_svc_check(void) int bt_le_nimble_mcs_init(void) { uint8_t count = CONFIG_BT_MCS_INSTANCE_COUNT; + bool mcs_added = false; int rc; /* NULL when CONFIG_BT_MCS_INSTANCE_COUNT == 0: nothing discrete to add. */ @@ -809,8 +827,8 @@ int bt_le_nimble_mcs_init(void) /* One ble_gatt_svc_def per instance + a zeroed terminator. Persists for the * GATT DB lifetime (NimBLE references these defs until ble_gatts_start). */ - gatt_svc_mcs = calloc(count + 1, sizeof(struct ble_gatt_svc_def)); - assert(gatt_svc_mcs); + gatt_svc_mcs = bt_le_ext_calloc(count + 1, sizeof(struct ble_gatt_svc_def)); + BT_LE_ASSERT(gatt_svc_mcs); for (int i = 0; i < count; i++) { gatt_svc_mcs[i].type = BLE_GATT_SVC_TYPE_PRIMARY; @@ -836,6 +854,7 @@ int bt_le_nimble_mcs_init(void) LOG_ERR("[N]McsAddSvcsFail[%d]", rc); goto free; } + mcs_added = true; rc = mcs_svc_check(); if (rc) { @@ -845,7 +864,12 @@ int bt_le_nimble_mcs_init(void) return 0; free: - free(gatt_svc_mcs); - gatt_svc_mcs = NULL; + /* Once ble_gatts_add_svcs() succeeds NimBLE keeps the svc_def pointer and + * offers no per-service unregister, so an added service must be leaked + * rather than freed into a dangling entry of its global list. */ + if (!mcs_added) { + free(gatt_svc_mcs); + gatt_svc_mcs = NULL; + } return rc; } diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/mics.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/mics.c index 472729d2a3e..760c30252a3 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/mics.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/mics.c @@ -26,6 +26,7 @@ #include "nimble/server.h" #include "common/host.h" +#include "common/audio_attr.h" #include "../../../lib/include/audio.h" @@ -34,7 +35,7 @@ LOG_MODULE_REGISTER(LEA_MICS, CONFIG_BT_ISO_LOG_LEVEL); #define INC_AICS_CHR_COUNT (6 + 1) #define INC_AICS_CHR_FLAGS_STATE \ - (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC) + (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC) #define INC_AICS_CHR_FLAGS_GAIN \ (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC) @@ -43,18 +44,18 @@ LOG_MODULE_REGISTER(LEA_MICS, CONFIG_BT_ISO_LOG_LEVEL); (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC) #define INC_AICS_CHR_FLAGS_STATUS \ - (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC) + (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC) #define INC_AICS_CHR_FLAGS_CONTROL \ (BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_ENC) #define INC_AICS_CHR_FLAGS_DESCRIPTION \ - (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_WRITE_NO_RSP | \ + (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC | \ BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_WRITE_ENC) -static uint8_t inc_aics_svc_count; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint8_t inc_aics_svc_count; -static struct inc_aics_inst { +static BT_AUDIO_EXT_RAM_BSS_ATTR struct inc_aics_inst { struct bt_gatt_service *svc_p; uint16_t state_handle; uint16_t gain_handle; @@ -64,7 +65,7 @@ static struct inc_aics_inst { uint16_t description_handle; } *inc_aics_insts; -static struct ble_gatt_svc_def *gatt_svc_inc_aics; +static BT_AUDIO_EXT_RAM_BSS_ATTR struct ble_gatt_svc_def *gatt_svc_inc_aics; static const ble_uuid16_t inc_aics_uuid_svc = BLE_UUID16_INIT(BT_UUID_AICS_VAL); static const ble_uuid16_t inc_aics_uuid_state = BLE_UUID16_INIT(BT_UUID_AICS_STATE_VAL); @@ -74,9 +75,9 @@ static const ble_uuid16_t inc_aics_uuid_status = BLE_UUID16_INIT(BT_UUID_AICS_IN static const ble_uuid16_t inc_aics_uuid_control = BLE_UUID16_INIT(BT_UUID_AICS_CONTROL_VAL); static const ble_uuid16_t inc_aics_uuid_description = BLE_UUID16_INIT(BT_UUID_AICS_DESCRIPTION_VAL); -static struct ble_gatt_svc_def **mics_inc_svcs; +static BT_AUDIO_EXT_RAM_BSS_ATTR struct ble_gatt_svc_def **mics_inc_svcs; -static uint16_t mics_mute_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t mics_mute_handle; static struct ble_gatt_svc_def gatt_svc_mics[] = { { @@ -91,7 +92,7 @@ static struct ble_gatt_svc_def gatt_svc_mics[] = { .access_cb = bt_le_nimble_gatts_access_cb_safe, .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | \ + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC | \ BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_READ_ENC | \ BLE_GATT_CHR_F_WRITE_ENC, .min_key_size = 16, @@ -115,8 +116,8 @@ static int inc_aics_svc_check(void) * the service exist in the service defined by Zephyr. */ - assert(gatt_svc_inc_aics); - assert(inc_aics_insts); + BT_LE_ASSERT(gatt_svc_inc_aics); + BT_LE_ASSERT(inc_aics_insts); LOG_DBG("[N]IncAicsSvcCheck[%u]", inc_aics_svc_count); @@ -124,7 +125,7 @@ static int inc_aics_svc_check(void) struct ble_gatt_svc_def *aics = &gatt_svc_inc_aics[i]; struct bt_gatt_service *svc = inc_aics_insts[i].svc_p; - assert(svc); + BT_LE_ASSERT(svc); for (const struct ble_gatt_chr_def *chr = aics->characteristics; chr && chr->uuid; chr++) { @@ -135,7 +136,7 @@ static int inc_aics_svc_check(void) for (size_t j = 0; j < svc->attr_count; j++) { uuid = (const struct bt_uuid_16 *)(svc->attrs + j)->uuid; - if (uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && + if (uuid && uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && uuid->val == check->value) { chr_found = true; break; @@ -175,7 +176,7 @@ static int mics_svc_check(void) for (size_t i = 0; i < mics_svc->attr_count; i++) { uuid = (const struct bt_uuid_16 *)(mics_svc->attrs + i)->uuid; - if (uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && + if (uuid && uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && uuid->val == check->value) { chr_found = true; break; @@ -197,14 +198,14 @@ static int inc_aics_attr_handle_set(void) uint16_t start_handle; uint16_t end_handle; - assert(inc_aics_insts); + BT_LE_ASSERT(inc_aics_insts); LOG_DBG("[N]IncAicsAttrHdlSet[%u]", inc_aics_svc_count); for (size_t i = 0; i < inc_aics_svc_count; i++) { - assert(inc_aics_insts[i].svc_p); + BT_LE_ASSERT(inc_aics_insts[i].svc_p); - assert(inc_aics_insts[i].state_handle >= 2); + BT_LE_ASSERT(inc_aics_insts[i].state_handle >= 2); start_handle = inc_aics_insts[i].state_handle - 2; /* server attr handle & char def handle */ end_handle = inc_aics_insts[i].description_handle + 1; /* cccd for chr Audio Input Description */ @@ -305,8 +306,8 @@ static void inc_aics_svc_init(struct inc_aics_inst *inst, svc->uuid = &inc_aics_uuid_svc.u; svc->includes = NULL; - svc->characteristics = calloc(INC_AICS_CHR_COUNT, sizeof(struct ble_gatt_chr_def)); - assert(svc->characteristics); + svc->characteristics = bt_le_ext_calloc(INC_AICS_CHR_COUNT, sizeof(struct ble_gatt_chr_def)); + BT_LE_ASSERT(svc->characteristics); /* Characteristic - Audio Input State */ inc_aics_chr_init((void *)&svc->characteristics[0], @@ -345,9 +346,15 @@ static void inc_aics_svc_init(struct inc_aics_inst *inst, INC_AICS_CHR_FLAGS_DESCRIPTION); } +void bt_le_nimble_mics_state_reset(void) +{ + inc_aics_svc_count = 0; +} + int bt_le_nimble_mics_init(void *micp_inc) { struct bt_micp_included *micp_included; + bool inc_aics_added = false; uint8_t inc_count; int rc; @@ -369,17 +376,17 @@ int bt_le_nimble_mics_init(void *micp_inc) /* Extra one for terminating the included service array with NULL */ inc_count = inc_aics_svc_count + 1; - mics_inc_svcs = calloc(inc_count, sizeof(struct ble_gatt_svc_def *)); - assert(mics_inc_svcs); + mics_inc_svcs = bt_le_ext_calloc(inc_count, sizeof(struct ble_gatt_svc_def *)); + BT_LE_ASSERT(mics_inc_svcs); /* MICS may include zero or more instances of AICS */ if (inc_aics_svc_count) { - inc_aics_insts = calloc(inc_aics_svc_count, sizeof(struct inc_aics_inst)); - assert(inc_aics_insts); + inc_aics_insts = bt_le_ext_calloc(inc_aics_svc_count, sizeof(struct inc_aics_inst)); + BT_LE_ASSERT(inc_aics_insts); /* Extra one for terminating the AICS service array */ - gatt_svc_inc_aics = calloc(inc_aics_svc_count + 1, sizeof(struct ble_gatt_svc_def)); - assert(gatt_svc_inc_aics); + gatt_svc_inc_aics = bt_le_ext_calloc(inc_aics_svc_count + 1, sizeof(struct ble_gatt_svc_def)); + BT_LE_ASSERT(gatt_svc_inc_aics); for (size_t i = 0; i < inc_aics_svc_count; i++) { inc_aics_svc_init(&inc_aics_insts[i], &gatt_svc_inc_aics[i]); @@ -405,6 +412,7 @@ int bt_le_nimble_mics_init(void *micp_inc) LOG_ERR("[N]IncAicsAddSvcsFail[%d]", rc); goto free; } + inc_aics_added = true; rc = inc_aics_svc_check(); if (rc) { @@ -438,22 +446,28 @@ int bt_le_nimble_mics_init(void *micp_inc) return 0; free: + /* Once ble_gatts_add_svcs() succeeds NimBLE keeps the svc_def pointer and + * offers no per-service unregister, so an added service must be leaked + * rather than freed into a dangling entry of its global list. That covers + * inc_aics_insts too: the registered chr_defs point at its handle fields. */ if (micp_included) { free(mics_inc_svcs); mics_inc_svcs = NULL; if (inc_aics_svc_count) { - free(inc_aics_insts); - inc_aics_insts = NULL; + if (!inc_aics_added) { + free(inc_aics_insts); + inc_aics_insts = NULL; - for (size_t i = 0; i < inc_aics_svc_count; i++) { - free((void *)gatt_svc_inc_aics[i].characteristics); - gatt_svc_inc_aics[i].characteristics = NULL; + for (size_t i = 0; i < inc_aics_svc_count; i++) { + free((void *)gatt_svc_inc_aics[i].characteristics); + gatt_svc_inc_aics[i].characteristics = NULL; + } + + free(gatt_svc_inc_aics); + gatt_svc_inc_aics = NULL; } - free(gatt_svc_inc_aics); - gatt_svc_inc_aics = NULL; - inc_aics_svc_count = 0; } } diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/pacs.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/pacs.c index c93107a5150..3fd530ecb9d 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/pacs.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/pacs.c @@ -24,27 +24,28 @@ #include "nimble/server.h" #include "common/host.h" +#include "common/audio_attr.h" #include "../../../lib/include/audio.h" LOG_MODULE_REGISTER(LEA_PACS, CONFIG_BT_ISO_LOG_LEVEL); #if CONFIG_BT_PAC_SNK -static uint16_t pacs_snk_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t pacs_snk_handle; #if CONFIG_BT_PAC_SNK_LOC -static uint16_t pacs_snk_loc_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t pacs_snk_loc_handle; #endif /* CONFIG_BT_PAC_SNK_LOC */ #endif /* CONFIG_BT_PAC_SNK */ #if CONFIG_BT_PAC_SRC -static uint16_t pacs_src_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t pacs_src_handle; #if CONFIG_BT_PAC_SRC_LOC -static uint16_t pacs_src_loc_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t pacs_src_loc_handle; #endif /* CONFIG_BT_PAC_SRC_LOC */ #endif /* CONFIG_BT_PAC_SRC */ -static uint16_t pacs_ava_ctx_handle; -static uint16_t pacs_sup_ctx_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t pacs_ava_ctx_handle; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t pacs_sup_ctx_handle; static const struct ble_gatt_svc_def gatt_svc_pacs[] = { { @@ -61,7 +62,7 @@ static const struct ble_gatt_svc_def gatt_svc_pacs[] = { .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ #if CONFIG_BT_PAC_SNK_NOTIFIABLE - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, #else /* CONFIG_BT_PAC_SNK_NOTIFIABLE */ .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC, #endif /* CONFIG_BT_PAC_SNK_NOTIFIABLE */ @@ -76,13 +77,13 @@ static const struct ble_gatt_svc_def gatt_svc_pacs[] = { .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ #if CONFIG_BT_PAC_SNK_LOC_WRITEABLE && CONFIG_BT_PAC_SNK_LOC_NOTIFIABLE .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | \ - BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | \ + BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC | \ BLE_GATT_CHR_F_WRITE_ENC, #elif CONFIG_BT_PAC_SNK_LOC_WRITEABLE && !CONFIG_BT_PAC_SNK_LOC_NOTIFIABLE .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | \ BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_WRITE_ENC, #elif !CONFIG_BT_PAC_SNK_LOC_WRITEABLE && CONFIG_BT_PAC_SNK_LOC_NOTIFIABLE - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, #else /* !CONFIG_BT_PAC_SNK_LOC_WRITEABLE && !CONFIG_BT_PAC_SNK_LOC_NOTIFIABLE */ .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC, #endif /* CONFIG_BT_PAC_SNK_LOC_WRITEABLE && CONFIG_BT_PAC_SNK_LOC_NOTIFIABLE */ @@ -98,7 +99,7 @@ static const struct ble_gatt_svc_def gatt_svc_pacs[] = { .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ #if CONFIG_BT_PAC_SRC_NOTIFIABLE - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, #else /* CONFIG_BT_PAC_SRC_NOTIFIABLE */ .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC, #endif /* CONFIG_BT_PAC_SRC_NOTIFIABLE */ @@ -113,13 +114,13 @@ static const struct ble_gatt_svc_def gatt_svc_pacs[] = { .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ #if CONFIG_BT_PAC_SRC_LOC_WRITEABLE && CONFIG_BT_PAC_SRC_LOC_NOTIFIABLE .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | \ - BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | \ + BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC | \ BLE_GATT_CHR_F_WRITE_ENC, #elif CONFIG_BT_PAC_SRC_LOC_WRITEABLE && !CONFIG_BT_PAC_SRC_LOC_NOTIFIABLE .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | \ BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_WRITE_ENC, #elif !CONFIG_BT_PAC_SRC_LOC_WRITEABLE && CONFIG_BT_PAC_SRC_LOC_NOTIFIABLE - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, #else /* !CONFIG_BT_PAC_SRC_LOC_WRITEABLE && !CONFIG_BT_PAC_SRC_LOC_NOTIFIABLE */ .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC, #endif /* CONFIG_BT_PAC_SRC_LOC_WRITEABLE && CONFIG_BT_PAC_SRC_LOC_NOTIFIABLE */ @@ -133,7 +134,7 @@ static const struct ble_gatt_svc_def gatt_svc_pacs[] = { .access_cb = bt_le_nimble_gatts_access_cb_safe, .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, .val_handle = &pacs_ava_ctx_handle, }, { @@ -142,7 +143,7 @@ static const struct ble_gatt_svc_def gatt_svc_pacs[] = { .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ #if CONFIG_BT_PACS_SUPPORTED_CONTEXT_NOTIFIABLE - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, #else /* CONFIG_BT_PACS_SUPPORTED_CONTEXT_NOTIFIABLE */ .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC, #endif /* CONFIG_BT_PACS_SUPPORTED_CONTEXT_NOTIFIABLE */ @@ -172,13 +173,13 @@ int bt_le_nimble_pacs_attr_handle_set(void) } #if CONFIG_BT_PAC_SNK - assert(pacs_snk_handle >= 2); + BT_LE_ASSERT(pacs_snk_handle >= 2); start_handle = pacs_snk_handle - 2; /* server attr handle & char def handle */ #elif CONFIG_BT_PAC_SRC - assert(pacs_src_handle >= 2); + BT_LE_ASSERT(pacs_src_handle >= 2); start_handle = pacs_src_handle - 2; /* server attr handle & char def handle */ #else - assert(pacs_ava_ctx_handle >= 2); + BT_LE_ASSERT(pacs_ava_ctx_handle >= 2); start_handle = pacs_ava_ctx_handle - 2; /* server attr handle & char def handle */ #endif #if CONFIG_BT_PACS_SUPPORTED_CONTEXT_NOTIFIABLE @@ -233,7 +234,7 @@ static int pacs_svc_check(void) for (size_t i = 0; i < pacs_svc->attr_count; i++) { uuid = (const struct bt_uuid_16 *)(pacs_svc->attrs + i)->uuid; - if (uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && + if (uuid && uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && uuid->val == check->value) { chr_found = true; break; @@ -274,3 +275,9 @@ int bt_le_nimble_pacs_init(void) return 0; } + +int bt_le_nimble_pacs_deinit(void) +{ + LOG_DBG("[N]PacsDeinit"); + return 0; +} diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/tbs.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/tbs.c index 4c67b780618..940a7a3f91d 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/tbs.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/tbs.c @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include @@ -24,6 +26,7 @@ #include "nimble/server.h" #include "common/host.h" +#include "common/audio_attr.h" #include "../../../lib/include/audio.h" @@ -42,7 +45,7 @@ static const struct ble_gatt_svc_def gatt_svc_gtbs[] = { .access_cb = bt_le_nimble_gatts_access_cb_safe, .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, }, { .uuid = BLE_UUID16_DECLARE(BT_UUID_TBS_UCI_VAL), @@ -56,21 +59,21 @@ static const struct ble_gatt_svc_def gatt_svc_gtbs[] = { .access_cb = bt_le_nimble_gatts_access_cb_safe, .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, }, { .uuid = BLE_UUID16_DECLARE(BT_UUID_TBS_URI_LIST_VAL), .access_cb = bt_le_nimble_gatts_access_cb_safe, .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, }, { .uuid = BLE_UUID16_DECLARE(BT_UUID_TBS_SIGNAL_STRENGTH_VAL), .access_cb = bt_le_nimble_gatts_access_cb_safe, .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, }, { .uuid = BLE_UUID16_DECLARE(BT_UUID_TBS_SIGNAL_INTERVAL_VAL), @@ -86,7 +89,7 @@ static const struct ble_gatt_svc_def gatt_svc_gtbs[] = { .access_cb = bt_le_nimble_gatts_access_cb_safe, .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, }, { .uuid = BLE_UUID16_DECLARE(BT_UUID_CCID_VAL), @@ -100,21 +103,21 @@ static const struct ble_gatt_svc_def gatt_svc_gtbs[] = { .access_cb = bt_le_nimble_gatts_access_cb_safe, .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, }, { .uuid = BLE_UUID16_DECLARE(BT_UUID_TBS_INCOMING_URI_VAL), .access_cb = bt_le_nimble_gatts_access_cb_safe, .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, }, { .uuid = BLE_UUID16_DECLARE(BT_UUID_TBS_CALL_STATE_VAL), .access_cb = bt_le_nimble_gatts_access_cb_safe, .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, }, { .uuid = BLE_UUID16_DECLARE(BT_UUID_TBS_CALL_CONTROL_POINT_VAL), @@ -122,7 +125,7 @@ static const struct ble_gatt_svc_def gatt_svc_gtbs[] = { .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ .flags = BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_WRITE | \ - BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_WRITE_ENC, + BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_WRITE_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, }, { .uuid = BLE_UUID16_DECLARE(BT_UUID_TBS_OPTIONAL_OPCODES_VAL), @@ -136,21 +139,21 @@ static const struct ble_gatt_svc_def gatt_svc_gtbs[] = { .access_cb = bt_le_nimble_gatts_access_cb_safe, .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ - .flags = BLE_GATT_CHR_F_NOTIFY, + .flags = BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, }, { .uuid = BLE_UUID16_DECLARE(BT_UUID_TBS_INCOMING_CALL_VAL), .access_cb = bt_le_nimble_gatts_access_cb_safe, .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, }, { .uuid = BLE_UUID16_DECLARE(BT_UUID_TBS_FRIENDLY_NAME_VAL), .access_cb = bt_le_nimble_gatts_access_cb_safe, .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, }, { 0, /* No more characteristics in this service. */ @@ -162,19 +165,43 @@ static const struct ble_gatt_svc_def gatt_svc_gtbs[] = { }, }; -/* Discrete TBS (0x184B) shares GTBS's characteristic table, bound at init (a - * static initializer can't reference another object's member). */ -static struct ble_gatt_svc_def gatt_svc_tbs[] = { - { - .type = BLE_GATT_SVC_TYPE_PRIMARY, - .uuid = BLE_UUID16_DECLARE(BT_UUID_TBS_VAL), - .includes = NULL, - .characteristics = NULL, - }, - { - 0, /* No more services. */ - }, -}; +#if CONFIG_BT_TBS_BEARER_COUNT > 0 +static const ble_uuid16_t tbs_uuid_svc = BLE_UUID16_INIT(BT_UUID_TBS_VAL); + +/* A def and a characteristic table per bearer: val_handle lives in the table, + * so a shared one would only ever hold the last bearer's handles. */ +static BT_AUDIO_EXT_RAM_BSS_ATTR struct ble_gatt_svc_def *tbs_svc_defs[CONFIG_BT_TBS_BEARER_COUNT]; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t tbs_anchor_handle[CONFIG_BT_TBS_BEARER_COUNT]; + +static struct ble_gatt_svc_def *tbs_svc_def_new(uint16_t *anchor) +{ + const struct ble_gatt_chr_def *src = gatt_svc_gtbs[0].characteristics; + struct ble_gatt_chr_def *chrs; + struct ble_gatt_svc_def *def; + size_t count = 0; + + while (src[count].uuid) { + count++; + } + + def = bt_le_ext_calloc(2, sizeof(*def)); + chrs = bt_le_ext_calloc(count + 1, sizeof(*chrs)); + if (!def || !chrs) { + free(def); + free(chrs); + return NULL; + } + + memcpy(chrs, src, count * sizeof(*chrs)); + chrs[0].val_handle = anchor; + + def[0].type = BLE_GATT_SVC_TYPE_PRIMARY; + def[0].uuid = &tbs_uuid_svc.u; + def[0].characteristics = chrs; + + return def; +} +#endif /* CONFIG_BT_TBS_BEARER_COUNT > 0 */ int bt_le_nimble_gtbs_attr_handle_set(void) { @@ -233,7 +260,7 @@ static int gtbs_svc_check(void) for (size_t i = 0; i < gtbs_svc->attr_count; i++) { uuid = (const struct bt_uuid_16 *)(gtbs_svc->attrs + i)->uuid; - if (uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && + if (uuid && uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && uuid->val == check->value) { chr_found = true; break; @@ -277,34 +304,49 @@ int bt_le_nimble_gtbs_init(void) int bt_le_nimble_tbs_attr_handle_set(void) { +#if CONFIG_BT_TBS_BEARER_COUNT > 0 struct bt_gatt_service *tbs_list; - uint16_t handle = 0; - int rc; + const struct bt_uuid_16 *uuid; + uint16_t base; - rc = ble_gatts_find_svc(BLE_UUID16_DECLARE(BT_UUID_TBS_VAL), &handle); - if (rc) { + tbs_list = lib_tbs_server_list_get(); + if (!tbs_list) { LOG_DBG("[N]TbsNotInit"); return 0; } - tbs_list = lib_tbs_server_list_get(); - if (!tbs_list) { - LOG_ERR("[N]TbsSvcListGetFail"); - return -ENODEV; - } + for (int i = 0; i < CONFIG_BT_TBS_BEARER_COUNT; i++) { + struct bt_gatt_service *svc = &tbs_list[i]; - /* ble_gatts_find_svc returns the first match, so only the single discrete - * bearer is mapped (sufficient for CONFIG_BT_TBS_BEARER_COUNT==1). - */ - LOG_DBG("[N]TbsAttrHdlSet[%u][%u]", handle, tbs_list[0].attr_count); + /* Zero when the bearer was registered after the boot's ble_gatts_start(): + * it never reached the ATT database, so there is no range to anchor. */ + if (!tbs_svc_defs[i] || tbs_anchor_handle[i] < 2) { + continue; + } - for (size_t i = 0; i < tbs_list[0].attr_count; i++) { - (tbs_list[0].attrs + i)->handle = handle + i; + base = tbs_anchor_handle[i] - 2; /* service decl & char def handle */ + + /* Holds only while the lib orders this bearer the way NimBLE registered it. */ + uuid = svc->attr_count > 2 ? (const struct bt_uuid_16 *)svc->attrs[2].uuid : NULL; + + if (!uuid || uuid->uuid.type != BT_UUID_TYPE_16 || + uuid->val != BT_UUID_TBS_PROVIDER_NAME_VAL) { + LOG_ERR("[N]TbsAnchorMismatch[%d][%u]", i, tbs_anchor_handle[i]); + return -1; + } + + LOG_DBG("[N]TbsAttrHdlSet[%d][%u][%u]", i, base, svc->attr_count); + + for (size_t j = 0; j < svc->attr_count; j++) { + (svc->attrs + j)->handle = base + j; + } } +#endif /* CONFIG_BT_TBS_BEARER_COUNT > 0 */ return 0; } +#if CONFIG_BT_TBS_BEARER_COUNT > 0 static int tbs_svc_check(void) { struct bt_gatt_service *tbs_list; @@ -319,7 +361,8 @@ static int tbs_svc_check(void) LOG_DBG("[N]TbsSvcCheck"); - for (const struct ble_gatt_chr_def *chr = gatt_svc_tbs[0].characteristics; + /* Every bearer's table is a copy of this one. */ + for (const struct ble_gatt_chr_def *chr = gatt_svc_gtbs[0].characteristics; chr && chr->uuid; chr++) { const ble_uuid16_t *check = (const ble_uuid16_t *)chr->uuid; @@ -328,7 +371,7 @@ static int tbs_svc_check(void) for (size_t i = 0; i < tbs_list[0].attr_count; i++) { uuid = (const struct bt_uuid_16 *)(tbs_list[0].attrs + i)->uuid; - if (uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && + if (uuid && uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && uuid->val == check->value) { chr_found = true; break; @@ -343,32 +386,70 @@ static int tbs_svc_check(void) return 0; } +#endif /* CONFIG_BT_TBS_BEARER_COUNT > 0 */ int bt_le_nimble_tbs_init(void) { +#if CONFIG_BT_TBS_BEARER_COUNT > 0 + struct bt_gatt_service *tbs_list; int rc; + int i; - LOG_DBG("[N]TbsInit"); - - /* Bind the shared characteristic table (see gatt_svc_tbs comment). */ - gatt_svc_tbs[0].characteristics = gatt_svc_gtbs[0].characteristics; - - rc = ble_gatts_count_cfg(gatt_svc_tbs); - if (rc) { - LOG_ERR("[N]TbsCountCfgFail[%d]", rc); - return rc; + tbs_list = lib_tbs_server_list_get(); + if (!tbs_list) { + return 0; } - rc = ble_gatts_add_svcs(gatt_svc_tbs); - if (rc) { - LOG_ERR("[N]TbsAddSvcsFail[%d]", rc); - return rc; + /* Runs once per bearer registration and gets no index, so sweep for the slot + * that is registered but not yet added - as bt_le_bluedroid_tbs_init() does. */ + for (i = 0; i < CONFIG_BT_TBS_BEARER_COUNT; i++) { + if (tbs_list[i].attr_count == 0 || tbs_svc_defs[i]) { + continue; + } + + LOG_DBG("[N]TbsInit[%d]", i); + + tbs_svc_defs[i] = tbs_svc_def_new(&tbs_anchor_handle[i]); + if (!tbs_svc_defs[i]) { + LOG_ERR("[N]TbsSvcDefAllocFail[%d]", i); + return -ENOMEM; + } + + rc = ble_gatts_count_cfg(tbs_svc_defs[i]); + if (rc) { + LOG_ERR("[N]TbsCountCfgFail[%d]", rc); + goto free; + } + + rc = ble_gatts_add_svcs(tbs_svc_defs[i]); + if (rc) { + LOG_ERR("[N]TbsAddSvcsFail[%d]", rc); + goto free; + } } - rc = tbs_svc_check(); - if (rc) { - return rc; - } + return tbs_svc_check(); +free: + /* Only reachable before ble_gatts_add_svcs() succeeded; after that NimBLE + * holds the def and it must be leaked instead. */ + free((void *)tbs_svc_defs[i]->characteristics); + free(tbs_svc_defs[i]); + tbs_svc_defs[i] = NULL; + return rc; +#else + return 0; +#endif /* CONFIG_BT_TBS_BEARER_COUNT > 0 */ +} + +int bt_le_nimble_gtbs_deinit(void) +{ + LOG_DBG("[N]GtbsDeinit"); + return 0; +} + +int bt_le_nimble_tbs_deinit(uint8_t bearer_index) +{ + LOG_DBG("[N]TbsDeinit[%u]", bearer_index); return 0; } diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/tmas.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/tmas.c index 6e861c8013d..791f1f95a63 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/tmas.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/tmas.c @@ -112,7 +112,7 @@ static int tmas_svc_check(void) for (size_t i = 0; i < tmas_svc->attr_count; i++) { uuid = (const struct bt_uuid_16 *)(tmas_svc->attrs + i)->uuid; - if (uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && + if (uuid && uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && uuid->val == check->value) { chr_found = true; break; diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/vcs.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/vcs.c index 2c775fe6f5e..4de10b13bae 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/vcs.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/vcs.c @@ -26,6 +26,7 @@ #include "nimble/server.h" #include "common/host.h" +#include "common/audio_attr.h" #include "../../../lib/include/audio.h" @@ -36,22 +37,22 @@ LOG_MODULE_REGISTER(LEA_VCS, CONFIG_BT_ISO_LOG_LEVEL); #define INC_VOCS_CHR_COUNT (4 + 1) #define INC_VOCS_CHR_FLAGS_STATE \ - (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC) + (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC) #define INC_VOCS_CHR_FLAGS_LOCATION \ - (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_WRITE_NO_RSP | \ + (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC | \ BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_WRITE_ENC) #define INC_VOCS_CHR_FLAGS_CONTROL \ (BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_ENC) #define INC_VOCS_CHR_FLAGS_DESCRIPTION \ - (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_WRITE_NO_RSP | \ + (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC | \ BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_WRITE_ENC) -static uint8_t inc_vocs_svc_count; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint8_t inc_vocs_svc_count; -static struct inc_vocs_inst { +static BT_AUDIO_EXT_RAM_BSS_ATTR struct inc_vocs_inst { struct bt_gatt_service *svc_p; uint16_t state_handle; uint16_t location_handle; @@ -59,7 +60,7 @@ static struct inc_vocs_inst { uint16_t description_handle; } inc_vocs_insts[VOCS_INST_COUNT]; -static struct ble_gatt_svc_def *gatt_svc_inc_vocs; +static BT_AUDIO_EXT_RAM_BSS_ATTR struct ble_gatt_svc_def *gatt_svc_inc_vocs; static const ble_uuid16_t inc_vocs_uuid_svc = BLE_UUID16_INIT(BT_UUID_VOCS_VAL); static const ble_uuid16_t inc_vocs_uuid_state = BLE_UUID16_INIT(BT_UUID_VOCS_STATE_VAL); @@ -72,7 +73,7 @@ static const ble_uuid16_t inc_vocs_uuid_description = BLE_UUID16_INIT(BT_UUID_VO #define INC_AICS_CHR_COUNT (6 + 1) #define INC_AICS_CHR_FLAGS_STATE \ - (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC) + (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC) #define INC_AICS_CHR_FLAGS_GAIN \ (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC) @@ -81,18 +82,18 @@ static const ble_uuid16_t inc_vocs_uuid_description = BLE_UUID16_INIT(BT_UUID_VO (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC) #define INC_AICS_CHR_FLAGS_STATUS \ - (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC) + (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC) #define INC_AICS_CHR_FLAGS_CONTROL \ (BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_ENC) #define INC_AICS_CHR_FLAGS_DESCRIPTION \ - (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_WRITE_NO_RSP | \ + (BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC | \ BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_WRITE_ENC) -static uint8_t inc_aics_svc_count; +static BT_AUDIO_EXT_RAM_BSS_ATTR uint8_t inc_aics_svc_count; -static struct inc_aics_inst { +static BT_AUDIO_EXT_RAM_BSS_ATTR struct inc_aics_inst { struct bt_gatt_service *svc_p; uint16_t state_handle; uint16_t gain_handle; @@ -102,7 +103,7 @@ static struct inc_aics_inst { uint16_t description_handle; } inc_aics_insts[AICS_INST_COUNT]; -static struct ble_gatt_svc_def *gatt_svc_inc_aics; +static BT_AUDIO_EXT_RAM_BSS_ATTR struct ble_gatt_svc_def *gatt_svc_inc_aics; static const ble_uuid16_t inc_aics_uuid_svc = BLE_UUID16_INIT(BT_UUID_AICS_VAL); static const ble_uuid16_t inc_aics_uuid_state = BLE_UUID16_INIT(BT_UUID_AICS_STATE_VAL); @@ -112,7 +113,7 @@ static const ble_uuid16_t inc_aics_uuid_status = BLE_UUID16_INIT(BT_UUID_AICS_IN static const ble_uuid16_t inc_aics_uuid_control = BLE_UUID16_INIT(BT_UUID_AICS_CONTROL_VAL); static const ble_uuid16_t inc_aics_uuid_description = BLE_UUID16_INIT(BT_UUID_AICS_DESCRIPTION_VAL); -static struct ble_gatt_svc_def **vcs_inc_svcs; +static BT_AUDIO_EXT_RAM_BSS_ATTR struct ble_gatt_svc_def **vcs_inc_svcs; static struct ble_gatt_svc_def gatt_svc_vcs[] = { { @@ -128,7 +129,7 @@ static struct ble_gatt_svc_def gatt_svc_vcs[] = { .access_cb = bt_le_nimble_gatts_access_cb_safe, .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, .min_key_size = 16, }, { /* Volume Control Service -- Volume COntrol Point characteristic */ @@ -145,7 +146,7 @@ static struct ble_gatt_svc_def gatt_svc_vcs[] = { .arg = NULL, .descriptors = NULL, /* NULL if no descriptors. Do not include CCCD */ #if CONFIG_BT_VCP_VOL_REND_VOL_FLAGS_NOTIFIABLE - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC, #else /* CONFIG_BT_VCP_VOL_REND_VOL_FLAGS_NOTIFIABLE */ .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC, #endif /* CONFIG_BT_VCP_VOL_REND_VOL_FLAGS_NOTIFIABLE */ @@ -169,7 +170,7 @@ static int inc_vocs_svc_check(void) * the service exist in the service defined by Zephyr. */ - assert(gatt_svc_inc_vocs); + BT_LE_ASSERT(gatt_svc_inc_vocs); LOG_DBG("[N]IncVocsSvcCheck[%u]", inc_vocs_svc_count); @@ -177,7 +178,7 @@ static int inc_vocs_svc_check(void) struct ble_gatt_svc_def *vocs = &gatt_svc_inc_vocs[i]; struct bt_gatt_service *svc = inc_vocs_insts[i].svc_p; - assert(svc); + BT_LE_ASSERT(svc); for (const struct ble_gatt_chr_def *chr = vocs->characteristics; chr && chr->uuid; chr++) { @@ -188,7 +189,7 @@ static int inc_vocs_svc_check(void) for (size_t j = 0; j < svc->attr_count; j++) { uuid = (const struct bt_uuid_16 *)(svc->attrs + j)->uuid; - if (uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && + if (uuid && uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && uuid->val == check->value) { chr_found = true; break; @@ -214,7 +215,7 @@ static int inc_aics_svc_check(void) * the service exist in the service defined by Zephyr. */ - assert(gatt_svc_inc_aics); + BT_LE_ASSERT(gatt_svc_inc_aics); LOG_DBG("[N]IncAicsSvcCheck[%u]", inc_aics_svc_count); @@ -222,7 +223,7 @@ static int inc_aics_svc_check(void) struct ble_gatt_svc_def *aics = &gatt_svc_inc_aics[i]; struct bt_gatt_service *svc = inc_aics_insts[i].svc_p; - assert(svc); + BT_LE_ASSERT(svc); for (const struct ble_gatt_chr_def *chr = aics->characteristics; chr && chr->uuid; chr++) { @@ -233,7 +234,7 @@ static int inc_aics_svc_check(void) for (size_t j = 0; j < svc->attr_count; j++) { uuid = (const struct bt_uuid_16 *)(svc->attrs + j)->uuid; - if (uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && + if (uuid && uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && uuid->val == check->value) { chr_found = true; break; @@ -277,7 +278,7 @@ static int vcs_svc_check(void) for (size_t i = 0; i < vcs_svc->attr_count; i++) { uuid = (const struct bt_uuid_16 *)(vcs_svc->attrs + i)->uuid; - if (uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && + if (uuid && uuid->uuid.type == BT_LE_NIMBLE_GATT_UUID_TO_Z(check->u.type) && uuid->val == check->value) { chr_found = true; break; @@ -302,9 +303,9 @@ static int inc_vocs_attr_handle_set(void) LOG_DBG("[N]IncVocsAttrHdlSet[%u]", inc_vocs_svc_count); for (size_t i = 0; i < inc_vocs_svc_count; i++) { - assert(inc_vocs_insts[i].svc_p); + BT_LE_ASSERT(inc_vocs_insts[i].svc_p); - assert(inc_vocs_insts[i].state_handle >= 2); + BT_LE_ASSERT(inc_vocs_insts[i].state_handle >= 2); start_handle = inc_vocs_insts[i].state_handle - 2; /* server attr handle & char def handle */ end_handle = inc_vocs_insts[i].description_handle + 1; /* cccd for chr Audio Output Description */ @@ -338,9 +339,9 @@ static int inc_aics_attr_handle_set(void) LOG_DBG("[N]IncAicsAttrHdlSet[%u]", inc_aics_svc_count); for (size_t i = 0; i < inc_aics_svc_count; i++) { - assert(inc_aics_insts[i].svc_p); + BT_LE_ASSERT(inc_aics_insts[i].svc_p); - assert(inc_aics_insts[i].state_handle >= 2); + BT_LE_ASSERT(inc_aics_insts[i].state_handle >= 2); start_handle = inc_aics_insts[i].state_handle - 2; /* server attr handle & char def handle */ end_handle = inc_aics_insts[i].description_handle + 1; /* cccd for chr Audio Input Description */ @@ -448,8 +449,8 @@ static void inc_vocs_svc_init(struct inc_vocs_inst *inst, svc->uuid = &inc_vocs_uuid_svc.u; svc->includes = NULL; - svc->characteristics = calloc(INC_VOCS_CHR_COUNT, sizeof(struct ble_gatt_chr_def)); - assert(svc->characteristics); + svc->characteristics = bt_le_ext_calloc(INC_VOCS_CHR_COUNT, sizeof(struct ble_gatt_chr_def)); + BT_LE_ASSERT(svc->characteristics); /* Characteristic - Volume Offset State */ inc_vocs_chr_init((void *)&svc->characteristics[0], @@ -501,8 +502,8 @@ static void inc_aics_svc_init(struct inc_aics_inst *inst, svc->uuid = &inc_aics_uuid_svc.u; svc->includes = NULL; - svc->characteristics = calloc(INC_AICS_CHR_COUNT, sizeof(struct ble_gatt_chr_def)); - assert(svc->characteristics); + svc->characteristics = bt_le_ext_calloc(INC_AICS_CHR_COUNT, sizeof(struct ble_gatt_chr_def)); + BT_LE_ASSERT(svc->characteristics); /* Characteristic - Audio Input State */ inc_aics_chr_init((void *)&svc->characteristics[0], @@ -541,9 +542,17 @@ static void inc_aics_svc_init(struct inc_aics_inst *inst, INC_AICS_CHR_FLAGS_DESCRIPTION); } +void bt_le_nimble_vcs_state_reset(void) +{ + inc_vocs_svc_count = 0; + inc_aics_svc_count = 0; +} + int bt_le_nimble_vcs_init(void *vcp_inc) { struct bt_vcp_included *vcp_included; + bool inc_vocs_added = false; + bool inc_aics_added = false; uint8_t inc_count; int rc; @@ -569,14 +578,14 @@ int bt_le_nimble_vcs_init(void *vcp_inc) /* Extra one for terminating the included service array with NULL */ inc_count = inc_vocs_svc_count + inc_aics_svc_count + 1; - vcs_inc_svcs = calloc(inc_count, sizeof(struct ble_gatt_svc_def *)); - assert(vcs_inc_svcs); + vcs_inc_svcs = bt_le_ext_calloc(inc_count, sizeof(struct ble_gatt_svc_def *)); + BT_LE_ASSERT(vcs_inc_svcs); /* VCS may include zero or more instances of VOCS */ if (inc_vocs_svc_count) { /* Extra one for terminating the VOCS service array */ - gatt_svc_inc_vocs = calloc(inc_vocs_svc_count + 1, sizeof(struct ble_gatt_svc_def)); - assert(gatt_svc_inc_vocs); + gatt_svc_inc_vocs = bt_le_ext_calloc(inc_vocs_svc_count + 1, sizeof(struct ble_gatt_svc_def)); + BT_LE_ASSERT(gatt_svc_inc_vocs); for (size_t i = 0; i < inc_vocs_svc_count; i++) { inc_vocs_svc_init(&inc_vocs_insts[i], &gatt_svc_inc_vocs[i]); @@ -602,6 +611,7 @@ int bt_le_nimble_vcs_init(void *vcp_inc) LOG_ERR("[N]IncVocsAddSvcsFail[%d]", rc); goto free; } + inc_vocs_added = true; rc = inc_vocs_svc_check(); if (rc) { @@ -612,8 +622,8 @@ int bt_le_nimble_vcs_init(void *vcp_inc) /* VCS may include zero or more instances of AICS */ if (inc_aics_svc_count) { /* Extra one for terminating the AICS service array */ - gatt_svc_inc_aics = calloc(inc_aics_svc_count + 1, sizeof(struct ble_gatt_svc_def)); - assert(gatt_svc_inc_aics); + gatt_svc_inc_aics = bt_le_ext_calloc(inc_aics_svc_count + 1, sizeof(struct ble_gatt_svc_def)); + BT_LE_ASSERT(gatt_svc_inc_aics); for (size_t i = 0; i < inc_aics_svc_count; i++) { inc_aics_svc_init(&inc_aics_insts[i], &gatt_svc_inc_aics[i]); @@ -639,6 +649,7 @@ int bt_le_nimble_vcs_init(void *vcp_inc) LOG_ERR("[N]IncAicsAddSvcsFail[%d]", rc); goto free; } + inc_aics_added = true; rc = inc_aics_svc_check(); if (rc) { @@ -672,15 +683,20 @@ int bt_le_nimble_vcs_init(void *vcp_inc) return 0; free: + /* Once ble_gatts_add_svcs() succeeds NimBLE keeps the svc_def pointer and + * offers no per-service unregister, so an added service must be leaked + * rather than freed into a dangling entry of its global list. */ if (vcp_included) { if (inc_vocs_svc_count) { - for (size_t i = 0; i < inc_vocs_svc_count; i++) { - free((void *)gatt_svc_inc_vocs[i].characteristics); - gatt_svc_inc_vocs[i].characteristics = NULL; - } + if (!inc_vocs_added) { + for (size_t i = 0; i < inc_vocs_svc_count; i++) { + free((void *)gatt_svc_inc_vocs[i].characteristics); + gatt_svc_inc_vocs[i].characteristics = NULL; + } - free(gatt_svc_inc_vocs); - gatt_svc_inc_vocs = NULL; + free(gatt_svc_inc_vocs); + gatt_svc_inc_vocs = NULL; + } inc_vocs_svc_count = 0; } @@ -688,7 +704,7 @@ free: if (inc_aics_svc_count) { /* A VOCS-phase failure reaches here with the count already set * but gatt_svc_inc_aics not yet allocated (still NULL). */ - if (gatt_svc_inc_aics) { + if (!inc_aics_added && gatt_svc_inc_aics) { for (size_t i = 0; i < inc_aics_svc_count; i++) { free((void *)gatt_svc_inc_aics[i].characteristics); gatt_svc_inc_aics[i].characteristics = NULL; diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/server.c b/components/bt/esp_ble_audio/host/adapter/nimble/server.c index 28b0a248749..aa5d848272a 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/server.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/server.c @@ -7,7 +7,6 @@ #include #include #include -#include #include #include @@ -27,6 +26,24 @@ LOG_MODULE_REGISTER(LEA_GSRV, CONFIG_BT_ISO_LOG_LEVEL); +/* NimBLE access callbacks must return 0 or a positive ATT error (stored as + * uint8_t). Zephyr GATT handlers return BT_GATT_ERR(att) = -att. */ +static int to_nimble_att_err(int gatt_err) +{ + int att; + + if (gatt_err >= 0) { + return 0; + } + + att = -gatt_err; + if (att > 0 && att <= UINT8_MAX) { + return att; + } + + return BLE_ATT_ERR_UNLIKELY; +} + static ssize_t gatts_read_cb(void *arg, uint16_t offset, const void *data, uint16_t len) { struct os_mbuf *om; @@ -35,20 +52,21 @@ static ssize_t gatts_read_cb(void *arg, uint16_t offset, const void *data, uint1 ARG_UNUSED(offset); om = (struct os_mbuf *)arg; - assert(om); + BT_LE_ASSERT(om); LOG_DBG("[N]GattsRdCb[%u][%u]", offset, len); if (data == NULL || len == 0) { - rc = 0; - } else { - rc = os_mbuf_append(om, data, len); - if (rc) { - LOG_ERR("[N]MbufAppendFail[%d]", rc); - } + return 0; } - return (rc == 0 ? len : 0); + rc = os_mbuf_append(om, data, len); + if (rc) { + LOG_ERR("[N]MbufAppendFail[%d]", rc); + return BT_GATT_ERR(BT_ATT_ERR_INSUFFICIENT_RESOURCES); + } + + return len; } static int gatts_access_cb(uint16_t conn_handle, uint16_t attr_handle, @@ -57,17 +75,17 @@ static int gatts_access_cb(uint16_t conn_handle, uint16_t attr_handle, struct bt_le_nimble_gatt_read_cb cb; const struct bt_gatt_attr *attr; struct bt_conn *conn; - uint8_t *data; + uint8_t *data = NULL; ssize_t rc; - assert(ctx); + BT_LE_ASSERT(ctx); LOG_DBG("[N]GattsAccessCb[%u][%u][%02x]", conn_handle, attr_handle, ctx->op); conn = bt_le_acl_conn_find(conn_handle); if (conn == NULL || conn->state != BT_CONN_CONNECTED) { LOG_ERR("[N]NotConn[%d]", __LINE__); - return -ENOTCONN; + return BLE_ATT_ERR_UNLIKELY; } switch (ctx->op) { @@ -75,12 +93,12 @@ static int gatts_access_cb(uint16_t conn_handle, uint16_t attr_handle, attr = bt_gatts_find_attr_by_handle(attr_handle); if (attr == NULL) { LOG_WRN("[N]RdAttrNotFound[%u]", attr_handle); - return BT_GATT_ERR(BT_ATT_ERR_INVALID_HANDLE); + return BLE_ATT_ERR_INVALID_HANDLE; } if (attr->read == NULL) { LOG_WRN("[N]RdNotPermit"); - return BT_GATT_ERR(BT_ATT_ERR_READ_NOT_PERMITTED); + return BLE_ATT_ERR_READ_NOT_PERMITTED; } cb.read_cb = gatts_read_cb; @@ -88,8 +106,8 @@ static int gatts_access_cb(uint16_t conn_handle, uint16_t attr_handle, rc = attr->read(conn, attr, (void *)&cb, UINT16_MAX, 0); if (rc < 0) { - LOG_DBG("[N]RdGattErr[%u][%d]", attr_handle, rc); - return BT_GATT_ERR(rc); + LOG_WRN("[N]RdGattErr[%u][%d]", attr_handle, rc); + return to_nimble_att_err(rc); } return 0; @@ -98,12 +116,12 @@ static int gatts_access_cb(uint16_t conn_handle, uint16_t attr_handle, attr = bt_gatts_find_attr_by_handle(attr_handle); if (attr == NULL) { LOG_WRN("[N]WrAttrNotFound[%u]", attr_handle); - return BT_GATT_ERR(BT_ATT_ERR_INVALID_HANDLE); + return BLE_ATT_ERR_INVALID_HANDLE; } if (attr->write == NULL) { LOG_WRN("[N]WrNotPermit"); - return BT_GATT_ERR(BT_ATT_ERR_WRITE_NOT_PERMITTED); + return BLE_ATT_ERR_WRITE_NOT_PERMITTED; } if (BT_UUID_16(attr->uuid)->val == BT_UUID_BASS_CONTROL_POINT_VAL) { @@ -120,14 +138,14 @@ static int gatts_access_cb(uint16_t conn_handle, uint16_t attr_handle, if (OS_MBUF_PKTLEN(ctx->om) > alloc_len) { LOG_WRN("[N]WrBassCtrlPtTooLong[%u > %u]", OS_MBUF_PKTLEN(ctx->om), alloc_len); - return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; } - data = calloc(1, alloc_len); - assert(data); + data = bt_le_ext_calloc(1, alloc_len); + BT_LE_ASSERT(data); rc = os_mbuf_copydata(ctx->om, 0, OS_MBUF_PKTLEN(ctx->om), data); - assert(rc == 0); + BT_LE_ASSERT(rc == 0); rc = attr->write(conn, attr, data, OS_MBUF_PKTLEN(ctx->om), 0, 0); @@ -136,20 +154,26 @@ static int gatts_access_cb(uint16_t conn_handle, uint16_t attr_handle, } else { LOG_DBG("[N]Wr[%u]", OS_MBUF_PKTLEN(ctx->om)); - data = calloc(1, OS_MBUF_PKTLEN(ctx->om)); - assert(data); + /* A zero-length write is a legal PDU, but calloc(0) returns NULL + * on IDF and would trip the assert; pass it on unallocated. */ + if (OS_MBUF_PKTLEN(ctx->om) > 0) { + data = bt_le_ext_calloc(1, OS_MBUF_PKTLEN(ctx->om)); + BT_LE_ASSERT(data); - rc = os_mbuf_copydata(ctx->om, 0, OS_MBUF_PKTLEN(ctx->om), data); - assert(rc == 0); + rc = os_mbuf_copydata(ctx->om, 0, OS_MBUF_PKTLEN(ctx->om), data); + BT_LE_ASSERT(rc == 0); + } rc = attr->write(conn, attr, data, OS_MBUF_PKTLEN(ctx->om), 0, 0); - free(data); - data = NULL; + if (data != NULL) { + free(data); + data = NULL; + } } if (rc < 0) { - LOG_DBG("[N]WrGattErr[%u][%d]", attr_handle, rc); - return BT_GATT_ERR(rc); + LOG_WRN("[N]WrGattErr[%u][%d]", attr_handle, rc); + return to_nimble_att_err(rc); } return 0; diff --git a/components/bt/esp_ble_audio/host/common/include/common/audio_attr.h b/components/bt/esp_ble_audio/host/common/include/common/audio_attr.h new file mode 100644 index 00000000000..84ef93aac8f --- /dev/null +++ b/components/bt/esp_ble_audio/host/common/include/common/audio_attr.h @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "esp_attr.h" + +/* Static (.bss) placement markers for esp_ble_audio source files. Host-neutral: + * used by the bluedroid adapter profiles AND the host-agnostic OTS service, so + * it lives in the audio component's common include (not under bluedroid/). + * + * Distinct from esp_ble_iso's utils/iso_attr.h in both name and include prefix, so + * neither collides on the bt component's shared flat include path. + * + * - BT_AUDIO_EXT_RAM_BSS_ATTR: eligible for external SPIRAM (cold, task-context, + * non-DMA). Moves to PSRAM when BT_AUDIO_BSS_SEG_EXTERNAL_MEMORY is set, else + * expands to nothing. + * - BT_AUDIO_CTRL_BSS_ATTR: must stay in internal DRAM (marker only). + * + * net_buf pools (NET_BUF_POOL_FIXED_DEFINE / NET_BUF_SIMPLE_DEFINE_STATIC) hide + * their .bss backing behind auto-generated names and cannot take a per-variable + * attribute; those objects (e.g. ots_l2cap) are moved via the linker fragment in + * linker_common.lf instead. + */ +#if CONFIG_BT_AUDIO_BSS_SEG_EXTERNAL_MEMORY +#define BT_AUDIO_EXT_RAM_BSS_ATTR EXT_RAM_BSS_ATTR +#else +#define BT_AUDIO_EXT_RAM_BSS_ATTR +#endif + +#define BT_AUDIO_CTRL_BSS_ATTR diff --git a/components/bt/esp_ble_audio/host/common/include/common/init.h b/components/bt/esp_ble_audio/host/common/include/common/init.h index 480e0bb9e1c..4b572bad5e3 100644 --- a/components/bt/esp_ble_audio/host/common/include/common/init.h +++ b/components/bt/esp_ble_audio/host/common/include/common/init.h @@ -45,16 +45,30 @@ struct bt_le_audio_start_info { int bt_le_audio_init(void); +void bt_le_audio_deinit(void); + int bt_le_ascs_init(void); +int bt_le_ascs_deinit(void); + +int bt_le_pacs_deinit(void); + +int bt_le_csis_deinit(const void *svc_inst); + int bt_le_bass_init(void); +int bt_le_bass_deinit(void); + int bt_le_tmas_init(void); int bt_le_gtbs_init(void); +int bt_le_gtbs_deinit(void); + int bt_le_tbs_init(void); +int bt_le_tbs_deinit(uint8_t bearer_index); + int bt_le_has_init(void); int bt_le_media_proxy_pl_init(void); diff --git a/components/bt/esp_ble_audio/host/common/init.c b/components/bt/esp_ble_audio/host/common/init.c index 08505740f3c..15339f45d8a 100644 --- a/components/bt/esp_ble_audio/host/common/init.c +++ b/components/bt/esp_ble_audio/host/common/init.c @@ -44,12 +44,19 @@ #include <../host/conn_internal.h> #include <../host/hci_core.h> +#include "utils/assert.h" +#include "utils/mem.h" + #if CONFIG_BT_BLUEDROID_ENABLED #include "bluedroid/init.h" #else #include "nimble/init.h" #endif +#if CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT +#include "ots/adapter/l2cap.h" +#endif + #include "../../../lib/include/audio.h" #include "esp_ble_audio_common_api.h" @@ -178,7 +185,7 @@ static const uint16_t ext_structs[] = { sizeof(struct bt_bond_info), }; -#define LEA_VERSION (0x20260722) +#define LEA_VERSION (0x20260828) struct lib_ext_cfgs { /* BLE */ @@ -1078,16 +1085,20 @@ struct lib_ext_funcs { /* Scan */ int (*_scan_cb_register)(struct bt_le_scan_cb *cb); + void (*_scan_cb_unregister)(struct bt_le_scan_cb *cb); int (*_scan_start)(const struct bt_le_scan_param *param, void *cb); int (*_scan_stop)(void); int (*_pa_sync_cb_register)(struct bt_le_per_adv_sync_cb *cb); + int (*_pa_sync_cb_unregister)(struct bt_le_per_adv_sync_cb *cb); int (*_pa_sync_get_info)(struct bt_le_per_adv_sync *per_adv_sync, struct bt_le_per_adv_sync_info *info); struct bt_le_per_adv_sync *(*_pa_sync_lookup_addr)(const bt_addr_le_t *adv_addr, uint8_t sid); /* Connection */ int (*_conn_cb_register)(struct bt_conn_cb *cb); + int (*_conn_cb_unregister)(struct bt_conn_cb *cb); int (*_conn_auth_info_cb_register)(struct bt_conn_auth_info_cb *cb); + int (*_conn_auth_info_cb_unregister)(struct bt_conn_auth_info_cb *cb); void (*_conn_foreach)(enum bt_conn_type type, void (*func)(struct bt_conn *conn, void *data), void *data); @@ -1203,32 +1214,21 @@ static void log_error(const char *format, ...) #endif /* (CONFIG_BT_AUDIO_LOG_LEVEL >= BT_ISO_LOG_ERROR) */ } -/* Fatal assert handler registered into lib_ext_funcs._assert. - * Always logged (no LOG_LEVEL gate) — this is the last message before - * abort, and the user needs the context to diagnose. - */ -static void assert_fatal(const char *tag, size_t info, - const char *file, int line, const char *func) -{ - esp_log_write(ESP_LOG_ERROR, LEA_TAG, - BT_ISO_LOG_COLOR_E - "E (%lu) %s: LibAssert[%s][info=%u][%s:%d][%s]" - BT_ISO_LOG_RESET_COLOR "\n", - esp_log_timestamp(), LEA_TAG, - tag, (unsigned)info, file, line, func); - abort(); -} - static const struct lib_ext_funcs ext_funcs = { ._log_dbg = (void *)log_debug, ._log_inf = (void *)log_info, ._log_wrn = (void *)log_warn, ._log_err = (void *)log_error, - ._assert = (void *)assert_fatal, + ._assert = (void *)bt_le_assert, +#if CONFIG_BT_AUDIO_HEAP_EXTERNAL_MEMORY + ._malloc = (void *)bt_le_ext_malloc, + ._calloc = (void *)bt_le_ext_calloc, +#else ._malloc = (void *)malloc, ._calloc = (void *)calloc, +#endif ._free = (void *)free, ._rand = (void *)bt_rand, @@ -1299,21 +1299,23 @@ static const struct lib_ext_funcs ext_funcs = { ._bt_foreach_bond = (void *)bt_foreach_bond, ._scan_cb_register = (void *)bt_le_scan_cb_register, + ._scan_cb_unregister = (void *)bt_le_scan_cb_unregister, ._scan_start = (void *)bt_le_scan_start, ._scan_stop = (void *)bt_le_scan_stop, ._pa_sync_cb_register = (void *)bt_le_per_adv_sync_cb_register, + ._pa_sync_cb_unregister = (void *)bt_le_per_adv_sync_cb_unregister, ._pa_sync_get_info = (void *)bt_le_per_adv_sync_get_info, ._pa_sync_lookup_addr = (void *)bt_le_per_adv_sync_lookup_addr, ._conn_cb_register = (void *)bt_conn_cb_register, + ._conn_cb_unregister = (void *)bt_conn_cb_unregister, ._conn_auth_info_cb_register = (void *)bt_conn_auth_info_cb_register, + ._conn_auth_info_cb_unregister = (void *)bt_conn_auth_info_cb_unregister, ._conn_foreach = (void *)bt_conn_foreach, ._conn_get_info = (void *)bt_conn_get_info, ._conn_index = (void *)bt_conn_index, ._conn_lookup_index = (void *)bt_conn_lookup_index, ._conn_get_dst = (void *)bt_conn_get_dst, - ._conn_ref = (void *)bt_conn_ref, - ._conn_unref = (void *)bt_conn_unref, ._gatt_svc_register = (void *)bt_gatt_service_register, ._gatt_svc_unregister = (void *)bt_gatt_service_unregister, @@ -2081,6 +2083,260 @@ static int lib_audio_resources_init(void) return err; } +/* Reverse order of lib_audio_resources_init(): a module may reference an earlier + * one. Failures log and continue - stopping half way leaves more behind. Keep + * every lib_*_init() listed here; a missing counterpart leaks silently. */ +static void lib_audio_resources_deinit(void) +{ +#if CONFIG_BT_GMAP + /* No init counterpart above: bt_gmap_register() allocates GMAS on demand, + * so only the deinit side is listed. */ + if (lib_gmap_server_deinit()) { + LOG_ERR("LibGmapSrvDeinitFail"); + } + + if (lib_gmap_client_deinit()) { + LOG_ERR("LibGmapCliDeinitFail"); + } +#endif /* CONFIG_BT_GMAP */ + +#if CONFIG_BT_HAS_CLIENT + if (lib_has_client_deinit()) { + LOG_ERR("LibHasCliDeinitFail"); + } +#endif /* CONFIG_BT_HAS_CLIENT */ + +#if CONFIG_BT_HAS + if (lib_has_deinit()) { + LOG_ERR("LibHasDeinitFail"); + } +#endif /* CONFIG_BT_HAS */ + +#if CONFIG_BT_VOCS_CLIENT + if (lib_vocs_client_deinit()) { + LOG_ERR("LibVocsClientDeinitFail"); + } +#endif /* CONFIG_BT_VOCS_CLIENT */ + +#if CONFIG_BT_VOCS || CONFIG_BT_VOCS_CLIENT + if (lib_vocs_deinit()) { + LOG_ERR("LibVocsDeinitFail"); + } +#endif /* CONFIG_BT_VOCS || CONFIG_BT_VOCS_CLIENT */ + +#if CONFIG_BT_VCP_VOL_REND + if (lib_vcp_vol_rend_deinit()) { + LOG_ERR("LibVcpVolRendDeinitFail"); + } +#endif /* CONFIG_BT_VCP_VOL_REND */ + +#if CONFIG_BT_VCP_VOL_CTLR + if (lib_vcp_vol_ctlr_deinit()) { + LOG_ERR("LibVcpVolCtlrDeinitFail"); + } +#endif /* CONFIG_BT_VCP_VOL_CTLR */ + +#if CONFIG_BT_TMAP + if (lib_tmap_deinit()) { + LOG_ERR("LibTmapDeinitFail"); + } +#endif /* CONFIG_BT_TMAP */ + +#if CONFIG_BT_TBS + if (lib_tbs_deinit()) { + LOG_ERR("LibTbsDeinitFail"); + } +#endif /* CONFIG_BT_TBS */ + +#if CONFIG_BT_TBS_CLIENT + if (lib_tbs_client_deinit()) { + LOG_ERR("LibTbsCliDeinitFail"); + } +#endif /* CONFIG_BT_TBS_CLIENT */ + +#if CONFIG_BT_PBP + if (lib_pbp_deinit()) { + LOG_ERR("LibPbpDeinitFail"); + } +#endif /* CONFIG_BT_PBP */ + +#if CONFIG_BT_MPL + if (lib_mpl_deinit()) { + LOG_ERR("LibMplDeinitFail"); + } +#endif /* CONFIG_BT_MPL */ + +#if CONFIG_BT_MICP_MIC_DEV + if (lib_micp_mic_dev_deinit()) { + LOG_ERR("LibMicpMicDevDeinitFail"); + } +#endif /* CONFIG_BT_MICP_MIC_DEV */ + +#if CONFIG_BT_MICP_MIC_CTLR + if (lib_micp_mic_ctlr_deinit()) { + LOG_ERR("LibMicpMicCtlrDeinitFail"); + } +#endif /* CONFIG_BT_MICP_MIC_CTLR */ + +#if CONFIG_BT_MCTL + if (lib_media_proxy_deinit()) { + LOG_ERR("LibMediaProxyDeinitFail"); + } +#endif /* CONFIG_BT_MCTL */ + +#if CONFIG_BT_MCS + if (lib_mcs_deinit()) { + LOG_ERR("LibMcsDeinitFail"); + } +#endif /* CONFIG_BT_MCS */ + +#if CONFIG_BT_MCC + if (lib_mcc_deinit()) { + LOG_ERR("LibMccDeinitFail"); + } +#endif /* CONFIG_BT_MCC */ + +#if CONFIG_BT_CSIP_SET_COORDINATOR + if (lib_csip_set_coordinator_deinit()) { + LOG_ERR("LibCsipSetCoordinatorDeinitFail"); + } +#endif /* CONFIG_BT_CSIP_SET_COORDINATOR */ + +#if CONFIG_BT_CSIP_SET_MEMBER + if (lib_csip_set_member_deinit()) { + LOG_ERR("LibCsipSetMemberDeinitFail"); + } +#endif /* CONFIG_BT_CSIP_SET_MEMBER */ + +#if CONFIG_BT_BAP_STREAM + if (lib_codec_deinit()) { + LOG_ERR("LibCodecDeinitFail"); + } +#endif /* CONFIG_BT_BAP_STREAM */ + +#if CONFIG_BT_CCP_CALL_CONTROL_SERVER + if (lib_ccp_call_control_server_deinit()) { + LOG_ERR("LibCcpCallControlServerDeinitFail"); + } +#endif /* CONFIG_BT_CCP_CALL_CONTROL_SERVER */ + +#if CONFIG_BT_CCP_CALL_CONTROL_CLIENT + if (lib_ccp_call_control_client_deinit()) { + LOG_ERR("LibCcpCallControlClientDeinitFail"); + } +#endif /* CONFIG_BT_CCP_CALL_CONTROL_CLIENT */ + + if (lib_ccid_deinit()) { + LOG_ERR("LibCcidDeinitFail"); + } + +#if CONFIG_BT_CAP_COMMANDER + if (lib_cap_commander_deinit()) { + LOG_ERR("LibCapComDeinitFail"); + } +#endif /* CONFIG_BT_CAP_COMMANDER */ + +#if CONFIG_BT_CAP_ACCEPTOR + if (lib_cap_acceptor_deinit()) { + LOG_ERR("LibCapAccDeinitFail"); + } +#endif /* CONFIG_BT_CAP_ACCEPTOR */ + +#if CONFIG_BT_CAP_INITIATOR + if (lib_cap_initiator_deinit()) { + LOG_ERR("LibCapIniDeinitFail"); + } +#endif /* CONFIG_BT_CAP_INITIATOR */ + +#if CONFIG_BT_CAP + if (lib_cap_stream_deinit()) { + LOG_ERR("LibCapStreamDeinitFail"); + } +#endif /* CONFIG_BT_CAP */ + +#if CONFIG_BT_CAP_INITIATOR_UNICAST || CONFIG_BT_CAP_COMMANDER + if (lib_cap_common_deinit()) { + LOG_ERR("LibCapCommonDeinitFail"); + } +#endif /* CONFIG_BT_CAP_INITIATOR_UNICAST || CONFIG_BT_CAP_COMMANDER */ + +#if CONFIG_BT_BAP_UNICAST_SERVER + if (lib_bap_unicast_server_deinit()) { + LOG_ERR("LibBapUniSrvDeinitFail"); + } +#endif /* CONFIG_BT_BAP_UNICAST_SERVER */ + +#if CONFIG_BT_BAP_UNICAST_CLIENT + if (lib_bap_unicast_client_deinit()) { + LOG_ERR("LibBapUniCliDeinitFail"); + } +#endif /* CONFIG_BT_BAP_UNICAST_CLIENT */ + +#if CONFIG_BT_BAP_BROADCAST_ASSISTANT + if (lib_bap_broadcast_assistant_deinit()) { + LOG_ERR("LibBapBaDeinitFail"); + } +#endif /* CONFIG_BT_BAP_BROADCAST_ASSISTANT */ + +#if CONFIG_BT_BAP_SCAN_DELEGATOR + if (lib_bap_scan_delegator_deinit()) { + LOG_ERR("LibBapSdeDeinitFail"); + } +#endif /* CONFIG_BT_BAP_SCAN_DELEGATOR */ + +#if CONFIG_BT_BAP_BROADCAST_SOURCE + if (lib_bap_broadcast_source_deinit()) { + LOG_ERR("LibBapBsrcDeinitFail"); + } +#endif /* CONFIG_BT_BAP_BROADCAST_SOURCE */ + +#if CONFIG_BT_BAP_BROADCAST_SINK + if (lib_bap_broadcast_sink_deinit()) { + LOG_ERR("LibBapBsnkDeinitFail"); + } +#endif /* CONFIG_BT_BAP_BROADCAST_SINK */ + +#if CONFIG_BT_PACS + if (lib_pacs_deinit()) { + LOG_ERR("LibPacsDeinitFail"); + } +#endif /* CONFIG_BT_PACS */ + +#if CONFIG_BT_ASCS + if (lib_ascs_deinit()) { + LOG_ERR("LibAscsDeinitFail"); + } +#endif /* CONFIG_BT_ASCS */ + +#if CONFIG_BT_BAP_STREAM + if (lib_bap_stream_deinit()) { + LOG_ERR("LibBapStreamDeinitFail"); + } + + if (lib_bap_iso_deinit()) { + LOG_ERR("LibBapIsoDeinitFail"); + } +#endif /* CONFIG_BT_BAP_STREAM */ + +#if CONFIG_BT_BAP_BASE + if (lib_bap_base_deinit()) { + LOG_ERR("LibBapBaseDeinitFail"); + } +#endif /* CONFIG_BT_BAP_BASE */ + +#if CONFIG_BT_AICS_CLIENT + if (lib_aics_client_deinit()) { + LOG_ERR("LibAicsClientDeinitFail"); + } +#endif /* CONFIG_BT_AICS_CLIENT */ + +#if CONFIG_BT_AICS || CONFIG_BT_AICS_CLIENT + if (lib_aics_deinit()) { + LOG_ERR("LibAicsDeinitFail"); + } +#endif /* CONFIG_BT_AICS || CONFIG_BT_AICS_CLIENT */ +} + int bt_le_audio_init(void) { int err; @@ -2118,14 +2374,49 @@ int bt_le_audio_init(void) err = lib_audio_resources_init(); if (err) { - return err; + goto deinit_lib; } -#if CONFIG_BT_BLUEDROID_ENABLED - return bt_le_bluedroid_audio_init(); -#else - return bt_le_nimble_audio_init(); +#if CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT + err = bt_le_l2cap_ots_init(); + if (err) { + goto deinit_lib; + } #endif + +#if CONFIG_BT_BLUEDROID_ENABLED + err = bt_le_bluedroid_audio_init(); +#else + err = bt_le_nimble_audio_init(); +#endif + if (err) { + goto deinit_ots; + } + + return 0; + +deinit_ots: +#if CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT + bt_le_l2cap_ots_deinit(); +#endif +deinit_lib: + lib_audio_resources_deinit(); + return err; +} + +void bt_le_audio_deinit(void) +{ +#if CONFIG_BT_BLUEDROID_ENABLED + bt_le_bluedroid_audio_deinit(); +#else + bt_le_nimble_audio_deinit(); +#endif + +#if CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT + bt_le_l2cap_ots_deinit(); +#endif + + lib_audio_resources_deinit(); } #if BLE_AUDIO_SVC_DEFERRED_ADD @@ -2140,6 +2431,17 @@ int bt_le_ascs_init(void) return bt_le_nimble_ascs_init(); #endif } + +int bt_le_ascs_deinit(void) +{ + LOG_DBG("AscsDeinit"); + +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_ascs_deinit(); +#else + return bt_le_nimble_ascs_deinit(); +#endif +} #endif /* CONFIG_BT_ASCS */ #if CONFIG_BT_BAP_SCAN_DELEGATOR @@ -2153,6 +2455,17 @@ int bt_le_bass_init(void) return bt_le_nimble_bass_init(); #endif } + +int bt_le_bass_deinit(void) +{ + LOG_DBG("BassDeinit"); + +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_bass_deinit(); +#else + return bt_le_nimble_bass_deinit(); +#endif +} #endif /* CONFIG_BT_BAP_SCAN_DELEGATOR */ #if CONFIG_BT_TMAP @@ -2180,6 +2493,17 @@ int bt_le_gtbs_init(void) #endif } +int bt_le_gtbs_deinit(void) +{ + LOG_DBG("GtbsDeinit"); + +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_gtbs_deinit(); +#else + return bt_le_nimble_gtbs_deinit(); +#endif +} + int bt_le_tbs_init(void) { LOG_DBG("TbsInit"); @@ -2190,6 +2514,17 @@ int bt_le_tbs_init(void) return bt_le_nimble_tbs_init(); #endif } + +int bt_le_tbs_deinit(uint8_t bearer_index) +{ + LOG_DBG("TbsDeinit[%u]", bearer_index); + +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_tbs_deinit(bearer_index); +#else + return bt_le_nimble_tbs_deinit(bearer_index); +#endif +} #endif /* CONFIG_BT_TBS */ #if CONFIG_BT_HAS @@ -2245,6 +2580,40 @@ int bt_le_micp_mic_dev_init(void) #endif /* CONFIG_BT_MICP_MIC_DEV */ #endif /* BLE_AUDIO_SVC_DEFERRED_ADD */ +#if CONFIG_BT_PACS +int bt_le_pacs_deinit(void) +{ + LOG_DBG("PacsDeinit"); + +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_pacs_deinit(); +#else + return bt_le_nimble_pacs_deinit(); +#endif +} +#endif /* CONFIG_BT_PACS */ + +#if CONFIG_BT_CSIP_SET_MEMBER +int bt_le_csis_deinit(const void *svc_inst) +{ + struct bt_gatt_service *csis_svc; + + LOG_DBG("CsisDeinit"); + + csis_svc = lib_csip_set_member_svc_get(svc_inst); + if (!csis_svc) { + LOG_ERR("CsisSvcGetFail"); + return -ENODEV; + } + +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_csis_deinit(csis_svc); +#else + return bt_le_nimble_csis_deinit(csis_svc); +#endif +} +#endif /* CONFIG_BT_CSIP_SET_MEMBER */ + int bt_le_audio_start(void *info) { LOG_DBG("AudioStart"); diff --git a/components/bt/esp_ble_audio/host/services/ots/Kconfig.ots.in b/components/bt/esp_ble_audio/host/services/ots/Kconfig.ots.in index 0bcb78b191b..4cef13da510 100644 --- a/components/bt/esp_ble_audio/host/services/ots/Kconfig.ots.in +++ b/components/bt/esp_ble_audio/host/services/ots/Kconfig.ots.in @@ -7,6 +7,8 @@ config BT_OTS bool "Object Transfer Service (OTS) [EXPERIMENTAL]" select BT_OTS_SECONDARY_SVC + # BT_GATTS_ENABLE over BT_BLUEDROID_ENABLED: it implies the full dependency chain. + select BT_BLE_L2CAP_COC_ENABLED if BT_GATTS_ENABLE help Enable Object Transfer Service. @@ -69,6 +71,8 @@ endif # BT_OTS config BT_OTS_CLIENT bool "Object Transfer Service Client [Experimental]" + # BT_GATTC_ENABLE over BT_BLUEDROID_ENABLED: it implies the full dependency chain. + select BT_BLE_L2CAP_COC_ENABLED if BT_GATTC_ENABLE help This option enables support for the Object Transfer Service Client. diff --git a/components/bt/esp_ble_audio/host/services/ots/adapter/bluedroid/l2cap.c b/components/bt/esp_ble_audio/host/services/ots/adapter/bluedroid/l2cap.c new file mode 100644 index 00000000000..a9804e7fbc5 --- /dev/null +++ b/components/bt/esp_ble_audio/host/services/ots/adapter/bluedroid/l2cap.c @@ -0,0 +1,802 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include <../host/conn_internal.h> + +#include "osi/allocator.h" +#include "osi/thread.h" +#include "stack/l2c_api.h" +#include "stack/btm_api.h" +#include "stack/btu.h" +#include "l2c_int.h" + +#include "common/host.h" +#include "common/audio_attr.h" + +#include "ots/adapter/l2cap.h" + +LOG_MODULE_REGISTER(ISO_B2CAP, CONFIG_BT_ISO_LOG_LEVEL); + +/* BT_BLE_L2CAP_COC_MAX_CHAN sizes ble_rcb_pool[] (registered LE PSMs, not + * channels); EATT holds one slot for PSM 0x0027. */ +#if CONFIG_BT_BLE_EATT_ENABLE +_Static_assert(CONFIG_BT_BLE_L2CAP_COC_MAX_CHAN >= 2, + "OTS and EATT each need an LE CoC PSM slot"); +#endif /* CONFIG_BT_BLE_EATT_ENABLE */ + +/* L2CA callbacks identify a channel by lcid only; this table maps lcid back to + * (conn_handle, psm). One slot per connection (OTS is single-PSM). */ +struct ots_chan_slot { + uint16_t conn_handle; + uint16_t lcid; + uint16_t psm; + BD_ADDR peer_addr; + bool used; + bool pending; /* connect request sent, awaiting cfm */ + bool tx_pending; /* SDU in flight, awaiting decongestion */ +}; + +static BT_AUDIO_EXT_RAM_BSS_ATTR struct ots_chan_slot ots_chans[L2CAP_OTS_MAX_CHAN]; + +static BT_AUDIO_EXT_RAM_BSS_ATTR uint16_t ots_reg_psm; + +/* ---- BTU executor types & state ------------------------------------------- + * All L2CA_* must run on BTU (single-threaded CCB). ISO entry points marshal + * via l2cap_btu_post (fire-and-forget) or l2cap_btu_invoke (sync, INIT/DEINIT + * only). ots_chans is BTU-only, so no host_lock. */ +enum l2cap_btu_cmd_type { + BTU_L2CAP_CMD_INIT, + BTU_L2CAP_CMD_DEINIT, + BTU_L2CAP_CMD_CONNECT, + BTU_L2CAP_CMD_DISCONNECT, + BTU_L2CAP_CMD_SEND, + BTU_L2CAP_CMD_ACCEPT_RSP, +}; + +struct l2cap_btu_cmd { + enum l2cap_btu_cmd_type type; + union { + struct { + uint16_t conn_handle; + } connect; + struct { + uint16_t conn_handle; + } disconnect; + struct { + uint16_t conn_handle; + BT_HDR *p_buf; + } send; + struct { + BD_ADDR bd_addr; + uint8_t id; + uint16_t lcid; + uint16_t result; + } accept_rsp; + }; + uint32_t gen; /* bumped per cycle so a late completion can be rejected */ + bool sync; /* true: give the sem on completion (INIT/DEINIT only) */ +}; + +/* Completion handoff for the sync invoke. Single set of globals, so it only works + * because INIT/DEINIT are the sole users and never overlap. */ +static BT_AUDIO_CTRL_BSS_ATTR struct k_sem btu_cmd_done; +static BT_AUDIO_CTRL_BSS_ATTR bool btu_cmd_done_init; +static BT_AUDIO_CTRL_BSS_ATTR uint32_t btu_done_gen; +static BT_AUDIO_CTRL_BSS_ATTR int btu_done_ret; + +static struct ots_chan_slot *ots_chan_find_used_by_lcid(uint16_t lcid) +{ + for (int i = 0; i < L2CAP_OTS_MAX_CHAN; i++) { + if (ots_chans[i].used && ots_chans[i].lcid == lcid) { + return &ots_chans[i]; + } + } + + return NULL; +} + +static struct ots_chan_slot *ots_chan_find_pending_by_addr(const BD_ADDR addr) +{ + for (int i = 0; i < L2CAP_OTS_MAX_CHAN; i++) { + if (ots_chans[i].pending && + memcmp(ots_chans[i].peer_addr, addr, sizeof(BD_ADDR)) == 0) { + return &ots_chans[i]; + } + } + + return NULL; +} + +/* psm is stamped here, not at cfm: a pending slot torn down by a link loss still + * has to post DISCONNECTED, and the upper layer keys that on the wire PSM. */ +static struct ots_chan_slot *ots_chan_alloc_pending(uint16_t conn_handle, const BD_ADDR addr) +{ + for (int i = 0; i < L2CAP_OTS_MAX_CHAN; i++) { + if (!ots_chans[i].used && !ots_chans[i].pending) { + memset(&ots_chans[i], 0, sizeof(ots_chans[i])); + ots_chans[i].conn_handle = conn_handle; + ots_chans[i].psm = L2CAP_LE_OTS_PSM; + memcpy(ots_chans[i].peer_addr, addr, sizeof(BD_ADDR)); + ots_chans[i].pending = true; + + return &ots_chans[i]; + } + } + + return NULL; +} + +static struct ots_chan_slot *ots_chan_find_pending_by_lcid(uint16_t lcid) +{ + for (int i = 0; i < L2CAP_OTS_MAX_CHAN; i++) { + if (ots_chans[i].pending && ots_chans[i].lcid == lcid) { + return &ots_chans[i]; + } + } + + return NULL; +} + +static struct ots_chan_slot *ots_chan_alloc_used(uint16_t conn_handle, uint16_t lcid, uint16_t psm) +{ + for (int i = 0; i < L2CAP_OTS_MAX_CHAN; i++) { + if (!ots_chans[i].used && !ots_chans[i].pending) { + memset(&ots_chans[i], 0, sizeof(ots_chans[i])); + ots_chans[i].conn_handle = conn_handle; + ots_chans[i].lcid = lcid; + ots_chans[i].psm = psm; + ots_chans[i].used = true; + + return &ots_chans[i]; + } + } + + return NULL; +} + +static void ots_chan_free(struct ots_chan_slot *slot) +{ + if (slot != NULL) { + memset(slot, 0, sizeof(*slot)); + } +} + +static void ots_cfg_init(tL2CAP_LE_CFG_INFO *cfg) +{ + /* mps/credits left at 0: the stack substitutes its CoC Kconfig defaults. */ + memset(cfg, 0, sizeof(*cfg)); + cfg->mtu = L2CAP_LE_OTS_MTU; +} + +/* All callbacks below run on BTU. */ + +#if CONFIG_BT_OTS +static void l2cap_connect_ind_cb(BD_ADDR bd_addr, UINT16 lcid, UINT16 psm, UINT8 id) +{ + struct gatt_conn *gatt_conn; + tL2CAP_LE_CFG_INFO cfg; + int err = -ENOTCONN; + + LOG_DBG("[B]L2capCocConnectInd[%04x][%04x][%u]", lcid, psm, id); + + gatt_conn = bt_le_bluedroid_find_gatt_conn_with_addr(0, bd_addr, true); + if (gatt_conn != NULL) { + err = bt_le_l2cap_post_accept(gatt_conn->conn_handle, psm, lcid, id); + } + + if (err) { + /* Nothing will answer on iso_task, so reject here. */ + LOG_ERR("[B]L2capIndRefuse[%04x][%p][%d]", lcid, gatt_conn, err); + + ots_cfg_init(&cfg); + + L2CA_ConnectLECocRsp(bd_addr, id, lcid, L2CAP_LE_ERR_NO_RESOURCES, 0, &cfg); + } +} +#endif /* CONFIG_BT_OTS */ + +/* Unwind the optimistic chan_add done by bt_l2cap_chan_connect()/l2cap_accept(): + * without a DISCONNECTED the upper layer waits for a result that never comes. */ +static void l2cap_cfm_fail(uint16_t conn_handle, const BD_ADDR addr) +{ + ots_chan_free(ots_chan_find_pending_by_addr(addr)); + + bt_le_l2cap_post_disconnected(conn_handle, L2CAP_LE_OTS_PSM); +} + +/* Fires for both roles. remote_cid has no public L2CA getter, and peer_addr + * is needed to match a pending slot, so the CCB is touched for those two only. */ +static void l2cap_connect_cfm_cb(UINT16 lcid, UINT16 result) +{ + struct gatt_conn *gatt_conn; + struct ots_chan_slot *slot; + tL2C_CCB *p_ccb; + tL2CAP_LE_CFG_INFO peer_cfg; + uint16_t conn_handle_h, psm_h; + + LOG_DBG("[B]L2capCocConnectCfm[%04x][%u]", lcid, result); + + p_ccb = l2cu_find_ccb_by_cid(NULL, lcid); + if (p_ccb == NULL || p_ccb->p_lcb == NULL) { + LOG_ERR("[B]L2capCfmNoCcb[%04x][%p]", lcid, p_ccb); + return; + } + + gatt_conn = bt_le_bluedroid_find_gatt_conn_with_addr(0, p_ccb->p_lcb->remote_bd_addr, true); + if (gatt_conn == NULL) { + LOG_ERR("[B]L2capCfmNoConn[%04x]", lcid); + + /* No gatt_conn, so the pending slot is the only source of conn_handle. */ + slot = ots_chan_find_pending_by_addr(p_ccb->p_lcb->remote_bd_addr); + if (slot != NULL) { + l2cap_cfm_fail(slot->conn_handle, p_ccb->p_lcb->remote_bd_addr); + } + return; + } + + if (result != L2CAP_CONN_OK) { + LOG_ERR("[B]L2capCocConnectFail[%04x][%u]", lcid, result); + l2cap_cfm_fail(gatt_conn->conn_handle, p_ccb->p_lcb->remote_bd_addr); + return; + } + + if (ots_chan_find_used_by_lcid(lcid) != NULL) { + /* Duplicate cfm for a live channel: the first one already reported it. */ + LOG_ERR("[B]L2capCocChanExist[%04x]", lcid); + return; + } + + if (!L2CA_GetPeerLECocConfig(lcid, &peer_cfg)) { + LOG_ERR("[B]L2capCfmNoPeerCfg[%04x]", lcid); + l2cap_cfm_fail(gatt_conn->conn_handle, p_ccb->p_lcb->remote_bd_addr); + return; + } + + /* Outgoing connect: reuse the pending slot reserved at chan_connect. + * Incoming accept: no pending slot, allocate a fresh one. */ + slot = ots_chan_find_pending_by_addr(p_ccb->p_lcb->remote_bd_addr); + if (slot != NULL) { + slot->pending = false; + slot->lcid = lcid; + slot->psm = L2CAP_LE_OTS_PSM; + slot->used = true; + } else { + slot = ots_chan_alloc_used(gatt_conn->conn_handle, lcid, L2CAP_LE_OTS_PSM); + if (slot == NULL) { + LOG_ERR("[B]L2capCocNoSlot[%04x]", lcid); + l2cap_cfm_fail(gatt_conn->conn_handle, p_ccb->p_lcb->remote_bd_addr); + return; + } + } + + conn_handle_h = slot->conn_handle; + psm_h = slot->psm; + + LOG_INF("[B]L2capCocConnect[%u][%04x][%04x][%04x][%u][%u]", + conn_handle_h, psm_h, + lcid, p_ccb->remote_cid, + L2CAP_LE_OTS_MTU, peer_cfg.mtu); + + bt_le_l2cap_post_connected(conn_handle_h, psm_h, + p_ccb->remote_cid, peer_cfg.mtu, + lcid, L2CAP_LE_OTS_MTU); +} + +/* Also fires for local disconnect, so DisconnectCfm needs no handler. */ +static void l2cap_disconnect_ind_cb(UINT16 lcid, BOOLEAN local_init) +{ + struct ots_chan_slot *slot; + uint16_t conn_handle_h, psm_h; + + LOG_DBG("[B]L2capCocDisconnectInd[%04x][%u]", lcid, local_init); + + slot = ots_chan_find_used_by_lcid(lcid); + if (slot == NULL) { + /* A link lost while the connect was still outstanding arrives here, not + * as a failed cfm (see the LP_DISCONNECT_IND case of l2c_csm's + * CST_W4_L2CAP_CONNECT_RSP). Without this the pending slot would leak and + * block every later OTS connect on that conn_handle. */ + slot = ots_chan_find_pending_by_lcid(lcid); + if (slot == NULL) { + LOG_ERR("[B]L2capDisconnectInvCocChan[%04x]", lcid); + return; + } + } + + conn_handle_h = slot->conn_handle; + psm_h = slot->psm; + ots_chan_free(slot); + + LOG_INF("[B]L2capCocDisconnect[%u][%04x]", conn_handle_h, psm_h); + + bt_le_l2cap_post_disconnected(conn_handle_h, psm_h); +} + +static void l2cap_data_ind_cb(UINT16 lcid, BT_HDR *p_buf) +{ + struct ots_chan_slot *slot; + uint16_t conn_handle_h, psm_h; + + if (p_buf == NULL) { + LOG_ERR("[B]L2capRecvNullBuf[%04x]", lcid); + return; + } + + LOG_DBG("[B]L2capCocRecv[%04x][%u]", lcid, p_buf->len); + + slot = ots_chan_find_used_by_lcid(lcid); + if (slot == NULL) { + LOG_ERR("[B]L2capRecvOnInvCocChan[%04x]", lcid); + osi_free(p_buf); + return; + } + + conn_handle_h = slot->conn_handle; + psm_h = slot->psm; + + bt_le_l2cap_post_received(conn_handle_h, psm_h, + p_buf->data + p_buf->offset, p_buf->len); + + /* SDU ownership was handed to this callback; the stack osi_malloc'd it. */ + osi_free(p_buf); +} + +/* Decongestion doubles as TX-done but can fire twice per SDU; tx_pending + * collapses the pair into one ops->sent. */ +static void l2cap_congestion_cb(UINT16 lcid, BOOLEAN congested) +{ + struct ots_chan_slot *slot; + uint16_t conn_handle_h, psm_h; + + LOG_DBG("[B]L2capCocCongestion[%04x][%u]", lcid, congested); + + slot = ots_chan_find_used_by_lcid(lcid); + if (slot == NULL) { + LOG_ERR("[B]L2capCongestionInvCocChan[%04x]", lcid); + return; + } + + if (congested || !slot->tx_pending) { + return; + } + + slot->tx_pending = false; + conn_handle_h = slot->conn_handle; + psm_h = slot->psm; + + bt_le_l2cap_post_sent(conn_handle_h, psm_h); +} + +/* ---- BTU executor functions ---------------------------------------------- */ + +static int l2cap_btu_do_accept_rsp(struct l2cap_btu_cmd *cmd) +{ + tL2CAP_LE_CFG_INFO cfg; + + ots_cfg_init(&cfg); + + if (!L2CA_ConnectLECocRsp(cmd->accept_rsp.bd_addr, cmd->accept_rsp.id, + cmd->accept_rsp.lcid, cmd->accept_rsp.result, 0, &cfg)) { + LOG_ERR("[B]L2capAcceptFail[%04x]", cmd->accept_rsp.lcid); + return -EIO; + } + + return 0; +} + +static void l2cap_btu_exec(void *ctx) +{ + struct l2cap_btu_cmd *cmd = ctx; + tL2CAP_LE_CFG_INFO cfg; + uint16_t lcid; + int ret = -EINVAL; + + switch (cmd->type) { + case BTU_L2CAP_CMD_INIT: { + tL2CAP_APPL_INFO appl = {0}; +#if CONFIG_BT_OTS + appl.pL2CA_ConnectInd_Cb = l2cap_connect_ind_cb; +#endif /* CONFIG_BT_OTS */ + appl.pL2CA_ConnectCfm_Cb = l2cap_connect_cfm_cb; + appl.pL2CA_DisconnectInd_Cb = l2cap_disconnect_ind_cb; + appl.pL2CA_DataInd_Cb = l2cap_data_ind_cb; + appl.pL2CA_CongestionStatus_Cb = l2cap_congestion_cb; + + ots_reg_psm = L2CA_RegisterLECoc(L2CAP_LE_OTS_PSM, &appl); + if (ots_reg_psm == 0) { + ret = -EIO; + break; + } + + BTM_SetSecurityLevel(TRUE, "BLE_L2CAP_OTS", BTM_SEC_SERVICE_GEN_NET, + BTM_SEC_NONE, ots_reg_psm, BTM_SEC_PROTO_L2CAP, 0); + BTM_SetSecurityLevel(FALSE, "BLE_L2CAP_OTS", BTM_SEC_SERVICE_GEN_NET, + BTM_SEC_NONE, ots_reg_psm, BTM_SEC_PROTO_L2CAP, 0); + ret = 0; + break; + } + case BTU_L2CAP_CMD_DEINIT: + /* Tear our channels down first: L2CA_DeregisterLECoc only inspects each + * link's first CCB, so a CoC CCB sitting behind another one survives and + * is left pointing at the released RCB (p_ccb->p_rcb->api use-after-free). */ + for (int i = 0; i < L2CAP_OTS_MAX_CHAN; i++) { + /* pending too: its CCB is live from ConnectLECocReq onwards, and + LECocDisconnect cancels a not-yet-open channel locally. */ + if (ots_chans[i].used || ots_chans[i].pending) { + L2CA_LECocDisconnect(ots_chans[i].lcid); + } + } + + if (ots_reg_psm) { + L2CA_DeregisterLECoc(ots_reg_psm); + ots_reg_psm = 0; + } + ret = 0; + break; + case BTU_L2CAP_CMD_CONNECT: { + struct gatt_conn *gatt_conn; + struct ots_chan_slot *slot; + + gatt_conn = bt_le_bluedroid_find_gatt_conn_with_handle(cmd->connect.conn_handle); + if (gatt_conn == NULL) { + LOG_ERR("[B]L2capNoConnInfo[%u]", cmd->connect.conn_handle); + ret = -ENOTCONN; + break; + } + + /* Reject if this connection already has an OTS channel. */ + for (int i = 0; i < L2CAP_OTS_MAX_CHAN; i++) { + if ((ots_chans[i].used || ots_chans[i].pending) && + ots_chans[i].conn_handle == cmd->connect.conn_handle) { + LOG_WRN("[B]L2capOtsChanExist[%u]", cmd->connect.conn_handle); + ret = -EALREADY; + break; + } + } + if (ret == -EALREADY) { + break; + } + + slot = ots_chan_alloc_pending(cmd->connect.conn_handle, gatt_conn->peer.val); + if (slot == NULL) { + LOG_ERR("[B]L2capOtsNoSlot[%u]", cmd->connect.conn_handle); + ret = -ENOMEM; + break; + } + + ots_cfg_init(&cfg); + + /* Returns 0 on failure, LCID on success. Keep the LCID: if the link dies + * before the cfm the stack reports DisconnectInd, and by then the CCB is + * released so the LCID is the only way back to this slot. */ + lcid = L2CA_ConnectLECocReq(ots_reg_psm, gatt_conn->peer.val, &cfg); + if (lcid == 0) { + ret = -EIO; + ots_chan_free(slot); + /* Post DISCONNECTED to clean up the optimistic chan_add. */ + bt_le_l2cap_post_disconnected(cmd->connect.conn_handle, L2CAP_LE_OTS_PSM); + break; + } + + slot->lcid = lcid; + ret = 0; + break; + } + case BTU_L2CAP_CMD_DISCONNECT: { + struct ots_chan_slot *slot = NULL; + + for (int i = 0; i < L2CAP_OTS_MAX_CHAN; i++) { + if (ots_chans[i].used && ots_chans[i].conn_handle == cmd->disconnect.conn_handle) { + slot = &ots_chans[i]; + break; + } + } + if (slot == NULL) { + LOG_WRN("[B]L2capNoOtsChan"); + ret = -ENOTCONN; + break; + } + + ret = L2CA_LECocDisconnect(slot->lcid) ? 0 : -EIO; + break; + } + case BTU_L2CAP_CMD_SEND: { + struct ots_chan_slot *slot = NULL; + + for (int i = 0; i < L2CAP_OTS_MAX_CHAN; i++) { + if (ots_chans[i].used && ots_chans[i].conn_handle == cmd->send.conn_handle) { + slot = &ots_chans[i]; + break; + } + } + if (slot == NULL) { + LOG_WRN("[B]L2capNoOtsChan"); + /* Slot gone (disconnect won the race); free p_buf ourselves. */ + osi_free(cmd->send.p_buf); + ret = -ENOTCONN; + break; + } + + /* Armed before the write: a non-congested SDU completes inside it and + * raises the decongestion cb synchronously (same BTU task). */ + slot->tx_pending = true; + /* On DW_FAILED the stack frees p_buf; on SUCCESS/CONGESTED it owns it. */ + ret = (L2CA_LECocDataWrite(slot->lcid, cmd->send.p_buf) == L2CAP_DW_FAILED) + ? -EIO : 0; + if (ret != 0) { + slot->tx_pending = false; + } + break; + } + case BTU_L2CAP_CMD_ACCEPT_RSP: + ret = l2cap_btu_do_accept_rsp(cmd); + break; + default: + ret = -EINVAL; + break; + } + + if (cmd->sync) { + btu_done_ret = ret; + btu_done_gen = cmd->gen; + k_sem_give(&btu_cmd_done); + } + + free(cmd); +} + +/* Post cmd to BTU fire-and-forget. Returns 0 on success, -EIO if BTU is + * unavailable (cmd is freed on failure). Never blocks. No self-delivery check is + * needed - unlike the sync invoke, posting to our own queue cannot deadlock. */ +static int l2cap_btu_post(struct l2cap_btu_cmd *cmd) +{ + osi_thread_t *btu = btu_get_current_thread(); + + cmd->sync = false; + + if (btu == NULL || !osi_thread_post(btu, l2cap_btu_exec, cmd, 0, 0)) { + LOG_ERR("[B]L2capBtuPostFail[%u]", cmd->type); + free(cmd); + return -EIO; + } + + return 0; +} + +void bt_le_bluedroid_l2cap_accept_result(uint16_t conn_handle, uint8_t l2cap_id, + uint16_t chan_handle, uint16_t result) +{ + struct gatt_conn *gatt_conn; + struct l2cap_btu_cmd *cmd; + + LOG_DBG("[B]L2capAcceptResult[%04x][%04x]", chan_handle, result); + + gatt_conn = bt_le_bluedroid_find_gatt_conn_with_handle(conn_handle); + if (gatt_conn == NULL) { + LOG_ERR("[B]L2capAcceptNoConn[%u]", conn_handle); + return; + } + + cmd = bt_le_ext_calloc(1, sizeof(*cmd)); + if (cmd == NULL) { + LOG_ERR("[B]L2capAcceptNoCmd"); + return; + } + + cmd->type = BTU_L2CAP_CMD_ACCEPT_RSP; + cmd->accept_rsp.id = l2cap_id; + cmd->accept_rsp.lcid = chan_handle; + cmd->accept_rsp.result = result; + memcpy(cmd->accept_rsp.bd_addr, gatt_conn->peer.val, sizeof(BD_ADDR)); + + l2cap_btu_post(cmd); +} + +int bt_le_bluedroid_l2cap_chan_connect(uint16_t conn_handle) +{ + struct l2cap_btu_cmd *cmd; + + cmd = bt_le_ext_calloc(1, sizeof(*cmd)); + if (cmd == NULL) { + LOG_ERR("[B]L2capConnectNoCmd"); + return -ENOMEM; + } + + cmd->type = BTU_L2CAP_CMD_CONNECT; + cmd->connect.conn_handle = conn_handle; + + /* Result comes back as CONNECTED, or DISCONNECTED if BTU's connect fails. */ + return l2cap_btu_post(cmd); +} + +int bt_le_bluedroid_l2cap_chan_disconnect(struct bt_l2cap_chan *chan) +{ + struct l2cap_btu_cmd *cmd; + + cmd = bt_le_ext_calloc(1, sizeof(*cmd)); + if (cmd == NULL) { + LOG_ERR("[B]L2capDisconnectNoCmd"); + return -ENOMEM; + } + + cmd->type = BTU_L2CAP_CMD_DISCONNECT; + cmd->disconnect.conn_handle = chan->conn->handle; + + /* Result comes back as DISCONNECTED via l2cap_disconnect_ind_cb. */ + return l2cap_btu_post(cmd); +} + +int bt_le_bluedroid_l2cap_chan_send(struct bt_l2cap_chan *chan, struct net_buf *buf) +{ + struct l2cap_btu_cmd *cmd; + BT_HDR *p_buf; + int ret; + + /* osi_malloc, not bt_le_int_malloc: the stack frees this SDU with osi_free, + * and it never reaches DMA - the TX path copies it into a K-frame buffer. */ + p_buf = (BT_HDR *)osi_malloc(sizeof(BT_HDR) + buf->len); + if (p_buf == NULL) { + LOG_ERR("[B]L2capNoBufForSend[%u]", buf->len); + return -ENOMEM; + } + + /* offset stays 0: the CoC TX path builds its own K-frames, no headroom here. */ + memset(p_buf, 0, sizeof(*p_buf)); + p_buf->len = buf->len; + memcpy(p_buf->data, buf->data, buf->len); + + cmd = bt_le_ext_calloc(1, sizeof(*cmd)); + if (cmd == NULL) { + LOG_ERR("[B]L2capSendNoCmd"); + osi_free(p_buf); + return -ENOMEM; + } + + cmd->type = BTU_L2CAP_CMD_SEND; + cmd->send.conn_handle = chan->conn->handle; + cmd->send.p_buf = p_buf; + + /* Completion comes back as ops->sent; on DW_FAILED nothing does, and the + * eventual DISCONNECTED aborts the transfer. */ + ret = l2cap_btu_post(cmd); + if (ret != 0) { + /* BTU unavailable: p_buf not queued, free it. buf stays with caller. */ + osi_free(p_buf); + return ret; + } + + /* Posted: payload is in p_buf, release caller's buf else ot_chan_tx_pool + * (1 entry) leaks. */ + net_buf_unref(buf); + + return 0; +} + +/* Run cmd on BTU and wait — INIT/DEINIT only. Hot-path ops use + * l2cap_btu_post (fire-and-forget). cmd is freed by l2cap_btu_exec. */ +static int l2cap_btu_invoke(struct l2cap_btu_cmd *cmd) +{ + osi_thread_t *btu = btu_get_current_thread(); + uint32_t gen = ++btu_done_gen; /* unique per cycle; stale gives are ignored */ + + cmd->gen = gen; + cmd->sync = true; + + /* Self-delivery would deadlock: run inline and drain the give. */ + if (btu != NULL && strcmp(osi_thread_name(btu), pcTaskGetName(NULL)) == 0) { + l2cap_btu_exec(cmd); + k_sem_take(&btu_cmd_done, 0); + return btu_done_ret; + } + + k_sem_reset(&btu_cmd_done); + + if (btu == NULL || !osi_thread_post(btu, l2cap_btu_exec, cmd, 0, 0)) { + LOG_WRN("[B]L2capBtuUnavailable[%u]", cmd->type); + l2cap_btu_exec(cmd); + k_sem_take(&btu_cmd_done, 0); + return btu_done_ret; + } + + if (k_sem_take(&btu_cmd_done, K_SEM_SHORT) != 0) { + LOG_ERR("[B]L2capBtuTimeout[%u]", cmd->type); + /* cmd still on the BTU queue; bump gen so a late completion is rejected. */ + btu_done_gen++; + return -ETIMEDOUT; + } + + if (btu_done_gen != gen) { + LOG_ERR("[B]L2capBtuStaleGen[%u][%u]", gen, btu_done_gen); + return -ETIMEDOUT; + } + + return btu_done_ret; +} + +int bt_le_bluedroid_l2cap_init(void) +{ + struct l2cap_btu_cmd *cmd; + int ret; + + LOG_DBG("[B]L2capInit"); + + memset(ots_chans, 0, sizeof(ots_chans)); + + if (!btu_cmd_done_init) { + k_sem_create(&btu_cmd_done); + btu_cmd_done_init = true; + } + + /* Drop any stale give so the first invoke doesn't match an old completion. */ + k_sem_reset(&btu_cmd_done); + + cmd = bt_le_ext_calloc(1, sizeof(*cmd)); + if (cmd == NULL) { + LOG_ERR("[B]L2capInitNoCmd"); + return -ENOMEM; + } + + cmd->type = BTU_L2CAP_CMD_INIT; + + ret = l2cap_btu_invoke(cmd); + if (ret == -ETIMEDOUT) { + /* The command is still queued and will register the PSM once BTU drains + * it, while we report failure. Queue a DEINIT behind it (the BTU queue + * is FIFO) so the registration cannot outlive this failed init. */ + cmd = bt_le_ext_calloc(1, sizeof(*cmd)); + if (cmd == NULL) { + LOG_ERR("[B]L2capInitNoUndoCmd"); + return ret; + } + + cmd->type = BTU_L2CAP_CMD_DEINIT; + l2cap_btu_post(cmd); + } + + return ret; +} + +void bt_le_bluedroid_l2cap_deinit(void) +{ + struct l2cap_btu_cmd *cmd; + int ret; + + LOG_DBG("[B]L2capDeinit"); + + /* If init never ran, nothing to tear down and the sem is unsafe to touch. */ + if (!btu_cmd_done_init || ots_reg_psm == 0) { + memset(ots_chans, 0, sizeof(ots_chans)); + return; + } + + cmd = bt_le_ext_calloc(1, sizeof(*cmd)); + if (cmd == NULL) { + LOG_ERR("[B]L2capDeinitNoCmd"); + memset(ots_chans, 0, sizeof(ots_chans)); + return; + } + + cmd->type = BTU_L2CAP_CMD_DEINIT; + + ret = l2cap_btu_invoke(cmd); + if (ret == 0) { + memset(ots_chans, 0, sizeof(ots_chans)); + } else { + /* Timed out: DEINIT still queued. Leave ots_chans so the BTU handler's + * disconnect loop runs before PSM deregister. */ + LOG_ERR("[B]L2capDeinitTimeout"); + } +} diff --git a/components/bt/esp_ble_audio/host/services/ots/adapter/l2cap.c b/components/bt/esp_ble_audio/host/services/ots/adapter/l2cap.c new file mode 100644 index 00000000000..8f8016216de --- /dev/null +++ b/components/bt/esp_ble_audio/host/services/ots/adapter/l2cap.c @@ -0,0 +1,851 @@ +/* + * SPDX-FileCopyrightText: 2015-2016 Intel Corporation + * SPDX-FileCopyrightText: 2023 Nordic Semiconductor + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include + +#include <../host/conn_internal.h> + +#include "common/host.h" +#include "common/audio_attr.h" +#include "common/app/gap.h" + +#include "ots/adapter/l2cap.h" + +LOG_MODULE_REGISTER(ISO_L2CAP, CONFIG_BT_ISO_LOG_LEVEL); + +#define L2CAP_LE_MIN_MTU 23 +#define L2CAP_ECRED_MIN_MTU 64 + +#define L2CAP_LE_CID_DYN_START 0x0040 +#define L2CAP_LE_CID_DYN_END 0x007F +#define L2CAP_LE_CID_IS_DYN(_cid) (_cid >= L2CAP_LE_CID_DYN_START && _cid <= L2CAP_LE_CID_DYN_END) + +#define L2CAP_LE_PSM_FIXED_START 0x0001 +#define L2CAP_LE_PSM_FIXED_END 0x007F +#define L2CAP_LE_PSM_DYN_START 0x0080 +#define L2CAP_LE_PSM_DYN_END 0x00FF +#define L2CAP_LE_PSM_IS_DYN(_psm) (_psm >= L2CAP_LE_PSM_DYN_START && _psm <= L2CAP_LE_PSM_DYN_END) + +static BT_AUDIO_EXT_RAM_BSS_ATTR sys_slist_t l2cap_servers; + +/* OTS internals, declared here rather than including the OTS private headers. */ +#if CONFIG_BT_OTS +extern int bt_gatt_ots_conn_cb_register(void); +extern void bt_gatt_ots_conn_cb_unregister(void); +extern int bt_gatt_ots_instances_prepare(void); +extern void bt_ots_instances_rewind(void); +#endif /* CONFIG_BT_OTS */ +#if CONFIG_BT_OTS_CLIENT +extern int bt_gatt_ots_client_conn_cb_register(void); +extern void bt_gatt_ots_client_conn_cb_unregister(void); +extern void bt_ots_client_unregister_all(void); +#endif /* CONFIG_BT_OTS_CLIENT */ +extern int bt_gatt_ots_l2cap_init(void); + +static struct bt_l2cap_chan *l2cap_lookup_tx_cid(struct bt_conn *conn, uint16_t cid) +{ + struct bt_l2cap_chan *chan; + + SYS_SLIST_FOR_EACH_CONTAINER(&conn->channels, chan, node) { + if (BT_L2CAP_LE_CHAN(chan)->tx.cid == cid) { + return chan; + } + } + + return NULL; +} + +__attribute__((unused)) +static struct bt_l2cap_chan *l2cap_lookup_rx_cid(struct bt_conn *conn, uint16_t cid) +{ + struct bt_l2cap_chan *chan; + + SYS_SLIST_FOR_EACH_CONTAINER(&conn->channels, chan, node) { + if (BT_L2CAP_LE_CHAN(chan)->rx.cid == cid) { + return chan; + } + } + + return NULL; +} + +static struct bt_l2cap_chan *l2cap_lookup_psm(struct bt_conn *conn, uint16_t psm) +{ + struct bt_l2cap_chan *chan; + + SYS_SLIST_FOR_EACH_CONTAINER(&conn->channels, chan, node) { + if (BT_L2CAP_LE_CHAN(chan)->psm == psm) { + return chan; + } + } + + return NULL; +} + +static bool l2cap_chan_add(struct bt_conn *conn, struct bt_l2cap_chan *chan, uint16_t psm) +{ + LOG_DBG("L2capChanAdd[%04x]", psm); + + /* Attach channel to the connection */ + if (sys_slist_find(&conn->channels, &chan->node, NULL)) { + LOG_WRN("L2capChanExists[%04x]", psm); + return false; + } + + sys_slist_append(&conn->channels, &chan->node); + chan->conn = conn; + + /* Set channel PSM */ + BT_L2CAP_LE_CHAN(chan)->psm = psm; + + return true; +} + +static struct bt_l2cap_server *l2cap_server_lookup_psm(uint16_t psm) +{ + struct bt_l2cap_server *server = NULL; + + SYS_SLIST_FOR_EACH_CONTAINER(&l2cap_servers, server, node) { + if (server->psm == psm) { + break; + } + } + + return server; +} + +static inline uint16_t err_to_result(int err) +{ + switch (err) { + case -ENOMEM: + return L2CAP_LE_ERR_NO_RESOURCES; + case -EACCES: + return L2CAP_LE_ERR_AUTHORIZATION; + case -EPERM: + return L2CAP_LE_ERR_KEY_SIZE; + case -ENOTSUP: + /* This handle the cases where a fixed channel is registered but + * for some reason (e.g. controller not supporting a feature) + * cannot be used. + */ + return L2CAP_LE_ERR_PSM_NOT_SUPP; + default: + return L2CAP_LE_ERR_UNACCEPT_PARAMS; + } +} + +static int l2cap_accept(uint16_t conn_handle, uint16_t psm, + uint16_t scid, uint16_t mtu, + uint16_t mps, uint16_t credits, + uint16_t *result) +{ + struct bt_l2cap_server *server; + struct bt_l2cap_chan *chan; + struct bt_conn *conn; + int err; + + ARG_UNUSED(credits); + + LOG_DBG("L2capAccept[%u][%04x][%04x][%u][%u]", conn_handle, psm, scid, mtu, mps); + + conn = bt_le_acl_conn_find(conn_handle); + if (conn == NULL || conn->state != BT_CONN_CONNECTED) { + LOG_INF("L2capAcceptNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); + + *result = L2CAP_LE_ERR_INVALID_PARAMS; + return -ENOTCONN; + } + + /* Check if there is a server registered */ + server = l2cap_server_lookup_psm(psm); + if (server == NULL) { + LOG_ERR("L2capSrvNotReg[%04x]", psm); + + *result = L2CAP_LE_ERR_PSM_NOT_SUPP; + return -ENOTSUP; + } + + if (!L2CAP_LE_CID_IS_DYN(scid)) { + LOG_ERR("L2capNotDynScid[%04x]", scid); + + *result = L2CAP_LE_ERR_INVALID_SCID; + return -EINVAL; + } + + chan = l2cap_lookup_tx_cid(conn, scid); + if (chan) { + LOG_WRN("L2capScidUsed[%04x]", scid); + + *result = L2CAP_LE_ERR_SCID_IN_USE; + return -EALREADY; + } + + /* Every event is routed back by (conn, psm), so a second channel for the + * same pair would deliver to whichever chan l2cap_lookup_psm() hits first. */ + if (l2cap_lookup_psm(conn, psm) != NULL) { + LOG_WRN("L2capPsmChanExists[%u][%04x]", conn_handle, psm); + + *result = L2CAP_LE_ERR_NO_RESOURCES; + return -EALREADY; + } + + if (server->accept == NULL) { + LOG_ERR("L2capSrvAcceptNull"); + + *result = L2CAP_LE_ERR_INVALID_PARAMS; + return -EIO; + } + + err = server->accept(conn, server, &chan); + if (err) { + LOG_ERR("L2capSrvAcceptFail[%d]", err); + + *result = err_to_result(err); + return -EIO; + } + + if (chan == NULL) { + LOG_ERR("L2capSrvAcceptNullChan"); + *result = L2CAP_LE_ERR_NO_RESOURCES; + return -ENOMEM; + } + + if (l2cap_chan_add(conn, chan, psm) == false) { + *result = L2CAP_LE_ERR_NO_RESOURCES; + return -ENOMEM; + } + + *result = L2CAP_LE_SUCCESS; + return 0; +} + +#if CONFIG_BT_BLUEDROID_ENABLED +static void l2cap_handle_accept(const struct bt_le_l2cap_event *qev) +{ + uint16_t result = L2CAP_LE_ERR_NO_RESOURCES; + + /* chan_handle is our local CID; the peer's is unknown until the channel is + * up, so it stands in for scid (dynamic-CID check and duplicate detection). + * mtu/mps are logging-only in l2cap_accept(). */ + (void)l2cap_accept(qev->conn_handle, qev->psm, qev->accept.chan_handle, + 0, 0, 0, &result); + + /* Forward result, not a bool: l2cap_accept() distinguishes authentication, + * key size and SCID-in-use, and the peer is entitled to the exact reason. */ + bt_le_bluedroid_l2cap_accept_result(qev->conn_handle, qev->accept.l2cap_id, + qev->accept.chan_handle, result); +} +#endif /* CONFIG_BT_BLUEDROID_ENABLED */ + +static void l2cap_connected(uint16_t conn_handle, uint16_t psm, + uint16_t tx_cid, uint16_t tx_mtu, + uint16_t rx_cid, uint16_t rx_mtu) +{ + struct bt_l2cap_chan *chan; + struct bt_conn *conn; + + LOG_DBG("L2capConnected[%u][%04x][%04x][%u][%04x][%u]", + conn_handle, psm, tx_cid, tx_mtu, rx_cid, rx_mtu); + + conn = bt_le_acl_conn_find(conn_handle); + if (conn == NULL || conn->state != BT_CONN_CONNECTED) { + LOG_INF("L2capConnectedNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); + return; + } + + chan = l2cap_lookup_psm(conn, psm); + if (chan == NULL) { + LOG_ERR("L2capPsmNotFound[%04x]", psm); + return; + } + + BT_L2CAP_LE_CHAN(chan)->tx.cid = tx_cid; + BT_L2CAP_LE_CHAN(chan)->tx.mtu = tx_mtu; + BT_L2CAP_LE_CHAN(chan)->rx.cid = rx_cid; + BT_L2CAP_LE_CHAN(chan)->rx.mtu = rx_mtu; + + if (chan->ops->connected) { + chan->ops->connected(chan); + } +} + +static void l2cap_disconnected(uint16_t conn_handle, uint16_t psm) +{ + struct bt_l2cap_chan *chan; + struct bt_conn *conn; + + LOG_DBG("L2capDisconnected[%u][%04x]", conn_handle, psm); + + conn = bt_le_acl_conn_find(conn_handle); + if (conn == NULL || conn->state != BT_CONN_CONNECTED) { + LOG_INF("L2capDisconnectedNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); + return; + } + + chan = l2cap_lookup_psm(conn, psm); + if (chan == NULL) { + LOG_ERR("L2capPsmNotFound[%04x]", psm); + return; + } + + /* Unlink first so no later event rediscovers it by PSM. The callback must not + * free chan: it reads chan->conn, and clearing conn after returns the pool ctx. */ + sys_slist_find_and_remove(&conn->channels, &chan->node); + + if (chan->ops->disconnected) { + chan->ops->disconnected(chan); + } + + chan->conn = NULL; +} + +static void l2cap_received(uint16_t conn_handle, uint16_t psm, + uint8_t *data, uint16_t len) +{ + struct bt_l2cap_chan *chan; + struct net_buf buf = {0}; + struct bt_conn *conn; + + LOG_DBG("L2capReceived[%u][%04x][%u]", conn_handle, psm, len); + + conn = bt_le_acl_conn_find(conn_handle); + if (conn == NULL || conn->state != BT_CONN_CONNECTED) { + LOG_INF("L2capReceivedNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); + return; + } + + chan = l2cap_lookup_psm(conn, psm); + if (chan == NULL) { + LOG_ERR("L2capPsmNotFound[%04x]", psm); + return; + } + + buf.data = data; + buf.len = len; + + if (chan->ops->recv) { + chan->ops->recv(chan, &buf); + } +} + +static void l2cap_sent(uint16_t conn_handle, uint16_t psm) +{ + struct bt_l2cap_chan *chan; + struct bt_conn *conn; + + LOG_DBG("L2capSent[%u][%04x]", conn_handle, psm); + + conn = bt_le_acl_conn_find(conn_handle); + if (conn == NULL || conn->state != BT_CONN_CONNECTED) { + LOG_INF("L2capSentNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); + return; + } + + chan = l2cap_lookup_psm(conn, psm); + if (chan == NULL) { + LOG_ERR("L2capPsmNotFound[%04x]", psm); + return; + } + + if (chan->ops->sent) { + chan->ops->sent(chan); + } +} + +_IDF_ONLY +void bt_le_l2cap_event_free(void *data) +{ + struct bt_le_l2cap_event *qev = data; + + if (qev == NULL) { + return; + } + + /* Only RECEIVED carries a buffer of its own; the rest are header-only. */ + if (qev->type == BT_LE_L2CAP_EVENT_RECEIVED) { + free(qev->received.data); + } + + free(qev); +} + +static int l2cap_post_event(struct bt_le_l2cap_event *qev) +{ + int err; + + err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_L2CAP_EVENT, qev, sizeof(*qev)); + if (err) { + ISO_POST_FAIL_LOG(err, "L2capPostFail[%d][%u]", err, qev->type); + bt_le_l2cap_event_free(qev); + } + + return err; +} + +/* Alloc + stamp the common header. NULL on OOM rather than abort: L2CAP is off + * the ISO data path, so losing one event beats taking the whole stack down. */ +static struct bt_le_l2cap_event *l2cap_event_alloc(uint8_t type, uint16_t conn_handle, + uint16_t psm) +{ + struct bt_le_l2cap_event *qev; + + qev = bt_le_ext_calloc(1, sizeof(*qev)); + if (qev == NULL) { + return NULL; + } + + qev->type = type; + qev->conn_handle = conn_handle; + qev->psm = psm; + + return qev; +} + +_IDF_ONLY +int bt_le_l2cap_post_connected(uint16_t conn_handle, uint16_t psm, + uint16_t tx_cid, uint16_t tx_mtu, + uint16_t rx_cid, uint16_t rx_mtu) +{ + struct bt_le_l2cap_event *qev; + + qev = l2cap_event_alloc(BT_LE_L2CAP_EVENT_CONNECTED, conn_handle, psm); + if (qev == NULL) { + LOG_ERR("L2capConnectedNoMem[%u][%04x]", conn_handle, psm); + return -ENOMEM; + } + + qev->connected.tx_cid = tx_cid; + qev->connected.tx_mtu = tx_mtu; + qev->connected.rx_cid = rx_cid; + qev->connected.rx_mtu = rx_mtu; + + return l2cap_post_event(qev); +} + +_IDF_ONLY +int bt_le_l2cap_post_disconnected(uint16_t conn_handle, uint16_t psm) +{ + struct bt_le_l2cap_event *qev; + + qev = l2cap_event_alloc(BT_LE_L2CAP_EVENT_DISCONNECTED, conn_handle, psm); + if (qev == NULL) { + LOG_ERR("L2capDisconnectedNoMem[%u][%04x]", conn_handle, psm); + return -ENOMEM; + } + + return l2cap_post_event(qev); +} + +_IDF_ONLY +int bt_le_l2cap_post_received(uint16_t conn_handle, uint16_t psm, + const uint8_t *data, uint16_t len) +{ + struct bt_le_l2cap_event *qev; + + qev = l2cap_event_alloc(BT_LE_L2CAP_EVENT_RECEIVED, conn_handle, psm); + if (qev == NULL) { + LOG_ERR("L2capReceivedNoMem[%u][%04x][%u]", conn_handle, psm, len); + return -ENOMEM; + } + + qev->received.len = len; + + if (len) { + qev->received.data = bt_le_ext_calloc(1, len); + if (qev->received.data == NULL) { + LOG_ERR("L2capReceivedNoDataMem[%u][%04x][%u]", conn_handle, psm, len); + free(qev); + return -ENOMEM; + } + memcpy(qev->received.data, data, len); + } + + return l2cap_post_event(qev); +} + +_IDF_ONLY +int bt_le_l2cap_post_sent(uint16_t conn_handle, uint16_t psm) +{ + struct bt_le_l2cap_event *qev; + + qev = l2cap_event_alloc(BT_LE_L2CAP_EVENT_SENT, conn_handle, psm); + if (qev == NULL) { + LOG_ERR("L2capSentNoMem[%u][%04x]", conn_handle, psm); + return -ENOMEM; + } + + return l2cap_post_event(qev); +} + +#if CONFIG_BT_BLUEDROID_ENABLED +_IDF_ONLY +int bt_le_l2cap_post_accept(uint16_t conn_handle, uint16_t psm, + uint16_t chan_handle, uint8_t l2cap_id) +{ + struct bt_le_l2cap_event *qev; + + qev = l2cap_event_alloc(BT_LE_L2CAP_EVENT_ACCEPT, conn_handle, psm); + if (qev == NULL) { + LOG_ERR("L2capAcceptNoMem[%u][%04x][%04x]", conn_handle, psm, chan_handle); + return -ENOMEM; + } + + qev->accept.chan_handle = chan_handle; + qev->accept.l2cap_id = l2cap_id; + + return l2cap_post_event(qev); +} +#else /* !CONFIG_BT_BLUEDROID_ENABLED */ +_IDF_ONLY +int bt_le_l2cap_accept_safe(uint16_t conn_handle, uint16_t psm, + uint16_t scid, uint16_t mtu, + uint16_t mps, uint16_t credits, + uint16_t *result) +{ + int err; + bt_le_host_lock(); + err = l2cap_accept(conn_handle, psm, scid, mtu, mps, credits, result); + bt_le_host_unlock(); + return err; +} +#endif /* !CONFIG_BT_BLUEDROID_ENABLED */ + +_IDF_ONLY +void bt_le_l2cap_handle_event(void *data, size_t data_len) +{ + struct bt_le_l2cap_event *qev = data; + + if (qev == NULL) { + LOG_ERR("L2capEvtNull"); + return; + } + + if (data_len != sizeof(*qev)) { + LOG_ERR("L2capEvtBadLen[%u]", (unsigned)data_len); + free(qev); + return; + } + + bt_le_host_lock(); + + switch (qev->type) { + case BT_LE_L2CAP_EVENT_CONNECTED: + l2cap_connected(qev->conn_handle, qev->psm, + qev->connected.tx_cid, qev->connected.tx_mtu, + qev->connected.rx_cid, qev->connected.rx_mtu); + break; + case BT_LE_L2CAP_EVENT_DISCONNECTED: + l2cap_disconnected(qev->conn_handle, qev->psm); + break; + case BT_LE_L2CAP_EVENT_RECEIVED: + l2cap_received(qev->conn_handle, qev->psm, + qev->received.data, qev->received.len); + free(qev->received.data); + break; + case BT_LE_L2CAP_EVENT_SENT: + l2cap_sent(qev->conn_handle, qev->psm); + break; +#if CONFIG_BT_BLUEDROID_ENABLED + case BT_LE_L2CAP_EVENT_ACCEPT: + l2cap_handle_accept(qev); + break; +#endif /* CONFIG_BT_BLUEDROID_ENABLED */ + default: + LOG_ERR("L2capEvtUnknown[%u]", qev->type); + break; + } + + bt_le_host_unlock(); + + free(qev); +} + +_IDF_ONLY +int bt_l2cap_chan_connect(struct bt_conn *conn, struct bt_l2cap_chan *chan, uint16_t psm) +{ + int err; + + LOG_DBG("L2capChanConnect[%04x]", psm); + + if (chan == NULL) { + LOG_ERR("L2capChanNull"); + return -EINVAL; + } + + if (psm < L2CAP_LE_PSM_FIXED_START || psm > L2CAP_LE_PSM_DYN_END) { + LOG_ERR("L2capInvPsm[%04x]", psm); + return -EINVAL; + } + + /* Caller holds bt_le_host_lock. chan_add is done BEFORE the adapter call + * (optimistic): the Bluedroid adapter posts fire-and-forget, so the + * CONNECTED/DISCONNECTED event needs the chan already in conn->channels. + * On failure the adapter posts DISCONNECTED, matching the Zephyr + * connect-fail contract. */ + if (conn == NULL || conn->state != BT_CONN_CONNECTED) { + LOG_ERR("L2capChanNotConn[%p]", conn); + return -ENOTCONN; + } + + /* Same (conn, psm) uniqueness as l2cap_accept(): event routing keys on it. + * l2cap_chan_add only rejects re-adding this very chan object. */ + if (l2cap_lookup_psm(conn, psm) != NULL) { + LOG_WRN("L2capPsmChanExists[%u][%04x]", conn->handle, psm); + return -EALREADY; + } + + if (!l2cap_chan_add(conn, chan, psm)) { + return -EALREADY; + } + +#if CONFIG_BT_BLUEDROID_ENABLED + err = bt_le_bluedroid_l2cap_chan_connect(conn->handle); +#else + err = bt_le_nimble_l2cap_chan_connect(conn->handle); +#endif + if (err) { + /* Roll back the optimistic chan_add. */ + sys_slist_find_and_remove(&conn->channels, &chan->node); + chan->conn = NULL; + return err; + } + + return 0; +} + +_IDF_ONLY +int bt_l2cap_chan_disconnect(struct bt_l2cap_chan *chan) +{ + int err; + + LOG_DBG("L2capChanDisconnect"); + + if (chan == NULL) { + LOG_ERR("L2capChanNull"); + return -EINVAL; + } + + if (chan->conn == NULL || chan->conn->state != BT_CONN_CONNECTED) { + LOG_ERR("L2capChanNotConn[%p]", chan->conn); + return -ENOTCONN; + } + +#if CONFIG_BT_BLUEDROID_ENABLED + err = bt_le_bluedroid_l2cap_chan_disconnect(chan); +#else + err = bt_le_nimble_l2cap_chan_disconnect(chan); +#endif + if (err) { + /* If the disconnect failed, remove the channel from the connection. + * Otherwise the removal will be handled by the disconnect callback. */ + if (chan->conn != NULL) { + sys_slist_find_and_remove(&chan->conn->channels, &chan->node); + chan->conn = NULL; + } + } + + return err; +} + +_IDF_ONLY +int bt_l2cap_chan_send(struct bt_l2cap_chan *chan, struct net_buf *buf) +{ + int err; + + if (chan == NULL || buf == NULL) { + LOG_ERR("L2capChanBufNull[%p][%p]", chan, buf); + return -EINVAL; + } + + LOG_DBG("L2capChanSend[%u]", buf->len); + + /* Caller holds bt_le_host_lock. The Bluedroid adapter posts fire-and-forget; + * returns 0 (buf ownership transferred) or error (buf stays with caller). + * TX completion via ops->sent. */ + if (chan->conn == NULL || chan->conn->state != BT_CONN_CONNECTED) { + LOG_ERR("L2capChanNotConn[%p]", chan->conn); + return -ENOTCONN; + } + + if (buf->len > BT_L2CAP_LE_CHAN(chan)->tx.mtu) { + LOG_ERR("L2capTooLargeBufToSend[%u][%u]", buf->len, + BT_L2CAP_LE_CHAN(chan)->tx.mtu); + return -EMSGSIZE; + } + +#if CONFIG_BT_BLUEDROID_ENABLED + err = bt_le_bluedroid_l2cap_chan_send(chan, buf); +#else + err = bt_le_nimble_l2cap_chan_send(chan, buf); +#endif + + return err; +} + +_IDF_ONLY +int bt_l2cap_server_register(struct bt_l2cap_server *server) +{ + LOG_DBG("L2capSrvReg"); + + if (server == NULL) { + LOG_ERR("L2capSrvNull"); + return -EINVAL; + } + + if (server->accept == NULL) { + LOG_ERR("L2capSrvAcceptNull"); + return -EINVAL; + } + + if (server->sec_level > BT_SECURITY_L4) { + LOG_ERR("L2capInvSecLevel[%u]", server->sec_level); + return -EINVAL; + } + + /* Init path (no concurrency). l2cap_servers is shared with the accept + * lookup in the event handler (iso_task). */ + if (server->psm) { + if (server->psm < L2CAP_LE_PSM_FIXED_START || + server->psm > L2CAP_LE_PSM_DYN_END) { + LOG_ERR("L2capInvPsm[%04x]", server->psm); + return -EINVAL; + } + + /* Check if given PSM is already in use */ + if (l2cap_server_lookup_psm(server->psm)) { + LOG_WRN("L2capPsmReg"); + return -EADDRINUSE; + } + + LOG_DBG("L2capSrvPsm[%04x]", server->psm); + } else { + uint16_t psm; + + for (psm = L2CAP_LE_PSM_DYN_START; + psm <= L2CAP_LE_PSM_DYN_END; psm++) { + if (l2cap_server_lookup_psm(psm) == NULL) { + break; + } + } + + if (psm > L2CAP_LE_PSM_DYN_END) { + LOG_ERR("L2capNoFreeDynPsm"); + return -EADDRNOTAVAIL; + } + + LOG_DBG("L2capPsmNew[%04x]", psm); + + server->psm = psm; + } + + if (server->sec_level < BT_SECURITY_L1) { + server->sec_level = BT_SECURITY_L1; + } + + sys_slist_append(&l2cap_servers, &server->node); + + return 0; +} + +int bt_le_l2cap_ots_init(void) +{ + int err; + + LOG_DBG("L2capOtsInit"); + + /* No unregister API, so a re-init would otherwise find the stale entry and + * fail bt_l2cap_server_register() with -EADDRINUSE. */ + sys_slist_init(&l2cap_servers); + +#if CONFIG_BT_OTS + err = bt_gatt_ots_conn_cb_register(); + if (err) { + LOG_ERR("L2capOtsConnCbRegFail[%d]", err); + return err; + } + + err = bt_gatt_ots_instances_prepare(); + if (err) { + LOG_ERR("L2capPrepOtsInstsFail[%d]", err); + goto unreg_conn_cb; + } +#endif /* CONFIG_BT_OTS */ + +#if CONFIG_BT_OTS_CLIENT + err = bt_gatt_ots_client_conn_cb_register(); + if (err) { + LOG_ERR("L2capOtsCliConnCbRegFail[%d]", err); + goto unreg_conn_cb; + } +#endif /* CONFIG_BT_OTS_CLIENT */ + + err = bt_gatt_ots_l2cap_init(); + if (err) { + LOG_ERR("L2capOtsInitFail[%d]", err); + goto unreg_client_conn_cb; + } + +#if CONFIG_BT_BLUEDROID_ENABLED + err = bt_le_bluedroid_l2cap_init(); +#else + err = bt_le_nimble_l2cap_init(); +#endif + if (err) { + LOG_ERR("L2capOtsAdapterInitFail[%d]", err); + /* bt_l2cap_server_register() has no unregister, so drop the whole list + * rather than leave the OTS PSM claimed by a half-initialised service. */ + sys_slist_init(&l2cap_servers); + goto unreg_client_conn_cb; + } + + return 0; + +unreg_client_conn_cb: +#if CONFIG_BT_OTS_CLIENT + bt_gatt_ots_client_conn_cb_unregister(); +#endif /* CONFIG_BT_OTS_CLIENT */ +unreg_conn_cb: +#if CONFIG_BT_OTS + /* Leaving it registered would fail every later retry with -EEXIST. */ + bt_gatt_ots_conn_cb_unregister(); +#endif /* CONFIG_BT_OTS */ + return err; +} + +void bt_le_l2cap_ots_deinit(void) +{ + LOG_DBG("L2capOtsDeinit"); + + /* Symmetric with the registers in _init: conn_cbs is never reset elsewhere. */ +#if CONFIG_BT_OTS + bt_gatt_ots_conn_cb_unregister(); + + /* Give the instances handed out by bt_ots_free_instance_get() back to the + * pool; the profiles that took them are released right after this. */ + bt_ots_instances_rewind(); +#endif /* CONFIG_BT_OTS */ +#if CONFIG_BT_OTS_CLIENT + bt_gatt_ots_client_conn_cb_unregister(); + + /* Before lib_audio_resources_deinit() frees the bt_ots_client instances + * the table points at - this runs first in bt_le_audio_deinit(). */ + bt_ots_client_unregister_all(); +#endif /* CONFIG_BT_OTS_CLIENT */ + +#if CONFIG_BT_BLUEDROID_ENABLED + bt_le_bluedroid_l2cap_deinit(); +#else + bt_le_nimble_l2cap_deinit(); +#endif +} diff --git a/components/bt/esp_ble_audio/host/services/ots/adapter/l2cap.h b/components/bt/esp_ble_audio/host/services/ots/adapter/l2cap.h new file mode 100644 index 00000000000..4f93eb515e7 --- /dev/null +++ b/components/bt/esp_ble_audio/host/services/ots/adapter/l2cap.h @@ -0,0 +1,119 @@ +/* + * SPDX-FileCopyrightText: 2015-2016 Intel Corporation + * SPDX-FileCopyrightText: 2023 Nordic Semiconductor + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef OTS_ADAPTER_L2CAP_H_ +#define OTS_ADAPTER_L2CAP_H_ + +#include + +#include "sdkconfig.h" + +#include + +#if CONFIG_BT_BLUEDROID_ENABLED +#include "bluedroid/l2cap.h" +#else +#include "nimble/l2cap.h" +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#define L2CAP_LE_SUCCESS 0x0000 +#define L2CAP_LE_ERR_PSM_NOT_SUPP 0x0002 +#define L2CAP_LE_ERR_NO_RESOURCES 0x0004 +#define L2CAP_LE_ERR_AUTHENTICATION 0x0005 +#define L2CAP_LE_ERR_AUTHORIZATION 0x0006 +#define L2CAP_LE_ERR_KEY_SIZE 0x0007 +#define L2CAP_LE_ERR_ENCRYPTION 0x0008 +#define L2CAP_LE_ERR_INVALID_SCID 0x0009 +#define L2CAP_LE_ERR_SCID_IN_USE 0x000A +#define L2CAP_LE_ERR_UNACCEPT_PARAMS 0x000B +#define L2CAP_LE_ERR_INVALID_PARAMS 0x000C + +#define L2CAP_LE_OTS_PSM 0x0025 +#define L2CAP_LE_OTS_MTU MIN(CONFIG_BT_OTS_L2CAP_CHAN_TX_MTU, \ + CONFIG_BT_OTS_L2CAP_CHAN_RX_MTU) +#define L2CAP_OTS_MAX_CHAN CONFIG_BT_MAX_CONN + +enum bt_le_l2cap_event_type { + BT_LE_L2CAP_EVENT_CONNECTED, + BT_LE_L2CAP_EVENT_DISCONNECTED, + BT_LE_L2CAP_EVENT_RECEIVED, + BT_LE_L2CAP_EVENT_SENT, +#if CONFIG_BT_BLUEDROID_ENABLED + /* Bluedroid answers an inbound connect request out-of-band via + * L2CA_ConnectLECocRsp(), so unlike NimBLE it needs no inline verdict. */ + BT_LE_L2CAP_EVENT_ACCEPT, +#endif /* CONFIG_BT_BLUEDROID_ENABLED */ +}; + +struct bt_le_l2cap_event { + uint8_t type; + uint16_t conn_handle; + uint16_t psm; + + union { + struct { + uint16_t tx_cid; + uint16_t tx_mtu; + uint16_t rx_cid; + uint16_t rx_mtu; + } connected; + + struct { + uint8_t *data; /* heap copy, freed after dispatch */ + uint16_t len; + } received; + +#if CONFIG_BT_BLUEDROID_ENABLED + struct { + uint16_t chan_handle; + uint8_t l2cap_id; + } accept; +#endif /* CONFIG_BT_BLUEDROID_ENABLED */ + }; +}; + +#if CONFIG_BT_BLUEDROID_ENABLED +int bt_le_l2cap_post_accept(uint16_t conn_handle, uint16_t psm, + uint16_t chan_handle, uint8_t l2cap_id); +#else /* !CONFIG_BT_BLUEDROID_ENABLED */ +int bt_le_l2cap_accept_safe(uint16_t conn_handle, uint16_t psm, + uint16_t scid, uint16_t mtu, + uint16_t mps, uint16_t credits, + uint16_t *result); +#endif /* !CONFIG_BT_BLUEDROID_ENABLED */ + +int bt_le_l2cap_post_connected(uint16_t conn_handle, uint16_t psm, + uint16_t tx_cid, uint16_t tx_mtu, + uint16_t rx_cid, uint16_t rx_mtu); + +int bt_le_l2cap_post_disconnected(uint16_t conn_handle, uint16_t psm); + +int bt_le_l2cap_post_received(uint16_t conn_handle, uint16_t psm, + const uint8_t *data, uint16_t len); + +/* Neither host reports a per-SDU TX completion for LE CoC, so each adapter + * synthesises it from its write's return code plus its credit-restored event. */ +int bt_le_l2cap_post_sent(uint16_t conn_handle, uint16_t psm); + +void bt_le_l2cap_handle_event(void *data, size_t data_len); + +void bt_le_l2cap_event_free(void *data); + +int bt_le_l2cap_ots_init(void); + +void bt_le_l2cap_ots_deinit(void); + +#ifdef __cplusplus +} +#endif + +#endif /* OTS_ADAPTER_L2CAP_H_ */ diff --git a/components/bt/esp_ble_audio/host/services/ots/adapter/nimble/l2cap.c b/components/bt/esp_ble_audio/host/services/ots/adapter/nimble/l2cap.c new file mode 100644 index 00000000000..46ce21c90cd --- /dev/null +++ b/components/bt/esp_ble_audio/host/services/ots/adapter/nimble/l2cap.c @@ -0,0 +1,506 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#include +#include +#include + +#include <../host/conn_internal.h> + +#include "host/ble_hs.h" +#include "host/ble_l2cap.h" +#include "host/ble_hs_mbuf.h" +#include "host/ble_gap.h" + +#include "../../../nimble/host/src/ble_l2cap_priv.h" + +#include "common/host.h" +#include "common/audio_attr.h" +#include "nimble/hs_error.h" + +#include "ots/adapter/l2cap.h" + +LOG_MODULE_REGISTER(ISO_N2CAP, CONFIG_BT_ISO_LOG_LEVEL); + +_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) +#define OTS_L2CAP_MEM_SIZE OS_MEMPOOL_SIZE(OTS_L2CAP_BUF_COUNT, L2CAP_LE_OTS_MTU * 2) + +/* Staging only - both directions copy through msys mbufs, so nothing here is + * DMA-reached and PSRAM is fine. */ +static BT_AUDIO_EXT_RAM_BSS_ATTR os_membuf_t ots_mem[OTS_L2CAP_MEM_SIZE]; +static BT_AUDIO_EXT_RAM_BSS_ATTR struct os_mempool ots_mbuf_mempool; +static BT_AUDIO_EXT_RAM_BSS_ATTR struct os_mbuf_pool ots_mbuf_pool; + +/* One slot per connection (OTS is single-PSM); pending bridges the async gap + * between ble_l2cap_connect and COC_CONNECTED. + * + * Read without host_lock on both sides: the event callback must not take it + * (NimBLE emits COC events under ble_hs_lock, so that would invert the ISO + * path's host_lock -> ble_hs_lock order). psm is cached so a stale slot never + * has to dereference chan; see chan_send for the residual window. */ +struct ots_chan_slot { + struct ble_l2cap_chan *chan; + uint16_t conn_handle; + uint16_t psm; + bool used; + bool pending; +}; + +static BT_AUDIO_EXT_RAM_BSS_ATTR struct ots_chan_slot ots_chans[L2CAP_OTS_MAX_CHAN]; + +static struct ots_chan_slot *ots_chan_find_used_by_chan(struct ble_l2cap_chan *chan) +{ + for (int i = 0; i < L2CAP_OTS_MAX_CHAN; i++) { + if (ots_chans[i].used && ots_chans[i].chan == chan) { + return &ots_chans[i]; + } + } + return NULL; +} + +static struct ots_chan_slot *ots_chan_find_used_by_conn(uint16_t conn_handle) +{ + for (int i = 0; i < L2CAP_OTS_MAX_CHAN; i++) { + if (ots_chans[i].used && ots_chans[i].conn_handle == conn_handle) { + return &ots_chans[i]; + } + } + return NULL; +} + +static struct ots_chan_slot *ots_chan_find_pending_by_conn(uint16_t conn_handle) +{ + for (int i = 0; i < L2CAP_OTS_MAX_CHAN; i++) { + if (ots_chans[i].pending && ots_chans[i].conn_handle == conn_handle) { + return &ots_chans[i]; + } + } + return NULL; +} + +static struct ots_chan_slot *ots_chan_alloc_pending(uint16_t conn_handle) +{ + for (int i = 0; i < L2CAP_OTS_MAX_CHAN; i++) { + if (!ots_chans[i].used && !ots_chans[i].pending) { + memset(&ots_chans[i], 0, sizeof(ots_chans[i])); + ots_chans[i].conn_handle = conn_handle; + ots_chans[i].pending = true; + return &ots_chans[i]; + } + } + return NULL; +} + +static struct ots_chan_slot *ots_chan_alloc_used(struct ble_l2cap_chan *chan, uint16_t conn_handle) +{ + for (int i = 0; i < L2CAP_OTS_MAX_CHAN; i++) { + if (!ots_chans[i].used && !ots_chans[i].pending) { + memset(&ots_chans[i], 0, sizeof(ots_chans[i])); + ots_chans[i].chan = chan; + ots_chans[i].conn_handle = conn_handle; + ots_chans[i].psm = chan->psm; + ots_chans[i].used = true; + return &ots_chans[i]; + } + } + return NULL; +} + +static void ots_chan_free(struct ots_chan_slot *slot) +{ + if (slot != NULL) { + memset(slot, 0, sizeof(*slot)); + } +} + +static int ots_l2cap_recv_ready(struct ble_l2cap_chan *chan) +{ + struct os_mbuf *sdu_rx; + int rc; + + LOG_DBG("[N]L2capOtsRecvReady"); + + sdu_rx = os_mbuf_get_pkthdr(&ots_mbuf_pool, 0); + if (sdu_rx == NULL) { + LOG_ERR("[N]L2capNoBufForL2capRecv"); + return -ENOMEM; + } + + rc = ble_l2cap_recv_ready(chan, sdu_rx); + if (rc) { + LOG_ERR("[N]L2capRecvFail[%d]", rc); + + os_mbuf_free_chain(sdu_rx); + return rc; + } + + return 0; +} + +static int ots_l2cap_event_cb(struct ble_l2cap_event *event, void *arg) +{ + struct ble_l2cap_chan_info chan_info; + uint16_t result = 0; + size_t sdu_len; + uint16_t psm; + uint8_t *sdu; + int err; + + LOG_DBG("[N]L2capOtsEvtCb[%u]", event->type); + + switch (event->type) { + case BLE_L2CAP_EVENT_COC_CONNECTED: { + struct ots_chan_slot *slot; + + if (event->connect.status) { + LOG_ERR("[N]L2capCocConnectFail[%d]", event->connect.status); + ots_chan_free(ots_chan_find_pending_by_conn(event->connect.conn_handle)); + /* Unwind the optimistic chan_add done by bt_l2cap_chan_connect()/ + * l2cap_accept(), else the upper layer waits for a result forever. */ + bt_le_l2cap_post_disconnected(event->connect.conn_handle, L2CAP_LE_OTS_PSM); + return 0; + } + + if (ots_chan_find_used_by_chan(event->connect.chan) != NULL) { + LOG_ERR("[N]L2capCocChanExist"); + return 0; + } + + /* Outgoing connect: reuse the pending slot reserved at chan_connect. + * Incoming accept: no pending slot, allocate a fresh one. */ + slot = ots_chan_find_pending_by_conn(event->connect.conn_handle); + if (slot != NULL) { + slot->pending = false; + slot->chan = event->connect.chan; + slot->psm = event->connect.chan->psm; + slot->used = true; + } else { + slot = ots_chan_alloc_used(event->connect.chan, event->connect.conn_handle); + if (slot == NULL) { + LOG_ERR("[N]L2capCocNoSlot"); + bt_le_l2cap_post_disconnected(event->connect.conn_handle, L2CAP_LE_OTS_PSM); + return -EIO; + } + } + + if (ble_l2cap_get_chan_info(event->connect.chan, &chan_info)) { + LOG_ERR("[N]L2capCocGetChanInfoFail"); + /* Roll back the slot so the next COC_CONNECTED isn't refused. */ + ots_chan_free(ots_chan_find_used_by_chan(event->connect.chan)); + bt_le_l2cap_post_disconnected(event->connect.conn_handle, L2CAP_LE_OTS_PSM); + return -EIO; + } + + LOG_INF("[N]L2capCocConnect[%u][%04x][%04x][%04x][%u][%u][%u][%u]", + event->connect.conn_handle, chan_info.scid, chan_info.dcid, + chan_info.psm, chan_info.our_l2cap_mtu, chan_info.peer_l2cap_mtu, + chan_info.our_coc_mtu, chan_info.peer_coc_mtu); + + bt_le_l2cap_post_connected(event->connect.conn_handle, chan_info.psm, + chan_info.dcid, chan_info.peer_coc_mtu, + chan_info.scid, chan_info.our_coc_mtu); + return 0; + } + + case BLE_L2CAP_EVENT_COC_DISCONNECTED: { + struct ots_chan_slot *slot; + + slot = ots_chan_find_used_by_chan(event->disconnect.chan); + if (slot == NULL) { + LOG_ERR("[N]L2capDisconnectInvCocChan"); + return 0; + } + + psm = slot->psm; + /* Drop the slot before posting: NimBLE frees the chan once this returns, + * and the queue pairing then makes iso_task not find a dead chan. */ + ots_chan_free(slot); + + LOG_INF("[N]L2capCocDisconnect[%u][%04x]", event->disconnect.conn_handle, psm); + + bt_le_l2cap_post_disconnected(event->disconnect.conn_handle, psm); + return 0; + } + + case BLE_L2CAP_EVENT_COC_ACCEPT: + /* LE CoC allows asymmetric MTUs; peer_sdu_size is the peer's RX MTU + * (our TX ceiling) — harmless. (PTS uses 1024 > our 256; rejecting broke SCP.) */ + + LOG_DBG("[N]L2capCocAccept[%u][%04x][%04x][%u][%u][%u][%u]", + event->accept.conn_handle, event->accept.chan->psm, + event->accept.chan->dcid, event->accept.chan->coc_tx.mtu, + event->accept.chan->peer_coc_mps, event->accept.peer_sdu_size, + event->accept.chan->coc_tx.credits); + + err = bt_le_l2cap_accept_safe(event->accept.conn_handle, + event->accept.chan->psm, + event->accept.chan->dcid, + event->accept.chan->coc_tx.mtu, + event->accept.chan->peer_coc_mps, + event->accept.chan->coc_tx.credits, + &result); + if (err) { + return err; + } + + ARG_UNUSED(result); + + return ots_l2cap_recv_ready(event->accept.chan); + + case BLE_L2CAP_EVENT_COC_DATA_RECEIVED: { + if (event->receive.sdu_rx == NULL) { + LOG_ERR("[N]L2capRecvNullSdu"); + return 0; + } + + LOG_DBG("[N]L2capCocRecv[%u][%04x][%u]", + event->receive.conn_handle, event->receive.chan->psm, + event->receive.sdu_rx->om_len); + + sdu_len = OS_MBUF_PKTLEN(event->receive.sdu_rx); + + sdu = bt_le_ext_calloc(1, sdu_len); + if (sdu == NULL) { + LOG_ERR("[N]L2capRecvNoMem[%u]", (unsigned)sdu_len); + /* Drop the SDU but keep the channel usable: hand a fresh RX buffer + * back so the peer can retry rather than stalling on credits. */ + os_mbuf_free_chain(event->receive.sdu_rx); + return ots_l2cap_recv_ready(event->receive.chan); + } + + err = os_mbuf_copydata(event->receive.sdu_rx, 0, sdu_len, sdu); + if (err) { + LOG_ERR("[N]L2capRecvCopyFail[%d]", err); + } else { + bt_le_l2cap_post_received(event->receive.conn_handle, + event->receive.chan->psm, sdu, sdu_len); + } + + os_mbuf_free_chain(event->receive.sdu_rx); + free(sdu); + + return ots_l2cap_recv_ready(event->receive.chan); + } + + case BLE_L2CAP_EVENT_COC_TX_UNSTALLED: { + LOG_DBG("[N]L2capCocTxUnstalled[%u][%d]", + event->tx_unstalled.conn_handle, event->tx_unstalled.status); + + /* Report completion even on failure: ops->sent carries no status, and + * silence would leave the upper layer's TX pending forever. */ + if (event->tx_unstalled.status) { + LOG_ERR("[N]L2capCocTxFail[%d]", event->tx_unstalled.status); + } + + bt_le_l2cap_post_sent(event->tx_unstalled.conn_handle, + event->tx_unstalled.chan->psm); + return 0; + } + + default: + return 0; + } +} + +int bt_le_nimble_l2cap_chan_connect(uint16_t conn_handle) +{ + struct os_mbuf *sdu_rx; + struct ots_chan_slot *slot; + int rc; + + /* Reject if this connection already has an OTS channel. ots_chans is a + * cache under ble_hs_lock, so no host_lock here. */ + for (int i = 0; i < L2CAP_OTS_MAX_CHAN; i++) { + if ((ots_chans[i].used || ots_chans[i].pending) && + ots_chans[i].conn_handle == conn_handle) { + LOG_WRN("[N]L2capOtsChanExist[%u]", conn_handle); + return -EALREADY; + } + } + slot = ots_chan_alloc_pending(conn_handle); + if (slot == NULL) { + LOG_ERR("[N]L2capOtsNoSlot[%u]", conn_handle); + return -ENOMEM; + } + + sdu_rx = os_mbuf_get_pkthdr(&ots_mbuf_pool, 0); + if (sdu_rx == NULL) { + ots_chan_free(slot); + LOG_ERR("[N]L2capNoBufForConnect"); + return -ENOMEM; + } + + rc = ble_l2cap_connect(conn_handle, L2CAP_LE_OTS_PSM, L2CAP_LE_OTS_MTU, + sdu_rx, ots_l2cap_event_cb, NULL); + if (rc) { + /* NimBLE takes ownership of sdu_rx and frees it on every failure path */ + ots_chan_free(slot); + LOG_ERR("[N]L2capConnectFail[%d]", rc); + return nimble_err_to_errno(rc); + } + + return 0; +} + +int bt_le_nimble_l2cap_chan_disconnect(struct bt_l2cap_chan *chan) +{ + struct ots_chan_slot *slot; + struct ble_l2cap_chan *ble_chan = NULL; + uint16_t conn_handle_h; + struct ble_gap_conn_desc desc; + int rc; + + slot = ots_chan_find_used_by_conn(chan->conn->handle); + if (slot == NULL) { + LOG_WRN("[N]L2capNoOtsChan"); + return -ENOTCONN; + } + ble_chan = slot->chan; + conn_handle_h = slot->conn_handle; + + /* Best-effort liveness check: narrows the window where ble_chan could be + * freed between this lookup and ble_l2cap_disconnect acquiring ble_hs_lock. */ + if (ble_gap_conn_find(conn_handle_h, &desc) != 0) { + LOG_WRN("[N]L2capDiscConnGone[%u]", conn_handle_h); + return -ENOTCONN; + } + + rc = ble_l2cap_disconnect(ble_chan); + if (rc) { + LOG_ERR("[N]L2capDisconnectFail[%d]", rc); + return nimble_err_to_errno(rc); + } + + return 0; +} + +int bt_le_nimble_l2cap_chan_send(struct bt_l2cap_chan *chan, struct net_buf *buf) +{ + struct ots_chan_slot *slot; + struct ble_l2cap_chan *ble_chan = NULL; + uint16_t conn_handle_h, psm_h; + struct os_mbuf *sdu_tx; + struct ble_gap_conn_desc desc; + int rc; + + slot = ots_chan_find_used_by_conn(chan->conn->handle); + if (slot == NULL) { + LOG_WRN("[N]L2capNoOtsChan"); + return -ENOTCONN; + } + ble_chan = slot->chan; + conn_handle_h = slot->conn_handle; + psm_h = slot->psm; + + /* Best-effort liveness check: ble_gap_conn_find takes ble_hs_lock, so the + * conn can't be torn down while it runs. It narrows but does not close the + * window before ble_l2cap_send — that reads chan->coc_tx.mtu before taking + * ble_hs_lock, so a concurrently freed chan is still dereferenced there. */ + if (ble_gap_conn_find(conn_handle_h, &desc) != 0) { + LOG_WRN("[N]L2capSendConnGone[%u]", conn_handle_h); + return -ENOTCONN; + } + + sdu_tx = os_mbuf_get_pkthdr(&ots_mbuf_pool, 0); + if (sdu_tx == NULL) { + LOG_ERR("[N]L2capNoBufForSend"); + return -ENOMEM; + } + + rc = os_mbuf_append(sdu_tx, buf->data, buf->len); + if (rc) { + LOG_ERR("[N]L2capAppendBufFail[%d]", rc); + os_mbuf_free_chain(sdu_tx); + return -EIO; + } + + rc = ble_l2cap_send(ble_chan, sdu_tx); + if (rc && rc != BLE_HS_ESTALLED) { + 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 { + /* NimBLE only consumes the mbuf on success; free it on any failure. */ + LOG_ERR("[N]L2capSendInternalFail[%d]", rc); + os_mbuf_free_chain(sdu_tx); + } + + return nimble_err_to_errno(rc); + } + + /* Payload now in sdu_tx; release caller's buf else ot_chan_tx_pool + * (1 entry) leaks. */ + net_buf_unref(buf); + + if (rc == BLE_HS_ESTALLED) { + /* Out of credits: queued, reported by COC_TX_UNSTALLED. If the conn + * drops while stalled, NimBLE fires only COC_DISCONNECTED; the OTS + * disconnected callback cleans up tx state, so the missing sent is + * harmless. */ + LOG_DBG("[N]L2capMoreCreditsForSend"); + return 0; + } + + /* Fully transmitted; NimBLE raises COC_TX_UNSTALLED only for a stalled SDU, + * so this is the sole completion signal. */ + bt_le_l2cap_post_sent(conn_handle_h, psm_h); + + return 0; +} + +int bt_le_nimble_l2cap_init(void) +{ + int rc; + + memset(ots_chans, 0, sizeof(ots_chans)); + + rc = os_mempool_init(&ots_mbuf_mempool, OTS_L2CAP_BUF_COUNT, L2CAP_LE_OTS_MTU * 2, ots_mem, "ots_pool"); + if (rc) { + LOG_ERR("[N]L2capInitOtsMempoolFail[%d]", rc); + return rc; + } + + rc = os_mbuf_pool_init(&ots_mbuf_pool, &ots_mbuf_mempool, L2CAP_LE_OTS_MTU, OTS_L2CAP_BUF_COUNT); + if (rc) { + LOG_ERR("[N]L2capInitOtsMbufPoolFail[%d]", rc); + return rc; + } + +#if CONFIG_BT_OTS + rc = ble_l2cap_create_server(L2CAP_LE_OTS_PSM, L2CAP_LE_OTS_MTU, ots_l2cap_event_cb, NULL); + /* NimBLE has no delete-server API, so the registration from a previous cycle + * survives deinit. It still points at ots_l2cap_event_cb, so treat EALREADY + * as success - failing here would make any re-init impossible. */ + if (rc == BLE_HS_EALREADY) { + LOG_WRN("[N]L2capL2capSrvExist"); + } else if (rc) { + LOG_ERR("[N]L2capCreateL2capSrvFail[%d]", rc); + return rc; + } +#endif /* CONFIG_BT_OTS */ + + return 0; +} + +void bt_le_nimble_l2cap_deinit(void) +{ + LOG_DBG("[N]L2capDeinit"); + + /* Nothing to release: ots_mem is static BSS and os_mempool_init re-registers + * an existing pool by name, so re-init needs no teardown here. The CoC server + * cannot be dropped either - NimBLE exposes no delete-server API - which is + * why init tolerates BLE_HS_EALREADY. */ + + memset(ots_chans, 0, sizeof(ots_chans)); +} diff --git a/components/bt/esp_ble_audio/host/services/ots/ots.c b/components/bt/esp_ble_audio/host/services/ots/ots.c index 292bc9e6be8..e1b2f93c916 100644 --- a/components/bt/esp_ble_audio/host/services/ots/ots.c +++ b/components/bt/esp_ble_audio/host/services/ots/ots.c @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Nordic Semiconductor ASA + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -121,7 +122,7 @@ static ssize_t ots_feature_read(struct bt_conn *conn, { struct bt_ots *ots = (struct bt_ots *) attr->user_data; - LOG_DBG("OTS Feature GATT Read Operation"); + LOG_DBG("OtsFeatRd"); return bt_gatt_attr_read(conn, attr, buf, len, offset, &ots->features, sizeof(ots->features)); @@ -133,15 +134,15 @@ static ssize_t ots_obj_name_read(struct bt_conn *conn, { struct bt_ots *ots = (struct bt_ots *) attr->user_data; - LOG_DBG("OTS Object Name GATT Read Operation"); + LOG_DBG("OtsObjNameRd"); if (!ots->cur_obj) { - LOG_DBG("No Current Object selected in OTS!"); + LOG_WRN("OtsCurObjNotSel"); return BT_GATT_ERR(BT_GATT_OTS_OBJECT_NOT_SELECTED); } if (!ots->cur_obj->metadata.name) { - LOG_DBG("Current Object has no name!"); + LOG_WRN("OtsCurObjNoName"); return BT_GATT_ERR(BT_GATT_OTS_OBJECT_NOT_SELECTED); } @@ -161,26 +162,26 @@ ssize_t ots_obj_name_write(struct bt_conn *conn, int rc = 0; char name[CONFIG_BT_OTS_OBJ_MAX_NAME_LEN + 1]; - LOG_DBG("OTS Object Name GATT Write Operation"); + LOG_DBG("OtsObjNameWr"); if (!ots->cur_obj) { - LOG_DBG("No Current Object selected in OTS!"); + LOG_WRN("OtsCurObjNotSel"); return BT_GATT_ERR(BT_GATT_OTS_OBJECT_NOT_SELECTED); } if (IS_ENABLED(CONFIG_BT_OTS_DIR_LIST_OBJ) && ots->cur_obj->id == OTS_OBJ_ID_DIR_LIST) { - LOG_DBG("Rejecting name write for the directory list object."); + LOG_WRN("OtsDirListNameWrRej"); return BT_GATT_ERR(BT_GATT_OTS_WRITE_REQUEST_REJECTED); } if (offset > 0) { - LOG_DBG("Rejecting a long write, offset must be 0!"); + LOG_WRN("OtsNameWrInvOft"); return BT_GATT_ERR(BT_GATT_OTS_WRITE_REQUEST_REJECTED); } if (len == 0 || len > CONFIG_BT_OTS_OBJ_MAX_NAME_LEN) { - LOG_DBG("Invalid object name length!"); + LOG_WRN("OtsInvObjNameLen"); return BT_GATT_ERR(BT_GATT_OTS_WRITE_REQUEST_REJECTED); } @@ -191,7 +192,7 @@ ssize_t ots_obj_name_write(struct bt_conn *conn, rc = bt_gatt_ots_obj_manager_first_obj_get(ots->obj_manager, &obj); while (rc == 0) { if (obj != ots->cur_obj && strcmp(name, obj->metadata.name) == 0) { - LOG_DBG("Object name is duplicated!"); + LOG_WRN("OtsObjNameDup"); return BT_GATT_ERR(BT_GATT_OTS_OBJECT_NAME_ALREADY_EXISTS); } rc = bt_gatt_ots_obj_manager_next_obj_get(ots->obj_manager, obj, &obj); @@ -220,10 +221,10 @@ static ssize_t ots_obj_type_read(struct bt_conn *conn, struct bt_ots *ots = (struct bt_ots *) attr->user_data; struct bt_ots_obj_metadata *obj_meta; - LOG_DBG("OTS Object Type GATT Read Operation"); + LOG_DBG("OtsObjTypeRd"); if (!ots->cur_obj) { - LOG_DBG("No Current Object selected in OTS!"); + LOG_WRN("OtsCurObjNotSel"); return BT_GATT_ERR(BT_GATT_OTS_OBJECT_NOT_SELECTED); } @@ -248,10 +249,10 @@ static ssize_t ots_obj_size_read(struct bt_conn *conn, { struct bt_ots *ots = (struct bt_ots *) attr->user_data; - LOG_DBG("OTS Object Size GATT Read Operation"); + LOG_DBG("OtsObjSizeRd"); if (!ots->cur_obj) { - LOG_DBG("No Current Object selected in OTS!"); + LOG_WRN("OtsCurObjNotSel"); return BT_GATT_ERR(BT_GATT_OTS_OBJECT_NOT_SELECTED); } @@ -268,10 +269,10 @@ static ssize_t ots_obj_id_read(struct bt_conn *conn, uint8_t id[BT_OTS_OBJ_ID_SIZE]; char id_str[BT_OTS_OBJ_ID_STR_LEN]; - LOG_DBG("OTS Object ID GATT Read Operation"); + LOG_DBG("OtsObjIdRd"); if (!ots->cur_obj) { - LOG_DBG("No Current Object selected in OTS!"); + LOG_WRN("OtsCurObjNotSel"); return BT_GATT_ERR(BT_GATT_OTS_OBJECT_NOT_SELECTED); } @@ -279,7 +280,7 @@ static ssize_t ots_obj_id_read(struct bt_conn *conn, bt_ots_obj_id_to_str(ots->cur_obj->id, id_str, sizeof(id_str)); - LOG_DBG("Current Object ID: %s", id_str); + LOG_DBG("OtsCurObjId[%s]", id_str); return bt_gatt_attr_read(conn, attr, buf, len, offset, id, sizeof(id)); } @@ -290,10 +291,10 @@ static ssize_t ots_obj_prop_read(struct bt_conn *conn, { struct bt_ots *ots = (struct bt_ots *) attr->user_data; - LOG_DBG("OTS Object Properties GATT Read Operation"); + LOG_DBG("OtsObjPropRd"); if (!ots->cur_obj) { - LOG_DBG("No Current Object selected in OTS!"); + LOG_WRN("OtsCurObjNotSel"); return BT_GATT_ERR(BT_GATT_OTS_OBJECT_NOT_SELECTED); } @@ -310,15 +311,25 @@ int bt_ots_obj_add_internal(struct bt_ots *ots, struct bt_conn *conn, struct bt_gatt_ots_object *new_obj; struct bt_ots_obj_created_desc created_desc; + /* Directory listing Object Type is 16- or 128-bit only (same as OACP Create). */ + switch (param->type.uuid.type) { + case BT_UUID_TYPE_16: + case BT_UUID_TYPE_128: + break; + default: + LOG_ERR("OtsObjAddInvType[%u]", param->type.uuid.type); + return -EINVAL; + } + if (IS_ENABLED(CONFIG_BT_OTS_DIR_LIST_OBJ) && ots->dir_list && !bt_ots_dir_list_is_idle(ots->dir_list)) { - LOG_DBG("Directory Listing Object is being read"); + LOG_DBG("OtsDirListBusy"); return -EBUSY; } err = bt_gatt_ots_obj_manager_obj_add(ots->obj_manager, &new_obj); if (err) { - LOG_ERR("No space available in the object manager"); + LOG_ERR("OtsObjMgrNoSpace"); return err; } @@ -330,11 +341,14 @@ int bt_ots_obj_add_internal(struct bt_ots *ots, struct bt_conn *conn, if (err) { (void)bt_gatt_ots_obj_manager_obj_delete(new_obj); + if (IS_ENABLED(CONFIG_BT_OTS_DIR_LIST_OBJ) && ots->dir_list) { + bt_ots_dir_list_content_changed(ots->dir_list, ots->obj_manager); + } return err; } if (!ots_obj_validate_prop_against_oacp(created_desc.props, ots->features.oacp)) { - LOG_ERR("Object properties (0x%04X) are not a subset of OACP (0x%04X)", + LOG_ERR("OtsObjPropNotSubsetOacp[%04x][%04x]", created_desc.props, ots->features.oacp); (void)bt_ots_obj_delete(ots, new_obj->id); @@ -342,28 +356,36 @@ int bt_ots_obj_add_internal(struct bt_ots *ots, struct bt_conn *conn, } if (created_desc.name == NULL) { - LOG_ERR("Object name must be set by application after object creation."); + LOG_ERR("OtsObjNameNotSet"); (void)bt_ots_obj_delete(ots, new_obj->id); return -ECANCELED; } if (created_desc.size.alloc < param->size) { - LOG_ERR("Object allocated size must >= requested size."); + LOG_ERR("OtsObjAllocSizeTooSmall"); (void)bt_ots_obj_delete(ots, new_obj->id); return -ECANCELED; } } else { /* obj_created callback is required to populate the descriptor */ - LOG_ERR("obj_created callback is not set"); + LOG_ERR("OtsObjCreatedCbNotSet"); (void)bt_gatt_ots_obj_manager_obj_delete(new_obj); + + if (IS_ENABLED(CONFIG_BT_OTS_DIR_LIST_OBJ) && ots->dir_list) { + bt_ots_dir_list_content_changed(ots->dir_list, ots->obj_manager); + } return -EINVAL; } new_obj->metadata.type = param->type; - new_obj->metadata.name = created_desc.name; + /* Own the name: a client name write copies into metadata.name, and the + * application's buffer may be read-only or shorter than the max name. */ + strncpy(new_obj->metadata.name_c, created_desc.name, CONFIG_BT_OTS_OBJ_MAX_NAME_LEN); + new_obj->metadata.name_c[CONFIG_BT_OTS_OBJ_MAX_NAME_LEN] = '\0'; + new_obj->metadata.name = new_obj->metadata.name_c; new_obj->metadata.size = created_desc.size; new_obj->metadata.props = created_desc.props; @@ -386,7 +408,7 @@ int bt_ots_obj_add(struct bt_ots *ots, const struct bt_ots_obj_add_param *param) } if (obj->metadata.name == NULL) { - LOG_ERR("Object name is NULL"); + LOG_ERR("OtsObjNameNull"); (void)bt_ots_obj_delete(ots, obj->id); return -ECANCELED; @@ -394,14 +416,14 @@ int bt_ots_obj_add(struct bt_ots *ots, const struct bt_ots_obj_add_param *param) name_len = strlen(obj->metadata.name); if (name_len == 0 || name_len > CONFIG_BT_OTS_OBJ_MAX_NAME_LEN) { - LOG_ERR("Invalid name length %zu", name_len); + LOG_ERR("OtsInvNameLen[%zu]", name_len); (void)bt_ots_obj_delete(ots, obj->id); return -ECANCELED; } if (obj->metadata.size.cur > param->size) { - LOG_ERR("Object current size must be less than or equal to requested size."); + LOG_ERR("OtsObjCurSizeTooBig"); (void)bt_ots_obj_delete(ots, obj->id); return -ECANCELED; @@ -416,7 +438,7 @@ int bt_ots_obj_delete(struct bt_ots *ots, uint64_t id) struct bt_gatt_ots_object *obj; CHECKIF(!BT_OTS_VALID_OBJ_ID(id)) { - LOG_DBG("Invalid object ID 0x%016llx", id); + LOG_WRN("OtsInvObjId[%016llx]", id); return -EINVAL; } @@ -432,7 +454,7 @@ int bt_ots_obj_delete(struct bt_ots *ots, uint64_t id) if (IS_ENABLED(CONFIG_BT_OTS_DIR_LIST_OBJ) && ots->dir_list && !bt_ots_dir_list_is_idle(ots->dir_list)) { - LOG_DBG("Directory Listing Object is being read"); + LOG_DBG("OtsDirListBusy"); return -EBUSY; } @@ -448,6 +470,11 @@ int bt_ots_obj_delete(struct bt_ots *ots, uint64_t id) return err; } + if (IS_ENABLED(CONFIG_BT_OTS_DIR_LIST_OBJ) && ots->dir_list) { + /* Object removed from the list: drop stale anchor and refresh size. */ + bt_ots_dir_list_content_changed(ots->dir_list, ots->obj_manager); + } + if (ots->cur_obj == obj) { ots->cur_obj = NULL; } @@ -464,18 +491,48 @@ void *bt_ots_svc_decl_get(struct bt_ots *ots) static void oacp_indicate_work_handler(struct k_work *work) { - struct bt_gatt_ots_indicate *ind = CONTAINER_OF(work, struct bt_gatt_ots_indicate, work); + struct k_work_delayable *dwork = k_work_delayable_from_work(work); + struct bt_gatt_ots_indicate *ind = + CONTAINER_OF(dwork, struct bt_gatt_ots_indicate, work); struct bt_ots *ots = CONTAINER_OF(ind, struct bt_ots, oacp_ind); + int err; - bt_gatt_indicate(NULL, &ots->oacp_ind.params); + if (!ind->conn) { + LOG_WRN("OtsOacpIndNoConn"); + ots->oacp_ind.ind_in_flight = false; + return; + } + + LOG_INF("OtsOacpInd[%04x]", ind->conn->handle); + + err = bt_gatt_indicate(ind->conn, &ots->oacp_ind.params); + if (err) { + LOG_ERR("OtsOacpIndFail[%04x][%d]", ind->conn->handle, err); + ots->oacp_ind.ind_in_flight = false; + } } static void olcp_indicate_work_handler(struct k_work *work) { - struct bt_gatt_ots_indicate *ind = CONTAINER_OF(work, struct bt_gatt_ots_indicate, work); + struct k_work_delayable *dwork = k_work_delayable_from_work(work); + struct bt_gatt_ots_indicate *ind = + CONTAINER_OF(dwork, struct bt_gatt_ots_indicate, work); struct bt_ots *ots = CONTAINER_OF(ind, struct bt_ots, olcp_ind); + int err; - bt_gatt_indicate(NULL, &ots->olcp_ind.params); + if (!ind->conn) { + LOG_WRN("OtsOlcpIndNoConn"); + ots->olcp_ind.ind_in_flight = false; + return; + } + + LOG_INF("OtsOlcpInd[%04x]", ind->conn->handle); + + err = bt_gatt_indicate(ind->conn, &ots->olcp_ind.params); + if (err) { + LOG_ERR("OtsOlcpIndFail[%04x][%d]", ind->conn->handle, err); + ots->olcp_ind.ind_in_flight = false; + } } int bt_ots_init(struct bt_ots *ots, @@ -506,6 +563,9 @@ int bt_ots_init(struct bt_ots *ots, /* Check OACP supported features against Kconfig. */ if (ots_init->features.oacp & (~((uint32_t) OACP_FEAT))) { + /* cb is what bt_ots_instances_rewind() reads to decide there is an init + * to reverse, so every bail after the assignment above has to undo it. */ + ots->cb = NULL; return -ENOTSUP; } @@ -514,25 +574,32 @@ int bt_ots_init(struct bt_ots *ots, "Object creation requires object write to be supported"); ots->features.oacp = ots_init->features.oacp; - LOG_DBG("OACP features: 0x%04X", ots->features.oacp); + LOG_DBG("OtsOacpFeat[%04x]", ots->features.oacp); /* Check OLCP supported features against Kconfig. */ if (ots_init->features.olcp & (~((uint32_t) OLCP_FEAT))) { + ots->cb = NULL; return -ENOTSUP; } ots->features.olcp = ots_init->features.olcp; - LOG_DBG("OLCP features: 0x%04X", ots->features.olcp); + LOG_DBG("OtsOlcpFeat[%04x]", ots->features.olcp); /* Register L2CAP context. */ err = bt_gatt_ots_l2cap_register(&ots->l2cap); if (err) { + ots->cb = NULL; return err; } err = bt_gatt_service_register(ots->service); if (err) { - bt_gatt_ots_l2cap_unregister(&ots->l2cap); + int unreg_err = bt_gatt_ots_l2cap_unregister(&ots->l2cap); + if (unreg_err) { + LOG_ERR("OtsL2capUnregFail[%d]", unreg_err); + } + + ots->cb = NULL; return err; } @@ -540,10 +607,10 @@ int bt_ots_init(struct bt_ots *ots, bt_ots_dir_list_init(&ots->dir_list, ots->obj_manager); } - k_work_init(&ots->oacp_ind.work, oacp_indicate_work_handler); - k_work_init(&ots->olcp_ind.work, olcp_indicate_work_handler); + k_work_init_delayable(&ots->oacp_ind.work, oacp_indicate_work_handler); + k_work_init_delayable(&ots->olcp_ind.work, olcp_indicate_work_handler); - LOG_DBG("Initialized OTS"); + LOG_DBG("OtsInit"); return 0; } @@ -616,6 +683,7 @@ static void ots_delete_empty_name_objects(struct bt_ots *ots, struct bt_conn *co char id_str[BT_OTS_OBJ_ID_STR_LEN]; struct bt_gatt_ots_object *obj; struct bt_gatt_ots_object *next_obj; + bool deleted = false; int err; err = bt_gatt_ots_obj_manager_first_obj_get(ots->obj_manager, &next_obj); @@ -629,18 +697,43 @@ static void ots_delete_empty_name_objects(struct bt_ots *ots, struct bt_conn *co if (strlen(obj->metadata.name) == 0) { bt_ots_obj_id_to_str(obj->id, id_str, sizeof(id_str)); - LOG_DBG("Deleting object with %s ID due to empty name", id_str); + LOG_DBG("OtsDelEmptyNameObj[%s]", id_str); if (ots->cb && ots->cb->obj_deleted) { ots->cb->obj_deleted(ots, conn, obj->id); } if (bt_gatt_ots_obj_manager_obj_delete(obj)) { - LOG_ERR("Failed to remove object with %s ID from object manager", + LOG_ERR("OtsObjMgrDelFail[%s]", id_str); + } else { + deleted = true; } } } + + /* Refresh once after the loop: while empty-name objects are being deleted + * the manager still holds not-yet-removed ones, and dir_list_update_size + * would assert (name_len > 0) on them. + */ + if (deleted && IS_ENABLED(CONFIG_BT_OTS_DIR_LIST_OBJ) && ots->dir_list) { + bt_ots_dir_list_content_changed(ots->dir_list, ots->obj_manager); + } +} + +static void ots_ind_on_disconnect(struct bt_gatt_ots_indicate *ind, struct bt_conn *conn) +{ + if (ind->conn != conn) { + LOG_INF("OtsIndSkipDisc[%04x][%04x]", + ind->conn ? ind->conn->handle : 0xFFFF, conn->handle); + return; + } + + LOG_INF("OtsIndClrOnDisc[%04x]", conn->handle); + + (void)k_work_cancel_delayable(&ind->work); + ind->ind_in_flight = false; + ind->conn = NULL; } static void ots_conn_disconnected(struct bt_conn *conn, uint8_t reason) @@ -652,7 +745,10 @@ static void ots_conn_disconnected(struct bt_conn *conn, uint8_t reason) index < instance_cnt; instance++, index++) { - LOG_DBG("Processing disconnect for OTS instance %u", index); + LOG_DBG("OtsInstDisconnect[%u]", index); + + ots_ind_on_disconnect(&instance->oacp_ind, conn); + ots_ind_on_disconnect(&instance->olcp_ind, conn); if (instance->cur_obj != NULL) { __ASSERT(instance->cur_obj->state.type == BT_GATT_OTS_OBJECT_IDLE_STATE, @@ -678,7 +774,39 @@ struct bt_ots *bt_ots_free_instance_get(void) return &BT_GATT_OTS_INSTANCE_LIST_START[instance_cnt++]; } -static int bt_gatt_ots_instances_prepare(void) +/* Both pools only count up, so without this a second init finds them exhausted + * (McsNoFreeOtsInst / OtsObjMgrInstUnavail). Safe here: the profile that took an + * instance has dropped its reference by now. */ +void bt_ots_instances_rewind(void) +{ + for (struct bt_ots *instance = BT_GATT_OTS_INSTANCE_LIST_START; + instance != BT_GATT_OTS_INSTANCE_LIST_END; instance++) { + /* cb is the marker for "bt_ots_init() ran here": it is set on entry and + * cleared again on every failure path, so it is exactly the set of + * instances with something to reverse. Reverse order of that function. */ + if (instance->cb != NULL) { + k_work_deinit_delayable(&instance->olcp_ind.work); + k_work_deinit_delayable(&instance->oacp_ind.work); + + if (IS_ENABLED(CONFIG_BT_OTS_DIR_LIST_OBJ)) { + bt_ots_dir_list_deinit(&instance->dir_list); + } + + (void)bt_gatt_service_unregister(instance->service); + (void)bt_gatt_ots_l2cap_unregister(&instance->l2cap); + instance->cb = NULL; + } + + if (instance->obj_manager != NULL) { + bt_gatt_ots_obj_manager_release(instance->obj_manager); + instance->obj_manager = NULL; + } + } + + instance_cnt = 0; +} + +int bt_gatt_ots_instances_prepare(void) { uint32_t index; struct bt_ots *instance; @@ -690,7 +818,7 @@ static int bt_gatt_ots_instances_prepare(void) instance->obj_manager = bt_gatt_ots_obj_manager_assign(); if (!instance->obj_manager) { - LOG_ERR("OTS Object manager instance not available"); + LOG_ERR("OtsObjMgrInstUnavail"); return -ENOMEM; } @@ -709,10 +837,12 @@ static int bt_gatt_ots_instances_prepare(void) return 0; } -SYS_INIT(bt_gatt_ots_instances_prepare, APPLICATION, - CONFIG_KERNEL_INIT_PRIORITY_DEFAULT); - int bt_gatt_ots_conn_cb_register(void) { return bt_conn_cb_register_safe((void *)&bt_conn_cb_conn_callbacks); } + +void bt_gatt_ots_conn_cb_unregister(void) +{ + (void)bt_conn_cb_unregister_safe((void *)&bt_conn_cb_conn_callbacks); +} diff --git a/components/bt/esp_ble_audio/host/services/ots/ots_client.c b/components/bt/esp_ble_audio/host/services/ots/ots_client.c index c563561ba2a..4adf1e0cd77 100644 --- a/components/bt/esp_ble_audio/host/services/ots/ots_client.c +++ b/components/bt/esp_ble_audio/host/services/ots/ots_client.c @@ -2,6 +2,7 @@ * @brief Bluetooth Object Transfer Client * * SPDX-FileCopyrightText: 2020-2022 Nordic Semiconductor ASA + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -131,13 +132,14 @@ static void read_next_metadata(struct bt_conn *conn, static int read_attr(struct bt_conn *conn, struct bt_otc_internal_instance_t *inst, uint16_t handle, bt_gatt_read_func_t cb); +static void oacp_clr_cur_inst(struct bt_otc_internal_instance_t *inst); /* L2CAP callbacks */ static void tx_done(struct bt_gatt_ots_l2cap *l2cap_ctx, struct bt_conn *conn) { /* Not doing any writes yet */ - LOG_ERR("Unexpected call, context: %p, conn: %p", l2cap_ctx, (void *)conn); + LOG_ERR("OtsCliUnexpCall"); } static void write_obj_tx_done(struct bt_gatt_ots_l2cap *l2cap_ctx, @@ -147,16 +149,16 @@ static void write_obj_tx_done(struct bt_gatt_ots_l2cap *l2cap_ctx, size_t written; if (cur_inst == NULL) { - LOG_ERR("OTS instance invalid"); + LOG_ERR("OtsCliInstInv"); return; } written = cur_inst->sent_size; - LOG_DBG("ctx: %p, conn: %p, written: %d", l2cap_ctx, (void *)conn, written); + LOG_DBG("OtsCliObjWritten[%zu]", written); err = bt_gatt_ots_l2cap_disconnect(l2cap_ctx); if (err < 0) { - LOG_WRN("Disconnecting L2CAP returned error %d", err); + LOG_WRN("OtsCliL2capDiscErr[%d]", err); } if ((cur_inst->otc_inst != NULL) && (cur_inst->otc_inst->cb != NULL)) { @@ -172,36 +174,40 @@ static ssize_t rx_done(struct bt_gatt_ots_l2cap *l2cap_ctx, struct bt_conn *conn, struct net_buf *buf) { if (cur_inst == NULL) { - LOG_ERR("OTS instance invalid"); + LOG_ERR("OtsCliInstInv"); return -EINVAL; } const uint32_t offset = cur_inst->rcvd_size; + uint32_t len = buf->len; bool is_complete = false; const struct bt_ots_obj_metadata *cur_object = &cur_inst->otc_inst->cur_object; int cb_ret; - LOG_DBG("Incoming L2CAP data, context: %p, conn: %p, len: %u, offset: %u", l2cap_ctx, - (void *)conn, buf->len, offset); + LOG_DBG("OtsCliL2capRecv[%u][%u]", buf->len, offset); - cur_inst->rcvd_size += buf->len; - - if (cur_inst->rcvd_size >= cur_object->size.cur) { - is_complete = true; + if (offset >= cur_object->size.cur) { + LOG_WRN("OtsCliRecvPastEnd[%u][%u]", offset, cur_object->size.cur); + (void)bt_gatt_ots_l2cap_disconnect(l2cap_ctx); + cur_inst = NULL; + return -EMSGSIZE; } - if (cur_inst->rcvd_size > cur_object->size.cur) { - LOG_WRN("Received %u but expected maximum %u", cur_inst->rcvd_size, - cur_object->size.cur); + if (offset + len > cur_object->size.cur) { + LOG_WRN("OtsCliRecvExceedMax[%u][%u]", offset + len, cur_object->size.cur); + len = cur_object->size.cur - offset; } + cur_inst->rcvd_size = offset + len; + is_complete = (cur_inst->rcvd_size >= cur_object->size.cur); + if (cur_inst->otc_inst->cb != NULL && cur_inst->otc_inst->cb->obj_data_read != NULL) { cb_ret = cur_inst->otc_inst->cb->obj_data_read(cur_inst->otc_inst, conn, offset, - buf->len, buf->data, + len, buf->data, is_complete); } else { - LOG_ERR("obj_data_read callback not set"); + LOG_ERR("OtsCliObjDataRdCbNull"); return -EINVAL; } @@ -209,11 +215,10 @@ static ssize_t rx_done(struct bt_gatt_ots_l2cap *l2cap_ctx, const uint32_t rcv_size = cur_object->size.cur; int err; - LOG_DBG("Received the whole object (%u bytes). " - "Disconnecting L2CAP CoC", rcv_size); + LOG_DBG("OtsCliRecvWholeObj[%u]", rcv_size); err = bt_gatt_ots_l2cap_disconnect(l2cap_ctx); if (err < 0) { - LOG_WRN("Disconnecting L2CAP returned error %d", err); + LOG_WRN("OtsCliL2capDiscErr[%d]", err); } cur_inst = NULL; @@ -221,12 +226,11 @@ static ssize_t rx_done(struct bt_gatt_ots_l2cap *l2cap_ctx, const uint32_t rcv_size = cur_object->size.cur; int err; - LOG_DBG("Stopped receiving after%u bytes. " - "Disconnecting L2CAP CoC", rcv_size); + LOG_DBG("OtsCliRecvStopped[%u]", rcv_size); err = bt_gatt_ots_l2cap_disconnect(l2cap_ctx); if (err < 0) { - LOG_WRN("Disconnecting L2CAP returned error %d", err); + LOG_WRN("OtsCliL2capDiscErr[%d]", err); } cur_inst = NULL; @@ -238,10 +242,15 @@ static ssize_t rx_done(struct bt_gatt_ots_l2cap *l2cap_ctx, static void chan_closed(struct bt_gatt_ots_l2cap *l2cap_ctx, struct bt_conn *conn) { - LOG_DBG("L2CAP closed, context: %p, conn: %p", l2cap_ctx, (void *)conn); - if (cur_inst) { - cur_inst = NULL; - } + struct bt_otc_internal_instance_t *inst = + CONTAINER_OF(l2cap_ctx, struct bt_otc_internal_instance_t, l2cap_ctx); + + ARG_UNUSED(conn); + + LOG_DBG("OtsCliL2capClosed"); + + /* Only release if this channel belonged to the active transfer. */ + oacp_clr_cur_inst(inst); } /* End L2CAP callbacks */ @@ -249,14 +258,14 @@ static void print_oacp_response(enum bt_gatt_ots_oacp_proc_type req_opcode, enum bt_gatt_ots_oacp_res_code result_code) { if (req_opcode < ARRAY_SIZE(lit_request)) { - LOG_DBG("Request OP Code: %s", lit_request[req_opcode]); + LOG_DBG("OtsCliReqOpcode[%s]", lit_request[req_opcode]); } else { - LOG_DBG("Request OP Code: Unknown (0x%02X)", req_opcode); + LOG_DBG("OtsCliReqOpcodeUnk[%02x]", req_opcode); } if (result_code < ARRAY_SIZE(lit_result)) { - LOG_DBG("Result Code : %s", lit_result[result_code]); + LOG_DBG("OtsCliResultCode[%s]", lit_result[result_code]); } else { - LOG_DBG("Result Code : Unknown (0x%02X)", result_code); + LOG_DBG("OtsCliResultCodeUnk[%02x]", result_code); } } @@ -264,14 +273,14 @@ static void print_olcp_response(enum bt_gatt_ots_olcp_proc_type req_opcode, enum bt_gatt_ots_olcp_res_code result_code) { if (req_opcode < ARRAY_SIZE(lit_olcp_request)) { - LOG_DBG("Request OP Code: %s", lit_olcp_request[req_opcode]); + LOG_DBG("OtsCliReqOpcode[%s]", lit_olcp_request[req_opcode]); } else { - LOG_DBG("Request OP Code: Unknown (0x%02X)", req_opcode); + LOG_DBG("OtsCliReqOpcodeUnk[%02x]", req_opcode); } if (result_code < ARRAY_SIZE(lit_olcp_result)) { - LOG_DBG("Result Code : %s", lit_olcp_result[result_code]); + LOG_DBG("OtsCliResultCode[%s]", lit_olcp_result[result_code]); } else { - LOG_DBG("Result Code : Unknown (0x%02X)", result_code); + LOG_DBG("OtsCliResultCodeUnk[%02x]", result_code); } } @@ -296,7 +305,7 @@ static struct bt_otc_internal_instance_t *lookup_inst_by_handle(uint16_t handle) } } - LOG_DBG("Could not find OTS instance with handle 0x%04x", handle); + LOG_DBG("OtsCliLkupInstNotFound[%04x]", handle); return NULL; } @@ -312,7 +321,7 @@ static void on_object_selected(struct bt_conn *conn, otc_inst->cb->obj_selected(otc_inst, conn, res); } - LOG_DBG("Object selected"); + LOG_DBG("OtsCliObjSel"); } static void olcp_ind_handler(struct bt_conn *conn, @@ -322,8 +331,10 @@ static void olcp_ind_handler(struct bt_conn *conn, enum bt_gatt_ots_olcp_proc_type op_code; struct net_buf_simple net_buf; - if (length < sizeof(op_code)) { - LOG_DBG("Invalid indication length: %u", length); + /* Op Code is 1 byte on the wire; sizeof(enum) is 4 here and would reject + * every valid 3-byte response. */ + if (length < sizeof(uint8_t)) { + LOG_WRN("OtsCliInvIndLen[%u]", length); return; } @@ -331,12 +342,11 @@ static void olcp_ind_handler(struct bt_conn *conn, op_code = net_buf_simple_pull_u8(&net_buf); - LOG_DBG("OLCP indication"); + LOG_DBG("OtsCliOlcpInd"); if (op_code == BT_GATT_OTS_OLCP_PROC_RESP) { if (net_buf.len < (sizeof(uint8_t) + sizeof(uint8_t))) { - LOG_DBG("Invalid indication length for op_code %u: %u", op_code, - net_buf.len); + LOG_WRN("OtsCliInvIndLenForOpcode[%u][%u]", op_code, net_buf.len); return; } @@ -349,61 +359,72 @@ static void olcp_ind_handler(struct bt_conn *conn, switch (req_opcode) { case BT_GATT_OTS_OLCP_PROC_FIRST: - LOG_DBG("First"); + LOG_DBG("OtsCliFirst"); on_object_selected(conn, result_code, otc_inst); break; case BT_GATT_OTS_OLCP_PROC_LAST: - LOG_DBG("Last"); + LOG_DBG("OtsCliLast"); on_object_selected(conn, result_code, otc_inst); break; case BT_GATT_OTS_OLCP_PROC_PREV: - LOG_DBG("Previous"); + LOG_DBG("OtsCliPrev"); on_object_selected(conn, result_code, otc_inst); break; case BT_GATT_OTS_OLCP_PROC_NEXT: - LOG_DBG("Next"); + LOG_DBG("OtsCliNext"); on_object_selected(conn, result_code, otc_inst); break; case BT_GATT_OTS_OLCP_PROC_GOTO: - LOG_DBG("Goto"); + LOG_DBG("OtsCliGoto"); on_object_selected(conn, result_code, otc_inst); break; case BT_GATT_OTS_OLCP_PROC_ORDER: - LOG_DBG("Order"); + LOG_DBG("OtsCliOrder"); on_object_selected(conn, result_code, otc_inst); break; case BT_GATT_OTS_OLCP_PROC_REQ_NUM_OBJS: - LOG_DBG("Request number of objects"); + LOG_DBG("OtsCliReqNumObjs"); if (net_buf.len == sizeof(uint32_t)) { uint32_t obj_cnt = net_buf_simple_pull_le32(&net_buf); - LOG_DBG("Number of objects %u", obj_cnt); + LOG_DBG("OtsCliNumObjs[%u]", obj_cnt); } break; case BT_GATT_OTS_OLCP_PROC_CLEAR_MARKING: - LOG_DBG("Clear marking"); + LOG_DBG("OtsCliClearMarking"); break; default: - LOG_DBG("Invalid indication req opcode %u", req_opcode); + LOG_WRN("OtsCliInvIndReqOpcode[%u]", req_opcode); break; } } else { - LOG_DBG("Invalid indication opcode %u", op_code); + LOG_WRN("OtsCliInvIndOpcode[%u]", op_code); + } +} + +static void oacp_clr_cur_inst(struct bt_otc_internal_instance_t *inst) +{ + if (cur_inst == inst) { + cur_inst = NULL; } } static void oacp_ind_handler(struct bt_conn *conn, - struct bt_ots_client *otc_inst, + struct bt_otc_internal_instance_t *inst, const void *data, uint16_t length) { + struct bt_ots_client *otc_inst = inst->otc_inst; enum bt_gatt_ots_oacp_proc_type op_code; enum bt_gatt_ots_oacp_proc_type req_opcode; enum bt_gatt_ots_oacp_res_code result_code; uint32_t checksum; struct net_buf_simple net_buf; - if (length < sizeof(op_code)) { - LOG_DBG("Invalid indication length: %u", length); + /* Op Code is 1 byte on the wire; sizeof(enum) is 4 here and would reject + * every valid 3-byte response. */ + if (length < sizeof(uint8_t)) { + LOG_WRN("OtsCliInvIndLen[%u]", length); + oacp_clr_cur_inst(inst); return; } @@ -411,35 +432,44 @@ static void oacp_ind_handler(struct bt_conn *conn, op_code = net_buf_simple_pull_u8(&net_buf); - LOG_DBG("OACP indication"); + LOG_DBG("OtsCliOacpInd"); if (op_code == BT_GATT_OTS_OACP_PROC_RESP) { - if (net_buf.len >= (sizeof(req_opcode) + sizeof(result_code))) { + if (net_buf.len >= (sizeof(uint8_t) + sizeof(uint8_t))) { req_opcode = net_buf_simple_pull_u8(&net_buf); result_code = net_buf_simple_pull_u8(&net_buf); } else { - LOG_ERR("Invalid indication data len %u", net_buf.len); + LOG_WRN("OtsCliInvIndDataLen[%u]", net_buf.len); + oacp_clr_cur_inst(inst); return; } if (req_opcode == BT_GATT_OTS_OACP_PROC_CHECKSUM_CALC) { if (net_buf.len == sizeof(checksum)) { checksum = net_buf_simple_pull_le32(&net_buf); - LOG_DBG("Object checksum 0x%08x", checksum); + LOG_DBG("OtsCliObjChecksum[%08x]", checksum); if (otc_inst->cb && otc_inst->cb->obj_checksum_calculated) { otc_inst->cb->obj_checksum_calculated( otc_inst, conn, result_code, checksum); } } else { - LOG_ERR("Invalid indication data len %u after opcode and result " - "pulled", net_buf.len); - return; + LOG_WRN("OtsCliInvChecksumLen[%u]", net_buf.len); } + /* Checksum never uses L2CAP; always release cur_inst. */ + oacp_clr_cur_inst(inst); + } else if ((req_opcode == BT_GATT_OTS_OACP_PROC_READ || + req_opcode == BT_GATT_OTS_OACP_PROC_WRITE) && + result_code != BT_GATT_OTS_OACP_RES_SUCCESS) { + /* Read/Write SUCCESS still waits on L2CAP (rx/tx_done/closed). */ + LOG_DBG("OtsCliOacpFailClr[%02x][%02x]", req_opcode, result_code); + oacp_clr_cur_inst(inst); + (void)bt_gatt_ots_l2cap_disconnect(&inst->l2cap_ctx); } print_oacp_response(req_opcode, result_code); } else { - LOG_DBG("Invalid indication opcode %u", op_code); + LOG_WRN("OtsCliInvIndOpcode[%u]", op_code); + oacp_clr_cur_inst(inst); } } @@ -461,7 +491,7 @@ uint8_t bt_ots_client_indicate_handler(struct bt_conn *conn, */ if (!inst) { - LOG_ERR("Instance not found"); + LOG_ERR("OtsCliInstNotFound"); return BT_GATT_ITER_STOP; } @@ -471,7 +501,7 @@ uint8_t bt_ots_client_indicate_handler(struct bt_conn *conn, if (handle == inst->otc_inst->olcp_handle) { olcp_ind_handler(conn, inst->otc_inst, data, length); } else if (handle == inst->otc_inst->oacp_handle) { - oacp_ind_handler(conn, inst->otc_inst, data, length); + oacp_ind_handler(conn, inst, data, length); } } return BT_GATT_ITER_CONTINUE; @@ -491,14 +521,14 @@ static uint8_t read_feature_cb(struct bt_conn *conn, uint8_t err, net_buf_simple_init_with_data(&net_buf, (void *)data, length); if (!inst) { - LOG_ERR("Instance not found"); + LOG_ERR("OtsCliInstNotFound"); return BT_GATT_ITER_STOP; } inst->busy = false; if (err) { - LOG_DBG("err: 0x%02X", err); + LOG_WRN("OtsCliRdErr[%02x]", err); } else if (data) { if (length == OTS_FEATURE_LEN) { inst->otc_inst->features.oacp = @@ -507,10 +537,9 @@ static uint8_t read_feature_cb(struct bt_conn *conn, uint8_t err, inst->otc_inst->features.olcp = net_buf_simple_pull_le32(&net_buf); - LOG_DBG("features : oacp 0x%x, olcp 0x%x", inst->otc_inst->features.oacp, - inst->otc_inst->features.olcp); + LOG_DBG("OtsCliFeat[%x][%x]", inst->otc_inst->features.oacp, inst->otc_inst->features.olcp); } else { - LOG_DBG("Invalid length %u (expected %u)", length, OTS_FEATURE_LEN); + LOG_WRN("OtsCliInvLen[%u][%u]", length, OTS_FEATURE_LEN); cb_err = BT_ATT_ERR_INVALID_ATTRIBUTE_LEN; } } @@ -518,8 +547,50 @@ static uint8_t read_feature_cb(struct bt_conn *conn, uint8_t err, return BT_GATT_ITER_STOP; } +/* Disconnect fires no subscription callback, so the OLCP/OACP indication that + * releases busy never arrives — an in-flight procedure would strand -EBUSY. */ +static void ots_client_conn_disconnected(struct bt_conn *conn, uint8_t reason) +{ + ARG_UNUSED(conn); + ARG_UNUSED(reason); + + for (int i = 0; i < ARRAY_SIZE(otc_insts); i++) { + struct bt_otc_internal_instance_t *inst = &otc_insts[i]; + + if (inst->otc_inst == NULL || !inst->busy) { + continue; + } + + LOG_WRN("OtsCliDisconnBusy[%d]", i); + + inst->busy = false; + oacp_clr_cur_inst(inst); + } +} + +BT_CONN_CB_DEFINE(client_conn_callbacks) = { + .disconnected = ots_client_conn_disconnected, +}; + +int bt_gatt_ots_client_conn_cb_register(void) +{ + return bt_conn_cb_register_safe((void *)&bt_conn_cb_client_conn_callbacks); +} + +void bt_gatt_ots_client_conn_cb_unregister(void) +{ + (void)bt_conn_cb_unregister_safe((void *)&bt_conn_cb_client_conn_callbacks); +} + int bt_ots_client_register(struct bt_ots_client *otc_inst) { + /* A NULL inst would leave the slot marked free after registering its + * L2CAP node, so the next call re-appends the same node and self-links it. */ + if (otc_inst == NULL) { + LOG_ERR("OtsCliInvInst"); + return -EINVAL; + } + for (int i = 0; i < ARRAY_SIZE(otc_insts); i++) { int err; @@ -527,10 +598,10 @@ int bt_ots_client_register(struct bt_ots_client *otc_inst) continue; } - LOG_DBG("%u", i); + LOG_DBG("OtsCliRegInst[%d]", i); err = bt_gatt_ots_l2cap_register(&otc_insts[i].l2cap_ctx); if (err) { - LOG_WRN("Could not register L2CAP context %d", err); + LOG_WRN("OtsCliL2capCtxRegFail[%d]", err); return err; } @@ -541,18 +612,41 @@ int bt_ots_client_register(struct bt_ots_client *otc_inst) return -ENOMEM; } +__attribute__((unused)) int bt_ots_client_unregister(uint8_t index) { - if (index < ARRAY_SIZE(otc_insts)) { - bt_gatt_ots_l2cap_unregister(&otc_insts[index].l2cap_ctx); - memset(&otc_insts[index], 0, sizeof(otc_insts[index])); - } else { + int err; + + if (index >= ARRAY_SIZE(otc_insts)) { return -EINVAL; } + err = bt_gatt_ots_l2cap_unregister(&otc_insts[index].l2cap_ctx); + if (err) { + LOG_WRN("OtsCliL2capUnregFail[%d]", err); + return err; + } + + if (cur_inst == &otc_insts[index]) { + LOG_DBG("OtsCliUnregClrCurInst[%u]", index); + cur_inst = NULL; + } + + memset(&otc_insts[index], 0, sizeof(otc_insts[index])); + return 0; } +void bt_ots_client_unregister_all(void) +{ + for (uint8_t i = 0; i < ARRAY_SIZE(otc_insts); i++) { + if (otc_insts[i].otc_inst != NULL) { + (void)bt_ots_client_unregister(i); + } + } +} + +__attribute__((unused)) int bt_ots_client_read_feature(struct bt_ots_client *otc_inst, struct bt_conn *conn) { @@ -561,20 +655,20 @@ int bt_ots_client_read_feature(struct bt_ots_client *otc_inst, int err; if (!conn) { - LOG_WRN("Invalid Connection"); + LOG_WRN("OtsCliInvConn"); return -ENOTCONN; } else if (!otc_inst) { - LOG_ERR("Invalid OTC instance"); + LOG_ERR("OtsCliInvInst"); return -EINVAL; } else if (!otc_inst->feature_handle) { - LOG_DBG("Handle not set"); + LOG_DBG("OtsCliHdlNotSet"); return -EINVAL; } inst = lookup_inst_by_handle(otc_inst->start_handle); if (!inst) { - LOG_ERR("Invalid OTC instance"); + LOG_ERR("OtsCliInvInst"); return -EINVAL; } else if (inst->busy) { return -EBUSY; @@ -592,7 +686,7 @@ int bt_ots_client_read_feature(struct bt_ots_client *otc_inst, return err; } - LOG_DBG("Not supported"); + LOG_DBG("OtsCliNotSupp"); return -EOPNOTSUPP; } @@ -602,14 +696,22 @@ static void write_olcp_cb(struct bt_conn *conn, uint8_t err, struct bt_otc_internal_instance_t *inst = lookup_inst_by_handle(params->handle); - LOG_DBG("Write %s (0x%02X)", err ? "failed" : "successful", err); + LOG_DBG("OtsCliWrCpCb[%02x]", err); if (!inst) { - LOG_ERR("Instance not found"); + LOG_ERR("OtsCliInstNotFound"); return; } - inst->busy = false; + /* Keep busy until OLCP indication; only release on write failure. + * Notify the app — without an indication, obj_selected would never fire. + */ + if (err) { + LOG_WRN("OtsCliOlcpWrFail[%02x]", err); + inst->busy = false; + on_object_selected(conn, BT_GATT_OTS_OLCP_RES_OPERATION_FAILED, + inst->otc_inst); + } } static int write_olcp(struct bt_otc_internal_instance_t *inst, @@ -640,12 +742,13 @@ static int write_olcp(struct bt_otc_internal_instance_t *inst, return err; } +__attribute__((unused)) int bt_ots_client_select_id(struct bt_ots_client *otc_inst, struct bt_conn *conn, uint64_t obj_id) { CHECKIF(!BT_OTS_VALID_OBJ_ID(obj_id)) { - LOG_DBG("Invalid object ID 0x%016llx", obj_id); + LOG_WRN("OtsCliInvObjId[%016llx]", obj_id); return -EINVAL; } @@ -655,20 +758,20 @@ int bt_ots_client_select_id(struct bt_ots_client *otc_inst, uint8_t param[BT_OTS_OBJ_ID_SIZE]; if (!conn) { - LOG_WRN("Invalid Connection"); + LOG_WRN("OtsCliInvConn"); return -ENOTCONN; } else if (!otc_inst) { - LOG_ERR("Invalid OTC instance"); + LOG_ERR("OtsCliInvInst"); return -EINVAL; } else if (!otc_inst->olcp_handle) { - LOG_DBG("Handle not set"); + LOG_DBG("OtsCliHdlNotSet"); return -EINVAL; } inst = lookup_inst_by_handle(otc_inst->start_handle); if (!inst) { - LOG_ERR("Invalid OTC instance"); + LOG_ERR("OtsCliInvInst"); return -EINVAL; } else if (inst->busy) { return -EBUSY; @@ -682,10 +785,11 @@ int bt_ots_client_select_id(struct bt_ots_client *otc_inst, param, BT_OTS_OBJ_ID_SIZE); } - LOG_DBG("Not supported"); + LOG_DBG("OtsCliNotSupp"); return -EOPNOTSUPP; } +__attribute__((unused)) int bt_ots_client_select_first(struct bt_ots_client *otc_inst, struct bt_conn *conn) { @@ -693,20 +797,20 @@ int bt_ots_client_select_first(struct bt_ots_client *otc_inst, struct bt_otc_internal_instance_t *inst; if (!conn) { - LOG_WRN("Invalid Connection"); + LOG_WRN("OtsCliInvConn"); return -ENOTCONN; } else if (!otc_inst) { - LOG_ERR("Invalid OTC instance"); + LOG_ERR("OtsCliInvInst"); return -EINVAL; } else if (!otc_inst->olcp_handle) { - LOG_DBG("Handle not set"); + LOG_DBG("OtsCliHdlNotSet"); return -EINVAL; } inst = lookup_inst_by_handle(otc_inst->start_handle); if (!inst) { - LOG_ERR("Invalid OTC instance"); + LOG_ERR("OtsCliInvInst"); return -EINVAL; } else if (inst->busy) { return -EBUSY; @@ -716,10 +820,11 @@ int bt_ots_client_select_first(struct bt_ots_client *otc_inst, NULL, 0); } - LOG_DBG("Not supported"); + LOG_DBG("OtsCliNotSupp"); return -EOPNOTSUPP; } +__attribute__((unused)) int bt_ots_client_select_last(struct bt_ots_client *otc_inst, struct bt_conn *conn) { @@ -727,20 +832,20 @@ int bt_ots_client_select_last(struct bt_ots_client *otc_inst, struct bt_otc_internal_instance_t *inst; if (!conn) { - LOG_WRN("Invalid Connection"); + LOG_WRN("OtsCliInvConn"); return -ENOTCONN; } else if (!otc_inst) { - LOG_ERR("Invalid OTC instance"); + LOG_ERR("OtsCliInvInst"); return -EINVAL; } else if (!otc_inst->olcp_handle) { - LOG_DBG("Handle not set"); + LOG_DBG("OtsCliHdlNotSet"); return -EINVAL; } inst = lookup_inst_by_handle(otc_inst->start_handle); if (!inst) { - LOG_ERR("Invalid OTC instance"); + LOG_ERR("OtsCliInvInst"); return -EINVAL; } else if (inst->busy) { return -EBUSY; @@ -751,10 +856,11 @@ int bt_ots_client_select_last(struct bt_ots_client *otc_inst, } - LOG_DBG("Not supported"); + LOG_DBG("OtsCliNotSupp"); return -EOPNOTSUPP; } +__attribute__((unused)) int bt_ots_client_select_next(struct bt_ots_client *otc_inst, struct bt_conn *conn) { @@ -762,20 +868,20 @@ int bt_ots_client_select_next(struct bt_ots_client *otc_inst, struct bt_otc_internal_instance_t *inst; if (!conn) { - LOG_WRN("Invalid Connection"); + LOG_WRN("OtsCliInvConn"); return -ENOTCONN; } else if (!otc_inst) { - LOG_ERR("Invalid OTC instance"); + LOG_ERR("OtsCliInvInst"); return -EINVAL; } else if (!otc_inst->olcp_handle) { - LOG_DBG("Handle not set"); + LOG_DBG("OtsCliHdlNotSet"); return -EINVAL; } inst = lookup_inst_by_handle(otc_inst->start_handle); if (!inst) { - LOG_ERR("Invalid OTC instance"); + LOG_ERR("OtsCliInvInst"); return -EINVAL; } else if (inst->busy) { return -EBUSY; @@ -785,10 +891,11 @@ int bt_ots_client_select_next(struct bt_ots_client *otc_inst, NULL, 0); } - LOG_DBG("Not supported"); + LOG_DBG("OtsCliNotSupp"); return -EOPNOTSUPP; } +__attribute__((unused)) int bt_ots_client_select_prev(struct bt_ots_client *otc_inst, struct bt_conn *conn) { @@ -796,20 +903,20 @@ int bt_ots_client_select_prev(struct bt_ots_client *otc_inst, struct bt_otc_internal_instance_t *inst; if (!conn) { - LOG_WRN("Invalid Connection"); + LOG_WRN("OtsCliInvConn"); return -ENOTCONN; } else if (!otc_inst) { - LOG_ERR("Invalid OTC instance"); + LOG_ERR("OtsCliInvInst"); return -EINVAL; } else if (!otc_inst->olcp_handle) { - LOG_DBG("Handle not set"); + LOG_DBG("OtsCliHdlNotSet"); return -EINVAL; } inst = lookup_inst_by_handle(otc_inst->start_handle); if (!inst) { - LOG_ERR("Invalid OTC instance"); + LOG_ERR("OtsCliInvInst"); return -EINVAL; } else if (inst->busy) { return -EBUSY; @@ -819,7 +926,7 @@ int bt_ots_client_select_prev(struct bt_ots_client *otc_inst, NULL, 0); } - LOG_DBG("Not supported"); + LOG_DBG("OtsCliNotSupp"); return -EOPNOTSUPP; } @@ -833,18 +940,18 @@ static uint8_t read_object_size_cb(struct bt_conn *conn, uint8_t err, net_buf_simple_init_with_data(&net_buf, (void *)data, length); - LOG_DBG("handle %d, length %u", params->single.handle, length); + LOG_DBG("OtsCliRdCb[%u][%u]", params->single.handle, length); if (!inst) { - LOG_ERR("Instance not found"); + LOG_ERR("OtsCliInstNotFound"); return BT_GATT_ITER_STOP; } if (err) { - LOG_DBG("err: 0x%02X", err); + LOG_WRN("OtsCliRdErr[%02x]", err); } else if (data) { if (length != OTS_SIZE_LEN) { - LOG_DBG("Invalid length %u (expected %u)", length, OTS_SIZE_LEN); + LOG_WRN("OtsCliInvLen[%u][%u]", length, OTS_SIZE_LEN); err = BT_ATT_ERR_INVALID_ATTRIBUTE_LEN; } else { struct bt_ots_obj_metadata *cur_object = @@ -854,21 +961,14 @@ static uint8_t read_object_size_cb(struct bt_conn *conn, uint8_t err, cur_object->size.alloc = net_buf_simple_pull_le32(&net_buf); - LOG_DBG("Object Size : current size %u, " - "allocated size %u", - cur_object->size.cur, - cur_object->size.alloc); + LOG_DBG("OtsCliObjSize[%u][%u]", cur_object->size.cur, cur_object->size.alloc); if (cur_object->size.cur == 0) { - LOG_WRN("Obj size read returned a current " - "size of 0"); + LOG_WRN("OtsCliObjSizeZero"); } else if (cur_object->size.cur > cur_object->size.alloc && cur_object->size.alloc != 0) { - LOG_WRN("Allocated size %u is smaller than " - "current size %u", - cur_object->size.alloc, - cur_object->size.cur); + LOG_WRN("OtsCliAllocLtCurSize[%u][%u]", cur_object->size.alloc, cur_object->size.cur); } BT_OTS_SET_METADATA_REQ_SIZE(inst->metadata_read); @@ -876,7 +976,7 @@ static uint8_t read_object_size_cb(struct bt_conn *conn, uint8_t err, } if (err) { - LOG_WRN("err: 0x%02X", err); + LOG_WRN("OtsCliMetaRdErr[%02x]", err); if (!inst->metadata_err) { inst->metadata_err = err; } @@ -897,15 +997,15 @@ static uint8_t read_obj_id_cb(struct bt_conn *conn, uint8_t err, net_buf_simple_init_with_data(&net_buf, (void *)data, length); - LOG_DBG("handle %d, length %u", params->single.handle, length); + LOG_DBG("OtsCliRdCb[%u][%u]", params->single.handle, length); if (!inst) { - LOG_ERR("Instance not found"); + LOG_ERR("OtsCliInstNotFound"); return BT_GATT_ITER_STOP; } if (err) { - LOG_DBG("err: 0x%02X", err); + LOG_WRN("OtsCliRdErr[%02x]", err); } else if (data) { if (length == BT_OTS_OBJ_ID_SIZE) { uint64_t obj_id = net_buf_simple_pull_le48(&net_buf); @@ -914,7 +1014,7 @@ static uint8_t read_obj_id_cb(struct bt_conn *conn, uint8_t err, &inst->otc_inst->cur_object; (void)bt_ots_obj_id_to_str(obj_id, t, sizeof(t)); - LOG_DBG("Object Id : %s", t); + LOG_DBG("OtsCliObjId[%s]", t); if (cur_object->id != OTS_CLIENT_UNKNOWN_ID && cur_object->id != obj_id) { @@ -922,21 +1022,21 @@ static uint8_t read_obj_id_cb(struct bt_conn *conn, uint8_t err, (void)bt_ots_obj_id_to_str(cur_object->id, str, sizeof(str)); - LOG_INF("Read Obj Id %s not selected obj Id %s", t, str); + LOG_INF("OtsCliRdObjIdNotSel[%s][%s]", t, str); } else { - LOG_INF("Read Obj Id confirmed correct Obj Id"); + LOG_INF("OtsCliRdObjIdConfirmed"); cur_object->id = obj_id; BT_OTS_SET_METADATA_REQ_ID(inst->metadata_read); } } else { - LOG_DBG("Invalid length %u (expected %u)", length, BT_OTS_OBJ_ID_SIZE); + LOG_WRN("OtsCliInvLen[%u][%u]", length, BT_OTS_OBJ_ID_SIZE); err = BT_ATT_ERR_INVALID_ATTRIBUTE_LEN; } } if (err) { - LOG_WRN("err: 0x%02X", err); + LOG_WRN("OtsCliMetaRdErr[%02x]", err); if (!inst->metadata_err) { inst->metadata_err = err; } @@ -954,10 +1054,10 @@ static uint8_t read_obj_name_cb(struct bt_conn *conn, uint8_t err, struct bt_otc_internal_instance_t *inst = lookup_inst_by_handle(params->single.handle); - LOG_DBG("handle %d, length %u", params->single.handle, length); + LOG_DBG("OtsCliRdCb[%u][%u]", params->single.handle, length); if (!inst) { - LOG_ERR("Instance not found"); + LOG_ERR("OtsCliInstNotFound"); return BT_GATT_ITER_STOP; } @@ -968,14 +1068,13 @@ static uint8_t read_obj_name_cb(struct bt_conn *conn, uint8_t err, BT_OTS_SET_METADATA_REQ_NAME(inst->metadata_read); } else { - LOG_WRN("Invalid length %u (expected max %u)", length, - CONFIG_BT_OTS_OBJ_MAX_NAME_LEN); + LOG_WRN("OtsCliInvLen[%u][%u]", length, CONFIG_BT_OTS_OBJ_MAX_NAME_LEN); err = BT_ATT_ERR_INVALID_ATTRIBUTE_LEN; } } if (err) { - LOG_WRN("err: 0x%02X", err); + LOG_WRN("OtsCliMetaRdErr[%02x]", err); if (!inst->metadata_err) { inst->metadata_err = err; } @@ -993,10 +1092,10 @@ static uint8_t read_obj_type_cb(struct bt_conn *conn, uint8_t err, struct bt_otc_internal_instance_t *inst = lookup_inst_by_handle(params->single.handle); - LOG_DBG("handle %d, length %u", params->single.handle, length); + LOG_DBG("OtsCliRdCb[%u][%u]", params->single.handle, length); if (!inst) { - LOG_ERR("Instance not found"); + LOG_ERR("OtsCliInstNotFound"); return BT_GATT_ITER_STOP; } @@ -1007,22 +1106,22 @@ static uint8_t read_obj_type_cb(struct bt_conn *conn, uint8_t err, &inst->otc_inst->cur_object.type.uuid; if (!bt_uuid_create(uuid, data, length)) { - LOG_WRN("Failed to create UUID from data"); + LOG_WRN("OtsCliUuidCreateFail"); err = BT_ATT_ERR_UNLIKELY; } else { bt_uuid_to_str(uuid, uuid_str, sizeof(uuid_str)); - LOG_DBG("UUID type read: %s", uuid_str); + LOG_DBG("OtsCliUuidTypeRd[%s]", uuid_str); BT_OTS_SET_METADATA_REQ_TYPE(inst->metadata_read); } } else { - LOG_WRN("Invalid length %u (expected max %u)", length, OTS_TYPE_MAX_LEN); + LOG_WRN("OtsCliInvLen[%u][%u]", length, OTS_TYPE_MAX_LEN); err = BT_ATT_ERR_INVALID_ATTRIBUTE_LEN; } } if (err) { - LOG_WRN("err: 0x%02X", err); + LOG_WRN("OtsCliMetaRdErr[%02x]", err); if (!inst->metadata_err) { inst->metadata_err = err; } @@ -1043,10 +1142,10 @@ static uint8_t read_obj_created_cb(struct bt_conn *conn, uint8_t err, net_buf_simple_init_with_data(&net_buf, (void *)data, length); - LOG_DBG("handle %d, length %u", params->single.handle, length); + LOG_DBG("OtsCliRdCb[%u][%u]", params->single.handle, length); if (!inst) { - LOG_ERR("Instance not found"); + LOG_ERR("OtsCliInstNotFound"); return BT_GATT_ITER_STOP; } @@ -1055,15 +1154,15 @@ static uint8_t read_obj_created_cb(struct bt_conn *conn, uint8_t err, date_time_decode( &net_buf, &inst->otc_inst->cur_object.first_created); + BT_OTS_SET_METADATA_REQ_CREATED(inst->metadata_read); } else { - LOG_WRN("Invalid length %u (expected max %u)", length, - BT_OTS_DATE_TIME_FIELD_SIZE); + LOG_WRN("OtsCliInvLen[%u][%u]", length, BT_OTS_DATE_TIME_FIELD_SIZE); err = BT_ATT_ERR_INVALID_ATTRIBUTE_LEN; } } if (err) { - LOG_WRN("err: 0x%02X", err); + LOG_WRN("OtsCliMetaRdErr[%02x]", err); if (!inst->metadata_err) { inst->metadata_err = err; } @@ -1084,10 +1183,10 @@ static uint8_t read_obj_modified_cb(struct bt_conn *conn, uint8_t err, net_buf_simple_init_with_data(&net_buf, (void *)data, length); - LOG_DBG("handle %d, length %u", params->single.handle, length); + LOG_DBG("OtsCliRdCb[%u][%u]", params->single.handle, length); if (!inst) { - LOG_ERR("Instance not found"); + LOG_ERR("OtsCliInstNotFound"); return BT_GATT_ITER_STOP; } @@ -1095,15 +1194,15 @@ static uint8_t read_obj_modified_cb(struct bt_conn *conn, uint8_t err, if (length == BT_OTS_DATE_TIME_FIELD_SIZE) { date_time_decode(&net_buf, &inst->otc_inst->cur_object.modified); + BT_OTS_SET_METADATA_REQ_MODIFIED(inst->metadata_read); } else { - LOG_WRN("Invalid length %u (expected max %u)", length, - BT_OTS_DATE_TIME_FIELD_SIZE); + LOG_WRN("OtsCliInvLen[%u][%u]", length, BT_OTS_DATE_TIME_FIELD_SIZE); err = BT_ATT_ERR_INVALID_ATTRIBUTE_LEN; } } if (err) { - LOG_WRN("err: 0x%02X", err); + LOG_WRN("OtsCliMetaRdErr[%02x]", err); if (!inst->metadata_err) { inst->metadata_err = err; } @@ -1119,10 +1218,11 @@ static int read_attr(struct bt_conn *conn, uint16_t handle, bt_gatt_read_func_t cb) { if (!handle) { - LOG_DBG("Handle not set"); - return -EINVAL; + /* Characteristic not discovered — skip without poisoning metadata_err. */ + LOG_DBG("OtsCliHdlNotSet"); + return -ENOENT; } else if (cb == NULL) { - LOG_ERR("No callback set"); + LOG_ERR("OtsCliCbNull"); return -EINVAL; } @@ -1146,35 +1246,37 @@ static uint8_t read_obj_properties_cb(struct bt_conn *conn, uint8_t err, net_buf_simple_init_with_data(&net_buf, (void *)data, length); - LOG_INF("handle %d, length %u", params->single.handle, length); + LOG_DBG("OtsCliRdCb[%u][%u]", params->single.handle, length); if (!inst) { - LOG_ERR("Instance not found"); + LOG_ERR("OtsCliInstNotFound"); return BT_GATT_ITER_STOP; } if (err) { - LOG_WRN("err: 0x%02X", err); - } else if (data && length == OTS_PROPERTIES_LEN) { - struct bt_ots_obj_metadata *cur_object = - &inst->otc_inst->cur_object; + LOG_WRN("OtsCliMetaRdErr[%02x]", err); + } else if (data) { + if (length != OTS_PROPERTIES_LEN) { + LOG_WRN("OtsCliInvLen[%u][%u]", length, OTS_PROPERTIES_LEN); + cb_err = BT_ATT_ERR_INVALID_ATTRIBUTE_LEN; + } else { + struct bt_ots_obj_metadata *cur_object = + &inst->otc_inst->cur_object; - cur_object->props = net_buf_simple_pull_le32(&net_buf); + cur_object->props = net_buf_simple_pull_le32(&net_buf); - LOG_INF("Object properties (raw) : 0x%x", cur_object->props); + LOG_INF("OtsCliObjPropsRaw[%x]", cur_object->props); - if (!BT_OTS_OBJ_GET_PROP_READ(cur_object->props)) { - LOG_WRN("Obj properties: Obj read not supported"); + if (!BT_OTS_OBJ_GET_PROP_READ(cur_object->props)) { + LOG_WRN("OtsCliObjRdNotSupp"); + } + + BT_OTS_SET_METADATA_REQ_PROPS(inst->metadata_read); } - - BT_OTS_SET_METADATA_REQ_PROPS(inst->metadata_read); - } else { - LOG_WRN("Invalid length %u (expected %u)", length, OTS_PROPERTIES_LEN); - cb_err = BT_ATT_ERR_INVALID_ATTRIBUTE_LEN; } if (cb_err) { - LOG_WRN("err: 0x%02X", cb_err); + LOG_WRN("OtsCliMetaRdErr[%02x]", cb_err); if (!inst->metadata_err) { inst->metadata_err = cb_err; } @@ -1191,14 +1293,21 @@ static void write_oacp_cp_cb(struct bt_conn *conn, uint8_t err, struct bt_otc_internal_instance_t *inst = lookup_inst_by_handle(params->handle); - LOG_DBG("Write %s (0x%02X)", err ? "failed" : "successful", err); + LOG_DBG("OtsCliWrCpCb[%02x]", err); if (!inst) { - LOG_ERR("Instance not found"); + LOG_ERR("OtsCliInstNotFound"); return; } - inst->busy = false; + /* Keep busy until OACP indication; only release on write failure. */ + if (err) { + LOG_WRN("OtsCliOacpWrFail[%02x]", err); + inst->busy = false; + oacp_clr_cur_inst(inst); + /* oacp_read may already have connected L2CAP before the GATT write. */ + (void)bt_gatt_ots_l2cap_disconnect(&inst->l2cap_ctx); + } } static void write_oacp_cp_write_req_cb(struct bt_conn *conn, uint8_t err, @@ -1208,16 +1317,17 @@ static void write_oacp_cp_write_req_cb(struct bt_conn *conn, uint8_t err, lookup_inst_by_handle(params->handle); uint32_t len; - LOG_DBG("Write Object request %s (0x%02X)", err ? "failed" : "successful", err); + LOG_DBG("OtsCliWrObjReqCb[%02x]", err); if (!inst) { - LOG_ERR("Instance not found"); + LOG_ERR("OtsCliInstNotFound"); return; } if (err) { - LOG_WRN("OACP Write request failed (0x%02X), aborting L2CAP send", err); + LOG_WRN("OtsCliOacpWrReqFail[%02x]", err); inst->busy = false; cur_inst = NULL; + (void)bt_gatt_ots_l2cap_disconnect(&inst->l2cap_ctx); return; } @@ -1225,9 +1335,9 @@ static void write_oacp_cp_write_req_cb(struct bt_conn *conn, uint8_t err, inst->l2cap_ctx.tx.len = 0; err = bt_gatt_ots_l2cap_send(&inst->l2cap_ctx, inst->l2cap_ctx.tx.data, len); if (err) { - LOG_WRN("L2CAP CoC error: %d while trying to execute OACP " - "Write procedure", err); + LOG_WRN("OtsCliL2capSendErr[%d]", err); cur_inst = NULL; + (void)bt_gatt_ots_l2cap_disconnect(&inst->l2cap_ctx); } inst->busy = false; @@ -1242,7 +1352,7 @@ static int oacp_read(struct bt_conn *conn, struct bt_gatt_ots_l2cap *l2cap; if (!inst->otc_inst->oacp_handle) { - LOG_DBG("Handle not set"); + LOG_DBG("OtsCliHdlNotSet"); return -EINVAL; } else if (inst->busy) { return -EBUSY; @@ -1254,9 +1364,10 @@ static int oacp_read(struct bt_conn *conn, * transfer? */ + l2cap = &inst->l2cap_ctx; err = bt_gatt_ots_l2cap_connect(conn, &l2cap); if (err) { - LOG_DBG("Could not connect l2cap: %d", err); + LOG_WRN("OtsCliL2capConnectFail[%d]", err); return err; } @@ -1287,6 +1398,8 @@ static int oacp_read(struct bt_conn *conn, if (!err) { inst->busy = true; cur_inst = inst; + } else { + (void)bt_gatt_ots_l2cap_disconnect(l2cap); } inst->rcvd_size = 0; @@ -1302,7 +1415,7 @@ static int oacp_write(struct bt_conn *conn, struct bt_otc_internal_instance_t *i struct bt_gatt_ots_l2cap *l2cap; if (!inst->otc_inst->oacp_handle) { - LOG_DBG("Handle not set"); + LOG_DBG("OtsCliHdlNotSet"); return -EINVAL; } else if (inst->busy) { return -EBUSY; @@ -1310,9 +1423,10 @@ static int oacp_write(struct bt_conn *conn, struct bt_otc_internal_instance_t *i return -EBUSY; } + l2cap = &inst->l2cap_ctx; err = bt_gatt_ots_l2cap_connect(conn, &l2cap); if (err) { - LOG_DBG("Could not connect l2cap: %d", err); + LOG_WRN("OtsCliL2capConnectFail[%d]", err); return err; } @@ -1346,6 +1460,8 @@ static int oacp_write(struct bt_conn *conn, struct bt_otc_internal_instance_t *i if (!err) { inst->busy = true; cur_inst = inst; + } else { + (void)bt_gatt_ots_l2cap_disconnect(l2cap); } inst->rcvd_size = 0; @@ -1359,13 +1475,13 @@ static int oacp_checksum(struct bt_conn *conn, struct bt_otc_internal_instance_t int err; if (!inst->otc_inst->oacp_handle) { - LOG_DBG("Handle not set"); + LOG_DBG("OtsCliHdlNotSet"); return -EINVAL; } else if (inst->busy) { - LOG_DBG("Client is busy"); + LOG_DBG("OtsCliBusy"); return -EBUSY; } else if (cur_inst) { - LOG_DBG("Previous operation is not finished"); + LOG_DBG("OtsCliPrevOpNotFinished"); return -EBUSY; } @@ -1401,28 +1517,29 @@ int bt_ots_client_read_object_data(struct bt_ots_client *otc_inst, struct bt_otc_internal_instance_t *inst; if (!conn) { - LOG_WRN("Invalid Connection"); + LOG_WRN("OtsCliInvConn"); return -ENOTCONN; } else if (!otc_inst) { - LOG_ERR("Invalid OTC instance"); + LOG_ERR("OtsCliInvInst"); return -EINVAL; } inst = lookup_inst_by_handle(otc_inst->start_handle); if (!inst) { - LOG_ERR("Invalid OTC instance"); + LOG_ERR("OtsCliInvInst"); return -EINVAL; } if (otc_inst->cur_object.size.cur == 0) { - LOG_WRN("Unknown object size"); + LOG_WRN("OtsCliUnkObjSize"); return -EINVAL; } return oacp_read(conn, inst); } +__attribute__((unused)) int bt_ots_client_write_object_data(struct bt_ots_client *otc_inst, struct bt_conn *conn, const void *buf, size_t len, off_t offset, enum bt_ots_oacp_write_op_mode mode) @@ -1430,18 +1547,18 @@ int bt_ots_client_write_object_data(struct bt_ots_client *otc_inst, struct bt_otc_internal_instance_t *inst; CHECKIF(!conn) { - LOG_WRN("Invalid Connection"); + LOG_WRN("OtsCliInvConn"); return -ENOTCONN; } CHECKIF(!otc_inst) { - LOG_ERR("Invalid OTC instance"); + LOG_ERR("OtsCliInvInst"); return -EINVAL; } CHECKIF((mode != BT_OTS_OACP_WRITE_OP_MODE_NONE) && (mode != BT_OTS_OACP_WRITE_OP_MODE_TRUNCATE)) { - LOG_ERR("Invalid write object mode parameter %d", mode); + LOG_ERR("OtsCliInvWrObjMode[%d]", mode); return -EINVAL; } @@ -1449,60 +1566,60 @@ int bt_ots_client_write_object_data(struct bt_ots_client *otc_inst, * Offset and Length field are UINT32 Length */ CHECKIF(len > UINT32_MAX) { - LOG_ERR("length %zu exceeds UINT32", len); + LOG_ERR("OtsCliLenExceedsUint32[%zu]", len); return -EINVAL; } CHECKIF(len == 0) { - LOG_ERR("length equals zero"); + LOG_ERR("OtsCliLenZero"); return -EINVAL; } CHECKIF((sizeof(offset) > sizeof(uint32_t) && (offset > UINT32_MAX)) || (offset < 0)) { - LOG_ERR("offset %ld exceeds UINT32 and must be >= 0", offset); + LOG_ERR("OtsCliOftExceedsUint32[%ld]", offset); return -EINVAL; } CHECKIF(offset > otc_inst->cur_object.size.cur) { - LOG_ERR("offset %ld exceeds cur size %zu", offset, otc_inst->cur_object.size.cur); + LOG_ERR("OtsCliOftExceedsCurSize[%ld][%u]", offset, otc_inst->cur_object.size.cur); return -EINVAL; } CHECKIF((offset < otc_inst->cur_object.size.cur) && !BT_OTS_OBJ_GET_PROP_PATCH(otc_inst->cur_object.props)) { - LOG_ERR("Patch is not supported"); + LOG_ERR("OtsCliPatchNotSupp"); return -EACCES; } CHECKIF(((uint64_t)len + (uint64_t)offset > otc_inst->cur_object.size.alloc) && !BT_OTS_OBJ_GET_PROP_APPEND(otc_inst->cur_object.props)) { - LOG_ERR("APPEND is not supported. Invalid new end of object %lu alloc %zu." - , (len + offset), otc_inst->cur_object.size.alloc); + LOG_ERR("OtsCliAppendNotSupp[%zu][%u]", (len + offset), otc_inst->cur_object.size.alloc); return -EINVAL; } inst = lookup_inst_by_handle(otc_inst->start_handle); if (!inst) { - LOG_ERR("Invalid OTC instance"); + LOG_ERR("OtsCliInvInst"); return -EINVAL; } return oacp_write(conn, inst, buf, (uint32_t)len, (uint32_t)offset, mode); } +__attribute__((unused)) int bt_ots_client_get_object_checksum(struct bt_ots_client *otc_inst, struct bt_conn *conn, off_t offset, size_t len) { struct bt_otc_internal_instance_t *inst; CHECKIF(!conn) { - LOG_DBG("Invalid Connection"); + LOG_WRN("OtsCliInvConn"); return -ENOTCONN; } CHECKIF(!otc_inst) { - LOG_DBG("Invalid OTC instance"); + LOG_ERR("OtsCliInvInst"); return -EINVAL; } @@ -1510,29 +1627,28 @@ int bt_ots_client_get_object_checksum(struct bt_ots_client *otc_inst, struct bt_ * Offset and Length field are UINT32 Length */ CHECKIF(len > UINT32_MAX) { - LOG_DBG("length %zu exceeds UINT32", len); + LOG_WRN("OtsCliLenExceedsUint32[%zu]", len); return -EINVAL; } CHECKIF(len == 0) { - LOG_DBG("length equals zero"); + LOG_WRN("OtsCliLenZero"); return -EINVAL; } CHECKIF((sizeof(offset) > sizeof(uint32_t) && (offset > UINT32_MAX)) || (offset < 0)) { - LOG_DBG("offset exceeds %ld UINT32 and must be >= 0", offset); + LOG_WRN("OtsCliOftExceedsUint32[%ld]", offset); return -EINVAL; } CHECKIF((len + offset) > otc_inst->cur_object.size.cur) { - LOG_DBG("The sum of offset (%ld) and length (%zu) equals %lu which exceeds " - "the Current Size %zu.", offset, len, (len + offset), otc_inst->cur_object.size.cur); + LOG_WRN("OtsCliOftLenExceedSize[%ld][%zu][%u]", offset, len, otc_inst->cur_object.size.cur); return -EINVAL; } inst = lookup_inst_by_handle(otc_inst->start_handle); if (!inst) { - LOG_DBG("Invalid OTC instance"); + LOG_ERR("OtsCliInvInst"); return -EINVAL; } @@ -1546,7 +1662,7 @@ static void read_next_metadata(struct bt_conn *conn, inst->metadata_to_read ^ inst->metadata_read_attempted; int err = 0; - LOG_DBG("Attempting to read metadata 0x%02X", metadata_remaining); + LOG_DBG("OtsCliRdNextMeta[%02x]", metadata_remaining); if (BT_OTS_GET_METADATA_REQ_NAME(metadata_remaining)) { BT_OTS_SET_METADATA_REQ_NAME(inst->metadata_read_attempted); @@ -1588,7 +1704,11 @@ static void read_next_metadata(struct bt_conn *conn, } if (err) { - LOG_DBG("Metadata read failed (%d), trying next", err); + LOG_INF("OtsCliMetaRdFailTryNext[%d]", err); + /* -ENOENT: handle not discovered, skip. Other errors: keep first. */ + if (err != -ENOENT && !inst->metadata_err) { + inst->metadata_err = err; + } read_next_metadata(conn, inst); } } @@ -1600,20 +1720,20 @@ int bt_ots_client_read_object_metadata(struct bt_ots_client *otc_inst, struct bt_otc_internal_instance_t *inst; if (!conn) { - LOG_WRN("Invalid Connection"); + LOG_WRN("OtsCliInvConn"); return -ENOTCONN; } else if (!otc_inst) { - LOG_ERR("Invalid OTC instance"); + LOG_ERR("OtsCliInvInst"); return -EINVAL; } else if (!metadata) { - LOG_WRN("No metadata to read"); + LOG_WRN("OtsCliNoMetaToRd"); return -ENOEXEC; } inst = lookup_inst_by_handle(otc_inst->start_handle); if (!inst) { - LOG_ERR("Invalid OTC instance"); + LOG_ERR("OtsCliInvInst"); return -EINVAL; } else if (inst->busy) { return -EBUSY; @@ -1638,16 +1758,12 @@ static int decode_record(struct net_buf_simple *buf, rec->len = net_buf_simple_pull_le16(buf); if (rec->len < 2 || rec->len > start_len) { - LOG_WRN("incorrect DirListing record length %u, " - "longer than remaining size %u", - rec->len, start_len); + LOG_WRN("OtsCliDirRecLenInv[%u][%u]", rec->len, start_len); return -EINVAL; } if ((start_len - buf->len) + BT_OTS_OBJ_ID_SIZE > rec->len) { - LOG_WRN("incorrect DirListing record, reclen %u too short, " - "includes only record length", - rec->len); + LOG_WRN("OtsCliDirRecTooShortNoObjId[%u]", rec->len); return -EINVAL; } @@ -1658,14 +1774,12 @@ static int decode_record(struct net_buf_simple *buf, char t[BT_OTS_OBJ_ID_STR_LEN]; (void)bt_ots_obj_id_to_str(rec->metadata.id, t, sizeof(t)); - LOG_DBG("Object ID 0x%s", t); + LOG_DBG("OtsCliObjId[%s]", t); } #endif if ((start_len - buf->len) + sizeof(uint8_t) > rec->len) { - LOG_WRN("incorrect DirListing record, reclen %u too short, " - "includes only record length + ObjId", - rec->len); + LOG_WRN("OtsCliDirRecTooShortNoNameLen[%u]", rec->len); return -EINVAL; } @@ -1675,15 +1789,12 @@ static int decode_record(struct net_buf_simple *buf, uint8_t *name; if ((start_len - buf->len) + rec->name_len > rec->len) { - LOG_WRN("incorrect DirListing record, remaining length " - "%u shorter than name length %u", - rec->len - (start_len - buf->len), - rec->name_len); + LOG_WRN("OtsCliDirRecNameExceedsRemaining[%u][%u]", rec->len - (start_len - buf->len), rec->name_len); return -EINVAL; } if (rec->name_len >= sizeof(rec->metadata.name_c)) { - LOG_WRN("Name length %u too long, invalid record", rec->name_len); + LOG_WRN("OtsCliDirRecNameLenTooLong[%u]", rec->name_len); return -EINVAL; } @@ -1695,36 +1806,31 @@ static int decode_record(struct net_buf_simple *buf, rec->flags = 0; if ((start_len - buf->len) + sizeof(uint8_t) > rec->len) { - LOG_WRN("incorrect DirListing record, reclen %u too short, " - "does not include flags", rec->len); + LOG_WRN("OtsCliDirRecTooShortNoFlags[%u]", rec->len); return -EINVAL; } rec->flags = net_buf_simple_pull_u8(buf); - LOG_DBG("flags 0x%x", rec->flags); + LOG_DBG("OtsCliDirRecFlags[%x]", rec->flags); if (BT_OTS_DIR_LIST_GET_FLAG_TYPE_128(rec->flags)) { uint8_t *uuid; if ((start_len - buf->len) + BT_UUID_SIZE_128 > rec->len) { - LOG_WRN("incorrect DirListing record, reclen %u " - "flags indicates uuid128, too short", - rec->len); - LOG_INF("flags 0x%x", rec->flags); + LOG_WRN("OtsCliDirRecTooShortUuid128[%u]", rec->len); + LOG_DBG("OtsCliDirRecFlags[%x]", rec->flags); return -EINVAL; } uuid = net_buf_simple_pull_mem(buf, BT_UUID_SIZE_128); if (!bt_uuid_create(&rec->metadata.type.uuid, uuid, BT_UUID_SIZE_128)) { - LOG_DBG("Failed to create UUID"); + LOG_WRN("OtsCliUuidCreateFail"); return -EINVAL; } } else { if ((start_len - buf->len) + BT_UUID_SIZE_16 > rec->len) { - LOG_WRN("incorrect DirListing record, reclen %u " - "flags indicates uuid16, too short", - rec->len); - LOG_INF("flags 0x%x", rec->flags); + LOG_WRN("OtsCliDirRecTooShortUuid16[%u]", rec->len); + LOG_DBG("OtsCliDirRecFlags[%x]", rec->flags); return -EINVAL; } @@ -1735,10 +1841,8 @@ static int decode_record(struct net_buf_simple *buf, if (BT_OTS_DIR_LIST_GET_FLAG_CUR_SIZE(rec->flags)) { if ((start_len - buf->len) + sizeof(uint32_t) > rec->len) { - LOG_WRN("incorrect DirListing record, reclen %u " - "flags indicates cur_size, too short", - rec->len); - LOG_INF("flags 0x%x", rec->flags); + LOG_WRN("OtsCliDirRecTooShortCurSize[%u]", rec->len); + LOG_DBG("OtsCliDirRecFlags[%x]", rec->flags); return -EINVAL; } @@ -1747,10 +1851,8 @@ static int decode_record(struct net_buf_simple *buf, if (BT_OTS_DIR_LIST_GET_FLAG_ALLOC_SIZE(rec->flags)) { if ((start_len - buf->len) + sizeof(uint32_t) > rec->len) { - LOG_WRN("incorrect DirListing record, reclen %u " - "flags indicates allocated size, too short", - rec->len); - LOG_INF("flags 0x%x", rec->flags); + LOG_WRN("OtsCliDirRecTooShortAllocSize[%u]", rec->len); + LOG_DBG("OtsCliDirRecFlags[%x]", rec->flags); return -EINVAL; } @@ -1759,10 +1861,8 @@ static int decode_record(struct net_buf_simple *buf, if (BT_OTS_DIR_LIST_GET_FLAG_FIRST_CREATED(rec->flags)) { if ((start_len - buf->len) + BT_OTS_DATE_TIME_FIELD_SIZE > rec->len) { - LOG_WRN("incorrect DirListing record, reclen %u " - "too short flags indicates first_created", - rec->len); - LOG_INF("flags 0x%x", rec->flags); + LOG_WRN("OtsCliDirRecTooShortFirstCreated[%u]", rec->len); + LOG_DBG("OtsCliDirRecFlags[%x]", rec->flags); return -EINVAL; } @@ -1771,10 +1871,8 @@ static int decode_record(struct net_buf_simple *buf, if (BT_OTS_DIR_LIST_GET_FLAG_LAST_MODIFIED(rec->flags)) { if ((start_len - buf->len) + BT_OTS_DATE_TIME_FIELD_SIZE > rec->len) { - LOG_WRN("incorrect DirListing record, reclen %u " - "flags indicates las_mod, too short", - rec->len); - LOG_INF("flags 0x%x", rec->flags); + LOG_WRN("OtsCliDirRecTooShortLastModified[%u]", rec->len); + LOG_DBG("OtsCliDirRecFlags[%x]", rec->flags); return -EINVAL; } @@ -1783,10 +1881,8 @@ static int decode_record(struct net_buf_simple *buf, if (BT_OTS_DIR_LIST_GET_FLAG_PROPERTIES(rec->flags)) { if ((start_len - buf->len) + sizeof(uint32_t) > rec->len) { - LOG_WRN("incorrect DirListing record, reclen %u " - "flags indicates properties, too short", - rec->len); - LOG_INF("flags 0x%x", rec->flags); + LOG_WRN("OtsCliDirRecTooShortProps[%u]", rec->len); + LOG_DBG("OtsCliDirRecFlags[%x]", rec->flags); return -EINVAL; } @@ -1800,6 +1896,7 @@ static int decode_record(struct net_buf_simple *buf, return rec->len; } +__attribute__((unused)) int bt_ots_client_decode_dirlisting(uint8_t *data, uint16_t length, bt_ots_client_dirlisting_cb cb) { @@ -1816,18 +1913,21 @@ int bt_ots_client_decode_dirlisting(uint8_t *data, uint16_t length, while (net_buf.len) { int ret; + /* Optional fields written only when flagged; clear between records. */ + memset(&record, 0, sizeof(record)); + count++; if (net_buf.len < sizeof(uint16_t)) { - LOG_WRN("incorrect DirListing record, len %u too short", net_buf.len); + LOG_WRN("OtsCliDirRecBufTooShort[%u]", net_buf.len); return -EINVAL; } - LOG_DBG("Decoding record %u", count); + LOG_DBG("OtsCliDecodeRec[%d]", count); ret = decode_record(&net_buf, &record); if (ret < 0) { - LOG_WRN("DirListing, record %u invalid", count); + LOG_WRN("OtsCliDirRecInv[%d]", count); return ret; } @@ -1844,59 +1944,59 @@ int bt_ots_client_decode_dirlisting(uint8_t *data, uint16_t length, void bt_ots_metadata_display(struct bt_ots_obj_metadata *metadata, uint16_t count) { - LOG_INF("--- Displaying %u metadata records ---", count); + LOG_INF("OtsCliDisplayMetaRecords[%u]", count); for (int i = 0; i < count; i++) { char t[BT_OTS_OBJ_ID_STR_LEN]; (void)bt_ots_obj_id_to_str(metadata[i].id, t, sizeof(t)); - LOG_INF("Object ID: 0x%s", t); - LOG_INF("Object name: %s", metadata[i].name_c); - LOG_INF("Object Current Size: %u", metadata[i].size.cur); - LOG_INF("Object Allocate Size: %u", metadata[i].size.alloc); + LOG_INF("OtsCliObjId[%s]", t); + LOG_INF("OtsCliObjName[%s]", metadata[i].name_c); + LOG_INF("OtsCliObjCurSize[%u]", metadata[i].size.cur); + LOG_INF("OtsCliObjAllocSize[%u]", metadata[i].size.alloc); if (!bt_uuid_cmp(&metadata[i].type.uuid, BT_UUID_OTS_TYPE_MPL_ICON)) { - LOG_INF("Type: Icon Obj Type"); + LOG_INF("OtsCliObjTypeIcon"); } else if (!bt_uuid_cmp(&metadata[i].type.uuid, BT_UUID_OTS_TYPE_TRACK_SEGMENT)) { - LOG_INF("Type: Track Segment Obj Type"); + LOG_INF("OtsCliObjTypeTrackSegment"); } else if (!bt_uuid_cmp(&metadata[i].type.uuid, BT_UUID_OTS_TYPE_TRACK)) { - LOG_INF("Type: Track Obj Type"); + LOG_INF("OtsCliObjTypeTrack"); } else if (!bt_uuid_cmp(&metadata[i].type.uuid, BT_UUID_OTS_TYPE_GROUP)) { - LOG_INF("Type: Group Obj Type"); + LOG_INF("OtsCliObjTypeGroup"); } else if (!bt_uuid_cmp(&metadata[i].type.uuid, BT_UUID_OTS_DIRECTORY_LISTING)) { - LOG_INF("Type: Directory Listing"); + LOG_INF("OtsCliObjTypeDirListing"); } - LOG_INF("Properties:0x%x", metadata[i].props); + LOG_INF("OtsCliProps[%x]", metadata[i].props); if (BT_OTS_OBJ_GET_PROP_APPEND(metadata[i].props)) { - LOG_INF(" - append permitted"); + LOG_INF("OtsCliPropAppend"); } if (BT_OTS_OBJ_GET_PROP_DELETE(metadata[i].props)) { - LOG_INF(" - delete permitted"); + LOG_INF("OtsCliPropDelete"); } if (BT_OTS_OBJ_GET_PROP_EXECUTE(metadata[i].props)) { - LOG_INF(" - execute permitted"); + LOG_INF("OtsCliPropExecute"); } if (BT_OTS_OBJ_GET_PROP_MARKED(metadata[i].props)) { - LOG_INF(" - marked"); + LOG_INF("OtsCliPropMarked"); } if (BT_OTS_OBJ_GET_PROP_PATCH(metadata[i].props)) { - LOG_INF(" - patch permitted"); + LOG_INF("OtsCliPropPatch"); } if (BT_OTS_OBJ_GET_PROP_READ(metadata[i].props)) { - LOG_INF(" - read permitted"); + LOG_INF("OtsCliPropRd"); } if (BT_OTS_OBJ_GET_PROP_TRUNCATE(metadata[i].props)) { - LOG_INF(" - truncate permitted"); + LOG_INF("OtsCliPropTrunc"); } if (BT_OTS_OBJ_GET_PROP_WRITE(metadata[i].props)) { - LOG_INF(" - write permitted"); + LOG_INF("OtsCliPropWr"); } } } diff --git a/components/bt/esp_ble_audio/host/services/ots/ots_client_internal.h b/components/bt/esp_ble_audio/host/services/ots/ots_client_internal.h index 8109f407cf9..6bd4473d200 100644 --- a/components/bt/esp_ble_audio/host/services/ots/ots_client_internal.h +++ b/components/bt/esp_ble_audio/host/services/ots/ots_client_internal.h @@ -4,6 +4,7 @@ * For use with the Object Transfer Service Client (OTC) * * SPDX-FileCopyrightText: 2020-2022 Nordic Semiconductor ASA + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ diff --git a/components/bt/esp_ble_audio/host/services/ots/ots_dir_list.c b/components/bt/esp_ble_audio/host/services/ots/ots_dir_list.c index c6fd36f93cf..4c5cd579bcd 100644 --- a/components/bt/esp_ble_audio/host/services/ots/ots_dir_list.c +++ b/components/bt/esp_ble_audio/host/services/ots/ots_dir_list.c @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2021 Nordic Semiconductor ASA + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -146,7 +147,7 @@ static int bt_ots_dir_list_search_forward(struct bt_ots_dir_list *dir_list, void size_t rec_len = dir_list_object_record_size(obj); bt_ots_obj_id_to_str(obj->id, id_str, sizeof(id_str)); - LOG_DBG("Searching forward for offset %ld starting at %ld with object ID %s", + LOG_DBG("OtsDirListSrchFwd[%ld][%ld][%s]", (long)offset, (long)dir_list->anchor_offset, id_str); while (dir_list->anchor_offset + rec_len <= offset) { @@ -173,7 +174,7 @@ static int bt_ots_dir_list_search_backward(struct bt_ots_dir_list *dir_list, voi struct bt_gatt_ots_object *obj = dir_list->anchor_object; bt_ots_obj_id_to_str(obj->id, id_str, sizeof(id_str)); - LOG_DBG("Searching backward for offset %ld starting at %ld with object ID %s", + LOG_DBG("OtsDirListSrchBwd[%ld][%ld][%s]", (long)offset, (long)dir_list->anchor_offset, id_str); while (dir_list->anchor_offset > offset) { @@ -208,7 +209,7 @@ static int bt_ots_dir_list_search(struct bt_ots_dir_list *dir_list, void *obj_ma } else { size_t rec_len; - LOG_DBG("Offset %ld is closer to %zu than %ld, start from end", + LOG_DBG("OtsDirListSrchFromEnd[%ld][%zu][%ld]", (long)offset, last, (long)dir_list->anchor_offset); err = bt_gatt_ots_obj_manager_last_obj_get(obj_manager, &dir_list->anchor_object); if (err) { @@ -222,7 +223,7 @@ static int bt_ots_dir_list_search(struct bt_ots_dir_list *dir_list, void *obj_ma const size_t mid = dir_list->anchor_offset / 2; if (offset < mid) { - LOG_DBG("Offset %ld is closer to 0 than %ld, start from beginning", + LOG_DBG("OtsDirListSrchFromStart[%ld][%ld]", (long)offset, (long)dir_list->anchor_offset); bt_ots_dir_list_reset_anchor(dir_list, obj_manager); err = bt_ots_dir_list_search_forward(dir_list, obj_manager, offset); @@ -236,7 +237,7 @@ static int bt_ots_dir_list_search(struct bt_ots_dir_list *dir_list, void *obj_ma } bt_ots_obj_id_to_str(dir_list->anchor_object->id, id_str, sizeof(id_str)); - LOG_DBG("Found offset %ld starting at %ld in object with ID %s", + LOG_DBG("OtsDirListFoundOft[%ld][%ld][%s]", (long)offset, (long)dir_list->anchor_offset, id_str); return 0; @@ -259,10 +260,16 @@ static void dir_list_update_size(struct bt_ots_dir_list *dir_list, void *obj_man err = bt_gatt_ots_obj_manager_next_obj_get(obj_manager, obj, &obj); } while (!err); - LOG_DBG("Update directory listing current size to 0x%zx", len); + LOG_DBG("OtsDirListUpdCurSize[%zx]", len); dir_list->dir_list_obj->metadata.size.cur = len; } +void bt_ots_dir_list_content_changed(struct bt_ots_dir_list *dir_list, void *obj_manager) +{ + bt_ots_dir_list_reset_anchor(dir_list, obj_manager); + dir_list_update_size(dir_list, obj_manager); +} + void bt_ots_dir_list_selected(struct bt_ots_dir_list *dir_list, void *obj_manager, struct bt_gatt_ots_object *cur_obj) { @@ -273,8 +280,7 @@ void bt_ots_dir_list_selected(struct bt_ots_dir_list *dir_list, void *obj_manage return; } - bt_ots_dir_list_reset_anchor(dir_list, obj_manager); - dir_list_update_size(dir_list, obj_manager); + bt_ots_dir_list_content_changed(dir_list, obj_manager); } void bt_ots_dir_list_init(struct bt_ots_dir_list **dir_list, void *obj_manager) @@ -314,6 +320,16 @@ void bt_ots_dir_list_init(struct bt_ots_dir_list **dir_list, void *obj_manager) dir_list_update_size(*dir_list, obj_manager); } +void bt_ots_dir_list_deinit(struct bt_ots_dir_list **dir_list) +{ + if (*dir_list == NULL) { + return; + } + + memset(*dir_list, 0, sizeof(**dir_list)); + *dir_list = NULL; +} + ssize_t bt_ots_dir_list_content_get(struct bt_ots_dir_list *dir_list, void *obj_manager, void **data, size_t len, off_t offset) { diff --git a/components/bt/esp_ble_audio/host/services/ots/ots_dir_list_internal.h b/components/bt/esp_ble_audio/host/services/ots/ots_dir_list_internal.h index ea30cc4bf27..1124b55816d 100644 --- a/components/bt/esp_ble_audio/host/services/ots/ots_dir_list_internal.h +++ b/components/bt/esp_ble_audio/host/services/ots/ots_dir_list_internal.h @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2021 - 2022 Nordic Semiconductor ASA + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -158,7 +159,9 @@ enum { void bt_ots_dir_list_selected(struct bt_ots_dir_list *dir_list, void *obj_manager, struct bt_gatt_ots_object *cur_obj); +void bt_ots_dir_list_content_changed(struct bt_ots_dir_list *dir_list, void *obj_manager); void bt_ots_dir_list_init(struct bt_ots_dir_list **dir_list, void *obj_manager); +void bt_ots_dir_list_deinit(struct bt_ots_dir_list **dir_list); ssize_t bt_ots_dir_list_content_get(struct bt_ots_dir_list *dir_list, void *obj_manager, void **data, size_t len, off_t offset); bool bt_ots_dir_list_is_idle(const struct bt_ots_dir_list *dir_list); diff --git a/components/bt/esp_ble_audio/host/services/ots/ots_internal.h b/components/bt/esp_ble_audio/host/services/ots/ots_internal.h index a692d4c3715..0810b495a1c 100644 --- a/components/bt/esp_ble_audio/host/services/ots/ots_internal.h +++ b/components/bt/esp_ble_audio/host/services/ots/ots_internal.h @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020-2022 Nordic Semiconductor ASA + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -12,6 +13,7 @@ extern "C" { #endif #include +#include <../host/conn_internal.h> #include "ots_l2cap_internal.h" #include "ots_oacp_internal.h" #include "ots_olcp_internal.h" @@ -122,7 +124,11 @@ struct bt_gatt_ots_indicate { struct bt_gatt_attr attr; struct bt_gatt_ccc_managed_user_data ccc; bool is_enabled; - struct k_work work; + /* True from schedule until indication confirm (or indicate submit fail). */ + bool ind_in_flight; + /* Peer that wrote the control point; indication must target this conn only. */ + struct bt_conn *conn; + struct k_work_delayable work; uint8_t res[OACP_OLCP_RES_MAX_SIZE]; }; diff --git a/components/bt/esp_ble_audio/host/services/ots/ots_l2cap.c b/components/bt/esp_ble_audio/host/services/ots/ots_l2cap.c index 8e4dde8fa71..49179b43ce5 100644 --- a/components/bt/esp_ble_audio/host/services/ots/ots_l2cap.c +++ b/components/bt/esp_ble_audio/host/services/ots/ots_l2cap.c @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020-2022 Nordic Semiconductor ASA + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -46,10 +47,11 @@ static int ots_l2cap_send(struct bt_gatt_ots_l2cap *l2cap_ctx) len = MIN(l2cap_ctx->ot_chan.tx.mtu, CONFIG_BT_OTS_L2CAP_CHAN_TX_MTU); len = MIN(len, l2cap_ctx->tx.len - l2cap_ctx->tx.len_sent); - /* Prepare buffer for sending. */ - buf = net_buf_alloc(&ot_chan_tx_pool, K_FOREVER); + /* Single-buffer pool, and iso_task is both the only allocator and the only + * path that releases it, so waiting here would self-deadlock. */ + buf = net_buf_alloc(&ot_chan_tx_pool, K_NO_WAIT); if (buf == NULL) { - LOG_ERR("OtsTxBufAllocFail"); + LOG_ERR("OtsL2capTxBufAllocFail"); return -ENOMEM; } @@ -58,7 +60,7 @@ static int ots_l2cap_send(struct bt_gatt_ots_l2cap *l2cap_ctx) ret = bt_l2cap_chan_send(&l2cap_ctx->ot_chan.chan, buf); if (ret < 0) { - LOG_ERR("Unable to send data over CoC: %d", ret); + LOG_WRN("OtsL2capChanSendFail[%d]", ret); net_buf_unref(buf); return -ENOEXEC; @@ -67,7 +69,7 @@ static int ots_l2cap_send(struct bt_gatt_ots_l2cap *l2cap_ctx) /* Mark that L2CAP TX was accepted. */ l2cap_ctx->tx.len_sent += len; - LOG_DBG("Sending TX chunk with %d bytes on L2CAP CoC", len); + LOG_DBG("OtsL2capTxChunk[%u]", len); return 0; } @@ -75,9 +77,9 @@ static int ots_l2cap_send(struct bt_gatt_ots_l2cap *l2cap_ctx) #if (CONFIG_BT_OTS_L2CAP_CHAN_RX_MTU > BT_L2CAP_SDU_RX_MTU) static struct net_buf *l2cap_alloc_buf(struct bt_l2cap_chan *chan) { - LOG_DBG("Channel %p allocating buffer", chan); + LOG_DBG("OtsL2capAllocBuf"); - return net_buf_alloc(&ot_chan_rx_pool, K_FOREVER); + return net_buf_alloc(&ot_chan_rx_pool, K_NO_WAIT); } #endif @@ -86,19 +88,18 @@ static void l2cap_sent(struct bt_l2cap_chan *chan) struct bt_l2cap_le_chan *l2chan = CONTAINER_OF(chan, struct bt_l2cap_le_chan, chan); struct bt_gatt_ots_l2cap *l2cap_ctx; - LOG_DBG("Outgoing data channel %p transmitted", chan); + LOG_DBG("OtsL2capSent"); l2cap_ctx = CONTAINER_OF(l2chan, struct bt_gatt_ots_l2cap, ot_chan); /* Ongoing TX - sending next chunk. */ if (l2cap_ctx->tx.len != l2cap_ctx->tx.len_sent) { if (ots_l2cap_send(l2cap_ctx)) { - /* Send failed - clean up TX state to unblock channel. */ - LOG_ERR("Failed to send next chunk, aborting TX"); - memset(&l2cap_ctx->tx, 0, sizeof(l2cap_ctx->tx)); - if (l2cap_ctx->tx_done) { - l2cap_ctx->tx_done(l2cap_ctx, chan->conn); - } + /* Do not call tx_done: it means success to upper layers + * (oacp_read would skip unread bytes; write_obj_tx_done + * would report a full write). Abort via disconnect → closed. */ + LOG_WRN("OtsL2capTxAbort"); + (void)bt_l2cap_chan_disconnect(chan); } return; @@ -107,7 +108,7 @@ static void l2cap_sent(struct bt_l2cap_chan *chan) /* TX completed - notify upper layers and clean up. */ memset(&l2cap_ctx->tx, 0, sizeof(l2cap_ctx->tx)); - LOG_DBG("Scheduled TX on L2CAP CoC is complete"); + LOG_DBG("OtsL2capTxComp"); if (l2cap_ctx->tx_done) { l2cap_ctx->tx_done(l2cap_ctx, chan->conn); @@ -119,7 +120,7 @@ static int l2cap_recv(struct bt_l2cap_chan *chan, struct net_buf *buf) struct bt_l2cap_le_chan *l2chan = CONTAINER_OF(chan, struct bt_l2cap_le_chan, chan); struct bt_gatt_ots_l2cap *l2cap_ctx; - LOG_DBG("Incoming data channel %p received", chan); + LOG_DBG("OtsL2capRecv"); l2cap_ctx = CONTAINER_OF(l2chan, struct bt_gatt_ots_l2cap, ot_chan); @@ -132,12 +133,12 @@ static int l2cap_recv(struct bt_l2cap_chan *chan, struct net_buf *buf) static void l2cap_status(struct bt_l2cap_chan *chan, atomic_t *status) { - LOG_DBG("Channel %p status %lu", chan, atomic_get(status)); + LOG_DBG("OtsL2capStatus[%u]", atomic_get(status)); } static void l2cap_connected(struct bt_l2cap_chan *chan) { - LOG_DBG("Channel %p connected", chan); + LOG_DBG("OtsL2capConnected"); } static void l2cap_disconnected(struct bt_l2cap_chan *chan) @@ -145,7 +146,7 @@ static void l2cap_disconnected(struct bt_l2cap_chan *chan) struct bt_l2cap_le_chan *l2chan = CONTAINER_OF(chan, struct bt_l2cap_le_chan, chan); struct bt_gatt_ots_l2cap *l2cap_ctx; - LOG_DBG("Channel %p disconnected", chan); + LOG_DBG("OtsL2capDisconnected"); l2cap_ctx = CONTAINER_OF(l2chan, struct bt_gatt_ots_l2cap, ot_chan); @@ -155,6 +156,12 @@ static void l2cap_disconnected(struct bt_l2cap_chan *chan) if (l2cap_ctx->closed) { l2cap_ctx->closed(l2cap_ctx, chan->conn); } + + /* Contexts are reused via find_free_l2cap_ctx; drop procedure hooks + * so a later accept/connect cannot invoke stale callbacks. */ + l2cap_ctx->closed = NULL; + l2cap_ctx->rx_done = NULL; + l2cap_ctx->tx_done = NULL; } static const struct bt_l2cap_chan_ops l2cap_ops = { @@ -173,7 +180,7 @@ static inline void l2cap_chan_init(struct bt_l2cap_le_chan *chan) chan->rx.mtu = CONFIG_BT_OTS_L2CAP_CHAN_RX_MTU; chan->chan.ops = &l2cap_ops; - LOG_DBG("RX MTU set to %u", chan->rx.mtu); + LOG_DBG("OtsL2capRxMtu[%u]", chan->rx.mtu); } static struct bt_gatt_ots_l2cap *find_free_l2cap_ctx(void) @@ -196,7 +203,7 @@ static int l2cap_accept(struct bt_conn *conn, struct bt_l2cap_server *server, { struct bt_gatt_ots_l2cap *l2cap_ctx; - LOG_DBG("Incoming conn %p", (void *)conn); + LOG_DBG("OtsL2capIncomingConn"); l2cap_ctx = find_free_l2cap_ctx(); if (l2cap_ctx) { @@ -216,7 +223,7 @@ static struct bt_l2cap_server l2cap_server = { .accept = l2cap_accept, }; -static int bt_gatt_ots_l2cap_init(void) +int bt_gatt_ots_l2cap_init(void) { int err; @@ -224,11 +231,11 @@ static int bt_gatt_ots_l2cap_init(void) err = bt_l2cap_server_register(&l2cap_server); if (err) { - LOG_ERR("Unable to register OTS PSM"); + LOG_ERR("OtsL2capPsmRegFail"); return err; } - LOG_DBG("Initialized OTS L2CAP"); + LOG_DBG("OtsL2capInit"); return 0; } @@ -245,7 +252,7 @@ int bt_gatt_ots_l2cap_send(struct bt_gatt_ots_l2cap *l2cap_ctx, int err; if (l2cap_ctx->tx.len != 0) { - LOG_ERR("L2CAP TX in progress"); + LOG_WRN("OtsL2capTxInProgress"); return -EAGAIN; } @@ -253,11 +260,11 @@ int bt_gatt_ots_l2cap_send(struct bt_gatt_ots_l2cap *l2cap_ctx, l2cap_ctx->tx.data = data; l2cap_ctx->tx.len = len; - LOG_DBG("Starting TX on L2CAP CoC with %d byte packet", len); + LOG_DBG("OtsL2capTxStart[%u]", len); err = ots_l2cap_send(l2cap_ctx); if (err) { - LOG_ERR("Unable to send data over CoC: %d", err); + LOG_WRN("OtsL2capSendFail[%d]", err); memset(&l2cap_ctx->tx, 0, sizeof(l2cap_ctx->tx)); return err; @@ -268,6 +275,10 @@ int bt_gatt_ots_l2cap_send(struct bt_gatt_ots_l2cap *l2cap_ctx, int bt_gatt_ots_l2cap_register(struct bt_gatt_ots_l2cap *l2cap_ctx) { + if (sys_slist_find(&channels, &l2cap_ctx->node, NULL)) { + return -EALREADY; + } + sys_slist_append(&channels, &l2cap_ctx->node); return 0; @@ -275,6 +286,11 @@ int bt_gatt_ots_l2cap_register(struct bt_gatt_ots_l2cap *l2cap_ctx) int bt_gatt_ots_l2cap_unregister(struct bt_gatt_ots_l2cap *l2cap_ctx) { + if (l2cap_ctx->ot_chan.chan.conn) { + LOG_WRN("OtsL2capUnregBusy"); + return -EBUSY; + } + sys_slist_find_and_remove(&channels, &l2cap_ctx->node); return 0; @@ -288,31 +304,41 @@ int bt_gatt_ots_l2cap_connect(struct bt_conn *conn, struct bt_gatt_ots_l2cap *ctx; if (!conn) { - LOG_WRN("Invalid Connection"); + LOG_WRN("OtsL2capConnNull"); return -ENOTCONN; } if (!l2cap_ctx) { - LOG_WRN("Invalid context"); + LOG_WRN("OtsL2capCtxNull"); return -EINVAL; } + /* Callers owning a context pass it in, so the context they later use in + * their callbacks is the one actually connected. The global free-list + * lookup would otherwise hand out another instance's (or the server's) + * context whenever more than one is registered. */ + ctx = *l2cap_ctx; *l2cap_ctx = NULL; - ctx = find_free_l2cap_ctx(); if (!ctx) { - return -ENOMEM; + ctx = find_free_l2cap_ctx(); + if (!ctx) { + return -ENOMEM; + } + } else if (ctx->ot_chan.chan.conn) { + LOG_WRN("OtsL2capCtxAlreadyConnected"); + return -EBUSY; } l2cap_chan_init(&ctx->ot_chan); (void)memset(&ctx->tx, 0, sizeof(ctx->tx)); - LOG_DBG("Connecting L2CAP CoC"); + LOG_DBG("OtsL2capConnecting"); err = bt_l2cap_chan_connect(conn, &ctx->ot_chan.chan, BT_GATT_OTS_L2CAP_PSM); if (err) { - LOG_WRN("Unable to connect to psm %u (err %d)", BT_GATT_OTS_L2CAP_PSM, err); + LOG_WRN("OtsL2capConnFail[%u][%d]", BT_GATT_OTS_L2CAP_PSM, err); } else { - LOG_DBG("L2CAP connection pending"); + LOG_DBG("OtsL2capConnPending"); *l2cap_ctx = ctx; } @@ -323,6 +349,3 @@ int bt_gatt_ots_l2cap_disconnect(struct bt_gatt_ots_l2cap *l2cap_ctx) { return bt_l2cap_chan_disconnect(&l2cap_ctx->ot_chan.chan); } - -SYS_INIT(bt_gatt_ots_l2cap_init, APPLICATION, - CONFIG_APPLICATION_INIT_PRIORITY); diff --git a/components/bt/esp_ble_audio/host/services/ots/ots_l2cap_internal.h b/components/bt/esp_ble_audio/host/services/ots/ots_l2cap_internal.h index 765c0f88530..24ab91d6d50 100644 --- a/components/bt/esp_ble_audio/host/services/ots/ots_l2cap_internal.h +++ b/components/bt/esp_ble_audio/host/services/ots/ots_l2cap_internal.h @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020-2022 Nordic Semiconductor ASA + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -49,11 +50,12 @@ int bt_gatt_ots_l2cap_unregister(struct bt_gatt_ots_l2cap *l2cap_ctx); /** @brief Connect OTS L2CAP channel * * This function is for the OTS client to make an L2CAP connection to - * the OTS server. One of the available registered L2CAP contexts - * will be used for the connection. + * the OTS server. * - * @param[in] conn Connection pointer - * @param[out] l2cap_ctx The context that was connected + * @param[in] conn Connection pointer + * @param[in,out] l2cap_ctx On entry, the caller's context to connect, or NULL + * to pick any free registered one. On success, set + * to the connected context; NULL on failure. * * @return 0 in case of success or negative value in case of error */ diff --git a/components/bt/esp_ble_audio/host/services/ots/ots_oacp.c b/components/bt/esp_ble_audio/host/services/ots/ots_oacp.c index 162787a719d..f56d44d83c6 100644 --- a/components/bt/esp_ble_audio/host/services/ots/ots_oacp.c +++ b/components/bt/esp_ble_audio/host/services/ots/ots_oacp.c @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Nordic Semiconductor ASA + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -37,13 +38,16 @@ static void oacp_l2cap_closed(struct bt_gatt_ots_l2cap *l2cap_ctx, ots = CONTAINER_OF(l2cap_ctx, struct bt_ots, l2cap); + /* Always drop procedure sinks — cur_obj may already be cleared (e.g. + * deleted while idle after a failed send that left callbacks set). */ + l2cap_ctx->rx_done = NULL; + l2cap_ctx->tx_done = NULL; + if (!ots->cur_obj) { return; } ots->cur_obj->state.type = BT_GATT_OTS_OBJECT_IDLE_STATE; - l2cap_ctx->rx_done = NULL; - l2cap_ctx->tx_done = NULL; } #if defined(CONFIG_BT_OTS_OACP_CREATE_SUPPORT) @@ -61,14 +65,19 @@ static enum bt_gatt_ots_oacp_res_code oacp_create_proc_validate( }; bt_uuid_to_str(¶m.type.uuid, str, BT_UUID_STR_LEN); - LOG_DBG("Validating Create procedure with size: 0x%08X and " - "type: %s", param.size, str); + LOG_DBG("OtsOacpValCreate[%08x][%s]", param.size, str); if (!BT_OTS_OACP_GET_FEAT_CREATE(ots->features.oacp)) { - LOG_DBG("Create Procedure is not supported."); + LOG_DBG("OtsOacpCreateNotSupp"); return BT_GATT_OTS_OACP_RES_OPCODE_NOT_SUP; } + if (ots->cur_obj && + ots->cur_obj->state.type != BT_GATT_OTS_OBJECT_IDLE_STATE) { + LOG_DBG("OtsOacpCreateObjLocked"); + return BT_GATT_OTS_OACP_RES_OBJ_LOCKED; + } + err = bt_ots_obj_add_internal(ots, conn, ¶m, &obj); if (err) { goto exit; @@ -76,21 +85,21 @@ static enum bt_gatt_ots_oacp_res_code oacp_create_proc_validate( /* Verify Initialization Metadata */ if (strlen(obj->metadata.name) > 0) { - LOG_ERR("Object name shall be a zero length string after object creation."); + LOG_ERR("OtsOacpObjNameNotEmpty"); (void)bt_ots_obj_delete(ots, obj->id); err = -ECANCELED; goto exit; } if (obj->metadata.size.cur > 0) { - LOG_ERR("Object current size must be 0."); + LOG_ERR("OtsOacpObjCurSizeNotZero"); (void)bt_ots_obj_delete(ots, obj->id); err = -ECANCELED; goto exit; } if (!BT_OTS_OBJ_GET_PROP_WRITE(obj->metadata.props)) { - LOG_ERR("Created object must have write property."); + LOG_ERR("OtsOacpObjNoWrProp"); (void)bt_ots_obj_delete(ots, obj->id); err = -ECANCELED; goto exit; @@ -99,7 +108,7 @@ static enum bt_gatt_ots_oacp_res_code oacp_create_proc_validate( ots->cur_obj = obj; ots->cur_obj->state.type = BT_GATT_OTS_OBJECT_IDLE_STATE; - LOG_DBG("Create procedure is complete"); + LOG_DBG("OtsOacpCreateComp"); exit: switch (err) { @@ -127,27 +136,27 @@ static enum bt_gatt_ots_oacp_res_code oacp_delete_proc_validate( int err; if (!BT_OTS_OACP_GET_FEAT_DELETE(ots->features.oacp)) { - LOG_DBG("Delete Procedure is not supported."); + LOG_DBG("OtsOacpDeleteNotSupp"); return BT_GATT_OTS_OACP_RES_OPCODE_NOT_SUP; } if (!ots->cur_obj) { - LOG_DBG("No object is selected."); + LOG_DBG("OtsOacpNoObjSel"); return BT_GATT_OTS_OACP_RES_INV_OBJ; } if (!BT_OTS_OBJ_GET_PROP_DELETE(ots->cur_obj->metadata.props)) { - LOG_DBG("Object properties do not permit deletion."); + LOG_DBG("OtsOacpDeleteNotPermitted"); return BT_GATT_OTS_OACP_RES_NOT_PERMITTED; } err = bt_ots_obj_delete(ots, ots->cur_obj->id); if (err) { - LOG_ERR("Deleting object during Delete procedure failed: %d", err); + LOG_ERR("OtsOacpDeleteObjFail[%d]", err); goto exit; } - LOG_DBG("Delete procedure is complete"); + LOG_DBG("OtsOacpDeleteComp"); exit: switch (err) { @@ -173,8 +182,7 @@ static enum bt_gatt_ots_oacp_res_code oacp_checksum_proc_validate( int err; uint32_t checksum; - LOG_DBG("Validating Checksum procedure with offset: 0x%08X and " - "length: 0x%08X", params->offset, params->len); + LOG_DBG("OtsOacpValChecksum[%08x][%08x]", params->offset, params->len); if (!ots->cur_obj) { return BT_GATT_OTS_OACP_RES_INV_OBJ; @@ -201,8 +209,7 @@ static enum bt_gatt_ots_oacp_res_code oacp_checksum_proc_validate( checksum = bt_ots_client_calc_checksum((const uint8_t *)obj_data, params->len); net_buf_simple_add_le32(resp_param, checksum); - LOG_DBG("Calculate from offset %u len %u checksum 0x%08x", params->offset, - params->len, checksum); + LOG_DBG("OtsOacpChecksumCalc[%u][%u][%08x]", params->offset, params->len, checksum); return BT_GATT_OTS_OACP_RES_SUCCESS; } else { return BT_GATT_OTS_OACP_RES_OPER_FAILED; @@ -217,8 +224,12 @@ static enum bt_gatt_ots_oacp_res_code oacp_read_proc_validate( { const struct bt_gatt_ots_oacp_read_params *params = &proc->read_params; - LOG_DBG("Validating Read procedure with offset: 0x%08X and " - "length: 0x%08X", params->offset, params->len); + LOG_DBG("OtsOacpValRd[%08x][%08x]", params->offset, params->len); + + if (!BT_OTS_OACP_GET_FEAT_READ(ots->features.oacp)) { + LOG_DBG("OtsOacpRdNotSupp"); + return BT_GATT_OTS_OACP_RES_OPCODE_NOT_SUP; + } if (!ots->cur_obj) { return BT_GATT_OTS_OACP_RES_INV_OBJ; @@ -246,7 +257,7 @@ static enum bt_gatt_ots_oacp_res_code oacp_read_proc_validate( memcpy(&ots->cur_obj->state.read_op.oacp_params, &proc->read_params, sizeof(ots->cur_obj->state.read_op.oacp_params)); - LOG_DBG("Read procedure is accepted"); + LOG_DBG("OtsOacpRdAccepted"); return BT_GATT_OTS_OACP_RES_SUCCESS; } @@ -259,15 +270,14 @@ static enum bt_gatt_ots_oacp_res_code oacp_write_proc_validate( { struct bt_gatt_ots_oacp_write_params *params = &proc->write_params; - LOG_DBG("Validating Write procedure with offset: 0x%08X and " - "length: 0x%08X", params->offset, params->len); + LOG_DBG("OtsOacpValWr[%08x][%08x]", params->offset, params->len); if (!ots->cur_obj) { return BT_GATT_OTS_OACP_RES_INV_OBJ; } if (!BT_OTS_OACP_GET_FEAT_WRITE(ots->features.oacp)) { - LOG_DBG("Write Procedure is not supported."); + LOG_DBG("OtsOacpWrNotSupp"); return BT_GATT_OTS_OACP_RES_OPCODE_NOT_SUP; } @@ -302,8 +312,9 @@ static enum bt_gatt_ots_oacp_res_code oacp_write_proc_validate( return BT_GATT_OTS_OACP_RES_INV_PARAM; } - /* append is not supported */ - if ((params->offset + (uint64_t) params->len) > ots->cur_obj->metadata.size.cur) { + /* Growing the object is allowed (see the size.cur update in + * oacp_write_proc_cb), but never past what the application allocated. */ + if ((params->offset + (uint64_t) params->len) > ots->cur_obj->metadata.size.alloc) { return BT_GATT_OTS_OACP_RES_INV_PARAM; } @@ -318,7 +329,7 @@ static enum bt_gatt_ots_oacp_res_code oacp_write_proc_validate( memcpy(&ots->cur_obj->state.write_op.oacp_params, params, sizeof(ots->cur_obj->state.write_op.oacp_params)); - LOG_DBG("Write procedure is accepted"); + LOG_DBG("OtsOacpWrAccepted"); return BT_GATT_OTS_OACP_RES_SUCCESS; } @@ -463,14 +474,14 @@ static void oacp_read_proc_cb(struct bt_gatt_ots_l2cap *l2cap_ctx, ots = CONTAINER_OF(l2cap_ctx, struct bt_ots, l2cap); if (!ots->cur_obj) { - LOG_ERR("OTS Read operation failed: no current object"); + LOG_ERR("OtsOacpRdNoCurObj"); return; } if (ots->cb->obj_read == NULL && !(IS_ENABLED(CONFIG_BT_OTS_DIR_LIST_OBJ) && ots->cur_obj->id == OTS_OBJ_ID_DIR_LIST)) { ots->cur_obj->state.type = BT_GATT_OTS_OBJECT_IDLE_STATE; - LOG_ERR("OTS Read operation failed: there is no OTS Read callback"); + LOG_ERR("OtsOacpRdNoCb"); return; } @@ -479,10 +490,10 @@ static void oacp_read_proc_cb(struct bt_gatt_ots_l2cap *l2cap_ctx, offset = read_op->oacp_params.offset + read_op->sent_len; if (read_op->sent_len >= read_op->oacp_params.len) { - LOG_DBG("OACP Read Op over L2CAP is completed"); + LOG_DBG("OtsOacpRdL2capComp"); if (read_op->sent_len > read_op->oacp_params.len) { - LOG_WRN("More bytes sent that the client requested"); + LOG_WRN("OtsOacpSentMoreThanReq"); } ots->cur_obj->state.type = BT_GATT_OTS_OBJECT_IDLE_STATE; @@ -508,7 +519,7 @@ static void oacp_read_proc_cb(struct bt_gatt_ots_l2cap *l2cap_ctx, } if (len < 0) { - LOG_ERR("OCAP Read Op failed with error: %zd", len); + LOG_ERR("OtsOacpRdOpFail[%zd]", len); bt_gatt_ots_l2cap_disconnect(&ots->l2cap); ots->cur_obj->state.type = BT_GATT_OTS_OBJECT_IDLE_STATE; @@ -516,12 +527,31 @@ static void oacp_read_proc_cb(struct bt_gatt_ots_l2cap *l2cap_ctx, return; } + /* Early EOF while bytes remain: do not L2CAP-send len 0 (tx_done would + * re-enter with sent_len unchanged → busy loop). + */ + if (len == 0) { + LOG_WRN("OtsOacpRdEofEarly[%u][%u]", read_op->sent_len, + read_op->oacp_params.len); + + bt_gatt_ots_l2cap_disconnect(&ots->l2cap); + ots->cur_obj->state.type = BT_GATT_OTS_OBJECT_IDLE_STATE; + + if (IS_ENABLED(CONFIG_BT_OTS_DIR_LIST_OBJ) && + ots->cur_obj->id == OTS_OBJ_ID_DIR_LIST) { + return; + } + + ots->cb->obj_read(ots, conn, ots->cur_obj->id, NULL, 0, offset); + return; + } + ots->l2cap.tx_done = oacp_read_proc_cb; ots->l2cap.closed = oacp_l2cap_closed; err = bt_gatt_ots_l2cap_send(&ots->l2cap, obj_chunk, len); if (err) { - LOG_ERR("L2CAP CoC error: %d while trying to execute OACP " - "Read procedure", err); + LOG_WRN("OtsOacpL2capErr[%d]", err); + bt_gatt_ots_l2cap_disconnect(&ots->l2cap); ots->cur_obj->state.type = BT_GATT_OTS_OBJECT_IDLE_STATE; } else { read_op->sent_len += len; @@ -534,14 +564,13 @@ static void oacp_read_proc_execute(struct bt_ots *ots, struct bt_gatt_ots_oacp_read_params *params; if (!ots->cur_obj) { - LOG_ERR("Invalid Current Object on OACP Read procedure"); + LOG_ERR("OtsOacpRdInvCurObj"); return; } params = &ots->cur_obj->state.read_op.oacp_params; - LOG_DBG("Executing Read procedure with offset: 0x%08X and " - "length: 0x%08X", params->offset, params->len); + LOG_DBG("OtsOacpExecRd[%08x][%08x]", params->offset, params->len); oacp_read_proc_cb(&ots->l2cap, conn); } @@ -560,14 +589,21 @@ static ssize_t oacp_write_proc_cb(struct bt_gatt_ots_l2cap *l2cap_ctx, ots = CONTAINER_OF(l2cap_ctx, struct bt_ots, l2cap); if (!ots->cur_obj) { - LOG_ERR("Invalid Current Object on OACP Write procedure"); + LOG_ERR("OtsOacpWrInvCurObj"); return -ENODEV; } + /* Reading state.write_op while another procedure owns the union would + * confuse recv_len with the read op's fields and underflow len below. */ + if (ots->cur_obj->state.type != BT_GATT_OTS_OBJECT_WRITE_OP_STATE) { + LOG_ERR("OtsOacpWrInvState[%d]", ots->cur_obj->state.type); + return -EINVAL; + } + if (!ots->cb->obj_write) { - LOG_ERR("OTS Write operation failed: " - "there is no OTS Write callback"); + LOG_ERR("OtsOacpWrNoCb"); ots->cur_obj->state.type = BT_GATT_OTS_OBJECT_IDLE_STATE; + l2cap_ctx->rx_done = NULL; return -ENODEV; } @@ -575,7 +611,7 @@ static ssize_t oacp_write_proc_cb(struct bt_gatt_ots_l2cap *l2cap_ctx, offset = write_op->oacp_params.offset + write_op->recv_len; len = buf->len; if (write_op->recv_len + len > write_op->oacp_params.len) { - LOG_WRN("More bytes received than the client indicated"); + LOG_WRN("OtsOacpRecvMoreThanInd"); len = write_op->oacp_params.len - write_op->recv_len; } rem = write_op->oacp_params.len - (write_op->recv_len + len); @@ -591,24 +627,24 @@ static ssize_t oacp_write_proc_cb(struct bt_gatt_ots_l2cap *l2cap_ctx, * released by the l2cap layer. This is an unsupported use case at the moment. */ if (rc == -EINPROGRESS) { - LOG_ERR("Unsupported error code %zd returned by object write callback", rc); + LOG_ERR("OtsOacpWrCbUnsuppRc[%zd]", rc); } - LOG_ERR("OTS Write operation failed with error: %zd", rc); + LOG_ERR("OtsOacpWrOpFail[%zd]", rc); ots->cur_obj->state.type = BT_GATT_OTS_OBJECT_IDLE_STATE; } else { /* Return -EIO as an error if all of data was not written */ if (rc != len) { len = 0; rc = -EIO; - LOG_ERR("OTS Write operation partially failed"); + LOG_ERR("OtsOacpWrPartialFail"); ots->cur_obj->state.type = BT_GATT_OTS_OBJECT_IDLE_STATE; } } write_op->recv_len += len; if (write_op->recv_len == write_op->oacp_params.len) { - LOG_DBG("OACP Write Op over L2CAP is completed"); + LOG_DBG("OtsOacpWrL2capComp"); ots->cur_obj->state.type = BT_GATT_OTS_OBJECT_IDLE_STATE; } @@ -616,6 +652,13 @@ static ssize_t oacp_write_proc_cb(struct bt_gatt_ots_l2cap *l2cap_ctx, ots->cur_obj->metadata.size.cur = offset + len; } + /* Back to idle means this write is over (completed or failed above). The + * CoC stays open, so drop the sink or a later procedure's inbound data + * would still land here. */ + if (ots->cur_obj->state.type == BT_GATT_OTS_OBJECT_IDLE_STATE) { + l2cap_ctx->rx_done = NULL; + } + return rc; } #endif @@ -626,10 +669,27 @@ static void oacp_ind_cb(struct bt_conn *conn, { struct bt_ots *ots = (struct bt_ots *) params->attr->user_data; - LOG_DBG("Received OACP Indication ACK with status: 0x%04X", err); + LOG_DBG("OtsOacpRecvIndAck[%04x]", err); + + ots->oacp_ind.ind_in_flight = false; + ots->oacp_ind.conn = NULL; if (!ots->cur_obj) { - LOG_DBG("There is no object associated with this ACK"); + LOG_DBG("OtsOacpNoObjForAck"); + return; + } + + if (err) { + /* Client did not ACK the OACP response — do not start L2CAP I/O. + * Leave READ/WRITE would permanently OBJ_LOCKED. */ + LOG_WRN("OtsOacpIndFail[%02x][%d]", err, ots->cur_obj->state.type); + if (ots->cur_obj->state.type == BT_GATT_OTS_OBJECT_READ_OP_STATE || + ots->cur_obj->state.type == BT_GATT_OTS_OBJECT_WRITE_OP_STATE) { + ots->cur_obj->state.type = BT_GATT_OTS_OBJECT_IDLE_STATE; + ots->l2cap.rx_done = NULL; + ots->l2cap.tx_done = NULL; + (void)bt_gatt_ots_l2cap_disconnect(&ots->l2cap); + } return; } @@ -644,12 +704,13 @@ static void oacp_ind_cb(struct bt_conn *conn, /* procedure is not in progress and was already completed */ break; default: - LOG_ERR("Unsupported OTS state: %d", ots->cur_obj->state.type); + LOG_ERR("OtsOacpUnsuppState[%d]", ots->cur_obj->state.type); break; } } -static void oacp_ind_send(const struct bt_gatt_attr *oacp_attr, +static void oacp_ind_send(struct bt_conn *conn, + const struct bt_gatt_attr *oacp_attr, struct bt_gatt_ots_oacp_proc oacp_proc, enum bt_gatt_ots_oacp_res_code oacp_status, struct net_buf_simple *resp_param) @@ -657,6 +718,7 @@ static void oacp_ind_send(const struct bt_gatt_attr *oacp_attr, struct bt_ots *ots = (struct bt_ots *) oacp_attr->user_data; uint8_t *oacp_res = ots->oacp_ind.res; uint16_t oacp_res_len = 0; + int err; /* Encode OACP Response */ oacp_res[oacp_res_len++] = BT_GATT_OTS_OACP_PROC_RESP; @@ -664,7 +726,7 @@ static void oacp_ind_send(const struct bt_gatt_attr *oacp_attr, oacp_res[oacp_res_len++] = oacp_status; if (oacp_proc.type == BT_GATT_OTS_OACP_PROC_CHECKSUM_CALC && - oacp_status == BT_GATT_OTS_OACP_RES_SUCCESS) { + oacp_status == BT_GATT_OTS_OACP_RES_SUCCESS) { sys_put_le32(net_buf_simple_pull_le32(resp_param), (oacp_res + oacp_res_len)); oacp_res_len += sizeof(uint32_t); } @@ -677,9 +739,16 @@ static void oacp_ind_send(const struct bt_gatt_attr *oacp_attr, ots->oacp_ind.params.data = oacp_res; ots->oacp_ind.params.len = oacp_res_len; - LOG_DBG("Sending OACP indication"); + LOG_DBG("OtsOacpSendInd[%u]", conn->handle); - k_work_submit(&ots->oacp_ind.work); + ots->oacp_ind.conn = conn; + ots->oacp_ind.ind_in_flight = true; + err = k_work_schedule(&ots->oacp_ind.work, K_NO_WAIT_ASYNC); + if (err < 0) { + LOG_ERR("OtsOacpSchIndFail[%u][%d]", conn->handle, err); + ots->oacp_ind.ind_in_flight = false; + ots->oacp_ind.conn = NULL; + } } ssize_t bt_gatt_ots_oacp_write(struct bt_conn *conn, @@ -693,20 +762,21 @@ ssize_t bt_gatt_ots_oacp_write(struct bt_conn *conn, struct bt_ots *ots = (struct bt_ots *) attr->user_data; NET_BUF_SIMPLE_DEFINE(resp_param, sizeof(uint32_t)); - LOG_DBG("Object Action Control Point GATT Write Operation"); + LOG_DBG("OtsOacpGattWr"); if (!ots->oacp_ind.is_enabled) { - LOG_WRN("OACP indications not enabled"); + LOG_WRN("OtsOacpIndNotEnabled"); return BT_GATT_ERR(BT_ATT_ERR_CCC_IMPROPER_CONF); } if (offset != 0) { - LOG_ERR("Invalid offset of OACP Write Request"); + LOG_WRN("OtsOacpWrReqInvOft"); return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); } - if (k_work_is_pending(&ots->oacp_ind.work)) { - LOG_ERR("OACP Write received before indication sent"); + if (ots->oacp_ind.ind_in_flight || + k_work_is_pending(&ots->oacp_ind.work.work)) { + LOG_WRN("OtsOacpWrBeforeIndSent"); return BT_GATT_ERR(BT_ATT_ERR_PROCEDURE_IN_PROGRESS); } @@ -715,26 +785,25 @@ ssize_t bt_gatt_ots_oacp_write(struct bt_conn *conn, case 0: oacp_status = oacp_proc_validate(conn, ots, &oacp_proc, &resp_param); if (oacp_status != BT_GATT_OTS_OACP_RES_SUCCESS) { - LOG_WRN("OACP Write error status: 0x%02X", oacp_status); + LOG_WRN("OtsOacpWrErrStatus[%02x]", oacp_status); } break; case -ENOTSUP: oacp_status = BT_GATT_OTS_OACP_RES_OPCODE_NOT_SUP; - LOG_WRN("OACP unsupported procedure type: 0x%02X", oacp_proc.type); + LOG_WRN("OtsOacpUnsuppProcType[%02x]", oacp_proc.type); break; case -EBADMSG: - LOG_ERR("Invalid length of OACP Write Request for 0x%02X " - "Op Code", oacp_proc.type); + LOG_WRN("OtsOacpWrReqInvLen[%02x]", oacp_proc.type); return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); case -ENODATA: - LOG_ERR("Invalid length of OACP Write Request"); + LOG_WRN("OtsOacpWrReqNoData"); return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); default: - LOG_ERR("Invalid return code from oacp_command_decode: %d", decode_status); + LOG_ERR("OtsOacpDecodeInvRc[%d]", decode_status); return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY); } - oacp_ind_send(attr, oacp_proc, oacp_status, &resp_param); + oacp_ind_send(conn, attr, oacp_proc, oacp_status, &resp_param); return len; } @@ -745,10 +814,15 @@ void bt_gatt_ots_oacp_cfg_changed(const struct bt_gatt_attr *attr, CONTAINER_OF((struct bt_gatt_ccc_managed_user_data *) attr->user_data, struct bt_gatt_ots_indicate, ccc); - LOG_DBG("Object Action Control Point CCCD value: 0x%04X", value); + LOG_DBG("OtsOacpCccd[%04x]", value); oacp_ind->is_enabled = false; if (value == BT_GATT_CCC_INDICATE) { oacp_ind->is_enabled = true; + } else { + LOG_DBG("OtsOacpIndClrOnCcc"); + (void)k_work_cancel_delayable(&oacp_ind->work); + oacp_ind->ind_in_flight = false; + oacp_ind->conn = NULL; } } diff --git a/components/bt/esp_ble_audio/host/services/ots/ots_oacp_internal.h b/components/bt/esp_ble_audio/host/services/ots/ots_oacp_internal.h index a72f3e61f54..caa7ad8adfd 100644 --- a/components/bt/esp_ble_audio/host/services/ots/ots_oacp_internal.h +++ b/components/bt/esp_ble_audio/host/services/ots/ots_oacp_internal.h @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Nordic Semiconductor ASA + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ diff --git a/components/bt/esp_ble_audio/host/services/ots/ots_obj_manager.c b/components/bt/esp_ble_audio/host/services/ots/ots_obj_manager.c index 4ebc3c5148e..2b723b74cfc 100644 --- a/components/bt/esp_ble_audio/host/services/ots/ots_obj_manager.c +++ b/components/bt/esp_ble_audio/host/services/ots/ots_obj_manager.c @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Nordic Semiconductor ASA + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -185,7 +186,7 @@ int bt_gatt_ots_obj_manager_obj_add( if (!cur_obj->is_allocated) { cur_obj->is_allocated = true; - /* TODO: do we need to reset cur_obj->val to 0 here? */ + (void)memset(&cur_obj->val, 0, sizeof(cur_obj->val)); cur_obj->val.id = obj_index_to_id(i); sys_dlist_append(&obj_manager->list, &cur_obj->dnode); @@ -255,3 +256,19 @@ void *bt_gatt_ots_obj_manager_assign(void) return cur_manager; } + +void bt_gatt_ots_obj_manager_release(void *obj_manager) +{ + struct bt_gatt_ots_obj_manager *manager = obj_manager; + + if (manager == NULL) { + return; + } + + for (size_t i = 0; i < CONFIG_BT_OTS_MAX_OBJ_CNT; i++) { + manager->pool[i].is_allocated = false; + } + + sys_dlist_init(&manager->list); + manager->is_assigned = false; +} diff --git a/components/bt/esp_ble_audio/host/services/ots/ots_obj_manager_internal.h b/components/bt/esp_ble_audio/host/services/ots/ots_obj_manager_internal.h index 4bc2f5cba4c..d4627db6737 100644 --- a/components/bt/esp_ble_audio/host/services/ots/ots_obj_manager_internal.h +++ b/components/bt/esp_ble_audio/host/services/ots/ots_obj_manager_internal.h @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Nordic Semiconductor ASA + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -42,6 +43,8 @@ bool bt_gatt_ots_obj_manager_obj_contains(void *obj_manager, struct bt_gatt_ots_ void *bt_gatt_ots_obj_manager_assign(void); +void bt_gatt_ots_obj_manager_release(void *obj_manager); + #ifdef __cplusplus } #endif diff --git a/components/bt/esp_ble_audio/host/services/ots/ots_olcp.c b/components/bt/esp_ble_audio/host/services/ots/ots_olcp.c index f6e84d12732..6f30cd94f99 100644 --- a/components/bt/esp_ble_audio/host/services/ots/ots_olcp.c +++ b/components/bt/esp_ble_audio/host/services/ots/ots_olcp.c @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Nordic Semiconductor ASA + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -119,7 +120,7 @@ static enum bt_gatt_ots_olcp_res_code olcp_goto_proc_execute( struct bt_gatt_ots_object *id_obj; if (!BT_OTS_VALID_OBJ_ID(id)) { - LOG_DBG("Invalid object ID 0x%016llx", id); + LOG_WRN("OtsOlcpInvObjId[%016llx]", id); return BT_GATT_OTS_OLCP_RES_INVALID_PARAMETER; } @@ -139,7 +140,7 @@ static enum bt_gatt_ots_olcp_res_code olcp_goto_proc_execute( static enum bt_gatt_ots_olcp_res_code olcp_proc_execute( struct bt_ots *ots, struct bt_gatt_ots_olcp_proc *proc) { - LOG_DBG("Executing OLCP procedure with 0x%02X Op Code", proc->type); + LOG_DBG("OtsOlcpExecProc[%02x]", proc->type); switch (proc->type) { case BT_GATT_OTS_OLCP_PROC_FIRST: @@ -200,16 +201,23 @@ static void olcp_ind_cb(struct bt_conn *conn, struct bt_gatt_indicate_params *params, uint8_t err) { - LOG_DBG("Received OLCP Indication ACK with status: 0x%04X", err); + struct bt_ots *ots = (struct bt_ots *) params->attr->user_data; + + LOG_DBG("OtsOlcpRecvIndAck[%04x]", err); + + ots->olcp_ind.ind_in_flight = false; + ots->olcp_ind.conn = NULL; } -static void olcp_ind_send(const struct bt_gatt_attr *olcp_attr, +static void olcp_ind_send(struct bt_conn *conn, + const struct bt_gatt_attr *olcp_attr, enum bt_gatt_ots_olcp_proc_type req_op_code, enum bt_gatt_ots_olcp_res_code olcp_status) { struct bt_ots *ots = (struct bt_ots *) olcp_attr->user_data; uint8_t *olcp_res = ots->olcp_ind.res; uint16_t olcp_res_len = 0; + int err; /* Encode OLCP Response */ olcp_res[olcp_res_len++] = BT_GATT_OTS_OLCP_PROC_RESP; @@ -219,7 +227,7 @@ static void olcp_ind_send(const struct bt_gatt_attr *olcp_attr, /* Prepare indication parameters */ memset(&ots->olcp_ind.params, 0, sizeof(ots->olcp_ind.params)); memcpy(&ots->olcp_ind.attr, olcp_attr, sizeof(ots->olcp_ind.attr)); - ots->olcp_ind.params.attr = olcp_attr; + ots->olcp_ind.params.attr = &ots->olcp_ind.attr; ots->olcp_ind.params.func = olcp_ind_cb; ots->olcp_ind.params.data = olcp_res; ots->olcp_ind.params.len = olcp_res_len; @@ -227,9 +235,16 @@ static void olcp_ind_send(const struct bt_gatt_attr *olcp_attr, ots->olcp_ind.params.chan_opt = BT_ATT_CHAN_OPT_NONE; #endif /* CONFIG_BT_EATT */ - LOG_DBG("Sending OLCP indication"); + LOG_DBG("OtsOlcpSendInd[%u]", conn->handle); - k_work_submit(&ots->olcp_ind.work); + ots->olcp_ind.conn = conn; + ots->olcp_ind.ind_in_flight = true; + err = k_work_schedule(&ots->olcp_ind.work, K_NO_WAIT_ASYNC); + if (err < 0) { + LOG_ERR("OtsOlcpSchIndFail[%u][%d]", conn->handle, err); + ots->olcp_ind.ind_in_flight = false; + ots->olcp_ind.conn = NULL; + } } ssize_t bt_gatt_ots_olcp_write(struct bt_conn *conn, @@ -243,20 +258,27 @@ ssize_t bt_gatt_ots_olcp_write(struct bt_conn *conn, struct bt_gatt_ots_olcp_proc olcp_proc; struct bt_ots *ots = (struct bt_ots *) attr->user_data; - LOG_DBG("Object List Control Point GATT Write Operation"); + LOG_DBG("OtsOlcpGattWr"); if (!ots->olcp_ind.is_enabled) { - LOG_WRN("OLCP indications not enabled"); + LOG_WRN("OtsOlcpIndNotEnabled"); return BT_GATT_ERR(BT_ATT_ERR_CCC_IMPROPER_CONF); } if (offset != 0) { - LOG_ERR("Invalid offset of OLCP Write Request"); + LOG_WRN("OtsOlcpWrReqInvOft"); return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); } - if (k_work_is_pending(&ots->olcp_ind.work)) { - LOG_ERR("OLCP Write received before indication sent"); + if (ots->olcp_ind.ind_in_flight || + k_work_is_pending(&ots->olcp_ind.work.work)) { + LOG_WRN("OtsOlcpWrBeforeIndSent"); + return BT_GATT_ERR(BT_ATT_ERR_PROCEDURE_IN_PROGRESS); + } + + if (ots->cur_obj && + ots->cur_obj->state.type != BT_GATT_OTS_OBJECT_IDLE_STATE) { + LOG_WRN("OtsOlcpWrObjBusy[%d]", ots->cur_obj->state.type); return BT_GATT_ERR(BT_ATT_ERR_PROCEDURE_IN_PROGRESS); } @@ -267,14 +289,13 @@ ssize_t bt_gatt_ots_olcp_write(struct bt_conn *conn, case 0: olcp_status = olcp_proc_execute(ots, &olcp_proc); if (olcp_status != BT_GATT_OTS_OLCP_RES_SUCCESS) { - LOG_WRN("OLCP Write error status: 0x%02X", olcp_status); + LOG_WRN("OtsOlcpWrErrStatus[%02x]", olcp_status); } else if (old_obj != ots->cur_obj) { char id[BT_OTS_OBJ_ID_STR_LEN]; bt_ots_obj_id_to_str(ots->cur_obj->id, id, sizeof(id)); - LOG_DBG("Selecting a new Current Object with id: %s", - id); + LOG_DBG("OtsOlcpSelNewCurObj[%s]", id); if (IS_ENABLED(CONFIG_BT_OTS_DIR_LIST_OBJ)) { bt_ots_dir_list_selected(ots->dir_list, ots->obj_manager, @@ -288,21 +309,20 @@ ssize_t bt_gatt_ots_olcp_write(struct bt_conn *conn, break; case -ENOTSUP: olcp_status = BT_GATT_OTS_OLCP_RES_PROC_NOT_SUP; - LOG_WRN("OLCP unsupported procedure type: 0x%02X", olcp_proc.type); + LOG_WRN("OtsOlcpUnsuppProcType[%02x]", olcp_proc.type); break; case -EBADMSG: - LOG_ERR("Invalid length of OLCP Write Request for 0x%02X " - "Op Code", olcp_proc.type); + LOG_WRN("OtsOlcpWrReqInvLen[%02x]", olcp_proc.type); return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); case -ENODATA: - LOG_ERR("Invalid size of OLCP Write Request"); + LOG_WRN("OtsOlcpWrReqNoData"); return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); default: - LOG_ERR("Invalid return code from olcp_command_decode: %d", decode_status); + LOG_ERR("OtsOlcpDecodeInvRc[%d]", decode_status); return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY); } - olcp_ind_send(attr, olcp_proc.type, olcp_status); + olcp_ind_send(conn, attr, olcp_proc.type, olcp_status); return len; } @@ -313,10 +333,15 @@ void bt_gatt_ots_olcp_cfg_changed(const struct bt_gatt_attr *attr, CONTAINER_OF((struct bt_gatt_ccc_managed_user_data *) attr->user_data, struct bt_gatt_ots_indicate, ccc); - LOG_DBG("Object List Control Point CCCD value: 0x%04X", value); + LOG_DBG("OtsOlcpCccd[%04x]", value); olcp_ind->is_enabled = false; if (value == BT_GATT_CCC_INDICATE) { olcp_ind->is_enabled = true; + } else { + LOG_DBG("OtsOlcpIndClrOnCcc"); + (void)k_work_cancel_delayable(&olcp_ind->work); + olcp_ind->ind_in_flight = false; + olcp_ind->conn = NULL; } } diff --git a/components/bt/esp_ble_audio/host/services/ots/ots_olcp_internal.h b/components/bt/esp_ble_audio/host/services/ots/ots_olcp_internal.h index d0fa74fb6f9..7d00d39e812 100644 --- a/components/bt/esp_ble_audio/host/services/ots/ots_olcp_internal.h +++ b/components/bt/esp_ble_audio/host/services/ots/ots_olcp_internal.h @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020-2022 Nordic Semiconductor ASA + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ diff --git a/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/bap.h b/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/bap.h index b78e45b180e..597677c7004 100644 --- a/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/bap.h +++ b/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/bap.h @@ -1505,6 +1505,7 @@ struct bt_bap_unicast_server_cb { * * @return 0 in case of success, negative error code otherwise. */ +int bt_bap_unicast_server_register(const struct bt_bap_unicast_server_register_param *param); int bt_bap_unicast_server_register_safe(const struct bt_bap_unicast_server_register_param *param); /** @@ -1521,6 +1522,7 @@ int bt_bap_unicast_server_register_safe(const struct bt_bap_unicast_server_regis * * @return 0 in case of success, negative error code otherwise. */ +int bt_bap_unicast_server_unregister(void); int bt_bap_unicast_server_unregister_safe(void); /** @@ -1974,7 +1976,8 @@ struct bt_bap_unicast_client_cb { * @param dir The type of remote endpoints and capabilities discovered. * @param codec_cap Remote capabilities. * - * If discovery procedure has complete both @p codec and @p ep are set to NULL. + * Called once per record; the end of the procedure is reported by the + * discover callback below, not by a NULL @p codec_cap. */ void (*pac_record)(struct bt_conn *conn, enum bt_audio_dir dir, const struct bt_audio_codec_cap *codec_cap); @@ -1988,21 +1991,21 @@ struct bt_bap_unicast_client_cb { * @param dir The type of remote endpoints and capabilities discovered. * @param ep Remote endpoint. * - * If discovery procedure has complete both @p codec and @p ep are set to NULL. + * Called once per endpoint; the end of the procedure is reported by the + * discover callback below, not by a NULL @p ep. */ void (*endpoint)(struct bt_conn *conn, enum bt_audio_dir dir, struct bt_bap_ep *ep); /** * @brief BAP discovery callback function. * - * If discovery procedure has completed @p ep is set to NULL and @p err is 0. + * Called once the discovery procedure has completed, for the direction it + * covered. * * @param conn Connection to the remote unicast server. * @param err Error value. 0 on success, GATT error on positive value or errno on * negative value. * @param dir The type of remote endpoints and capabilities discovered. - * - * If discovery procedure has complete both @p codec and @p ep are set to NULL. */ void (*discover)(struct bt_conn *conn, int err, enum bt_audio_dir dir); @@ -2695,6 +2698,7 @@ int bt_bap_broadcast_sink_delete_safe(struct bt_bap_broadcast_sink *sink); * * @return 0 in case of success or negative value in case of error. */ +int bt_bap_scan_delegator_register(struct bt_bap_scan_delegator_cb *cb); int bt_bap_scan_delegator_register_safe(struct bt_bap_scan_delegator_cb *cb); /** @@ -2705,6 +2709,7 @@ int bt_bap_scan_delegator_register_safe(struct bt_bap_scan_delegator_cb *cb); * * @return 0 in case of success or negative value in case of error. */ +int bt_bap_scan_delegator_unregister(void); int bt_bap_scan_delegator_unregister_safe(void); /** diff --git a/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/cap.h b/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/cap.h index ca2be9b9c9e..cfa4a098957 100644 --- a/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/cap.h +++ b/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/cap.h @@ -996,19 +996,21 @@ struct bt_cap_handover_broadcast_to_unicast_param { /** @brief Broadcast ID of the @p broadcast_source * - * Ignored if @p reception_stop_param is not NULL. + * Always required: receive state notifications are matched against the + * {broadcast_id, adv_sid, adv_type} triple to decide that reception stopped, + * whether or not @p reception_stop_param is given. */ uint32_t broadcast_id; /** @brief Advertising set ID of the @p broadcast_source * - * Ignored if @p reception_stop_param is not NULL. + * Always required, see @p broadcast_id. */ uint8_t adv_sid; /** @brief Advertising type of the advertising address of @p broadcast_source * - * Ignored if @p reception_stop_param is not NULL. + * Always required, see @p broadcast_id. */ uint8_t adv_type; diff --git a/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/gmap.h b/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/gmap.h index a9a65cfa04c..4940c5f2958 100644 --- a/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/gmap.h +++ b/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/gmap.h @@ -66,19 +66,19 @@ enum bt_gmap_ugg_feat { /** * @brief Support transmitting multiple LC3 codec frames per block in an SDU * - * Requires @kconfig{CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT} > 0 + * Requires @kconfig{CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT} > 0 */ BT_GMAP_UGG_FEAT_MULTIPLEX = BIT(0), /** * @brief 96 kbps source support * - * Requires @kconfig{CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT} > 0 + * Requires @kconfig{CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT} > 0 */ BT_GMAP_UGG_FEAT_96KBPS_SOURCE = BIT(1), /** * @brief Support for receiving at least two channels of audio, each in a separate CIS * - * Requires @kconfig{CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT} > 1 and + * Requires @kconfig{CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT} > 1 and * @kconfig{CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT} > 1 */ BT_GMAP_UGG_FEAT_MULTISINK = BIT(2), diff --git a/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/has.h b/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/has.h index 546ae5eebd4..95c6e5ab6d1 100644 --- a/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/has.h +++ b/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/has.h @@ -392,6 +392,7 @@ struct bt_has_preset_register_param { * * @return 0 if success, errno on failure. */ +int bt_has_register(const struct bt_has_features_param *features); int bt_has_register_safe(const struct bt_has_features_param *features); /** 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 0afa3453af5..f3ff59566d5 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 @@ -1601,6 +1601,7 @@ int bt_media_proxy_pl_register_safe(struct media_proxy_pl_calls *pl_calls); * * TODO: Move to player header file */ +int bt_media_proxy_pl_init(void); int bt_media_proxy_pl_init_safe(void); /** diff --git a/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/micp.h b/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/micp.h index 353eaccda78..615e3c4b9ef 100644 --- a/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/micp.h +++ b/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/micp.h @@ -94,6 +94,7 @@ struct bt_micp_included { * * @return 0 if success, errno on failure. */ +int bt_micp_mic_dev_register(struct bt_micp_mic_dev_register_param *param); int bt_micp_mic_dev_register_safe(struct bt_micp_mic_dev_register_param *param); /** diff --git a/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/tbs.h b/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/tbs.h index b0ffc14449c..8d47ea2838e 100644 --- a/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/tbs.h +++ b/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/tbs.h @@ -591,6 +591,7 @@ struct bt_tbs_register_param { * @kconfig{CONFIG_BT_TBS_BEARER_COUNT}) * @retval -ENOEXEC The service failed to be registered */ +int bt_tbs_register_bearer(const struct bt_tbs_register_param *param); int bt_tbs_register_bearer_safe(const struct bt_tbs_register_param *param); /** @@ -611,6 +612,7 @@ int bt_tbs_register_bearer_safe(const struct bt_tbs_register_param *param); * registered. * @retval -ENOEXEC The service failed to be unregistered */ +int bt_tbs_unregister_bearer(uint8_t bearer_index); int bt_tbs_unregister_bearer_safe(uint8_t bearer_index); /** @brief Prints all calls of all services to the debug log */ diff --git a/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/tmap.h b/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/tmap.h index 95c2ac5c522..91b4e2e1f28 100644 --- a/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/tmap.h +++ b/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/tmap.h @@ -101,6 +101,7 @@ struct bt_tmap_cb { * * @return 0 on success or negative error value on failure. */ +int bt_tmap_register(enum bt_tmap_role role); int bt_tmap_register_safe(enum bt_tmap_role role); /** diff --git a/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/vcp.h b/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/vcp.h index db605ea47df..e6d1c4b0718 100644 --- a/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/vcp.h +++ b/components/bt/esp_ble_audio/include/zephyr/bluetooth/audio/vcp.h @@ -131,6 +131,7 @@ int bt_vcp_vol_rend_included_get_safe(struct bt_vcp_included *included); * * @return 0 if success, errno on failure. */ +int bt_vcp_vol_rend_register(struct bt_vcp_vol_rend_register_param *param); int bt_vcp_vol_rend_register_safe(struct bt_vcp_vol_rend_register_param *param); /** diff --git a/components/bt/esp_ble_audio/lib/include/audio.h b/components/bt/esp_ble_audio/lib/include/audio.h index 37533c83d52..00500ca1085 100644 --- a/components/bt/esp_ble_audio/lib/include/audio.h +++ b/components/bt/esp_ble_audio/lib/include/audio.h @@ -32,86 +32,128 @@ struct bt_gatt_service; extern struct bt_aics *lib_aics_insts_get(void); extern void *lib_aics_svc_get(struct bt_aics *aics); extern int lib_aics_init(void); +extern int lib_aics_deinit(void); extern int lib_aics_client_init(void); +extern int lib_aics_client_deinit(void); extern int lib_bap_base_init(void); +extern int lib_bap_base_deinit(void); extern int lib_bap_iso_init(void); +extern int lib_bap_iso_deinit(void); extern int lib_bap_stream_init(void); +extern int lib_bap_stream_deinit(void); extern struct bt_gatt_service *lib_ascs_svc_get(void); extern int lib_ascs_init(void); +extern int lib_ascs_deinit(void); extern struct bt_gatt_service *lib_bap_bass_svc_get(void); extern int lib_bap_broadcast_assistant_init(void); +extern int lib_bap_broadcast_assistant_deinit(void); extern int lib_bap_broadcast_sink_init(void); +extern int lib_bap_broadcast_sink_deinit(void); extern int lib_bap_broadcast_source_init(void); +extern int lib_bap_broadcast_source_deinit(void); extern int lib_bap_scan_delegator_init(void); +extern int lib_bap_scan_delegator_deinit(void); extern int lib_bap_unicast_client_init(void); +extern int lib_bap_unicast_client_deinit(void); extern int lib_bap_unicast_server_init(void); +extern int lib_bap_unicast_server_deinit(void); extern struct bt_gatt_service *lib_pacs_svc_get(void); extern int lib_pacs_init(void); +extern int lib_pacs_deinit(void); extern struct bt_gatt_service *lib_cas_svc_get(void); extern int lib_cap_common_init(void); +extern int lib_cap_common_deinit(void); extern int lib_cap_stream_init(void); +extern int lib_cap_stream_deinit(void); extern int lib_cap_initiator_init(void); +extern int lib_cap_initiator_deinit(void); extern int lib_cap_acceptor_init(void); +extern int lib_cap_acceptor_deinit(void); extern int lib_cap_commander_init(void); +extern int lib_cap_commander_deinit(void); extern int lib_ccid_init(void); +extern int lib_ccid_deinit(void); extern int lib_ccp_call_control_client_init(void); +extern int lib_ccp_call_control_client_deinit(void); extern int lib_ccp_call_control_server_init(void); +extern int lib_ccp_call_control_server_deinit(void); extern int lib_codec_init(void); +extern int lib_codec_deinit(void); extern int lib_csip_set_coordinator_init(void); +extern int lib_csip_set_coordinator_deinit(void); extern void *lib_csip_set_member_svc_get(const void *svc_inst); extern int lib_csip_set_member_init(void); +extern int lib_csip_set_member_deinit(void); extern int lib_gmap_client_init(void); +extern int lib_gmap_client_deinit(void); extern struct bt_gatt_service *lib_gmas_svc_get(void); +extern int lib_gmap_server_deinit(void); extern struct bt_gatt_service *lib_has_svc_get(void); extern int lib_has_init(void); +extern int lib_has_deinit(void); extern int lib_has_client_init(void); +extern int lib_has_client_deinit(void); extern int lib_mcc_init(void); +extern int lib_mcc_deinit(void); extern struct bt_gatt_service *lib_gmcs_svc_get(void); extern struct bt_gatt_service *lib_mcs_server_list_get(void); extern int lib_mcs_init(void); +extern int lib_mcs_deinit(void); extern int lib_media_proxy_init(void); +extern int lib_media_proxy_deinit(void); extern int lib_micp_mic_ctlr_init(void); +extern int lib_micp_mic_ctlr_deinit(void); extern struct bt_gatt_service *lib_mics_svc_get(void); extern int lib_micp_mic_dev_init(void); +extern int lib_micp_mic_dev_deinit(void); extern int lib_mpl_init(void); +extern int lib_mpl_deinit(void); extern int lib_pbp_init(void); +extern int lib_pbp_deinit(void); extern int lib_tbs_client_init(void); +extern int lib_tbs_client_deinit(void); extern struct bt_gatt_service *lib_gtbs_svc_get(void); extern struct bt_gatt_service *lib_tbs_server_list_get(void); extern int lib_tbs_init(void); +extern int lib_tbs_deinit(void); extern struct bt_gatt_service *lib_tmas_svc_get(void); extern int lib_tmap_init(void); +extern int lib_tmap_deinit(void); extern int lib_vcp_vol_ctlr_init(void); +extern int lib_vcp_vol_ctlr_deinit(void); extern struct bt_gatt_service *lib_vcs_svc_get(void); extern int lib_vcp_vol_rend_init(void); +extern int lib_vcp_vol_rend_deinit(void); struct bt_vocs; struct bt_vocs_server; extern int lib_vocs_client_init(void); +extern int lib_vocs_client_deinit(void); extern struct bt_vocs_server *lib_vocs_insts_get(void); extern void *lib_vocs_svc_get(struct bt_vocs *vocs); extern int lib_vocs_init(void); +extern int lib_vocs_deinit(void); struct bt_conn; struct bt_gatt_attr; diff --git a/components/bt/esp_ble_audio/lib/lib b/components/bt/esp_ble_audio/lib/lib index b69d90c8604..774d7777665 160000 --- a/components/bt/esp_ble_audio/lib/lib +++ b/components/bt/esp_ble_audio/lib/lib @@ -1 +1 @@ -Subproject commit b69d90c86049f7d49bcc5ca7961c91e2691bc253 +Subproject commit 774d7777665adf21ea4bcf2d8f5f283516a0c391 diff --git a/components/bt/esp_ble_audio/linker.lf b/components/bt/esp_ble_audio/linker.lf new file mode 100644 index 00000000000..d810e860e83 --- /dev/null +++ b/components/bt/esp_ble_audio/linker.lf @@ -0,0 +1,31 @@ +# BLE Audio external-RAM (.bss -> PSRAM) link-time placement. +# +# The bt_extram_bss scheme and bt_bss/bt_common sections referenced below are +# defined in the component's linker_common.lf. ldgen merges all registered +# fragments into one model, so the cross-file reference resolves. + +# Prebuilt archive: EXT_RAM_BSS_ATTR can't apply at its compile time, so move +# its .bss to SPIRAM at link time instead. +[mapping:ble_audio] +archive: libble_audio.a +entries: + if BT_AUDIO_BSS_SEG_EXTERNAL_MEMORY = y: + * (bt_extram_bss) + +# Move the OTS objects' .bss to SPIRAM at link time (their net_buf pools can't +# take a per-var attr). Gates mirror CMakeLists so no entry names an object +# that isn't built. +[mapping:ble_audio_ots] +archive: libbt.a +entries: + if BT_AUDIO_BSS_SEG_EXTERNAL_MEMORY = y && (BT_OTS = y || BT_OTS_CLIENT = y): + ots_l2cap (bt_extram_bss) + if BT_AUDIO_BSS_SEG_EXTERNAL_MEMORY = y && BT_OTS = y: + ots (bt_extram_bss) + ots_oacp (bt_extram_bss) + ots_obj_manager (bt_extram_bss) + ots_olcp (bt_extram_bss) + if BT_AUDIO_BSS_SEG_EXTERNAL_MEMORY = y && BT_OTS = y && BT_OTS_DIR_LIST_OBJ = y: + ots_dir_list (bt_extram_bss) + if BT_AUDIO_BSS_SEG_EXTERNAL_MEMORY = y && BT_OTS_CLIENT = y: + ots_client (bt_extram_bss) diff --git a/components/bt/esp_ble_iso/CMakeLists.txt b/components/bt/esp_ble_iso/CMakeLists.txt index 6c3e32f58a0..32e5d80ce4d 100644 --- a/components/bt/esp_ble_iso/CMakeLists.txt +++ b/components/bt/esp_ble_iso/CMakeLists.txt @@ -44,36 +44,24 @@ list(APPEND ble_iso_srcs "${CMAKE_CURRENT_LIST_DIR}/host/common/app/gap.c" "${CMAKE_CURRENT_LIST_DIR}/host/common/app/gatt.c" "${CMAKE_CURRENT_LIST_DIR}/host/utils/crc/crc32_sw.c" + "${CMAKE_CURRENT_LIST_DIR}/host/utils/assert.c" "${CMAKE_CURRENT_LIST_DIR}/host/utils/addr.c" "${CMAKE_CURRENT_LIST_DIR}/host/utils/bt_str.c" "${CMAKE_CURRENT_LIST_DIR}/host/utils/buf.c" "${CMAKE_CURRENT_LIST_DIR}/host/utils/crypto.c" + "${CMAKE_CURRENT_LIST_DIR}/host/utils/keys.c" + "${CMAKE_CURRENT_LIST_DIR}/host/utils/mem.c" "${CMAKE_CURRENT_LIST_DIR}/host/utils/timer.c" "${CMAKE_CURRENT_LIST_DIR}/host/utils/utf8.c" - "${CMAKE_CURRENT_LIST_DIR}/host/utils/utils.c" "${CMAKE_CURRENT_LIST_DIR}/host/utils/uuid.c" "${CMAKE_CURRENT_LIST_DIR}/host/iso/iso.c" "${CMAKE_CURRENT_LIST_DIR}/api/esp_ble_iso_common_api.c" ) -# L2CAP host shim wiring is currently only consumed by the OTS service -# (which lives in esp_ble_audio). common/l2cap.c has both host branches -# (Bluedroid returns -ENOTSUP), so compile it under either host whenever -# OTS is enabled; the NimBLE-only adapter glue stays NimBLE-gated. -if(CONFIG_BT_OTS OR CONFIG_BT_OTS_CLIENT) - list(APPEND ble_iso_srcs - "${CMAKE_CURRENT_LIST_DIR}/host/common/l2cap.c" - ) - if(CONFIG_BT_NIMBLE_ENABLED) - list(APPEND ble_iso_srcs - "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/l2cap.c" - ) - endif() -endif() - list(APPEND ble_iso_include_dirs "${CMAKE_CURRENT_LIST_DIR}/api/include" "${CMAKE_CURRENT_LIST_DIR}/host/common/include" + "${CMAKE_CURRENT_LIST_DIR}/host/utils/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_iso/Kconfig.in b/components/bt/esp_ble_iso/Kconfig.in index ebb4f041719..186af695748 100644 --- a/components/bt/esp_ble_iso/Kconfig.in +++ b/components/bt/esp_ble_iso/Kconfig.in @@ -104,6 +104,38 @@ if BT_ISO endif # BT_ISO_BROADCAST + config BT_ISO_BSS_SEG_EXTERNAL_MEMORY + bool "Place BLE ISO .bss segment in external SPIRAM" + depends on SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY + default n + help + Place the zero-initialized (.bss) static data of the esp_ble_iso + component in external SPIRAM instead of internal DRAM, to free + internal RAM. + + Safe because the esp_ble_iso sources register no IRAM ISRs: their + data is only touched in task context, never while the flash cache + is disabled. Hot ISO data-path state that must stay internal for + latency is excluded at each placement site, not by this switch. + + Requires SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY (which makes SPIRAM + mandatory at boot). Only .bss is moved; initialized (.data) statistics + stay internal. + + config BT_ISO_HEAP_EXTERNAL_MEMORY + bool "Place BLE ISO heap allocations in external SPIRAM" + depends on SPIRAM + default n + help + Route esp_ble_iso runtime heap allocations (control-plane only: + GATT/GAP event marshalling, CIS/CIG setup params, CCC pools) to + external SPIRAM via heap_caps_*(MALLOC_CAP_SPIRAM) instead of the + internal heap. + + The real-time ISO data path (TX/RX SDU packet buffers) is excluded + at each allocation site and stays in internal DRAM. On SPIRAM + exhaustion the allocation returns NULL (no internal fallback). + config BT_ISO_DISPATCH_MONITOR bool "Monitor ISO task callback dispatch latency" depends on !BT_ISO_NO_LOG @@ -157,6 +189,8 @@ if BT_ISO choice BT_ISO_LOG_LEVEL prompt "BLE_ISO_LOG_LEVEL" + default BT_ISO_LOG_LEVEL_INFO if BT_LOG_CRITICAL_ONLY_ISO + default BT_ISO_LOG_LEVEL_DEBUG if BLE_COMPRESSED_LOG_ENABLE default BT_ISO_LOG_LEVEL_WARNING depends on !BT_ISO_NO_LOG help diff --git a/components/bt/esp_ble_iso/api/esp_ble_iso_common_api.c b/components/bt/esp_ble_iso/api/esp_ble_iso_common_api.c index 0f9f6deb998..a260efe385c 100644 --- a/components/bt/esp_ble_iso/api/esp_ble_iso_common_api.c +++ b/components/bt/esp_ble_iso/api/esp_ble_iso_common_api.c @@ -6,8 +6,14 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include + +#include + #include "esp_ble_iso_common_api.h" +LOG_MODULE_REGISTER(ISO_API, CONFIG_BT_ISO_LOG_LEVEL); + esp_err_t esp_ble_iso_data_parse(const uint8_t ltv[], size_t size, bool (*func)(uint8_t type, const uint8_t *data, @@ -35,15 +41,9 @@ esp_err_t esp_ble_iso_data_parse(const uint8_t ltv[], size_t size, type = ltv[i + 1]; data_len = len - sizeof(uint8_t); - /* Skip empty value entries in strict parsing mode. */ - if (data_len == 0) { - i += (size_t)len + 1; - continue; - } - - /* A callback returning false aborts parsing; report it as an error so - * callers detect it (bt_audio_data_parse() returns -ECANCELED here). */ - if (func(type, <v[i + 2], data_len, user_data) == false) { + /* Zero-length values are valid (e.g. BROADCAST_IMMEDIATE flag-only LTV). + * Match Zephyr bt_audio_data_parse: invoke callback with data=NULL. */ + if (func(type, data_len > 0 ? <v[i + 2] : NULL, data_len, user_data) == false) { return ESP_FAIL; } @@ -134,6 +134,7 @@ esp_err_t esp_ble_iso_chan_connect(esp_ble_iso_connect_param_t *param, conn = bt_le_acl_conn_find(conn_handle); if (conn == NULL) { + LOG_WRN("AclUnknown[%u]", conn_handle); ret = ESP_ERR_NOT_FOUND; goto unlock; } @@ -272,6 +273,7 @@ esp_err_t esp_ble_iso_big_create(uint8_t adv_handle, adv = bt_le_ext_adv_find(adv_handle); if (adv == NULL) { + LOG_WRN("ExtAdvUnknown[%u]", adv_handle); ret = ESP_ERR_NOT_FOUND; goto unlock; } @@ -292,20 +294,27 @@ esp_err_t esp_ble_iso_big_sync(uint16_t sync_handle, esp_ble_iso_big_sync_param_t *param, esp_ble_iso_big_t **out_big) { + esp_err_t ret = ESP_OK; void *per_adv_sync; int err; - per_adv_sync = bt_le_per_adv_sync_find_safe(sync_handle); + bt_le_host_lock(); + + per_adv_sync = bt_le_per_adv_sync_find(sync_handle); if (per_adv_sync == NULL) { - return ESP_ERR_NOT_FOUND; + LOG_WRN("PaSyncUnknown[%u]", sync_handle); + ret = ESP_ERR_NOT_FOUND; + goto unlock; } - err = bt_iso_big_sync_safe(per_adv_sync, param, out_big); + err = bt_iso_big_sync(per_adv_sync, param, out_big); if (err) { - return ESP_FAIL; + ret = ESP_FAIL; } - return ESP_OK; +unlock: + bt_le_host_unlock(); + return ret; } #endif /* CONFIG_BT_ISO_SYNC_RECEIVER */ @@ -399,7 +408,7 @@ esp_err_t esp_ble_iso_chan_send_ts(esp_ble_iso_chan_t *chan, } #endif /* CONFIG_BT_ISO_TX */ -void esp_ble_iso_gap_app_post_event(uint8_t type, void *param) +void esp_ble_iso_gap_app_post_event(uint16_t type, void *param) { bt_le_gap_app_post_event(type, param); } @@ -434,6 +443,44 @@ unregister_gap: return ESP_FAIL; } +esp_err_t esp_ble_iso_common_deinit(const esp_ble_iso_deinit_info_t *info) +{ + bool reset_ext_adv = true; + bool reset_pa_sync = true; + int err; + + if (info) { + if (!info->reset_acl_conn) { + LOG_WRN("ConnKeepUnsupported"); + return ESP_ERR_NOT_SUPPORTED; + } + + reset_ext_adv = info->reset_ext_adv; + reset_pa_sync = info->reset_pa_sync; + } + + if (bt_le_host_check_idle()) { + return ESP_ERR_INVALID_STATE; + } + + err = bt_le_host_deinit(); + if (err) { + return ESP_ERR_TIMEOUT; + } + + if (reset_ext_adv) { + bt_le_ext_adv_state_reset(); + } + + if (reset_pa_sync) { + bt_le_per_adv_sync_state_reset(); + } + + bt_le_gap_app_cb_unregister(); + + return ESP_OK; +} + #if CONFIG_BT_BLUEDROID_ENABLED uint8_t esp_ble_iso_bluedroid_get_gattc_if(void) { diff --git a/components/bt/esp_ble_iso/api/include/esp_ble_iso_common_api.h b/components/bt/esp_ble_iso/api/include/esp_ble_iso_common_api.h index a9560718f74..37ff644bcdb 100644 --- a/components/bt/esp_ble_iso/api/include/esp_ble_iso_common_api.h +++ b/components/bt/esp_ble_iso/api/include/esp_ble_iso_common_api.h @@ -367,6 +367,10 @@ esp_err_t esp_ble_iso_cig_terminate(esp_ble_iso_cig_t *cig); * @param conn_handle Connection handle. * @param count Number of channels to connect. * + * @note ISO only knows links whose connection event it saw, so one + * established before esp_ble_iso_common_init() returns + * ESP_ERR_NOT_FOUND until it is handed back. + * * @return ESP_OK on success, or an error code on failure. */ esp_err_t esp_ble_iso_chan_connect(esp_ble_iso_connect_param_t *param, @@ -567,7 +571,17 @@ esp_err_t esp_ble_iso_chan_send_ts(esp_ble_iso_chan_t *chan, #define ESP_BLE_ISO_GAP_EVENT_SECURITY_CHANGE BT_LE_GAP_APP_EVENT_SECURITY_CHANGE /*!< ISO GAP BIGInfo Adv Report event */ #define ESP_BLE_ISO_GAP_EVENT_BIGINFO_RECV BT_LE_GAP_APP_EVENT_BIGINFO_RECV -/** ISO GAP application event structure */ +/** + * @brief ISO GAP application event structure + * + * @note Addresses carried by these events are in the **active host's own byte + * order**: on-air/LSB-first under NimBLE, MSB-first under Bluedroid + * (the order every `esp_ble_gap_*` API takes). Feeding one straight back + * to a host API is therefore always correct; comparing one against an + * address the audio layer holds, such as a Broadcast Receive State, + * needs a reversal under Bluedroid. See `struct bt_le_addr` in + * common/app/gap.h for the full convention. + */ typedef struct bt_le_gap_app_event esp_ble_iso_gap_app_event_t; /** ISO initialization information structure */ @@ -578,12 +592,37 @@ typedef struct { /** * @brief Post an application-layer GAP event for ISO internal usage. * - * @note This function is only needed while using NimBLE Host. + * @note NimBLE-only: NimBLE has no global GAP callback, so ISO sees only what + * the application forwards here. An event left out is dropped silently. + * Bluedroid's ISO layer hooks the stack directly. * - * @param type Event type. - * @param param Event parameters. + * @note Forward every event below that the application's flows can produce. + * Forwarding one ISO does not use is harmless; omitting one leaves the + * matching ISO state uncreated and fails later and elsewhere: + * - BLE_GAP_EVENT_EXT_DISC extended advertising report + * - BLE_GAP_EVENT_CONNECT ACL established or failed + * - BLE_GAP_EVENT_DISCONNECT ACL closed + * - BLE_GAP_EVENT_ENC_CHANGE encryption changed + * - BLE_GAP_EVENT_PERIODIC_SYNC PA sync established + * - BLE_GAP_EVENT_PERIODIC_SYNC_LOST PA sync lost + * - BLE_GAP_EVENT_PERIODIC_REPORT periodic advertising report + * - BLE_GAP_EVENT_PERIODIC_TRANSFER PA sync received over PAST + * - BLE_GAP_EVENT_PERIODIC_TRANSFER_V2 as above, PAwR + * + * BIGInfo needs no forwarding: ISO takes it from the controller itself + * and delivers it as ESP_BLE_ISO_GAP_EVENT_BIGINFO_RECV. + * + * @param type Event type, i.e. ble_gap_event::type. + * @param param Event parameters, i.e. the struct ble_gap_event pointer. */ -void esp_ble_iso_gap_app_post_event(uint8_t type, void *param); +void esp_ble_iso_gap_app_post_event(uint16_t type, void *param); + +/** Which of ISO's records of application-owned objects deinit clears. */ +typedef struct { + bool reset_ext_adv; /*!< Clear the advertising sets added for BIG */ + bool reset_pa_sync; /*!< Clear the periodic advertising syncs */ + bool reset_acl_conn; /*!< Clear the ACL links. Must be true - see below */ +} esp_ble_iso_deinit_info_t; /** * @brief Initialize ISO common functionality. @@ -594,6 +633,34 @@ void esp_ble_iso_gap_app_post_event(uint8_t type, void *param); */ esp_err_t esp_ble_iso_common_init(esp_ble_iso_init_info_t *info); +/** + * @brief Deinitialize ISO common functionality. + * + * The application must first disconnect all CIS/BIS and terminate every CIG/BIG. + * If any is still up this returns ESP_ERR_INVALID_STATE and releases nothing, + * logging each offending item at ERROR level. + * + * ACL links, advertising sets and periodic advertising syncs belong to the + * application: they neither block deinit nor get torn down, only ISO's record of + * them goes. @p info chooses which records to drop - a dropped one is logged at + * WARNING level and must be handed back after the next init. NULL drops all three. + * + * reset_acl_conn must be true for now: on Bluedroid the GATT teardown drops ISO's + * reference, so a link ISO alone held is disconnected while a shared one stays up, + * and ISO cannot yet tell the two apart. + * + * @param info Which records to clear, or NULL to clear all. + * + * @note Must not be called from an ISO callback (they run on the ISO task, + * which this stops). + * + * @return ESP_OK on success, + * ESP_ERR_INVALID_STATE if something ISO owns is still active, + * ESP_ERR_TIMEOUT if the ISO task could not be stopped. + * Nothing is released unless ESP_OK is returned. + */ +esp_err_t esp_ble_iso_common_deinit(const esp_ble_iso_deinit_info_t *info); + #if CONFIG_BT_BLUEDROID_ENABLED /** * @brief Get the engine's internal GATTC interface handle (Bluedroid only). diff --git a/components/bt/esp_ble_iso/host/adapter/bluedroid/gap.c b/components/bt/esp_ble_iso/host/adapter/bluedroid/gap.c index ceb63f64dba..33bb6251c35 100644 --- a/components/bt/esp_ble_iso/host/adapter/bluedroid/gap.c +++ b/components/bt/esp_ble_iso/host/adapter/bluedroid/gap.c @@ -63,7 +63,9 @@ extern void btc_ble_5_gap_callback(tBTA_DM_BLE_5_GAP_EVENT event, * matching the NimBLE host contract; then both this tracker and the * synthesis below become dead code. */ #define ISO_PA_SYNC_HANDLE_NONE 0xFFFF -static uint16_t active_pa_sync_handle = ISO_PA_SYNC_HANDLE_NONE; +/* Reset in bt_le_bluedroid_gap_init() (not static init) so a deinit/init cycle + * restarts from "no active sync"; static init runs only once at boot. */ +static BT_ISO_EXT_RAM_BSS_ATTR uint16_t active_pa_sync_handle; #endif /* BLE_50_EXTEND_SYNC_EN == TRUE */ /* Fast-path BTA → iso-queue post. @@ -151,8 +153,8 @@ void bt_le_bluedroid_gap_post_event(uint16_t event, void *param) struct bt_le_gap_app_param *qev = NULL; int err; - qev = calloc(1, sizeof(*qev)); - assert(qev); + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); /* Only AUTH_CMPL reaches here from the application's * esp_ble_gap_register_callback path. EXT_ADV_REPORT / PA_SYNC_ESTAB / @@ -162,26 +164,17 @@ void bt_le_bluedroid_gap_post_event(uint16_t event, void *param) switch (event) { case ESP_GAP_BLE_AUTH_CMPL_EVT: { const esp_ble_auth_cmpl_t *a = &p->ble_security.auth_cmpl; - struct gatt_conn *gatt_conn; uint8_t sec_level; - gatt_conn = bt_le_bluedroid_find_gatt_conn_with_addr(a->addr_type, a->bd_addr, false); - if (gatt_conn == NULL) { - LOG_ERR("[B]UnknownDevForEnc"); - free(qev); - return; - } - qev->type = BT_LE_GAP_APP_PARAM_SECURITY_CHANGE; qev->security_change.status = (a->success ? 0x00 : 0xFF); - /* Populate connection identity unconditionally so failure events - * carry valid conn_handle / role / dst to the application. */ - qev->security_change.conn_handle = gatt_conn->conn_handle; - qev->security_change.role = gatt_conn->role; - qev->security_change.dst.type = gatt_conn->peer.type; - memcpy(qev->security_change.dst.val, gatt_conn->peer.val, BT_ADDR_SIZE); + /* dst only: conn_handle/role are resolved from it by the iso_task handler. + * Looking them up here would mean reading gatt_conns[] from BTC, which + * iso_task mutates under host_lock - and BTC must not take that lock. */ + qev->security_change.dst.type = a->addr_type; + memcpy(qev->security_change.dst.val, a->bd_addr, BT_ADDR_SIZE); if (qev->security_change.status == 0) { /* Derive level from auth_mode bits (mirrors NimBLE's sec_state @@ -202,9 +195,9 @@ void bt_le_bluedroid_gap_post_event(uint16_t event, void *param) /* Attach the bonded LTK (CSIS SIRK-encryption key K). */ if (bd_read_bonded_ltk(a->bd_addr, qev->security_change.ltk)) { qev->security_change.ltk_present = 1; - LOG_INF("[B]LtkFromStore[%u]", qev->security_change.conn_handle); + LOG_INF("[B]LtkFromStore"); } else { - LOG_WRN("[B]NoLtkForEnc[%u]", qev->security_change.conn_handle); + LOG_WRN("[B]NoLtkForEnc"); } } @@ -219,7 +212,7 @@ void bt_le_bluedroid_gap_post_event(uint16_t event, void *param) err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GAP_EVENT, qev, sizeof(*qev)); if (err) { - LOG_ERR("[B]GapPostEvtFail[%d][%u]", err, qev->type); + ISO_POST_FAIL_LOG(err, "[B]GapPostEvtFail[%d][%u]", err, qev->type); free(qev); } } @@ -231,8 +224,8 @@ static void bt_le_bluedroid_gap_post_event_bta(tBTA_DM_BLE_5_GAP_EVENT event, enum iso_queue_item_type q_type; int err; - qev = calloc(1, sizeof(*qev)); - assert(qev); + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); switch (event) { #if (BLE_50_EXTEND_SCAN_EN == TRUE) @@ -255,8 +248,8 @@ static void bt_le_bluedroid_gap_post_event_bta(tBTA_DM_BLE_5_GAP_EVENT event, qev->ext_scan_recv.data_len = r->adv_data_len; if (qev->ext_scan_recv.data_len) { - qev->ext_scan_recv.data = calloc(1, qev->ext_scan_recv.data_len); - assert(qev->ext_scan_recv.data); + qev->ext_scan_recv.data = bt_le_ext_calloc(1, qev->ext_scan_recv.data_len); + BT_LE_ASSERT(qev->ext_scan_recv.data); memcpy(qev->ext_scan_recv.data, r->adv_data, qev->ext_scan_recv.data_len); } break; @@ -344,8 +337,8 @@ static void bt_le_bluedroid_gap_post_event_bta(tBTA_DM_BLE_5_GAP_EVENT event, qev->pa_sync_recv.data_len = r->data_length; if (qev->pa_sync_recv.data_len) { - qev->pa_sync_recv.data = calloc(1, qev->pa_sync_recv.data_len); - assert(qev->pa_sync_recv.data); + qev->pa_sync_recv.data = bt_le_ext_calloc(1, qev->pa_sync_recv.data_len); + BT_LE_ASSERT(qev->pa_sync_recv.data); memcpy(qev->pa_sync_recv.data, r->data, qev->pa_sync_recv.data_len); } break; @@ -355,16 +348,6 @@ static void bt_le_bluedroid_gap_post_event_bta(tBTA_DM_BLE_5_GAP_EVENT event, #if (BLE_FEAT_PERIODIC_ADV_SYNC_TRANSFER == TRUE) case BTM_BLE_GAP_PERIODIC_ADV_SYNC_TRANS_RECV_EVT: { const tBTM_BLE_PERIOD_ADV_SYNC_TRANS_RECV *r = ¶ms->past_recv; - struct gatt_conn *gatt_conn; - - /* Look up the PAST-delivering ACL handle by peer BDA — past_recv - * doesn't carry addr type. */ - gatt_conn = bt_le_bluedroid_find_gatt_conn_with_addr(0, r->addr, true); - if (gatt_conn == NULL) { - LOG_ERR("[B]UnknownPastSrc"); - free(qev); - return; - } qev->type = BT_LE_GAP_APP_PARAM_PA_SYNC_PAST; @@ -376,7 +359,13 @@ static void bt_le_bluedroid_gap_post_event_bta(tBTA_DM_BLE_5_GAP_EVENT event, qev->pa_sync_past.adv_phy = r->adv_phy; qev->pa_sync_past.per_adv_itvl = r->adv_interval; qev->pa_sync_past.adv_ca = r->adv_clk_accuracy; - qev->pa_sync_past.conn_handle = gatt_conn->conn_handle; + + /* Sender identity: HCI reports it as a Connection_Handle and carries no + * address for it (adv_addr_type/adv_addr above are the advertiser's). + * btu_hcif copies the BDA out of the LCB but drops both the handle and + * the address type, so .type stays unset and the handler matches on the + * value alone. */ + memcpy(qev->pa_sync_past.src_addr.val, r->addr, BT_ADDR_SIZE); #if (BLE_50_EXTEND_SYNC_EN == TRUE) /* PAST-established syncs need tracker too: a later app-initiated @@ -420,12 +409,8 @@ static void bt_le_bluedroid_gap_post_event_bta(tBTA_DM_BLE_5_GAP_EVENT event, err = bt_le_iso_task_post(q_type, qev, sizeof(*qev)); if (err) { - /* Floodable reports drop by design when the queue is full; only a - * failure on the reliable (normal-queue) path is a real error. */ if (q_type == ISO_QUEUE_ITEM_TYPE_GAP_EVENT) { - LOG_ERR("[B]GapPostEvtBtaFail[%d][%u]", err, qev->type); - } else { - LOG_DBG("[B]GapRptDrop[%u]", qev->type); + ISO_POST_FAIL_LOG(err, "[B]GapPostEvtBtaFail[%d][%u]", err, qev->type); } goto free; } @@ -433,26 +418,7 @@ static void bt_le_bluedroid_gap_post_event_bta(tBTA_DM_BLE_5_GAP_EVENT event, return; free: - /* Mirror nimble/gap.c cleanup: free nested data buffers carried by - * specific event types before freeing the qev container itself. */ - switch (qev->type) { - case BT_LE_GAP_APP_PARAM_EXT_SCAN_RECV: - if (qev->ext_scan_recv.data) { - free(qev->ext_scan_recv.data); - qev->ext_scan_recv.data = NULL; - } - break; - case BT_LE_GAP_APP_PARAM_PA_SYNC_RECV: - if (qev->pa_sync_recv.data) { - free(qev->pa_sync_recv.data); - qev->pa_sync_recv.data = NULL; - } - break; - default: - break; - } - - free(qev); + bt_le_gap_event_free(qev); } int bt_le_bluedroid_scan_start(const struct bt_le_scan_param *param) @@ -557,6 +523,10 @@ int bt_le_bluedroid_scan_stop(void) int bt_le_bluedroid_gap_init(void) { +#if (BLE_50_EXTEND_SYNC_EN == TRUE) + active_pa_sync_handle = ISO_PA_SYNC_HANDLE_NONE; +#endif + BTM_BleGapRegisterCallback(gap_app_cb); return 0; diff --git a/components/bt/esp_ble_iso/host/adapter/bluedroid/gatt/gatt.c b/components/bt/esp_ble_iso/host/adapter/bluedroid/gatt/gatt.c index 28aa82bb80a..41268ba502f 100644 --- a/components/bt/esp_ble_iso/host/adapter/bluedroid/gatt/gatt.c +++ b/components/bt/esp_ble_iso/host/adapter/bluedroid/gatt/gatt.c @@ -21,6 +21,7 @@ #include "bta/bta_gatt_common.h" #include "bta_gattc_int.h" #include "btc_gatt_util.h" +#include "osi/allocator.h" #include "stack/btm_ble_api.h" #include "common/host.h" @@ -34,39 +35,29 @@ LOG_MODULE_REGISTER(ISO_BGAT, CONFIG_BT_ISO_LOG_LEVEL); * module that follows this pattern. */ #define GATTS_APP_UUID_BYTE 0x98 -static const tBT_UUID gatts_app_uuid = { - .len = LEN_UUID_128, - .uu.uuid128 = { - [0 ... 15] = GATTS_APP_UUID_BYTE, - }, -}; -static tBTA_GATTS_IF gatts_if; +static BT_ISO_EXT_RAM_BSS_ATTR tBT_UUID gatts_app_uuid; +static BT_ISO_EXT_RAM_BSS_ATTR tBTA_GATTS_IF gatts_if; /* Use UUID with a fixed pattern 0x99 for ISO & LE Audio GATT Client */ #define GATTC_APP_UUID_BYTE 0x99 -static tBT_UUID gattc_app_uuid = { - .len = LEN_UUID_128, - .uu.uuid128 = { - [0 ... 15] = GATTC_APP_UUID_BYTE, - }, -}; -static tBTA_GATTC_IF gattc_if; +static BT_ISO_EXT_RAM_BSS_ATTR tBT_UUID gattc_app_uuid; +static BT_ISO_EXT_RAM_BSS_ATTR tBTA_GATTC_IF gattc_if; -static struct gatts_svc_cb *gatts_svc_cb; +static BT_ISO_EXT_RAM_BSS_ATTR struct gatts_svc_cb *gatts_svc_cb; -static struct gatt_conn gatt_conns[CONFIG_BT_MAX_CONN]; +static BT_ISO_EXT_RAM_BSS_ATTR struct gatt_conn gatt_conns[CONFIG_BT_MAX_CONN]; /* Sems block bt_le_bluedroid_gatt_init() until the BTA app registrations * report back. gatts_sem is also reused by the audio adapter — only one * gatts_svc_cb is registered at a time, so sequential reuse is safe. */ -static struct k_sem gatts_sem; -static struct k_sem gattc_sem; +static BT_ISO_CTRL_BSS_ATTR struct k_sem gatts_sem; +static BT_ISO_CTRL_BSS_ATTR struct k_sem gattc_sem; /* Set by deinit before deleting the sems so a late BTA_*_REG_EVT skips the * give on a deleted handle. Same accepted residual race as hci.c's * direct_hci_shutting_down; the init-timeout window is near-unreachable. */ -static volatile bool gatt_shutting_down; +static BT_ISO_EXT_RAM_BSS_ATTR volatile bool gatt_shutting_down; enum { GATTC_OP_READ, @@ -90,7 +81,7 @@ static struct gattc_list_node *gattc_list_node_alloc(uint8_t type, void *params) { struct gattc_list_node *op; - op = calloc(1, sizeof(*op)); + op = bt_le_ext_calloc(1, sizeof(*op)); if (op == NULL) { return NULL; } @@ -158,7 +149,7 @@ static int gatts_indicate_resolve(const struct bt_gatt_attr *attr_in, handle = data.handle; if (bt_uuid_cmp(data.attr->uuid, BT_UUID_GATT_CHRC) == 0) { - assert(data.attr->user_data); + BT_LE_ASSERT(data.attr->user_data); chrc = data.attr->user_data; if ((chrc->properties & BT_GATT_CHRC_INDICATE) == 0) { @@ -180,7 +171,7 @@ static struct gatts_list_node *gatts_list_node_alloc(struct bt_gatt_indicate_par { struct gatts_list_node *n; - n = calloc(1, sizeof(*n)); + n = bt_le_ext_calloc(1, sizeof(*n)); if (n == NULL) { return NULL; } @@ -191,9 +182,9 @@ static struct gatts_list_node *gatts_list_node_alloc(struct bt_gatt_indicate_par n->data_copy = NULL; if (ip->len > 0) { - assert(ip->data); + BT_LE_ASSERT(ip->data); - n->data_copy = malloc(ip->len); + n->data_copy = bt_le_ext_malloc(ip->len); if (n->data_copy == NULL) { free(n); return NULL; @@ -236,6 +227,27 @@ static void gatts_notify_list_drain(struct gatt_conn *gatt_conn) } } +/* Remove the first notify marker whose value_handle matches. Used by + * handle_gatts_notify_tx_event so an indication CONF cannot be mistaken for + * a notify CONF when a notify marker is already queued (BTU/ISO race). */ +static struct gatts_notify_node * +gatts_notify_take_by_handle(struct gatt_conn *gatt_conn, uint16_t handle) +{ + struct gatts_notify_node *notify; + struct gatts_notify_node *tmp; + sys_snode_t *prev = NULL; + + SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&gatt_conn->gatts_notify_list, notify, tmp, node) { + if (notify->value_handle == handle) { + sys_slist_remove(&gatt_conn->gatts_notify_list, prev, ¬ify->node); + return notify; + } + prev = ¬ify->node; + } + + return NULL; +} + /* Rewrite a slot's conn_id to the GATTS/GATTC gatt_if (low byte); the BTA index * (high byte) is shared on one ACL. Lets a dual-role link op on the right conn_id. */ static inline uint16_t to_gatts_conn_id(uint16_t conn_id) @@ -290,6 +302,13 @@ uint8_t bt_le_bluedroid_gattc_get_if(void) uint8_t bt_le_bluedroid_gatts_get_if(void) { + /* Callers pass this straight to BTA without checking, so the value matters + * less than the log: a silent 0 surfaces only as an unexplained sem timeout. */ + if (gatts_if == 0) { + LOG_WRN("[B]GetGattsIfBeforeInit"); + return 0xFF; /* ESP_GATT_IF_NONE */ + } + return gatts_if; } @@ -444,8 +463,8 @@ static void gattc_connect_event_handler(tBTA_GATTC_CONNECT *connect) struct bt_le_gatt_event_param *qev; int err; - qev = calloc(1, sizeof(*qev)); - assert(qev); + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); qev->type = BT_LE_GATTC_CONNECT_EVENT; @@ -457,7 +476,7 @@ static void gattc_connect_event_handler(tBTA_GATTC_CONNECT *connect) err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); if (err) { - LOG_ERR("[B]GattcConnPostFail[%d]", err); + ISO_POST_FAIL_LOG(err, "[B]GattcConnPostFail[%d]", err); free(qev); } } @@ -467,8 +486,8 @@ static void gattc_disconnect_event_handler(tBTA_GATTC_DISCONNECT *disconnect) struct bt_le_gatt_event_param *qev; int err; - qev = calloc(1, sizeof(*qev)); - assert(qev); + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); qev->type = BT_LE_GATTC_DISCONNECT_EVENT; @@ -477,7 +496,7 @@ static void gattc_disconnect_event_handler(tBTA_GATTC_DISCONNECT *disconnect) err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); if (err) { - LOG_ERR("[B]GattcDiscPostFail[%d]", err); + ISO_POST_FAIL_LOG(err, "[B]GattcDiscPostFail[%d]", err); free(qev); } } @@ -487,8 +506,8 @@ static void gattc_open_event_handler(tBTA_GATTC_OPEN *open) struct bt_le_gatt_event_param *qev; int err; - qev = calloc(1, sizeof(*qev)); - assert(qev); + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); qev->type = BT_LE_GATTC_OPEN_EVENT; @@ -497,7 +516,7 @@ static void gattc_open_event_handler(tBTA_GATTC_OPEN *open) err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); if (err) { - LOG_ERR("[B]GattcOpenPostFail[%d]", err); + ISO_POST_FAIL_LOG(err, "[B]GattcOpenPostFail[%d]", err); free(qev); } } @@ -507,8 +526,8 @@ static void gattc_mtu_event_handler(tBTA_GATTC_CFG_MTU *cfg_mtu) struct bt_le_gatt_event_param *qev; int err; - qev = calloc(1, sizeof(*qev)); - assert(qev); + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); qev->type = BT_LE_GATTC_MTU_EVENT; @@ -518,7 +537,7 @@ static void gattc_mtu_event_handler(tBTA_GATTC_CFG_MTU *cfg_mtu) err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); if (err) { - LOG_ERR("[B]GattcMtuPostFail[%d]", err); + ISO_POST_FAIL_LOG(err, "[B]GattcMtuPostFail[%d]", err); free(qev); } } @@ -528,8 +547,8 @@ static void gattc_disc_cmpl_event_handler(tBTA_GATTC_DIS_CMPL *disc_cmpl) struct bt_le_gatt_event_param *qev; int err; - qev = calloc(1, sizeof(*qev)); - assert(qev); + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); qev->type = BT_LE_GATTC_DISC_CMPL_EVENT; @@ -538,7 +557,7 @@ static void gattc_disc_cmpl_event_handler(tBTA_GATTC_DIS_CMPL *disc_cmpl) err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); if (err) { - LOG_ERR("[B]GattcDiscCmplPostFail[%d]", err); + ISO_POST_FAIL_LOG(err, "[B]GattcDiscCmplPostFail[%d]", err); free(qev); } } @@ -548,8 +567,8 @@ static void gattc_read_chrc_event_handler(tBTA_GATTC_READ *read) struct bt_le_gatt_event_param *qev; int err; - qev = calloc(1, sizeof(*qev)); - assert(qev); + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); qev->type = BT_LE_GATTC_READ_CHRC_EVENT; @@ -562,19 +581,16 @@ static void gattc_read_chrc_event_handler(tBTA_GATTC_READ *read) read->p_value->p_value) { qev->gattc_read_chrc.len = read->p_value->len; - qev->gattc_read_chrc.value = calloc(1, read->p_value->len); - assert(qev->gattc_read_chrc.value); + qev->gattc_read_chrc.value = bt_le_ext_calloc(1, read->p_value->len); + BT_LE_ASSERT(qev->gattc_read_chrc.value); memcpy(qev->gattc_read_chrc.value, read->p_value->p_value, read->p_value->len); } err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); if (err) { - LOG_ERR("[B]GattcReadChrcPostFail[%d]", err); - if (qev->gattc_read_chrc.value) { - free(qev->gattc_read_chrc.value); - } - free(qev); + ISO_POST_FAIL_LOG(err, "[B]GattcReadChrcPostFail[%d]", err); + bt_le_gatt_event_free(qev); } } @@ -583,8 +599,8 @@ static void gattc_write_chrc_event_handler(tBTA_GATTC_WRITE *write) struct bt_le_gatt_event_param *qev; int err; - qev = calloc(1, sizeof(*qev)); - assert(qev); + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); qev->type = BT_LE_GATTC_WRITE_CHRC_EVENT; @@ -595,7 +611,7 @@ static void gattc_write_chrc_event_handler(tBTA_GATTC_WRITE *write) err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); if (err) { - LOG_ERR("[B]GattcWriteChrcPostFail[%d]", err); + ISO_POST_FAIL_LOG(err, "[B]GattcWriteChrcPostFail[%d]", err); free(qev); } } @@ -605,16 +621,16 @@ static void gatts_notify_tx_event_handler(tBTA_GATTS_REQ *req) struct bt_le_gatt_event_param *qev; int err; - qev = calloc(1, sizeof(*qev)); - assert(qev); + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); qev->type = BT_LE_GATTS_NOTIFY_TX_EVENT; /* BTA fires CONF_EVT for both indication acks and immediate notify * completions; tBTA_GATTS_REQ carries no flag to tell them apart. The - * bluedroid handler disambiguates by popping a parallel notify marker - * list first (see gatts_notify_enqueue / handle_gatts_notify_tx_event), - * so is_notify here is left unused on this path. */ + * bluedroid handler matches by attr_handle against gatts_list head / + * gatts_notify_list markers (see gatts_notify_take_by_handle), so + * is_notify here is left unused on this path. */ qev->gatts_notify_tx.is_notify = false; qev->gatts_notify_tx.conn_id = req->conn_id; qev->gatts_notify_tx.attr_handle = req->handle; @@ -622,7 +638,7 @@ static void gatts_notify_tx_event_handler(tBTA_GATTS_REQ *req) err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); if (err) { - LOG_ERR("[B]GattsNotifyTxPostFail[%d]", err); + ISO_POST_FAIL_LOG(err, "[B]GattsNotifyTxPostFail[%d]", err); free(qev); } } @@ -632,8 +648,8 @@ static void gattc_notify_rx_event_handler(tBTA_GATTC_NOTIFY *notify) struct bt_le_gatt_event_param *qev; int err; - qev = calloc(1, sizeof(*qev)); - assert(qev); + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); qev->type = BT_LE_GATTC_NOTIFY_RX_EVENT; @@ -644,15 +660,15 @@ static void gattc_notify_rx_event_handler(tBTA_GATTC_NOTIFY *notify) if (notify->len) { qev->gattc_notify_rx.len = notify->len; - qev->gattc_notify_rx.value = calloc(1, notify->len); - assert(qev->gattc_notify_rx.value); + qev->gattc_notify_rx.value = bt_le_ext_calloc(1, notify->len); + BT_LE_ASSERT(qev->gattc_notify_rx.value); memcpy(qev->gattc_notify_rx.value, notify->value, notify->len); } err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); if (err) { - LOG_ERR("[B]GattcNotifyRxPostFail[%d]", err); + ISO_POST_FAIL_LOG(err, "[B]GattcNotifyRxPostFail[%d]", err); if (qev->gattc_notify_rx.value) { free(qev->gattc_notify_rx.value); } @@ -665,8 +681,8 @@ static void gatts_connect_event_handler(tBTA_GATTS_CONN *connect) struct bt_le_gatt_event_param *qev; int err; - qev = calloc(1, sizeof(*qev)); - assert(qev); + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); qev->type = BT_LE_GATTS_CONNECT_EVENT; @@ -678,7 +694,7 @@ static void gatts_connect_event_handler(tBTA_GATTS_CONN *connect) err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); if (err) { - LOG_ERR("[B]GattsConnPostFail[%d]", err); + ISO_POST_FAIL_LOG(err, "[B]GattsConnPostFail[%d]", err); free(qev); } } @@ -688,8 +704,8 @@ static void gatts_disconnect_event_handler(tBTA_GATTS_CONN *disconnect) struct bt_le_gatt_event_param *qev; int err; - qev = calloc(1, sizeof(*qev)); - assert(qev); + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); qev->type = BT_LE_GATTS_DISCONNECT_EVENT; @@ -698,7 +714,7 @@ static void gatts_disconnect_event_handler(tBTA_GATTS_CONN *disconnect) err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); if (err) { - LOG_ERR("[B]GattsDiscPostFail[%d]", err); + ISO_POST_FAIL_LOG(err, "[B]GattsDiscPostFail[%d]", err); free(qev); } } @@ -709,8 +725,8 @@ static void gatts_mtu_event_handler(tBTA_GATTS_REQ *req) int err; /* req->p_data is non-NULL here: BTA always passes a stack object. */ - qev = calloc(1, sizeof(*qev)); - assert(qev); + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); qev->type = BT_LE_GATTS_MTU_EVENT; @@ -719,7 +735,7 @@ static void gatts_mtu_event_handler(tBTA_GATTS_REQ *req) err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); if (err) { - LOG_ERR("[B]GattsMtuPostFail[%d]", err); + ISO_POST_FAIL_LOG(err, "[B]GattsMtuPostFail[%d]", err); free(qev); } } @@ -729,8 +745,8 @@ static void gatts_read_req_handler(tBTA_GATTS_REQ *req) struct bt_le_gatt_event_param *qev; int err; - qev = calloc(1, sizeof(*qev)); - assert(qev); + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); qev->type = BT_LE_GATTS_READ_EVENT; @@ -744,7 +760,7 @@ static void gatts_read_req_handler(tBTA_GATTS_REQ *req) err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); if (err) { - LOG_ERR("[B]GattsReadPostFail[%d]", err); + ISO_POST_FAIL_LOG(err, "[B]GattsReadPostFail[%d]", err); free(qev); } } @@ -755,8 +771,8 @@ static void gatts_write_req_handler(tBTA_GATTS_REQ *req) int err; /* req->p_data is non-NULL here: BTA always passes a stack object. */ - qev = calloc(1, sizeof(*qev)); - assert(qev); + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); qev->type = BT_LE_GATTS_WRITE_EVENT; @@ -771,19 +787,16 @@ static void gatts_write_req_handler(tBTA_GATTS_REQ *req) if (req->p_data->write_req.len) { qev->gatts_write.len = req->p_data->write_req.len; - qev->gatts_write.value = calloc(1, req->p_data->write_req.len); - assert(qev->gatts_write.value); + qev->gatts_write.value = bt_le_ext_calloc(1, req->p_data->write_req.len); + BT_LE_ASSERT(qev->gatts_write.value); memcpy(qev->gatts_write.value, req->p_data->write_req.value, req->p_data->write_req.len); } err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); if (err) { - LOG_ERR("[B]GattsWritePostFail[%d]", err); - if (qev->gatts_write.value) { - free(qev->gatts_write.value); - } - free(qev); + ISO_POST_FAIL_LOG(err, "[B]GattsWritePostFail[%d]", err); + bt_le_gatt_event_free(qev); } } @@ -792,8 +805,8 @@ static void gatts_exec_write_req_handler(tBTA_GATTS_REQ *req) struct bt_le_gatt_event_param *qev; int err; - qev = calloc(1, sizeof(*qev)); - assert(qev); + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); qev->type = BT_LE_GATTS_EXEC_WRITE_EVENT; qev->gatts_exec_write.conn_id = req->conn_id; @@ -803,13 +816,20 @@ static void gatts_exec_write_req_handler(tBTA_GATTS_REQ *req) err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); if (err) { - LOG_ERR("[B]GattsExecPostFail[%d]", err); + ISO_POST_FAIL_LOG(err, "[B]GattsExecPostFail[%d]", err); free(qev); } } static void gatts_app_cb(tBTA_GATTS_EVT event, tBTA_GATTS *p_data) { + /* BTA_GATTS_AppDeregister is async, so its events land after deinit returns. + * Drop on the flag, not gatts_if - a re-register can hand back the same if. */ + if (gatt_shutting_down) { + LOG_INF("[B]GattsDropEvt[%u]", event); + return; + } + switch (event) { case BTA_GATTS_REG_EVT: LOG_DBG("[B]GattsRegEvt[%u][%u]", @@ -819,10 +839,6 @@ static void gatts_app_cb(tBTA_GATTS_EVT event, tBTA_GATTS *p_data) p_data->reg_oper.uuid.uu.uuid128[0], p_data->reg_oper.uuid.uu.uuid128[1]); - if (gatt_shutting_down) { - break; - } - if (p_data->reg_oper.status == BTA_GATT_OK && memcmp(&p_data->reg_oper.uuid, &gatts_app_uuid, sizeof(tBT_UUID)) == 0) { gatts_if = p_data->reg_oper.server_if; @@ -850,7 +866,7 @@ static void gatts_app_cb(tBTA_GATTS_EVT event, tBTA_GATTS *p_data) p_data->conn.conn_params.timeout); if (p_data->conn.server_if != gatts_if) { - LOG_ERR("[B]GattsConnUnknownIf[%u]", p_data->conn.server_if); + LOG_ERR("[B]GattsConnUnknownIf[%u][%u]", p_data->conn.server_if, gatts_if); break; } @@ -866,7 +882,7 @@ static void gatts_app_cb(tBTA_GATTS_EVT event, tBTA_GATTS *p_data) p_data->conn.remote_bda[4], p_data->conn.remote_bda[5]); if (p_data->conn.server_if != gatts_if) { - LOG_ERR("[B]GattsDisconnUnknownIf[%u]", p_data->conn.server_if); + LOG_ERR("[B]GattsDisconnUnknownIf[%u][%u]", p_data->conn.server_if, gatts_if); break; } @@ -894,8 +910,8 @@ static void gatts_app_cb(tBTA_GATTS_EVT event, tBTA_GATTS *p_data) p_data->req_data.p_data ? p_data->req_data.p_data->mtu : 0); if (BTC_GATT_GET_GATT_IF(p_data->req_data.conn_id) != gatts_if) { - LOG_ERR("[B]GattsMtuUnknownIf[%u]", - BTC_GATT_GET_GATT_IF(p_data->req_data.conn_id)); + LOG_ERR("[B]GattsMtuUnknownIf[%u][%u]", + BTC_GATT_GET_GATT_IF(p_data->req_data.conn_id), gatts_if); break; } @@ -936,7 +952,7 @@ static void gatts_app_cb(tBTA_GATTS_EVT event, tBTA_GATTS *p_data) p_data->create.uuid.uu.uuid128[1]); if (p_data->create.server_if != gatts_if) { - LOG_ERR("[B]GattsCreateUnknownIf[%u]", p_data->create.server_if); + LOG_ERR("[B]GattsCreateUnknownIf[%u][%u]", p_data->create.server_if, gatts_if); break; } @@ -953,7 +969,7 @@ static void gatts_app_cb(tBTA_GATTS_EVT event, tBTA_GATTS *p_data) p_data->add_result.attr_id, p_data->add_result.service_id); if (p_data->add_result.server_if != gatts_if) { - LOG_ERR("[B]GattsAddInclSvcUnknownIf[%u]", p_data->add_result.server_if); + LOG_ERR("[B]GattsAddInclSvcUnknownIf[%u][%u]", p_data->add_result.server_if, gatts_if); break; } @@ -971,7 +987,7 @@ static void gatts_app_cb(tBTA_GATTS_EVT event, tBTA_GATTS *p_data) p_data->add_result.char_uuid.len, p_data->add_result.char_uuid.uu.uuid16); if (p_data->add_result.server_if != gatts_if) { - LOG_ERR("[B]GattsAddCharUnknownIf[%u]", p_data->add_result.server_if); + LOG_ERR("[B]GattsAddCharUnknownIf[%u][%u]", p_data->add_result.server_if, gatts_if); break; } @@ -989,7 +1005,7 @@ static void gatts_app_cb(tBTA_GATTS_EVT event, tBTA_GATTS *p_data) p_data->add_result.char_uuid.len, p_data->add_result.char_uuid.uu.uuid16); if (p_data->add_result.server_if != gatts_if) { - LOG_ERR("[B]GattsAddDescrUnknownIf[%u]", p_data->add_result.server_if); + LOG_ERR("[B]GattsAddDescrUnknownIf[%u][%u]", p_data->add_result.server_if, gatts_if); break; } @@ -1015,8 +1031,8 @@ static void gatts_app_cb(tBTA_GATTS_EVT event, tBTA_GATTS *p_data) p_data->req_data.p_data->read_req.need_rsp); if (BTC_GATT_GET_GATT_IF(p_data->req_data.conn_id) != gatts_if) { - LOG_ERR("[B]GattsReadUnknownIf[%u]", - BTC_GATT_GET_GATT_IF(p_data->req_data.conn_id)); + LOG_ERR("[B]GattsReadUnknownIf[%u][%u]", + BTC_GATT_GET_GATT_IF(p_data->req_data.conn_id), gatts_if); break; } @@ -1039,8 +1055,8 @@ static void gatts_app_cb(tBTA_GATTS_EVT event, tBTA_GATTS *p_data) } if (BTC_GATT_GET_GATT_IF(p_data->req_data.conn_id) != gatts_if) { - LOG_ERR("[B]GattsWriteUnknownIf[%u]", - BTC_GATT_GET_GATT_IF(p_data->req_data.conn_id)); + LOG_ERR("[B]GattsWriteUnknownIf[%u][%u]", + BTC_GATT_GET_GATT_IF(p_data->req_data.conn_id), gatts_if); break; } @@ -1059,8 +1075,8 @@ static void gatts_app_cb(tBTA_GATTS_EVT event, tBTA_GATTS *p_data) } if (BTC_GATT_GET_GATT_IF(p_data->req_data.conn_id) != gatts_if) { - LOG_ERR("[B]GattsExecUnknownIf[%u]", - BTC_GATT_GET_GATT_IF(p_data->req_data.conn_id)); + LOG_ERR("[B]GattsExecUnknownIf[%u][%u]", + BTC_GATT_GET_GATT_IF(p_data->req_data.conn_id), gatts_if); break; } @@ -1098,6 +1114,12 @@ static void gatts_app_cb(tBTA_GATTS_EVT event, tBTA_GATTS *p_data) static void gattc_app_cb(tBTA_GATTC_EVT event, tBTA_GATTC *p_data) { + /* See gatts_app_cb. */ + if (gatt_shutting_down) { + LOG_INF("[B]GattcDropEvt[%u]", event); + return; + } + switch (event) { case BTA_GATTC_REG_EVT: LOG_DBG("[B]GattcRegEvt[%u][%u]", @@ -1107,10 +1129,6 @@ static void gattc_app_cb(tBTA_GATTC_EVT event, tBTA_GATTC *p_data) p_data->reg_oper.app_uuid.uu.uuid128[0], p_data->reg_oper.app_uuid.uu.uuid128[1]); - if (gatt_shutting_down) { - break; - } - if (p_data->reg_oper.status == BTA_GATT_OK && memcmp(&p_data->reg_oper.app_uuid, &gattc_app_uuid, sizeof(tBT_UUID)) == 0) { gattc_if = p_data->reg_oper.client_if; @@ -1137,7 +1155,7 @@ static void gattc_app_cb(tBTA_GATTC_EVT event, tBTA_GATTC *p_data) p_data->connect.conn_params.latency, p_data->connect.conn_params.timeout); if (p_data->connect.client_if != gattc_if) { - LOG_ERR("[B]GattcConnUnknownIf[%u]", p_data->connect.client_if); + LOG_ERR("[B]GattcConnUnknownIf[%u][%u]", p_data->connect.client_if, gattc_if); break; } @@ -1153,7 +1171,7 @@ static void gattc_app_cb(tBTA_GATTC_EVT event, tBTA_GATTC *p_data) p_data->disconnect.remote_bda[4], p_data->disconnect.remote_bda[5]); if (p_data->disconnect.client_if != gattc_if) { - LOG_ERR("[B]GattcDisconnUnknownIf[%u]", p_data->disconnect.client_if); + LOG_ERR("[B]GattcDisconnUnknownIf[%u][%u]", p_data->disconnect.client_if, gattc_if); break; } @@ -1170,7 +1188,7 @@ static void gattc_app_cb(tBTA_GATTC_EVT event, tBTA_GATTC *p_data) p_data->open.remote_bda[4], p_data->open.remote_bda[5]); if (p_data->open.client_if != gattc_if) { - LOG_ERR("[B]GattcOpenUnknownIf[%u]", p_data->open.client_if); + LOG_ERR("[B]GattcOpenUnknownIf[%u][%u]", p_data->open.client_if, gattc_if); break; } @@ -1194,8 +1212,8 @@ static void gattc_app_cb(tBTA_GATTC_EVT event, tBTA_GATTC *p_data) p_data->cfg_mtu.status, p_data->cfg_mtu.conn_id, p_data->cfg_mtu.mtu); if (BTC_GATT_GET_GATT_IF(p_data->cfg_mtu.conn_id) != gattc_if) { - LOG_ERR("[B]GattcMtuUnknownIf[%u]", - BTC_GATT_GET_GATT_IF(p_data->cfg_mtu.conn_id)); + LOG_ERR("[B]GattcMtuUnknownIf[%u][%u]", + BTC_GATT_GET_GATT_IF(p_data->cfg_mtu.conn_id), gattc_if); break; } @@ -1216,8 +1234,8 @@ static void gattc_app_cb(tBTA_GATTC_EVT event, tBTA_GATTC *p_data) p_data->dis_cmpl.status, p_data->dis_cmpl.conn_id); if (BTC_GATT_GET_GATT_IF(p_data->dis_cmpl.conn_id) != gattc_if) { - LOG_ERR("[B]GattcDiscSvcCmplUnknownIf[%u]", - BTC_GATT_GET_GATT_IF(p_data->dis_cmpl.conn_id)); + LOG_ERR("[B]GattcDiscSvcCmplUnknownIf[%u][%u]", + BTC_GATT_GET_GATT_IF(p_data->dis_cmpl.conn_id), gattc_if); break; } @@ -1235,8 +1253,8 @@ static void gattc_app_cb(tBTA_GATTC_EVT event, tBTA_GATTC *p_data) p_data->srvc_res.service_uuid.inst_id); if (BTC_GATT_GET_GATT_IF(p_data->srvc_res.conn_id) != gattc_if) { - LOG_ERR("[B]GattcSearchResUnknownIf[%u]", - BTC_GATT_GET_GATT_IF(p_data->srvc_res.conn_id)); + LOG_ERR("[B]GattcSearchResUnknownIf[%u][%u]", + BTC_GATT_GET_GATT_IF(p_data->srvc_res.conn_id), gattc_if); break; } break; @@ -1248,8 +1266,8 @@ static void gattc_app_cb(tBTA_GATTC_EVT event, tBTA_GATTC *p_data) BTA_GATTC_SERVICE_INFO_FROM_REMOTE_DEVICE) ? "Remote" : "NVS"); if (BTC_GATT_GET_GATT_IF(p_data->search_cmpl.conn_id) != gattc_if) { - LOG_ERR("[B]GattcSearchCmplUnknownIf[%u]", - BTC_GATT_GET_GATT_IF(p_data->search_cmpl.conn_id)); + LOG_ERR("[B]GattcSearchCmplUnknownIf[%u][%u]", + BTC_GATT_GET_GATT_IF(p_data->search_cmpl.conn_id), gattc_if); break; } break; @@ -1259,8 +1277,8 @@ static void gattc_app_cb(tBTA_GATTC_EVT event, tBTA_GATTC *p_data) p_data->read.status, p_data->read.conn_id, p_data->read.handle); if (BTC_GATT_GET_GATT_IF(p_data->read.conn_id) != gattc_if) { - LOG_ERR("[B]GattcReadCharUnknownIf[%u]", - BTC_GATT_GET_GATT_IF(p_data->read.conn_id)); + LOG_ERR("[B]GattcReadCharUnknownIf[%u][%u]", + BTC_GATT_GET_GATT_IF(p_data->read.conn_id), gattc_if); break; } @@ -1288,8 +1306,8 @@ static void gattc_app_cb(tBTA_GATTC_EVT event, tBTA_GATTC *p_data) p_data->write.handle, p_data->write.offset); if (BTC_GATT_GET_GATT_IF(p_data->write.conn_id) != gattc_if) { - LOG_ERR("[B]GattcWriteCharUnknownIf[%u]", - BTC_GATT_GET_GATT_IF(p_data->write.conn_id)); + LOG_ERR("[B]GattcWriteCharUnknownIf[%u][%u]", + BTC_GATT_GET_GATT_IF(p_data->write.conn_id), gattc_if); break; } @@ -1321,8 +1339,8 @@ static void gattc_app_cb(tBTA_GATTC_EVT event, tBTA_GATTC *p_data) p_data->notify.bda[4], p_data->notify.bda[5]); if (BTC_GATT_GET_GATT_IF(p_data->notify.conn_id) != gattc_if) { - LOG_ERR("[B]GattcNotifUnknownIf[%u]", - BTC_GATT_GET_GATT_IF(p_data->notify.conn_id)); + LOG_ERR("[B]GattcNotifUnknownIf[%u][%u]", + BTC_GATT_GET_GATT_IF(p_data->notify.conn_id), gattc_if); break; } @@ -1378,17 +1396,17 @@ static void post_acl_connect_app_event(struct gatt_conn *gatt_conn) struct bt_le_gap_app_param *qev; int err; - qev = calloc(1, sizeof(*qev)); - assert(qev); + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); qev->type = BT_LE_GAP_APP_PARAM_ACL_CONNECT; qev->acl_connect.status = gatt_conn->status; /* Populate identity unconditionally: connect_event_handler fills these - * fields before OPEN_EVT arrives, so failure events still carry valid - * conn_handle / role / dst to the application — matches NimBLE's - * BLE_GAP_EVENT_CONNECT, which always exposes the full descriptor. */ + * fields before OPEN_EVT arrives, so a failure that got that far still + * carries valid conn_handle / role / dst to the application. One that + * did not is reported by post_acl_connect_fail_app_event() instead. */ qev->acl_connect.conn_handle = gatt_conn->conn_handle; qev->acl_connect.role = gatt_conn->role; qev->acl_connect.dst.type = gatt_conn->peer.type; @@ -1396,7 +1414,29 @@ static void post_acl_connect_app_event(struct gatt_conn *gatt_conn) err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GAP_EVENT, qev, sizeof(*qev)); if (err) { - LOG_ERR("[B]AclConnPostFail[%d]", err); + ISO_POST_FAIL_LOG(err, "[B]AclConnPostFail[%d]", err); + free(qev); + } +} + +/* A connect that never reached the link layer gets no CONNECT_EVT, so nothing + * would tell the application its request died. Identity stays zero, matching + * NimBLE, which fills the descriptor only on status 0. */ +static void post_acl_connect_fail_app_event(uint8_t status) +{ + struct bt_le_gap_app_param *qev; + int err; + + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); + + qev->type = BT_LE_GAP_APP_PARAM_ACL_CONNECT; + + qev->acl_connect.status = status; + + err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GAP_EVENT, qev, sizeof(*qev)); + if (err) { + ISO_POST_FAIL_LOG(err, "[B]AclConnFailPostFail[%d]", err); free(qev); } } @@ -1406,8 +1446,8 @@ static void post_acl_disconnect_app_event(uint16_t conn_handle, uint8_t reason) struct bt_le_gap_app_param *qev; int err; - qev = calloc(1, sizeof(*qev)); - assert(qev); + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); qev->type = BT_LE_GAP_APP_PARAM_ACL_DISCONNECT; @@ -1416,11 +1456,29 @@ static void post_acl_disconnect_app_event(uint16_t conn_handle, uint8_t reason) err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GAP_EVENT, qev, sizeof(*qev)); if (err) { - LOG_ERR("[B]AclDiscPostFail[%d]", err); + ISO_POST_FAIL_LOG(err, "[B]AclDiscPostFail[%d]", err); free(qev); } } +/* Fire func(err) for every pending GATTS indication and free the nodes, so lib + * state machines waiting on a confirm unblock. conn may be NULL (already gone) + * — func(NULL, ...) still releases the waiter. */ +static void gatts_list_fire_and_drain(struct gatt_conn *gatt_conn, + struct bt_conn *conn, uint8_t err) +{ + struct gatts_list_node *n; + sys_snode_t *snode; + + while ((snode = sys_slist_get(&gatt_conn->gatts_list)) != NULL) { + n = CONTAINER_OF(snode, struct gatts_list_node, node); + if (n->params_copy.func) { + n->params_copy.func(conn, &n->params_copy, err); + } + gatts_list_node_free(n); + } +} + static void handle_gattc_connect_event(struct bt_le_gattc_connect_event *event) { struct gatt_conn *gatt_conn; @@ -1433,8 +1491,22 @@ static void handle_gattc_connect_event(struct bt_le_gattc_connect_event *event) gatt_conn = bt_le_bluedroid_find_gatt_conn_with_addr(event->peer.type, event->peer.val, false); if (gatt_conn) { - LOG_ERR("[B]DevAlreadyExists"); - return; + if (gatt_conn->conn_handle == event->conn_handle) { + LOG_ERR("[B]DevAlreadyExists"); + return; + } + + /* Same peer, different handle: prior disconnect was missed/delayed. + * Drop the stale slot so reconnect can proceed. */ + LOG_WRN("[B]StaleGattcConn[%u][%u]", gatt_conn->conn_handle, event->conn_handle); + + post_acl_disconnect_app_event(gatt_conn->conn_handle, HCI_ERR_UNSPECIFIED); + + /* Stale slot never gets a disconnect event: fire pending indication + * funcs so lib state machines unblock before the drain in reset. */ + gatts_list_fire_and_drain(gatt_conn, NULL, BT_ATT_ERR_UNLIKELY); + + reset_gatt_conn(gatt_conn); } /* App initiated via esp_ble_gattc_aux_open(engine_gattc_if, ...). BTA @@ -1460,9 +1532,7 @@ static void handle_gattc_connect_event(struct bt_le_gattc_connect_event *event) static void handle_gattc_disconnect_event(struct bt_le_gattc_disconnect_event *event) { struct gatt_conn *gatt_conn; - struct gatts_list_node *n; struct bt_conn *conn; - sys_snode_t *snode; gatt_conn = find_gatt_conn_with_conn_id(event->conn_id); if (gatt_conn == NULL) { @@ -1473,22 +1543,15 @@ static void handle_gattc_disconnect_event(struct bt_le_gattc_disconnect_event *e event->conn_handle = gatt_conn->conn_handle; if (gatt_conn->role == BTM_ROLE_MASTER) { - /* CENTRAL may also run a GATT server (e.g. CAP Initiator with PACS). - * Mirror handle_gatts_disconnect_event: fire func(err) for every - * pending indication so lib state machines don't stall. */ conn = bt_le_acl_conn_find(event->conn_handle); - while ((snode = sys_slist_get(&gatt_conn->gatts_list)) != NULL) { - n = CONTAINER_OF(snode, struct gatts_list_node, node); - if (conn && n->params_copy.func) { - n->params_copy.func(conn, &n->params_copy, BT_ATT_ERR_UNLIKELY); - } - gatts_list_node_free(n); - } + /* CENTRAL may also run a GATT server (e.g. CAP Initiator with PACS): + * fire func(err) for pending indications so lib state machines don't + * stall. conn may be gone — func(NULL, ...) still unblocks waiters. */ + gatts_list_fire_and_drain(gatt_conn, conn, BT_ATT_ERR_UNLIKELY); post_acl_disconnect_app_event(gatt_conn->conn_handle, event->reason); - /* Reset the corresponding gatt_conn */ reset_gatt_conn(gatt_conn); } } @@ -1500,7 +1563,16 @@ static void handle_gattc_open_event(struct bt_le_gattc_open_event *event) gatt_conn = find_gatt_conn_by_conn_id_or_index(event->conn_id); if (gatt_conn == NULL) { - LOG_ERR("[B]GattcOpenUnknownDev"); + /* A non-zero status here is the application's own connect request failing + * before any link existed; a success without a slot is a real + * inconsistency and stays an error. */ + if (event->status) { + LOG_WRN("[B]GattcOpenFailed[%u][0x%04x]", event->status, event->conn_id); + post_acl_connect_fail_app_event(event->status); + } else { + LOG_ERR("[B]GattcOpenUnknownDev[0x%04x]", event->conn_id); + } + return; } @@ -1512,13 +1584,11 @@ static void handle_gattc_open_event(struct bt_le_gattc_open_event *event) post_acl_connect_app_event(gatt_conn); if (gatt_conn->status) { - /* If failed to create connection, reset the corresponding gatt_conn */ reset_gatt_conn(gatt_conn); return; } } else { if (gatt_conn->gattc_open && gatt_conn->status != BTA_GATT_OK) { - /* Failed to open gatt client, post an event to notify the app layer */ bt_le_gattc_app_open_event(event, gattc_if); return; } @@ -1536,15 +1606,13 @@ static void handle_gattc_open_event(struct bt_le_gattc_open_event *event) gatt_conn->cfg_mtu = 1; } - /* Post an event to the app layer, we need this event to discover - * services and characteristics, etc. - */ bt_le_gattc_app_open_event(event, gattc_if); } static void handle_gattc_mtu_event(struct bt_le_gattc_mtu_event *event) { struct gatt_conn *gatt_conn; + uint16_t prev_mtu; gatt_conn = find_gatt_conn_by_conn_id_or_index(event->conn_id); if (gatt_conn == NULL) { @@ -1558,6 +1626,8 @@ static void handle_gattc_mtu_event(struct bt_le_gattc_mtu_event *event) * related to the Link Layer role, hence we don't check the Link layer * role here and only check if the MTU has already been exchanged here. */ + prev_mtu = gatt_conn->mtu; + if (gatt_conn->mtu != 0) { LOG_INF("[B]GattcMtuExchanged[%u][%u][%u]", gatt_conn->conn_handle, gatt_conn->mtu, event->mtu); @@ -1568,13 +1638,17 @@ static void handle_gattc_mtu_event(struct bt_le_gattc_mtu_event *event) } if (gatt_conn->cfg_mtu && gatt_conn->mtu_posted == 0) { - /* Post an event to the app layer, we need this event to discover - * services and characteristics, etc. Report the clamped value so - * the app stays consistent with gatt_conn state (mirrors NimBLE's - * BLE_GAP_EVENT_MTU which already carries the negotiated value). */ + /* Report the clamped value so the app stays consistent with + * gatt_conn state (mirrors NimBLE's BLE_GAP_EVENT_MTU). */ bt_le_gatt_app_mtu_change_event(event->conn_handle, gatt_conn->mtu); - gatt_conn->mtu_posted = 1; /* Mark MTU event as posted */ + gatt_conn->mtu_posted = 1; + } else if (gatt_conn->mtu_posted && gatt_conn->mtu < prev_mtu) { + /* Dual-role: peer/side may clamp after the first post; app must + * learn the smaller value or it will oversize ATT PDUs. */ + LOG_INF("[B]GattcMtuClamped[%u][%u][%u]", + event->conn_handle, prev_mtu, gatt_conn->mtu); + bt_le_gatt_app_mtu_change_event(event->conn_handle, gatt_conn->mtu); } } @@ -1627,7 +1701,6 @@ static void handle_gattc_read_chrc_event(struct bt_le_gattc_read_chrc_event *eve const uint8_t *val; sys_snode_t *snode; uint16_t vlen; - uint16_t off; uint8_t ret; gatt_conn = find_gatt_conn_by_conn_id_or_index(event->conn_id); @@ -1670,11 +1743,14 @@ static void handle_gattc_read_chrc_event(struct bt_le_gattc_read_chrc_event *eve goto end; } - if ((read_copy.handle_count == 0 && - (event->attr_handle < read_copy.by_uuid.start_handle || - event->attr_handle > read_copy.by_uuid.end_handle)) || - (read_copy.handle_count == 1 && - event->attr_handle != read_copy.single.handle)) { + /* On error BTA often returns attr_handle 0; only validate the handle when + * the read succeeded, otherwise pass the real ATT status to the caller. */ + if (event->status == 0 && + ((read_copy.handle_count == 0 && + (event->attr_handle < read_copy.by_uuid.start_handle || + event->attr_handle > read_copy.by_uuid.end_handle)) || + (read_copy.handle_count == 1 && + event->attr_handle != read_copy.single.handle))) { LOG_ERR("[B]GattcRdCharInvRsp[%u][%u][%u][%u][%u]", read_copy.handle_count, event->attr_handle, read_copy.by_uuid.start_handle, @@ -1686,13 +1762,6 @@ static void handle_gattc_read_chrc_event(struct bt_le_gattc_read_chrc_event *eve val = event->value; vlen = event->len; - if (read_copy.handle_count == 1 && read_copy.single.offset != 0 && - event->status == 0) { - off = read_copy.single.offset; - val = (off < event->len) ? event->value + off : NULL; - vlen = (off < event->len) ? (event->len - off) : 0; - } - /* By-UUID read: report the matched handle in params->by_uuid.start_handle (like * Zephyr); callers (has_client active-index read) use it as the value handle. */ if (read_copy.handle_count == 0 && event->status == 0) { @@ -1755,7 +1824,7 @@ static void handle_gattc_write_chrc_event(struct bt_le_gattc_write_chrc_event *e params = op->write_params; free(op); - assert(params && params->func); + BT_LE_ASSERT(params && params->func); if (event->attr_handle != params->handle) { LOG_ERR("[B]GattcWrCharInvRsp[%u][%u]", event->attr_handle, params->handle); @@ -1812,7 +1881,7 @@ static void handle_gattc_notify_event(struct bt_le_gattc_notify_rx_event *event) * tearing down a core subscription like the ASCS control point * over one bad PDU would drop every later notification. Tolerate * the bad PDU and keep the subscription. */ - params->notify(conn, params, event->value, event->len); + params->notify(conn, params, NOTIFY_VALUE(event), event->len); } } } @@ -1836,8 +1905,22 @@ static void handle_gatts_connect_event(struct bt_le_gatts_connect_event *event) gatt_conn = bt_le_bluedroid_find_gatt_conn_with_addr(event->peer.type, event->peer.val, false); if (gatt_conn) { - LOG_ERR("[B]DevAlreadyExists"); - return; + if (gatt_conn->conn_handle == event->conn_handle) { + LOG_ERR("[B]DevAlreadyExists"); + return; + } + + /* Same peer, different handle: prior disconnect was missed/delayed. + * Drop the stale slot so reconnect can proceed. */ + LOG_WRN("[B]StaleGattsConn[%u][%u]", gatt_conn->conn_handle, event->conn_handle); + + post_acl_disconnect_app_event(gatt_conn->conn_handle, HCI_ERR_UNSPECIFIED); + + /* Stale slot never gets a disconnect event: fire pending indication + * funcs so lib state machines unblock before the drain in reset. */ + gatts_list_fire_and_drain(gatt_conn, NULL, BT_ATT_ERR_UNLIKELY); + + reset_gatt_conn(gatt_conn); } gatt_conn = bt_le_bluedroid_find_free_gatt_conn(); @@ -1870,14 +1953,12 @@ static void handle_gatts_connect_event(struct bt_le_gatts_connect_event *event) static void handle_gatts_disconnect_event(struct bt_le_gatts_disconnect_event *event) { - struct gatts_list_node *n; struct gatt_conn *gatt_conn; struct bt_conn *conn; - sys_snode_t *snode; gatt_conn = find_gatt_conn_with_conn_id(event->conn_id); if (gatt_conn == NULL) { - LOG_WRN("[B]GattsDisconnUnknownDev"); + LOG_DBG("[B]GattsDisconnUnknownDev"); return; } @@ -1889,32 +1970,22 @@ static void handle_gatts_disconnect_event(struct bt_le_gatts_disconnect_event *e return; } - /* conn may already have been torn down by the ACL layer before this event - * arrives — best-effort lookup, NULL is tolerated and the drain still runs - * (params->func is just skipped for nodes we can't address). */ conn = bt_le_acl_conn_find(event->conn_handle); - /* Fire func with err for every pending indication. Mirrors NimBLE stack's - * ble_gatts_indicate_fail_notconn on disconn — BTA never delivers CONF_EVT - * for in-flight indications when the conn drops, so the adapter has to - * simulate that fail path to keep lib state machines from stalling. */ - while ((snode = sys_slist_get(&gatt_conn->gatts_list)) != NULL) { - n = CONTAINER_OF(snode, struct gatts_list_node, node); - if (conn && n->params_copy.func) { - n->params_copy.func(conn, &n->params_copy, BT_ATT_ERR_UNLIKELY); - } - gatts_list_node_free(n); - } + /* conn may be gone — fire func(NULL, err) for pending indications so upper + * layers don't stall waiting for a CONF_EVT BTA never delivers after the + * link drops. */ + gatts_list_fire_and_drain(gatt_conn, conn, BT_ATT_ERR_UNLIKELY); post_acl_disconnect_app_event(gatt_conn->conn_handle, event->reason); - /* Reset the corresponding gatt_conn */ reset_gatt_conn(gatt_conn); } static void handle_gatts_mtu_event(struct bt_le_gatts_mtu_event *event) { struct gatt_conn *gatt_conn; + uint16_t prev_mtu; gatt_conn = find_gatt_conn_by_conn_id_or_index(event->conn_id); if (gatt_conn == NULL) { @@ -1928,6 +1999,8 @@ static void handle_gatts_mtu_event(struct bt_le_gatts_mtu_event *event) * related to the Link Layer role, hence we don't check the Link layer * role here and only check if the MTU has already been exchanged here. */ + prev_mtu = gatt_conn->mtu; + if (gatt_conn->mtu != 0) { LOG_INF("[B]GattsMtuExchanged[%u][%u][%u]", gatt_conn->conn_handle, gatt_conn->mtu, event->mtu); @@ -1941,7 +2014,12 @@ static void handle_gatts_mtu_event(struct bt_le_gatts_mtu_event *event) /* Report clamped value — see handle_gattc_mtu_event. */ bt_le_gatt_app_mtu_change_event(event->conn_handle, gatt_conn->mtu); - gatt_conn->mtu_posted = 1; /* Mark MTU event as posted */ + gatt_conn->mtu_posted = 1; + } else if (gatt_conn->mtu < prev_mtu) { + /* Dual-role clamp after first post — see handle_gattc_mtu_event. */ + LOG_INF("[B]GattsMtuClamped[%u][%u][%u]", + event->conn_handle, prev_mtu, gatt_conn->mtu); + bt_le_gatt_app_mtu_change_event(event->conn_handle, gatt_conn->mtu); } } @@ -1979,11 +2057,11 @@ static void handle_gatts_read_event(struct bt_le_gatts_read_event *event) goto end; } - /* The tBTA_GATT_STATUS structure is too large, hence use + /* The tBTA_GATTS_RSP structure is too large, hence use * dynamic memory here to avoid stack overflow. */ - rsp = calloc(1, sizeof(*(rsp))); - assert(rsp); + rsp = bt_le_ext_calloc(1, sizeof(*(rsp))); + BT_LE_ASSERT(rsp); rsp->attr_value.handle = event->attr_handle; @@ -1993,7 +2071,7 @@ static void handle_gatts_read_event(struct bt_le_gatts_read_event *event) * values larger than one PDU (e.g. a BASS Broadcast Receive State). */ ret = attr->read(conn, attr, (void *)rsp, GATT_MAX_ATTR_LEN, event->offset); if (ret < 0) { - LOG_DBG("[B]GattsRdEvtErr[%u][%d]", event->attr_handle, ret); + LOG_WRN("[B]GattsRdEvtErr[%u][%d]", event->attr_handle, ret); status = BT_GATT_ERR(ret); } @@ -2078,8 +2156,8 @@ static void handle_gatts_prepare_write(struct bt_le_gatts_write_event *event) if (status == BTA_GATT_OK) { /* The prepare-write response must echo handle/offset/value. */ - rsp = calloc(1, sizeof(*rsp)); - assert(rsp); + rsp = bt_le_ext_calloc(1, sizeof(*rsp)); + BT_LE_ASSERT(rsp); rsp->attr_value.handle = event->attr_handle; rsp->attr_value.offset = event->offset; @@ -2143,7 +2221,6 @@ static void handle_gatts_write_event(struct bt_le_gatts_write_event *event) goto end; } - /* Check if the attribute UUID is CCCD */ if (bt_uuid_cmp(attr->uuid, BT_UUID_GATT_CCC) == 0) { if (event->len != 2) { LOG_ERR("[B]GattsWrInvPdu[%u]", event->len); @@ -2188,7 +2265,7 @@ static void handle_gatts_write_event(struct bt_le_gatts_write_event *event) } else { ret = attr->write(conn, attr, event->value, event->len, 0, 0); if (ret < 0) { - LOG_DBG("[B]GattsWrEvtErr[%u][%d]", event->attr_handle, ret); + LOG_WRN("[B]GattsWrEvtErr[%u][%d]", event->attr_handle, ret); status = BT_GATT_ERR(ret); } @@ -2249,7 +2326,7 @@ static void handle_gatts_exec_write_event(struct bt_le_gatts_exec_write_event *e } else { ret = attr->write(conn, attr, gatt_conn->prep_buf, gatt_conn->prep_len, 0, 0); if (ret < 0) { - LOG_DBG("[B]GattsExecWrErr[%u][%d]", gatt_conn->prep_attr_handle, (int)ret); + LOG_WRN("[B]GattsExecWrErr[%u][%d]", gatt_conn->prep_attr_handle, (int)ret); status = BT_GATT_ERR(ret); } } @@ -2266,10 +2343,12 @@ static void handle_gatts_exec_write_event(struct bt_le_gatts_exec_write_event *e static void handle_gatts_notify_tx_event(struct bt_le_gatts_notify_tx_event *event) { - struct gatts_list_node *n; + struct gatts_notify_node *notify; struct gatt_conn *gatt_conn; + struct gatts_list_node *n; struct bt_conn *conn; sys_snode_t *snode; + bool ind_match; /* Find conn once up front. If the acl_conn was already torn down (race * with a queued CLOSE_EVT behind this CONF_EVT), leave the head in the @@ -2288,28 +2367,51 @@ static void handle_gatts_notify_tx_event(struct bt_le_gatts_notify_tx_event *eve return; } - /* Notify CONF_EVT first: BTA fires CONF_EVT immediately after send for - * notifications; pop the matching marker so we don't mistake it for an - * indication ack and corrupt the indication queue. */ - snode = sys_slist_get(&gatt_conn->gatts_notify_list); + /* BTA fires CONF_EVT for both notify completions and indication acks, + * with no type flag. Match by attr_handle — do NOT assume "any notify + * marker means this CONF is a notify": a notify may be enqueued on the + * ISO path while an earlier indication's peer ACK is already queued, + * which would swallow the indication CONF and stall gatts_list. When + * both heads share the same handle, prefer indication (peer ACK); the + * notify CONF arrives shortly and claims the leftover marker. */ + snode = sys_slist_peek_head(&gatt_conn->gatts_list); + ind_match = false; + n = NULL; if (snode != NULL) { - struct gatts_notify_node *nn = CONTAINER_OF(snode, struct gatts_notify_node, node); + n = CONTAINER_OF(snode, struct gatts_list_node, node); + ind_match = (n->value_handle == event->attr_handle); + } - if (nn->value_handle != event->attr_handle) { - LOG_DBG("[B]NotifyTxHdlSkew[%u][%u]", event->attr_handle, nn->value_handle); + if (!ind_match) { + notify = gatts_notify_take_by_handle(gatt_conn, event->attr_handle); + if (notify != NULL) { + free(notify); + return; } - free(nn); + + if (n == NULL) { + LOG_WRN("[B]NotifyTxNoPending[%u][%u]", + event->conn_handle, event->attr_handle); + return; + } + + /* Indication head exists but handle disagrees, and no notify marker + * matched either — list is out of sync. Drop the head to keep + * advancing (alternative is freezing the queue). */ + LOG_ERR("[B]NotifyTxHdlMismatch[%u][%u]", event->attr_handle, n->value_handle); + + (void)sys_slist_get(&gatt_conn->gatts_list); + + if (n->params_copy.func) { + n->params_copy.func(conn, &n->params_copy, BT_ATT_ERR_UNLIKELY); + } + + gatts_list_node_free(n); return; } - /* No notify pending — this CONF_EVT is an indication ack. */ - snode = sys_slist_get(&gatt_conn->gatts_list); - if (snode == NULL) { - LOG_WRN("[B]NotifyTxNoPending[%u]", event->conn_handle); - return; - } - - n = CONTAINER_OF(snode, struct gatts_list_node, node); + /* Indication ack for the current head. */ + (void)sys_slist_get(&gatt_conn->gatts_list); /* Submit next pending BEFORE firing the completion cb. host_mutex is * recursive, so a cb that re-enters bt_gatt_indicate isn't blocked; @@ -2329,20 +2431,6 @@ static void handle_gatts_notify_tx_event(struct bt_le_gatts_notify_tx_event *eve } } - if (n->value_handle != event->attr_handle) { - LOG_ERR("[B]NotifyTxHdlMismatch[%u][%u]", event->attr_handle, n->value_handle); - /* Mismatch is fatal here: BTA delivers CONF_EVT in FIFO order, so a - * head-vs-event handle disagreement means our list is out of sync. - * Drop the head to keep advancing (alternative is freezing the queue). - * Notify lib with err so its state machine doesn't stall waiting - * for a callback that will never match. */ - if (n->params_copy.func) { - n->params_copy.func(conn, &n->params_copy, BT_ATT_ERR_UNLIKELY); - } - gatts_list_node_free(n); - return; - } - if (n->params_copy.func) { n->params_copy.func(conn, &n->params_copy, event->status); } @@ -2420,7 +2508,7 @@ void bt_le_bluedroid_gatt_handle_event(uint8_t *data, size_t data_len) break; default: - assert(0); + BT_LE_ASSERT(0); break; } @@ -2438,7 +2526,7 @@ void bt_le_bluedroid_gatts_svc_cb_register(struct gatts_svc_cb *cb) void bt_le_bluedroid_gatt_uuid_convert(const struct bt_uuid *uuid_in, void *uuid_out) { - assert(uuid_out && uuid_in); + BT_LE_ASSERT(uuid_out && uuid_in); if (uuid_in->type == BT_UUID_TYPE_16) { ((tBT_UUID *)uuid_out)->len = LEN_UUID_16; @@ -2450,7 +2538,7 @@ void bt_le_bluedroid_gatt_uuid_convert(const struct bt_uuid *uuid_in, void *uuid ((tBT_UUID *)uuid_out)->len = LEN_UUID_128; memcpy(((tBT_UUID *)uuid_out)->uu.uuid128, BT_UUID_128(uuid_in)->val, LEN_UUID_128); } else { - assert(0); + BT_LE_ASSERT(0); } } @@ -2536,8 +2624,8 @@ ssize_t bt_le_bluedroid_gatts_attr_read(struct bt_conn *conn, const struct bt_ga return len; } -/* Append a notify marker so handle_gatts_notify_tx_event can pop it off the - * notify list ahead of any pending indication head. Returns -ENOTCONN when +/* Append a notify marker so handle_gatts_notify_tx_event can match CONF_EVT + * by attr_handle against pending notifications. Returns -ENOTCONN when * the gatt_conn is already torn down (caller should skip the BTA send to * avoid a "Unknown connection ID" log from BTA), -ENOMEM on alloc fail. */ static int gatts_notify_enqueue(struct bt_conn *conn, uint16_t value_handle) @@ -2553,7 +2641,7 @@ static int gatts_notify_enqueue(struct bt_conn *conn, uint16_t value_handle) return -ENOTCONN; } - n = calloc(1, sizeof(*n)); + n = bt_le_ext_calloc(1, sizeof(*n)); if (n == NULL) { LOG_ERR("[B]GattsNotifyNodeAllocFail[%u]", conn->handle); return -ENOMEM; @@ -2592,7 +2680,6 @@ static int gatts_notify(struct bt_conn *conn, data.attr = *attr; data.handle = bt_gatt_attr_get_handle(data.attr); - /* Lookup UUID if it was given */ if (uuid) { if (bt_gatts_find_attr_by_uuid(&data, uuid) == false) { return -ENOENT; @@ -2605,12 +2692,11 @@ static int gatts_notify(struct bt_conn *conn, } } - /* Check if attribute is a characteristic then adjust the handle */ if (bt_uuid_cmp(data.attr->uuid, BT_UUID_GATT_CHRC) == 0) { struct bt_gatt_chrc *chrc; uint16_t required; - assert(data.attr->user_data); + BT_LE_ASSERT(data.attr->user_data); chrc = data.attr->user_data; required = need_cfm ? BT_GATT_CHRC_INDICATE : BT_GATT_CHRC_NOTIFY; @@ -2785,7 +2871,6 @@ int bt_le_bluedroid_gattc_disc_start(uint16_t conn_handle) true, BTA_GATT_TRANSPORT_LE, false, 0, false, 0xff, 0xff, 0, NULL, NULL, NULL); - /* Mark the gattc open as in progress */ gatt_conn->gattc_open = 1; return 0; @@ -2869,11 +2954,32 @@ static int gattc_disc_primary_svc(struct bt_conn *conn, uint16_t conn_id, end: if (db) { - free(db); + osi_free(db); } return 0; } +/* BTA expands every DB UUID to 128-bit. Return true and write the 16-bit + * value only when it matches the Bluetooth Base UUID; otherwise unsupported. */ +static bool bluedroid_db_uuid_as_16(const bt_uuid_t *src, uint16_t *out_val) +{ + static const uint8_t base[16] = { + BT_UUID_128_ENCODE(0x00000000, 0x0000, 0x1000, 0x8000, 0x00805F9B34FB) + }; + + for (int i = 0; i < 16; i++) { + if (i == 12 || i == 13) { + continue; + } + if (src->uu[i] != base[i]) { + return false; + } + } + + *out_val = sys_get_le16(&src->uu[12]); + return true; +} + static int gattc_disc_included_svc(struct bt_conn *conn, uint16_t conn_id, struct bt_gatt_discover_params *params) { @@ -2917,11 +3023,14 @@ static int gattc_disc_included_svc(struct bt_conn *conn, uint16_t conn_id, i, db[i].type, db[i].start_handle, db[i].end_handle, db[i].attribute_handle, db[i].properties); + /* Only 16-bit Bluetooth Base UUIDs are supported; skip 32/128-bit + * rather than silently truncating them to a bogus 16-bit value. */ + if (!bluedroid_db_uuid_as_16(&db[i].uuid, &svc_uuid.val)) { + LOG_WRN("[B]InclSvcNon16BitUuidSkip[%u]", db[i].attribute_handle); + continue; + } + svc_uuid.uuid.type = BT_UUID_TYPE_16; - /* The LSB 12-octets is Bluetooth_Base_UUID, and the remaining - * 2-octets or 4-octets is used by 16-bit or 32-bit UUID. - */ - svc_uuid.val = sys_get_le16(db[i].uuid.uu + 12); inc_svc.uuid = &svc_uuid.uuid; inc_svc.start_handle = db[i].start_handle; @@ -2956,7 +3065,7 @@ static int gattc_disc_included_svc(struct bt_conn *conn, uint16_t conn_id, end: if (db) { - free(db); + osi_free(db); } return 0; } @@ -3051,7 +3160,7 @@ static int gattc_disc_chrc(struct bt_conn *conn, uint16_t conn_id, end: if (db) { - free(db); + osi_free(db); } return 0; } @@ -3068,10 +3177,10 @@ static int gattc_disc_chrc_desc(struct bt_conn *conn, uint16_t conn_id, uint16_t count = 0; int err = 0; - assert(params); - assert(params->uuid); - assert(params->func); - assert(params->sub_params); + BT_LE_ASSERT(params); + BT_LE_ASSERT(params->uuid); + BT_LE_ASSERT(params->func); + BT_LE_ASSERT(params->sub_params); /* Only descriptors can be filtered */ if (bt_uuid_cmp(params->uuid, BT_UUID_GATT_PRIMARY) == 0 || @@ -3110,6 +3219,9 @@ static int gattc_disc_chrc_desc(struct bt_conn *conn, uint16_t conn_id, db[0].type, db[0].start_handle, db[0].end_handle, db[0].attribute_handle, db[0].properties); + /* Zephyr bt_gatt_attr contract: attr->uuid must be non-NULL. For + * filtered descriptor discovery this is the requested descriptor UUID. */ + attr.uuid = params->uuid; attr.handle = db[0].attribute_handle; status = BTA_GATTC_RegisterForNotifications(gattc_if, conn->le.dst.a.val, chrc_handle); @@ -3134,7 +3246,7 @@ static int gattc_disc_chrc_desc(struct bt_conn *conn, uint16_t conn_id, end: if (db) { - free(db); + osi_free(db); } return err; } @@ -3196,6 +3308,17 @@ int bt_le_bluedroid_gattc_read(struct bt_conn *conn, struct bt_gatt_read_params return -ENODEV; } + /* BTA cannot emit ATT Read Blob: bta_gattc_read hardcodes GATT_READ_BY_HANDLE + * and tBTA_GATTC_API_READ has no offset. Rejecting beats a Read Request from + * byte 0, which makes a continuation read stall silently. Do not hoist this + * into bt_gatt_read() - NimBLE has ble_gattc_read_long. + * TODO: drop once Bluedroid exposes a read-with-offset API. */ + if (params->handle_count == 1 && params->single.offset != 0) { + LOG_ERR("[B]GattcRdNoBlob[%u][%u]", + params->single.handle, params->single.offset); + return -ENOTSUP; + } + /* BTA serializes via p_cmd_list (FIFO); we mirror with gattc_list so the * EVT can recover the caller's params. Append before submit so the head * is set when BTA delivers the cmpl synchronously on error paths. */ @@ -3240,6 +3363,13 @@ int bt_le_bluedroid_gattc_write(struct bt_conn *conn, struct bt_gatt_write_param LOG_DBG("[B]GattcWr[%u]", conn->handle); + /* bta_gattc_write memcpy's into a stack tGATT_CL_COMPLETE sized + * GATT_MAX_ATTR_LEN, with no length check of its own */ + if (params->length > BTA_GATT_MAX_ATTR_LEN) { + LOG_ERR("[B]GattcWrValTooBig[%u]", params->length); + return -EMSGSIZE; + } + gatt_conn = bt_le_bluedroid_find_gatt_conn_with_handle(conn->handle); if (gatt_conn == NULL) { LOG_ERR("[B]NoConnInfo[%u]", conn->handle); @@ -3271,6 +3401,12 @@ int bt_le_bluedroid_gattc_write_without_rsp(struct bt_conn *conn, uint16_t handl LOG_DBG("[B]GattcWrCmd[%u][%u][%u]", conn->handle, handle, length); + /* Same BTU stack-overflow hazard as gattc_write */ + if (length > BTA_GATT_MAX_ATTR_LEN) { + LOG_ERR("[B]GattcWrCmdValTooBig[%u]", length); + return -EMSGSIZE; + } + /* Mirror sibling GATT ops: refuse to forward doomed writes after * disconnect cleared the gatt_conn slot (bt_conn->state may briefly * still look CONNECTED during the teardown race). */ @@ -3357,6 +3493,13 @@ int bt_le_bluedroid_gatt_init(void) * └─ btc_transfer_context classic app cb hops to BTC task here */ + /* Runtime init (not static) so the UUID is restored across a deinit/init + * cycle; static init runs only once at boot. */ + gatts_app_uuid = (tBT_UUID){ + .len = LEN_UUID_128, + .uu.uuid128 = { [0 ... 15] = GATTS_APP_UUID_BYTE }, + }; + k_sem_reset(&gatts_sem); BTA_GATTS_AppRegister(&gatts_app_uuid, gatts_app_cb); @@ -3365,6 +3508,11 @@ int bt_le_bluedroid_gatt_init(void) return -1; } + gattc_app_uuid = (tBT_UUID){ + .len = LEN_UUID_128, + .uu.uuid128 = { [0 ... 15] = GATTC_APP_UUID_BYTE }, + }; + k_sem_reset(&gattc_sem); BTA_GATTC_AppRegister(&gattc_app_uuid, gattc_app_cb); @@ -3382,7 +3530,8 @@ int bt_le_bluedroid_gatt_init(void) void bt_le_bluedroid_gatt_deinit(void) { - /* Block late REG_EVT gives before tearing down the sems. */ + /* Must precede the deregisters: it is what makes the event callbacks drop + * everything BTA still delivers for this registration. */ gatt_shutting_down = true; if (gattc_if != 0) { diff --git a/components/bt/esp_ble_iso/host/adapter/bluedroid/hci.c b/components/bt/esp_ble_iso/host/adapter/bluedroid/hci.c index 6dffd4aa135..82ec349c905 100644 --- a/components/bt/esp_ble_iso/host/adapter/bluedroid/hci.c +++ b/components/bt/esp_ble_iso/host/adapter/bluedroid/hci.c @@ -12,8 +12,6 @@ #include "bluedroid/hci.h" -#if USE_DIRECT_HCI - /* hci/hci_layer.h pulls in osi/osi.h which defines a 2-arg CONCAT(a, b) * macro that conflicts with the variadic CONCAT(...) from Zephyr's * . hci.c does not use CONCAT itself, so drop the @@ -29,6 +27,8 @@ LOG_MODULE_REGISTER(ISO_BHCI, CONFIG_BT_ISO_LOG_LEVEL); +#if USE_DIRECT_HCI + /* Adapter-private sync cmd path. Decouples sync HCI cmds from BTU's global * ble_sync_info entirely: each cmd carries its own command_complete_cb (this * file's direct_hci_complete_cb) and the caller waits on direct_hci_sem, @@ -43,15 +43,15 @@ LOG_MODULE_REGISTER(ISO_BHCI, CONFIG_BT_ISO_LOG_LEVEL); * * Concurrent safety: send_sync is serialized by callers via bt_le_host_lock, * so the static rsp_buf pointer / opcode latch are single-slot. */ -static struct k_sem direct_hci_sem; +static BT_ISO_CTRL_BSS_ATTR struct k_sem direct_hci_sem; /* Set by deinit before deleting the sem. Late-arriving cb's must check * this before touching the sem. Residual race exists (cb past the check * but pre-give vs. deinit completing the delete) — accepted in practice * because the BTU/HCI layer offers no way to cancel an in-flight cmd. */ -static volatile bool direct_hci_shutting_down; +static BT_ISO_EXT_RAM_BSS_ATTR volatile bool direct_hci_shutting_down; -static struct { +static BT_ISO_EXT_RAM_BSS_ATTR struct { uint16_t opcode; /* expected — set by caller, verified by cb */ uint8_t status; /* HCI status from Command_Complete */ } direct_hci_rsp; @@ -64,8 +64,8 @@ static struct { * teardown race. Sized for the largest payload any direct-HCI caller reads * back (SET_CIG: 2 + 2*cis_count, ISO_READ_TX_SYNC: 11). */ #define DIRECT_HCI_RSP_MAX MAX(2 + 2 * CONFIG_BT_ISO_MAX_CHAN, 11) -static uint8_t direct_hci_rsp_data[DIRECT_HCI_RSP_MAX]; -static uint8_t direct_hci_rsp_data_len; +static BT_ISO_EXT_RAM_BSS_ATTR uint8_t direct_hci_rsp_data[DIRECT_HCI_RSP_MAX]; +static BT_ISO_EXT_RAM_BSS_ATTR uint8_t direct_hci_rsp_data_len; static void direct_hci_complete_cb(BT_HDR *response, void *context) { @@ -80,6 +80,8 @@ static void direct_hci_complete_cb(BT_HDR *response, void *context) ARG_UNUSED(context); + LOG_INF("[B]DirectHciCompleteCb[%04x]", direct_hci_rsp.opcode); + event_param_len = response->data[response->offset + 1]; STREAM_TO_UINT16(opcode, stream); @@ -109,12 +111,25 @@ static void direct_hci_complete_cb(BT_HDR *response, void *context) /* deinit may have set the shutdown flag and be about to delete the * sem. Skip the give to avoid asserting on a NULL handle. */ if (direct_hci_shutting_down) { + LOG_INF("[B]DirectHciDropCmpl[0x%04x]", opcode); return; } k_sem_give(&direct_hci_sem); } +/* Dropped-wakeup recovery in send_sync below, and the only one that works for + * our own command: it recovers precisely because the sem wait deschedules + * iso_task, which is what lets the poster holding POSTING run and clear it (a + * re-post issued back-to-back cannot — see common/task.h). SLICE stays above + * the ~5 ms round trip so a healthy command never kicks, but far below the old + * 50 ms: iso_task is the sole consumer of the ISO RX queue, and every + * millisecond spent here backs up SDUs. The first kick already recovers, and + * the leftover budget is one final wait, keeping the total K_SEM_SHORT. */ +#define DIRECT_HCI_KICK_SLICE (10 / portTICK_PERIOD_MS) +#define DIRECT_HCI_KICK_MAX 3 +#define DIRECT_HCI_WAIT_REST (K_SEM_SHORT - DIRECT_HCI_KICK_MAX * DIRECT_HCI_KICK_SLICE) + tBTM_STATUS bt_le_bluedroid_hci_send_sync(uint16_t opcode, const uint8_t *cmd_params, uint8_t cmd_params_len, @@ -123,6 +138,7 @@ tBTM_STATUS bt_le_bluedroid_hci_send_sync(uint16_t opcode, { BT_HDR *p; UINT8 *pp; + uint8_t kicks; hci_cmd_metadata_t *metadata; p = HCI_GET_CMD_BUF(cmd_params_len); @@ -158,7 +174,30 @@ tBTM_STATUS bt_le_bluedroid_hci_send_sync(uint16_t opcode, hci_layer_get_interface()->transmit_command(p, direct_hci_complete_cb, NULL, NULL); - if (k_sem_take(&direct_hci_sem, K_SEM_SHORT) != 0) { + /* Close the POSTING window transmit_command() just opened; empty by + * default. */ + bt_le_bluedroid_hci_drain_downstream(); + + for (kicks = 0; kicks < DIRECT_HCI_KICK_MAX; kicks++) { + if (k_sem_take_poll(&direct_hci_sem, DIRECT_HCI_KICK_SLICE) == 0) { + break; + } + + LOG_WRN("[B]DirectHciKick[0x%04x][%u]", opcode, kicks + 1); + + /* Still no response: this command sits in command_queue with nothing + * scheduled to drain it — its post was rejected inside another poster's + * POSTING window, and it never reached commands_pending_response either, + * so COMMAND_PENDING_TIMEOUT never armed. The wait above descheduled us, + * so that poster has since cleared POSTING; this post lands and hciT + * drains the queue. Bluedroid-only: NimBLE writes the command inline + * from the caller's task (ble_hs_hci_cmd_tx), so it has no wakeup to + * lose. */ + hci_downstream_data_post(OSI_THREAD_MAX_TIMEOUT); + } + + if (kicks == DIRECT_HCI_KICK_MAX && + k_sem_take(&direct_hci_sem, DIRECT_HCI_WAIT_REST) != 0) { LOG_ERR("[B]DirectHciTimeout[0x%04x]", opcode); return BTM_ERR_PROCESSING; } @@ -196,3 +235,27 @@ void bt_le_bluedroid_hci_deinit(void) } #endif /* USE_DIRECT_HCI */ + +/* Re-post the downstream event after one of our HCI commands, to flush a packet + * whose own post was rejected inside the window that command opened. + * + * Both the command queue and the ACL data queue are drained by one shared + * osi_event (hci_host_env.downstream_data_ready). osi_thread_post_event() holds + * OSI_EVENT_FLAG_POSTING from before its osi_thread_post() — which yields to the + * higher-priority hciT — until the poster is rescheduled, and + * osi_event_can_post_locked() rejects any post landing in that window. Both + * victims are silent: transmit_downward() and host_send_pkt_available_cb() + * discard the return value, and a command rejected this way never reaches + * commands_pending_response, so COMMAND_PENDING_TIMEOUT never arms. The stranded + * packet then waits for the next successful post from anyone — measured at + * 56.6 s on a peripheral holding a CSIS set lock, where the lock's 60 s timeout + * notification happened to be that next post. + * + * Empty by default: ISO_TASK_PRIO above BTU closes the window instead, and this + * re-post cannot help once it does. See common/task.h. */ +void bt_le_bluedroid_hci_drain_downstream(void) +{ +#if ISO_HCI_DRAIN_DOWNSTREAM + hci_downstream_data_post(OSI_THREAD_MAX_TIMEOUT); +#endif /* ISO_HCI_DRAIN_DOWNSTREAM */ +} diff --git a/components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/hci.h b/components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/hci.h index 92e208f6b21..debc03baf62 100644 --- a/components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/hci.h +++ b/components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/hci.h @@ -26,6 +26,12 @@ int bt_le_bluedroid_hci_init(void); void bt_le_bluedroid_hci_deinit(void); +/* Re-post Bluedroid's downstream-data event; call right after any HCI command + * this adapter sends from the iso task. Empty unless ISO_HCI_DRAIN_DOWNSTREAM + * is enabled — see hci.c for the mechanism and common/task.h for why it is off + * by default. */ +void bt_le_bluedroid_hci_drain_downstream(void); + /* Send a sync HCI command, bypassing BTM and ble_sync_info. `cmd_params` * is the cmd parameter payload (without opcode/length preamble). * diff --git a/components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/l2cap.h b/components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/l2cap.h new file mode 100644 index 00000000000..35b045e0a9b --- /dev/null +++ b/components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/l2cap.h @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct bt_l2cap_chan; +struct net_buf; + +int bt_le_bluedroid_l2cap_chan_connect(uint16_t conn_handle); + +int bt_le_bluedroid_l2cap_chan_disconnect(struct bt_l2cap_chan *chan); + +int bt_le_bluedroid_l2cap_chan_send(struct bt_l2cap_chan *chan, struct net_buf *buf); + +void bt_le_bluedroid_l2cap_accept_result(uint16_t conn_handle, uint8_t l2cap_id, + uint16_t chan_handle, uint16_t result); + +int bt_le_bluedroid_l2cap_init(void); + +void bt_le_bluedroid_l2cap_deinit(void); + +#ifdef __cplusplus +} +#endif diff --git a/components/bt/esp_ble_iso/host/adapter/bluedroid/iso.c b/components/bt/esp_ble_iso/host/adapter/bluedroid/iso.c index 2716859fe33..2caf350613c 100644 --- a/components/bt/esp_ble_iso/host/adapter/bluedroid/iso.c +++ b/components/bt/esp_ble_iso/host/adapter/bluedroid/iso.c @@ -29,6 +29,26 @@ LOG_MODULE_REGISTER(ISO_BISO, CONFIG_BT_ISO_LOG_LEVEL); * path (bt_le_bluedroid_hci_send_sync) nor the fire-and-forget BTM_* * calls need bt_le_host_lock; single-task affinity is the serialization. * + * The bt_le_bluedroid_hci_drain_downstream() call after each fire-and-forget + * BTM_* command below compiles away by default: what actually keeps a + * concurrent BTU ACL post from being rejected inside our POSTING window is + * ISO_TASK_PRIO sitting above BTU. See common/task.h for both. + * + * TODO: + * The opposite direction is uncovered for these commands. If BTU's own POSTING + * window rejects the post one of them makes, it strands in command_queue until + * the next successful post from anyone — unbounded when the link happens to go + * quiet. send_sync survives this via its kick loop, which works only because + * the sem wait deschedules us so BTU can clear POSTING; fire-and-forget has no + * response to wait on, so it has no equivalent. Raising ISO_TASK_PRIO above BTU + * makes us win that race more often, so the odds went up. + * + * Not observed yet. Each call site below names the event that goes missing when + * it happens. The fix is an explicit yield (vTaskDelay(1)) after the BTM call, + * so BTU can clear POSTING, then a re-post via + * bt_le_bluedroid_hci_drain_downstream() — add it only once this shows up, and + * only at the site that showed it. + * * iso_sem is the legacy sync-response mechanism for the USE_DIRECT_HCI=0 * fallback only (set_cig_params / read_tx_sync): iso_evt_rx fills the * file-scope tx_sync / set_cig_params buffers on BTU, then gives the @@ -47,9 +67,9 @@ LOG_MODULE_REGISTER(ISO_BISO, CONFIG_BT_ISO_LOG_LEVEL); * zero would yield bogus packet_seq_num / cis_handle to the lib). * K_SEM_SHORT must exceed the worst-case BTU dispatch chain. */ -static struct k_sem iso_sem; +static BT_ISO_CTRL_BSS_ATTR struct k_sem iso_sem; -static struct { +static BT_ISO_EXT_RAM_BSS_ATTR struct { uint8_t status; uint16_t conn_handle; uint16_t packet_seq_num; @@ -57,7 +77,7 @@ static struct { uint32_t time_offset; } tx_sync; -static struct { +static BT_ISO_EXT_RAM_BSS_ATTR struct { uint8_t status; uint8_t cig_id; uint8_t cis_count; @@ -69,7 +89,7 @@ static int hci_cmd_read_iso_tx_sync(struct net_buf *buf, struct net_buf **rsp) uint16_t conn_handle; tBTM_STATUS status; - assert(rsp); + BT_LE_ASSERT(rsp); conn_handle = sys_get_le16(buf->data + 3); @@ -89,7 +109,7 @@ static int hci_cmd_read_iso_tx_sync(struct net_buf *buf, struct net_buf **rsp) tx_sync.packet_seq_num = sys_get_le16(rsp_buf + 2); tx_sync.tx_time_stamp = sys_get_le32(rsp_buf + 4); tx_sync.time_offset = sys_get_le24(rsp_buf + 8) & 0xFFFFFF; - assert(tx_sync.conn_handle == conn_handle); + BT_LE_ASSERT(tx_sync.conn_handle == conn_handle); } #else /* USE_DIRECT_HCI */ bt_le_host_lock(); @@ -100,7 +120,7 @@ static int hci_cmd_read_iso_tx_sync(struct net_buf *buf, struct net_buf **rsp) LOG_ERR("[B]RdIsoTxSyncRspTimeout[0x%03x]", conn_handle); status = BTM_ERR_PROCESSING; } else { - assert(tx_sync.conn_handle == conn_handle); + BT_LE_ASSERT(tx_sync.conn_handle == conn_handle); status = tx_sync.status; } bt_le_host_unlock(); @@ -134,7 +154,7 @@ static int hci_cmd_set_cig_params(struct net_buf *buf, struct net_buf **rsp) uint8_t cis_count; uint8_t cig_id; - assert(rsp); + BT_LE_ASSERT(rsp); cig_id = buf->data[3]; cis_count = buf->data[17]; @@ -162,8 +182,8 @@ static int hci_cmd_set_cig_params(struct net_buf *buf, struct net_buf **rsp) for (uint8_t i = 0; i < rsp_cis_count; i++) { set_cig_params.cis_handle[i] = sys_get_le16(rsp_buf + 2 + i * 2); } - assert(set_cig_params.cig_id == cig_id); - assert(set_cig_params.cis_count == cis_count); + BT_LE_ASSERT(set_cig_params.cig_id == cig_id); + BT_LE_ASSERT(set_cig_params.cis_count == cis_count); } #else /* USE_DIRECT_HCI */ struct ble_hci_le_cis_params *cis_params; @@ -183,8 +203,8 @@ static int hci_cmd_set_cig_params(struct net_buf *buf, struct net_buf **rsp) mtl_c_to_p = sys_get_le16(buf->data + 13); mtl_p_to_c = sys_get_le16(buf->data + 15); - cis_params = calloc(1, cis_count * sizeof(struct ble_hci_le_cis_params)); - assert(cis_params); + cis_params = bt_le_ext_calloc(1, cis_count * sizeof(struct ble_hci_le_cis_params)); + BT_LE_ASSERT(cis_params); for (size_t i = 0; i < cis_count; i++) { cis_params[i].cis_id = buf->data[18 + i * sizeof(struct ble_hci_le_cis_params)]; @@ -213,8 +233,8 @@ static int hci_cmd_set_cig_params(struct net_buf *buf, struct net_buf **rsp) LOG_ERR("[B]SetCigParamsRspTimeout[%u]", cig_id); status = BTM_ERR_PROCESSING; } else { - assert(set_cig_params.cig_id == cig_id); - assert(set_cig_params.cis_count == cis_count); + BT_LE_ASSERT(set_cig_params.cig_id == cig_id); + BT_LE_ASSERT(set_cig_params.cis_count == cis_count); status = set_cig_params.status; if (status) { LOG_ERR("[B]SetCigParamsCtrlFail[%u][%02x]", cig_id, status); @@ -250,7 +270,7 @@ static int hci_cmd_set_cig_params_test(struct net_buf *buf, struct net_buf **rsp uint8_t cis_count; uint8_t cig_id; - assert(rsp); + BT_LE_ASSERT(rsp); cig_id = buf->data[3]; cis_count = buf->data[17]; @@ -278,8 +298,8 @@ static int hci_cmd_set_cig_params_test(struct net_buf *buf, struct net_buf **rsp for (uint8_t i = 0; i < rsp_cis_count; i++) { set_cig_params.cis_handle[i] = sys_get_le16(rsp_buf + 2 + i * 2); } - assert(set_cig_params.cig_id == cig_id); - assert(set_cig_params.cis_count == cis_count); + BT_LE_ASSERT(set_cig_params.cig_id == cig_id); + BT_LE_ASSERT(set_cig_params.cis_count == cis_count); } #else /* USE_DIRECT_HCI */ struct ble_hci_le_cis_params_test *cis_params; @@ -301,8 +321,8 @@ static int hci_cmd_set_cig_params_test(struct net_buf *buf, struct net_buf **rsp packing = buf->data[15]; framing = buf->data[16]; - cis_params = calloc(1, cis_count * sizeof(struct ble_hci_le_cis_params_test)); - assert(cis_params); + cis_params = bt_le_ext_calloc(1, cis_count * sizeof(struct ble_hci_le_cis_params_test)); + BT_LE_ASSERT(cis_params); for (size_t i = 0; i < cis_count; i++) { cis_params[i].cis_id = buf->data[18 + i * sizeof(struct ble_hci_le_cis_params_test)]; @@ -335,8 +355,8 @@ static int hci_cmd_set_cig_params_test(struct net_buf *buf, struct net_buf **rsp LOG_ERR("[B]SetCigParamsTestRspTimeout[%u]", cig_id); status = BTM_ERR_PROCESSING; } else { - assert(set_cig_params.cig_id == cig_id); - assert(set_cig_params.cis_count == cis_count); + BT_LE_ASSERT(set_cig_params.cig_id == cig_id); + BT_LE_ASSERT(set_cig_params.cis_count == cis_count); status = set_cig_params.status; if (status) { LOG_ERR("[B]SetCigParamsTestCtrlFail[%u][%02x]", cig_id, status); @@ -376,17 +396,19 @@ static int hci_cmd_create_cis(struct net_buf *buf, struct net_buf **rsp) cis_count = buf->data[3]; - cis_params = calloc(1, cis_count * sizeof(struct ble_hci_cis_hdls)); - assert(cis_params); + cis_params = bt_le_ext_calloc(1, cis_count * sizeof(struct ble_hci_cis_hdls)); + BT_LE_ASSERT(cis_params); for (size_t i = 0; i < cis_count; i++) { - cis_params[i].cis_hdl = sys_get_le16(buf->data + 4 + i * sizeof(struct ble_hci_cis_hdls)); + cis_params[i].cis_hdl = sys_get_le16(buf->data + 4 + i * sizeof(struct ble_hci_cis_hdls)); cis_params[i].acl_hdl = sys_get_le16(buf->data + 6 + i * sizeof(struct ble_hci_cis_hdls)); } /* No direct_hci variant: HCI Create CIS returns Command_Status; - * outcome arrives via BTM_BLE_ISO_CIS_ESTABLISHED_EVT. */ + * outcome arrives via BTM_BLE_ISO_CIS_ESTABLISHED_EVT. + * A lost post (file-top TODO) means that event never arrives. */ status = BTM_BleCreateCis(cis_count, (void *)cis_params); + bt_le_bluedroid_hci_drain_downstream(); net_buf_unref(buf); free(cis_params); @@ -423,8 +445,10 @@ static int hci_cmd_accept_cis_req(struct net_buf *buf, struct net_buf **rsp) cis_handle = sys_get_le16(buf->data + 3); /* No direct_hci variant: HCI Accept CIS Request returns Command_Status; - * outcome arrives via BTM_BLE_ISO_CIS_ESTABLISHED_EVT. */ + * outcome arrives via BTM_BLE_ISO_CIS_ESTABLISHED_EVT. + * A lost post (file-top TODO) means that event never arrives. */ status = BTM_BleAcceptCisReq(cis_handle); + bt_le_bluedroid_hci_drain_downstream(); net_buf_unref(buf); @@ -483,7 +507,8 @@ static int hci_cmd_create_big(struct net_buf *buf, struct net_buf **rsp) bst_code = buf->data + 18; /* No direct_hci variant: HCI Create BIG returns Command_Status; - * outcome arrives via BTM_BLE_ISO_BIG_CREATE_COMPLETE_EVT. */ + * outcome arrives via BTM_BLE_ISO_BIG_CREATE_COMPLETE_EVT. + * A lost post (file-top TODO) means that event never arrives. */ status = BTM_BleBigCreate(big_handle, adv_handle, num_bis, @@ -496,6 +521,7 @@ static int hci_cmd_create_big(struct net_buf *buf, struct net_buf **rsp) framing, encryption, bst_code); + bt_le_bluedroid_hci_drain_downstream(); net_buf_unref(buf); @@ -542,7 +568,8 @@ static int hci_cmd_create_big_test(struct net_buf *buf, struct net_buf **rsp) bst_code = buf->data + 23; /* No direct_hci variant: HCI Create BIG Test returns Command_Status; - * outcome arrives via BTM_BLE_ISO_BIG_CREATE_COMPLETE_EVT. */ + * outcome arrives via BTM_BLE_ISO_BIG_CREATE_COMPLETE_EVT. + * A lost post (file-top TODO) means that event never arrives. */ status = BTM_BleBigCreateTest(big_handle, adv_handle, num_bis, @@ -559,6 +586,7 @@ static int hci_cmd_create_big_test(struct net_buf *buf, struct net_buf **rsp) pto, encryption, bst_code); + bt_le_bluedroid_hci_drain_downstream(); net_buf_unref(buf); @@ -577,8 +605,10 @@ static int hci_cmd_terminate_big(struct net_buf *buf, struct net_buf **rsp) reason = buf->data[4]; /* No direct_hci variant: HCI Terminate BIG returns Command_Status; - * outcome arrives via BTM_BLE_ISO_BIG_TERMINATE_COMPLETE_EVT. */ + * outcome arrives via BTM_BLE_ISO_BIG_TERMINATE_COMPLETE_EVT. + * A lost post (file-top TODO) means that event never arrives. */ status = BTM_BleBigTerminate(big_handle, reason); + bt_le_bluedroid_hci_drain_downstream(); net_buf_unref(buf); @@ -609,7 +639,8 @@ static int hci_cmd_big_create_sync(struct net_buf *buf, struct net_buf **rsp) bis = buf->data + 27; /* No direct_hci variant: HCI BIG Create Sync returns Command_Status; - * outcome arrives via BTM_BLE_ISO_BIG_SYNC_ESTABLISHED_EVT. */ + * outcome arrives via BTM_BLE_ISO_BIG_SYNC_ESTABLISHED_EVT. + * A lost post (file-top TODO) means that event never arrives. */ status = BTM_BleBigSyncCreate(big_handle, sync_handle, encryption, @@ -618,6 +649,7 @@ static int hci_cmd_big_create_sync(struct net_buf *buf, struct net_buf **rsp) sync_timeout, num_bis, bis); + bt_le_bluedroid_hci_drain_downstream(); net_buf_unref(buf); @@ -629,7 +661,7 @@ static int hci_cmd_big_terminate_sync(struct net_buf *buf, struct net_buf **rsp) uint8_t big_handle; tBTM_STATUS status; - assert(rsp); + BT_LE_ASSERT(rsp); big_handle = buf->data[3]; @@ -661,7 +693,7 @@ static int hci_cmd_setup_iso_data_path(struct net_buf *buf, struct net_buf **rsp uint16_t conn_handle; tBTM_STATUS status; - assert(rsp); + BT_LE_ASSERT(rsp); conn_handle = sys_get_le16(buf->data + 3); @@ -721,7 +753,7 @@ static int hci_cmd_remove_iso_data_path(struct net_buf *buf, struct net_buf **rs uint16_t conn_handle; tBTM_STATUS status; - assert(rsp); + BT_LE_ASSERT(rsp); conn_handle = sys_get_le16(buf->data + 3); @@ -858,8 +890,8 @@ static void iso_evt_handler(tBTM_BLE_ISO_EVENT event, tBTM_BLE_ISO_CB_PARAMS *pa struct bt_hci_evt_disconn_complete ev = {0}; qdata_len = 2 + sizeof(ev); - qdata = calloc(1, qdata_len); - assert(qdata); + qdata = bt_le_ext_calloc(1, qdata_len); + BT_LE_ASSERT(qdata); ev.status = 0x00; ev.handle = params->btm_cis_disconnectd_evt.cis_handle; @@ -875,8 +907,8 @@ static void iso_evt_handler(tBTM_BLE_ISO_EVENT event, tBTM_BLE_ISO_CB_PARAMS *pa struct bt_hci_evt_le_cis_established ev = {0}; qdata_len = 2 + 1 + sizeof(ev); - qdata = calloc(1, qdata_len); - assert(qdata); + qdata = bt_le_ext_calloc(1, qdata_len); + BT_LE_ASSERT(qdata); ev.status = iso_hci_status(params->btm_cis_established_evt.status); ev.conn_handle = params->btm_cis_established_evt.conn_handle; @@ -906,8 +938,8 @@ static void iso_evt_handler(tBTM_BLE_ISO_EVENT event, tBTM_BLE_ISO_CB_PARAMS *pa struct bt_hci_evt_le_cis_req ev = {0}; qdata_len = 2 + 1 + sizeof(ev); - qdata = calloc(1, qdata_len); - assert(qdata); + qdata = bt_le_ext_calloc(1, qdata_len); + BT_LE_ASSERT(qdata); ev.acl_handle = params->btm_cis_request_evt.acl_handle; ev.cis_handle = params->btm_cis_request_evt.cis_handle; @@ -925,8 +957,8 @@ static void iso_evt_handler(tBTM_BLE_ISO_EVENT event, tBTM_BLE_ISO_CB_PARAMS *pa struct bt_hci_evt_le_big_complete ev = {0}; qdata_len = 2 + 1 + sizeof(ev) + params->btm_big_cmpl.num_bis * 2; - qdata = calloc(1, qdata_len); - assert(qdata); + qdata = bt_le_ext_calloc(1, qdata_len); + BT_LE_ASSERT(qdata); ev.status = iso_hci_status(params->btm_big_cmpl.status); ev.big_handle = params->btm_big_cmpl.big_handle; @@ -956,8 +988,8 @@ static void iso_evt_handler(tBTM_BLE_ISO_EVENT event, tBTM_BLE_ISO_CB_PARAMS *pa struct bt_hci_evt_le_big_terminate ev = {0}; qdata_len = 2 + 1 + sizeof(ev); - qdata = calloc(1, qdata_len); - assert(qdata); + qdata = bt_le_ext_calloc(1, qdata_len); + BT_LE_ASSERT(qdata); ev.big_handle = params->btm_big_term.big_handle; ev.reason = params->btm_big_term.reason; @@ -973,8 +1005,8 @@ static void iso_evt_handler(tBTM_BLE_ISO_EVENT event, tBTM_BLE_ISO_CB_PARAMS *pa struct bt_hci_evt_le_big_sync_established ev = {0}; qdata_len = 2 + 1 + sizeof(ev) + params->btm_big_sync_estab.num_bis * 2; - qdata = calloc(1, qdata_len); - assert(qdata); + qdata = bt_le_ext_calloc(1, qdata_len); + BT_LE_ASSERT(qdata); ev.status = iso_hci_status(params->btm_big_sync_estab.status); ev.big_handle = params->btm_big_sync_estab.big_handle; @@ -1002,8 +1034,8 @@ static void iso_evt_handler(tBTM_BLE_ISO_EVENT event, tBTM_BLE_ISO_CB_PARAMS *pa struct bt_hci_evt_le_big_sync_lost ev = {0}; qdata_len = 2 + 1 + sizeof(ev); - qdata = calloc(1, qdata_len); - assert(qdata); + qdata = bt_le_ext_calloc(1, qdata_len); + BT_LE_ASSERT(qdata); ev.big_handle = params->btm_big_sync_lost.big_handle; ev.reason = params->btm_big_sync_lost.reason; @@ -1019,8 +1051,8 @@ static void iso_evt_handler(tBTM_BLE_ISO_EVENT event, tBTM_BLE_ISO_CB_PARAMS *pa struct bt_hci_evt_le_biginfo_adv_report ev = {0}; qdata_len = 2 + 1 + sizeof(ev); - qdata = calloc(1, qdata_len); - assert(qdata); + qdata = bt_le_ext_calloc(1, qdata_len); + BT_LE_ASSERT(qdata); ev.sync_handle = params->btm_biginfo_report.sync_handle; ev.num_bis = params->btm_biginfo_report.num_bis; @@ -1057,7 +1089,9 @@ static void iso_evt_handler(tBTM_BLE_ISO_EVENT event, tBTM_BLE_ISO_CB_PARAMS *pa err = bt_le_iso_task_post(q_type, qdata, qdata_len); if (err) { - LOG_ERR("[B]IsoPostEvtFail[%d][%02x]", err, event); + if (q_type == ISO_QUEUE_ITEM_TYPE_ISO_HCI_EVENT) { + ISO_POST_FAIL_LOG(err, "[B]IsoPostEvtFail[%d][%02x]", err, event); + } free(qdata); } } @@ -1241,8 +1275,10 @@ int bt_le_bluedroid_iso_disconnect(uint16_t conn_handle, uint8_t reason) LOG_DBG("[B]IsoDisconn[0x%03x][%02x]", conn_handle, reason); /* No direct_hci variant: HCI Disconnect returns Command_Status; - * outcome arrives via BTM_BLE_ISO_CIS_DISCONNECTED_EVT. */ + * outcome arrives via BTM_BLE_ISO_CIS_DISCONNECTED_EVT. + * A lost post (file-top TODO) means that event never arrives. */ status = BTM_BleDisconCis(conn_handle, reason); + bt_le_bluedroid_hci_drain_downstream(); if (status != BTM_SUCCESS) { LOG_ERR("[B]IsoDisconnFail[0x%03x][%02x]", conn_handle, status); @@ -1275,7 +1311,7 @@ int bt_le_bluedroid_iso_init(void) err = iso_enable_cis(); if (err) { /* Roll back local init so a retry doesn't trip k_sem_create's - * assert(handle == NULL). Reverse order of init, skipping the + * BT_LE_ASSERT(handle == NULL). Reverse order of init, skipping the * disable_cis step since enable never took effect. */ #if CONFIG_BT_ISO_RX ble_host_register_rx_iso_data_cb(NULL); @@ -1296,8 +1332,12 @@ int bt_le_bluedroid_iso_init(void) void bt_le_bluedroid_iso_deinit(void) { #if CONFIG_BT_ISO_UNICAST - /* Mirror bt_le_iso_init() which enables bit 32 only on unicast build. */ - iso_disable_cis(); + /* Core 6.0 Vol 4 Part E 7.8.115: LE Set Host Feature is Command Disallowed + * while any connection exists, and the ACL outlives ISO deinit. Skip - a + * stale host-support bit is harmless and the next init sets it again. */ + if (bt_le_acl_conn_count() == 0) { + iso_disable_cis(); + } #endif /* CONFIG_BT_ISO_UNICAST */ #if CONFIG_BT_ISO_RX diff --git a/components/bt/esp_ble_iso/host/adapter/nimble/gap.c b/components/bt/esp_ble_iso/host/adapter/nimble/gap.c index 5ca85a1dc6b..d804df9fc16 100644 --- a/components/bt/esp_ble_iso/host/adapter/nimble/gap.c +++ b/components/bt/esp_ble_iso/host/adapter/nimble/gap.c @@ -82,8 +82,8 @@ void bt_le_nimble_gap_post_event(void *param) enum iso_queue_item_type q_type; int err; - qev = calloc(1, sizeof(*qev)); - assert(qev); + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); memset(&desc, 0, sizeof(desc)); ev = param; @@ -116,8 +116,8 @@ void bt_le_nimble_gap_post_event(void *param) qev->ext_scan_recv.data_len = ev->ext_disc.length_data; if (qev->ext_scan_recv.data_len) { - qev->ext_scan_recv.data = calloc(1, qev->ext_scan_recv.data_len); - assert(qev->ext_scan_recv.data); + qev->ext_scan_recv.data = bt_le_ext_calloc(1, qev->ext_scan_recv.data_len); + BT_LE_ASSERT(qev->ext_scan_recv.data); memcpy(qev->ext_scan_recv.data, ev->ext_disc.data, qev->ext_scan_recv.data_len); } @@ -171,8 +171,8 @@ void bt_le_nimble_gap_post_event(void *param) qev->pa_sync_recv.data_len = ev->periodic_report.data_length; if (qev->pa_sync_recv.data_len) { - qev->pa_sync_recv.data = calloc(1, qev->pa_sync_recv.data_len); - assert(qev->pa_sync_recv.data); + qev->pa_sync_recv.data = bt_le_ext_calloc(1, qev->pa_sync_recv.data_len); + BT_LE_ASSERT(qev->pa_sync_recv.data); memcpy(qev->pa_sync_recv.data, ev->periodic_report.data, qev->pa_sync_recv.data_len); } @@ -184,7 +184,7 @@ void bt_le_nimble_gap_post_event(void *param) qev->acl_connect.status = ev->connect.status; if (qev->acl_connect.status == 0) { err = ble_gap_conn_find(ev->connect.conn_handle, &desc); - assert(err == 0); + BT_LE_ASSERT(err == 0); qev->acl_connect.conn_handle = desc.conn_handle; qev->acl_connect.role = desc.role; @@ -206,7 +206,7 @@ void bt_le_nimble_gap_post_event(void *param) qev->security_change.status = ev->enc_change.status; if (qev->security_change.status == 0) { err = ble_gap_conn_find(ev->enc_change.conn_handle, &desc); - assert(err == 0); + BT_LE_ASSERT(err == 0); qev->security_change.conn_handle = ev->enc_change.conn_handle; qev->security_change.role = desc.role; @@ -262,12 +262,8 @@ void bt_le_nimble_gap_post_event(void *param) err = bt_le_iso_task_post(q_type, qev, sizeof(*qev)); if (err) { - /* Floodable reports drop by design when the queue is full; only a - * failure on the reliable (normal-queue) path is a real error. */ if (q_type == ISO_QUEUE_ITEM_TYPE_GAP_EVENT) { - LOG_ERR("[N]GapPostEvtFail[%d][%u]", err, qev->type); - } else { - LOG_DBG("[N]GapRptDrop[%u]", qev->type); + ISO_POST_FAIL_LOG(err, "[N]GapPostEvtFail[%d][%u]", err, qev->type); } goto free; } @@ -275,24 +271,7 @@ void bt_le_nimble_gap_post_event(void *param) return; free: - switch (qev->type) { - case BT_LE_GAP_APP_PARAM_EXT_SCAN_RECV: - if (qev->ext_scan_recv.data) { - free(qev->ext_scan_recv.data); - qev->ext_scan_recv.data = NULL; - } - break; - case BT_LE_GAP_APP_PARAM_PA_SYNC_RECV: - if (qev->pa_sync_recv.data) { - free(qev->pa_sync_recv.data); - qev->pa_sync_recv.data = NULL; - } - break; - default: - break; - } - - free(qev); + bt_le_gap_event_free(qev); } int bt_le_nimble_scan_start(const struct bt_le_scan_param *param, ble_gap_event_fn *cb) @@ -325,9 +304,15 @@ int bt_le_nimble_scan_stop(void) LOG_DBG("[N]ScanStop"); rc = ble_gap_disc_cancel(); - if (rc) { + if (rc && rc != BLE_HS_EALREADY) { LOG_ERR("[N]ScanStopFail[%d]", rc); } + /* EALREADY (not scanning, e.g. after privacy preemption): treat as success + * so bt_le_scan_stop clears a stale BT_DEV_SCANNING instead of locking out. */ + if (rc == BLE_HS_EALREADY) { + rc = 0; + } + return nimble_err_to_errno(rc); } diff --git a/components/bt/esp_ble_iso/host/adapter/nimble/gatt/gatt.c b/components/bt/esp_ble_iso/host/adapter/nimble/gatt/gatt.c index 4c582efbf64..6dd164e3310 100644 --- a/components/bt/esp_ble_iso/host/adapter/nimble/gatt/gatt.c +++ b/components/bt/esp_ble_iso/host/adapter/nimble/gatt/gatt.c @@ -44,8 +44,8 @@ void bt_le_nimble_gatt_post_event(void *param) return; } - qev = calloc(1, sizeof(*qev)); - assert(qev); + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); switch (ev->type) { case BLE_GAP_EVENT_MTU: @@ -78,8 +78,8 @@ void bt_le_nimble_gatt_post_event(void *param) uint16_t total_len = OS_MBUF_PKTLEN(ev->notify_rx.om); qev->gattc_notify_rx.len = total_len; - qev->gattc_notify_rx.value = calloc(1, total_len); - assert(qev->gattc_notify_rx.value); + qev->gattc_notify_rx.value = bt_le_ext_calloc(1, total_len); + BT_LE_ASSERT(qev->gattc_notify_rx.value); os_mbuf_copydata(ev->notify_rx.om, 0, total_len, qev->gattc_notify_rx.value); } @@ -102,15 +102,8 @@ void bt_le_nimble_gatt_post_event(void *param) err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); if (err) { - LOG_ERR("[N]GattPostEvtFail[%d][%u]", err, qev->type); - - if (qev->type == BT_LE_GATTC_NOTIFY_RX_EVENT && - qev->gattc_notify_rx.value) { - free(qev->gattc_notify_rx.value); - qev->gattc_notify_rx.value = NULL; - } - - free(qev); + ISO_POST_FAIL_LOG(err, "[N]GattPostEvtFail[%d][%u]", err, qev->type); + bt_le_gatt_event_free(qev); } } @@ -128,8 +121,8 @@ int bt_le_nimble_gatt_post_disc_event(uint16_t conn_handle, ble_uuid16_t *uuid, return -EINVAL; } - qev = calloc(1, sizeof(*qev)); - assert(qev); + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); qev->type = BT_LE_GATTC_DISCOVER_EVENT; @@ -140,7 +133,7 @@ int bt_le_nimble_gatt_post_disc_event(uint16_t conn_handle, ble_uuid16_t *uuid, err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); if (err) { - LOG_ERR("[N]GattPostDiscEvtFail[%d][%u]", err, type); + ISO_POST_FAIL_LOG(err, "[N]GattPostDiscEvtFail[%d][%u]", err, type); free(qev); return err; } @@ -155,8 +148,8 @@ int bt_le_nimble_gatt_post_disc_cmpl_event(uint16_t conn_handle, uint8_t status) LOG_DBG("[N]GattcDiscCmplEvtHdlr[%u][%02x]", conn_handle, status); - qev = calloc(1, sizeof(*qev)); - assert(qev); + qev = bt_le_ext_calloc(1, sizeof(*qev)); + BT_LE_ASSERT(qev); qev->type = BT_LE_GATTC_DISC_CMPL_EVENT; @@ -165,7 +158,7 @@ int bt_le_nimble_gatt_post_disc_cmpl_event(uint16_t conn_handle, uint8_t status) err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); if (err) { - LOG_ERR("[N]GattPostDiscCmplEvtFail[%d][%u][%02x]", err, conn_handle, status); + ISO_POST_FAIL_LOG(err, "[N]GattPostDiscCmplEvtFail[%d][%u][%02x]", err, conn_handle, status); free(qev); return err; } @@ -228,7 +221,7 @@ static void handle_gattc_notify_rx_event_safe(struct bt_le_gattc_notify_rx_event * tearing down a core subscription like the ASCS control point * over one bad PDU would drop every later notification. Tolerate * the bad PDU and keep the subscription. */ - params->notify(conn, params, event->value, event->len); + params->notify(conn, params, NOTIFY_VALUE(event), event->len); } } } @@ -299,7 +292,7 @@ void bt_le_nimble_gatt_handle_event(uint8_t *data, size_t data_len) break; default: - assert(0); + BT_LE_ASSERT(0); break; } @@ -317,16 +310,19 @@ ssize_t bt_le_nimble_gatts_attr_read(struct bt_conn *conn, const struct bt_gatt_ void *buf, uint16_t buf_len, uint16_t offset, const void *value, uint16_t value_len) { - uint16_t len; + ssize_t len; if (buf_len == UINT16_MAX) { /* TODO: A better solution for using a variable for the cb pointer */ struct bt_le_nimble_gatt_read_cb *cb = buf; - assert(cb->read_cb); + BT_LE_ASSERT(cb->read_cb); len = cb->read_cb(cb->read_arg, offset, value, value_len); + if (len < 0) { + return len; + } } else { - assert(attr); + BT_LE_ASSERT(attr); LOG_DBG("[N]Hdl[%u]", attr->handle); @@ -339,7 +335,7 @@ ssize_t bt_le_nimble_gatts_attr_read(struct bt_conn *conn, const struct bt_gatt_ memcpy(buf, (uint8_t *)value + offset, len); } - LOG_DBG("[N]GattsAttrRd[%u]", len); + LOG_DBG("[N]GattsAttrRd[%u]", (unsigned)len); return len; } @@ -375,7 +371,7 @@ static int gatts_notify(struct bt_conn *conn, struct bt_gatt_notify_params *para if (bt_uuid_cmp(data.attr->uuid, BT_UUID_GATT_CHRC) == 0) { struct bt_gatt_chrc *chrc; - assert(data.attr->user_data); + BT_LE_ASSERT(data.attr->user_data); chrc = data.attr->user_data; if ((chrc->properties & BT_GATT_CHRC_NOTIFY) == 0) { @@ -486,7 +482,7 @@ int bt_le_nimble_gattc_discover(struct bt_conn *conn, struct bt_gatt_discover_pa return bt_le_nimble_gattc_db_disc_all_chrs(conn, params); case BT_GATT_DISCOVER_DESCRIPTOR: - assert(params->uuid); + BT_LE_ASSERT(params->uuid); /* Only descriptors can be filtered */ if (bt_uuid_cmp(params->uuid, BT_UUID_GATT_PRIMARY) == 0 || diff --git a/components/bt/esp_ble_iso/host/adapter/nimble/gatt/gatt.db.c b/components/bt/esp_ble_iso/host/adapter/nimble/gatt/gatt.db.c index 7e28339e5aa..13f6e114f2d 100644 --- a/components/bt/esp_ble_iso/host/adapter/nimble/gatt/gatt.db.c +++ b/components/bt/esp_ble_iso/host/adapter/nimble/gatt/gatt.db.c @@ -104,7 +104,7 @@ struct gattc_db_dsc { sys_snode_t node; }; -static struct gattc_db { +static BT_ISO_EXT_RAM_BSS_ATTR struct gattc_db { uint16_t conn_handle; enum { DISC_IDLE = 0, @@ -113,12 +113,7 @@ static struct gattc_db { DISC_FAIL, } status; sys_slist_t svc_list; /* List of the GATT server database of the connection */ -} gattc_db[CONFIG_BT_MAX_CONN] = { - [0 ...(CONFIG_BT_MAX_CONN - 1)] = { - .conn_handle = UINT16_MAX, - .status = DISC_IDLE, - }, -}; +} gattc_db[CONFIG_BT_MAX_CONN]; static struct gattc_db *gattc_db_add(uint16_t conn_handle) { @@ -162,7 +157,7 @@ static void gattc_db_del(struct gattc_db *adb) struct gattc_db_svc *asvc; sys_snode_t *node; - assert(adb); + BT_LE_ASSERT(adb); while ((node = sys_slist_get(&adb->svc_list)) != NULL) { asvc = SYS_SLIST_CONTAINER(node, asvc, node); @@ -186,8 +181,8 @@ static void gattc_db_svc_insert(struct gattc_db *adb, const struct ble_gatt_svc { struct gattc_db_svc *asvc; - asvc = calloc(1, sizeof(*asvc)); - assert(asvc); + asvc = bt_le_ext_calloc(1, sizeof(*asvc)); + BT_LE_ASSERT(asvc); memcpy(&asvc->svc, svc, sizeof(asvc->svc)); sys_slist_init(&asvc->inc_svc_list); @@ -202,8 +197,8 @@ static void gattc_db_inc_svc_insert(struct gattc_db_svc *asvc, const struct ble_ { struct gattc_db_inc_svc *ainc_svc; - ainc_svc = calloc(1, sizeof(*ainc_svc)); - assert(ainc_svc); + ainc_svc = bt_le_ext_calloc(1, sizeof(*ainc_svc)); + BT_LE_ASSERT(ainc_svc); memcpy(&ainc_svc->svc, inc_svc, sizeof(ainc_svc->svc)); sys_slist_init(&ainc_svc->chrc_list); @@ -224,8 +219,8 @@ static void gattc_db_chrc_insert(sys_slist_t *chrc_list, const struct ble_gatt_c { struct gattc_db_chrc *achrc; - achrc = calloc(1, sizeof(*achrc)); - assert(achrc); + achrc = bt_le_ext_calloc(1, sizeof(*achrc)); + BT_LE_ASSERT(achrc); memcpy(&achrc->chrc, chrc, sizeof(achrc->chrc)); @@ -235,26 +230,33 @@ static void gattc_db_chrc_insert(sys_slist_t *chrc_list, const struct ble_gatt_c } static void gattc_db_dsc_cccd_store(sys_slist_t *chrc_list, - uint16_t chr_val_handle, const struct ble_gatt_dsc *dsc) { struct gattc_db_chrc *achrc; + struct gattc_db_chrc *owner = NULL; - /* LOG_DBG("[N]GattcDbDscCccdStore[%u]", chr_val_handle); */ - + /* chrc_list is handle-ordered; the CCCD belongs to the last chrc whose + * value handle is below the descriptor. NimBLE's service-range + * disc_all_dscs reports one chr_val_handle for all descriptors, so locate + * the owner by descriptor handle instead. */ SYS_SLIST_FOR_EACH_CONTAINER(chrc_list, achrc, node) { - if (achrc->chrc.val_handle == dsc->handle - 1) { - if (achrc->cccd.handle) { - LOG_WRN("[N]GattcDbCccAlreadyUpd[%u][%u]", chr_val_handle, achrc->cccd.handle); - return; - } - - /* LOG_DBG("[N]GattcDbCccUpd[%u][%u]", achrc->chrc.val_handle, dsc->handle); */ - - memcpy(&achrc->cccd, dsc, sizeof(achrc->cccd)); - return; + if (achrc->chrc.val_handle < dsc->handle) { + owner = achrc; + } else { + break; } } + + if (owner == NULL) { + return; + } + + if (owner->cccd.handle) { + LOG_WRN("[N]GattcDbCccAlreadyUpd[%u][%u]", owner->chrc.val_handle, owner->cccd.handle); + return; + } + + memcpy(&owner->cccd, dsc, sizeof(owner->cccd)); } static struct gattc_db_svc *gattc_db_disc_find(struct gattc_db *adb) @@ -587,12 +589,12 @@ static int gattc_db_disc_all_svcs_cb_safe(uint16_t conn_handle, bt_le_host_lock(); adb = arg; - assert(adb); - assert(adb->conn_handle == conn_handle); + BT_LE_ASSERT(adb); + BT_LE_ASSERT(adb->conn_handle == conn_handle); switch (error->status) { case 0: - assert(svc); + BT_LE_ASSERT(svc); LOG_DBG("[N]GattcDbDiscAllSvcs[0x%04x][%u][%u]", svc->uuid.u16.value, @@ -633,7 +635,7 @@ static int gattc_db_find_inc_svcs_cb_safe(uint16_t conn_handle, int rc = 0; asvc = arg; - assert(asvc); + BT_LE_ASSERT(asvc); /* LOG_DBG("[N]GattcDbFindIncSvcsCb[%u][%04x]", conn_handle, error->status); */ @@ -648,7 +650,7 @@ static int gattc_db_find_inc_svcs_cb_safe(uint16_t conn_handle, switch (error->status) { case 0: - assert(svc); + BT_LE_ASSERT(svc); LOG_DBG("[N]GattcDbFindIncSvcs[0x%04x][%u][%u]", svc->uuid.u16.value, @@ -721,7 +723,7 @@ static int gattc_db_disc_all_inc_chrs_cb_safe(uint16_t conn_handle, int rc = 0; ainc_svc = arg; - assert(ainc_svc); + BT_LE_ASSERT(ainc_svc); /* LOG_DBG("[N]GattcDbDiscAllIncChrsCb[%u][%04x]", conn_handle, error->status); */ @@ -736,7 +738,7 @@ static int gattc_db_disc_all_inc_chrs_cb_safe(uint16_t conn_handle, switch (error->status) { case 0: - assert(chrc); + BT_LE_ASSERT(chrc); LOG_DBG("[N]GattcDbDiscAllIncChrs[%u][%u]", chrc->def_handle, chrc->val_handle); @@ -779,7 +781,7 @@ static int gattc_db_disc_all_chrs_cb_safe(uint16_t conn_handle, int rc = 0; asvc = arg; - assert(asvc); + BT_LE_ASSERT(asvc); /* LOG_DBG("[N]GattcDbDiscAllChrsCb[%u][%04x]", conn_handle, error->status); */ @@ -794,7 +796,7 @@ static int gattc_db_disc_all_chrs_cb_safe(uint16_t conn_handle, switch (error->status) { case 0: - assert(chrc); + BT_LE_ASSERT(chrc); LOG_DBG("[N]GattcDbDiscAllChrs[%u][%u]", chrc->def_handle, chrc->val_handle); @@ -838,7 +840,7 @@ static int gattc_db_disc_all_inc_dscs_cb_safe(uint16_t conn_handle, int rc = 0; ainc_svc = arg; - assert(ainc_svc); + BT_LE_ASSERT(ainc_svc); LOG_DBG("[N]GattcDbDiscAllIncDscsCb[%u][%04x][%u]", conn_handle, error->status, chr_val_handle); @@ -854,13 +856,13 @@ static int gattc_db_disc_all_inc_dscs_cb_safe(uint16_t conn_handle, switch (error->status) { case 0: - assert(dsc); + BT_LE_ASSERT(dsc); if (dsc->uuid.u.type == BLE_UUID_TYPE_16 && dsc->uuid.u16.value == BT_UUID_GATT_CCC_VAL) { LOG_DBG("[N]GattcDbDiscAllIncDscs[%u][%u]", chr_val_handle, dsc->handle); - gattc_db_dsc_cccd_store(&ainc_svc->chrc_list, chr_val_handle, dsc); + gattc_db_dsc_cccd_store(&ainc_svc->chrc_list, dsc); } break; @@ -901,7 +903,7 @@ static int gattc_db_disc_all_dscs_cb_safe(uint16_t conn_handle, int rc = 0; asvc = arg; - assert(asvc); + BT_LE_ASSERT(asvc); /* LOG_DBG("[N]GattcDbDiscAllDscsCb[%u][%04x][%u]", */ /* conn_handle, error->status, chr_val_handle); */ @@ -917,13 +919,13 @@ static int gattc_db_disc_all_dscs_cb_safe(uint16_t conn_handle, switch (error->status) { case 0: - assert(dsc); + BT_LE_ASSERT(dsc); if (dsc->uuid.u.type == BLE_UUID_TYPE_16 && dsc->uuid.u16.value == BT_UUID_GATT_CCC_VAL) { LOG_DBG("[N]GattcDbDiscAllDscs[%u]", dsc->handle); - gattc_db_dsc_cccd_store(&asvc->chrc_list, chr_val_handle, dsc); + gattc_db_dsc_cccd_store(&asvc->chrc_list, dsc); } break; @@ -963,7 +965,7 @@ static int gattc_db_enable_notify_cb_safe(uint16_t conn_handle, int rc = 0; achrc = arg; - assert(achrc); + BT_LE_ASSERT(achrc); /* LOG_DBG("[N]GattcDbEnableNtfCb[%u][%04x]", conn_handle, error->status); */ @@ -978,7 +980,7 @@ static int gattc_db_enable_notify_cb_safe(uint16_t conn_handle, switch (error->status) { case 0: - assert(attr); + BT_LE_ASSERT(attr); LOG_DBG("[N]GattcDbEnableNotify[%u]", attr->handle); break; @@ -1140,13 +1142,18 @@ static uint8_t gattc_disc_chr_each(struct bt_conn *conn, chrc_uuid.val = achrc->chrc.uuid.u16.value; break; default: - return BT_GATT_ITER_STOP; + /* Skip unsupported UUID types (e.g. 128-bit custom) instead of aborting + * the whole discovery; LE Audio only uses 16-bit characteristics. */ + LOG_WRN("[N]GattcDbChrcSkipUuid[%u][%u][%u]", + conn->handle, achrc->chrc.uuid.u.type, achrc->chrc.val_handle); + return BT_GATT_ITER_CONTINUE; } chrc.uuid = &chrc_uuid.uuid; chrc.value_handle = achrc->chrc.val_handle; chrc.properties = achrc->chrc.properties; + attr.uuid = BT_UUID_GATT_CHRC; /* declaration attr UUID */ attr.user_data = &chrc; attr.handle = achrc->chrc.def_handle; @@ -1252,7 +1259,7 @@ void handle_gattc_db_disc_event_safe(struct bt_le_gattc_discover_event *event) goto end; } - assert(event->params); + BT_LE_ASSERT(event->params); switch (event->type) { case GATTC_DISC_TYPE_SVC_BY_UUID: @@ -1278,7 +1285,7 @@ void handle_gattc_db_disc_event_safe(struct bt_le_gattc_discover_event *event) break; default: - assert(0); + BT_LE_ASSERT(0); break; } @@ -1349,7 +1356,7 @@ static int handle_gattc_disc_all_dscs(struct bt_conn *conn, */ sub_params = params->sub_params; - assert(sub_params); + BT_LE_ASSERT(sub_params); LOG_DBG("[N]GattcDbHdlDiscAllDscs[%u][%u]", conn->handle, sub_params->value_handle); @@ -1387,6 +1394,10 @@ static int handle_gattc_disc_all_dscs(struct bt_conn *conn, SYS_SLIST_FOR_EACH_CONTAINER(&asvc->chrc_list, achrc, node) { if (achrc->chrc.val_handle == sub_params->value_handle) { + LOG_DBG("[N]GattcDbCccLookup[%u][%u][%u][%u]", + asvc->svc.start_handle, asvc->svc.end_handle, + achrc->chrc.val_handle, achrc->cccd.handle); + if (achrc->cccd.handle) { attr.handle = achrc->cccd.handle; found = &attr; @@ -1399,6 +1410,12 @@ static int handle_gattc_disc_all_dscs(struct bt_conn *conn, } end: + if (found == NULL && rc == 0) { + /* Char missing, or char found with cccd.handle==0 — not success. + * Returning 0 here made bt_gatt_subscribe() append a dead node. */ + rc = -ENODEV; + } + params->func(conn, found, params); return rc; @@ -1475,3 +1492,25 @@ void bt_le_nimble_gattc_db_remove(uint16_t conn_handle) gattc_db_del(adb); } + +/* Runtime init (not static): the array is .bss (PSRAM-eligible), so a + * deinit/init cycle must restart with every slot free. conn_handle 0 is a + * valid handle, so the free marker is UINT16_MAX. */ +void bt_le_nimble_gattc_db_init(void) +{ + for (size_t i = 0; i < ARRAY_SIZE(gattc_db); i++) { + gattc_db[i].conn_handle = UINT16_MAX; + gattc_db[i].status = DISC_IDLE; + sys_slist_init(&gattc_db[i].svc_list); + } +} + +/* Free each slot's discovery nodes on deinit so an init/deinit/re-init cycle stays + * leak-free (mirrors the Bluedroid bt_le_bluedroid_gatt_deinit). gattc_db_del is a + * safe no-op on an already-free slot. */ +void bt_le_nimble_gattc_db_deinit(void) +{ + for (size_t i = 0; i < ARRAY_SIZE(gattc_db); i++) { + gattc_db_del(&gattc_db[i]); + } +} diff --git a/components/bt/esp_ble_iso/host/adapter/nimble/gatt/gatt.nrp.c b/components/bt/esp_ble_iso/host/adapter/nimble/gatt/gatt.nrp.c index c40b1142ecd..cd78c2aa300 100644 --- a/components/bt/esp_ble_iso/host/adapter/nimble/gatt/gatt.nrp.c +++ b/components/bt/esp_ble_iso/host/adapter/nimble/gatt/gatt.nrp.c @@ -79,15 +79,11 @@ struct gatt_nrp_node { sys_snode_t node; }; -static struct gatt_nrp { +static BT_ISO_EXT_RAM_BSS_ATTR struct gatt_nrp { uint16_t conn_handle; sys_slist_t list; /* List of GATT need response pdu (e.g., read req, write req, indication, etc.) */ -} gatt_nrp[CONFIG_BT_MAX_CONN] = { - [0 ...(CONFIG_BT_MAX_CONN - 1)] = { - .conn_handle = UINT16_MAX, - }, -}; +} gatt_nrp[CONFIG_BT_MAX_CONN]; static struct gatt_nrp *gatt_nrp_find(uint16_t conn_handle); @@ -105,11 +101,11 @@ static int gattc_nrp_read_by_uuid_cb_safe(uint16_t conn_handle, int rc = 0; read_params = arg; - assert(read_params); - assert(read_params->func); - assert(read_params->handle_count == 0); - assert(read_params->by_uuid.uuid); - assert(read_params->by_uuid.uuid->type == BT_UUID_TYPE_16); + BT_LE_ASSERT(read_params); + BT_LE_ASSERT(read_params->func); + BT_LE_ASSERT(read_params->handle_count == 0); + BT_LE_ASSERT(read_params->by_uuid.uuid); + BT_LE_ASSERT(read_params->by_uuid.uuid->type == BT_UUID_TYPE_16); LOG_DBG("[N]GattcNrpRdByUuidCb[%u][%04x][%s]", conn_handle, error->status, bt_uuid_str(read_params->by_uuid.uuid)); @@ -127,19 +123,19 @@ static int gattc_nrp_read_by_uuid_cb_safe(uint16_t conn_handle, * events followed by EDONE, so the head must survive across events. * It is removed only on terminal status (EDONE / error) below. */ nrp = gatt_nrp_find(conn->handle); - assert(nrp); + BT_LE_ASSERT(nrp); node = sys_slist_peek_head(&nrp->list); - assert(node); + BT_LE_ASSERT(node); nrp_node = CONTAINER_OF(node, struct gatt_nrp_node, node); - assert(nrp_node->type == GATTC_NRP_READ_BY_UUID); - assert(nrp_node->read_by_uuid.params == read_params); + BT_LE_ASSERT(nrp_node->type == GATTC_NRP_READ_BY_UUID); + BT_LE_ASSERT(nrp_node->read_by_uuid.params == read_params); iter_stopped = nrp_node->read_by_uuid.iter_stopped; switch (error->status) { case 0: - assert(attr); - assert(attr->om); + BT_LE_ASSERT(attr); + BT_LE_ASSERT(attr->om); LOG_DBG("[N]GattcNrpHdl[%u][%u][%u]Len[%u]", attr->handle, read_params->by_uuid.start_handle, @@ -202,10 +198,10 @@ static int gattc_nrp_read_long_cb_safe(uint16_t conn_handle, int rc = 0; read_params = arg; - assert(read_params); - assert(read_params->func); - assert(read_params->handle_count == 1); - assert(read_params->single.offset != 0); + BT_LE_ASSERT(read_params); + BT_LE_ASSERT(read_params->func); + BT_LE_ASSERT(read_params->handle_count == 1); + BT_LE_ASSERT(read_params->single.offset != 0); LOG_DBG("[N]GattcNrpRdLongCb[%u][%04x]", conn_handle, error->status); @@ -221,19 +217,19 @@ static int gattc_nrp_read_long_cb_safe(uint16_t conn_handle, /* Peek (don't pop) — per-fragment events may precede EDONE; head is * removed only on terminal status below. */ nrp = gatt_nrp_find(conn->handle); - assert(nrp); + BT_LE_ASSERT(nrp); node = sys_slist_peek_head(&nrp->list); - assert(node); + BT_LE_ASSERT(node); nrp_node = CONTAINER_OF(node, struct gatt_nrp_node, node); - assert(nrp_node->type == GATTC_NRP_READ_LONG); - assert(nrp_node->read_long.params == read_params); + BT_LE_ASSERT(nrp_node->type == GATTC_NRP_READ_LONG); + BT_LE_ASSERT(nrp_node->read_long.params == read_params); iter_stopped = nrp_node->read_long.iter_stopped; switch (error->status) { case 0: - assert(attr); - assert(attr->om); + BT_LE_ASSERT(attr); + BT_LE_ASSERT(attr->om); LOG_DBG("[N]GattcNrpHdl[%u][%u]Offset[%u]Len[%u]", attr->handle, read_params->single.handle, attr->offset, attr->om->om_len); @@ -293,9 +289,9 @@ static int gattc_nrp_read_single_cb_safe(uint16_t conn_handle, original = nrp_node->read_single.params; func = params_copy->func; - assert(func); - assert(params_copy->handle_count == 1); - assert(params_copy->single.offset == 0); + BT_LE_ASSERT(func); + BT_LE_ASSERT(params_copy->handle_count == 1); + BT_LE_ASSERT(params_copy->single.offset == 0); LOG_DBG("[N]GattcNrpRdSingleCb[%u][%04x]", conn_handle, error->status); @@ -313,8 +309,8 @@ static int gattc_nrp_read_single_cb_safe(uint16_t conn_handle, switch (error->status) { case 0: - assert(attr); - assert(attr->om); + BT_LE_ASSERT(attr); + BT_LE_ASSERT(attr->om); LOG_DBG("[N]GattcNrpHdl[%u][%u]Len[%u]", attr->handle, original->single.handle, attr->om->om_len); @@ -409,8 +405,8 @@ static int gattc_nrp_write_cb_safe(uint16_t conn_handle, int rc = 0; write_params = arg; - assert(write_params); - assert(write_params->func); + BT_LE_ASSERT(write_params); + BT_LE_ASSERT(write_params->func); LOG_DBG("[N]GattcNrpWrCb[%u][%04x]", conn_handle, error->status); @@ -428,7 +424,7 @@ static int gattc_nrp_write_cb_safe(uint16_t conn_handle, switch (error->status) { case 0: - assert(attr); + BT_LE_ASSERT(attr); LOG_DBG("[N]GattcNrpHdl[%u][%u]", attr->handle, write_params->handle); write_params->func(conn, 0, write_params); @@ -487,7 +483,7 @@ static int gattc_nrp_subscribe_cb_safe(uint16_t conn_handle, int rc = 0; sub_params = arg; - assert(sub_params); + BT_LE_ASSERT(sub_params); LOG_DBG("[N]GattcNrpSubCb[%u][%04x]", conn_handle, error->status); @@ -505,7 +501,7 @@ static int gattc_nrp_subscribe_cb_safe(uint16_t conn_handle, switch (error->status) { case 0: - assert(attr); + BT_LE_ASSERT(attr); LOG_DBG("[N]GattcNrpHdl[%u]", attr->handle); #if 0 @@ -565,7 +561,7 @@ static int gattc_nrp_subscribe(struct bt_conn *conn, struct bt_gatt_subscribe_pa void bt_le_nimble_gatts_nrp_indicate_cb(uint16_t conn_handle, int16_t attr_handle, - uint8_t status) + int status) { struct bt_gatt_indicate_params params = {0}; struct bt_gatt_attr attr = {0}; @@ -608,13 +604,13 @@ static int gatts_nrp_indicate(struct bt_conn *conn, struct bt_gatt_indicate_para struct os_mbuf *om = NULL; int rc; - assert(conn && params); - assert(params->attr); + BT_LE_ASSERT(conn && params); + BT_LE_ASSERT(params->attr); /* 0-length indications are valid (spec); ble_hs_mbuf_from_flat accepts * (NULL, 0). But (NULL, len>0) would manifest as a misleading -ENOMEM, * so catch the programmer error here as a defense-in-depth guard. */ - assert(params->len == 0 || params->data != NULL); + BT_LE_ASSERT(params->len == 0 || params->data != NULL); data.attr = params->attr; data.handle = bt_gatt_attr_get_handle(data.attr); @@ -639,7 +635,7 @@ static int gatts_nrp_indicate(struct bt_conn *conn, struct bt_gatt_indicate_para if (bt_uuid_cmp(data.attr->uuid, BT_UUID_GATT_CHRC) == 0) { struct bt_gatt_chrc *chrc; - assert(data.attr->user_data); + BT_LE_ASSERT(data.attr->user_data); chrc = data.attr->user_data; if ((chrc->properties & BT_GATT_CHRC_INDICATE) == 0) { @@ -698,7 +694,7 @@ static void gatt_nrp_del(struct gatt_nrp *nrp) struct gatt_nrp_node *nrp_head; sys_snode_t *node; - assert(nrp); + BT_LE_ASSERT(nrp); while (1) { node = sys_slist_get(&nrp->list); @@ -738,8 +734,8 @@ static int gatt_nrp_insert(struct bt_conn *conn, uint8_t type, void *params) return -ENOMEM; } - nrp_node = calloc(1, sizeof(*nrp_node)); - assert(nrp_node); + nrp_node = bt_le_ext_calloc(1, sizeof(*nrp_node)); + BT_LE_ASSERT(nrp_node); nrp_node->type = type; @@ -759,8 +755,8 @@ static int gatt_nrp_insert(struct bt_conn *conn, uint8_t type, void *params) nrp_node->write_req.params = wp; nrp_node->write_req.data_copy = NULL; if (wp->length > 0) { - assert(wp->data); - nrp_node->write_req.data_copy = malloc(wp->length); + BT_LE_ASSERT(wp->data); + nrp_node->write_req.data_copy = bt_le_ext_malloc(wp->length); if (nrp_node->write_req.data_copy == NULL) { LOG_ERR("[N]GattNrpWrDataAllocFail[%u]", wp->length); free(nrp_node); @@ -779,8 +775,8 @@ static int gatt_nrp_insert(struct bt_conn *conn, uint8_t type, void *params) nrp_node->indicate.params_copy = *ip; nrp_node->indicate.data_copy = NULL; if (ip->len > 0) { - assert(ip->data); - nrp_node->indicate.data_copy = malloc(ip->len); + BT_LE_ASSERT(ip->data); + nrp_node->indicate.data_copy = bt_le_ext_malloc(ip->len); if (nrp_node->indicate.data_copy == NULL) { LOG_ERR("[N]GattNrpIndDataAllocFail[%u]", ip->len); free(nrp_node); @@ -793,7 +789,7 @@ static int gatt_nrp_insert(struct bt_conn *conn, uint8_t type, void *params) break; } default: - assert(0); + BT_LE_ASSERT(0); } if (sys_slist_is_empty(&nrp->list)) { @@ -893,31 +889,31 @@ int bt_le_nimble_gatt_nrp_remove(struct bt_conn *conn, uint8_t type, void *param /* Fetch and remove the first node from list */ node = sys_slist_get(&nrp->list); - assert(node); + BT_LE_ASSERT(node); nrp_head = CONTAINER_OF(node, struct gatt_nrp_node, node); - assert(nrp_head->type == type); + BT_LE_ASSERT(nrp_head->type == type); switch (type) { case GATTC_NRP_READ_BY_UUID: - assert(nrp_head->read_by_uuid.params == params); + BT_LE_ASSERT(nrp_head->read_by_uuid.params == params); break; case GATTC_NRP_READ_LONG: - assert(nrp_head->read_long.params == params); + BT_LE_ASSERT(nrp_head->read_long.params == params); break; case GATTC_NRP_READ_SINGLE: - assert(nrp_head->read_single.params == params); + BT_LE_ASSERT(nrp_head->read_single.params == params); break; case GATTC_NRP_WRITE_REQ: - assert(nrp_head->write_req.params == params); + BT_LE_ASSERT(nrp_head->write_req.params == params); free(nrp_head->write_req.data_copy); nrp_head->write_req.data_copy = NULL; break; case GATTC_NRP_SUBSCRIBE: - assert(nrp_head->subscribe.params == params); + BT_LE_ASSERT(nrp_head->subscribe.params == params); break; case GATTS_NRP_INDICATE: - assert(nrp_head->indicate.params); + BT_LE_ASSERT(nrp_head->indicate.params); /* params is the deep copy taken at insert time; cb sees the copy's * address, not the caller's original (which may have been reused). @@ -941,7 +937,7 @@ int bt_le_nimble_gatt_nrp_remove(struct bt_conn *conn, uint8_t type, void *param nrp_head->indicate.data_copy = NULL; break; default: - assert(0); + BT_LE_ASSERT(0); } LOG_DBG("[N]GattNrpFree[%p]", nrp_head); @@ -962,7 +958,7 @@ int bt_le_nimble_gatt_nrp_remove(struct bt_conn *conn, uint8_t type, void *param if (rc) { LOG_ERR("[N]GattcNrpRdByUuidFail[%d]", rc); - assert(nrp_head->read_by_uuid.params->func); + BT_LE_ASSERT(nrp_head->read_by_uuid.params->func); nrp_head->read_by_uuid.params->func(conn, rc, nrp_head->read_by_uuid.params, NULL, 0); } } else if (nrp_head->type == GATTC_NRP_READ_LONG) { @@ -970,7 +966,7 @@ int bt_le_nimble_gatt_nrp_remove(struct bt_conn *conn, uint8_t type, void *param if (rc) { LOG_ERR("[N]GattcNrpRdLongFail[%d]", rc); - assert(nrp_head->read_long.params->func); + BT_LE_ASSERT(nrp_head->read_long.params->func); nrp_head->read_long.params->func(conn, rc, nrp_head->read_long.params, NULL, 0); } } else if (nrp_head->type == GATTC_NRP_READ_SINGLE) { @@ -978,7 +974,7 @@ int bt_le_nimble_gatt_nrp_remove(struct bt_conn *conn, uint8_t type, void *param if (rc) { LOG_ERR("[N]GattcNrpRdSingleFail[%d]", rc); - assert(nrp_head->read_single.params_copy.func); + BT_LE_ASSERT(nrp_head->read_single.params_copy.func); nrp_head->read_single.params_copy.func(conn, rc, nrp_head->read_single.params, NULL, 0); } @@ -989,7 +985,7 @@ int bt_le_nimble_gatt_nrp_remove(struct bt_conn *conn, uint8_t type, void *param if (rc) { LOG_ERR("[N]GattcNrpWrFail[%d]", rc); - assert(nrp_head->write_req.params->func); + BT_LE_ASSERT(nrp_head->write_req.params->func); nrp_head->write_req.params->func(conn, rc, nrp_head->write_req.params); } } else if (nrp_head->type == GATTC_NRP_SUBSCRIBE) { @@ -1002,7 +998,7 @@ int bt_le_nimble_gatt_nrp_remove(struct bt_conn *conn, uint8_t type, void *param if (rc) { LOG_ERR("[N]GattsNrpIndFail[%d]", rc); - assert(nrp_head->indicate.params->func); + BT_LE_ASSERT(nrp_head->indicate.params->func); nrp_head->indicate.params->func(conn, nrp_head->indicate.params, rc); } } @@ -1043,3 +1039,22 @@ void bt_le_nimble_gatt_nrp_clear(uint16_t conn_handle) gatt_nrp_del(nrp); } + +/* Runtime init (not static): .bss array (PSRAM-eligible) that a deinit/init + * cycle must restart all-free (UINT16_MAX, since conn_handle 0 is valid). */ +void bt_le_nimble_gatt_nrp_init(void) +{ + for (size_t i = 0; i < ARRAY_SIZE(gatt_nrp); i++) { + gatt_nrp[i].conn_handle = UINT16_MAX; + sys_slist_init(&gatt_nrp[i].list); + } +} + +/* Free each slot's queued NRP nodes on deinit (mirrors bt_le_nimble_gattc_db_deinit); + * gatt_nrp_del is a safe no-op on an already-free slot. */ +void bt_le_nimble_gatt_nrp_deinit(void) +{ + for (size_t i = 0; i < ARRAY_SIZE(gatt_nrp); i++) { + gatt_nrp_del(&gatt_nrp[i]); + } +} diff --git a/components/bt/esp_ble_iso/host/adapter/nimble/include/nimble/gap.h b/components/bt/esp_ble_iso/host/adapter/nimble/include/nimble/gap.h index 7d897e25cd8..42ebe601607 100644 --- a/components/bt/esp_ble_iso/host/adapter/nimble/include/nimble/gap.h +++ b/components/bt/esp_ble_iso/host/adapter/nimble/include/nimble/gap.h @@ -9,6 +9,8 @@ #include +#include + #include "host/ble_gap.h" #ifdef __cplusplus diff --git a/components/bt/esp_ble_iso/host/adapter/nimble/include/nimble/gatt.h b/components/bt/esp_ble_iso/host/adapter/nimble/include/nimble/gatt.h index 01f246e04a0..5542ae3f6c2 100644 --- a/components/bt/esp_ble_iso/host/adapter/nimble/include/nimble/gatt.h +++ b/components/bt/esp_ble_iso/host/adapter/nimble/include/nimble/gatt.h @@ -8,13 +8,14 @@ #define HOST_NIMBLE_GATT_H_ #include -#include #include #include "host/ble_uuid.h" #include "host/ble_gatt.h" +#include "utils/assert.h" + #ifdef __cplusplus extern "C" { #endif @@ -35,7 +36,7 @@ static inline uint8_t BT_LE_NIMBLE_GATT_UUID_TO_Z(uint8_t type) } else if (type == BLE_UUID_TYPE_128) { return BT_UUID_TYPE_128; } else { - assert(0); + BT_LE_ASSERT(0); return BT_UUID_TYPE_16; } } @@ -96,6 +97,10 @@ int bt_le_nimble_gattc_db_auto_disc(uint16_t conn_handle); void bt_le_nimble_gattc_db_remove(uint16_t conn_handle); +void bt_le_nimble_gattc_db_init(void); + +void bt_le_nimble_gattc_db_deinit(void); + enum { GATTC_NRP_READ_BY_UUID, GATTC_NRP_READ_LONG, @@ -109,16 +114,18 @@ enum { void bt_le_nimble_gatts_nrp_indicate_cb(uint16_t conn_handle, int16_t attr_handle, - uint8_t status); + int status); int bt_le_nimble_gatt_nrp_insert(struct bt_conn *conn, uint8_t type, void *params); -/* err is forwarded only to the INDICATE func cb; other NRP types invoke their - * own func with err in cb_safe and pass 0 here. */ int bt_le_nimble_gatt_nrp_remove(struct bt_conn *conn, uint8_t type, void *params, uint8_t err); void bt_le_nimble_gatt_nrp_clear(uint16_t conn_handle); +void bt_le_nimble_gatt_nrp_init(void); + +void bt_le_nimble_gatt_nrp_deinit(void); + #ifdef __cplusplus } #endif diff --git a/components/bt/esp_ble_iso/host/adapter/nimble/iso.c b/components/bt/esp_ble_iso/host/adapter/nimble/iso.c index e83958c32f4..9d28ba32d75 100644 --- a/components/bt/esp_ble_iso/host/adapter/nimble/iso.c +++ b/components/bt/esp_ble_iso/host/adapter/nimble/iso.c @@ -37,9 +37,9 @@ static int hci_cmd_read_iso_tx_sync(struct net_buf *buf, struct net_buf **rsp) uint16_t conn_handle; int status; - assert(rsp); + BT_LE_ASSERT(rsp); - assert(buf->len >= 5); + BT_LE_ASSERT(buf->len >= 5); conn_handle = sys_get_le16(buf->data + 3); @@ -87,9 +87,9 @@ static int hci_cmd_set_cig_params(struct net_buf *buf, struct net_buf **rsp) uint8_t *rp; int status; - assert(rsp); + BT_LE_ASSERT(rsp); - assert(buf->len >= 18); + BT_LE_ASSERT(buf->len >= 18); cig_id = buf->data[3]; sdu_interval_c_to_p = sys_get_le24(buf->data + 4); @@ -101,17 +101,17 @@ static int hci_cmd_set_cig_params(struct net_buf *buf, struct net_buf **rsp) mtl_p_to_c = sys_get_le16(buf->data + 15); cis_count = buf->data[17]; - assert(buf->len >= 18 + cis_count * sizeof(struct ble_hci_le_cis_params)); + BT_LE_ASSERT(buf->len >= 18 + cis_count * sizeof(struct ble_hci_le_cis_params)); /* The cis_count can be 0 */ if (cis_count) { - cis_params = calloc(1, cis_count * sizeof(struct ble_hci_le_cis_params)); - assert(cis_params); + cis_params = bt_le_ext_calloc(1, cis_count * sizeof(struct ble_hci_le_cis_params)); + BT_LE_ASSERT(cis_params); } rp_len = sizeof(struct ble_hci_le_set_cig_params_rp) + cis_count * 2; - rp = calloc(1, rp_len); - assert(rp); + rp = bt_le_ext_calloc(1, rp_len); + BT_LE_ASSERT(rp); for (size_t i = 0; i < cis_count; i++) { cis_params[i].cis_id = buf->data[18 + i * sizeof(struct ble_hci_le_cis_params)]; @@ -180,9 +180,9 @@ static int hci_cmd_set_cig_params_test(struct net_buf *buf, struct net_buf **rsp uint8_t *rp; int status; - assert(rsp); + BT_LE_ASSERT(rsp); - assert(buf->len >= 18); + BT_LE_ASSERT(buf->len >= 18); cig_id = buf->data[3]; sdu_interval_c_to_p = sys_get_le24(buf->data + 4); @@ -195,17 +195,17 @@ static int hci_cmd_set_cig_params_test(struct net_buf *buf, struct net_buf **rsp framing = buf->data[16]; cis_count = buf->data[17]; - assert(buf->len >= 18 + cis_count * sizeof(struct ble_hci_le_cis_params_test)); + BT_LE_ASSERT(buf->len >= 18 + cis_count * sizeof(struct ble_hci_le_cis_params_test)); /* The cis_count can be 0 */ if (cis_count) { - cis_params = calloc(1, cis_count * sizeof(struct ble_hci_le_cis_params_test)); - assert(cis_params); + cis_params = bt_le_ext_calloc(1, cis_count * sizeof(struct ble_hci_le_cis_params_test)); + BT_LE_ASSERT(cis_params); } rp_len = sizeof(struct ble_hci_le_set_cig_params_test_rp) + cis_count * 2; - rp = calloc(1, rp_len); - assert(rp); + rp = bt_le_ext_calloc(1, rp_len); + BT_LE_ASSERT(rp); for (size_t i = 0; i < cis_count; i++) { cis_params[i].cis_id = buf->data[18 + i * sizeof(struct ble_hci_le_cis_params_test)]; @@ -269,15 +269,15 @@ static int hci_cmd_create_cis(struct net_buf *buf, struct net_buf **rsp) ARG_UNUSED(rsp); - assert(buf->len >= 4); + BT_LE_ASSERT(buf->len >= 4); /* The cis_count shall be at least 1 */ cis_count = buf->data[3]; - assert(buf->len >= 4 + cis_count * sizeof(struct ble_hci_le_create_cis_params)); + BT_LE_ASSERT(buf->len >= 4 + cis_count * sizeof(struct ble_hci_le_create_cis_params)); - cis_params = calloc(1, cis_count * sizeof(struct ble_hci_le_create_cis_params)); - assert(cis_params); + cis_params = bt_le_ext_calloc(1, cis_count * sizeof(struct ble_hci_le_create_cis_params)); + BT_LE_ASSERT(cis_params); for (size_t i = 0; i < cis_count; i++) { cis_params[i].cis_handle = sys_get_le16(buf->data + 4 + i * sizeof(struct ble_hci_le_create_cis_params)); @@ -306,7 +306,7 @@ static int hci_cmd_remove_cig(struct net_buf *buf, struct net_buf **rsp) ARG_UNUSED(rsp); - assert(buf->len >= 4); + BT_LE_ASSERT(buf->len >= 4); cig_id = buf->data[3]; @@ -331,7 +331,7 @@ static int hci_cmd_accept_cis_req(struct net_buf *buf, struct net_buf **rsp) ARG_UNUSED(rsp); - assert(buf->len >= 5); + BT_LE_ASSERT(buf->len >= 5); cis_handle = sys_get_le16(buf->data + 3); @@ -357,7 +357,7 @@ static int hci_cmd_reject_cis_req(struct net_buf *buf, struct net_buf **rsp) ARG_UNUSED(rsp); - assert(buf->len >= 6); + BT_LE_ASSERT(buf->len >= 6); cis_handle = sys_get_le16(buf->data + 3); reason = buf->data[5]; @@ -394,7 +394,7 @@ static int hci_cmd_create_big(struct net_buf *buf, struct net_buf **rsp) ARG_UNUSED(rsp); - assert(buf->len >= 34); + BT_LE_ASSERT(buf->len >= 34); big_handle = buf->data[3]; adv_handle = buf->data[4]; @@ -456,7 +456,7 @@ static int hci_cmd_create_big_test(struct net_buf *buf, struct net_buf **rsp) ARG_UNUSED(rsp); - assert(buf->len >= 39); + BT_LE_ASSERT(buf->len >= 39); big_handle = buf->data[3]; adv_handle = buf->data[4]; @@ -512,7 +512,7 @@ static int hci_cmd_terminate_big(struct net_buf *buf, struct net_buf **rsp) ARG_UNUSED(rsp); - assert(buf->len >= 5); + BT_LE_ASSERT(buf->len >= 5); big_handle = buf->data[3]; reason = buf->data[4]; @@ -545,7 +545,7 @@ static int hci_cmd_big_create_sync(struct net_buf *buf, struct net_buf **rsp) ARG_UNUSED(rsp); - assert(buf->len >= 27); + BT_LE_ASSERT(buf->len >= 27); big_handle = buf->data[3]; sync_handle = sys_get_le16(buf->data + 4); @@ -556,7 +556,7 @@ static int hci_cmd_big_create_sync(struct net_buf *buf, struct net_buf **rsp) num_bis = buf->data[26]; bis = buf->data + 27; - assert(buf->len >= 27 + num_bis); + BT_LE_ASSERT(buf->len >= 27 + num_bis); ble_hs_lock(); @@ -584,9 +584,9 @@ static int hci_cmd_big_terminate_sync(struct net_buf *buf, struct net_buf **rsp) uint8_t big_handle; int status; - assert(rsp); + BT_LE_ASSERT(rsp); - assert(buf->len >= 4); + BT_LE_ASSERT(buf->len >= 4); big_handle = buf->data[3]; @@ -625,9 +625,9 @@ static int hci_cmd_setup_iso_data_path(struct net_buf *buf, struct net_buf **rsp uint8_t *codec_cfg; int status; - assert(rsp); + BT_LE_ASSERT(rsp); - assert(buf->len >= 16); + BT_LE_ASSERT(buf->len >= 16); conn_handle = sys_get_le16(buf->data + 3); data_path_direction = buf->data[5]; @@ -639,7 +639,7 @@ static int hci_cmd_setup_iso_data_path(struct net_buf *buf, struct net_buf **rsp codec_cfg_len = buf->data[15]; codec_cfg = buf->data + 16; - assert(buf->len >= 16 + codec_cfg_len); + BT_LE_ASSERT(buf->len >= 16 + codec_cfg_len); ble_hs_lock(); @@ -677,9 +677,9 @@ static int hci_cmd_remove_iso_data_path(struct net_buf *buf, struct net_buf **rs uint16_t conn_handle; int status; - assert(rsp); + BT_LE_ASSERT(rsp); - assert(buf->len >= 6); + BT_LE_ASSERT(buf->len >= 6); conn_handle = sys_get_le16(buf->data + 3); data_path_direction = buf->data[5]; @@ -792,8 +792,8 @@ static void iso_evt_rx(uint8_t event, const void *data, qdata_len = len + 2; - qdata = calloc(1, qdata_len); - assert(qdata); + qdata = bt_le_ext_calloc(1, qdata_len); + BT_LE_ASSERT(qdata); qdata[0] = le_meta; qdata[1] = event; @@ -808,7 +808,9 @@ static void iso_evt_rx(uint8_t event, const void *data, err = bt_le_iso_task_post(q_type, qdata, qdata_len); if (err) { - LOG_ERR("[N]IsoPostEvtFail[%d][%02x]", err, event); + if (q_type == ISO_QUEUE_ITEM_TYPE_ISO_HCI_EVENT) { + ISO_POST_FAIL_LOG(err, "[N]IsoPostEvtFail[%d][%02x]", err, event); + } free(qdata); } } diff --git a/components/bt/esp_ble_iso/host/adapter/nimble/l2cap.c b/components/bt/esp_ble_iso/host/adapter/nimble/l2cap.c deleted file mode 100644 index 53bedbc3506..00000000000 --- a/components/bt/esp_ble_iso/host/adapter/nimble/l2cap.c +++ /dev/null @@ -1,336 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#include -#include -#include -#include -#include - -#include -#include -#include - -#include <../host/conn_internal.h> - -#include "host/ble_hs.h" -#include "host/ble_l2cap.h" -#include "host/ble_hs_mbuf.h" -#include "../../../nimble/host/src/ble_l2cap_priv.h" - -#include "common/host.h" - -#include "nimble/hs_error.h" - -LOG_MODULE_REGISTER(ISO_N2CAP, CONFIG_BT_ISO_LOG_LEVEL); - -_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) -#define OTS_L2CAP_MEM_SIZE OS_MEMPOOL_SIZE(OTS_L2CAP_BUF_COUNT, L2CAP_LE_OTS_MTU * 2) - -static os_membuf_t ots_mem[OTS_L2CAP_MEM_SIZE]; -static struct os_mempool ots_mbuf_mempool; -static struct os_mbuf_pool ots_mbuf_pool; - -static struct ble_l2cap_chan *ots_chan; - -static int ots_l2cap_recv_ready(struct ble_l2cap_chan *chan) -{ - struct os_mbuf *sdu_rx; - int rc; - - LOG_DBG("[N]L2capOtsRecvReady"); - - sdu_rx = os_mbuf_get_pkthdr(&ots_mbuf_pool, 0); - if (sdu_rx == NULL) { - LOG_ERR("[N]L2capNoBufForL2capRecv"); - return -ENOMEM; - } - - rc = ble_l2cap_recv_ready(chan, sdu_rx); - if (rc) { - LOG_ERR("[N]L2capRecvFail[%d]", rc); - - os_mbuf_free_chain(sdu_rx); - return rc; - } - - return 0; -} - -static int ots_l2cap_event_cb(struct ble_l2cap_event *event, void *arg) -{ - struct ble_l2cap_chan_info chan_info; - uint16_t result = 0; - size_t sdu_len; - uint8_t *sdu; - int err; - - LOG_DBG("[N]L2capOtsEvtCb[%u]", event->type); - - switch (event->type) { - case BLE_L2CAP_EVENT_COC_CONNECTED: - if (event->connect.status) { - LOG_ERR("[N]L2capCocConnectFail[%d]", event->connect.status); - return 0; - } - - if (ots_chan) { - LOG_ERR("[N]L2capCocChanExist"); - return 0; - } - - ots_chan = event->connect.chan; - - if (ble_l2cap_get_chan_info(event->connect.chan, &chan_info)) { - LOG_ERR("[N]L2capCocGetChanInfoFail"); - /* Roll back the latch so the next COC_CONNECTED isn't refused - * by the if (ots_chan) guard above. */ - ots_chan = NULL; - return -EIO; - } - - LOG_INF("[N]L2capCocConnect[%u][%04x][%04x][%04x][%u][%u][%u][%u]", - event->connect.conn_handle, chan_info.scid, chan_info.dcid, - chan_info.psm, chan_info.our_l2cap_mtu, chan_info.peer_l2cap_mtu, - chan_info.our_coc_mtu, chan_info.peer_coc_mtu); - - bt_le_l2cap_connected(event->connect.conn_handle, chan_info.psm, - chan_info.dcid, chan_info.peer_coc_mtu, - chan_info.scid, chan_info.our_coc_mtu); - return 0; - - case BLE_L2CAP_EVENT_COC_DISCONNECTED: - if (ots_chan != event->disconnect.chan) { - LOG_ERR("[N]L2capDisconnectInvCocChan"); - return 0; - } - - LOG_INF("[N]L2capCocDisconnect[%u][%04x]", - event->disconnect.conn_handle, event->disconnect.chan->psm); - - bt_le_l2cap_disconnected(event->disconnect.conn_handle, event->disconnect.chan->psm); - - ots_chan = NULL; - return 0; - - case BLE_L2CAP_EVENT_COC_ACCEPT: - /* Don't reject on peer_sdu_size: LE CoC allows asymmetric MTUs. It's the peer's - * RX MTU (our TX ceiling) — harmless; our RX is bounded by ble_l2cap_create_server. - * (PTS uses 1024 > our 256; rejecting broke SCP.) */ - - LOG_DBG("[N]L2capCocAccept[%u][%04x][%04x][%u][%u][%u][%u]", - event->accept.conn_handle, event->accept.chan->psm, - event->accept.chan->dcid, event->accept.chan->coc_tx.mtu, - event->accept.chan->peer_coc_mps, event->accept.peer_sdu_size, - event->accept.chan->coc_tx.credits); - - err = bt_le_l2cap_accept(event->accept.conn_handle, - event->accept.chan->psm, - event->accept.chan->dcid, - event->accept.chan->coc_tx.mtu, - event->accept.chan->peer_coc_mps, - event->accept.chan->coc_tx.credits, - &result); - if (err) { - return err; - } - - ARG_UNUSED(result); - - return ots_l2cap_recv_ready(event->accept.chan); - - case BLE_L2CAP_EVENT_COC_DATA_RECEIVED: - if (ots_chan != event->receive.chan) { - LOG_ERR("[N]L2capRecvOnInvCocChan"); - return 0; - } - - assert(event->receive.sdu_rx); - - LOG_DBG("[N]L2capCocRecv[%u][%04x][%u]", - event->receive.conn_handle, event->receive.chan->psm, - event->receive.sdu_rx->om_len); - - sdu_len = OS_MBUF_PKTLEN(event->receive.sdu_rx); - - sdu = calloc(1, sdu_len); - assert(sdu); - - err = os_mbuf_copydata(event->receive.sdu_rx, 0, sdu_len, sdu); - assert(err == 0); - - bt_le_l2cap_received(event->receive.conn_handle, - event->receive.chan->psm, - sdu, sdu_len); - - os_mbuf_free_chain(event->receive.sdu_rx); - free(sdu); - - return ots_l2cap_recv_ready(event->receive.chan); - - case BLE_L2CAP_EVENT_COC_TX_UNSTALLED: - if (ots_chan != event->tx_unstalled.chan) { - LOG_ERR("[N]L2capTxUnstalledOnInvCocChan"); - return 0; - } - - LOG_WRN("[N]L2capCocTxUnstalled[%u][%d]", - event->tx_unstalled.conn_handle, event->tx_unstalled.status); - - /* TODO: transmit the remaining data */ - return 0; - - default: - return 0; - } -} - -int bt_le_nimble_l2cap_chan_connect(uint16_t conn_handle) -{ - struct os_mbuf *sdu_rx; - int rc; - - if (ots_chan) { - LOG_WRN("[N]L2capOtsChanExist"); - return -EALREADY; - } - - sdu_rx = os_mbuf_get_pkthdr(&ots_mbuf_pool, 0); - if (sdu_rx == NULL) { - LOG_ERR("[N]L2capNoBufForConnect"); - return -ENOMEM; - } - - rc = ble_l2cap_connect(conn_handle, L2CAP_LE_OTS_PSM, L2CAP_LE_OTS_MTU, - sdu_rx, ots_l2cap_event_cb, NULL); - if (rc) { - LOG_ERR("[N]L2capConnectFail[%d]", rc); - - os_mbuf_free_chain(sdu_rx); - return nimble_err_to_errno(rc); - } - - return 0; -} - -int bt_le_nimble_l2cap_chan_disconnect(struct bt_l2cap_chan *chan) -{ - int rc; - - if (ots_chan == NULL) { - LOG_WRN("[N]L2capNoOtsChan"); - return -ENOTCONN; - } - - if (ble_l2cap_get_conn_handle(ots_chan) != chan->conn->handle) { - LOG_ERR("[N]L2capUnexpOtsChan[%u][%u]", - ble_l2cap_get_conn_handle(ots_chan), chan->conn->handle); - return -EINVAL; - } - - rc = ble_l2cap_disconnect(ots_chan); - if (rc) { - LOG_ERR("[N]L2capDisconnectFail[%d]", rc); - return nimble_err_to_errno(rc); - } - - return 0; -} - -int bt_le_nimble_l2cap_chan_send(struct bt_l2cap_chan *chan, struct net_buf *buf) -{ - struct os_mbuf *sdu_tx; - int rc; - - if (ots_chan == NULL) { - LOG_WRN("[N]L2capNoOtsChan"); - return -ENOTCONN; - } - - if (ble_l2cap_get_conn_handle(ots_chan) != chan->conn->handle) { - LOG_ERR("[N]L2capUnexpOtsChan[%u][%u]", - ble_l2cap_get_conn_handle(ots_chan), chan->conn->handle); - return -EINVAL; - } - - sdu_tx = os_mbuf_get_pkthdr(&ots_mbuf_pool, 0); - if (sdu_tx == NULL) { - LOG_ERR("[N]L2capNoBufForSend"); - return -ENOMEM; - } - - rc = os_mbuf_append(sdu_tx, buf->data, buf->len); - if (rc) { - LOG_ERR("[N]L2capAppendBufFail[%d]", rc); - os_mbuf_free_chain(sdu_tx); - return -EIO; - } - - rc = ble_l2cap_send(ots_chan, sdu_tx); - if (rc) { - if (rc == BLE_HS_ESTALLED) { - /* sdu is queued in tx->sdus[0]; NimBLE will continue on - * BLE_L2CAP_EVENT_COC_TX_UNSTALLED. Do NOT free here. - */ - LOG_WRN("[N]L2capMoreCreditsForSend"); - } 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]L2capSendInternalFail[%d]", rc); - } - - return nimble_err_to_errno(rc); - } - - /* Payload copied into sdu_tx; consume buf here (no async TX-done to unref it), - * else the 1-buffer ot_chan_tx_pool leaks. Failure paths leave buf to caller. */ - net_buf_unref(buf); - - return 0; -} - -int bt_le_nimble_l2cap_init(void) -{ - int rc; - - rc = os_mempool_init(&ots_mbuf_mempool, OTS_L2CAP_BUF_COUNT, L2CAP_LE_OTS_MTU * 2, ots_mem, "ots_pool"); - if (rc) { - LOG_ERR("[N]L2capInitOtsMempoolFail[%d]", rc); - return rc; - } - - rc = os_mbuf_pool_init(&ots_mbuf_pool, &ots_mbuf_mempool, L2CAP_LE_OTS_MTU, OTS_L2CAP_BUF_COUNT); - if (rc) { - LOG_ERR("[N]L2capInitOtsMbufPoolFail[%d]", rc); - return rc; - } - -#if CONFIG_BT_OTS - rc = ble_l2cap_create_server(L2CAP_LE_OTS_PSM, L2CAP_LE_OTS_MTU, ots_l2cap_event_cb, NULL); - if (rc) { - LOG_ERR("[N]L2capCreateL2capSrvFail[%d]", rc); - return rc; - } -#endif /* CONFIG_BT_OTS */ - - return 0; -} - -void bt_le_nimble_l2cap_deinit(void) -{ - LOG_DBG("[N]L2capDeinit"); - - /* TODO: free the ots_mbuf_pool and ots_mbuf_mempool */ - -#if CONFIG_BT_OTS - /* TODO: destroy the server */ -#endif /* CONFIG_BT_OTS */ -} diff --git a/components/bt/esp_ble_iso/host/common/adv.c b/components/bt/esp_ble_iso/host/common/adv.c index a359d95f575..749627d098d 100644 --- a/components/bt/esp_ble_iso/host/common/adv.c +++ b/components/bt/esp_ble_iso/host/common/adv.c @@ -20,15 +20,14 @@ LOG_MODULE_REGISTER(ISO_ADV, CONFIG_BT_ISO_LOG_LEVEL); -static struct bt_le_ext_adv ext_adv_pool[CONFIG_BT_EXT_ADV_MAX_ADV_SET]; +static BT_ISO_EXT_RAM_BSS_ATTR struct bt_le_ext_adv ext_adv_pool[CONFIG_BT_EXT_ADV_MAX_ADV_SET]; static struct bt_le_ext_adv *ext_adv_find(uint8_t adv_handle) { struct bt_le_ext_adv *adv = NULL; for (size_t i = 0; i < ARRAY_SIZE(ext_adv_pool); i++) { - if (atomic_test_bit(ext_adv_pool[i].flags, - BT_PER_ADV_PARAMS_SET) && + if (atomic_test_bit(ext_adv_pool[i].flags, BT_PER_ADV_PARAMS_SET) && ext_adv_pool[i].handle == adv_handle) { LOG_DBG("ExtAdvFound[%u][%u]", i, adv_handle); adv = &ext_adv_pool[i]; @@ -44,8 +43,7 @@ static struct bt_le_ext_adv *ext_adv_new(void) struct bt_le_ext_adv *adv = NULL; for (size_t i = 0; i < ARRAY_SIZE(ext_adv_pool); i++) { - if (atomic_test_bit(ext_adv_pool[i].flags, - BT_PER_ADV_PARAMS_SET) == false) { + if (atomic_test_bit(ext_adv_pool[i].flags, BT_PER_ADV_PARAMS_SET) == false) { adv = &ext_adv_pool[i]; memset(adv, 0, sizeof(*adv)); @@ -70,6 +68,17 @@ struct bt_le_ext_adv *bt_le_ext_adv_find(uint8_t adv_handle) } _IDF_ONLY +void bt_le_ext_adv_state_reset(void) +{ + for (size_t i = 0; i < ARRAY_SIZE(ext_adv_pool); i++) { + if (atomic_test_bit(ext_adv_pool[i].flags, BT_PER_ADV_PARAMS_SET)) { + LOG_WRN("DeinitDropExtAdv[%u]", ext_adv_pool[i].handle); + } + } + + memset(ext_adv_pool, 0, sizeof(ext_adv_pool)); +} + int bt_le_ext_adv_new_safe(uint8_t adv_handle, uint8_t addr_type, const uint8_t *addr, uint8_t sid) { @@ -149,3 +158,23 @@ int bt_le_ext_adv_get_info(const struct bt_le_ext_adv *adv, return 0; } + +_LIB_ONLY +struct bt_le_ext_adv *bt_le_ext_adv_lookup_addr(const bt_addr_le_t *adv_addr, uint8_t sid) +{ + struct bt_le_ext_adv *adv = NULL; + + BT_LE_ASSERT(adv_addr); + + for (size_t i = 0; i < ARRAY_SIZE(ext_adv_pool); i++) { + if (atomic_test_bit(ext_adv_pool[i].flags, BT_PER_ADV_PARAMS_SET) && + bt_addr_le_eq(&ext_adv_pool[i].addr, adv_addr) && + ext_adv_pool[i].sid == sid) { + LOG_INF("ExtAdvLookupAddrFound[%u][%u]", i, sid); + adv = &ext_adv_pool[i]; + break; + } + } + + return adv; +} diff --git a/components/bt/esp_ble_iso/host/common/app/gap.c b/components/bt/esp_ble_iso/host/common/app/gap.c index ebf9aa79b8e..c4bdbb82dfe 100644 --- a/components/bt/esp_ble_iso/host/common/app/gap.c +++ b/components/bt/esp_ble_iso/host/common/app/gap.c @@ -20,7 +20,7 @@ LOG_MODULE_REGISTER(ISO_AGAP, CONFIG_BT_ISO_LOG_LEVEL); -static bt_le_gap_app_cb gap_app_cb; +static BT_ISO_EXT_RAM_BSS_ATTR bt_le_gap_app_cb gap_app_cb; int bt_le_gap_app_cb_register(bt_le_gap_app_cb cb) { @@ -175,7 +175,7 @@ static void handle_pa_sync_event_safe(struct bt_le_gap_app_param *param) } err = bt_le_per_adv_sync_establish_listener(event.pa_sync.sync_handle); - assert(err == 0); + BT_LE_ASSERT(err == 0); end: bt_le_host_unlock(); @@ -203,6 +203,29 @@ static void handle_pa_sync_past_event_safe(struct bt_le_gap_app_param *param) bt_le_host_lock(); +#if CONFIG_BT_BLUEDROID_ENABLED + /* See handle_security_change_event_safe: the producer runs on BTU and cannot + * read gatt_conns[], so it sends src_addr and the handle is resolved here. + * HCI identifies the PAST sender by handle and gives no address for it, so + * btu_hcif recovers the BDA from the LCB without its type - match on value. */ + { + struct gatt_conn *gatt_conn; + + gatt_conn = bt_le_bluedroid_find_gatt_conn_with_addr(0, + param->pa_sync_past.src_addr.val, + true); + if (gatt_conn == NULL) { + /* ACL disconnected between PAST enqueue and host processing (scan.c + * names this race). Keep the sync connection-less, like the non-PAST + * path and NimBLE — synced cb fires with conn=NULL. */ + LOG_WRN("GapPastUnknownSrc"); + event.pa_sync_past.conn_handle = BT_CONN_HANDLE_INVALID; + } else { + event.pa_sync_past.conn_handle = gatt_conn->conn_handle; + } + } +#endif /* CONFIG_BT_BLUEDROID_ENABLED */ + if (event.pa_sync_past.status) { goto end; } @@ -220,7 +243,7 @@ static void handle_pa_sync_past_event_safe(struct bt_le_gap_app_param *param) } err = bt_le_per_adv_sync_establish_listener(event.pa_sync_past.sync_handle); - assert(err == 0); + BT_LE_ASSERT(err == 0); end: bt_le_host_unlock(); @@ -246,7 +269,7 @@ static void handle_pa_sync_lost_event_safe(struct bt_le_gap_app_param *param) } err = bt_le_per_adv_sync_delete(event.pa_sync_lost.sync_handle); - assert(err == 0); + BT_LE_ASSERT(err == 0); end: bt_le_host_unlock(); @@ -321,7 +344,7 @@ static void handle_acl_connect_event_safe(struct bt_le_gap_app_param *param) } err = bt_le_acl_conn_connected_listener(event.acl_connect.conn_handle); - assert(err == 0); + BT_LE_ASSERT(err == 0); end: bt_le_host_unlock(); @@ -348,7 +371,7 @@ static void handle_acl_disconnect_event_safe(struct bt_le_gap_app_param *param) } err = bt_le_acl_conn_delete(event.acl_disconnect.conn_handle); - assert(err == 0); + BT_LE_ASSERT(err == 0); end: bt_le_host_unlock(); @@ -378,6 +401,27 @@ static void handle_security_change_event_safe(struct bt_le_gap_app_param *param) bt_le_host_lock(); +#if CONFIG_BT_BLUEDROID_ENABLED + /* Bluedroid's AUTH_CMPL arrives on BTC, which carries no conn_handle and may + * not read gatt_conns[] (iso_task mutates it under this lock). So the producer + * sends dst only and the identity is resolved here instead. NimBLE gets both + * from its own event and fills them at post time. */ + { + struct gatt_conn *gatt_conn; + + gatt_conn = bt_le_bluedroid_find_gatt_conn_with_addr(event.security_change.dst.type, + event.security_change.dst.val, + false); + if (gatt_conn == NULL) { + LOG_ERR("GapSecChgUnknownDev"); + goto end; + } + + event.security_change.conn_handle = gatt_conn->conn_handle; + event.security_change.role = gatt_conn->role; + } +#endif /* CONFIG_BT_BLUEDROID_ENABLED */ + if (event.security_change.status) { goto end; } @@ -393,11 +437,16 @@ static void handle_security_change_event_safe(struct bt_le_gap_app_param *param) if (err) { goto end; } + + /* Created already encrypted — mirror update()'s *encrypted out-param. */ + if (event.security_change.sec_level > BT_SECURITY_L1) { + encrypted = true; + } } else { err = bt_le_acl_conn_update(event.security_change.conn_handle, event.security_change.sec_level, &encrypted); - assert(err == 0); + BT_LE_ASSERT(err == 0); } /* Point conn->le.keys at the bonded LTK the adapter captured, so the lib's @@ -413,12 +462,11 @@ static void handle_security_change_event_safe(struct bt_le_gap_app_param *param) err = bt_le_acl_conn_security_changed_listener(event.security_change.conn_handle, event.security_change.sec_level); - assert(err == 0); + BT_LE_ASSERT(err == 0); if (encrypted) { /* TODO: check if bonded or not */ - err = bt_le_acl_conn_pairing_completed_listener(event.security_change.conn_handle, true); - assert(err == 0); + (void)bt_le_acl_conn_pairing_completed_listener(event.security_change.conn_handle, true); } end: @@ -479,11 +527,37 @@ static void handle_bond_delete_event_safe(struct bt_le_gap_app_param *param) bt_le_gap_app_cb_evt(&event); } +void bt_le_gap_event_free(void *data) +{ + struct bt_le_gap_app_param *qev = data; + + if (qev == NULL) { + return; + } + + switch (qev->type) { + case BT_LE_GAP_APP_PARAM_EXT_SCAN_RECV: + if (qev->ext_scan_recv.data) { + free(qev->ext_scan_recv.data); + } + break; + case BT_LE_GAP_APP_PARAM_PA_SYNC_RECV: + if (qev->pa_sync_recv.data) { + free(qev->pa_sync_recv.data); + } + break; + default: + break; + } + + free(qev); +} + void bt_le_gap_handle_event(uint8_t *data, size_t data_len) { struct bt_le_gap_app_param *param; - assert(data && data_len); + BT_LE_ASSERT(data && data_len); param = (struct bt_le_gap_app_param *)data; @@ -519,14 +593,14 @@ void bt_le_gap_handle_event(uint8_t *data, size_t data_len) handle_bond_delete_event_safe(param); break; default: - assert(0); + BT_LE_ASSERT(0); break; } free(data); } -void bt_le_gap_app_post_event(uint8_t type, void *param) +void bt_le_gap_app_post_event(uint16_t type, void *param) { #if CONFIG_BT_BLUEDROID_ENABLED /* For Bluedroid, post the typed event to the ISO task instead. */ diff --git a/components/bt/esp_ble_iso/host/common/app/gatt.c b/components/bt/esp_ble_iso/host/common/app/gatt.c index 36015841dfa..8d603e76eed 100644 --- a/components/bt/esp_ble_iso/host/common/app/gatt.c +++ b/components/bt/esp_ble_iso/host/common/app/gatt.c @@ -20,7 +20,7 @@ LOG_MODULE_REGISTER(ISO_AGAT, CONFIG_BT_ISO_LOG_LEVEL); -static bt_le_gatt_app_cb gatt_app_cb; +static BT_ISO_EXT_RAM_BSS_ATTR bt_le_gatt_app_cb gatt_app_cb; int bt_le_gatt_app_cb_register(bt_le_gatt_app_cb cb) { diff --git a/components/bt/esp_ble_iso/host/common/conn.c b/components/bt/esp_ble_iso/host/common/conn.c index c59ccba4244..b3796a20964 100644 --- a/components/bt/esp_ble_iso/host/common/conn.c +++ b/components/bt/esp_ble_iso/host/common/conn.c @@ -8,10 +8,12 @@ #include #include +#include #include #include #include +#include #include #include <../host/keys.h> @@ -23,18 +25,18 @@ LOG_MODULE_REGISTER(ISO_CONN, CONFIG_BT_ISO_LOG_LEVEL); -static struct bt_conn acl_conns[CONFIG_BT_MAX_CONN]; +static BT_ISO_EXT_RAM_BSS_ATTR struct bt_conn acl_conns[CONFIG_BT_MAX_CONN]; /* Per-ACL LTK backing store (indexed in lockstep with acl_conns[]). conn->le.keys is * a bare pointer the adapters fill after bonding; point it at the matching slot so the * lib's CSIS sirk_encrypt can read conn->le.keys->ltk.val. */ -static struct bt_keys conn_ltk[CONFIG_BT_MAX_CONN]; +static BT_ISO_EXT_RAM_BSS_ATTR struct bt_keys conn_ltk[CONFIG_BT_MAX_CONN]; extern struct bt_conn iso_conns[CONFIG_BT_ISO_MAX_CHAN]; -static sys_slist_t conn_cbs = SYS_SLIST_STATIC_INIT(&conn_cbs); +static BT_ISO_EXT_RAM_BSS_ATTR sys_slist_t conn_cbs; -static sys_slist_t auth_info_cbs = SYS_SLIST_STATIC_INIT(&auth_info_cbs); +static BT_ISO_EXT_RAM_BSS_ATTR sys_slist_t auth_info_cbs; _IDF_ONLY void bt_conn_get_acl_conns(struct bt_conn **conns, uint8_t *count) @@ -45,6 +47,31 @@ void bt_conn_get_acl_conns(struct bt_conn **conns, uint8_t *count) *count = ARRAY_SIZE(acl_conns); } +size_t bt_le_acl_conn_count(void) +{ + size_t count = 0; + + for (size_t i = 0; i < ARRAY_SIZE(acl_conns); i++) { + if (acl_conns[i].state != BT_CONN_DISCONNECTED) { + count++; + } + } + + return count; +} + +void bt_le_conn_reset(void) +{ + LOG_DBG("ConnReset"); + + memset(acl_conns, 0, sizeof(acl_conns)); + memset(conn_ltk, 0, sizeof(conn_ltk)); + memset(iso_conns, 0, sizeof(struct bt_conn) * CONFIG_BT_ISO_MAX_CHAN); + + sys_slist_init(&conn_cbs); + sys_slist_init(&auth_info_cbs); +} + _IDF_ONLY bool bt_conn_is_peer_addr_le(const struct bt_conn *conn, uint8_t id, const bt_addr_le_t *peer) @@ -53,9 +80,10 @@ bool bt_conn_is_peer_addr_le(const struct bt_conn *conn, uint8_t id, ARG_UNUSED(id); - assert(conn && peer); + BT_LE_ASSERT(conn && peer); + + /* TODO(privacy): also match init/resp or resolved identity; dst may stay RPA. */ - /* Check against conn dst address as it may be the identity address */ if (bt_addr_le_eq(peer, &conn->le.dst)) { return true; } @@ -63,24 +91,12 @@ bool bt_conn_is_peer_addr_le(const struct bt_conn *conn, uint8_t id, return false; } -_LIB_IDF -struct bt_conn *bt_conn_ref(struct bt_conn *conn) -{ - return conn; -} - -_LIB_IDF -void bt_conn_unref(struct bt_conn *conn) -{ - ARG_UNUSED(conn); -} - _LIB_ONLY void bt_conn_foreach(enum bt_conn_type type, void (*func)(struct bt_conn *conn, void *data), void *data) { - assert(type == BT_CONN_TYPE_LE); + BT_LE_ASSERT(type == BT_CONN_TYPE_LE); for (size_t i = 0; i < ARRAY_SIZE(acl_conns); i++) { struct bt_conn *conn = &acl_conns[i]; @@ -95,7 +111,7 @@ void bt_conn_foreach(enum bt_conn_type type, _LIB_ONLY const bt_addr_le_t *bt_conn_get_dst(const struct bt_conn *conn) { - assert(conn); + BT_LE_ASSERT(conn); return &conn->le.dst; } @@ -105,19 +121,19 @@ uint8_t bt_conn_index(const struct bt_conn *conn) { ptrdiff_t index = 0; - assert(conn); + BT_LE_ASSERT(conn); switch (conn->type) { case BT_CONN_TYPE_LE: index = conn - acl_conns; - assert(index >= 0 && index < ARRAY_SIZE(acl_conns)); + BT_LE_ASSERT(index >= 0 && index < ARRAY_SIZE(acl_conns)); break; case BT_CONN_TYPE_ISO: index = conn - iso_conns; - assert(index >= 0 && index < ARRAY_SIZE(iso_conns)); + BT_LE_ASSERT(index >= 0 && index < ARRAY_SIZE(iso_conns)); break; default: - assert(0); + BT_LE_ASSERT(0); break; } @@ -126,19 +142,6 @@ uint8_t bt_conn_index(const struct bt_conn *conn) return (uint8_t)index; } -#if CONFIG_BT_SMP -_IDF_ONLY -int bt_conn_set_security(struct bt_conn *conn, bt_security_t sec) -{ - /* This function is used by CIS to set security level, - * currently always return failure here to make sure - * that before creating CIS, the ACL connection is - * encrypted. - */ - return -ENOTSUP; -} -#endif /* CONFIG_BT_SMP */ - static enum bt_conn_state conn_internal_to_public_state(bt_conn_state_t state) { switch (state) { @@ -152,7 +155,7 @@ static enum bt_conn_state conn_internal_to_public_state(bt_conn_state_t state) case BT_CONN_DISCONNECTING: return BT_CONN_STATE_DISCONNECTING; default: - assert(0); + BT_LE_ASSERT(0); return 0; } } @@ -160,7 +163,10 @@ static enum bt_conn_state conn_internal_to_public_state(bt_conn_state_t state) _LIB_ONLY int bt_conn_get_info(const struct bt_conn *conn, struct bt_conn_info *info) { - assert(conn && info); + BT_LE_ASSERT(conn && info); + + /* Callers today only pass ACL (BT_CONN_TYPE_LE). ISO would need + * info->le.* from conn->iso.acl (union); not implemented until needed. */ info->type = conn->type; info->role = conn->role; @@ -218,7 +224,6 @@ int bt_conn_cb_register_safe(struct bt_conn_cb *cb) return err; } -_NOT_USED int bt_conn_cb_unregister(struct bt_conn_cb *cb) { LOG_DBG("ConnCbUnreg"); @@ -236,6 +241,16 @@ int bt_conn_cb_unregister(struct bt_conn_cb *cb) return 0; } +_IDF_ONLY +int bt_conn_cb_unregister_safe(struct bt_conn_cb *cb) +{ + int err; + bt_le_host_lock(); + err = bt_conn_cb_unregister(cb); + bt_le_host_unlock(); + return err; +} + _LIB_ONLY int bt_conn_auth_info_cb_register(struct bt_conn_auth_info_cb *cb) { @@ -277,7 +292,7 @@ int bt_conn_auth_info_cb_unregister(struct bt_conn_auth_info_cb *cb) _IDF_ONLY struct bt_conn *bt_conn_new(struct bt_conn *conns, size_t size) { - assert(conns); + BT_LE_ASSERT(conns); for (size_t i = 0; i < size; i++) { struct bt_conn *conn = &conns[i]; @@ -322,7 +337,7 @@ struct bt_conn *bt_conn_lookup_handle(uint16_t handle, enum bt_conn_type type) { struct bt_conn *conn = NULL; - assert(type == BT_CONN_TYPE_LE || type == BT_CONN_TYPE_ISO); + BT_LE_ASSERT(type == BT_CONN_TYPE_LE || type == BT_CONN_TYPE_ISO); /* LOG_DBG("ConnLookupHdl[%u][%u]", handle, type); */ @@ -337,11 +352,20 @@ struct bt_conn *bt_conn_lookup_handle(uint16_t handle, enum bt_conn_type type) struct bt_conn *bt_conn_lookup_index(uint8_t index) { + struct bt_conn *conn; + if (index >= ARRAY_SIZE(acl_conns)) { return NULL; } - return bt_conn_ref(&acl_conns[index]); + conn = &acl_conns[index]; + + /* Free/wiped slots are type NONE after acl_conn_delete. */ + if (conn->type == BT_CONN_TYPE_NONE) { + return NULL; + } + + return conn; } _IDF_ONLY @@ -352,17 +376,6 @@ struct bt_conn *bt_le_acl_conn_find(uint16_t conn_handle) return bt_conn_lookup_handle(conn_handle, BT_CONN_TYPE_LE); } -_IDF_ONLY -struct bt_conn *bt_le_acl_conn_find_safe(uint16_t conn_handle) -{ - struct bt_conn *conn = NULL; - /* LOG_DBG("AclConnFind[%u]", conn_handle); */ - bt_le_host_lock(); - conn = bt_conn_lookup_handle(conn_handle, BT_CONN_TYPE_LE); - bt_le_host_unlock(); - return conn; -} - _IDF_ONLY int bt_le_acl_conn_new(uint16_t conn_handle, uint8_t role, @@ -371,7 +384,7 @@ int bt_le_acl_conn_new(uint16_t conn_handle, { struct bt_conn *conn; - assert(dst); + BT_LE_ASSERT(dst); LOG_DBG("AclNew[%u][%u][%u][%s]", conn_handle, role, sec_level, bt_addr_le_str(dst)); @@ -383,6 +396,10 @@ int bt_le_acl_conn_new(uint16_t conn_handle, conn->role = role; conn->state = BT_CONN_CONNECTED; conn->sec_level = sec_level; + /* AUTH-before-ACL-slot path may create with L2+ directly. */ + if (sec_level > BT_SECURITY_L1) { + conn->encrypt = 1; + } memcpy(&conn->le.dst, dst, sizeof(conn->le.dst)); } else { LOG_ERR("NoFreeConn[%u]", conn_handle); @@ -391,6 +408,27 @@ int bt_le_acl_conn_new(uint16_t conn_handle, return (conn ? 0 : -ENOMEM); } +_IDF_ONLY +int bt_le_acl_conn_new_safe(uint16_t conn_handle, uint8_t role, uint8_t addr_type, + const uint8_t *addr, uint8_t sec_level) +{ + bt_addr_le_t dst; + int err; + + if (addr == NULL) { + return -EINVAL; + } + + dst.type = addr_type; + bt_addr_copy(&dst.a, (const bt_addr_t *)addr); + + bt_le_host_lock(); + err = bt_le_acl_conn_new(conn_handle, role, &dst, sec_level); + bt_le_host_unlock(); + + return err; +} + /* Point conn->le.keys at this ACL connection's LTK slot, filled with the bonded * LTK the adapter captured. Used as key K by the lib's CSIS SIRK encryption. */ _IDF_ONLY @@ -418,6 +456,20 @@ void bt_conn_le_set_ltk(struct bt_conn *conn, const uint8_t *ltk) LOG_INF("ConnSetLtk[%u][%s]", conn->handle, bt_hex(ltk, 16)); } +#if CONFIG_BT_ISO_UNICAST +/* Defer ACL slot wipe while a CIS still holds iso.acl. */ +static bool bt_iso_acl_has_cis(const struct bt_conn *acl) +{ + for (size_t i = 0; i < ARRAY_SIZE(iso_conns); i++) { + if (iso_conns[i].type == BT_CONN_TYPE_ISO && iso_conns[i].iso.acl == acl) { + return true; + } + } + + return false; +} +#endif /* CONFIG_BT_ISO_UNICAST */ + _IDF_ONLY int bt_le_acl_conn_delete(uint16_t conn_handle) { @@ -432,6 +484,27 @@ int bt_le_acl_conn_delete(uint16_t conn_handle) return -ENOTCONN; } +#if CONFIG_BT_ISO_UNICAST + if (bt_iso_acl_has_cis(conn)) { + LOG_INF("AclConnDelDeferred[%u]", conn_handle); + return 0; + } +#endif /* CONFIG_BT_ISO_UNICAST */ + + /* OTS CoC (and any L2CAP) keep chan->conn on this list. Wipe without + * detaching leaves dangling pointers if ACL slot is reused. */ + { + struct bt_l2cap_chan *chan, *tmp; + + SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&conn->channels, chan, tmp, node) { + sys_slist_find_and_remove(&conn->channels, &chan->node); + if (chan->ops != NULL && chan->ops->disconnected != NULL) { + chan->ops->disconnected(chan); + } + chan->conn = NULL; + } + } + /* Wipe this connection's LTK slot (key hygiene); the memset below then nulls * conn->le.keys. */ memset(&conn_ltk[conn - acl_conns], 0, sizeof(conn_ltk[0])); @@ -461,8 +534,8 @@ int bt_le_acl_conn_update(uint16_t conn_handle, } if (conn->sec_level < sec_level) { - /* No encryption to encryption */ - if (conn->encrypt == 0 && conn->sec_level == BT_SECURITY_L1) { + /* Mark encrypted on first step into L2+ (not only from L1). */ + if (conn->encrypt == 0 && sec_level > BT_SECURITY_L1) { conn->encrypt = 1; if (encrypted) { @@ -519,8 +592,8 @@ void bt_conn_set_state(struct bt_conn *conn, bt_conn_state_t state) { bt_conn_state_t old_state; - assert(conn); - assert(conn->type == BT_CONN_TYPE_LE || conn->type == BT_CONN_TYPE_ISO); + BT_LE_ASSERT(conn); + BT_LE_ASSERT(conn->type == BT_CONN_TYPE_LE || conn->type == BT_CONN_TYPE_ISO); LOG_DBG("ConnSetState[%u][%u]", conn->state, state); @@ -556,7 +629,7 @@ static int iso_disconnect(struct bt_conn *conn, uint8_t reason) _IDF_ONLY int bt_conn_disconnect(struct bt_conn *conn, uint8_t reason) { - assert(conn); + BT_LE_ASSERT(conn); LOG_DBG("ConnDisconnect[%u][%u][%02x]", conn->state, conn->type, reason); @@ -567,10 +640,14 @@ int bt_conn_disconnect(struct bt_conn *conn, uint8_t reason) } if (conn->type == BT_CONN_TYPE_ISO) { - return iso_disconnect(conn, reason); + int err = iso_disconnect(conn, reason); + if (err == 0) { + bt_conn_set_state(conn, BT_CONN_DISCONNECTING); + } + return err; } - assert(0); + BT_LE_ASSERT(0); case BT_CONN_DISCONNECTING: return 0; case BT_CONN_DISCONNECTED: @@ -627,9 +704,8 @@ int bt_le_acl_conn_disconnected_listener(uint16_t conn_handle, uint8_t reason) } } -#if CONFIG_BT_ISO_UNICAST + /* GATT sub/CCC cleanup is ACL-scoped, not ISO-unicast. */ bt_le_acl_conn_disconnected_gatt_listener(conn_handle); -#endif /* CONFIG_BT_ISO_UNICAST */ return 0; } @@ -676,6 +752,8 @@ int bt_le_acl_conn_identity_resolved_listener(uint16_t conn_handle, return -ENOTCONN; } + /* TODO(privacy): copy identity into conn->le.dst once adapters post resolve. */ + SYS_SLIST_FOR_EACH_CONTAINER(&conn_cbs, listener, _node) { if (listener->identity_resolved) { listener->identity_resolved(conn, rpa, identity); diff --git a/components/bt/esp_ble_iso/host/common/gatt.c b/components/bt/esp_ble_iso/host/common/gatt.c index ea5b1aabaa9..742d70c4562 100644 --- a/components/bt/esp_ble_iso/host/common/gatt.c +++ b/components/bt/esp_ble_iso/host/common/gatt.c @@ -25,9 +25,9 @@ LOG_MODULE_REGISTER(ISO_GATT, CONFIG_BT_ISO_LOG_LEVEL); -static sys_slist_t gatt_db = SYS_SLIST_STATIC_INIT(&gatt_db); +static BT_ISO_EXT_RAM_BSS_ATTR sys_slist_t gatt_db; -static struct gattc_sub subscriptions[CONFIG_BT_MAX_CONN]; +static BT_ISO_EXT_RAM_BSS_ATTR struct gattc_sub subscriptions[CONFIG_BT_MAX_CONN]; /* Each CCC descriptor's cfg is a heap pool (depth BT_GATT_CCC_MAX = bonded peers + * active conn), allocated here at registration; freed in gatts_free_svc_ccc_cfg. */ @@ -44,7 +44,7 @@ static int gatts_alloc_svc_ccc_cfg(struct bt_gatt_service *svc) } ccc = attr->user_data; - ccc->cfg = calloc(BT_GATT_CCC_MAX, sizeof(struct bt_gatt_ccc_cfg)); + ccc->cfg = bt_le_ext_calloc(BT_GATT_CCC_MAX, sizeof(struct bt_gatt_ccc_cfg)); if (ccc->cfg == NULL) { return -ENOMEM; } @@ -88,7 +88,7 @@ int bt_gatt_service_register(struct bt_gatt_service *svc) } uuid = svc->attrs[0].user_data; - assert(uuid); + BT_LE_ASSERT(uuid); LOG_DBG("GattSvc[%04x]", uuid->val); @@ -154,7 +154,7 @@ uint16_t bt_gatt_get_mtu(struct bt_conn *conn) { uint16_t mtu; - assert(conn); + BT_LE_ASSERT(conn); #if CONFIG_BT_BLUEDROID_ENABLED mtu = bt_le_bluedroid_gatt_get_mtu(conn); @@ -230,7 +230,7 @@ uint16_t bt_gatt_attr_value_handle(const struct bt_gatt_attr *attr) if (bt_uuid_cmp(attr->uuid, BT_UUID_GATT_CHRC) == 0) { struct bt_gatt_chrc *chrc = attr->user_data; - assert(chrc); + BT_LE_ASSERT(chrc); handle = chrc->value_handle; if (handle == 0) { @@ -267,7 +267,7 @@ struct bt_gatt_attr *bt_gatt_attr_next(const struct bt_gatt_attr *attr) LOG_DBG("GattAttrNext"); - assert(attr); + BT_LE_ASSERT(attr); handle = bt_gatt_attr_get_handle(attr); bt_gatt_foreach_attr(handle + 1, handle + 1, find_next, &next); @@ -307,7 +307,7 @@ static uint8_t gatt_foreach_iter(const struct bt_gatt_attr *attr, /* Match attribute user_data if set */ if (attr_data && attr_data != attr->user_data) { - LOG_DBG("GattForeachIterMismatchData"); + LOG_DBG("GattForeachIterSkipData"); return BT_GATT_ITER_CONTINUE; } @@ -359,7 +359,7 @@ static void foreach_attr_type_dyndb(uint16_t start_handle, for (size_t i = 0; i < svc->attr_count; i++) { struct bt_gatt_attr *attr = &svc->attrs[i]; - assert(attr->uuid); + BT_LE_ASSERT(attr->uuid); if (gatt_foreach_iter(attr, attr->handle, start_handle, @@ -448,7 +448,7 @@ ssize_t bt_gatt_attr_read(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t buf_len, uint16_t offset, const void *value, uint16_t value_len) { - assert(buf); + BT_LE_ASSERT(buf); LOG_DBG("GattAttrRd[%u][%u][%u]", buf_len, offset, value_len); @@ -471,7 +471,7 @@ int bt_gatt_notify_cb(struct bt_conn *conn, struct bt_gatt_notify_params *params { int err; - assert(params); + BT_LE_ASSERT(params); LOG_DBG("GattNtfCb[%u]", params->len); @@ -486,10 +486,8 @@ int bt_gatt_notify_cb(struct bt_conn *conn, struct bt_gatt_notify_params *params err = bt_le_nimble_gatts_notify(conn, params); #endif - /* gatts_notify is synchronous (mbuf-copy + dispatch on return); fire the - * caller's completion cb here so state machines like PACS_FLAG_NOTIFY_RDY - * advance. */ - if (err == 0 && params->func != NULL) { + /* Sync notify: complete cb only when conn is known (not broadcast-all). */ + if (err == 0 && params->func != NULL && conn != NULL) { params->func(conn, params->user_data); } @@ -499,8 +497,8 @@ int bt_gatt_notify_cb(struct bt_conn *conn, struct bt_gatt_notify_params *params _LIB_IDF int bt_gatt_indicate(struct bt_conn *conn, struct bt_gatt_indicate_params *params) { - assert(params); - assert(params->attr && params->attr->uuid); + BT_LE_ASSERT(params); + BT_LE_ASSERT(params->attr && params->attr->uuid); LOG_DBG("GattInd[%s]", bt_uuid_str(params->attr->uuid)); @@ -534,7 +532,7 @@ ssize_t bt_gatt_attr_read_service(struct bt_conn *conn, { struct bt_uuid *uuid = attr->user_data; - assert(uuid); + BT_LE_ASSERT(uuid); LOG_DBG("GattAttrRdSvc[%s]", bt_uuid_str(uuid)); @@ -545,7 +543,7 @@ ssize_t bt_gatt_attr_read_service(struct bt_conn *conn, } /* BT_UUID_TYPE_32 is not expected for service UUIDs in LE Audio */ - assert(uuid->type == BT_UUID_TYPE_128); + BT_LE_ASSERT(uuid->type == BT_UUID_TYPE_128); return bt_gatt_attr_read(conn, attr, buf, len, offset, BT_UUID_128(uuid)->val, 16); } @@ -581,8 +579,8 @@ ssize_t bt_gatt_attr_read_included(struct bt_conn *conn, uint16_t handle; incl = attr->user_data; - assert(incl); - assert(incl->user_data); + BT_LE_ASSERT(incl); + BT_LE_ASSERT(incl->user_data); handle = bt_gatt_attr_get_handle(incl); uuid = incl->user_data; @@ -618,8 +616,8 @@ ssize_t bt_gatt_attr_read_chrc(struct bt_conn *conn, struct gatt_chrc pdu; uint8_t value_len; - assert(chrc); - assert(chrc->uuid); + BT_LE_ASSERT(chrc); + BT_LE_ASSERT(chrc->uuid); pdu.properties = chrc->properties; pdu.value_handle = sys_cpu_to_le16(bt_gatt_attr_value_handle(attr)); @@ -661,7 +659,7 @@ static struct bt_gatt_ccc_cfg *gatts_find_free_ccc_cfg(struct bt_gatt_ccc_manage static struct bt_gatt_ccc_cfg *gatts_find_ccc_cfg_by_conn(const struct bt_conn *conn, struct bt_gatt_ccc_managed_user_data *ccc) { - assert(conn); + BT_LE_ASSERT(conn); for (size_t i = 0; i < ccc->cfg_count; i++) { struct bt_gatt_ccc_cfg *cfg = &ccc->cfg[i]; @@ -784,10 +782,10 @@ ssize_t bt_gatt_attr_read_ccc(struct bt_conn *conn, const struct bt_gatt_ccc_cfg *cfg; uint16_t value; - assert(attr); + BT_LE_ASSERT(attr); ccc = attr->user_data; - assert(ccc); + BT_LE_ASSERT(ccc); LOG_DBG("GattAttrRdCcc[%u][%u]", len, offset); @@ -814,10 +812,10 @@ ssize_t bt_gatt_attr_write_ccc(struct bt_conn *conn, bool new_entry; uint16_t value; - assert(attr); + BT_LE_ASSERT(attr); ccc = attr->user_data; - assert(ccc); + BT_LE_ASSERT(ccc); LOG_DBG("GattAttrWrCcc[%u][%u][%02x]", len, offset, flags); @@ -910,8 +908,8 @@ bool bt_gatt_is_subscribed(struct bt_conn *conn, { const struct bt_gatt_ccc_managed_user_data *ccc; - assert(conn); - assert(attr && attr->uuid); + BT_LE_ASSERT(conn); + BT_LE_ASSERT(attr && attr->uuid); LOG_DBG("GattIsSub[%04x][%s]", ccc_type, bt_uuid_str(attr->uuid)); @@ -950,7 +948,7 @@ bool bt_gatt_is_subscribed(struct bt_conn *conn, } attr = bt_gatt_attr_next(attr); - assert(attr && attr->uuid); + BT_LE_ASSERT(attr && attr->uuid); LOG_DBG("GattIsSubAttr[%s][%d]", bt_uuid_str(attr->uuid), __LINE__); } @@ -962,7 +960,7 @@ bool bt_gatt_is_subscribed(struct bt_conn *conn, return false; } - assert(attr->uuid); + BT_LE_ASSERT(attr->uuid); LOG_DBG("GattIsSubAttr[%s][%d]", bt_uuid_str(attr->uuid), __LINE__); } @@ -978,7 +976,7 @@ bool bt_gatt_is_subscribed(struct bt_conn *conn, } } - assert(attr->uuid); + BT_LE_ASSERT(attr->uuid); LOG_DBG("GattIsSubAttr[%s][%d]", bt_uuid_str(attr->uuid), __LINE__); @@ -987,7 +985,7 @@ bool bt_gatt_is_subscribed(struct bt_conn *conn, } ccc = attr->user_data; - assert(ccc); + BT_LE_ASSERT(ccc); /* Check if the connection is subscribed */ for (size_t i = 0; i < ccc->cfg_count; i++) { @@ -1013,7 +1011,7 @@ static int gatts_ccc_cfg_update(struct bt_conn *conn, bool value_changed; ccc = attr->user_data; - assert(ccc); + BT_LE_ASSERT(ccc); /* LOG_DBG("GattsCccCfgUpd[%04x]", value); */ @@ -1130,10 +1128,10 @@ int bt_gattc_disc_start_safe(uint16_t conn_handle) _LIB_ONLY int bt_gatt_discover(struct bt_conn *conn, struct bt_gatt_discover_params *params) { - assert(conn); - assert(params); - assert(params->start_handle && params->end_handle); - assert(params->start_handle <= params->end_handle); + BT_LE_ASSERT(conn); + BT_LE_ASSERT(params); + BT_LE_ASSERT(params->start_handle && params->end_handle); + BT_LE_ASSERT(params->start_handle <= params->end_handle); LOG_DBG("GattDisc[%u][%u]", params->start_handle, params->end_handle); @@ -1276,7 +1274,7 @@ static int gattc_ccc_discover(struct bt_conn *conn, struct bt_gatt_subscribe_par .val = BT_UUID_GATT_CCC_VAL, }; - assert(params->disc_params); + BT_LE_ASSERT(params->disc_params); LOG_DBG("GattcCccDisc[%u][%u]", params->value_handle, params->end_handle); @@ -1310,11 +1308,11 @@ int bt_gatt_subscribe(struct bt_conn *conn, struct bt_gatt_subscribe_params *par bool has_subscription = false; struct gattc_sub *sub; - assert(conn); - assert(params); - assert(params->notify); - assert(params->value); - assert(params->ccc_handle == BT_GATT_AUTO_DISCOVER_CCC_HANDLE || + BT_LE_ASSERT(conn); + BT_LE_ASSERT(params); + BT_LE_ASSERT(params->notify); + BT_LE_ASSERT(params->value); + BT_LE_ASSERT(params->ccc_handle == BT_GATT_AUTO_DISCOVER_CCC_HANDLE || (params->end_handle && params->disc_params)); LOG_DBG("GattcSub[%u][%u][%u][%04x]", @@ -1369,6 +1367,14 @@ int bt_gatt_subscribe(struct bt_conn *conn, struct bt_gatt_subscribe_params *par params->value_handle = 0; /* unlinked: clear retry guard */ return err; } + + /* Adapter may have returned 0 while leaving ccc unresolved + * (cb got attr==NULL). Do not append a dead subscription. */ + if (params->ccc_handle == BT_GATT_AUTO_DISCOVER_CCC_HANDLE) { + LOG_ERR("GattcSubCccUnresolved[%u]", params->value_handle); + params->value_handle = 0; + return -ENOENT; + } } else { /* Note: * If the CCC handle is auto-discovered, and during the @@ -1403,8 +1409,8 @@ int bt_gatt_unsubscribe(struct bt_conn *conn, struct bt_gatt_subscribe_params *p bool found = false; int err = 0; - assert(conn); - assert(params); + BT_LE_ASSERT(conn); + BT_LE_ASSERT(params); LOG_DBG("GattcUnsub[%04x][%u][%u][%u]", params->value, params->value_handle, params->ccc_handle, params->end_handle); @@ -1469,10 +1475,8 @@ int bt_gatt_unsubscribe(struct bt_conn *conn, struct bt_gatt_subscribe_params *p LOG_DBG("GattcUnsub[%u][%d]", has_subscription, err); - if (has_subscription || err) { - /* Notify with NULL data to complete unsubscribe */ - params->notify(conn, params, NULL, 0); - } + /* Notify with NULL data to complete unsubscribe */ + params->notify(conn, params, NULL, 0); return 0; } @@ -1480,8 +1484,8 @@ int bt_gatt_unsubscribe(struct bt_conn *conn, struct bt_gatt_subscribe_params *p _LIB_IDF int bt_gatt_read(struct bt_conn *conn, struct bt_gatt_read_params *params) { - assert(conn); - assert(params); + BT_LE_ASSERT(conn); + BT_LE_ASSERT(params); LOG_DBG("GattRd[%u]", params->handle_count); @@ -1496,7 +1500,7 @@ int bt_gatt_read(struct bt_conn *conn, struct bt_gatt_read_params *params) } if (params->handle_count == 0) { - assert(params->by_uuid.uuid); + BT_LE_ASSERT(params->by_uuid.uuid); if (params->by_uuid.uuid->type != BT_UUID_TYPE_16) { LOG_ERR("GattRdInvUuid[%s]", bt_uuid_str(params->by_uuid.uuid)); @@ -1514,11 +1518,11 @@ int bt_gatt_read(struct bt_conn *conn, struct bt_gatt_read_params *params) _LIB_IDF int bt_gatt_write(struct bt_conn *conn, struct bt_gatt_write_params *params) { - assert(conn); - assert(params); - assert(params->func); - assert(params->handle); - assert((!params->data ^ !params->length) == 0); + BT_LE_ASSERT(conn); + BT_LE_ASSERT(params); + BT_LE_ASSERT(params->func); + BT_LE_ASSERT(params->handle); + BT_LE_ASSERT((!params->data ^ !params->length) == 0); LOG_DBG("GattWr[%u][%u][%u]", params->handle, params->length, params->offset); @@ -1545,9 +1549,9 @@ int bt_gatt_write_without_response_cb(struct bt_conn *conn, uint16_t handle, bool sign, bt_gatt_complete_func_t func, void *user_data) { - assert(conn); - assert(handle); - assert((!data ^ !length) == 0); + BT_LE_ASSERT(conn); + BT_LE_ASSERT(handle); + BT_LE_ASSERT((!data ^ !length) == 0); ARG_UNUSED(func); ARG_UNUSED(user_data); @@ -1629,7 +1633,7 @@ void bt_le_acl_conn_bond_deleted_gatt_listener(uint8_t id, const bt_addr_le_t *p void bt_le_gatt_handle_event(uint8_t *data, size_t data_len) { - assert(data && data_len); + BT_LE_ASSERT(data && data_len); #if CONFIG_BT_BLUEDROID_ENABLED bt_le_bluedroid_gatt_handle_event(data, data_len); @@ -1637,3 +1641,34 @@ void bt_le_gatt_handle_event(uint8_t *data, size_t data_len) bt_le_nimble_gatt_handle_event(data, data_len); #endif } + +void bt_le_gatt_event_free(void *data) +{ + struct bt_le_gatt_event_param *qev = data; + + if (qev == NULL) { + return; + } + + switch (qev->type) { + case BT_LE_GATTC_NOTIFY_RX_EVENT: + if (qev->gattc_notify_rx.value) { + free(qev->gattc_notify_rx.value); + } + break; + case BT_LE_GATTC_READ_CHRC_EVENT: + if (qev->gattc_read_chrc.value) { + free(qev->gattc_read_chrc.value); + } + break; + case BT_LE_GATTS_WRITE_EVENT: + if (qev->gatts_write.value) { + free(qev->gatts_write.value); + } + break; + default: + break; + } + + free(qev); +} diff --git a/components/bt/esp_ble_iso/host/common/hci.c b/components/bt/esp_ble_iso/host/common/hci.c index e897997ebab..baa59c9a711 100644 --- a/components/bt/esp_ble_iso/host/common/hci.c +++ b/components/bt/esp_ble_iso/host/common/hci.c @@ -15,6 +15,8 @@ #include #include #include +#include +#include #include <../host/hci_core.h> #include <../host/iso_internal.h> @@ -23,6 +25,24 @@ LOG_MODULE_REGISTER(ISO_HCI, CONFIG_BT_ISO_LOG_LEVEL); +/* Device/HCI state, not an ISO packet, task-context -> eligible for PSRAM. */ +BT_ISO_EXT_RAM_BSS_ATTR struct bt_dev bt_dev; + +uint8_t bt_get_phy(uint8_t hci_phy) +{ + switch (hci_phy) { + case BT_HCI_LE_PHY_1M: + return BT_GAP_LE_PHY_1M; + case BT_HCI_LE_PHY_2M: + return BT_GAP_LE_PHY_2M; + case BT_HCI_LE_PHY_CODED: + return BT_GAP_LE_PHY_CODED; + default: + LOG_WRN("UnknownHciPhy[%u]", hci_phy); + return 0; + } +} + struct cmd_data { /** HCI status of the command completion */ uint8_t status; @@ -34,7 +54,7 @@ struct cmd_data { struct bt_hci_cmd_state_set *state; }; -static struct cmd_data cmd_data; +static BT_ISO_EXT_RAM_BSS_ATTR struct cmd_data cmd_data; #define cmd(buf) (&cmd_data) @@ -49,7 +69,7 @@ struct net_buf *bt_hci_cmd_create(uint16_t opcode, uint8_t param_len) LOG_DBG("HciCmdCreate[%04x][%u]", opcode, param_len); buf = net_buf_alloc(&hci_cmd_pool, K_NO_WAIT); - assert(buf); + BT_LE_ASSERT(buf); net_buf_reserve(buf, BT_BUF_RESERVE); diff --git a/components/bt/esp_ble_iso/host/common/host.c b/components/bt/esp_ble_iso/host/common/host.c index 127542a3ec7..6b3185eb8e3 100644 --- a/components/bt/esp_ble_iso/host/common/host.c +++ b/components/bt/esp_ble_iso/host/common/host.c @@ -13,18 +13,27 @@ #include #include +#include + +#include <../host/conn_internal.h> + #include "common/host.h" +#include "common/conn.h" #include "common/app/gap.h" #include "common/app/gatt.h" #if CONFIG_BT_BLUEDROID_ENABLED #include "bluedroid/gap.h" #include "bluedroid/gatt.h" +#else +#include "nimble/gatt.h" #endif LOG_MODULE_REGISTER(ISO_HOST, CONFIG_BT_ISO_LOG_LEVEL); -static struct k_mutex host_mutex; +static BT_ISO_CTRL_BSS_ATTR struct k_mutex host_mutex; + +extern struct bt_conn iso_conns[CONFIG_BT_ISO_MAX_CHAN]; #if HOST_LOCK_DEBUG void bt_le_host_lock_debug(const char *func, int line) @@ -38,7 +47,7 @@ void bt_le_host_lock(void) if (err) { /* K_MUTEX_SHORT wait failed: the host stack is wedged. k_mutex_lock has * already logged self/holder task names. Use libc abort() rather - * than assert(0) — assert is a no-op under NDEBUG, which would + * than BT_LE_ASSERT(0) — assert is a no-op under NDEBUG, which would * let the caller enter the critical section without the mutex * held and cause races. abort() halts in every build. */ @@ -67,6 +76,32 @@ void bt_le_host_unlock(void) k_mutex_unlock(&host_mutex); } +int bt_le_host_check_idle(void) +{ + struct bt_iso_chan *chan; + size_t busy = 0; + + /* Only what ISO created; the application's adv sets, sync and ACL are + * dropped by bt_le_host_deinit() rather than blocked on. Counts every + * offender so one attempt tells the caller the whole list. */ + + bt_le_host_lock(); + + for (size_t i = 0; i < ARRAY_SIZE(iso_conns); i++) { + chan = iso_conns[i].iso.chan; + if (chan && chan->state != BT_ISO_STATE_DISCONNECTED) { + LOG_ERR("DeinitBusyIsoChan[%u][state=%u]", i, chan->state); + busy++; + } + } + + busy += bt_le_iso_report_busy(); + + bt_le_host_unlock(); + + return busy ? -EBUSY : 0; +} + int bt_le_host_init(void) { int err; @@ -75,33 +110,35 @@ int bt_le_host_init(void) k_mutex_create(&host_mutex); + bt_le_conn_reset(); + + bt_le_iso_state_reset(); + err = bt_le_scan_init(); if (err) { goto delete_mutex; } -#if CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT - err = bt_le_l2cap_init(); - if (err) { - goto deinit_scan; - } -#endif /* CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT */ - #if CONFIG_BT_BLUEDROID_ENABLED err = bt_le_bluedroid_gap_init(); if (err) { - goto deinit_l2cap; + goto deinit_scan; } err = bt_le_bluedroid_gatt_init(); if (err) { - goto deinit_bluedroid_gatt; + goto deinit_gatt; } +#else + /* nimble: reset the per-conn GATT cache/NRP arrays (now .bss/PSRAM, no + * static init) so this init - and any later deinit/re-init - starts clean. */ + bt_le_nimble_gattc_db_init(); + bt_le_nimble_gatt_nrp_init(); #endif /* CONFIG_BT_BLUEDROID_ENABLED */ err = bt_le_iso_init(); if (err) { - goto deinit_bluedroid_gatt; + goto deinit_gatt; } err = bt_le_iso_task_init(); @@ -113,15 +150,11 @@ int bt_le_host_init(void) deinit_iso: bt_le_iso_deinit(); -deinit_bluedroid_gatt: +deinit_gatt: #if CONFIG_BT_BLUEDROID_ENABLED bt_le_bluedroid_gatt_deinit(); -deinit_l2cap: +deinit_scan: #endif /* CONFIG_BT_BLUEDROID_ENABLED */ -#if CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT - bt_le_l2cap_deinit(); -deinit_scan: /* only reachable when OTS path is compiled in */ -#endif /* CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT */ bt_le_scan_deinit(); delete_mutex: k_mutex_delete(&host_mutex); @@ -129,19 +162,33 @@ delete_mutex: return err; } -void bt_le_host_deinit(void) +int bt_le_host_deinit(void) { + int err; + LOG_DBG("HostDeinit"); - bt_le_iso_task_deinit(); + /* Everything below frees state the task dispatches into, so bail out while + * it is still alive rather than free underneath it. */ + err = bt_le_iso_task_deinit(); + if (err) { + return err; + } + bt_le_iso_deinit(); #if CONFIG_BT_BLUEDROID_ENABLED + /* No gap_deinit: BTM_BleGapRegisterCallback refuses NULL, so the callback + * stays. Harmless - task_post rejects once the task is gone. */ bt_le_bluedroid_gatt_deinit(); +#else + bt_le_nimble_gattc_db_deinit(); + bt_le_nimble_gatt_nrp_deinit(); #endif /* CONFIG_BT_BLUEDROID_ENABLED */ -#if CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT - bt_le_l2cap_deinit(); -#endif /* CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT */ bt_le_scan_deinit(); + /* Last: iso_task has exited and the callbacks above are gone, so nothing + * can take the mutex any more. */ k_mutex_delete(&host_mutex); + + return 0; } diff --git a/components/bt/esp_ble_iso/host/common/include/common/adv.h b/components/bt/esp_ble_iso/host/common/include/common/adv.h index fdaf9d66ae7..b519e58a1e0 100644 --- a/components/bt/esp_ble_iso/host/common/include/common/adv.h +++ b/components/bt/esp_ble_iso/host/common/include/common/adv.h @@ -22,6 +22,8 @@ int bt_le_ext_adv_new_safe(uint8_t adv_handle, uint8_t addr_type, int bt_le_ext_adv_delete_safe(uint8_t adv_handle); +void bt_le_ext_adv_state_reset(void); + #ifdef __cplusplus } #endif diff --git a/components/bt/esp_ble_iso/host/common/include/common/app/gap.h b/components/bt/esp_ble_iso/host/common/include/common/app/gap.h index b864d065f60..6c42a0869d7 100644 --- a/components/bt/esp_ble_iso/host/common/include/common/app/gap.h +++ b/components/bt/esp_ble_iso/host/common/include/common/app/gap.h @@ -21,6 +21,30 @@ extern "C" { #endif +/* Byte order of `val` is the active host's, NOT a single fixed convention: + * + * NimBLE on-air / LSB-first, val[0] is the least significant octet. + * Bluedroid MSB-first, val[0] is the most significant octet - the order + * esp_bd_addr_t uses and the order every esp_ble_gap_* API expects. + * + * That is deliberate: an address delivered by an event is normally handed + * straight back to the same host's API (create sync, connect, disconnect), so + * leaving it untouched keeps those paths correct on both hosts. + * + * The cost is that `bt_addr_le_t` in this port does not mean one thing either. + * On Bluedroid its contents follow whichever path filled it: + * + * conn->le.dst MSB - stored verbatim from the connect event and + * passed back to BTA_GATTC_* unchanged. + * bt_bond_info.addr MSB - read from the Bluedroid bond store as is. + * per_adv_sync->addr LSB - bt_le_per_adv_sync_new() reverses it, because + * it is compared against addresses off the air. + * BASS recv_state->addr LSB - arrives over ATT, where BASS defines LSB-first. + * + * So an application only has to convert where the two meet: comparing an event + * address against one the audio layer holds (a Broadcast Receive State, say) + * needs a reversal under CONFIG_BT_BLUEDROID_ENABLED and none under NimBLE. + */ struct bt_le_addr { uint8_t type; uint8_t val[6]; @@ -57,6 +81,9 @@ struct bt_le_gap_app_pa_sync_past_param { uint8_t adv_phy; uint16_t per_adv_itvl; uint8_t adv_ca; + /* PAST sender, not the advertiser in addr above; conn_handle is resolved + * from it on Bluedroid. */ + struct bt_le_addr src_addr; /* ACL conn that delivered the PAST. */ uint16_t conn_handle; }; @@ -300,7 +327,9 @@ void bt_le_gap_app_biginfo_event(uint8_t *param); void bt_le_gap_handle_event(uint8_t *data, size_t data_len); -void bt_le_gap_app_post_event(uint8_t type, void *param); +void bt_le_gap_event_free(void *data); + +void bt_le_gap_app_post_event(uint16_t type, void *param); #ifdef __cplusplus } diff --git a/components/bt/esp_ble_iso/host/common/include/common/conn.h b/components/bt/esp_ble_iso/host/common/include/common/conn.h index a456c47cb09..5659a8b7ecc 100644 --- a/components/bt/esp_ble_iso/host/common/include/common/conn.h +++ b/components/bt/esp_ble_iso/host/common/include/common/conn.h @@ -30,15 +30,20 @@ extern "C" { void bt_conn_get_acl_conns(struct bt_conn **conns, uint8_t *count); -struct bt_conn *bt_le_acl_conn_find(uint16_t conn_handle); +size_t bt_le_acl_conn_count(void); -struct bt_conn *bt_le_acl_conn_find_safe(uint16_t conn_handle); +void bt_le_conn_reset(void); + +struct bt_conn *bt_le_acl_conn_find(uint16_t conn_handle); int bt_le_acl_conn_new(uint16_t conn_handle, uint8_t role, bt_addr_le_t *dst, uint8_t sec_level); +int bt_le_acl_conn_new_safe(uint16_t conn_handle, uint8_t role, uint8_t addr_type, + const uint8_t *addr, uint8_t sec_level); + int bt_le_acl_conn_delete(uint16_t conn_handle); void bt_conn_le_set_ltk(struct bt_conn *conn, const uint8_t *ltk); diff --git a/components/bt/esp_ble_iso/host/common/include/common/gatt.h b/components/bt/esp_ble_iso/host/common/include/common/gatt.h index d4c2aaabc35..b020603902f 100644 --- a/components/bt/esp_ble_iso/host/common/include/common/gatt.h +++ b/components/bt/esp_ble_iso/host/common/include/common/gatt.h @@ -72,12 +72,24 @@ struct bt_le_gattc_notify_rx_event { uint8_t *value; }; +/* Neither adapter allocates a buffer for a zero-length notification, but NULL data is + * how gatt.c completes an unsubscribe: a lib notify handler that sees it drops its + * subscription. A zero-length notification is a real PDU (BASS sends one for an emptied + * Broadcast Receive State), so keep the pointer non-NULL when handing it to the lib. + */ +#define NOTIFY_VALUE(_event) \ + ((const void *)((_event)->value != NULL ? (const uint8_t *)(_event)->value \ + : (const uint8_t *)"")) + struct bt_le_gatts_notify_tx_event { bool is_notify; uint16_t conn_handle; uint16_t conn_id; uint16_t attr_handle; - uint8_t status; + /* int, not uint8_t: NimBLE reports BLE_HS_ERR_ATT_BASE(0x100)+att_code for a + * failed indication; uint8_t truncates 0x10e -> 0x0e == BLE_HS_EDONE (success), + * masking the failure as confirmation. */ + int status; }; /* Bluedroid-side adapter events. NimBLE produces ACL connect/disconnect via @@ -294,6 +306,8 @@ void bt_le_acl_conn_bond_deleted_gatt_listener(uint8_t id, const bt_addr_le_t *p void bt_le_gatt_handle_event(uint8_t *data, size_t data_len); +void bt_le_gatt_event_free(void *data); + #ifdef __cplusplus } #endif diff --git a/components/bt/esp_ble_iso/host/common/include/common/host.h b/components/bt/esp_ble_iso/host/common/include/common/host.h index 80b4bc15431..bf83a07e1c2 100644 --- a/components/bt/esp_ble_iso/host/common/include/common/host.h +++ b/components/bt/esp_ble_iso/host/common/include/common/host.h @@ -14,10 +14,13 @@ #include #include +#include "utils/assert.h" +#include "utils/iso_attr.h" +#include "utils/mem.h" + #include "common/adv.h" #include "common/conn.h" #include "common/iso.h" -#include "common/l2cap.h" #include "common/scan.h" #include "common/gatt.h" #include "common/task.h" @@ -41,7 +44,9 @@ void bt_le_host_unlock(void); int bt_le_host_init(void); -void bt_le_host_deinit(void); +int bt_le_host_check_idle(void); + +int bt_le_host_deinit(void); #ifdef __cplusplus } diff --git a/components/bt/esp_ble_iso/host/common/include/common/iso.h b/components/bt/esp_ble_iso/host/common/include/common/iso.h index 0e2d81c1fc7..61a8fe5d041 100644 --- a/components/bt/esp_ble_iso/host/common/include/common/iso.h +++ b/components/bt/esp_ble_iso/host/common/include/common/iso.h @@ -28,6 +28,7 @@ struct net_buf; struct bt_le_iso_cb { void (*cis_dis)(struct net_buf *buf); void (*cis_est)(struct net_buf *buf); + void (*cis_est_v2)(struct net_buf *buf); void (*cis_req)(struct net_buf *buf); void (*big_create)(struct net_buf *buf); void (*big_term)(struct net_buf *buf); @@ -74,6 +75,10 @@ int bt_le_iso_rx(const uint8_t *data, uint16_t len, void *arg); int bt_le_iso_disconnect(uint16_t conn_handle, uint8_t reason); +void bt_le_iso_state_reset(void); + +size_t bt_le_iso_report_busy(void); + int bt_le_iso_init(void); void bt_le_iso_deinit(void); diff --git a/components/bt/esp_ble_iso/host/common/include/common/l2cap.h b/components/bt/esp_ble_iso/host/common/include/common/l2cap.h deleted file mode 100644 index f071ab4afdd..00000000000 --- a/components/bt/esp_ble_iso/host/common/include/common/l2cap.h +++ /dev/null @@ -1,64 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2015-2016 Intel Corporation - * SPDX-FileCopyrightText: 2023 Nordic Semiconductor - * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef HOST_COMMON_L2CAP_H_ -#define HOST_COMMON_L2CAP_H_ - -#include - -#include "sdkconfig.h" - -#if CONFIG_BT_BLUEDROID_ENABLED -/* TODO */ -#else -#include "nimble/l2cap.h" -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -#define L2CAP_LE_SUCCESS 0x0000 -#define L2CAP_LE_ERR_PSM_NOT_SUPP 0x0002 -#define L2CAP_LE_ERR_NO_RESOURCES 0x0004 -#define L2CAP_LE_ERR_AUTHENTICATION 0x0005 -#define L2CAP_LE_ERR_AUTHORIZATION 0x0006 -#define L2CAP_LE_ERR_KEY_SIZE 0x0007 -#define L2CAP_LE_ERR_ENCRYPTION 0x0008 -#define L2CAP_LE_ERR_INVALID_SCID 0x0009 -#define L2CAP_LE_ERR_SCID_IN_USE 0x000A -#define L2CAP_LE_ERR_UNACCEPT_PARAMS 0x000B -#define L2CAP_LE_ERR_INVALID_PARAMS 0x000C - -#define L2CAP_LE_OTS_PSM 0x0025 -#define L2CAP_LE_OTS_MTU MIN(CONFIG_BT_OTS_L2CAP_CHAN_TX_MTU, \ - CONFIG_BT_OTS_L2CAP_CHAN_RX_MTU) - -int bt_le_l2cap_accept(uint16_t conn_handle, uint16_t psm, - uint16_t scid, uint16_t mtu, - uint16_t mps, uint16_t credits, - uint16_t *result); - -void bt_le_l2cap_connected(uint16_t conn_handle, uint16_t psm, - uint16_t tx_cid, uint16_t tx_mtu, - uint16_t rx_cid, uint16_t rx_mtu); - -void bt_le_l2cap_disconnected(uint16_t conn_handle, uint16_t psm); - -void bt_le_l2cap_received(uint16_t conn_handle, uint16_t psm, - uint8_t *data, uint16_t len); - -int bt_le_l2cap_init(void); - -void bt_le_l2cap_deinit(void); - -#ifdef __cplusplus -} -#endif - -#endif /* HOST_COMMON_L2CAP_H_ */ diff --git a/components/bt/esp_ble_iso/host/common/include/common/scan.h b/components/bt/esp_ble_iso/host/common/include/common/scan.h index 2a1d7b13edf..e58b2e8ea97 100644 --- a/components/bt/esp_ble_iso/host/common/include/common/scan.h +++ b/components/bt/esp_ble_iso/host/common/include/common/scan.h @@ -35,6 +35,8 @@ void bt_le_scan_recv_listener(uint16_t event_type, uint8_t data_len, uint8_t *data); +struct bt_le_per_adv_sync *bt_le_per_adv_sync_find(uint16_t sync_handle); + struct bt_le_per_adv_sync *bt_le_per_adv_sync_find_safe(uint16_t sync_handle); int bt_le_per_adv_sync_new(uint16_t sync_handle, @@ -46,6 +48,15 @@ int bt_le_per_adv_sync_new(uint16_t sync_handle, uint16_t conn_handle, struct bt_le_per_adv_sync **out_sync); +int bt_le_per_adv_sync_new_safe(uint16_t sync_handle, + uint8_t sid, + uint8_t phy, + uint16_t interval, + uint8_t addr_type, + const uint8_t addr[6], + uint16_t conn_handle, + struct bt_le_per_adv_sync **out_sync); + int bt_le_per_adv_sync_delete(uint16_t sync_handle); int bt_le_per_adv_sync_establish_listener(uint16_t sync_handle); @@ -60,6 +71,8 @@ int bt_le_scan_init(void); void bt_le_scan_deinit(void); +void bt_le_per_adv_sync_state_reset(void); + #ifdef __cplusplus } #endif diff --git a/components/bt/esp_ble_iso/host/common/include/common/task.h b/components/bt/esp_ble_iso/host/common/include/common/task.h index 5a3a8dfc395..7cd25f04c33 100644 --- a/components/bt/esp_ble_iso/host/common/include/common/task.h +++ b/components/bt/esp_ble_iso/host/common/include/common/task.h @@ -22,30 +22,52 @@ extern "C" { #if CONFIG_BT_BLUEDROID_ENABLED #if CONFIG_BT_BLUEDROID_PINNED_TO_CORE -#define ISO_TASK_CORE CONFIG_BT_BLUEDROID_PINNED_TO_CORE +#define ISO_TASK_CORE CONFIG_BT_BLUEDROID_PINNED_TO_CORE #else /* CONFIG_BT_BLUEDROID_PINNED_TO_CORE */ -#define ISO_TASK_CORE (0) +#define ISO_TASK_CORE (0) #endif /* CONFIG_BT_BLUEDROID_PINNED_TO_CORE */ #else /* CONFIG_BT_BLUEDROID_ENABLED */ #if CONFIG_BT_NIMBLE_PINNED_TO_CORE -#define ISO_TASK_CORE CONFIG_BT_NIMBLE_PINNED_TO_CORE +#define ISO_TASK_CORE CONFIG_BT_NIMBLE_PINNED_TO_CORE #else /* CONFIG_BT_NIMBLE_PINNED_TO_CORE */ -#define ISO_TASK_CORE (0) +#define ISO_TASK_CORE (0) #endif /* CONFIG_BT_NIMBLE_PINNED_TO_CORE */ #endif /* CONFIG_BT_BLUEDROID_ENABLED */ -#define ISO_TASK_STACK_SIZE 4096 -#define ISO_TASK_NAME "iso_task" +#define ISO_TASK_STACK_SIZE 4096 +#define ISO_TASK_NAME "iso_task" /* Ref: - * - Bluedroid BTC task: configMAX_PRIORITIES - 6 - * - Bluedroid BTU task: configMAX_PRIORITIES - 5 - * - NimBLE Host task: configMAX_PRIORITIES - 4 + * - Bluedroid hci_layer task: configMAX_PRIORITIES - 3 + * - NimBLE Host task: configMAX_PRIORITIES - 4 + * - Bluedroid BTU task: configMAX_PRIORITIES - 5 + * - Bluedroid BTC task: configMAX_PRIORITIES - 6 + * + * MUST stay strictly above BTU. Both tasks post to Bluedroid's single shared + * downstream event, and osi_thread_post_event() keeps OSI_EVENT_FLAG_POSTING + * set across its osi_thread_post() — which yields to the higher-priority + * hci_layer task — until the poster is rescheduled. Any post landing in that + * window is rejected, and the ACL path discards the result, stranding the + * packet until someone else posts (measured at 56.6 s). At equal priority the + * round-robin lets BTU run inside our window and lose a notification that way; + * one rung above BTU, BTU cannot be scheduled until we have cleared POSTING, + * so it can never be the victim. Same core is a precondition — see + * ISO_TASK_CORE above. The reverse (BTU's window rejecting our command) stays + * possible and is covered by the kick loop in adapter/bluedroid/hci.c. */ -#if CONFIG_BT_BLUEDROID_ENABLED -#define ISO_TASK_PRIO (configMAX_PRIORITIES - 5) -#else -#define ISO_TASK_PRIO (configMAX_PRIORITIES - 4) -#endif +#define ISO_TASK_PRIO (configMAX_PRIORITIES - 4) + +/* The other, weaker mitigation for the same lost-wakeup race: re-post the + * shared downstream event after every HCI command we send, hoping to flush a + * packet whose own post was rejected inside our POSTING window. + * + * Off because the priority above supersedes it, and once that holds it cannot + * help in either direction: BTU never runs inside our window, so there is no + * stranded ACL packet to flush; and if BTU's window rejected our command, BTU + * is not scheduled between our two posts either, so the re-post is rejected + * with it. Measured ineffective on hardware before the priority change too. + * Enable only together with lowering ISO_TASK_PRIO back to BTU's level. + * Mechanism: adapter/bluedroid/hci.c. */ +#define ISO_HCI_DRAIN_DOWNSTREAM 0 /* Ordered by priority tier, highest first. The numeric value is not used for * prioritization (that comes from which queue bt_le_iso_task_post routes to); @@ -60,6 +82,7 @@ enum iso_queue_item_type { ISO_QUEUE_ITEM_TYPE_TIMER_EVENT, ISO_QUEUE_ITEM_TYPE_GAP_EVENT, ISO_QUEUE_ITEM_TYPE_GATT_EVENT, + ISO_QUEUE_ITEM_TYPE_L2CAP_EVENT, ISO_QUEUE_ITEM_TYPE_ISO_HCI_EVENT, /* floodable: high-volume best-effort reports, posted non-blocking, newest @@ -93,10 +116,15 @@ struct iso_queue_item { #define ISO_CRITICAL_QUEUE_LEN 32 #define ISO_NORMAL_QUEUE_LEN 64 #define ISO_FLOODABLE_QUEUE_LEN 32 -/* The set must be able to hold one token per item across all three queues. */ +/* Not a tier. One-deep and written only by deinit, so the stop signal can never + * hit a full queue - which is what lets the task block on the set indefinitely + * instead of polling for iso_task_stopping. */ +#define ISO_CTRL_QUEUE_LEN 1 +/* The set must be able to hold one token per item across all queues. */ #define ISO_QUEUE_SET_LEN (ISO_CRITICAL_QUEUE_LEN + \ ISO_NORMAL_QUEUE_LEN + \ - ISO_FLOODABLE_QUEUE_LEN) + ISO_FLOODABLE_QUEUE_LEN + \ + ISO_CTRL_QUEUE_LEN) #define ISO_QUEUE_ITEM_SIZE sizeof(struct iso_queue_item) #if CONFIG_BT_ISO_DISPATCH_MONITOR @@ -109,9 +137,21 @@ void bt_le_iso_dispatch_stats_dump(void); int bt_le_iso_task_post(enum iso_queue_item_type type, void *data, size_t data_len); +/* -ESHUTDOWN is expected for as long as an ACL, scan or PA sync outlives the + * deinit that stopped it; anything else is a real drop. A macro so each file + * logs under its own LOG_MODULE_REGISTER. */ +#define ISO_POST_FAIL_LOG(_err, ...) \ + do { \ + if ((_err) == -ESHUTDOWN) { \ + LOG_DBG(__VA_ARGS__); \ + } else { \ + LOG_ERR(__VA_ARGS__); \ + } \ + } while (0) + int bt_le_iso_task_init(void); -void bt_le_iso_task_deinit(void); +int bt_le_iso_task_deinit(void); #ifdef __cplusplus } diff --git a/components/bt/esp_ble_iso/host/common/iso.c b/components/bt/esp_ble_iso/host/common/iso.c index 31e1dd0cd5d..f127e8cc3a1 100644 --- a/components/bt/esp_ble_iso/host/common/iso.c +++ b/components/bt/esp_ble_iso/host/common/iso.c @@ -44,7 +44,18 @@ LOG_MODULE_REGISTER(ISO_SHIM, CONFIG_BT_ISO_LOG_LEVEL); #define ISO_PKT_COMP_SDU (0b10) #define ISO_PKT_LAST_FRAG (0b11) -static sys_slist_t iso_cbs = SYS_SLIST_STATIC_INIT(&iso_cbs); +/* Both callbacks that feed the iso task from the controller task (iso_tx_comp_cb, + * bt_le_iso_rx) can fail to hand off an item, and neither may log there: + * esp_log_write is a blocking UART write (~3 ms/line at 115200), so reporting at + * the failure rate would spend most of a second inside it and deepen the very + * congestion it reports. They only count; the matching iso-task handler reports + * once every ISO_DROP_REPORT_STEP items, by which point the queue has room again + * so the report never competes with the burst that caused it. Causes are counted + * apart because they need different fixes: a full queue means the iso task is + * behind, a failed alloc means the heap is exhausted. */ +#define ISO_DROP_REPORT_STEP 100 + +static BT_ISO_EXT_RAM_BSS_ATTR sys_slist_t iso_cbs; #if CONFIG_BT_ISO_UNICAST static void hci_le_cis_disconnected(struct net_buf *buf); @@ -60,6 +71,7 @@ static struct bt_le_iso_cb iso_cb = { #if CONFIG_BT_ISO_UNICAST .cis_dis = hci_le_cis_disconnected, .cis_est = hci_le_cis_established, + .cis_est_v2 = hci_le_cis_established_v2, #if CONFIG_BT_ISO_PERIPHERAL .cis_req = hci_le_cis_req, #endif /* CONFIG_BT_ISO_PERIPHERAL */ @@ -135,12 +147,11 @@ static void hci_le_cis_disconnected(struct net_buf *buf) LOG_DBG("CisDisconnectedEvt[%u]", evt->handle); iso = bt_conn_lookup_handle(evt->handle, BT_CONN_TYPE_ISO); - assert(iso); + BT_LE_ASSERT(iso); iso->err = evt->reason; bt_iso_disconnected(iso); - bt_conn_unref(iso); } static int iso_cis_disconn_evt_listener_safe(uint8_t *data) @@ -189,6 +200,27 @@ static int iso_cis_est_evt_listener_safe(uint8_t *data) return 0; } +static int iso_cis_est_evt_v2_listener_safe(uint8_t *data) +{ + struct bt_le_iso_cb *listener = NULL; + struct net_buf buf = {0}; + + LOG_DBG("CisEstEvtV2Listener"); + + bt_le_host_lock(); + + SYS_SLIST_FOR_EACH_CONTAINER(&iso_cbs, listener, node) { + if (listener->cis_est_v2) { + buf.data = data + 1; /* The first octet is subev_code */ + listener->cis_est_v2(&buf); + } + } + + bt_le_host_unlock(); + + return 0; +} + #if CONFIG_BT_ISO_PERIPHERAL static int iso_cis_req_evt_listener_safe(uint8_t *data) { @@ -346,7 +378,7 @@ static void handle_iso_event(uint8_t *data, size_t data_len) #if CONFIG_BT_ISO_UNICAST if (le_meta == false) { - assert(event == BT_HCI_EVT_DISCONN_COMPLETE); + BT_LE_ASSERT(event == BT_HCI_EVT_DISCONN_COMPLETE); iso_cis_disconn_evt_listener_safe(data + 2); return; } @@ -359,6 +391,9 @@ static void handle_iso_event(uint8_t *data, size_t data_len) case BT_HCI_EVT_LE_CIS_ESTABLISHED: iso_cis_est_evt_listener_safe(data + 2); break; + case BT_HCI_EVT_LE_CIS_ESTABLISHED_V2: + iso_cis_est_evt_v2_listener_safe(data + 2); + break; #if CONFIG_BT_ISO_PERIPHERAL case BT_HCI_EVT_LE_CIS_REQ: iso_cis_req_evt_listener_safe(data + 2); @@ -385,14 +420,14 @@ static void handle_iso_event(uint8_t *data, size_t data_len) break; #endif /* CONFIG_BT_ISO_SYNC_RECEIVER */ default: - assert(0); + LOG_ERR("HandleIsoEvtUnknown[%u]", event); break; } } void bt_le_iso_handle_hci_event(uint8_t *data, size_t data_len) { - assert(data && data_len); + BT_LE_ASSERT(data && data_len); handle_iso_event(data, data_len); free(data); } @@ -407,13 +442,13 @@ struct iso_tx_sdu_node { sys_snode_t node; }; -static sys_slist_t iso_tx_sdu_list = SYS_SLIST_STATIC_INIT(&iso_tx_sdu_list); +static BT_ISO_EXT_RAM_BSS_ATTR sys_slist_t iso_tx_sdu_list; static int iso_tx_sdu_insert(struct bt_iso_chan *chan, const uint8_t *sdu, uint16_t sdu_len) { struct iso_tx_sdu_node *sdu_node; - sdu_node = calloc(1, sizeof(*sdu_node)); + sdu_node = bt_le_int_calloc(1, sizeof(*sdu_node)); if (sdu_node == NULL) { LOG_ERR("IsoTxSduInsertNoMem[%u]", sizeof(*sdu_node)); return -ENOMEM; @@ -442,6 +477,15 @@ static void iso_tx_sdu_clear(uint16_t handle, bool is_big) SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&iso_tx_sdu_list, sdu_node, sdu_tmp, node) { iso = sdu_node->chan->iso; + /* Peripheral CIS clears chan->iso during disconnect (before this runs); + * the SDU is orphaned — drop it instead of dereferencing NULL. */ + if (iso == NULL) { + sys_slist_remove(&iso_tx_sdu_list, prev, &sdu_node->node); + free((void *)sdu_node->sdu); + free(sdu_node); + continue; + } + if ((is_big == false && iso->handle == handle) || (is_big && iso->iso.info.type == BT_ISO_CHAN_TYPE_BROADCASTER && iso->iso.info.broadcaster.big_handle == handle)) { @@ -513,7 +557,7 @@ static int iso_tx_now(struct bt_iso_chan *chan, const uint8_t *sdu, * Check if ISO SDU needs to be fragmented here. */ - pkt = malloc(4 + data_total_len); + pkt = bt_le_int_malloc(4 + data_total_len); if (pkt == NULL) { LOG_ERR("IsoTxNowNoMem[%u]", 4 + data_total_len); return -ENOMEM; @@ -603,6 +647,11 @@ struct iso_tx_comp_event { struct bt_iso_tx_cb_info info; }; +/* See ISO_DROP_REPORT_STEP. */ +static BT_ISO_CTRL_BSS_ATTR uint32_t iso_tx_comp_drop_cnt; /* task queue full */ +static BT_ISO_CTRL_BSS_ATTR uint32_t iso_tx_comp_nomem_cnt; /* evt alloc failed */ +static BT_ISO_CTRL_BSS_ATTR uint32_t iso_tx_comp_reported; + void bt_le_iso_handle_tx_comp(uint8_t *data, size_t data_len) { struct iso_tx_comp_event *evt = (struct iso_tx_comp_event *)data; @@ -611,10 +660,18 @@ void bt_le_iso_handle_tx_comp(uint8_t *data, size_t data_len) struct bt_conn *iso; bt_conn_tx_cb_t cb; sys_snode_t *node; + uint32_t dropped; void *ud; int err; - assert(data && data_len == sizeof(*evt)); + BT_LE_ASSERT(data && data_len == sizeof(*evt)); + + dropped = iso_tx_comp_drop_cnt + iso_tx_comp_nomem_cnt; + if (dropped - iso_tx_comp_reported >= ISO_DROP_REPORT_STEP) { + iso_tx_comp_reported = dropped; + LOG_WRN("IsoTxCompDrop[q=%u][nomem=%u]", + iso_tx_comp_drop_cnt, iso_tx_comp_nomem_cnt); + } bt_le_host_lock(); @@ -647,7 +704,7 @@ void bt_le_iso_handle_tx_comp(uint8_t *data, size_t data_len) * If using controller from other vendors, the pkt buffer may * needs to be freed here. */ - LOG_WRN("IsoTxCompFail[%d]", err); + LOG_WRN("IsoTxCompFail[%d]", err); } free(sdu_node); @@ -659,7 +716,7 @@ void bt_le_iso_handle_tx_comp(uint8_t *data, size_t data_len) } chan = iso->iso.chan; - assert(chan); + BT_LE_ASSERT(chan); cb = NULL; ud = NULL; @@ -687,11 +744,11 @@ static void iso_tx_comp_cb(uint16_t conn_handle, void *info, size_t size) * the event to the ISO task for processing. */ - assert(size == sizeof(struct bt_iso_tx_cb_info)); + BT_LE_ASSERT(size == sizeof(struct bt_iso_tx_cb_info)); - evt = calloc(1, sizeof(*evt)); + evt = bt_le_int_calloc(1, sizeof(*evt)); if (evt == NULL) { - LOG_ERR("IsoTxCompNoMem[%u]", sizeof(*evt)); + iso_tx_comp_nomem_cnt++; return; } @@ -700,7 +757,7 @@ static void iso_tx_comp_cb(uint16_t conn_handle, void *info, size_t size) err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_ISO_TX_COMP, evt, sizeof(*evt)); if (err) { - LOG_ERR("IsoTxCompPostFail[%d]", err); + iso_tx_comp_drop_cnt++; free(evt); } } @@ -713,11 +770,23 @@ void bt_le_iso_handle_tx_comp(uint8_t *data, size_t data_len) #endif /* CONFIG_BT_ISO_TX */ #if CONFIG_BT_ISO_RX +/* See ISO_DROP_REPORT_STEP. */ +static BT_ISO_CTRL_BSS_ATTR uint32_t iso_rx_drop_cnt; /* task queue full */ +static BT_ISO_CTRL_BSS_ATTR uint32_t iso_rx_nomem_cnt; /* rx_data alloc failed */ +static BT_ISO_CTRL_BSS_ATTR uint32_t iso_rx_reported; + void bt_le_iso_handle_rx_data(uint8_t *data, size_t data_len) { struct net_buf buf = {0}; + uint32_t dropped; - assert(data && data_len); + BT_LE_ASSERT(data && data_len); + + dropped = iso_rx_drop_cnt + iso_rx_nomem_cnt; + if (dropped - iso_rx_reported >= ISO_DROP_REPORT_STEP) { + iso_rx_reported = dropped; + LOG_WRN("IsoRxDrop[q=%u][nomem=%u]", iso_rx_drop_cnt, iso_rx_nomem_cnt); + } bt_le_host_lock(); net_buf_simple_init_with_data(&buf.b, (void *)data, data_len); @@ -743,9 +812,9 @@ int bt_le_iso_rx(const uint8_t *data, uint16_t len, void *arg) ARG_UNUSED(arg); - rx_data = calloc(1, len); + rx_data = bt_le_int_calloc(1, len); if (rx_data == NULL) { - LOG_ERR("IsoRxNoMem[%u]", len); + iso_rx_nomem_cnt++; return -ENOMEM; } @@ -753,7 +822,7 @@ int bt_le_iso_rx(const uint8_t *data, uint16_t len, void *arg) err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_ISO_RX_DATA, rx_data, len); if (err) { - LOG_ERR("IsoRxPostFail[%d]", err); + iso_rx_drop_cnt++; free(rx_data); return -EIO; } diff --git a/components/bt/esp_ble_iso/host/common/l2cap.c b/components/bt/esp_ble_iso/host/common/l2cap.c deleted file mode 100644 index 79dc4256964..00000000000 --- a/components/bt/esp_ble_iso/host/common/l2cap.c +++ /dev/null @@ -1,533 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2015-2016 Intel Corporation - * SPDX-FileCopyrightText: 2023 Nordic Semiconductor - * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#include -#include -#include - -#include -#include -#include - -#include <../host/conn_internal.h> -#include - -#include "common/host.h" -#include "common/app/gap.h" - -LOG_MODULE_REGISTER(ISO_L2CAP, CONFIG_BT_ISO_LOG_LEVEL); - -#define L2CAP_LE_MIN_MTU 23 -#define L2CAP_ECRED_MIN_MTU 64 - -#define L2CAP_LE_CID_DYN_START 0x0040 -#define L2CAP_LE_CID_DYN_END 0x007F -#define L2CAP_LE_CID_IS_DYN(_cid) (_cid >= L2CAP_LE_CID_DYN_START && _cid <= L2CAP_LE_CID_DYN_END) - -#define L2CAP_LE_PSM_FIXED_START 0x0001 -#define L2CAP_LE_PSM_FIXED_END 0x007F -#define L2CAP_LE_PSM_DYN_START 0x0080 -#define L2CAP_LE_PSM_DYN_END 0x00FF -#define L2CAP_LE_PSM_IS_DYN(_psm) (_psm >= L2CAP_LE_PSM_DYN_START && _psm <= L2CAP_LE_PSM_DYN_END) - -static sys_slist_t l2cap_servers = SYS_SLIST_STATIC_INIT(&l2cap_servers); - -static struct bt_l2cap_chan *l2cap_lookup_tx_cid(struct bt_conn *conn, uint16_t cid) -{ - struct bt_l2cap_chan *chan; - - SYS_SLIST_FOR_EACH_CONTAINER(&conn->channels, chan, node) { - if (BT_L2CAP_LE_CHAN(chan)->tx.cid == cid) { - return chan; - } - } - - return NULL; -} - -__attribute__((unused)) -static struct bt_l2cap_chan *l2cap_lookup_rx_cid(struct bt_conn *conn, uint16_t cid) -{ - struct bt_l2cap_chan *chan; - - SYS_SLIST_FOR_EACH_CONTAINER(&conn->channels, chan, node) { - if (BT_L2CAP_LE_CHAN(chan)->rx.cid == cid) { - return chan; - } - } - - return NULL; -} - -static struct bt_l2cap_chan *l2cap_lookup_psm(struct bt_conn *conn, uint16_t psm) -{ - struct bt_l2cap_chan *chan; - - SYS_SLIST_FOR_EACH_CONTAINER(&conn->channels, chan, node) { - if (BT_L2CAP_LE_CHAN(chan)->psm == psm) { - return chan; - } - } - - return NULL; -} - -static bool l2cap_chan_add(struct bt_conn *conn, struct bt_l2cap_chan *chan, uint16_t psm) -{ - LOG_DBG("L2capChanAdd[%04x]", psm); - - /* Attach channel to the connection */ - if (sys_slist_find(&conn->channels, &chan->node, NULL)) { - LOG_WRN("L2capChanExists[%04x]", psm); - return false; - } - - sys_slist_append(&conn->channels, &chan->node); - chan->conn = conn; - - /* Set channel PSM */ - BT_L2CAP_LE_CHAN(chan)->psm = psm; - - return true; -} - -static struct bt_l2cap_server *l2cap_server_lookup_psm(uint16_t psm) -{ - struct bt_l2cap_server *server = NULL; - - SYS_SLIST_FOR_EACH_CONTAINER(&l2cap_servers, server, node) { - if (server->psm == psm) { - break; - } - } - - return server; -} - -static inline uint16_t err_to_result(int err) -{ - switch (err) { - case -ENOMEM: - return L2CAP_LE_ERR_NO_RESOURCES; - case -EACCES: - return L2CAP_LE_ERR_AUTHORIZATION; - case -EPERM: - return L2CAP_LE_ERR_KEY_SIZE; - case -ENOTSUP: - /* This handle the cases where a fixed channel is registered but - * for some reason (e.g. controller not supporting a feature) - * cannot be used. - */ - return L2CAP_LE_ERR_PSM_NOT_SUPP; - default: - return L2CAP_LE_ERR_UNACCEPT_PARAMS; - } -} - -_IDF_ONLY -int bt_le_l2cap_accept(uint16_t conn_handle, uint16_t psm, - uint16_t scid, uint16_t mtu, - uint16_t mps, uint16_t credits, - uint16_t *result) -{ - struct bt_l2cap_server *server; - struct bt_l2cap_chan *chan; - struct bt_conn *conn; - int err; - - ARG_UNUSED(credits); - - LOG_DBG("L2capAccept[%u][%04x][%04x][%u][%u]", conn_handle, psm, scid, mtu, mps); - - conn = bt_le_acl_conn_find(conn_handle); - if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_INF("L2capAcceptNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); - - *result = L2CAP_LE_ERR_INVALID_PARAMS; - return -ENOTCONN; - } - - /* Check if there is a server registered */ - server = l2cap_server_lookup_psm(psm); - if (server == NULL) { - LOG_ERR("SrvNotReg[%04x]", psm); - - *result = L2CAP_LE_ERR_PSM_NOT_SUPP; - return -ENOTSUP; - } - - if (!L2CAP_LE_CID_IS_DYN(scid)) { - LOG_ERR("NotDynScid[%04x]", scid); - - *result = L2CAP_LE_ERR_INVALID_SCID; - return -EINVAL; - } - - chan = l2cap_lookup_tx_cid(conn, scid); - if (chan) { - LOG_WRN("ScidUsed[%04x]", scid); - - *result = L2CAP_LE_ERR_SCID_IN_USE; - return -EALREADY; - } - - if (server->accept == NULL) { - LOG_ERR("SrvAcceptNull"); - - *result = L2CAP_LE_ERR_INVALID_PARAMS; - return -EIO; - } - - err = server->accept(conn, server, &chan); - if (err) { - LOG_ERR("SrvAcceptFail[%d]", err); - - *result = err_to_result(err); - return -EIO; - } - - if (chan == NULL) { - LOG_ERR("SrvAcceptNullChan"); - *result = L2CAP_LE_ERR_NO_RESOURCES; - return -ENOMEM; - } - - if (l2cap_chan_add(conn, chan, psm) == false) { - *result = L2CAP_LE_ERR_NO_RESOURCES; - return -ENOMEM; - } - - *result = L2CAP_LE_SUCCESS; - return 0; -} - -_IDF_ONLY -void bt_le_l2cap_connected(uint16_t conn_handle, uint16_t psm, - uint16_t tx_cid, uint16_t tx_mtu, - uint16_t rx_cid, uint16_t rx_mtu) -{ - struct bt_l2cap_chan *chan; - struct bt_conn *conn; - - LOG_DBG("L2capConnected[%u][%04x][%04x][%u][%04x][%u]", - conn_handle, psm, tx_cid, tx_mtu, rx_cid, rx_mtu); - - conn = bt_le_acl_conn_find(conn_handle); - if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_INF("L2capConnectedNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); - return; - } - - chan = l2cap_lookup_psm(conn, psm); - if (chan == NULL) { - LOG_ERR("PsmNotFound[%04x]", psm); - return; - } - - BT_L2CAP_LE_CHAN(chan)->tx.cid = tx_cid; - BT_L2CAP_LE_CHAN(chan)->tx.mtu = tx_mtu; - BT_L2CAP_LE_CHAN(chan)->rx.cid = rx_cid; - BT_L2CAP_LE_CHAN(chan)->rx.mtu = rx_mtu; - - if (chan->ops->connected) { - chan->ops->connected(chan); - } -} - -#define L2CAP_UGLY_TX_DONE_WORKAROUND 1 - -_IDF_ONLY -void bt_le_l2cap_disconnected(uint16_t conn_handle, uint16_t psm) -{ -#if L2CAP_UGLY_TX_DONE_WORKAROUND - struct bt_gatt_ots_l2cap *l2cap_ctx; - struct bt_l2cap_le_chan *l2chan; -#endif /* L2CAP_UGLY_TX_DONE_WORKAROUND */ - struct bt_l2cap_chan *chan; - struct bt_conn *conn; - - LOG_DBG("L2capDisconnected[%u][%04x]", conn_handle, psm); - - conn = bt_le_acl_conn_find(conn_handle); - if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_INF("L2capDisconnectedNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); - return; - } - - chan = l2cap_lookup_psm(conn, psm); - if (chan == NULL) { - LOG_ERR("PsmNotFound[%04x]", psm); - return; - } - -#if L2CAP_UGLY_TX_DONE_WORKAROUND - l2chan = CONTAINER_OF(chan, struct bt_l2cap_le_chan, chan); - l2cap_ctx = CONTAINER_OF(l2chan, struct bt_gatt_ots_l2cap, ot_chan); - - if (chan->ops->sent && l2cap_ctx->tx_done) { - chan->ops->sent(chan); - } -#endif /* L2CAP_UGLY_TX_DONE_WORKAROUND */ - - if (chan->ops->disconnected) { - chan->ops->disconnected(chan); - } - - sys_slist_find_and_remove(&conn->channels, &chan->node); - chan->conn = NULL; -} - -_IDF_ONLY -void bt_le_l2cap_received(uint16_t conn_handle, uint16_t psm, - uint8_t *data, uint16_t len) -{ - struct bt_l2cap_chan *chan; - struct net_buf buf = {0}; - struct bt_conn *conn; - - LOG_DBG("L2capReceived[%u][%04x][%u]", conn_handle, psm, len); - - conn = bt_le_acl_conn_find(conn_handle); - if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_INF("L2capReceivedNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); - return; - } - - chan = l2cap_lookup_psm(conn, psm); - if (chan == NULL) { - LOG_ERR("PsmNotFound[%04x]", psm); - return; - } - - buf.data = data; - buf.len = len; - - if (chan->ops->recv) { - chan->ops->recv(chan, &buf); - } -} - -_IDF_ONLY -int bt_l2cap_chan_connect(struct bt_conn *conn, struct bt_l2cap_chan *chan, uint16_t psm) -{ -#if !CONFIG_BT_BLUEDROID_ENABLED - int err; -#endif - - LOG_DBG("L2capChanConnect[%04x]", psm); - - if (chan == NULL) { - LOG_ERR("L2capChanNull"); - return -EINVAL; - } - - if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_ERR("L2capChanNotConn[%p]", conn); - return -ENOTCONN; - } - - if (psm < L2CAP_LE_PSM_FIXED_START || psm > L2CAP_LE_PSM_DYN_END) { - LOG_ERR("InvPsm[%04x]", psm); - return -EINVAL; - } - -#if CONFIG_BT_BLUEDROID_ENABLED - /* L2CAP COC is not yet implemented in the Bluedroid adapter (no - * bt_le_bluedroid_l2cap_chan_connect exists). Return early so callers - * like bt_gatt_ots_l2cap_connect see a clean -ENOTSUP. */ - return -ENOTSUP; -#else - err = bt_le_nimble_l2cap_chan_connect(conn->handle); - if (err) { - return err; - } - - l2cap_chan_add(conn, chan, psm); - - return 0; -#endif -} - -_IDF_ONLY -int bt_l2cap_chan_disconnect(struct bt_l2cap_chan *chan) -{ -#if !CONFIG_BT_BLUEDROID_ENABLED - int err; -#endif - - LOG_DBG("L2capChanDisconnect"); - - if (chan == NULL) { - LOG_ERR("L2capChanNull"); - return -EINVAL; - } - - if (chan->conn == NULL || chan->conn->state != BT_CONN_CONNECTED) { - LOG_ERR("L2capChanNotConn[%p]", chan->conn); - return -ENOTCONN; - } - -#if CONFIG_BT_BLUEDROID_ENABLED - return -ENOTSUP; -#else - err = bt_le_nimble_l2cap_chan_disconnect(chan); - if (err) { - /* If the disconnect failed, remove the channel from the connection. - * Otherwise the removal will be handled by the disconnect callback. - */ - sys_slist_find_and_remove(&chan->conn->channels, &chan->node); - chan->conn = NULL; - } - - return err; -#endif -} - -_IDF_ONLY -int bt_l2cap_chan_send(struct bt_l2cap_chan *chan, struct net_buf *buf) -{ - if (chan == NULL || buf == NULL) { - LOG_ERR("L2capChanBufNull[%p][%p]", chan, buf); - return -EINVAL; - } - - LOG_DBG("L2capChanSend[%u]", buf->len); - - if (chan->conn == NULL || chan->conn->state != BT_CONN_CONNECTED) { - LOG_ERR("L2capChanNotConn[%p]", chan->conn); - return -ENOTCONN; - } - - if (buf->len > L2CAP_LE_OTS_MTU) { - LOG_ERR("L2capTooLargeBufToSend[%u][%u]", buf->len, L2CAP_LE_OTS_MTU); - return -EMSGSIZE; - } - -#if CONFIG_BT_BLUEDROID_ENABLED - return -ENOTSUP; -#else - return bt_le_nimble_l2cap_chan_send(chan, buf); -#endif -} - -_IDF_ONLY -int bt_l2cap_server_register(struct bt_l2cap_server *server) -{ - LOG_DBG("L2capSrvReg"); - - if (server == NULL) { - LOG_ERR("SrvNull"); - return -EINVAL; - } - - if (server->accept == NULL) { - LOG_ERR("SrvAcceptNull"); - return -EINVAL; - } - - if (server->sec_level > BT_SECURITY_L4) { - LOG_ERR("InvSecLevel[%u]", server->sec_level); - return -EINVAL; - } - - if (server->psm) { - if (server->psm < L2CAP_LE_PSM_FIXED_START || - server->psm > L2CAP_LE_PSM_DYN_END) { - LOG_ERR("InvPsm[%04x]", server->psm); - return -EINVAL; - } - - /* Check if given PSM is already in use */ - if (l2cap_server_lookup_psm(server->psm)) { - LOG_WRN("PsmReg"); - return -EADDRINUSE; - } - - LOG_DBG("SrvPsm[%04x]", server->psm); - } else { - uint16_t psm; - - for (psm = L2CAP_LE_PSM_DYN_START; - psm <= L2CAP_LE_PSM_DYN_END; psm++) { - if (l2cap_server_lookup_psm(psm) == NULL) { - break; - } - } - - if (psm > L2CAP_LE_PSM_DYN_END) { - LOG_ERR("NoFreeDynPsm"); - return -EADDRNOTAVAIL; - } - - LOG_DBG("PsmNew[%04x]", psm); - - server->psm = psm; - } - - if (server->sec_level < BT_SECURITY_L1) { - server->sec_level = BT_SECURITY_L1; - } - - sys_slist_append(&l2cap_servers, &server->node); - - return 0; -} - -int bt_le_l2cap_init(void) -{ - int err; - - LOG_DBG("L2capInit"); - -#if CONFIG_BT_OTS - extern int bt_gatt_ots_conn_cb_register(void); - err = bt_gatt_ots_conn_cb_register(); - if (err) { - LOG_ERR("OtsConnCbRegFail[%d]", err); - return err; - } - - extern int bt_gatt_ots_instances_prepare_v2(void); - err = bt_gatt_ots_instances_prepare_v2(); - if (err) { - LOG_ERR("PrepOtsInstsFail[%d]", err); - return err; - } -#endif /* CONFIG_BT_OTS */ - -#if CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT - extern int bt_gatt_ots_l2cap_init_v2(void); - err = bt_gatt_ots_l2cap_init_v2(); - if (err) { - LOG_ERR("OtsL2capInitFail[%d]", err); - return err; - } -#endif /* CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT */ - -#if CONFIG_BT_BLUEDROID_ENABLED - /* No adapter-level L2CAP init exists (COC not implemented). Don't fail - * host init here — the host-agnostic OTS registrations above are still - * valid; any actual COC connect attempt later returns -ENOTSUP in - * bt_l2cap_chan_connect, so OTS features fail gracefully at use time - * instead of preventing the whole host from coming up. */ -#else - err = bt_le_nimble_l2cap_init(); - if (err) { - return err; - } -#endif - - return 0; -} - -void bt_le_l2cap_deinit(void) -{ - LOG_DBG("L2capDeinit"); - - /* TODO: L2CAP server deinit */ -} diff --git a/components/bt/esp_ble_iso/host/common/scan.c b/components/bt/esp_ble_iso/host/common/scan.c index a07686d9ba4..cd04b1cc644 100644 --- a/components/bt/esp_ble_iso/host/common/scan.c +++ b/components/bt/esp_ble_iso/host/common/scan.c @@ -25,7 +25,7 @@ LOG_MODULE_REGISTER(ISO_SCAN, CONFIG_BT_ISO_LOG_LEVEL); -static sys_slist_t scan_cbs = SYS_SLIST_STATIC_INIT(&scan_cbs); +static BT_ISO_EXT_RAM_BSS_ATTR sys_slist_t scan_cbs; _LIB_ONLY int bt_le_scan_cb_register(struct bt_le_scan_cb *cb) @@ -146,9 +146,9 @@ void bt_le_scan_recv_listener(uint16_t event_type, } } -static struct bt_le_per_adv_sync per_adv_sync_pool[CONFIG_BT_PER_ADV_SYNC_MAX]; +static BT_ISO_EXT_RAM_BSS_ATTR struct bt_le_per_adv_sync per_adv_sync_pool[CONFIG_BT_PER_ADV_SYNC_MAX]; -static sys_slist_t pa_sync_cbs = SYS_SLIST_STATIC_INIT(&pa_sync_cbs); +static BT_ISO_EXT_RAM_BSS_ATTR sys_slist_t pa_sync_cbs; _LIB_ONLY int bt_le_per_adv_sync_cb_register(struct bt_le_per_adv_sync_cb *cb) @@ -170,6 +170,24 @@ int bt_le_per_adv_sync_cb_register(struct bt_le_per_adv_sync_cb *cb) return 0; } +_LIB_ONLY +int bt_le_per_adv_sync_cb_unregister(struct bt_le_per_adv_sync_cb *cb) +{ + LOG_DBG("PaSyncCbUnreg"); + + if (cb == NULL) { + LOG_ERR("PaSyncCbNull"); + return -EINVAL; + } + + if (!sys_slist_find_and_remove(&pa_sync_cbs, &cb->node)) { + LOG_ERR("PaSyncCbNotReg[%p]", cb); + return -ENOENT; + } + + return 0; +} + _LIB_ONLY int bt_le_per_adv_sync_get_info(struct bt_le_per_adv_sync *per_adv_sync, struct bt_le_per_adv_sync_info *info) @@ -198,7 +216,7 @@ struct bt_le_per_adv_sync *bt_le_per_adv_sync_lookup_addr(const bt_addr_le_t *ad { struct bt_le_per_adv_sync *per_adv_sync = NULL; - assert(adv_addr); + BT_LE_ASSERT(adv_addr); LOG_INF("PaSyncLookupAddr[%s][%u]", bt_addr_le_str(adv_addr), sid); @@ -218,7 +236,8 @@ struct bt_le_per_adv_sync *bt_le_per_adv_sync_lookup_addr(const bt_addr_le_t *ad return per_adv_sync; } -static struct bt_le_per_adv_sync *per_adv_sync_find(uint16_t handle) +_IDF_ONLY +struct bt_le_per_adv_sync *bt_le_per_adv_sync_find(uint16_t handle) { struct bt_le_per_adv_sync *per_adv_sync = NULL; @@ -239,7 +258,7 @@ struct bt_le_per_adv_sync *bt_le_per_adv_sync_find_safe(uint16_t sync_handle) struct bt_le_per_adv_sync *per_adv_sync = NULL; LOG_DBG("PaSyncFind[%u]", sync_handle); bt_le_host_lock(); - per_adv_sync = per_adv_sync_find(sync_handle); + per_adv_sync = bt_le_per_adv_sync_find(sync_handle); bt_le_host_unlock(); return per_adv_sync; } @@ -284,7 +303,7 @@ int bt_le_per_adv_sync_new(uint16_t sync_handle, return -EINVAL; } - per_adv_sync = per_adv_sync_find(sync_handle); + per_adv_sync = bt_le_per_adv_sync_find(sync_handle); if (per_adv_sync) { LOG_WRN("PaSyncExist[%u]", sync_handle); return -EEXIST; @@ -321,13 +340,30 @@ int bt_le_per_adv_sync_new(uint16_t sync_handle, } _IDF_ONLY +int bt_le_per_adv_sync_new_safe(uint16_t sync_handle, + uint8_t sid, + uint8_t phy, + uint16_t interval, + uint8_t addr_type, + const uint8_t addr[6], + uint16_t conn_handle, + struct bt_le_per_adv_sync **out_sync) +{ + int err; + bt_le_host_lock(); + err = bt_le_per_adv_sync_new(sync_handle, sid, phy, interval, addr_type, + addr, conn_handle, out_sync); + bt_le_host_unlock(); + return err; +} + int bt_le_per_adv_sync_delete(uint16_t sync_handle) { struct bt_le_per_adv_sync *per_adv_sync = NULL; LOG_DBG("PaSyncDelete[%u]", sync_handle); - per_adv_sync = per_adv_sync_find(sync_handle); + per_adv_sync = bt_le_per_adv_sync_find(sync_handle); if (per_adv_sync == NULL) { LOG_ERR("PaSyncNotFound[%u]", sync_handle); return -ENODEV; @@ -347,7 +383,7 @@ int bt_le_per_adv_sync_establish_listener(uint16_t sync_handle) LOG_DBG("PaSyncEstabListener[%u]", sync_handle); - per_adv_sync = per_adv_sync_find(sync_handle); + per_adv_sync = bt_le_per_adv_sync_find(sync_handle); if (per_adv_sync == NULL) { LOG_ERR("PaSyncNotFound[%u]", sync_handle); return -ENODEV; @@ -374,10 +410,6 @@ int bt_le_per_adv_sync_establish_listener(uint16_t sync_handle) } } - if (info.conn) { - bt_conn_unref(info.conn); - } - return 0; } @@ -390,7 +422,7 @@ int bt_le_per_adv_sync_lost_listener(uint16_t sync_handle) LOG_DBG("PaSyncLostListener[%u]", sync_handle); - per_adv_sync = per_adv_sync_find(sync_handle); + per_adv_sync = bt_le_per_adv_sync_find(sync_handle); if (per_adv_sync == NULL) { LOG_ERR("PaSyncNotFound[%u]", sync_handle); return -ENODEV; @@ -426,7 +458,7 @@ int bt_le_per_adv_sync_report_recv_listener(uint16_t sync_handle, return -EINVAL; } - per_adv_sync = per_adv_sync_find(sync_handle); + per_adv_sync = bt_le_per_adv_sync_find(sync_handle); if (per_adv_sync == NULL) { LOG_ERR("PaSyncNotFound[%u]", sync_handle); return -ENODEV; @@ -458,7 +490,7 @@ void hci_le_biginfo_adv_report(struct net_buf *buf) /* LOG_DBG("BigInfoRecvListener[%u]", evt->sync_handle); */ - per_adv_sync = per_adv_sync_find(evt->sync_handle); + per_adv_sync = bt_le_per_adv_sync_find(evt->sync_handle); if (per_adv_sync == NULL) { LOG_ERR("PaSyncNotFound[%u]", evt->sync_handle); return; @@ -554,6 +586,17 @@ static void past_features_unset(void) } _IDF_ONLY +void bt_le_per_adv_sync_state_reset(void) +{ + for (size_t i = 0; i < ARRAY_SIZE(per_adv_sync_pool); i++) { + if (atomic_test_bit(per_adv_sync_pool[i].flags, BT_PER_ADV_SYNC_SYNCED)) { + LOG_WRN("DeinitDropPaSync[%u][%04x]", i, per_adv_sync_pool[i].handle); + } + } + + memset(per_adv_sync_pool, 0, sizeof(per_adv_sync_pool)); +} + int bt_le_scan_init(void) { LOG_DBG("ScanInit"); diff --git a/components/bt/esp_ble_iso/host/common/task.c b/components/bt/esp_ble_iso/host/common/task.c index 60f0898efa2..9e3dc1f44b5 100644 --- a/components/bt/esp_ble_iso/host/common/task.c +++ b/components/bt/esp_ble_iso/host/common/task.c @@ -20,28 +20,57 @@ #include "common/host.h" #include "common/iso.h" +#include "common/gatt.h" #include "common/app/gap.h" #include "common/app/gatt.h" LOG_MODULE_REGISTER(ISO_TASK, CONFIG_BT_ISO_LOG_LEVEL); +/* Nothing to poll for - iso_ctrl_queue wakes the task for deinit. The dispatch + * monitor is the exception: its periodic dump is driven from this loop, so it + * needs a wakeup even while no event arrives. */ +#if CONFIG_BT_ISO_DISPATCH_MONITOR +#define ISO_TASK_WAIT (ISO_STATS_DUMP_PERIOD_US / 1000 / portTICK_PERIOD_MS) +#else /* CONFIG_BT_ISO_DISPATCH_MONITOR */ +#define ISO_TASK_WAIT portMAX_DELAY +#endif /* CONFIG_BT_ISO_DISPATCH_MONITOR */ + +/* Generous: expiry means a dispatch handler is wedged, which is a bug + * elsewhere. Deinit reports it upward rather than freeing under a live task. */ +#define ISO_TASK_STOP_TIMEOUT (2000 / portTICK_PERIOD_MS) + /* Three priority tiers share one task via a queue set. The task drains * critical before normal before floodable, so a flood of GAP reports cannot * delay the latency-critical ISO data path. See common/task.h for the mapping. */ -static QueueHandle_t iso_critical_queue; -static QueueHandle_t iso_normal_queue; -static QueueHandle_t iso_floodable_queue; -static QueueSetHandle_t iso_queue_set; +static BT_ISO_CTRL_BSS_ATTR QueueHandle_t iso_critical_queue; +static BT_ISO_CTRL_BSS_ATTR QueueHandle_t iso_normal_queue; +static BT_ISO_CTRL_BSS_ATTR QueueHandle_t iso_floodable_queue; +/* Not a tier: deinit-only wakeup, see ISO_CTRL_QUEUE_LEN. */ +static BT_ISO_CTRL_BSS_ATTR QueueHandle_t iso_ctrl_queue; +static BT_ISO_CTRL_BSS_ATTR QueueSetHandle_t iso_queue_set; -static TaskHandle_t iso_task_handle; +static BT_ISO_CTRL_BSS_ATTR TaskHandle_t iso_task_handle; -extern void bt_le_timer_handle_event(void *arg); +/* Gate + handshake for deinit. iso_task_stopping also rejects new posts, so a + * producer cannot strand a payload on a queue nobody will drain. */ +static BT_ISO_CTRL_BSS_ATTR volatile bool iso_task_stopping; +static BT_ISO_CTRL_BSS_ATTR SemaphoreHandle_t iso_task_stopped; + +extern void bt_le_timer_handle_event(void *arg, size_t gen); + +#if CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT +/* Defined in esp_ble_audio (ots/adapter/l2cap.c) - the only L2CAP consumer is + * OTS, so the shim lives there. Declared instead of included to keep esp_ble_iso + * free of audio headers; both live in the bt component, so the link resolves. */ +extern void bt_le_l2cap_handle_event(void *data, size_t data_len); +extern void bt_le_l2cap_event_free(void *data); +#endif #if CONFIG_BT_ISO_DISPATCH_MONITOR /* Per-type dispatch timing, indexed by iso_queue_item_type. Written only by * iso_task (single writer); read by bt_le_iso_dispatch_stats_dump. */ -static struct iso_dispatch_stats { +static BT_ISO_CTRL_BSS_ATTR struct iso_dispatch_stats { int64_t max_us; uint32_t count; uint32_t slow_count; @@ -83,6 +112,52 @@ void bt_le_iso_dispatch_stats_dump(void) } #endif /* CONFIG_BT_ISO_DISPATCH_MONITOR */ +static void iso_item_release(const struct iso_queue_item *item) +{ + switch (item->type) { + case ISO_QUEUE_ITEM_TYPE_TIMER_EVENT: + /* data is the k_work, data_len its generation counter - not a block. */ + break; + case ISO_QUEUE_ITEM_TYPE_GATT_EVENT: + bt_le_gatt_event_free(item->data); + break; + case ISO_QUEUE_ITEM_TYPE_GAP_EVENT: + case ISO_QUEUE_ITEM_TYPE_EXT_ADV_REPORT: + case ISO_QUEUE_ITEM_TYPE_PER_ADV_REPORT: + bt_le_gap_event_free(item->data); + break; +#if CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT + case ISO_QUEUE_ITEM_TYPE_L2CAP_EVENT: + bt_le_l2cap_event_free(item->data); + break; +#endif /* CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT */ + default: + if (item->data) { + free(item->data); + } + break; + } +} + +/* Runs on iso_task after the loop exits, so no producer can be mid-dispatch and + * the queues are provably empty when iso_queues_destroy() deletes them. */ +static void iso_queues_drain(void) +{ + struct iso_queue_item item = {0}; + + while (xQueueReceive(iso_critical_queue, &item, 0) == pdTRUE) { + iso_item_release(&item); + } + + while (xQueueReceive(iso_normal_queue, &item, 0) == pdTRUE) { + iso_item_release(&item); + } + + while (xQueueReceive(iso_floodable_queue, &item, 0) == pdTRUE) { + iso_item_release(&item); + } +} + static void iso_dispatch_item(const struct iso_queue_item *item) { #if CONFIG_BT_ISO_DISPATCH_MONITOR @@ -94,7 +169,7 @@ static void iso_dispatch_item(const struct iso_queue_item *item) switch (item->type) { case ISO_QUEUE_ITEM_TYPE_TIMER_EVENT: - bt_le_timer_handle_event(item->data); + bt_le_timer_handle_event(item->data, item->data_len); break; case ISO_QUEUE_ITEM_TYPE_GAP_EVENT: case ISO_QUEUE_ITEM_TYPE_EXT_ADV_REPORT: @@ -104,6 +179,11 @@ static void iso_dispatch_item(const struct iso_queue_item *item) case ISO_QUEUE_ITEM_TYPE_GATT_EVENT: bt_le_gatt_handle_event(item->data, item->data_len); break; +#if CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT + case ISO_QUEUE_ITEM_TYPE_L2CAP_EVENT: + bt_le_l2cap_handle_event(item->data, item->data_len); + break; +#endif /* CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT */ case ISO_QUEUE_ITEM_TYPE_ISO_HCI_EVENT: case ISO_QUEUE_ITEM_TYPE_BIGINFO_ADV_REPORT: bt_le_iso_handle_hci_event(item->data, item->data_len); @@ -115,10 +195,8 @@ static void iso_dispatch_item(const struct iso_queue_item *item) bt_le_iso_handle_rx_data(item->data, item->data_len); break; default: - if (item->data) { - free(item->data); - } - assert(0); + iso_item_release(item); + BT_LE_ASSERT(0); break; } @@ -135,13 +213,11 @@ static void iso_task(void *p) #endif /* CONFIG_BT_ISO_DISPATCH_MONITOR */ struct iso_queue_item item = {0}; - while (1) { - /* Block until any tier has data. The returned member handle is ignored: - * we always service by strict priority below (critical > normal > - * floodable), processing one item per wakeup and re-checking critical - * first on the next loop. A pdFALSE receive is tolerated as a benign - * side effect of servicing queues outside xQueueSelectFromSet. */ - (void)xQueueSelectFromSet(iso_queue_set, portMAX_DELAY); + while (!iso_task_stopping) { + /* The returned handle is ignored: service by strict priority instead + * (critical > normal > floodable), one item per wakeup. A pdFALSE + * receive is benign - that is what a deinit wakeup looks like. */ + (void)xQueueSelectFromSet(iso_queue_set, ISO_TASK_WAIT); if (xQueueReceive(iso_critical_queue, &item, 0) == pdTRUE) { iso_dispatch_item(&item); @@ -158,6 +234,14 @@ static void iso_task(void *p) } #endif /* CONFIG_BT_ISO_DISPATCH_MONITOR */ } + + /* Draining here rather than in the deinit caller keeps payload ownership on + * a single task: no producer is mid-post and no consumer is mid-dispatch. */ + iso_queues_drain(); + + xSemaphoreGive(iso_task_stopped); + + vTaskDelete(NULL); } int bt_le_iso_task_post(enum iso_queue_item_type type, @@ -168,6 +252,13 @@ int bt_le_iso_task_post(enum iso_queue_item_type type, TickType_t wait; int ret; + /* No consumer before init or after deinit began, so accepting would strand + * the payload (callers free on failure). Distinct from the -1 below: this + * one persists, a full queue is transient. */ + if (iso_task_handle == NULL || iso_task_stopping) { + return -ESHUTDOWN; + } + item.type = type; item.data = data; item.data_len = data_len; @@ -188,9 +279,13 @@ int bt_le_iso_task_post(enum iso_queue_item_type type, wait = 0; break; default: - /* Timer / GATT / HCI / GAP lifecycle: reliable, block until space. */ + /* Timer / GATT / HCI / GAP lifecycle. A self-post from iso_task must + * not block (it is the sole consumer -> blocking on its own full queue + * deadlocks); external producers get a bounded wait, not portMAX_DELAY, + * so a wedged iso_task can't freeze esp_timer / the host task and stall + * the ISO data path. */ queue = iso_normal_queue; - wait = portMAX_DELAY; + wait = (xTaskGetCurrentTaskHandle() == iso_task_handle) ? 0 : K_QUEUE_SHORT; break; } @@ -233,6 +328,7 @@ static void iso_queues_destroy(void) iso_queue_destroy_one(&iso_critical_queue); iso_queue_destroy_one(&iso_normal_queue); iso_queue_destroy_one(&iso_floodable_queue); + iso_queue_destroy_one(&iso_ctrl_queue); if (iso_queue_set) { vQueueDelete(iso_queue_set); @@ -246,19 +342,31 @@ int bt_le_iso_task_init(void) LOG_DBG("IsoTaskInit"); + /* Reset here, not at definition, so a deinit/re-init cycle starts clean. */ + iso_task_stopping = false; + + iso_task_stopped = xSemaphoreCreateBinary(); + if (iso_task_stopped == NULL) { + LOG_ERR("IsoTaskSemCreateFail"); + return -EIO; + } + iso_critical_queue = xQueueCreate(ISO_CRITICAL_QUEUE_LEN, ISO_QUEUE_ITEM_SIZE); iso_normal_queue = xQueueCreate(ISO_NORMAL_QUEUE_LEN, ISO_QUEUE_ITEM_SIZE); iso_floodable_queue = xQueueCreate(ISO_FLOODABLE_QUEUE_LEN, ISO_QUEUE_ITEM_SIZE); + iso_ctrl_queue = xQueueCreate(ISO_CTRL_QUEUE_LEN, ISO_QUEUE_ITEM_SIZE); iso_queue_set = xQueueCreateSet(ISO_QUEUE_SET_LEN); if (iso_critical_queue == NULL || iso_normal_queue == NULL || - iso_floodable_queue == NULL || iso_queue_set == NULL) { + iso_floodable_queue == NULL || iso_ctrl_queue == NULL || + iso_queue_set == NULL) { LOG_ERR("IsoQCreateFail"); goto fail; } if (xQueueAddToSet(iso_critical_queue, iso_queue_set) != pdPASS || xQueueAddToSet(iso_normal_queue, iso_queue_set) != pdPASS || - xQueueAddToSet(iso_floodable_queue, iso_queue_set) != pdPASS) { + xQueueAddToSet(iso_floodable_queue, iso_queue_set) != pdPASS || + xQueueAddToSet(iso_ctrl_queue, iso_queue_set) != pdPASS) { LOG_ERR("IsoQSetAddFail"); goto fail; } @@ -279,22 +387,60 @@ int bt_le_iso_task_init(void) fail: iso_queues_destroy(); + vSemaphoreDelete(iso_task_stopped); + iso_task_stopped = NULL; return -EIO; } -void bt_le_iso_task_deinit(void) +int bt_le_iso_task_deinit(void) { + struct iso_queue_item item = {0}; + LOG_DBG("IsoTaskDeinit"); - if (iso_task_handle) { - vTaskDelete(iso_task_handle); - iso_task_handle = NULL; + if (iso_task_handle == NULL) { + return 0; } + /* This blocks on the task's own exit, so calling it from iso_task would + * wait for itself forever. */ + if (xTaskGetCurrentTaskHandle() == iso_task_handle) { + LOG_ERR("IsoTaskDeinitFromSelf"); + return -EDEADLK; + } + + /* Stops new posts as well, so the queues can only shrink from here. */ + iso_task_stopping = true; + + /* Setting the flag cannot wake a task blocked on the set. Posted after it so + * whichever select consumes this token re-checks the flag as true; deinit is + * the sole producer of a one-deep queue, so the send cannot fail. */ + (void)xQueueSend(iso_ctrl_queue, &item, 0); + + if (xSemaphoreTake(iso_task_stopped, ISO_TASK_STOP_TIMEOUT) != pdTRUE) { + /* A dispatch handler is wedged. Deleting the queues now would pull them + * out from under a live task, so leave everything in place and let the + * caller abort the teardown instead. */ + LOG_ERR("IsoTaskStopTimeout"); + iso_task_stopping = false; + /* Take the wakeup back: nothing reads this queue, so leaving it there + * would make the next attempt's send fail and never wake the task. */ + (void)xQueueReceive(iso_ctrl_queue, &item, 0); + return -ETIMEDOUT; + } + + iso_task_handle = NULL; + #if CONFIG_BT_ISO_DISPATCH_MONITOR /* Task is gone: no concurrent writer, safe to read the stats. */ bt_le_iso_dispatch_stats_dump(); #endif /* CONFIG_BT_ISO_DISPATCH_MONITOR */ + /* Drained by the task before it exited, so these are empty. */ iso_queues_destroy(); + + vSemaphoreDelete(iso_task_stopped); + iso_task_stopped = NULL; + + return 0; } diff --git a/components/bt/esp_ble_iso/host/iso/iso.c b/components/bt/esp_ble_iso/host/iso/iso.c index efb0352d6ea..024351633f2 100644 --- a/components/bt/esp_ble_iso/host/iso/iso.c +++ b/components/bt/esp_ble_iso/host/iso/iso.c @@ -55,11 +55,13 @@ LOG_MODULE_REGISTER(ISO_CORE, CONFIG_BT_ISO_LOG_LEVEL); #define iso_chan(_iso) ((_iso)->iso.chan); -struct bt_conn iso_conns[CONFIG_BT_ISO_MAX_CHAN]; +/* ISO connection state, not the SDU buffers: task-context only, no ISR/DMA, + * so PSRAM-eligible despite the per-SDU lookups. */ +BT_ISO_EXT_RAM_BSS_ATTR struct bt_conn iso_conns[CONFIG_BT_ISO_MAX_CHAN]; /* TODO: Allow more than one server? */ #if defined(CONFIG_BT_ISO_CENTRAL) -struct bt_iso_cig cigs[CONFIG_BT_ISO_MAX_CIG]; +static BT_ISO_EXT_RAM_BSS_ATTR struct bt_iso_cig cigs[CONFIG_BT_ISO_MAX_CIG]; static struct bt_iso_cig *get_cig(const struct bt_iso_chan *iso_chan); static int hci_le_create_cis(const struct bt_iso_connect_param *param, size_t count); @@ -67,13 +69,13 @@ static int hci_le_create_cis(const struct bt_iso_connect_param *param, size_t co #endif /* CONFIG_BT_ISO_CENTRAL */ #if defined(CONFIG_BT_ISO_PERIPHERAL) -static struct bt_iso_server *iso_server; +static BT_ISO_EXT_RAM_BSS_ATTR struct bt_iso_server *iso_server; static struct bt_conn *bt_conn_add_iso(struct bt_conn *acl); #endif /* CONFIG_BT_ISO_PERIPHERAL */ #if defined(CONFIG_BT_ISO_BROADCAST) -struct bt_iso_big bigs[CONFIG_BT_ISO_MAX_BIG]; +static BT_ISO_EXT_RAM_BSS_ATTR struct bt_iso_big bigs[CONFIG_BT_ISO_MAX_BIG]; static struct bt_iso_big *lookup_big_by_handle(uint8_t big_handle); #endif /* CONFIG_BT_ISO_BROADCAST */ @@ -84,7 +86,7 @@ static void bt_iso_sent_cb(struct bt_conn *iso, void *user_data, int err) struct bt_iso_chan *chan = iso->iso.chan; struct bt_iso_chan_ops *ops; - assert(chan != NULL && "NUllConnForISOSentCb"); + BT_LE_ASSERT(chan != NULL && "NUllConnForISOSentCb"); ops = chan->ops; @@ -133,7 +135,6 @@ void hci_iso(struct net_buf *buf) iso(buf)->index = bt_conn_index(iso); bt_iso_recv(iso, buf, flags); - bt_conn_unref(iso); } static void iso_get_and_clear_cb(struct bt_conn *conn, struct net_buf *buf, bt_conn_tx_cb_t *cb, @@ -286,6 +287,14 @@ static int validate_iso_setup_data_path_parms(const struct bt_iso_chan *chan, ui return -EINVAL; } + /* LE Setup ISO Data Path is one HCI command (251B usable in the 255B pool): + * sizeof(cp)+cc_len must fit, else uint8 param_len wraps and overflows. */ + CHECKIF(path->cc_len > 251 - sizeof(struct bt_hci_cp_le_setup_iso_path)) { + LOG_ERR("InvCcLenTooLarge[%u]", path->cc_len); + + return -EINVAL; + } + return 0; } @@ -430,14 +439,14 @@ void bt_iso_connected(struct bt_conn *iso) bt_iso_chan_set_state(chan, BT_ISO_STATE_CONNECTED); - if (chan->ops->connected) { + if (chan->ops && chan->ops->connected) { chan->ops->connected(chan); } } static void bt_iso_chan_disconnected(struct bt_iso_chan *chan, uint8_t reason) { - assert(chan->iso != NULL && "NullConnForIsoChan"); + BT_LE_ASSERT(chan->iso != NULL && "NullConnForIsoChan"); const uint8_t conn_type = chan->iso->iso.info.type; @@ -450,12 +459,12 @@ static void bt_iso_chan_disconnected(struct bt_iso_chan *chan, uint8_t reason) * the callback and to be more similar to the ACL disconnected callback. This also means * that the channel cannot be reused or memset in the callback */ - if (chan->ops->disconnected) { + if (chan->ops && chan->ops->disconnected) { chan->ops->disconnected(chan, reason); } /* The peripheral does not have the concept of a CIG, so once a CIS - * disconnects it is completely freed by unref'ing it + * disconnects it is completely freed. */ if (IS_ENABLED(CONFIG_BT_ISO_UNICAST) && (conn_type == BT_ISO_CHAN_TYPE_CENTRAL || conn_type == BT_ISO_CHAN_TYPE_PERIPHERAL)) { @@ -464,7 +473,6 @@ static void bt_iso_chan_disconnected(struct bt_iso_chan *chan, uint8_t reason) 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) } else { @@ -474,7 +482,7 @@ static void bt_iso_chan_disconnected(struct bt_iso_chan *chan, uint8_t reason) /* Update CIG state */ cig = get_cig(chan); - assert(cig != NULL && "CigNull"); + BT_LE_ASSERT(cig != NULL && "CigNull"); is_chan_connected = false; SYS_SLIST_FOR_EACH_CONTAINER(&cig->cis_channels, cis_chan, node) { @@ -677,7 +685,7 @@ void bt_iso_recv(struct bt_conn *iso, struct net_buf *buf, uint8_t flags) chan = iso_chan(iso); if (chan == NULL) { LOG_ERR("NoChanForIsoRecv"); - } else if (chan->ops->recv != NULL) { + } else if (chan->ops && chan->ops->recv) { chan->ops->recv(chan, &iso_info, buf->data, buf->len); } } @@ -852,12 +860,22 @@ int bt_iso_chan_disconnect(struct bt_iso_chan *chan) void bt_iso_cleanup_acl(struct bt_conn *iso) { - LOG_DBG("IsoCleanupAcl[%u]", iso->iso.acl ? iso->iso.acl->handle : UINT16_MAX); + struct bt_conn *acl = iso->iso.acl; - if (iso->iso.acl) { - bt_conn_unref(iso->iso.acl); - iso->iso.acl = NULL; + LOG_INF("IsoCleanupAcl[%u]", acl ? acl->handle : UINT16_MAX); + + if (acl == NULL) { + return; } + + iso->iso.acl = NULL; + +#if CONFIG_BT_ISO_UNICAST + /* Finalize deferred ACL wipe once no CIS still points at it. */ + if (acl->state == BT_CONN_DISCONNECTED) { + (void)bt_le_acl_conn_delete(acl->handle); + } +#endif /* CONFIG_BT_ISO_UNICAST */ } static void store_cis_info(const struct bt_hci_evt_le_cis_established *evt, struct bt_conn *iso) @@ -1007,6 +1025,10 @@ static void store_cis_info_v2(const struct bt_hci_evt_le_cis_established_v2 *evt LOG_DBG("StoreCisInfoV2"); chan = iso_conn->chan; + if (chan == NULL || chan->qos == NULL) { + LOG_ERR("CisChanOrQosNullV2[%p]", chan); + return; + } rx = chan->qos->rx; tx = chan->qos->tx; @@ -1083,8 +1105,6 @@ void hci_le_cis_established(struct net_buf *buf) iso->err = evt->status; bt_iso_disconnected(iso); } /* else we wait for disconnect event */ - - bt_conn_unref(iso); } void hci_le_cis_established_v2(struct net_buf *buf) @@ -1125,8 +1145,6 @@ void hci_le_cis_established_v2(struct net_buf *buf) iso->err = evt->status; bt_iso_disconnected(iso); } /* else we wait for disconnect event */ - - bt_conn_unref(iso); } #if defined(CONFIG_BT_ISO_PERIPHERAL) @@ -1284,7 +1302,6 @@ void hci_le_cis_req(struct net_buf *buf) if (iso) { LOG_ERR("InvCisHdl[%u]", cis_handle); hci_le_reject_cis(cis_handle, BT_HCI_ERR_CONN_LIMIT_EXCEEDED); - bt_conn_unref(iso); return; } @@ -1299,8 +1316,6 @@ void hci_le_cis_req(struct net_buf *buf) /* Add ISO connection */ iso = bt_conn_add_iso(acl); - bt_conn_unref(acl); - if (!iso) { LOG_ERR("AddCisToAclFail[%u]", acl_handle); hci_le_reject_cis(cis_handle, BT_HCI_ERR_INSUFFICIENT_RESOURCES); @@ -1316,7 +1331,8 @@ void hci_le_cis_req(struct net_buf *buf) if (err) { LOG_INF("AppRejectedCis[%d]", err); bt_iso_cleanup_acl(iso); - bt_conn_unref(iso); + /* Release the ISO slot back to the pool. */ + iso->type = BT_CONN_TYPE_NONE; hci_le_reject_cis(cis_handle, BT_HCI_ERR_INSUFFICIENT_RESOURCES); return; } @@ -1327,8 +1343,10 @@ void hci_le_cis_req(struct net_buf *buf) err = hci_le_accept_cis(cis_handle); if (err) { - bt_iso_cleanup_acl(iso); - bt_conn_unref(iso); + /* iso_accept already bound the app's chan: full teardown so it isn't left + CONNECTING with a dangling chan->iso once the slot is released. */ + iso->err = BT_HCI_ERR_INSUFFICIENT_RESOURCES; + bt_iso_disconnected(iso); hci_le_reject_cis(cis_handle, BT_HCI_ERR_INSUFFICIENT_RESOURCES); return; } @@ -1345,7 +1363,7 @@ static struct bt_conn *bt_conn_add_iso(struct bt_conn *acl) return NULL; } - iso->iso.acl = bt_conn_ref(acl); + iso->iso.acl = acl; return iso; } @@ -1649,7 +1667,7 @@ static struct bt_iso_cig *get_cig(const struct bt_iso_chan *iso_chan) return NULL; } - assert(iso_chan->iso->iso.info.unicast.cig_id < ARRAY_SIZE(cigs) && "InvCIGID"); + BT_LE_ASSERT(iso_chan->iso->iso.info.unicast.cig_id < ARRAY_SIZE(cigs) && "InvCIGID"); return &cigs[iso_chan->iso->iso.info.unicast.cig_id]; } @@ -1724,7 +1742,6 @@ static void cleanup_cig(struct bt_iso_cig *cig) 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; } @@ -1759,6 +1776,27 @@ static bool valid_cig_param(const struct bt_iso_cig_param *param, bool advanced, return false; } + /* Built in the 255B HCI cmd pool (hci.c): BT_BUF_RESERVE + the 3B cmd header + * eat into it first, and test params cost 14B per CIS instead of 9B, so + * 31 CIS (spec max) never fits. Cap num_cis to the real budget. */ + { + const size_t avail = 255U - BT_BUF_RESERVE - BT_HCI_CMD_HDR_SIZE; + size_t hdr_sz = sizeof(struct bt_hci_cp_le_set_cig_params); + size_t cis_sz = sizeof(struct bt_hci_cis_params); + +#if defined(CONFIG_BT_ISO_TEST_PARAMS) + if (advanced) { + hdr_sz = sizeof(struct bt_hci_cp_le_set_cig_params_test); + cis_sz = sizeof(struct bt_hci_cis_params_test); + } +#endif /* CONFIG_BT_ISO_TEST_PARAMS */ + + if (param->num_cis > (avail - hdr_sz) / cis_sz) { + LOG_ERR("TooLargeNumCisForHciCmd[%u][%u]", param->num_cis, advanced); + return false; + } + } + for (uint8_t i = 0; i < param->num_cis; i++) { struct bt_iso_chan *cis = param->cis_channels[i]; @@ -1989,7 +2027,8 @@ static void restore_cig(struct bt_iso_cig *cig, uint8_t existing_num_cis) * bt_iso_cig_reconfigure was called */ if (cis->iso != NULL && cis->iso->iso.info.unicast.cis_id >= existing_num_cis) { - bt_conn_unref(cis->iso); + /* Release the ISO slot back to the pool. */ + cis->iso->type = BT_CONN_TYPE_NONE; cis->iso = NULL; sys_slist_remove(&cig->cis_channels, prev, &cis->node); @@ -2241,12 +2280,12 @@ int bt_iso_chan_connect(const struct bt_iso_connect_param *param, size_t count) struct bt_iso_chan *iso_chan = param[i].iso_chan; struct bt_iso_cig *cig; - iso_chan->iso->iso.acl = bt_conn_ref(param[i].acl); + iso_chan->iso->iso.acl = param[i].acl; bt_conn_set_state(iso_chan->iso, BT_CONN_INITIATING); bt_iso_chan_set_state(iso_chan, BT_ISO_STATE_CONNECTING); cig = get_cig(iso_chan); - assert(cig && "CigNull"); + BT_LE_ASSERT(cig && "CigNull"); cig->state = BT_ISO_CIG_STATE_ACTIVE; } @@ -2256,7 +2295,7 @@ int bt_iso_chan_connect(const struct bt_iso_connect_param *param, size_t count) #endif /* CONFIG_BT_ISO_UNICAST */ #if defined(CONFIG_BT_ISO_BROADCAST) -static sys_slist_t iso_big_cbs = SYS_SLIST_STATIC_INIT(&iso_big_cbs); +static BT_ISO_EXT_RAM_BSS_ATTR sys_slist_t iso_big_cbs; static struct bt_iso_big *lookup_big_by_handle(uint8_t big_handle) { @@ -2311,7 +2350,6 @@ static void cleanup_big(struct bt_iso_big *big) 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; } @@ -2407,6 +2445,23 @@ int bt_iso_big_register_cb(struct bt_iso_big_cb *cb) return 0; } +int bt_iso_big_unregister_cb(struct bt_iso_big_cb *cb) +{ + CHECKIF(cb == NULL) { + LOG_ERR("BigCbNull"); + + return -EINVAL; + } + + if (!sys_slist_find_and_remove(&iso_big_cbs, &cb->_node)) { + LOG_ERR("BigCbNotReg[%p]", cb); + + return -EINVAL; + } + + return 0; +} + #if defined(CONFIG_BT_ISO_BROADCASTER) static int hci_le_create_big(struct bt_le_ext_adv *padv, struct bt_iso_big *big, struct bt_iso_big_create_param *param) @@ -2426,7 +2481,7 @@ static int hci_le_create_big(struct bt_le_ext_adv *padv, struct bt_iso_big *big, } bis = SYS_SLIST_PEEK_HEAD_CONTAINER(&big->bis_channels, bis, node); - assert(bis != NULL && "BisNull"); + BT_LE_ASSERT(bis != NULL && "BisNull"); /* All BIS will share the same QOS */ qos = bis->qos->tx; @@ -2489,7 +2544,7 @@ static int hci_le_create_big_test(const struct bt_le_ext_adv *padv, struct bt_is } bis = SYS_SLIST_PEEK_HEAD_CONTAINER(&big->bis_channels, bis, node); - assert(bis != NULL && "BisNull"); + BT_LE_ASSERT(bis != NULL && "BisNull"); /* All BIS will share the same QOS */ qos = bis->qos; @@ -2970,7 +3025,7 @@ int bt_iso_big_terminate(struct bt_iso_big *big) } bis = SYS_SLIST_PEEK_HEAD_CONTAINER(&big->bis_channels, bis, node); - assert(bis != NULL && "BisNull"); + BT_LE_ASSERT(bis != NULL && "BisNull"); if (IS_ENABLED(CONFIG_BT_ISO_BROADCASTER) && bis->iso->iso.info.type == BT_ISO_CHAN_TYPE_BROADCASTER) { @@ -3515,3 +3570,54 @@ void bt_iso_reset_safe(void) bt_iso_reset(); bt_le_host_unlock(); } + +void bt_le_iso_state_reset(void) +{ + /* Clear the CIG/BIG pools, the registered server and the BIG callback list. + * Not bt_iso_reset(), which is the HCI-reset path and tears down live + * channels. Call from init, before anything registers. */ + + LOG_DBG("IsoStateReset"); + +#if CONFIG_BT_ISO_CENTRAL + memset(cigs, 0, sizeof(cigs)); +#endif /* CONFIG_BT_ISO_CENTRAL */ + +#if CONFIG_BT_ISO_PERIPHERAL + iso_server = NULL; +#endif /* CONFIG_BT_ISO_PERIPHERAL */ + +#if CONFIG_BT_ISO_BROADCAST + memset(bigs, 0, sizeof(bigs)); + sys_slist_init(&iso_big_cbs); +#endif /* CONFIG_BT_ISO_BROADCAST */ +} + +size_t bt_le_iso_report_busy(void) +{ + size_t busy = 0; + + /* Number of CIG/BIG slots still allocated; each is logged at ERROR. */ + +#if CONFIG_BT_ISO_CENTRAL + /* A CIS disconnect only moves the CIG to INACTIVE; nothing but + * bt_iso_cig_terminate() frees the slot and the controller's CIG. */ + for (size_t i = 0; i < ARRAY_SIZE(cigs); i++) { + if (cigs[i].state != BT_ISO_CIG_STATE_IDLE) { + LOG_ERR("DeinitBusyCig[%u][state=%u]", i, cigs[i].state); + busy++; + } + } +#endif /* CONFIG_BT_ISO_CENTRAL */ + +#if CONFIG_BT_ISO_BROADCAST + for (size_t i = 0; i < ARRAY_SIZE(bigs); i++) { + if (atomic_test_bit(bigs[i].flags, BT_BIG_INITIALIZED)) { + LOG_ERR("DeinitBusyBig[%u]", i); + busy++; + } + } +#endif /* CONFIG_BT_ISO_BROADCAST */ + + return busy; +} diff --git a/components/bt/esp_ble_iso/host/utils/assert.c b/components/bt/esp_ble_iso/host/utils/assert.c new file mode 100644 index 00000000000..d01bdbc8e43 --- /dev/null +++ b/components/bt/esp_ble_iso/host/utils/assert.c @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include + +#include "esp_log.h" + +#include "utils/assert.h" + +#define TAG "ISO_ASSERT" + +/* Always logged (no LOG_LEVEL gate) — this is the last message before abort, and + * the user needs the context to diagnose. */ +void bt_le_assert(const char *tag, size_t info, + const char *file, int line, const char *func) +{ + esp_log_write(ESP_LOG_ERROR, TAG, + BT_ISO_LOG_COLOR_E + "E (%lu) %s: Assert[%s][info=%u][%s:%d][%s]" + BT_ISO_LOG_RESET_COLOR "\n", + esp_log_timestamp(), TAG, + tag, (unsigned)info, file, line, func); + abort(); +} diff --git a/components/bt/esp_ble_iso/host/utils/bt_str.c b/components/bt/esp_ble_iso/host/utils/bt_str.c index 43054e7e93b..b795f45675a 100644 --- a/components/bt/esp_ble_iso/host/utils/bt_str.c +++ b/components/bt/esp_ble_iso/host/utils/bt_str.c @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2022 Nordic Semiconductor ASA + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -18,10 +19,12 @@ #include #include +#include "utils/iso_attr.h" + const char *bt_hex(const void *buf, size_t len) { static const char hex[] = "0123456789abcdef"; - static char str[129]; + static BT_ISO_EXT_RAM_BSS_ATTR char str[129]; const uint8_t *b = buf; size_t i; @@ -39,7 +42,7 @@ const char *bt_hex(const void *buf, size_t len) const char *bt_addr_str(const bt_addr_t *addr) { - static char str[BT_ADDR_STR_LEN]; + static BT_ISO_EXT_RAM_BSS_ATTR char str[BT_ADDR_STR_LEN]; bt_addr_to_str(addr, str, sizeof(str)); @@ -48,7 +51,7 @@ const char *bt_addr_str(const bt_addr_t *addr) const char *bt_addr_le_str(const bt_addr_le_t *addr) { - static char str[BT_ADDR_LE_STR_LEN]; + static BT_ISO_EXT_RAM_BSS_ATTR char str[BT_ADDR_LE_STR_LEN]; bt_addr_le_to_str(addr, str, sizeof(str)); @@ -57,7 +60,7 @@ const char *bt_addr_le_str(const bt_addr_le_t *addr) const char *bt_uuid_str(const struct bt_uuid *uuid) { - static char str[BT_UUID_STR_LEN]; + static BT_ISO_EXT_RAM_BSS_ATTR char str[BT_UUID_STR_LEN]; bt_uuid_to_str(uuid, str, sizeof(str)); diff --git a/components/bt/esp_ble_iso/host/utils/include/utils/assert.h b/components/bt/esp_ble_iso/host/utils/include/utils/assert.h new file mode 100644 index 00000000000..4fe3f0957ab --- /dev/null +++ b/components/bt/esp_ble_iso/host/utils/include/utils/assert.h @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Also registered as lib_ext_funcs._assert, so tag/info stay free-form for the + * lib's own call sites; BT_LE_ASSERT() passes the stringified condition. */ +void bt_le_assert(const char *tag, size_t info, + const char *file, int line, const char *func) __attribute__((noreturn)); + +/* Use instead of assert(): CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE sets + * -DNDEBUG, which turns every assert() into a no-op and lets execution fall + * through onto the very pointer it was guarding. This one always checks. */ +#define BT_LE_ASSERT(_cond) \ + do { \ + if (!(_cond)) { \ + bt_le_assert(#_cond, 0, __FILE_NAME__, __LINE__, __func__); \ + } \ + } while (0) + +#ifdef __cplusplus +} +#endif diff --git a/components/bt/esp_ble_iso/host/utils/include/utils/iso_attr.h b/components/bt/esp_ble_iso/host/utils/include/utils/iso_attr.h new file mode 100644 index 00000000000..0061036ed80 --- /dev/null +++ b/components/bt/esp_ble_iso/host/utils/include/utils/iso_attr.h @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "esp_attr.h" + +/* Static (.bss) placement markers for the esp_ble_iso component. + * + * These sources compile into libbt.a, where object names collide + * (common/iso.c vs adapter/bluedroid/iso.c vs host/iso/iso.c, etc.), so a + * linker fragment cannot target them cleanly - placement is done per-variable + * via these attributes. + * + * - BT_ISO_EXT_RAM_BSS_ATTR: eligible for external SPIRAM (cold or + * control-plane, task-context, non-DMA). Moves to PSRAM when + * BT_ISO_BSS_SEG_EXTERNAL_MEMORY is set, else expands to nothing. Safe + * because esp_ble_iso registers no IRAM ISR (nothing here is touched with + * the flash cache disabled). + * + * - BT_ISO_CTRL_BSS_ATTR: must stay in internal DRAM - currently cannot go + * to PSRAM. Marker only (expands to nothing = default internal placement). + * Used for the real-time ISO data path and anything DMA-reachable or + * otherwise unsafe/uncertain in PSRAM. Kernel-object wrappers (k_sem/k_mutex) + * are marked CTRL too - synchronization primitives taken on hot/host paths + * (incl. the ISO dispatch), kept internal for determinism (and tiny anyway). + * net_buf pools and initialized (.data) statistics also stay internal but need + * no marker (auto-named .bss cannot take a per-var attribute; .data cannot + * use EXT_RAM_BSS). + * + * esp_ble_audio has its own equivalent (common/audio_attr.h, BT_AUDIO_* macros), + * kept separate for the audio layer. The distinct name and include prefix + * (utils/ here vs common/ there) avoid any collision on the bt component's shared + * flat include path. + */ +#if CONFIG_BT_ISO_BSS_SEG_EXTERNAL_MEMORY +#define BT_ISO_EXT_RAM_BSS_ATTR EXT_RAM_BSS_ATTR +#else +#define BT_ISO_EXT_RAM_BSS_ATTR +#endif + +#define BT_ISO_CTRL_BSS_ATTR diff --git a/components/bt/esp_ble_iso/host/utils/include/utils/mem.h b/components/bt/esp_ble_iso/host/utils/include/utils/mem.h new file mode 100644 index 00000000000..419c3300678 --- /dev/null +++ b/components/bt/esp_ble_iso/host/utils/include/utils/mem.h @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +/* Heap allocation policy for LE Audio. Every runtime allocation should use one + * of these two so intent is explicit (a bare malloc/calloc reads as "not yet + * classified" and risks being rerouted by a later PSRAM sweep). Both pin the + * allocation to a memory type with heap_caps, independent of the platform's + * default malloc policy (which, under CONFIG_SPIRAM_USE_MALLOC, may itself hand + * out PSRAM). + * + * - bt_le_ext_*: control-plane data (GATT/GAP event marshalling, profile setup, + * per-conn state, ...). Placed in external SPIRAM (MALLOC_CAP_SPIRAM) when + * BT_ISO_HEAP_EXTERNAL_MEMORY is set, else the internal heap. Returns NULL on + * SPIRAM exhaustion (no internal fallback). + * + * - bt_le_int_*: the real-time ISO TX/RX data path (SDU packet buffers handed to + * the controller, per-SDU nodes/events). Forced to internal DRAM + * (MALLOC_CAP_INTERNAL): controller/DMA-reachable and latency-critical, so it + * must never land in PSRAM. Plain malloc/calloc would NOT guarantee this - + * under CONFIG_SPIRAM_USE_MALLOC they can return PSRAM. + * + * Both are freed with plain free() (valid for heap_caps allocations in ESP-IDF). + */ +void *bt_le_ext_malloc(size_t size); +void *bt_le_ext_calloc(size_t n, size_t size); +void *bt_le_int_malloc(size_t size); +void *bt_le_int_calloc(size_t n, size_t size); diff --git a/components/bt/esp_ble_iso/host/utils/utils.c b/components/bt/esp_ble_iso/host/utils/keys.c similarity index 92% rename from components/bt/esp_ble_iso/host/utils/utils.c rename to components/bt/esp_ble_iso/host/utils/keys.c index 0a300567a12..969d2d9b0b7 100644 --- a/components/bt/esp_ble_iso/host/utils/utils.c +++ b/components/bt/esp_ble_iso/host/utils/keys.c @@ -24,27 +24,12 @@ #else #include "host/ble_gap.h" #include "host/ble_store.h" + +#include "utils/assert.h" #endif LOG_MODULE_REGISTER(ISO_UTILS, CONFIG_BT_ISO_LOG_LEVEL); -struct bt_dev bt_dev; - -uint8_t bt_get_phy(uint8_t hci_phy) -{ - switch (hci_phy) { - case BT_HCI_LE_PHY_1M: - return BT_GAP_LE_PHY_1M; - case BT_HCI_LE_PHY_2M: - return BT_GAP_LE_PHY_2M; - case BT_HCI_LE_PHY_CODED: - return BT_GAP_LE_PHY_CODED; - default: - LOG_WRN("UnknownHciPhy[%u]", hci_phy); - return 0; - } -} - /* Query the active host's persistent bond store (the old local key_pool was never * populated, so bt_le_bond_exists() was always false). `id` ignored (single identity); * addresses stay in native host byte order. */ @@ -57,7 +42,7 @@ void bt_foreach_bond(uint8_t id, void (*func)(const struct bt_bond_info *info, int num = esp_ble_get_bond_device_num(); esp_ble_bond_dev_t *list; - assert(func); + BT_LE_ASSERT(func); (void)id; LOG_DBG("[B]ForeachBond[%d]", num); @@ -152,7 +137,7 @@ void bt_foreach_bond(uint8_t id, void (*func)(const struct bt_bond_info *info, ble_addr_t peers[CONFIG_BT_MAX_PAIRED]; int num = 0; - assert(func); + BT_LE_ASSERT(func); (void)id; if (ble_store_util_bonded_peers(peers, &num, ARRAY_SIZE(peers)) != 0) { diff --git a/components/bt/esp_ble_iso/host/utils/mem.c b/components/bt/esp_ble_iso/host/utils/mem.c new file mode 100644 index 00000000000..514fd9407ca --- /dev/null +++ b/components/bt/esp_ble_iso/host/utils/mem.c @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "esp_heap_caps.h" + +#include "utils/mem.h" + +void *bt_le_ext_malloc(size_t size) +{ +#if CONFIG_BT_ISO_HEAP_EXTERNAL_MEMORY + return heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); +#else + return malloc(size); +#endif +} + +void *bt_le_ext_calloc(size_t n, size_t size) +{ +#if CONFIG_BT_ISO_HEAP_EXTERNAL_MEMORY + return heap_caps_calloc(n, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); +#else + return calloc(n, size); +#endif +} + +/* ISO TX/RX data path: force internal DRAM. Plain malloc/calloc follow IDF's + * default heap policy and could return PSRAM under CONFIG_SPIRAM_USE_MALLOC; + * MALLOC_CAP_INTERNAL guarantees internal regardless. */ +void *bt_le_int_malloc(size_t size) +{ + return heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); +} + +void *bt_le_int_calloc(size_t n, size_t size) +{ + return heap_caps_calloc(n, size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); +} diff --git a/components/bt/esp_ble_iso/host/utils/timer.c b/components/bt/esp_ble_iso/host/utils/timer.c index 781de09f890..61d4dfee826 100644 --- a/components/bt/esp_ble_iso/host/utils/timer.c +++ b/components/bt/esp_ble_iso/host/utils/timer.c @@ -28,24 +28,22 @@ static void iso_timer_cb(void *arg) struct k_work *work = arg; int err; - assert(work); - assert(work->timer); - assert(work->handler); + BT_LE_ASSERT(work); + BT_LE_ASSERT(work->timer); + BT_LE_ASSERT(work->handler); - err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_TIMER_EVENT, work, 0); + err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_TIMER_EVENT, work, work->gen); if (err) { - LOG_ERR("IsoTimerPostFail[%d]", err); + ISO_POST_FAIL_LOG(err, "TimerCbPostFail[%d]", err); } } int k_work_submit(struct k_work *work) { - LOG_DBG("WorkSubmit[%p]", work); - - assert(work); + BT_LE_ASSERT(work); if (work->handler == NULL) { - LOG_WRN("WorkHdlrNull"); + LOG_WRN("TimerSubmitHdlrNull"); return -EINVAL; } @@ -67,62 +65,56 @@ bool k_work_is_pending(struct k_work *work) { bool is_pending; - LOG_DBG("WorkIsPending[%p]", work); - - assert(work); + BT_LE_ASSERT(work); if (work->handler == NULL) { - LOG_WRN("WorkHdlrNull"); + LOG_WRN("TimerIsPendingHdlrNull"); return false; } is_pending = (work->timer ? esp_timer_is_active(work->timer) : false); - LOG_DBG("%sPending", is_pending ? "Is" : "Not"); + LOG_DBG("TimerIsPendingRet[%s]", is_pending ? "Is" : "Not"); return is_pending; } void k_work_init(struct k_work *work, k_work_handler_t handler) { - LOG_DBG("WorkInit[%p]", work); + BT_LE_ASSERT(work); - assert(work); + /* Clear timer/timeout_us/gen too: callers must not have to zero-init */ + memset(work, 0, sizeof(*work)); work->handler = handler; } struct k_work_delayable *k_work_delayable_from_work(struct k_work *work) { - LOG_DBG("DworkFromWork[%p]", work); - - assert(work); - + BT_LE_ASSERT(work); return (struct k_work_delayable *)work; } void k_work_init_delayable(struct k_work_delayable *dwork, k_work_handler_t handler) { - LOG_DBG("DworkInit[%p]", dwork); - - assert(dwork); + BT_LE_ASSERT(dwork); const esp_timer_create_args_t timer_args = { .callback = &iso_timer_cb, .arg = &dwork->work, - .name = "iso_timer", + .name = "IsoTimer", }; int err; if (dwork->work.timer) { - LOG_WRN("TimerCreated"); + LOG_WRN("TimerInitAlreadyCreated"); return; } err = esp_timer_create(&timer_args, (esp_timer_handle_t *)&dwork->work.timer); if (err) { - LOG_ERR("CreateTimerFail[%d]", err); + LOG_ERR("TimerInitCreateFail[%d]", err); /* Reset handler so the object remains in a clean uninitialized state */ dwork->work.handler = NULL; return; @@ -135,18 +127,21 @@ void k_work_deinit_delayable(struct k_work_delayable *dwork) { int err; - LOG_DBG("DworkDeinit[%p]", dwork); - - assert(dwork); + BT_LE_ASSERT(dwork); if (dwork->work.timer == NULL) { - LOG_INF("TimerNotCreated"); + LOG_INF("TimerDeinitNotCreated"); return; } + dwork->work.gen++; + + /* esp_timer_delete rejects a running timer */ + esp_timer_stop(dwork->work.timer); + err = esp_timer_delete(dwork->work.timer); if (err) { - LOG_ERR("DeleteTimerFail[%d]", err); + LOG_ERR("TimerDeinitDelFail[%d]", err); return; } @@ -155,16 +150,15 @@ void k_work_deinit_delayable(struct k_work_delayable *dwork) int k_work_cancel_delayable(struct k_work_delayable *dwork) { - LOG_DBG("DworkCancel[%p]", dwork); - - assert(dwork); + BT_LE_ASSERT(dwork); if (dwork->work.timer == NULL) { - LOG_INF("TimerNotCreated"); + LOG_INF("TimerCancelNotCreated"); return -EINVAL; } esp_timer_stop(dwork->work.timer); + dwork->work.gen++; return 0; } @@ -172,19 +166,19 @@ int k_work_cancel_delayable(struct k_work_delayable *dwork) bool k_work_cancel_delayable_sync(struct k_work_delayable *dwork, struct k_work_sync *sync) { - LOG_DBG("DworkCancelSync[%p]", dwork); - - assert(dwork); + BT_LE_ASSERT(dwork); ARG_UNUSED(sync); if (dwork->work.timer == NULL) { - LOG_INF("TimerNotCreated"); + LOG_INF("TimerCancelSyncNotCreated"); return false; } esp_timer_stop(dwork->work.timer); + dwork->work.gen++; + /* TODO: check the return result */ return false; } @@ -193,17 +187,17 @@ int k_work_schedule(struct k_work_delayable *dwork, k_timeout_t ms) { int err; - LOG_DBG("WorkSchedule[%p][%u]", dwork, ms); - - assert(dwork); + BT_LE_ASSERT(dwork); if (dwork->work.timer == NULL) { - LOG_WRN("TimerNotCreated"); + LOG_WRN("TimerSchNotCreated"); return -EINVAL; } esp_timer_stop(dwork->work.timer); + dwork->work.gen++; + dwork->work.timeout_us = esp_timer_get_time() + (int64_t)ms * 1000; if (ms == K_NO_WAIT) { @@ -213,7 +207,7 @@ int k_work_schedule(struct k_work_delayable *dwork, k_timeout_t ms) err = esp_timer_start_once(dwork->work.timer, ms * 1000); if (err) { - LOG_ERR("StartTimerFail[%d]", err); + LOG_ERR("TimerSchStartFail[%d]", err); return -EIO; } @@ -224,17 +218,17 @@ int k_work_reschedule(struct k_work_delayable *dwork, k_timeout_t ms) { int err; - LOG_DBG("WorkReschedule[%p][%u]", dwork, ms); - - assert(dwork); + BT_LE_ASSERT(dwork); if (dwork->work.timer == NULL) { - LOG_WRN("TimerNotCreated"); + LOG_WRN("TimerReschNotCreated"); return -EINVAL; } esp_timer_stop(dwork->work.timer); + dwork->work.gen++; + dwork->work.timeout_us = esp_timer_get_time() + (int64_t)ms * 1000; if (ms == K_NO_WAIT) { @@ -244,7 +238,7 @@ int k_work_reschedule(struct k_work_delayable *dwork, k_timeout_t ms) err = esp_timer_start_once(dwork->work.timer, ms * 1000); if (err) { - LOG_ERR("RestartTimerFail[%d]", err); + LOG_ERR("TimerReschStartFail[%d]", err); return -EIO; } @@ -255,21 +249,21 @@ int k_work_schedule_periodic_us(struct k_work_delayable *dwork, uint64_t period_ { int err; - LOG_DBG("WorkSchedulePeriodicUs[%p][%llu]", dwork, (unsigned long long)period_us); - - assert(dwork); - assert(period_us > 0); + BT_LE_ASSERT(dwork); + BT_LE_ASSERT(period_us > 0); if (dwork->work.timer == NULL) { - LOG_WRN("TimerNotCreated"); + LOG_WRN("TimerPeriodicNotCreated"); return -EINVAL; } esp_timer_stop(dwork->work.timer); + dwork->work.gen++; + err = esp_timer_start_periodic(dwork->work.timer, period_us); if (err) { - LOG_ERR("StartPeriodicTimerFail[%d]", err); + LOG_ERR("TimerPeriodicStartFail[%d]", err); return -EIO; } @@ -289,17 +283,15 @@ k_timeout_t k_work_delayable_remaining_get(struct k_work_delayable *dwork) k_timeout_t timeout; int64_t delta_us; - LOG_DBG("DworkRemainingGet[%p]", dwork); - - assert(dwork); + BT_LE_ASSERT(dwork); if (dwork->work.timer == NULL) { - LOG_WRN("TimerNotCreated"); + LOG_WRN("TimerRemainingNotCreated"); return 0; } if (dwork->work.timeout_us == 0) { - LOG_DBG("WorkTimeoutZero"); + LOG_DBG("TimerRemainingTimeoutZero"); return 0; } @@ -307,14 +299,19 @@ k_timeout_t k_work_delayable_remaining_get(struct k_work_delayable *dwork) timeout = (delta_us > 0 ? (k_timeout_t)(delta_us / 1000) : 0); - LOG_DBG("Timeout[%u]", timeout); + LOG_DBG("TimerRemaining[%u]", timeout); return timeout; } -void bt_le_timer_handle_event(void *arg) +void bt_le_timer_handle_event(void *arg, size_t gen) { struct k_work *work = arg; - k_work_submit_safe(work); + bt_le_host_lock(); + /* Drop the event if the work was re-armed or cancelled after it fired */ + if ((uint32_t)gen == work->gen) { + k_work_submit(work); + } + bt_le_host_unlock(); } diff --git a/components/bt/esp_ble_iso/include/zephyr/autoconf.h b/components/bt/esp_ble_iso/include/zephyr/autoconf.h index 5767bc700f5..fad9457f989 100644 --- a/components/bt/esp_ble_iso/include/zephyr/autoconf.h +++ b/components/bt/esp_ble_iso/include/zephyr/autoconf.h @@ -41,7 +41,6 @@ #endif /* CONFIG_BT_BLE_FEAT_PERIODIC_ADV_SYNC_TRANSFER */ #define CONFIG_BT_MAX_CONN CONFIG_BT_ACL_CONNECTIONS -#define CONFIG_BT_SMP CONFIG_BT_BLE_SMP_ENABLE #define CONFIG_BT_MAX_PAIRED CONFIG_BT_SMP_MAX_BONDS #if CONFIG_BT_ISO_UNICAST && CONFIG_BT_ISO_BROADCAST @@ -57,7 +56,6 @@ _Static_assert(CONFIG_BT_ISO_MAX_CHAN == 0, "Too large ISO channels"); #else /* CONFIG_BT_BLUEDROID_ENABLED */ #define CONFIG_BT_MAX_CONN CONFIG_BT_NIMBLE_MAX_CONNECTIONS -#define CONFIG_BT_SMP CONFIG_BT_NIMBLE_SECURITY_ENABLE #define CONFIG_BT_MAX_PAIRED CONFIG_BT_NIMBLE_MAX_BONDS #if CONFIG_BT_NIMBLE_MAX_EXT_ADV_INSTANCES diff --git a/components/bt/esp_ble_iso/include/zephyr/bluetooth/bluetooth.h b/components/bt/esp_ble_iso/include/zephyr/bluetooth/bluetooth.h index 8a73206c08d..e38b8902b00 100644 --- a/components/bt/esp_ble_iso/include/zephyr/bluetooth/bluetooth.h +++ b/components/bt/esp_ble_iso/include/zephyr/bluetooth/bluetooth.h @@ -406,6 +406,16 @@ int bt_le_per_adv_sync_get_info(struct bt_le_per_adv_sync *per_adv_sync, struct bt_le_per_adv_sync *bt_le_per_adv_sync_lookup_addr(const bt_addr_le_t *adv_addr, uint8_t sid); +/** + * @brief Look up a local extended advertising set by advertiser address. + * + * @param adv_addr Advertiser address. + * @param sid The advertising set ID. + * + * @return Extended advertising set object or NULL if not found. + */ +struct bt_le_ext_adv *bt_le_ext_adv_lookup_addr(const bt_addr_le_t *adv_addr, uint8_t sid); + /** * @brief Register periodic advertising sync callbacks. * @@ -422,6 +432,19 @@ struct bt_le_per_adv_sync *bt_le_per_adv_sync_lookup_addr(const bt_addr_le_t *ad */ int bt_le_per_adv_sync_cb_register(struct bt_le_per_adv_sync_cb *cb); +/** + * @brief Unregister periodic advertising sync callbacks. + * + * Counterpart of @ref bt_le_per_adv_sync_cb_register(). The list is boot-scoped, + * so a module that stops listening must remove itself or keep being invoked. + * + * @param cb Callback struct previously registered. + * + * @retval 0 Success. + * @retval -ENOENT if @p cb was not registered. + */ +int bt_le_per_adv_sync_cb_unregister(struct bt_le_per_adv_sync_cb *cb); + /** LE scan parameters */ struct bt_le_scan_param { /** Scan type. @ref BT_LE_SCAN_TYPE_ACTIVE or @ref BT_LE_SCAN_TYPE_PASSIVE. */ diff --git a/components/bt/esp_ble_iso/include/zephyr/bluetooth/conn.h b/components/bt/esp_ble_iso/include/zephyr/bluetooth/conn.h index e9fcbb6ab6a..02cb1d05049 100644 --- a/components/bt/esp_ble_iso/include/zephyr/bluetooth/conn.h +++ b/components/bt/esp_ble_iso/include/zephyr/bluetooth/conn.h @@ -231,44 +231,6 @@ int bt_conn_get_info(const struct bt_conn *conn, struct bt_conn_info *info); */ int bt_conn_disconnect(struct bt_conn *conn, uint8_t reason); -/** @brief Set security level for a connection. - * - * This function enable security (encryption) for a connection. If the device - * has bond information for the peer with sufficiently strong key encryption - * will be enabled. If the connection is already encrypted with sufficiently - * strong key this function does nothing. - * - * If the device has no bond information for the peer and is not already paired - * then the pairing procedure will be initiated. Note that @p sec has no effect - * on the security level selected for the pairing process. The selection is - * instead controlled by the values of the registered @ref bt_conn_auth_cb. If - * the device has bond information or is already paired and the keys are too - * weak then the pairing procedure will be initiated. - * - * This function may return an error if the required level of security defined using - * @p sec is not possible to achieve due to local or remote device limitation - * (e.g., input output capabilities), or if the maximum number of paired devices - * has been reached. - * - * This function may return an error if the pairing procedure has already been - * initiated by the local device or the peer device. - * - * @note When @kconfig{CONFIG_BT_SMP_SC_ONLY} is enabled then the security - * level will always be level 4. - * - * @note When @kconfig{CONFIG_BT_SMP_OOB_LEGACY_PAIR_ONLY} is enabled then the - * security level will always be level 3. - * - * @note When @ref BT_SECURITY_FORCE_PAIR within @p sec is enabled then the pairing - * procedure will always be initiated. - * - * @param conn Connection object. - * @param sec Requested minimum security level. - * - * @return 0 on success or negative error - */ -int bt_conn_set_security(struct bt_conn *conn, bt_security_t sec); - enum bt_security_err { /** Security procedure successful. */ BT_SECURITY_ERR_SUCCESS, @@ -417,6 +379,7 @@ int bt_conn_cb_register_safe(struct bt_conn_cb *cb); * @retval -ENOENT if @p cb was not registered */ int bt_conn_cb_unregister(struct bt_conn_cb *cb); +int bt_conn_cb_unregister_safe(struct bt_conn_cb *cb); /** * @brief Register a callback structure for connection events. diff --git a/components/bt/esp_ble_iso/include/zephyr/bluetooth/iso.h b/components/bt/esp_ble_iso/include/zephyr/bluetooth/iso.h index a555865eb22..04bf14e3dc1 100644 --- a/components/bt/esp_ble_iso/include/zephyr/bluetooth/iso.h +++ b/components/bt/esp_ble_iso/include/zephyr/bluetooth/iso.h @@ -1294,6 +1294,8 @@ int bt_iso_big_terminate_safe(struct bt_iso_big *big); * * @return 0 in case of success or negative value in case of error. */ +int bt_iso_big_sync(struct bt_le_per_adv_sync *sync, struct bt_iso_big_sync_param *param, + struct bt_iso_big **out_big); int bt_iso_big_sync_safe(struct bt_le_per_adv_sync *sync, struct bt_iso_big_sync_param *param, struct bt_iso_big **out_big); diff --git a/components/bt/esp_ble_iso/include/zephyr/kernel.h b/components/bt/esp_ble_iso/include/zephyr/kernel.h index b3ff4259442..876e9f1dfda 100644 --- a/components/bt/esp_ble_iso/include/zephyr/kernel.h +++ b/components/bt/esp_ble_iso/include/zephyr/kernel.h @@ -9,7 +9,6 @@ #include #include -#include #include #include @@ -21,6 +20,8 @@ #include "freertos/semphr.h" #include "toolchain.h" +#include "utils/assert.h" + #ifdef __cplusplus extern "C" { #endif @@ -36,17 +37,17 @@ struct k_mutex { static inline void k_mutex_create(struct k_mutex *mutex) { - assert(mutex); - assert(mutex->handle == NULL); + BT_LE_ASSERT(mutex); + BT_LE_ASSERT(mutex->handle == NULL); mutex->handle = xSemaphoreCreateRecursiveMutex(); - assert(mutex->handle); + BT_LE_ASSERT(mutex->handle); } static inline void k_mutex_delete(struct k_mutex *mutex) { - assert(mutex); - assert(mutex->handle); + BT_LE_ASSERT(mutex); + BT_LE_ASSERT(mutex->handle); vSemaphoreDelete(mutex->handle); mutex->handle = NULL; @@ -65,8 +66,8 @@ static inline void k_mutex_delete(struct k_mutex *mutex) static inline int k_mutex_lock(struct k_mutex *mutex, uint32_t timeout) { - assert(mutex); - assert(mutex->handle); + BT_LE_ASSERT(mutex); + BT_LE_ASSERT(mutex->handle); if (xSemaphoreTakeRecursive(mutex->handle, timeout) == pdTRUE) { return 0; @@ -89,8 +90,8 @@ static inline int k_mutex_lock(struct k_mutex *mutex, uint32_t timeout) static inline int k_mutex_unlock(struct k_mutex *mutex) { - assert(mutex); - assert(mutex->handle); + BT_LE_ASSERT(mutex); + BT_LE_ASSERT(mutex->handle); if (xSemaphoreGiveRecursive(mutex->handle) != pdTRUE) { K_MUTEX_LOG_ERR("UnlockFail"); @@ -112,18 +113,18 @@ struct k_sem { static inline void k_sem_create(struct k_sem *sem) { - assert(sem); - assert(sem->handle == NULL); + BT_LE_ASSERT(sem); + BT_LE_ASSERT(sem->handle == NULL); sem->handle = xSemaphoreCreateBinary(); - assert(sem->handle); + BT_LE_ASSERT(sem->handle); sem->result = 0; } static inline void k_sem_delete(struct k_sem *sem) { - assert(sem); - assert(sem->handle); + BT_LE_ASSERT(sem); + BT_LE_ASSERT(sem->handle); vSemaphoreDelete(sem->handle); sem->handle = NULL; @@ -139,10 +140,11 @@ static inline void k_sem_delete(struct k_sem *sem) #define K_SEM_LOG_ERR(fmt, args...) BT_ISO_LOGE("ISO_SEM", fmt, ## args) #endif -static inline int k_sem_take(struct k_sem *sem, uint32_t timeout) +/* Implementation of k_sem_take; call through the macro below. */ +static inline int k_sem_take_dbg(struct k_sem *sem, uint32_t timeout, const char *func) { - assert(sem); - assert(sem->handle); + BT_LE_ASSERT(sem); + BT_LE_ASSERT(sem->handle); /* Do NOT touch sem->result here. The producer may have already written * it and called k_sem_give before this take ran (BTU/HCI cb on a @@ -155,17 +157,32 @@ static inline int k_sem_take(struct k_sem *sem, uint32_t timeout) } #if !CONFIG_BT_ISO_NO_LOG && (CONFIG_BT_ISO_LOG_LEVEL >= BT_ISO_LOG_ERROR) - K_SEM_LOG_ERR("TakeFail[self=%s]", pcTaskGetName(NULL)); + K_SEM_LOG_ERR("TakeFail[%s][%s]", func, pcTaskGetName(NULL)); #else + ARG_UNUSED(func); K_SEM_LOG_ERR("TakeFail"); #endif return -EIO; } +/* Macro, not a wrapper function, so __func__ names the CALLER — that identifies + * both the wedged operation and the sem, with no per-sem RAM. */ +#define k_sem_take(sem, timeout) k_sem_take_dbg((sem), (timeout), __func__) + +/* Silent take for slice-polling callers, where only the final expiry is an error + * and k_sem_take would log TakeFail per slice. Same sem->result contract. */ +static inline int k_sem_take_poll(struct k_sem *sem, uint32_t timeout) +{ + BT_LE_ASSERT(sem); + BT_LE_ASSERT(sem->handle); + + return (xSemaphoreTake(sem->handle, timeout) == pdTRUE) ? 0 : -EIO; +} + static inline int k_sem_give(struct k_sem *sem) { - assert(sem); - assert(sem->handle); + BT_LE_ASSERT(sem); + BT_LE_ASSERT(sem->handle); if (xSemaphoreGive(sem->handle) != pdTRUE) { K_SEM_LOG_ERR("GiveFail"); @@ -181,13 +198,20 @@ static inline int k_sem_give(struct k_sem *sem) * caller would see uninitialized response data. */ static inline void k_sem_reset(struct k_sem *sem) { - assert(sem); - assert(sem->handle); + BT_LE_ASSERT(sem); + BT_LE_ASSERT(sem->handle); xQueueReset(sem->handle); sem->result = 0; } +/* Queue */ + +/* Bounded wait for a reliable-tier task-queue post instead of portMAX_DELAY, + * so a wedged consumer can't freeze an external producer (esp_timer / host + * task) and stall the ISO data path. */ +#define K_QUEUE_SHORT (1000 / portTICK_PERIOD_MS) + /* Timer */ typedef uint32_t k_timeout_t; @@ -202,6 +226,10 @@ typedef uint32_t k_timeout_t; #define K_MINUTES(m) K_SECONDS((m) * 60) #define K_HOURS(h) K_MINUTES((h) * 60) +/* Defer work onto iso_task. In this port K_NO_WAIT runs the handler INLINE + * (timer.c), which fires e.g. GATT indications before the CP write response. */ +#define K_NO_WAIT_ASYNC K_MSEC(1) + struct k_work; typedef void (*k_work_handler_t)(struct k_work *work); @@ -211,6 +239,8 @@ struct k_work { k_work_handler_t handler; int64_t timeout_us; void *user_data; + uint32_t gen; /* Bumped on (re)schedule/cancel/deinit so a timer event + * queued before the change is skipped (see timer.c). */ }; struct k_work_sync { diff --git a/components/bt/esp_ble_iso/include/zephyr/logging/log.h b/components/bt/esp_ble_iso/include/zephyr/logging/log.h index d061b50e331..715d5d97e6f 100644 --- a/components/bt/esp_ble_iso/include/zephyr/logging/log.h +++ b/components/bt/esp_ble_iso/include/zephyr/logging/log.h @@ -15,6 +15,8 @@ #include "esp_log.h" +#include "utils/assert.h" + /* esp_ble_iso, esp_ble_audio and any future ISO consumer are compiled * under the unified BLE_ISO compression channel and share one * iso_log_index.h with one monotonic log-id counter — no per-consumer @@ -112,13 +114,13 @@ extern "C" { #define NET_BUF_WARN(fmt, args...) /* TBD */ #define NET_BUF_INFO(fmt, args...) /* TBD */ #define NET_BUF_DBG(fmt, args...) /* TBD */ -#define NET_BUF_ASSERT assert +#define NET_BUF_ASSERT BT_LE_ASSERT #define NET_BUF_SIMPLE_ERR(fmt, args...) /* TBD */ #define NET_BUF_SIMPLE_WARN(fmt, args...) /* TBD */ #define NET_BUF_SIMPLE_INFO(fmt, args...) /* TBD */ #define NET_BUF_SIMPLE_DBG(fmt, args...) /* TBD */ -#define NET_BUF_SIMPLE_ASSERT assert +#define NET_BUF_SIMPLE_ASSERT BT_LE_ASSERT #ifdef __cplusplus } diff --git a/components/bt/esp_ble_iso/include/zephyr/sys/__assert.h b/components/bt/esp_ble_iso/include/zephyr/sys/__assert.h index b5564e10f70..13a8c09595d 100644 --- a/components/bt/esp_ble_iso/include/zephyr/sys/__assert.h +++ b/components/bt/esp_ble_iso/include/zephyr/sys/__assert.h @@ -8,15 +8,17 @@ #define ZEPHYR_INCLUDE_SYS_ASSERT_H_ #include -#include + +#include "utils/assert.h" #ifdef __cplusplus extern "C" { #endif -#define __ASSERT_NO_MSG(test) assert(test) +#define __ASSERT_NO_MSG(test) BT_LE_ASSERT(test) -#define __ASSERT(test, fmt, ...) assert(test) +/* fmt is dropped as before; BT_LE_ASSERT logs the stringified test instead. */ +#define __ASSERT(test, fmt, ...) BT_LE_ASSERT(test) #ifdef __cplusplus } diff --git a/components/bt/esp_ble_iso/include/zephyr/toolchain.h b/components/bt/esp_ble_iso/include/zephyr/toolchain.h index 7c7d6c2570e..4255ba03555 100644 --- a/components/bt/esp_ble_iso/include/zephyr/toolchain.h +++ b/components/bt/esp_ble_iso/include/zephyr/toolchain.h @@ -11,8 +11,6 @@ #include #include -#define SYS_INIT(init_fn, level, prio) int init_fn ## _v2(void) { return init_fn(); } - #ifndef BUILD_ASSERT #define BUILD_ASSERT(EXPR, MSG...) _Static_assert(EXPR, ## MSG) #endif diff --git a/docs/en/api-reference/bluetooth/esp-ble-audio.rst b/docs/en/api-reference/bluetooth/esp-ble-audio.rst index 91c597bb37d..157cd459dc5 100644 --- a/docs/en/api-reference/bluetooth/esp-ble-audio.rst +++ b/docs/en/api-reference/bluetooth/esp-ble-audio.rst @@ -51,6 +51,7 @@ Application Examples * :example:`bluetooth/esp_ble_audio/cap/acceptor` demonstrates how to act as a CAP Acceptor for unicast and broadcast flows. * :example:`bluetooth/esp_ble_audio/cap/initiator` demonstrates how to act as a CAP Initiator for unicast and broadcast flows. + * :example:`bluetooth/esp_ble_audio/cap/handover` demonstrates how to perform CAP handovers between unicast and broadcast flows. * **TMAP (Telephony and Media Audio Profile)** 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 109f05c6b86..6ad1df63d7d 100644 --- a/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/README.md +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/README.md @@ -64,7 +64,7 @@ For `esp32s31`, replace the chip overlay accordingly. 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. +9. `stream_started_cb` resets per-stream RX metrics and increments `stream_count_started`; `stream_recv_cb` updates metrics via `example_audio_rx_metrics_on_recv()`. When all streams have stopped, `stream_stopped_cb` clears `stream_started`; the sink itself is deleted from `broadcast_sink_stopped_cb` (the `stopped` sink callback), which runs once BASS has cleared `bis_sync` — deleting from `stream_stopped_cb` would race `rem_src` while `bis_sync` is still non-zero. `pa_sync_lost()` clears the cached `req_recv_state`, deletes any sink, and restarts the scanner. ## Expected Log @@ -92,6 +92,7 @@ On teardown / sync loss: ``` I (xxx) BAP_BSNK: [SNK #0] Stream stopped, reason 0x... (.../...) +I (xxx) BAP_BSNK: Broadcast sink stopped, reason 0x... I (xxx) BAP_BSNK: PA sync lost: sync_handle ... reason 0x... I (xxx) BAP_BSNK: PA sync terminated ``` @@ -105,4 +106,4 @@ Run [broadcast_source](../broadcast_source/) on a second board. Expected interac 3. Source's BIGInfo advertises the BIG as encrypted (broadcast code `"1234"`); sink reports `BIG encrypted`. 4. Source starts the BIG and the two BIS streams (`FRONT_LEFT`, `FRONT_RIGHT`); sink calls `esp_ble_audio_bap_broadcast_sink_sync()` with the chosen BIS bitfield and the matching broadcast code. 5. Source's TX scheduler keeps pushing SDUs at `preset_active.qos.interval`; sink stream `recv` callbacks deliver the data and update RX metrics. -6. Stopping the source (or losing PA sync) triggers `stream_stopped_cb` on the sink, which deletes the broadcast sink and resumes scanning. +6. Stopping the source (or losing PA sync) tears down the BIG; `broadcast_sink_stopped_cb` then deletes the sink (after BASS clears `bis_sync`) and scanning resumes. 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 1516d846817..5f4bd4849da 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 @@ -209,23 +209,14 @@ static int bis_sync_req_cb(esp_ble_conn_t *conn, stream_started ? "streaming" : "not streaming"); if (stream_started && requested_bis_sync == 0) { - /* The stream stopped callback will be called as part of this, and - * we do not need to wait for any events from the controller. Thus, - * when this returns, the `stream_started` is back to false. + /* stop() tears down the BIG; broadcast_sink_stopped_cb deletes the + * sink after BASS bis_sync has been cleared. */ err = esp_ble_audio_bap_broadcast_sink_stop(broadcast_sink); if (err) { ESP_LOGE(TAG, "Failed to stop broadcast sink, err %d", err); return -EIO; } - - err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink); - if (err) { - ESP_LOGE(TAG, "Failed to delete broadcast sink, err %d", err); - return -EIO; - } - - broadcast_sink = NULL; } return 0; @@ -319,9 +310,36 @@ static void syncable_cb(esp_ble_audio_bap_broadcast_sink_t *sink, } } +static void broadcast_sink_stopped_cb(esp_ble_audio_bap_broadcast_sink_t *sink, + uint8_t reason) +{ + esp_err_t err; + + ESP_LOGI(TAG, "Broadcast sink stopped, reason 0x%02x", reason); + + stream_started = false; + + /* Called from big_stopped after update_recv_state_big_cleared(), so + * BASS bis_sync is already 0 and rem_src inside delete can succeed. + * Do not delete from stream_ops.stopped — that runs before bis_sync clear. + */ + if (broadcast_sink == NULL) { + return; + } + + err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink); + if (err) { + ESP_LOGE(TAG, "Failed to delete broadcast sink, err %d", err); + return; + } + + broadcast_sink = NULL; +} + static esp_ble_audio_bap_broadcast_sink_cb_t broadcast_sink_cbs = { .base_recv = base_recv_cb, .syncable = syncable_cb, + .stopped = broadcast_sink_stopped_cb, }; static int stream_index(const esp_ble_audio_bap_stream_t *stream) @@ -352,21 +370,14 @@ 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_err_t err; - 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; - - err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink); - if (err) { - ESP_LOGE(TAG, "Failed to delete broadcast sink, err %d", err); - return; - } - - broadcast_sink = NULL; + /* Sink delete is deferred to broadcast_sink_stopped_cb (after BASS + * bis_sync clear). Deleting here races rem_src and leaves WRNs. + */ } } 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 85ae6dd730f..d18f16f3ce6 100644 --- a/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/sdkconfig.defaults @@ -29,3 +29,24 @@ CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=60 CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y CONFIG_FREERTOS_HZ=1000 + +# To place LE Audio .bss + control-plane heap in PSRAM, uncomment the +# lines below (needs a PSRAM-capable target, e.g. esp32s31/esp32h4). +# CONFIG_SPIRAM=y +# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_HEAP_EXTERNAL_MEMORY=y +# CONFIG_BT_AUDIO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_AUDIO_HEAP_EXTERNAL_MEMORY=y + +# Bandwidth-optimized (critical-only) compressed BLE Audio/ISO logging over +# UART DMA. Pins shown are for esp32s31 (UART0 TX = GPIO58, 3000000 baud); set +# CONFIG_BLE_LOG_PRPH_UART_DMA_* values to the port/pin your capture reads. +# CONFIG_BLE_LOG_ENABLED=y +# CONFIG_BT_LOG_CRITICAL_ONLY=y +# CONFIG_BLE_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_LOG_PRPH_UART_DMA=y +# CONFIG_BLE_LOG_PRPH_UART_DMA_PORT=0 +# CONFIG_BLE_LOG_PRPH_UART_DMA_BAUD_RATE=3000000 +# CONFIG_BLE_LOG_PRPH_UART_DMA_TX_IO_NUM=58 diff --git a/examples/bluetooth/esp_ble_audio/bap/broadcast_source/sdkconfig.defaults b/examples/bluetooth/esp_ble_audio/bap/broadcast_source/sdkconfig.defaults index cbc059c54a9..e714f62b10a 100644 --- a/examples/bluetooth/esp_ble_audio/bap/broadcast_source/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_source/sdkconfig.defaults @@ -19,3 +19,24 @@ CONFIG_BT_BAP_BROADCAST_SOURCE=y CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y CONFIG_FREERTOS_HZ=1000 + +# To place LE Audio .bss + control-plane heap in PSRAM, uncomment the +# lines below (needs a PSRAM-capable target, e.g. esp32s31/esp32h4). +# CONFIG_SPIRAM=y +# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_HEAP_EXTERNAL_MEMORY=y +# CONFIG_BT_AUDIO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_AUDIO_HEAP_EXTERNAL_MEMORY=y + +# Bandwidth-optimized (critical-only) compressed BLE Audio/ISO logging over +# UART DMA. Pins shown are for esp32s31 (UART0 TX = GPIO58, 3000000 baud); set +# CONFIG_BLE_LOG_PRPH_UART_DMA_* values to the port/pin your capture reads. +# CONFIG_BLE_LOG_ENABLED=y +# CONFIG_BT_LOG_CRITICAL_ONLY=y +# CONFIG_BLE_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_LOG_PRPH_UART_DMA=y +# CONFIG_BLE_LOG_PRPH_UART_DMA_PORT=0 +# CONFIG_BLE_LOG_PRPH_UART_DMA_BAUD_RATE=3000000 +# CONFIG_BLE_LOG_PRPH_UART_DMA_TX_IO_NUM=58 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 e87b76fc90a..008ac728f81 100644 --- a/examples/bluetooth/esp_ble_audio/bap/unicast_client/README.md +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_client/README.md @@ -61,8 +61,8 @@ For `esp32s31`, replace the chip overlay accordingly. 3. On a connectable match `ext_scan_recv` calls `ext_scan_stop` and then `conn_create` (NimBLE: `ble_gap_connect`; Bluedroid: `esp_ble_gattc_aux_open`); the resulting `ESP_BLE_AUDIO_GAP_EVENT_ACL_CONNECT` saves the handle and calls `pairing_start` (NimBLE: `ble_gap_security_initiate`; Bluedroid: `esp_ble_set_encryption`). 4. After the security-change event the client triggers `exchange_mtu`. On NimBLE this issues `ble_gattc_exchange_mtu`; on Bluedroid the GATTC adapter auto-configures MTU inside `BTA_GATTC_Enh_Open`, so `exchange_mtu` drives `esp_ble_audio_gattc_disc_start` directly. `gatt_mtu_change` then calls `esp_ble_audio_gattc_disc_start` (the retry is idempotent — `-EALREADY` is treated as success), and once both MTU and discovery are reported `discover_sinks` runs. 5. `discover_cb` chains sink discovery into `discover_sources`; `endpoint_cb` records each EP into the next free `sinks[]`/`sources[]` slot, and source completion kicks `configure_stream`, which configures every EP with `unicast_preset.codec_cfg`. -6. After the last `config_cb`, `create_group` builds `pair_params` (sinks first, then sources) with `ESP_BLE_ISO_PACKING_SEQUENTIAL` and calls `esp_ble_audio_bap_unicast_group_create`, then `set_stream_qos`. -7. When `is_all_stream_qos_set` returns true `enable_stream` walks every stream; on the final `enable_cb`, `connect_stream` ISO-connects each pair (sink first if available); when all are up `start_stream` starts only source streams. Sink streams are started by the server, and `stream_started_cb` calls `stream_tx_register` to begin TX. +6. `config_cb` configures each EP per Config CP ACK until all are done (`all_config_acked`); `create_group` (building `pair_params`, sinks first then sources, with `ESP_BLE_ISO_PACKING_SEQUENTIAL`) and `set_stream_qos` then run from `try_create_group_and_qos`, which waits for every EP to reach codec-configured (`stream_configured_cb` / `ops.configured`) — the CP ACK can arrive before the ASE state change, so driving QoS off the ACK alone races the last EP still in idle. +7. `enable_stream` runs once every EP reports qos-configured (`stream_qos_set_cb` / `ops.qos_set`), not off the QoS CP ACK; `connect_stream` ISO-connects each pair (sink first) once every EP reports enabling (`stream_enabled_cb` / `ops.enabled`), not off the Enable CP ACK. (The ACK callbacks `qos_cb` / `enable_cb` only handle rejects — an ACK can outrun the ASE state change.) 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. ## Expected Log 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 cb0cf4198f3..89bc72cdb79 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 @@ -59,6 +59,12 @@ static size_t configured_sink_stream_count; static size_t configured_source_stream_count; #define configured_stream_count (configured_sink_stream_count + configured_source_stream_count) +/* Number of streams that have entered codec-configured (ops.configured). + * CP config ACK can arrive while the EP is still idle; QoS must wait for this. + */ +static size_t codec_configured_count; +static bool all_config_acked; + static struct stream_pair_state { esp_ble_audio_bap_stream_t *sink_stream; esp_ble_audio_bap_stream_t *source_stream; @@ -147,6 +153,8 @@ static void reset_stream_state(void) { configured_sink_stream_count = 0; configured_source_stream_count = 0; + codec_configured_count = 0; + all_config_acked = false; reset_stream_pair_state(); @@ -388,6 +396,51 @@ static int set_stream_qos(void) return 0; } +static void try_create_group_and_qos(void) +{ + uint8_t stream_count = 0; + int err; + + /* Wait until every successful Config CP has a matching ASE state + * notification (ops.configured → EP codec-configured). Calling + * qos from config_cb alone races the last stream still in idle. + */ + if (!all_config_acked || unicast_group != NULL) { + return; + } + + if (configured_stream_count == 0) { + ESP_LOGW(TAG, "No streams were configured"); + return; + } + + if (codec_configured_count < configured_stream_count) { + return; + } + + for (size_t i = 0; i < ARRAY_SIZE(sinks); i++) { + if (sinks[i].configured == ASCS_RSP_SUCCESS) { + streams[stream_count++] = &sinks[i].stream; + } + } + + for (size_t i = 0; i < ARRAY_SIZE(sources); i++) { + if (sources[i].configured == ASCS_RSP_SUCCESS) { + streams[stream_count++] = &sources[i].stream; + } + } + + err = create_group(); + if (err) { + return; + } + + err = set_stream_qos(); + if (err) { + return; + } +} + static void stream_qos_set(esp_ble_audio_bap_stream_t *stream, bool success) { for (size_t i = 0; i < ARRAY_SIZE(sinks); i++) { @@ -682,18 +735,35 @@ static void stream_configured_cb(esp_ble_audio_bap_stream_t *stream, ESP_LOGI(TAG, "[%s #%d] Stream configured, QoS preference:", stream_dir_str(stream), stream_index(stream)); example_print_qos_pref(TAG, pref); + + codec_configured_count++; + + try_create_group_and_qos(); } static void stream_qos_set_cb(esp_ble_audio_bap_stream_t *stream) { - /* QoS set is also reported by qos_cb; skip the duplicate log here. */ - (void)stream; + /* ASE entered qos-configured — safe to enable. */ + stream_qos_set(stream, true); + + if (is_all_stream_qos_set()) { + enable_stream(); + } } static void stream_enabled_cb(esp_ble_audio_bap_stream_t *stream) { - /* Enabled is also reported by enable_cb; skip the duplicate log here. */ - (void)stream; + bool ret; + + /* ASE entered enabling — safe to connect after all enables complete. */ + stream_enabled(stream, true); + + ret = enable_stream(); + if (ret == false) { + return; + } + + connect_stream(); } static void stream_connected_cb(esp_ble_audio_bap_stream_t *stream) @@ -843,9 +913,7 @@ 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) { - uint8_t stream_count; bool ret; - int err; log_rsp("Config", stream, rsp_code, reason); @@ -856,34 +924,9 @@ static void config_cb(esp_ble_audio_bap_stream_t *stream, return; } - if (configured_stream_count == 0) { - ESP_LOGW(TAG, "No streams were configured"); - return; - } + all_config_acked = true; - stream_count = 0; - - for (size_t i = 0; i < ARRAY_SIZE(sinks); i++) { - if (sinks[i].configured == ASCS_RSP_SUCCESS) { - streams[stream_count++] = &sinks[i].stream; - } - } - - for (size_t i = 0; i < ARRAY_SIZE(sources); i++) { - if (sources[i].configured == ASCS_RSP_SUCCESS) { - streams[stream_count++] = &sources[i].stream; - } - } - - err = create_group(); - if (err) { - return; - } - - err = set_stream_qos(); - if (err) { - return; - } + try_create_group_and_qos(); } static void qos_cb(esp_ble_audio_bap_stream_t *stream, @@ -892,10 +935,14 @@ static void qos_cb(esp_ble_audio_bap_stream_t *stream, { log_rsp("QoS", stream, rsp_code, reason); - stream_qos_set(stream, rsp_code == ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_SUCCESS); - - if (is_all_stream_qos_set()) { - enable_stream(); + /* Mark failure from CP reject. Success waits for ops.qos_set (ASE + * qos-configured) — enabling on CP ACK alone races codec-configured. + */ + if (rsp_code != ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_SUCCESS) { + stream_qos_set(stream, false); + if (is_all_stream_qos_set()) { + enable_stream(); + } } } @@ -903,18 +950,19 @@ 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) { - bool ret; - log_rsp("Enable", stream, rsp_code, reason); - stream_enabled(stream, rsp_code == ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_SUCCESS); + /* Same as QoS: connect needs ENABLING from ASE state ntf (ops.enabled). */ + if (rsp_code != ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_SUCCESS) { + bool ret; - ret = enable_stream(); - if (ret == false) { - return; + stream_enabled(stream, false); + + ret = enable_stream(); + if (ret) { + connect_stream(); + } } - - connect_stream(); } static void start_cb(esp_ble_audio_bap_stream_t *stream, 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 caca8cb9515..f9bf77bd488 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 @@ -101,6 +101,12 @@ int stream_tx_register(esp_ble_audio_bap_stream_t *stream) return -EINVAL; } + for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) { + if (tx_streams[i].stream == stream) { + return 0; /* already armed */ + } + } + for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) { if (tx_streams[i].stream == NULL) { if (stream->qos == NULL || stream->qos->sdu == 0) { 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 56525a97b23..0e4c686f510 100644 --- a/examples/bluetooth/esp_ble_audio/bap/unicast_client/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_client/sdkconfig.defaults @@ -21,3 +21,24 @@ CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT=2 CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y CONFIG_FREERTOS_HZ=1000 + +# To place LE Audio .bss + control-plane heap in PSRAM, uncomment the +# lines below (needs a PSRAM-capable target, e.g. esp32s31/esp32h4). +# CONFIG_SPIRAM=y +# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_HEAP_EXTERNAL_MEMORY=y +# CONFIG_BT_AUDIO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_AUDIO_HEAP_EXTERNAL_MEMORY=y + +# Bandwidth-optimized (critical-only) compressed BLE Audio/ISO logging over +# UART DMA. Pins shown are for esp32s31 (UART0 TX = GPIO58, 3000000 baud); set +# CONFIG_BLE_LOG_PRPH_UART_DMA_* values to the port/pin your capture reads. +# CONFIG_BLE_LOG_ENABLED=y +# CONFIG_BT_LOG_CRITICAL_ONLY=y +# CONFIG_BLE_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_LOG_PRPH_UART_DMA=y +# CONFIG_BLE_LOG_PRPH_UART_DMA_PORT=0 +# CONFIG_BLE_LOG_PRPH_UART_DMA_BAUD_RATE=3000000 +# CONFIG_BLE_LOG_PRPH_UART_DMA_TX_IO_NUM=58 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 0b97edc72f0..6dc9be9c29c 100644 --- a/examples/bluetooth/esp_ble_audio/bap/unicast_server/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_server/sdkconfig.defaults @@ -28,3 +28,24 @@ CONFIG_BT_PACS_SUPPORTED_CONTEXT_NOTIFIABLE=y CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y CONFIG_FREERTOS_HZ=1000 + +# To place LE Audio .bss + control-plane heap in PSRAM, uncomment the +# lines below (needs a PSRAM-capable target, e.g. esp32s31/esp32h4). +# CONFIG_SPIRAM=y +# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_HEAP_EXTERNAL_MEMORY=y +# CONFIG_BT_AUDIO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_AUDIO_HEAP_EXTERNAL_MEMORY=y + +# Bandwidth-optimized (critical-only) compressed BLE Audio/ISO logging over +# UART DMA. Pins shown are for esp32s31 (UART0 TX = GPIO58, 3000000 baud); set +# CONFIG_BLE_LOG_PRPH_UART_DMA_* values to the port/pin your capture reads. +# CONFIG_BLE_LOG_ENABLED=y +# CONFIG_BT_LOG_CRITICAL_ONLY=y +# CONFIG_BLE_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_LOG_PRPH_UART_DMA=y +# CONFIG_BLE_LOG_PRPH_UART_DMA_PORT=0 +# CONFIG_BLE_LOG_PRPH_UART_DMA_BAUD_RATE=3000000 +# CONFIG_BLE_LOG_PRPH_UART_DMA_TX_IO_NUM=58 diff --git a/examples/bluetooth/esp_ble_audio/cap/acceptor/README.md b/examples/bluetooth/esp_ble_audio/cap/acceptor/README.md index ba4f1f56898..ea22f4b27a6 100644 --- a/examples/bluetooth/esp_ble_audio/cap/acceptor/README.md +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/README.md @@ -128,6 +128,7 @@ Source (SRC) Assistant (ASS) Acceptor (ACC) | `FLAG_BROADCAST_CODE_RECEIVED` | `broadcast_code_cb` (BASS Set Broadcast Code); self-scan local code | `broadcast_sink_reset` | | `FLAG_BROADCAST_SYNC_REQUESTED` | `bis_sync_req_cb` (bitmap ≠ 0); self-scan PA match | `bis_sync_req_cb` (bitmap = 0); `broadcast_sink_reset` | | `FLAG_BROADCAST_RESYNC_PENDING` | `bis_sync_req_cb` before `_stop` (bitmap change while streaming) | `stream_stopped_cb` after driving the re-sync; `_stop` failure; `broadcast_sink_reset` | +| `FLAG_BROADCAST_STOP_PENDING` | `broadcast_stream_stopped_cb` (last BIS stops) | `stopped_cb`; `broadcast_sink_reset` | | `FLAG_BROADCAST_SYNCING` | `check_sync_broadcast` after `_sync` returns OK | `stream_started_cb`; `stream_stopped_cb` | | `FLAG_BROADCAST_SYNCED` | `stream_started_cb` | `stream_stopped_cb` | @@ -161,9 +162,9 @@ BASE_RECEIVED && BROADCAST_SYNCABLE +-----+-----+ | | | BIG drops while PA gone | v - +---------> stream_stopped_cb + !PA_SYNCED + +--> stream_stopped_cb (set STOP_PENDING, !PA_SYNCED) | - | _delete + broadcast_sink_reset + | stopped_cb: _delete + broadcast_sink_reset v [end] ``` @@ -171,8 +172,8 @@ BASE_RECEIVED && BROADCAST_SYNCABLE Key invariants: - **PA loss does NOT tear down a running BIS.** Per BASS § 3.2.1.6 / § 3.2.1.9, `PA_Sync_State` and `BIS_Sync_State` are independent. While BIS is streaming/syncing, `broadcast_pa_lost` only notifies the assistant (`PA_Sync_State = 0x00`) and clears PA-only local state (`sync_handle`, `FLAG_PA_SYNCED`); the BIG keeps running and audio continues to flow. -- **PA loss with BIS idle tears down the sink.** The sink is bound to the now-dead sync handle and its cached BASE / BIGInfo are stale. `broadcast_pa_lost` calls `_delete` and clears `FLAG_BASE_RECEIVED` / `FLAG_BROADCAST_SYNCABLE` / `FLAG_BROADCAST_CODE_REQUIRED`. The assistant's subscription (`requested_bis_sync`, `FLAG_BROADCAST_SYNC_REQUESTED`, `FLAG_BROADCAST_CODE_RECEIVED`) is preserved so the next PA sync re-creates a fresh sink and resumes streaming. -- **Sink deletion happens in `stream_stopped_cb` when both PA and BIS are gone.** Triggers: assistant unsubscribes via `Modify Source bis_sync = 0`, or the broadcaster stops the BIG while PA is already gone. +- **PA loss with BIS idle tears down the sink.** The sink is bound to the now-dead sync handle and its cached BASE / BIGInfo are stale. `broadcast_pa_lost` calls `_delete` (unless `FLAG_BROADCAST_STOP_PENDING` is set, in which case `stopped_cb` deletes after `bis_sync` clear) and clears `FLAG_BASE_RECEIVED` / `FLAG_BROADCAST_SYNCABLE` / `FLAG_BROADCAST_CODE_REQUIRED`. The assistant's subscription (`requested_bis_sync`, `FLAG_BROADCAST_SYNC_REQUESTED`, `FLAG_BROADCAST_CODE_RECEIVED`) is preserved so the next PA sync re-creates a fresh sink and resumes streaming. +- **Sink deletion happens in `stopped_cb` (the `stopped` sink callback) once BASS has cleared `bis_sync`, when PA is gone.** `broadcast_stream_stopped_cb` of the last BIS only sets `FLAG_BROADCAST_STOP_PENDING` (and re-`_sync`s on a pending bitmap change) — it does not delete, because deleting there races `rem_src` while `bis_sync` is still non-zero. Triggers: assistant unsubscribes via `Modify Source bis_sync = 0`, or the broadcaster stops the BIG while PA is already gone. - `bis_sync_req_cb` going `X → 0` (Assistant pause) only issues `_stop`, never `_delete`. Going `X → Y` (BIS bitmap switch) likewise only `_stop`s; the next `check_sync_broadcast` (called from `stream_stopped_cb` when PA still synced) re-`_sync`s the same object. - `pa_sync_term_req_cb` issues the HCI Periodic Advertising Terminate Sync but does **not** clear `broadcast_sink.sync_handle`. Cleanup runs from `BLE_GAP_EVENT_PERIODIC_SYNC_LOST` → `broadcast_pa_lost`. Resetting the handle early would make that gate miss. @@ -181,17 +182,17 @@ Key invariants: | Event | Action | | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | | **PA lost while BIS active** (broadcaster moved mid-stream) | Set `PA_Sync_State = 0x00`; clear PA-only state (`sync_handle`, `FLAG_PA_SYNCED`). Sink + BIS untouched; BIG keeps running. | -| **PA lost while BIS idle** (Assistant `PA_Sync = 0`, or PA dropped after Assistant paused BIS) | Set `PA_Sync_State = 0x00` (skipped if BASS already updated it in-place); clear PA-only state; `_delete` the sink and clear `FLAG_BASE_RECEIVED` / `FLAG_BROADCAST_SYNCABLE` / `FLAG_BROADCAST_CODE_REQUIRED`. The next PA sync starts from a clean sink and lets the lib redeliver BASE / BIGInfo. | +| **PA lost while BIS idle** (Assistant `PA_Sync = 0`, or PA dropped after Assistant paused BIS) | Set `PA_Sync_State = 0x00` (skipped if BASS already updated it in-place); clear PA-only state; unless `FLAG_BROADCAST_STOP_PENDING` is set (BIG teardown in flight — `stopped_cb` deletes after `bis_sync` clear), `_delete` the sink and clear `FLAG_BASE_RECEIVED` / `FLAG_BROADCAST_SYNCABLE` / `FLAG_BROADCAST_CODE_REQUIRED`. The next PA sync starts from a clean sink and lets the lib redeliver BASE / BIGInfo. | | **Modify Source `bis_sync = 0`** (PA still synced) | `bis_sync_req_cb` clears `FLAG_BROADCAST_SYNC_REQUESTED` then `_stop`s the BIG. Sink retained. | | **Modify Source bitmap change** (PA still synced, streaming) | Update `requested_bis_sync` + `FLAG_BROADCAST_SYNC_REQUESTED` + set `FLAG_BROADCAST_RESYNC_PENDING`, then `_stop`. `stream_stopped_cb` clears the flag and re-`_sync`s with the new bitmap. | | **BIG drops while PA still synced** (e.g. broadcaster pause) | `stream_stopped_cb` clears SYNCED/SYNCING and exposes the loss via `BIS_Sync_State`. `FLAG_BROADCAST_RESYNC_PENDING` is not set, so no auto-retry — per BASS § 3.2.1.9 the assistant drives recovery via Modify Source. | -| **BIG drops after PA lost** (broadcaster turned off) | `stream_stopped_cb` of the last active stream sees `!PA_SYNCED` → `_delete` + `broadcast_sink_reset`. Multi-BIS: earlier callbacks just decrement `active_streams` so `_delete` is not called while the sink is still in use. | +| **BIG drops after PA lost** (broadcaster turned off) | `broadcast_stream_stopped_cb` of the last active stream sees `!PA_SYNCED` and sets `FLAG_BROADCAST_STOP_PENDING`; `stopped_cb` then runs `_delete` + `broadcast_sink_reset` once BASS has cleared `bis_sync`. Multi-BIS: earlier callbacks just decrement `active_streams` so the flag is not set while the sink is still in use. | | **Assistant Remove Source** | Spec allows only when BIS not synced; lib handles, app sees no special event. | Two recurring patterns that drive the above behavior: - **Update local state before calling `_stop`/`_delete`.** The lib may fire `stream_stopped_cb` synchronously from within `_stop`, so the callback must see the post-stop state. Applies in `bis_sync_req_cb` (updates `requested_bis_sync` + flag before `_stop`) and `broadcast_pa_lost` (no longer calls `_stop`). -- **Sink lifetime is BIS-driven, not PA-driven.** Sink is created on first PA sync and deleted only when the BIG itself stops and PA is also gone. This matches BASS spec's independent PA/BIS state model. +- **Sink lifetime is BIS-driven, not PA-driven.** Sink is created on first PA sync and deleted (from `stopped_cb`, after BASS clears `bis_sync`) only when the BIG itself stops and PA is also gone. This matches BASS spec's independent PA/BIS state model. ### Multi-BIS (stereo) configuration @@ -290,13 +291,14 @@ On PA sync loss: ``` I (xxx) CAP_ACC: [SNK #0] Stream stopped, reason 0x... +I (xxx) CAP_ACC: Broadcast sink 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. +Run the [initiator](../initiator/) on a second board with the matching role — both `EXAMPLE_UNICAST`, or both `EXAMPLE_BROADCAST`. For a CAP handover, run the [handover](../handover/) example instead, which needs both roles on this acceptor. ### Unicast @@ -310,5 +312,60 @@ Run the [initiator](../initiator/) on a second board. The initiator and acceptor 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`. +3. The acceptor PA-syncs, receives BASE and BIGInfo, and syncs one BIS per sink stream it has (`CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT`), up to what the first subgroup of the BASE offers: `[SNK #0] Stream started`, `[SNK #1] Stream started`. 4. On PA sync loss the acceptor cleans up; in self-scan mode it restarts scanning. + +### Handover + +1. Build this acceptor with both `EXAMPLE_UNICAST` and `EXAMPLE_BROADCAST` (the defaults), + so it exposes ASCS and BASS at the same time. No code change is needed. +2. Flash the [handover](../handover/) example on the other board. It connects, discovers + CAS, the ASEs and BASS, starts unicast audio, then alternates between unicast and broadcast. +3. On a unicast-to-broadcast handover the acceptor sees its sink ASEs released, then a + BASS Add Source from the collocated Commander, and PA/BIS-syncs to the new source. +4. On the way back the receive state is cleared and the sink ASEs are configured again. + +Only the sink direction moves; a broadcast Audio Stream has no return path, so the source +ASE is not part of the procedure. + +#### The sink stream pool is shared + +Unicast and broadcast draw sink streams from the **same** pool: `stream_alloc(SINK)` hands out +`peer.sink_streams[]` entries whose endpoint is unbound. That is what lets a handover reuse the +objects, and it also means that while every sink stream carries a BIS there is none left to accept +a unicast Config. + +The acceptor says so rather than letting the Initiator discover the shortage through a `NO_MEM`: +`sink_availability_update()` sets the **PACS Available Audio Contexts** for the sink direction to +`NONE` while receiving, and restores them when reception stops. CAP §7.3.1.8 / §7.3.1.9 describe +exactly this ("Start of reception **can** affect an Acceptor's availability for unicast Audio +Streams. In this case, the Acceptor **will** update its Available Audio Contexts characteristic"). + +It is driven off **BIS_Sync** in the Broadcast Receive State rather than off a local +"broadcasting" flag, because BIS_Sync is the field an Assistant clears *first* when it stops our +reception — which puts the restore comfortably ahead of the unicast Config that follows in a +broadcast-to-unicast handover. + +#### PAST is expected, self-scan is not + +The handover example is a collocated broadcaster and hands its periodic advertising train over +with **Set Info Transfer**. This acceptor must therefore be built **without** `EXAMPLE_SCAN_SELF` +(its Kconfig already makes that mutually exclusive with `EXAMPLE_UNICAST`). It reports +`PA_Sync_State = 1` (*SyncInfo Request*) and waits for the transfer; the Source ID arrives in the +**high octet** of the transfer's service data. + +#### Source IDs are ours to assign, and they are reused + +BASS Table 3.9: the Source_ID is *assigned by the server* and only has to be unique among the +receive states **currently exposed**. `next_src_id()` is a byte counter that skips values held by +active receive states, so a number becomes available again as soon as its receive state is +removed, and wraps after 256 allocations. + +Two consequences worth knowing when reading logs: + +* A receive state **outlives the Initiator's reboot** — it lives here. That is why the handover + example sweeps and clears leftovers when it connects; without that, its first Add Source after a + reflash is rejected with `0xFC` for duplicating the {address, SID, Broadcast ID} triple + (BAP §6.5.4). +* Source IDs keep climbing across the peer's restarts and only restart from 0 when **this** board + reboots. A jump back to 0 in the log means the acceptor restarted, not that a counter wrapped. 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 a122f1a884d..57bc79ad613 100644 --- a/examples/bluetooth/esp_ble_audio/cap/acceptor/main/Kconfig.projbuild +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/Kconfig.projbuild @@ -13,7 +13,6 @@ menu "Example: CAP Acceptor" config EXAMPLE_BROADCAST bool "Broadcast" default y if !EXAMPLE_UNICAST - select BT_NIMBLE_PERIODIC_ADV_SYNC_TRANSFER help If set, advertise as a Broadcast acceptor (BASS Scan Delegator) for syncable broadcast audio. Can coexist with EXAMPLE_UNICAST diff --git a/examples/bluetooth/esp_ble_audio/cap/acceptor/main/cap_acceptor.h b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/cap_acceptor.h index 77cd669bf9f..1338b9ffd64 100644 --- a/examples/bluetooth/esp_ble_audio/cap/acceptor/main/cap_acceptor.h +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/cap_acceptor.h @@ -45,9 +45,23 @@ ESP_BLE_AUDIO_CONTEXT_TYPE_MEDIA | \ ESP_BLE_AUDIO_CONTEXT_TYPE_GAME) +/* Number of stream objects the application backs per direction. + * + * Sink: one per exposed ASE, so a stereo Initiator can configure two independent + * sink streams. The CAP handover procedures hand over all streaming sink streams + * at once (CAP 7.3.1.10 applies to every CIS carrying Initiator-to-Acceptor audio), + * so the sink pool has to match the number of ASEs that can be configured. + * + * Source: one. Only the bidirectional (call) configuration uses it, and the TX + * pump in cap_acceptor_unicast.c drives a single stream. A Config request beyond + * these counts is answered with NO_MEM by stream_alloc(). + */ +#define ACCEPTOR_SINK_STREAM_COUNT CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT +#define ACCEPTOR_SOURCE_STREAM_COUNT 1 + struct peer_config { - esp_ble_audio_cap_stream_t source_stream; - esp_ble_audio_cap_stream_t sink_stream; + esp_ble_audio_cap_stream_t source_streams[ACCEPTOR_SOURCE_STREAM_COUNT]; + esp_ble_audio_cap_stream_t sink_streams[ACCEPTOR_SINK_STREAM_COUNT]; uint16_t conn_handle; /* Assistant's BD addr — used by Bluedroid pa_sync_with_past to set per-peer * PAST receive params. Populated at ACL_CONNECT, cleared at ACL_DISCONNECT. */ 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 ed54dc2e58a..45274e3a498 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 @@ -29,6 +29,11 @@ enum broadcast_flag { FLAG_BROADCAST_SYNCING, FLAG_BROADCAST_SYNCED, FLAG_BROADCAST_RESYNC_PENDING, + /* Set when the last BIS stream stops; cleared in stopped_cb. + * Bridges the window where EPs are IDLE but big_stopped has not yet cleared + * BASS bis_sync — pa_lost must not rem_src/delete during that window. + */ + FLAG_BROADCAST_STOP_PENDING, FLAG_BASE_RECEIVED, FLAG_PA_SYNCING, FLAG_PA_SYNCED, @@ -58,6 +63,21 @@ static inline bool flag_test_and_set(uint8_t bit) return was; } +/* The audio stack keeps addresses on-air (LSB-first) in bt_addr_le_t, Bluedroid takes + * and reports them MSB-first, NimBLE on-air. Convert whenever one meets the other; the + * reversal is its own inverse, so this serves both directions. + */ +static void addr_order_copy(uint8_t dst[6], const uint8_t src[6]) +{ +#if CONFIG_BT_BLUEDROID_ENABLED + for (size_t i = 0; i < 6; i++) { + dst[i] = src[5 - i]; + } +#else + memcpy(dst, src, 6); +#endif +} + static struct broadcast_sink { esp_ble_audio_cap_stream_t cap_streams[CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT]; const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state; @@ -183,8 +203,15 @@ void broadcast_scan_recv(esp_ble_audio_gap_app_event_t *event) } #endif /* CONFIG_EXAMPLE_SCAN_SELF */ +static void sink_availability_update(bool receiving); + static void broadcast_sink_reset(void) { + /* Reception is over however we got here (Remove Source, PA loss, ACL drop), + * so the sink pool is free again even if no receive state said so. + */ + sink_availability_update(false); + broadcast_sink.recv_state = NULL; broadcast_sink.sink = NULL; broadcast_sink.requested_bis_sync = 0; @@ -201,6 +228,7 @@ static void broadcast_sink_reset(void) flag_clear(FLAG_BROADCAST_CODE_RECEIVED); flag_clear(FLAG_BROADCAST_SYNC_REQUESTED); flag_clear(FLAG_BROADCAST_RESYNC_PENDING); + flag_clear(FLAG_BROADCAST_STOP_PENDING); flag_clear(FLAG_SCANNING); #if CONFIG_EXAMPLE_SCAN_SELF @@ -208,6 +236,29 @@ static void broadcast_sink_reset(void) #endif /* CONFIG_EXAMPLE_SCAN_SELF */ } +struct base_subgroup_pick { + uint32_t bis_indexes; + bool found; +}; + +static bool base_pick_subgroup_cb(const esp_ble_audio_bap_base_subgroup_t *subgroup, + void *user_data) +{ + struct base_subgroup_pick *pick = user_data; + + /* Keep the first subgroup only. BIS of different subgroups carry different + * codec configurations, so a sink synchronizes within one subgroup. + */ + if (pick->found == false && + esp_ble_audio_bap_base_subgroup_get_bis_indexes(subgroup, + &pick->bis_indexes) == ESP_OK) { + pick->found = true; + } + + /* Always continue: stopping early makes the iterator return -ECANCELED. */ + return true; +} + static void check_sync_broadcast(void) { esp_ble_audio_bap_stream_t *streams[CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT]; @@ -252,22 +303,33 @@ static void check_sync_broadcast(void) } if (broadcast_sink.requested_bis_sync == ESP_BLE_AUDIO_BAP_BIS_SYNC_NO_PREF) { + struct base_subgroup_pick pick = {0}; uint32_t base_bis; - /* Get the first BIS index from the BASE */ - err = esp_ble_audio_bap_base_get_bis_indexes( - (esp_ble_audio_bap_base_t *)broadcast_sink.received_base, &base_bis); - if (err) { + /* Get the BIS indexes offered by the first subgroup of the BASE */ + err = esp_ble_audio_bap_base_foreach_subgroup( + (esp_ble_audio_bap_base_t *)broadcast_sink.received_base, + base_pick_subgroup_cb, &pick); + if (err || pick.found == false) { ESP_LOGE(TAG, "Failed to get BIS indexes from BASE, err %d", err); return; } + base_bis = pick.bis_indexes; sync_bitfield = 0; + /* No Broadcast Assistant told us what to sync to, so take as many BIS as + * this sink has streams for: a stereo source advertises one BIS per + * channel and syncing to only the first would drop the other channel. + */ for (uint8_t i = ESP_BLE_ISO_BIS_INDEX_MIN; i <= ESP_BLE_ISO_BIS_INDEX_MAX; i++) { if (base_bis & ESP_BLE_ISO_BIS_INDEX_BIT(i)) { - sync_bitfield = ESP_BLE_ISO_BIS_INDEX_BIT(i); - break; + sync_bitfield |= ESP_BLE_ISO_BIS_INDEX_BIT(i); + + if (__builtin_popcount(sync_bitfield) >= + CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT) { + break; + } } } @@ -321,7 +383,9 @@ static void broadcast_stream_started_cb(esp_ble_audio_bap_stream_t *stream) ESP_LOGI(TAG, "[SNK #%u] Stream started", idx); - example_audio_rx_metrics_reset(&rx_metrics[idx]); + if (idx < ARRAY_SIZE(rx_metrics)) { + example_audio_rx_metrics_reset(&rx_metrics[idx]); + } broadcast_sink.active_streams++; flag_clear(FLAG_BROADCAST_SYNCING); @@ -330,8 +394,6 @@ 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_err_t err; - ESP_LOGI(TAG, "[SNK #%u] Stream stopped, reason 0x%02x", broadcast_stream_idx(stream), reason); @@ -349,21 +411,14 @@ static void broadcast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint flag_clear(FLAG_BROADCAST_SYNCING); flag_clear(FLAG_BROADCAST_SYNCED); + /* Mark BIG teardown in progress until stopped_cb runs + * (after update_recv_state_big_cleared). Do not delete here — that + * races rem_src while bis_sync is still non-zero. + */ + flag_set(FLAG_BROADCAST_STOP_PENDING); if (flag_test(FLAG_PA_SYNCED) == false) { - /* Both PA and BIS are gone — no path to recover BIGInfo, so the - * sink can no longer drive a new BIG sync. Delete it and clear all - * state. - */ - if (broadcast_sink.sink) { - err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink.sink); - if (err) { - ESP_LOGE(TAG, "Failed to delete broadcast sink, err %d", err); - return; - } - } - - broadcast_sink_reset(); + /* Both PA and BIS are gone — stopped_cb deletes. */ return; } @@ -378,6 +433,35 @@ static void broadcast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint } } +static void stopped_cb(esp_ble_audio_bap_broadcast_sink_t *sink, + uint8_t reason) +{ + esp_err_t err; + + ESP_LOGI(TAG, "Broadcast sink stopped, reason 0x%02x", reason); + + flag_clear(FLAG_BROADCAST_STOP_PENDING); + + /* Keep the sink when PA is still synced (assistant pause, BIS bitmap + * resync, or spontaneous BIG drop with PA alive). + */ + if (flag_test(FLAG_PA_SYNCED)) { + return; + } + + if (broadcast_sink.sink == NULL) { + return; + } + + err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink.sink); + if (err) { + ESP_LOGE(TAG, "Failed to delete broadcast sink, err %d", err); + return; + } + + broadcast_sink_reset(); +} + 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) @@ -385,6 +469,10 @@ static void broadcast_stream_recv_cb(esp_ble_audio_bap_stream_t *stream, uint8_t idx = broadcast_stream_idx(stream); char obj_name[10]; + if (idx >= ARRAY_SIZE(rx_metrics)) { + return; + } + rx_metrics[idx].last_sdu_len = len; snprintf(obj_name, sizeof(obj_name), "SNK #%u", idx); example_audio_rx_metrics_on_recv(info, &rx_metrics[idx], TAG, obj_name); @@ -434,16 +522,64 @@ static void syncable_cb(esp_ble_audio_bap_broadcast_sink_t *sink, } } +/* CAP 7.3.1.8 / 7.3.1.9: "Start of reception can affect an Acceptor's + * availability for unicast Audio Streams. In this case, the Acceptor will + * update its Available Audio Contexts characteristic." + * + * It does affect us: stream_alloc() hands unicast and broadcast the same sink + * pool, so while every sink stream carries a BIS there is none left to accept a + * unicast Config. Saying so keeps the Initiator's view honest instead of + * letting it discover the shortage through a NO_MEM. + * + * Driven off BIS_Sync rather than off a local "broadcasting" flag because that + * is the field the Assistant clears first when it stops our reception - which + * puts the restore well ahead of the unicast Config that follows in a + * broadcast-to-unicast handover. + */ +static void sink_availability_update(bool receiving) +{ + static bool reported_unavailable; + esp_ble_audio_context_t context; + esp_err_t err; + + if (receiving == reported_unavailable) { + return; + } + + context = receiving ? ESP_BLE_AUDIO_CONTEXT_TYPE_NONE : SINK_CONTEXT; + + err = esp_ble_audio_pacs_set_available_contexts(ESP_BLE_AUDIO_DIR_SINK, context); + if (err) { + ESP_LOGE(TAG, "Failed to update sink available contexts, err %d", err); + return; + } + + reported_unavailable = receiving; + + ESP_LOGI(TAG, "Sink available contexts now 0x%04x (%s broadcast)", + context, receiving ? "receiving" : "not receiving"); +} + static void recv_state_updated_cb(esp_ble_conn_t *conn, const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state) { + bool receiving = false; + ESP_LOGI(TAG, "Receive state updated, pa_sync 0x%02x encrypt 0x%02x", recv_state->pa_sync_state, recv_state->encrypt_state); for (uint8_t i = 0; i < recv_state->num_subgroups; i++) { ESP_LOGI(TAG, "subgroup %d bis_sync 0x%08x", i, recv_state->subgroups[i].bis_sync); + + /* BIS_SYNC_FAILED is non-zero but carries no BIS (BASS 3.1.1.5). */ + if (recv_state->subgroups[i].bis_sync != 0 && + recv_state->subgroups[i].bis_sync != ESP_BLE_AUDIO_BAP_BIS_SYNC_FAILED) { + receiving = true; + } } + sink_availability_update(receiving); + if (recv_state->pa_sync_state == ESP_BLE_AUDIO_BAP_PA_STATE_SYNCED) { broadcast_sink.recv_state = recv_state; } @@ -490,8 +626,11 @@ static int pa_sync_req_cb(esp_ble_conn_t *conn, ESP_LOGI(TAG, "Waiting for PAST..."); } else { - err = pa_sync_create(recv_state->addr.type, recv_state->addr.a.val, - recv_state->adv_sid); + uint8_t addr[6]; + + addr_order_copy(addr, recv_state->addr.a.val); + + err = pa_sync_create(recv_state->addr.type, addr, recv_state->adv_sid); if (err) { return err; } @@ -636,7 +775,7 @@ void broadcast_pa_synced(esp_ble_audio_gap_app_event_t *event) int err; addr.type = event->pa_sync.addr.type; - memcpy(addr.a.val, event->pa_sync.addr.val, sizeof(addr.a.val)); + addr_order_copy(addr.a.val, event->pa_sync.addr.val); if (broadcast_sink.sync_handle == PA_SYNC_HANDLE_INIT || (broadcast_sink.recv_state && @@ -740,14 +879,15 @@ void broadcast_pa_lost(uint16_t sync_handle) flag_clear(FLAG_PA_SYNCED); flag_clear(FLAG_PA_SYNCING); - /* BIS still active → BIG keeps running per § 3.2.1.9, leave the sink; - * stream_stopped_cb will tear it down when BIS eventually stops. + /* BIS still active or BIG teardown pending → leave the sink; + * stopped_cb will tear it down when bis_sync is clear. * Otherwise the sink is bound to a dead PA handle and its cached * BASE / BIGInfo are stale: delete the sink and clear the PA-derived * flags so the next PA sync creates a fresh sink and lets the lib * redeliver BASE / BIGInfo. */ - if (flag_test(FLAG_BROADCAST_SYNCING) || flag_test(FLAG_BROADCAST_SYNCED)) { + if (flag_test(FLAG_BROADCAST_SYNCING) || flag_test(FLAG_BROADCAST_SYNCED) || + flag_test(FLAG_BROADCAST_STOP_PENDING)) { return; } @@ -789,6 +929,7 @@ int cap_acceptor_broadcast_init(void) static esp_ble_audio_bap_broadcast_sink_cb_t broadcast_sink_cbs = { .base_recv = base_recv_cb, .syncable = syncable_cb, + .stopped = stopped_cb, }; static esp_ble_audio_bap_stream_ops_t broadcast_stream_ops = { .started = broadcast_stream_started_cb, 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 2acba3efa2c..841384516be 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 @@ -25,11 +25,20 @@ static const esp_ble_audio_bap_qos_cfg_pref_t qos_pref = 20000, /* Preferred Minimum Presentation Delay (usec) */ 40000); /* Preferred Maximum Presentation Delay (usec) */ -static example_audio_rx_metrics_t rx_metrics; +/* One set of metrics per sink stream: only the sink direction receives, and each + * stream has to be counted separately or a stereo setup reports the sum of both + * channels against whichever stream happened to cross the reporting threshold. + */ +static example_audio_rx_metrics_t rx_metrics[ACCEPTOR_SINK_STREAM_COUNT]; static example_audio_tx_scheduler_t tx_scheduler; static uint16_t tx_seq_num; static uint8_t *iso_data; +/* Source stream currently driven by the TX pump. Set when a source stream starts + * and cleared when it stops: stream_alloc() hands out *free* streams, so it can + * no longer be used to look this one up once it is attached. + */ +static esp_ble_audio_cap_stream_t *tx_cap_stream; static const struct peer_config *s_peer; @@ -53,11 +62,15 @@ static const char *stream_dir_str(const esp_ble_audio_bap_stream_t *stream) */ if (stream->ep == NULL) { if (s_peer != NULL) { - if (stream == &s_peer->sink_stream.bap_stream) { - return "SNK"; + for (size_t i = 0; i < ARRAY_SIZE(s_peer->sink_streams); i++) { + if (stream == &s_peer->sink_streams[i].bap_stream) { + return "SNK"; + } } - if (stream == &s_peer->source_stream.bap_stream) { - return "SRC"; + for (size_t i = 0; i < ARRAY_SIZE(s_peer->source_streams); i++) { + if (stream == &s_peer->source_streams[i].bap_stream) { + return "SRC"; + } } } return "???"; @@ -72,9 +85,26 @@ static const char *stream_dir_str(const esp_ble_audio_bap_stream_t *stream) 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; + if (s_peer == NULL || stream == NULL) { + return -1; + } + + /* Index within the pool of its own direction, so logs read as "SNK #0" / + * "SNK #1" for the two sink streams of a stereo Initiator. + */ + for (size_t i = 0; i < ARRAY_SIZE(s_peer->sink_streams); i++) { + if (stream == &s_peer->sink_streams[i].bap_stream) { + return (int)i; + } + } + + for (size_t i = 0; i < ARRAY_SIZE(s_peer->source_streams); i++) { + if (stream == &s_peer->source_streams[i].bap_stream) { + return (int)i; + } + } + + return -1; } static void tx_scheduler_cb(void *arg) @@ -289,14 +319,20 @@ static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream) ESP_LOGI(TAG, "[%s #%d] Stream started", stream_dir_str(stream), stream_index(stream)); - example_audio_rx_metrics_reset(&rx_metrics); - err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info); if (err) { ESP_LOGE(TAG, "Failed to get ep info, err %d", err); return; } + if (ep_info.dir == ESP_BLE_AUDIO_DIR_SINK) { + const int idx = stream_index(stream); + + if (idx >= 0 && idx < (int)ARRAY_SIZE(rx_metrics)) { + example_audio_rx_metrics_reset(&rx_metrics[idx]); + } + } + if (ep_info.dir == ESP_BLE_AUDIO_DIR_SOURCE) { if (stream->qos == NULL || stream->qos->sdu == 0) { ESP_LOGE(TAG, "Invalid stream qos"); @@ -311,12 +347,14 @@ static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream) } } + tx_cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream); tx_seq_num = 0; example_audio_tx_scheduler_reset(&tx_scheduler); err = example_audio_tx_scheduler_start(&tx_scheduler, stream->qos->interval); if (err) { ESP_LOGE(TAG, "Failed to start tx scheduler, err %d", err); + tx_cap_stream = NULL; return; } @@ -356,6 +394,8 @@ static void unicast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_ ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err); } + tx_cap_stream = NULL; + if (iso_data != NULL) { free(iso_data); iso_data = NULL; @@ -383,6 +423,8 @@ static void unicast_stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, u ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err); } + tx_cap_stream = NULL; + if (iso_data != NULL) { free(iso_data); iso_data = NULL; @@ -406,12 +448,17 @@ 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) { + const int idx = stream_index(stream); 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, name); + /* Only sink streams receive, so the index is always within the sink pool. */ + if (idx < 0 || idx >= (int)ARRAY_SIZE(rx_metrics)) { + return; + } + + snprintf(name, sizeof(name), "%s #%d", stream_dir_str(stream), idx); + rx_metrics[idx].last_sdu_len = len; + example_audio_rx_metrics_on_recv(info, &rx_metrics[idx], TAG, name); } static void unicast_stream_sent_cb(esp_ble_audio_bap_stream_t *stream, void *user_data) @@ -444,8 +491,10 @@ static void unicast_server_tx(void) esp_ble_audio_bap_stream_t *bap_stream; esp_err_t err; - cap_stream = stream_alloc(ESP_BLE_AUDIO_DIR_SOURCE); - assert(cap_stream); + cap_stream = tx_cap_stream; + if (cap_stream == NULL) { + return; + } bap_stream = &cap_stream->bap_stream; if (bap_stream->ep == NULL) { @@ -512,16 +561,20 @@ int cap_acceptor_unicast_init(struct peer_config *peer) cbs_registered = true; } - err = esp_ble_audio_cap_stream_ops_register(&peer->source_stream, &unicast_stream_ops); - if (err) { - ESP_LOGE(TAG, "Failed to register source stream ops, err %d", err); - return -1; + for (size_t i = 0; i < ARRAY_SIZE(peer->source_streams); i++) { + err = esp_ble_audio_cap_stream_ops_register(&peer->source_streams[i], &unicast_stream_ops); + if (err) { + ESP_LOGE(TAG, "Failed to register source stream ops [%zu], err %d", i, err); + return -1; + } } - err = esp_ble_audio_cap_stream_ops_register(&peer->sink_stream, &unicast_stream_ops); - if (err) { - ESP_LOGE(TAG, "Failed to register sink stream ops, err %d", err); - return -1; + for (size_t i = 0; i < ARRAY_SIZE(peer->sink_streams); i++) { + err = esp_ble_audio_cap_stream_ops_register(&peer->sink_streams[i], &unicast_stream_ops); + if (err) { + ESP_LOGE(TAG, "Failed to register sink stream ops [%zu], err %d", i, err); + return -1; + } } err = example_audio_tx_scheduler_init(&tx_scheduler, 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 633f82d8ae5..d670e366a04 100644 --- a/examples/bluetooth/esp_ble_audio/cap/acceptor/main/main.c +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/main.c @@ -16,7 +16,11 @@ static uint8_t codec_data[] = ESP_BLE_AUDIO_CODEC_CAP_FREQ_ANY, /* Sampling frequency Any */ ESP_BLE_AUDIO_CODEC_CAP_DURATION_7_5 | \ ESP_BLE_AUDIO_CODEC_CAP_DURATION_10, /* Frame duration 7.5ms/10ms */ - ESP_BLE_AUDIO_CODEC_CAP_CHAN_COUNT_SUPPORT(2), /* Supported channels 2 */ + /* Bitfield, not a maximum: SUPPORT(1, 2) advertises both 1- and + * 2-channel configurations. A stereo Initiator may either configure two + * 1-channel streams (one per ASE) or a single 2-channel stream. + */ + ESP_BLE_AUDIO_CODEC_CAP_CHAN_COUNT_SUPPORT(1, 2), 30, /* Minimum 30 octets per frame */ 155, /* Maximum 155 octets per frame */ 2); /* Maximum 2 codec frames per SDU */ @@ -86,12 +90,27 @@ static uint8_t ext_adv_data[] = { esp_ble_audio_cap_stream_t *stream_alloc(esp_ble_audio_dir_t dir) { + esp_ble_audio_cap_stream_t *pool; + size_t count; + if (dir == ESP_BLE_AUDIO_DIR_SINK) { - return &peer.sink_stream; + pool = peer.sink_streams; + count = ARRAY_SIZE(peer.sink_streams); + } else if (dir == ESP_BLE_AUDIO_DIR_SOURCE) { + pool = peer.source_streams; + count = ARRAY_SIZE(peer.source_streams); + } else { + return NULL; } - if (dir == ESP_BLE_AUDIO_DIR_SOURCE) { - return &peer.source_stream; + /* A stream is free while no endpoint is attached to it: the stack attaches + * the endpoint right after the Config callback returns and detaches it on + * release, so the pool needs no separate in-use flag. + */ + for (size_t i = 0; i < count; i++) { + if (pool[i].bap_stream.ep == NULL) { + return &pool[i]; + } } return NULL; @@ -99,10 +118,21 @@ esp_ble_audio_cap_stream_t *stream_alloc(esp_ble_audio_dir_t dir) void stream_released(const esp_ble_audio_cap_stream_t *cap_stream) { - if (cap_stream == &peer.source_stream) { - ESP_LOGI(TAG, "Source stream released"); - } else if (cap_stream == &peer.sink_stream) { - ESP_LOGI(TAG, "Sink stream released"); + /* Releasing detached the endpoint, which already returned the object to the + * pool (see stream_alloc), so there is nothing to book-keep here. + */ + for (size_t i = 0; i < ARRAY_SIZE(peer.sink_streams); i++) { + if (cap_stream == &peer.sink_streams[i]) { + ESP_LOGI(TAG, "Sink stream #%zu released", i); + return; + } + } + + for (size_t i = 0; i < ARRAY_SIZE(peer.source_streams); i++) { + if (cap_stream == &peer.source_streams[i]) { + ESP_LOGI(TAG, "Source stream #%zu released", i); + return; + } } } diff --git a/examples/bluetooth/esp_ble_audio/cap/acceptor/sdkconfig.defaults b/examples/bluetooth/esp_ble_audio/cap/acceptor/sdkconfig.defaults index 1da6c3dbbcf..6079089a0f5 100644 --- a/examples/bluetooth/esp_ble_audio/cap/acceptor/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/sdkconfig.defaults @@ -14,7 +14,7 @@ CONFIG_BT_GATTC_NOTIF_REG_MAX=20 CONFIG_BT_BLE_FEAT_ISO_EN=y CONFIG_BT_BLE_FEAT_PERIODIC_ADV_SYNC_TRANSFER=y -CONFIG_BT_ISO_MAX_CHAN=2 +CONFIG_BT_ISO_MAX_CHAN=4 CONFIG_BT_CAP_ACCEPTOR=y CONFIG_BT_BAP_UNICAST_SERVER=y @@ -33,5 +33,27 @@ CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=60 CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y CONFIG_EXAMPLE_UNICAST=y +CONFIG_EXAMPLE_BROADCAST=y CONFIG_FREERTOS_HZ=1000 + +# To place LE Audio .bss + control-plane heap in PSRAM, uncomment the +# lines below (needs a PSRAM-capable target, e.g. esp32s31/esp32h4). +# CONFIG_SPIRAM=y +# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_HEAP_EXTERNAL_MEMORY=y +# CONFIG_BT_AUDIO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_AUDIO_HEAP_EXTERNAL_MEMORY=y + +# Bandwidth-optimized (critical-only) compressed BLE Audio/ISO logging over +# UART DMA. Pins shown are for esp32s31 (UART0 TX = GPIO58, 3000000 baud); set +# CONFIG_BLE_LOG_PRPH_UART_DMA_* values to the port/pin your capture reads. +# CONFIG_BLE_LOG_ENABLED=y +# CONFIG_BT_LOG_CRITICAL_ONLY=y +# CONFIG_BLE_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_LOG_PRPH_UART_DMA=y +# CONFIG_BLE_LOG_PRPH_UART_DMA_PORT=0 +# CONFIG_BLE_LOG_PRPH_UART_DMA_BAUD_RATE=3000000 +# CONFIG_BLE_LOG_PRPH_UART_DMA_TX_IO_NUM=58 diff --git a/examples/bluetooth/esp_ble_audio/cap/handover/CMakeLists.txt b/examples/bluetooth/esp_ble_audio/cap/handover/CMakeLists.txt new file mode 100644 index 00000000000..de20d93ce7b --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/handover/CMakeLists.txt @@ -0,0 +1,8 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +# "Trim" the build. Include the minimal set of components, main, and anything it depends on. +idf_build_set_property(MINIMAL_BUILD ON) +project(cap_handover) diff --git a/examples/bluetooth/esp_ble_audio/cap/handover/README.md b/examples/bluetooth/esp_ble_audio/cap/handover/README.md new file mode 100644 index 00000000000..98b18492c64 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/handover/README.md @@ -0,0 +1,243 @@ +| Supported Targets | ESP32-H4 | ESP32-S31 | +| ----------------- | -------- | --------- | + +# CAP Handover Example + +(See the README.md file in the upper level `examples` directory for more information about examples.) + +## Overview + +This example implements a device with a **collocated CAP Initiator and CAP Commander** and moves +the audio back and forth between unicast and broadcast using the **CAP Handover procedures** +(CAP v1.0.1 §7.3.1.10 Unicast to Broadcast, §7.3.1.11 Broadcast to Unicast). + +It connects to a CAP Acceptor exactly like [`../initiator/`](../initiator/) does — scan, connect, +pair, exchange MTU, discover CAS / sink ASEs / source ASEs — and additionally discovers the +Acceptor's **Broadcast Audio Scan Service (BASS)**, which the Commander writes into to tell the +Acceptor about the broadcast source. Once unicast audio is streaming, a timer alternates between +the two procedures every `EXAMPLE_HANDOVER_PERIOD_S`. + +**Only the sink direction is handed over.** A broadcast Audio Stream is one-directional, so the +return (Acceptor to Initiator) stream is not part of the procedure: §7.3.1.10 applies to "all CISes +within the CIG that is carrying an Audio Stream from the Initiator to the Acceptor", and leaves the +reverse direction implementation specific. + +The very same `bt_cap_stream` objects carry the audio before and after a handover — that is a +requirement of the CAP handover API, not a shortcut: the procedure validates that every broadcast +stream is one of the unicast group's streaming sink streams, and that *all* of them are handed over. +Consequently there is **no standalone broadcast source** in this example: it is created by the +unicast-to-broadcast procedure and deleted by the reverse one. + +## Requirements + +* A board with BLE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31) +* A second board running [`../acceptor/`](../acceptor/) with **both** `EXAMPLE_UNICAST` and + `EXAMPLE_BROADCAST` enabled (its `sdkconfig.defaults` already does), so it exposes ASCS and BASS + at the same time +* The Acceptor must **not** be built with `EXAMPLE_SCAN_SELF` (its Kconfig already makes that + mutually exclusive with `EXAMPLE_UNICAST`) — see *Periodic advertising sync* below + +## Periodic advertising sync: PAST only + +This example is a **collocated broadcaster**: the same device is the Broadcast Source and the +Commander that tells the Acceptor about it. It therefore never scans for its own broadcast, and it +hands the periodic advertising train over with **PAST** rather than asking the Acceptor to find it. + +* The Add Source operation is written with `PA_Sync = 0x01` (*synchronize, PAST available*). +* The Acceptor answers by putting its Broadcast Receive State into `PA_Sync_State = 1` + (*SyncInfo Request*). +* Seeing that state, this example sends **LE Periodic Advertising Set Info Transfer** — the + variant for handing over one of *our own* advertising sets. (The other variant, Sync Transfer, + applies when an Assistant relays a sync it holds to a third-party broadcaster; that is not this + case.) The Source ID goes in the **high octet** of the service data, which is where the Acceptor + reads it from. + +There is deliberately **no self-scan fallback**: a real earbud is not asked to scan for a source +its phone already knows about. If the Acceptor were built to self-scan instead, it would sit at +"Waiting for BASE" forever. + +## Configuration + +```bash +idf.py menuconfig +``` + +Under **Example: CAP Handover**: + +* `EXAMPLE_HANDOVER_PERIOD_S` (default 300) — seconds between switches. Set to 0 to stay on + unicast after the initial setup. + +> **Size note.** The Broadcast Assistant (BASS *client*) is genuinely used — it is how the +> Commander writes Add Source / Modify Source / Remove Source. The Scan Delegator (BASS +> *server*) is **not**: it is pulled in only because every branch of `BT_CAP_COMMANDER`'s +> `depends on` that avoids it requires some other client role we do not need either +> (`BT_VCP_VOL_CTLR`, `BT_MICP_MIC_CTLR`, `BT_TBS_CLIENT`, `BT_MCC`). CAP §7.3.1.8 only requires +> a Commander to act as Scan Delegator when it is **not collocated** with the Initiator **and** +> the stream is encrypted — neither holds here — so this is an over-constraint in the Kconfig +> rather than a real dependency. + +### Security & Pairing + +Just-Works pairing (LE Secure Connections, no MITM) with bonding, 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 +``` + +For the **NimBLE** host, layer the overlay: + +```bash +idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor +``` + +(Exit serial monitor with `Ctrl-]`.) + +## Example Flow + +1. `app_main` brings up NVS, the controller, the LE Audio common layer, the unicast role, the + handover module and the TX pump, then starts scanning. +2. On finding CAS in a connectable advertisement: connect, pair, exchange MTU, GATT discovery. +3. Discover CAS, then the sink and source ASEs, then **BASS**. +4. **Sweep the Acceptor's Broadcast Receive States.** Discovery only subscribes to them, so the + example reads each one once and removes any left over from an earlier run of ours (matched by + Broadcast ID). Receive states live on the Acceptor and survive a reflash of *this* board, and + BAP §6.5.4 forbids an Add Source that would duplicate the + {address, SID, Broadcast ID} triple — without the sweep the first handover after a reboot + fails with `0xFC` (Write Request Rejected). The sweep then continues into + `cap_handover_unicast_setup_and_start()`. +5. A unicast group is created with one CIS per sink stream (the return direction shares the first + CIS) and the streams start. +6. **Unicast to broadcast**, every `EXAMPLE_HANDOVER_PERIOD_S`: + * the advertising set is brought up (extended advertising started, periodic advertising + configured but *stopped* — the BASE does not exist yet); + * the local public address and SID are registered with the audio stack, because the BASS Add + Source operation carries them; + * `esp_ble_audio_cap_handover_unicast_to_broadcast()` stops and releases the unicast group, + creates the broadcast source, and has the Commander write Add Source to the Acceptor; + * from the `created` callback the BASE is encoded and periodic advertising is started. +7. **Broadcast to unicast**, one period later: the Commander stops the reception on the Acceptor + (using the Source ID learned from the Broadcast Receive State), the broadcast source is stopped + and deleted, a new unicast group is created and the streams start again; advertising is stopped. +8. On ACL disconnect any broadcast source left over is deleted and scanning resumes. + +## Expected Log + +TAG: `CAP_HOV`. + +``` +I (xxx) CAP_HOV: CAP initiator unicast initialized +I (xxx) CAP_HOV: CAP initiator handover initialized (period 60 s) +I (xxx) CAP_HOV: Scanning for CAP Acceptor... +I (xxx) CAP_HOV: Found CAS in peer adv data! +I (xxx) CAP_HOV: Connected: handle ... role ... peer ... +I (xxx) CAP_HOV: Discover sources complete +I (xxx) CAP_HOV: Discovering BASS +I (xxx) CAP_HOV: BASS discovered (1 receive state(s)) +I (xxx) CAP_HOV: Created unicast group +I (xxx) CAP_HOV: [SNK #0] Stream started +I (xxx) CAP_HOV: [SNK #1] Stream started +I (xxx) CAP_HOV: Unicast start completed + +I (xxx) CAP_HOV: Handover: unicast -> broadcast (2 stream(s)) +I (xxx) CAP_HOV: Advertising started, BASE pending (handle 0) +I (xxx) CAP_HOV: Periodic advertising started (handle 0) +I (xxx) CAP_HOV: Acceptor receive state: src_id 1 pa_sync 2 +I (xxx) CAP_HOV: Handover to broadcast completed + +I (xxx) CAP_HOV: Handover: broadcast -> unicast (2 stream(s)) +I (xxx) CAP_HOV: Advertising stopped (handle 0) +I (xxx) CAP_HOV: Handover to unicast completed +``` + +Note the TX label follows the transport: `[SNK #0]` while the stream runs on a CIS, `[SRC #0]` +once the same stream object is bound to a broadcast source endpoint. Seeing `[SRC #0] TX: 6000 +packets` during a broadcast period is the check that audio really is going out over the BIS. + +On the acceptor side each switch shows the sink ASEs being released and a BASS Add Source arriving, +then the reverse. + +### Timing + +Each procedure is strictly sequential, so there is a silent gap at every switch. Measured on +ESP32-S31 with two sink streams, it is stable to within a few milliseconds: + +| Direction | Duration | +|---|---| +| unicast to broadcast | ~1.69 s | +| broadcast to unicast | ~2.65 s | + +## Diagnostics + +Either side of every handover the example samples the ISO timing of each stream and logs it under +`[]`, where phase is `u2b-pre`, `u2b-post`, `b2u-pre` or `b2u-post`: + +``` +I (xxx) CAP_HOV: [u2b-pre][0] state 4 ts 66125612 us offset 0 us seq 6025 +I (xxx) CAP_HOV: [u2b-pre][0] iso_interval 10000 us cig_sync_delay 2256 us cis_sync_delay 2256 us +I (xxx) CAP_HOV: [u2b-pre][0] c2p {ft 1 bn 1 latency 2256 us sdu_interval unknown (v1 event)} +I (xxx) CAP_HOV: [u2b-pre][0] p2c {ft 1 bn 0 latency 0 us sdu_interval unknown (v1 event)} +I (xxx) CAP_HOV: [u2b-pre][0] pd_pref [20000, 40000] us pd_range [20000, 40000] us +``` + +A BIS reports the same first line plus its own terms, and no QoS preference: + +``` +I (xxx) CAP_HOV: [u2b-post][0] state 4 (timestamp not read: transport just started) +I (xxx) CAP_HOV: [u2b-post][0] iso_interval 10000 us big_sync_delay 1974 us +I (xxx) CAP_HOV: [u2b-post][0] bis {pto 0 bn 1 latency 1974 us} +``` + +These are the terms CAP §7.3.1.10's rendering-point alignment would need. Reading them: + +* `ts` is `TX_Time_Stamp`: the CIG reference point or BIG anchor point, taken from the + controller's free-running clock. Both transports use the same clock, so the two can be + subtracted directly. +* The **post** samples deliberately do not read `ts`. HCI answers *Command Disallowed* until a + stream has sent its first SDU, and a transport that has just come up has not; the timestamp for + the new transport shows up in the next **pre** sample instead. +* `sdu_interval` is reported as unknown because it is only carried by + `HCI_LE_CIS_Established_V2`; the controller sends the v1 event. Use the QoS value instead. +* `latency` is the transport latency the controller actually achieved, not the maximum the preset + asked for. +* `ft` is derived: the host stores `flush_timeout = FT x ISO_Interval`, so it is divided back out + here — the alignment arithmetic wants FT itself. +* Only a CIS prints `pd_pref` / `pd_range`. Those come from the peer ASE's Codec Configured + notification, and a broadcast source has no peer to state a preference, so the fields would + only ever read zero. + +## Peer Pairing + +1. Flash [`../acceptor/`](../acceptor/) on one board with both roles enabled (default). +2. Flash this example on the other board. +3. Audio starts on CISes, then alternates with BISes every period. + +## Notes + +* The handover procedures in this stack are **sequential**: the unicast streams are stopped before + the broadcast source is created (CAP allows this as the "Initiator does not have the resources to + concurrently run both" path). There is therefore an audible gap at each switch. CAP §7.3.1.10 also + recommends aligning the rendering points of the two streams, which only makes sense on the + concurrent path and is not implemented. +* `Streaming_Audio_Contexts` and `CCID_List` are required by the specification to be the same on + both sides of a handover. So is the **codec configuration**: a different sampling frequency, + frame duration or frame size would make the Acceptor's decoder reconfigure exactly at the + switching point. Both paths therefore take their configuration from one + `HANDOVER_LC3_PRESET_DEFINE()` in `cap_handover.h`, and `handover_audio_config_check()` rejects + the handover if they ever diverge. Only the channel allocation differs by design: unicast puts + one channel in each stream, broadcast puts it in a per-BIS LTV. +* The **Source ID is assigned by the Acceptor**, not chosen here — the Add Source operation has no + such field. It is learned from the Broadcast Receive State and then used for Modify/Remove + Source. It keeps counting across a reflash of this board and only restarts when the *Acceptor* + reboots, which is why the receive-state sweep in step 4 exists. + +## Troubleshooting + +| Symptom | Cause | +|---|---| +| First handover after reflashing this board fails with `err 252` | A receive state left on the Acceptor by the previous run. The sweep in step 4 clears it; if it was removed, this is what comes back. | +| Acceptor logs "Syncing without PAST", then waits for the BASE forever | The Add Source went out with `PA_Sync = 0x02`. Check that the advertising set is registered with `esp_ble_audio_bap_broadcast_adv_add()` before the handover — its address and SID are what make PAST available. | +| Broadcast period is silent, no `[SRC #x] TX:` line | The TX pump did not register for the broadcast streams. It decides with `bt_bap_ep_info.can_send`; deriving it from `dir` instead does not work, because a broadcast source endpoint reports `dir = SOURCE` while a unicast client endpoint reports the peer's direction. | diff --git a/examples/bluetooth/esp_ble_audio/cap/handover/main/CMakeLists.txt b/examples/bluetooth/esp_ble_audio/cap/handover/main/CMakeLists.txt new file mode 100644 index 00000000000..253b9dd9987 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/handover/main/CMakeLists.txt @@ -0,0 +1,18 @@ +set(srcs "main.c" + "cap_handover_proc.c" + "cap_handover_unicast.c" + "cap_handover_broadcast.c" + "cap_handover_tx.c" + "cap_handover_diag.c") + +if(CONFIG_BT_BLUEDROID_ENABLED) + list(APPEND srcs "bluedroid/central.c" + "bluedroid/adv.c") +else() + list(APPEND srcs "nimble/central.c" + "nimble/adv.c") +endif() + +idf_component_register(SRCS ${srcs} + INCLUDE_DIRS "." + REQUIRES bt nvs_flash) diff --git a/examples/bluetooth/esp_ble_audio/cap/handover/main/Kconfig.projbuild b/examples/bluetooth/esp_ble_audio/cap/handover/main/Kconfig.projbuild new file mode 100644 index 00000000000..dd2c6311e4f --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/handover/main/Kconfig.projbuild @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 + +menu "Example: CAP Handover" + + config EXAMPLE_HANDOVER_PERIOD_S + int "Seconds between automatic handovers (0 to disable)" + default 300 + help + Once unicast audio is streaming the sample alternates between unicast + and broadcast every this many seconds. Set to 0 to stay on unicast + after the initial setup. + +endmenu diff --git a/examples/bluetooth/esp_ble_audio/cap/handover/main/bluedroid/adv.c b/examples/bluetooth/esp_ble_audio/cap/handover/main/bluedroid/adv.c new file mode 100644 index 00000000000..39d35dee209 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/handover/main/bluedroid/adv.c @@ -0,0 +1,190 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "esp_log.h" +#include "esp_err.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +#include "esp_bt_defs.h" +#include "esp_bt_device.h" +#include "esp_gap_ble_api.h" + +#include "adv.h" +#include "cap_handover.h" + +static SemaphoreHandle_t adv_sem; + +static esp_bt_status_t adv_op_status; + +#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, adv_sem, portMAX_DELAY, adv_op_status) + +static esp_ble_gap_ext_adv_params_t ext_adv_params = { + .type = ESP_BLE_GAP_SET_EXT_ADV_PROP_NONCONN_NONSCANNABLE_UNDIRECTED, + .interval_min = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS), + .interval_max = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS), + .channel_map = ADV_CHNL_ALL, + .filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, + .primary_phy = ESP_BLE_GAP_PHY_1M, + .max_skip = 0, + .secondary_phy = ESP_BLE_GAP_PHY_2M, + .sid = ADV_SID, + .scan_req_notif = false, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .tx_power = ADV_TX_POWER, +}; + +static esp_ble_gap_periodic_adv_params_t periodic_adv_params = { + .interval_min = ESP_BLE_GAP_PERIODIC_ADV_ITVL_MS(PER_ADV_INTERVAL_MS), + .interval_max = ESP_BLE_GAP_PERIODIC_ADV_ITVL_MS(PER_ADV_INTERVAL_MS), + .properties = 0, +}; + +static esp_ble_gap_ext_adv_t ext_adv_inst[1] = { + [0] = { ADV_HANDLE, 0, 0 }, +}; + +void adv_gap_event_handler(esp_gap_ble_cb_event_t event, + esp_ble_gap_cb_param_t *param) +{ + switch (event) { + case ESP_GAP_BLE_EXT_ADV_SET_PARAMS_COMPLETE_EVT: + adv_op_status = param->ext_adv_set_params.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_EXT_ADV_DATA_SET_COMPLETE_EVT: + adv_op_status = param->ext_adv_data_set.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_EXT_ADV_START_COMPLETE_EVT: + adv_op_status = param->ext_adv_start.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_EXT_ADV_STOP_COMPLETE_EVT: + adv_op_status = param->ext_adv_stop.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_PERIODIC_ADV_SET_PARAMS_COMPLETE_EVT: + adv_op_status = param->peroid_adv_set_params.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_PERIODIC_ADV_DATA_SET_COMPLETE_EVT: + adv_op_status = param->period_adv_data_set.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_PERIODIC_ADV_START_COMPLETE_EVT: + adv_op_status = param->period_adv_start.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_PERIODIC_ADV_STOP_COMPLETE_EVT: + adv_op_status = param->period_adv_stop.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_PERIODIC_ADV_SET_INFO_TRANS_COMPLETE_EVT: + /* Not awaited: the Acceptor reports the result through its receive state. */ + if (param->period_adv_set_info_trans.status != ESP_BT_STATUS_SUCCESS) { + ESP_LOGE(TAG, "PAST failed, status %d", param->period_adv_set_info_trans.status); + } + break; + default: + break; + } +} + +int adv_host_init(void) +{ + adv_sem = xSemaphoreCreateBinary(); + if (adv_sem == NULL) { + ESP_LOGE(TAG, "Failed to create adv semaphore"); + return -1; + } + + return 0; +} + +static int adv_set_configure(const uint8_t *ext_data, uint8_t ext_len) +{ + WAIT_API(esp_ble_gap_ext_adv_set_params(ADV_HANDLE, &ext_adv_params)); + WAIT_API(esp_ble_gap_config_ext_adv_data_raw(ADV_HANDLE, ext_len, ext_data)); + WAIT_API(esp_ble_gap_periodic_adv_set_params(ADV_HANDLE, &periodic_adv_params)); + + return 0; +} + +int per_adv_data_start(const uint8_t *per_data, uint8_t per_len) +{ +#if CONFIG_BT_BLE_FEAT_PERIODIC_ADV_ENH + WAIT_API(esp_ble_gap_config_periodic_adv_data_raw(ADV_HANDLE, per_len, per_data, false)); + WAIT_API(esp_ble_gap_periodic_adv_start(ADV_HANDLE, true)); +#else + WAIT_API(esp_ble_gap_config_periodic_adv_data_raw(ADV_HANDLE, per_len, per_data)); + WAIT_API(esp_ble_gap_periodic_adv_start(ADV_HANDLE)); +#endif + + ESP_LOGI(TAG, "Periodic advertising started (handle %u)", ADV_HANDLE); + return 0; +} + + +int ext_adv_start_without_base(const uint8_t *ext_data, uint8_t ext_len) +{ + int err; + + err = adv_set_configure(ext_data, ext_len); + if (err) { + return err; + } + + WAIT_API(esp_ble_gap_ext_adv_start(1, ext_adv_inst)); + + ESP_LOGI(TAG, "Advertising started, BASE pending (handle %u)", ADV_HANDLE); + return 0; +} + +int adv_stop(void) +{ + uint8_t instance = ADV_HANDLE; + + WAIT_API(esp_ble_gap_periodic_adv_stop(ADV_HANDLE)); + WAIT_API(esp_ble_gap_ext_adv_stop(1, &instance)); + + ESP_LOGI(TAG, "Advertising stopped (handle %u)", ADV_HANDLE); + return 0; +} + +int local_public_addr_get(uint8_t addr[6]) +{ + const uint8_t *bda = esp_bt_dev_get_address(); + + if (bda == NULL) { + ESP_LOGE(TAG, "Local BD address unavailable"); + return -1; + } + + /* Bluedroid hands out addresses MSB-first; the caller feeds this to the audio + * stack, which stores a bt_addr_le_t and puts it on air LSB-first. + */ + for (size_t i = 0; i < 6; i++) { + addr[i] = bda[5 - i]; + } + + return 0; +} + +int pa_set_info_transfer(uint16_t conn_handle, const uint8_t peer_addr[6], + uint16_t service_data) +{ + esp_bd_addr_t addr; + + (void)conn_handle; + memcpy(addr, peer_addr, sizeof(addr)); + + return esp_ble_gap_periodic_adv_set_info_trans(addr, service_data, ADV_HANDLE); +} diff --git a/examples/bluetooth/esp_ble_audio/cap/handover/main/bluedroid/adv.h b/examples/bluetooth/esp_ble_audio/cap/handover/main/bluedroid/adv.h new file mode 100644 index 00000000000..baf70d7a147 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/handover/main/bluedroid/adv.h @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "esp_gap_ble_api.h" + +void adv_gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); + +int adv_host_init(void); diff --git a/examples/bluetooth/esp_ble_audio/cap/handover/main/bluedroid/central.c b/examples/bluetooth/esp_ble_audio/cap/handover/main/bluedroid/central.c new file mode 100644 index 00000000000..30532bc3f14 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/handover/main/bluedroid/central.c @@ -0,0 +1,190 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "esp_log.h" +#include "esp_err.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +#include "esp_bt_defs.h" +#include "esp_gap_ble_api.h" +#include "esp_gattc_api.h" + +#include "esp_ble_audio_common_api.h" + +#include "adv.h" +#include "cap_handover.h" + +static SemaphoreHandle_t scan_sem; + +static esp_bt_status_t scan_op_status; + +#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, scan_sem, portMAX_DELAY, scan_op_status) + +static esp_bd_addr_t peer_bda; + +static esp_ble_ext_scan_params_t ext_scan_params = { + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE, + .cfg_mask = ESP_BLE_GAP_EXT_SCAN_CFG_UNCODE_MASK, + .uncoded_cfg = { + .scan_type = BLE_SCAN_TYPE_PASSIVE, + .scan_interval = SCAN_INTERVAL, + .scan_window = SCAN_WINDOW, + }, +}; + +static void gap_event_handler(esp_gap_ble_cb_event_t event, + esp_ble_gap_cb_param_t *param) +{ + switch (event) { + case ESP_GAP_BLE_SET_EXT_SCAN_PARAMS_COMPLETE_EVT: + scan_op_status = param->set_ext_scan_params.status; + xSemaphoreGive(scan_sem); + break; + case ESP_GAP_BLE_EXT_SCAN_START_COMPLETE_EVT: + scan_op_status = param->ext_scan_start.status; + xSemaphoreGive(scan_sem); + break; + case ESP_GAP_BLE_EXT_SCAN_STOP_COMPLETE_EVT: + scan_op_status = param->ext_scan_stop.status; + xSemaphoreGive(scan_sem); + break; + + case ESP_GAP_BLE_NC_REQ_EVT: + esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, true); + break; + + case ESP_GAP_BLE_AUTH_CMPL_EVT: + esp_ble_audio_gap_app_post_event(event, param); + break; + + default: + /* Bluedroid has one GAP callback slot; the advertising events belong + * to the other wrapper. The two event sets are disjoint. */ + adv_gap_event_handler(event, param); + break; + } +} + +int app_host_init(void) +{ + esp_err_t err; + + scan_sem = xSemaphoreCreateBinary(); + if (scan_sem == NULL) { + ESP_LOGE(TAG, "Failed to create scan semaphore"); + return -1; + } + + err = adv_host_init(); + if (err) { + vSemaphoreDelete(scan_sem); + return err; + } + + err = esp_ble_gap_register_callback(gap_event_handler); + if (err) { + ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err); + vSemaphoreDelete(scan_sem); + return err; + } + + return 0; +} + +int set_device_name(void) +{ + return esp_ble_gap_set_device_name(LOCAL_DEVICE_NAME); +} + +int ext_scan_start(void) +{ + WAIT_API(esp_ble_gap_set_ext_scan_params(&ext_scan_params)); + WAIT_API(esp_ble_gap_start_ext_scan(0, 0)); + + ESP_LOGI(TAG, "Scanning for CAP Acceptor..."); + return 0; +} + +int ext_scan_stop(void) +{ + WAIT_API(esp_ble_gap_stop_ext_scan()); + return 0; +} + +int conn_create(uint8_t addr_type, const uint8_t addr[6]) +{ + const esp_ble_gap_conn_params_t conn_params = { + .scan_interval = INIT_SCAN_INTERVAL, + .scan_window = INIT_SCAN_WINDOW, + .interval_min = CONN_INTERVAL, + .interval_max = CONN_INTERVAL, + .latency = CONN_LATENCY, + .supervision_timeout = CONN_TIMEOUT, + .min_ce_len = CONN_MIN_CE_LEN, + .max_ce_len = CONN_MAX_CE_LEN, + }; + esp_gatt_if_t gattc_if; + esp_err_t err; + + memcpy(peer_bda, addr, sizeof(peer_bda)); + + err = esp_ble_gap_prefer_ext_connect_params_set( + peer_bda, ESP_BLE_GAP_PHY_1M_PREF_MASK, &conn_params, NULL, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to set ext conn params, err %d", err); + return err; + } + + /* Use the audio engine's GATTC if — events route back to the engine + * which forwards them to the lib. Registering a separate example GATTC + * app would steer events to a handler the engine never sees. + * + * engine returns ESP_GATT_IF_NONE (0xFF) when GATTC is not yet + * registered; feeding that into aux_open silently no-ops in BTC and + * no acl_connect event ever fires, leaving the caller in a no-scan + * / no-conn dead state. */ + gattc_if = esp_ble_audio_bluedroid_get_gattc_if(); + if (gattc_if == ESP_GATT_IF_NONE) { + ESP_LOGE(TAG, "GATTC not registered"); + return ESP_ERR_INVALID_STATE; + } + + return esp_ble_gattc_aux_open(gattc_if, peer_bda, + (esp_ble_addr_type_t)addr_type, true); +} + +int pairing_start(uint16_t conn_handle) +{ + (void)conn_handle; + return esp_ble_set_encryption(peer_bda, ESP_BLE_SEC_ENCRYPT_NO_MITM); +} + +int exchange_mtu(uint16_t conn_handle) +{ + (void)conn_handle; + /* The Bluedroid GATTC adapter exchanges MTU automatically when aux_open + * brings up the ACL — the MTU_UPDATED event arrives without an explicit + * kick-off. NimBLE has no such auto-exchange, so this wrapper is a no-op + * on bluedroid and a real ble_gattc_exchange_mtu on nimble. */ + return 0; +} + +void security_failed_recover(uint16_t conn_handle, uint8_t status) +{ + (void)conn_handle; + + ESP_LOGE(TAG, "Security change failed, status %u, clearing local bond and reconnecting", status); + + esp_ble_remove_bond_device(peer_bda); + esp_ble_gap_disconnect(peer_bda); +} diff --git a/examples/bluetooth/esp_ble_audio/cap/handover/main/cap_handover.h b/examples/bluetooth/esp_ble_audio/cap/handover/main/cap_handover.h new file mode 100644 index 00000000000..4243d72e8cb --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/handover/main/cap_handover.h @@ -0,0 +1,150 @@ +/* + * SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "esp_log.h" + +#include "sdkconfig.h" + +#include "esp_ble_audio_lc3_defs.h" +#include "esp_ble_audio_bap_api.h" +#include "esp_ble_audio_cap_api.h" +#include "esp_ble_audio_pacs_api.h" +#include "esp_ble_audio_bap_lc3_preset_defs.h" + +#include "ble_audio_example_init.h" +#include "ble_audio_example_utils.h" + +#define TAG "CAP_HOV" + +#define CONN_HANDLE_INIT 0xFFFF + +#define SINK_STREAM_COUNT MIN(CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT, \ + CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT) + +#define LOCAL_DEVICE_NAME "CAP Handover" + +#define SCAN_INTERVAL 160 /* 100ms */ +#define SCAN_WINDOW 160 /* 100ms */ + +#define INIT_SCAN_INTERVAL 16 /* 10ms */ +#define INIT_SCAN_WINDOW 16 /* 10ms */ +#define CONN_INTERVAL 24 /* 30ms */ +#define CONN_LATENCY 0 +#define CONN_TIMEOUT 500 /* 5s */ +#define CONN_MIN_CE_LEN 0xFFFF +#define CONN_MAX_CE_LEN 0xFFFF + +#define ADV_HANDLE 0 +#define ADV_SID 0 +#define ADV_TX_POWER 127 +#define ADV_INTERVAL_MS 200 +#define PER_ADV_INTERVAL_MS 100 + +#define LOCAL_BROADCAST_ID 0x123456 + +#define HANDOVER_LC3_PRESET_DEFINE(_name, _loc) \ + ESP_BLE_AUDIO_BAP_LC3_UNICAST_PRESET_16_2_1_DEFINE( \ + _name, _loc, ESP_BLE_AUDIO_CONTEXT_TYPE_UNSPECIFIED) + +int app_host_init(void); + +int set_device_name(void); + +int ext_scan_start(void); + +int ext_scan_stop(void); + +int conn_create(uint8_t addr_type, const uint8_t addr[6]); + +int pairing_start(uint16_t conn_handle); + +int exchange_mtu(uint16_t conn_handle); + +void security_failed_recover(uint16_t conn_handle, uint8_t status); + +int ext_adv_start_without_base(const uint8_t *ext_data, uint8_t ext_len); + +int per_adv_data_start(const uint8_t *per_data, uint8_t per_len); + +int adv_stop(void); + +int local_public_addr_get(uint8_t addr[6]); + +int pa_set_info_transfer(uint16_t conn_handle, const uint8_t peer_addr[6], + uint16_t service_data); + +struct tx_stream { + esp_ble_audio_cap_stream_t *stream; + uint16_t seq_num; + uint8_t *data; + example_audio_tx_scheduler_t scheduler; + bool is_broadcast; +}; + +struct peer_config { + esp_ble_audio_cap_stream_t sink_streams[SINK_STREAM_COUNT]; + esp_ble_audio_bap_ep_t *sink_eps[SINK_STREAM_COUNT]; + size_t sink_ep_count; + + esp_ble_conn_t *conn; + uint16_t conn_handle; + uint8_t dst[6]; + + bool disc_completed; + bool mtu_exchanged; +}; + +extern struct peer_config peer; + +esp_ble_audio_cap_unicast_group_t *unicast_group_get(void); + +void unicast_group_set(esp_ble_audio_cap_unicast_group_t *group); + +int unicast_group_delete(void); + +esp_ble_audio_bap_lc3_preset_t *sink_preset_get(size_t idx); + +int cap_handover_unicast_setup_and_start(void); + +void cap_handover_unicast_gap_cb(esp_ble_audio_gap_app_event_t *event); + +void cap_handover_unicast_gatt_cb(esp_ble_audio_gatt_app_event_t *event); + +int cap_handover_unicast_start(void); + +int cap_handover_unicast_init(void); + +uint8_t *broadcast_ext_adv_data_get(uint8_t *data_len); + +uint8_t *broadcast_base_data_get(esp_ble_audio_cap_broadcast_source_t *source, + uint8_t *data_len); + +esp_ble_audio_bap_lc3_preset_t *broadcast_preset_get(void); + +esp_ble_audio_location_t broadcast_bis_location_get(size_t idx); + +int cap_handover_proc_init(void); + +int cap_handover_proc_discover(uint16_t conn_handle); + +void cap_handover_proc_unicast_started(void); + +void cap_handover_proc_broadcast_stopped(esp_ble_audio_cap_broadcast_source_t *source); + +void cap_handover_diag_sample(const char *phase, bool has_sent); + +void cap_handover_proc_reset(void); + +void cap_handover_tx_stream_sent(esp_ble_audio_bap_stream_t *stream, void *user_data); + +int cap_handover_tx_register_stream(esp_ble_audio_cap_stream_t *cap_stream, bool is_broadcast); + +int cap_handover_tx_unregister_stream(esp_ble_audio_cap_stream_t *cap_stream); + +void cap_handover_tx_init(void); diff --git a/examples/bluetooth/esp_ble_audio/cap/handover/main/cap_handover_broadcast.c b/examples/bluetooth/esp_ble_audio/cap/handover/main/cap_handover_broadcast.c new file mode 100644 index 00000000000..c5616011bdd --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/handover/main/cap_handover_broadcast.c @@ -0,0 +1,104 @@ +/* + * SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "cap_handover.h" + +/* Shared subgroup configuration; the per-BIS channel allocation goes in a + * separate LTV, so the location here stays mono. + */ +HANDOVER_LC3_PRESET_DEFINE(broadcast_preset, ESP_BLE_AUDIO_LOCATION_MONO_AUDIO); + +/* One BIS per channel; the handover module turns these into per-BIS LTVs. */ +static const esp_ble_audio_location_t bis_locations[] = { + ESP_BLE_AUDIO_LOCATION_FRONT_LEFT, + ESP_BLE_AUDIO_LOCATION_FRONT_RIGHT, +}; + +_Static_assert(ARRAY_SIZE(bis_locations) >= SINK_STREAM_COUNT, + "Need one channel allocation per broadcast stream"); + +uint8_t *broadcast_ext_adv_data_get(uint8_t *data_len) +{ + uint32_t broadcast_id; + uint8_t *data; + + broadcast_id = LOCAL_BROADCAST_ID; + + /* - Broadcast Audio Announcement Service UUID (2 octets) + * - Broadcast ID (3 octets) + * - Complete Device Name + */ + *data_len = 7 + 2 + strlen(LOCAL_DEVICE_NAME); + + data = calloc(1, *data_len); + if (data == NULL) { + ESP_LOGE(TAG, "Failed to alloc ext adv data (%u octets)", *data_len); + return NULL; + } + + data[0] = 0x06; /* 1 + 2 + 3 */ + data[1] = EXAMPLE_AD_TYPE_SERVICE_DATA16; + data[2] = (ESP_BLE_AUDIO_UUID_BROADCAST_AUDIO_VAL & 0xFF); + data[3] = ((ESP_BLE_AUDIO_UUID_BROADCAST_AUDIO_VAL >> 8) & 0xFF); + data[4] = (broadcast_id & 0xFF); + data[5] = ((broadcast_id >> 8) & 0xFF); + data[6] = ((broadcast_id >> 16) & 0xFF); + + data[7] = strlen(LOCAL_DEVICE_NAME) + 1; + data[8] = EXAMPLE_AD_TYPE_NAME_COMPLETE; + memcpy(data + 9, LOCAL_DEVICE_NAME, strlen(LOCAL_DEVICE_NAME)); + + return data; +} + +uint8_t *broadcast_base_data_get(esp_ble_audio_cap_broadcast_source_t *source, + uint8_t *data_len) +{ + NET_BUF_SIMPLE_DEFINE(base_buf, 128); + uint8_t *data; + esp_err_t err; + + /* Broadcast Audio Announcement Service UUID (2 octets) and + * Broadcast Audio Source Endpoint (BASE) + */ + + err = esp_ble_audio_cap_initiator_broadcast_get_base(source, &base_buf); + if (err) { + ESP_LOGE(TAG, "Failed to get encoded BASE, err %d", err); + return NULL; + } + + *data_len = 2 + base_buf.len; + + data = calloc(1, *data_len); + if (data == NULL) { + ESP_LOGE(TAG, "Failed to alloc per adv data (%u octets)", *data_len); + return NULL; + } + + /* base_buf.len has included the UUID length (2 octets) */ + data[0] = 1 + base_buf.len; + data[1] = EXAMPLE_AD_TYPE_SERVICE_DATA16; + memcpy(data + 2, base_buf.data, base_buf.len); + + return data; +} + +esp_ble_audio_bap_lc3_preset_t *broadcast_preset_get(void) +{ + return &broadcast_preset; +} + +esp_ble_audio_location_t broadcast_bis_location_get(size_t idx) +{ + return (idx < ARRAY_SIZE(bis_locations)) ? bis_locations[idx] + : ESP_BLE_AUDIO_LOCATION_MONO_AUDIO; +} diff --git a/examples/bluetooth/esp_ble_audio/cap/handover/main/cap_handover_diag.c b/examples/bluetooth/esp_ble_audio/cap/handover/main/cap_handover_diag.c new file mode 100644 index 00000000000..88361372715 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/handover/main/cap_handover_diag.c @@ -0,0 +1,115 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "esp_ble_iso_common_api.h" + +#include "cap_handover.h" + +/* flush_timeout holds FT x ISO_Interval, so divide FT back out. sdu_interval + * only comes with HCI_LE_CIS_Established_V2; a v1 event leaves it UNKNOWN. + */ +static void diag_log_cis_dir(const char *phase, size_t idx, const char *dir, + uint16_t iso_interval, + const struct bt_iso_unicast_tx_info *info) +{ + ESP_LOGI(TAG, "[%s][%zu] %s {ft %u bn %u latency %lu us sdu_interval %s}", + phase, idx, dir, + iso_interval != 0 ? (unsigned)(info->flush_timeout / iso_interval) : 0U, + info->bn, (unsigned long)info->latency, + info->sdu_interval == BT_ISO_SDU_INTERVAL_UNKNOWN ? "unknown (v1 event)" + : "see qos"); +} + +static void diag_sample_stream(const char *phase, size_t idx, + esp_ble_audio_cap_stream_t *stream, bool has_sent) +{ + esp_ble_audio_bap_ep_info_t ep_info = {0}; + esp_ble_iso_tx_info_t tx_info = {0}; + esp_ble_iso_info_t chan_info = {0}; + esp_ble_audio_bap_ep_t *ep = stream->bap_stream.ep; + esp_err_t err; + + if (ep == NULL) { + ESP_LOGI(TAG, "[%s][%zu] no endpoint", phase, idx); + return; + } + + if (esp_ble_audio_bap_ep_get_info(ep, &ep_info) != ESP_OK) { + ESP_LOGW(TAG, "[%s][%zu] endpoint info unavailable", phase, idx); + return; + } + + /* HCI answers Command Disallowed until the stream has sent an SDU, and the + * adapter logs that as an HCI error - so do not ask a transport that just + * came up. Its timestamp shows up in the next pre-handover sample. + */ + if (!has_sent) { + ESP_LOGI(TAG, "[%s][%zu] state %d (timestamp not read: transport just started)", + phase, idx, ep_info.state); + } else if ((err = esp_ble_audio_cap_stream_get_tx_sync(stream, &tx_info)) != ESP_OK) { + ESP_LOGW(TAG, "[%s][%zu] state %d tx_sync failed, err %d", + phase, idx, ep_info.state, err); + } else { + ESP_LOGI(TAG, "[%s][%zu] state %d ts %lu us offset %lu us seq %u", + phase, idx, ep_info.state, + (unsigned long)tx_info.ts, (unsigned long)tx_info.offset, + tx_info.seq_num); + } + + if (ep_info.iso_chan == NULL || + esp_ble_iso_chan_get_info(ep_info.iso_chan, &chan_info) != ESP_OK) { + ESP_LOGI(TAG, "[%s][%zu] no ISO channel info", phase, idx); + return; + } + + /* iso_interval is in 1.25 ms units; print us like every other duration. The + * channel kind shows in the field names, so it is not printed separately. + */ + if (chan_info.type == BT_ISO_CHAN_TYPE_BROADCASTER) { + ESP_LOGI(TAG, "[%s][%zu] iso_interval %lu us big_sync_delay %lu us", + phase, idx, + (unsigned long)chan_info.iso_interval * 1250UL, + (unsigned long)chan_info.broadcaster.sync_delay); + ESP_LOGI(TAG, "[%s][%zu] bis {pto %lu bn %u latency %lu us}", + phase, idx, + (unsigned long)chan_info.broadcaster.pto, + chan_info.broadcaster.bn, + (unsigned long)chan_info.broadcaster.latency); + + /* No qos_pref: it comes from a peer ASE's Codec Configured, and a + * broadcast source has no peer. Printing four zeros would mislead. + */ + return; + } + + ESP_LOGI(TAG, "[%s][%zu] iso_interval %lu us cig_sync_delay %lu us cis_sync_delay %lu us", + phase, idx, + (unsigned long)chan_info.iso_interval * 1250UL, + (unsigned long)chan_info.unicast.cig_sync_delay, + (unsigned long)chan_info.unicast.cis_sync_delay); + + diag_log_cis_dir(phase, idx, "c2p", chan_info.iso_interval, &chan_info.unicast.central); + diag_log_cis_dir(phase, idx, "p2c", chan_info.iso_interval, &chan_info.unicast.peripheral); + + if (ep_info.qos_pref != NULL) { + ESP_LOGI(TAG, "[%s][%zu] pd_pref [%lu, %lu] us pd_range [%lu, %lu] us", + phase, idx, + (unsigned long)ep_info.qos_pref->pref_pd_min, + (unsigned long)ep_info.qos_pref->pref_pd_max, + (unsigned long)ep_info.qos_pref->pd_min, + (unsigned long)ep_info.qos_pref->pd_max); + } +} + +void cap_handover_diag_sample(const char *phase, bool has_sent) +{ + for (size_t i = 0; i < peer.sink_ep_count && i < ARRAY_SIZE(peer.sink_streams); i++) { + diag_sample_stream(phase, i, &peer.sink_streams[i], has_sent); + } +} diff --git a/examples/bluetooth/esp_ble_audio/cap/handover/main/cap_handover_proc.c b/examples/bluetooth/esp_ble_audio/cap/handover/main/cap_handover_proc.c new file mode 100644 index 00000000000..fce4ca556a2 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/handover/main/cap_handover_proc.c @@ -0,0 +1,725 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#include "esp_timer.h" + +#include "cap_handover.h" + +/* 1.25 ms units. The real interval, not "unknown": the Acceptor's PA sync needs it. */ +#define HANDOVER_PA_INTERVAL ((PER_ADV_INTERVAL_MS * 1000) / 1250) + +/* Must stay valid until the procedure completes, so not on the stack. */ +static esp_ble_audio_cap_initiator_broadcast_stream_param_t bcast_stream_params[SINK_STREAM_COUNT]; +static esp_ble_audio_cap_initiator_broadcast_subgroup_param_t bcast_subgroup_param; +static esp_ble_audio_cap_initiator_broadcast_create_param_t bcast_create_param; +static uint8_t bcast_bis_data[SINK_STREAM_COUNT][6]; + +static esp_ble_audio_cap_unicast_group_stream_param_t uni_stream_params[SINK_STREAM_COUNT]; +static esp_ble_audio_cap_unicast_group_stream_pair_param_t uni_pair_params[SINK_STREAM_COUNT]; +static esp_ble_audio_cap_unicast_group_param_t uni_group_param; +static esp_ble_audio_cap_unicast_audio_start_stream_param_t uni_start_stream_params[SINK_STREAM_COUNT]; +static esp_ble_audio_cap_unicast_audio_start_param_t uni_start_param; + +static esp_ble_audio_cap_commander_broadcast_reception_stop_member_param_t stop_member_param; +static esp_ble_audio_cap_commander_broadcast_reception_stop_param_t stop_param; + +static esp_ble_audio_cap_broadcast_source_t *broadcast_source; +static esp_ble_audio_bap_broadcast_adv_info_t adv_info; +static esp_timer_handle_t switch_timer; +static esp_timer_handle_t start_timer; + +static bool adv_registered; +static bool proc_active; +static bool on_broadcast; + +/* Source ID of our broadcast on the Acceptor, needed to stop its reception. */ +static uint8_t recv_state_src_id; +static bool recv_state_valid; + +/* Sweep of the Acceptor's receive states, run once per connection. */ +static uint8_t recv_state_cnt; +static uint8_t recv_state_next; +static bool recv_state_sweeping; + +static void switch_timer_arm(void) +{ + esp_err_t err; + + if (CONFIG_EXAMPLE_HANDOVER_PERIOD_S == 0 || switch_timer == NULL) { + return; + } + + (void)esp_timer_stop(switch_timer); + + err = esp_timer_start_once(switch_timer, + (uint64_t)CONFIG_EXAMPLE_HANDOVER_PERIOD_S * 1000000); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to arm handover timer, err %d", err); + } +} + +static void start_timer_cb(void *arg) +{ + if (peer.conn == NULL) { + return; + } + + (void)cap_handover_unicast_setup_and_start(); +} + +/* CAP holds one procedure at a time and the library shares our callback lists, + * so starting a procedure from inside a callback changes the active procedure + * under a library handler that has not run yet. Leave the dispatch first. + */ +static void unicast_start_defer(void) +{ + esp_err_t err; + + (void)esp_timer_stop(start_timer); + + err = esp_timer_start_once(start_timer, 0); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to schedule unicast start, err %d", err); + } +} + +/* What the Acceptor's decoder must keep doing across the switch. Channel + * allocation is absent on purpose: it differs between the two paths by design. + */ +struct codec_shape { + esp_ble_audio_codec_cfg_freq_t freq; + esp_ble_audio_codec_cfg_frame_dur_t frame_dur; + uint16_t octets_per_frame; + uint8_t frame_blocks; +}; + +static int codec_shape_get(const esp_ble_audio_codec_cfg_t *codec_cfg, + struct codec_shape *out) +{ + if (esp_ble_audio_codec_cfg_get_freq(codec_cfg, &out->freq) != ESP_OK || + esp_ble_audio_codec_cfg_get_frame_dur(codec_cfg, &out->frame_dur) != ESP_OK || + esp_ble_audio_codec_cfg_get_octets_per_frame(codec_cfg, + &out->octets_per_frame) != ESP_OK || + esp_ble_audio_codec_cfg_get_frame_blocks_per_sdu(codec_cfg, &out->frame_blocks, + true) != ESP_OK) { + return -EINVAL; + } + + return 0; +} + +/* Never memcmp: the trailing padding is uninitialised stack. */ +static bool codec_shape_eq(const struct codec_shape *a, const struct codec_shape *b) +{ + return a->freq == b->freq && + a->frame_dur == b->frame_dur && + a->octets_per_frame == b->octets_per_frame && + a->frame_blocks == b->frame_blocks; +} + +/* A mismatch makes the Acceptor reconfigure its decoder at the switching point, + * which is audible. One macro feeds both paths, so this only fires if someone + * later points one of them elsewhere - better loudly than silently. + */ +static int handover_audio_config_check(void) +{ + const esp_ble_audio_bap_lc3_preset_t *bcast = broadcast_preset_get(); + struct codec_shape bcast_shape; + + if (bcast == NULL || codec_shape_get(&bcast->codec_cfg, &bcast_shape) != 0) { + ESP_LOGE(TAG, "Cannot read broadcast codec configuration"); + return -EINVAL; + } + + for (size_t i = 0; i < SINK_STREAM_COUNT; i++) { + const esp_ble_audio_bap_lc3_preset_t *uni = sink_preset_get(i); + struct codec_shape uni_shape; + + if (uni == NULL || codec_shape_get(&uni->codec_cfg, &uni_shape) != 0) { + ESP_LOGE(TAG, "Cannot read sink %u codec configuration", (unsigned)i); + return -EINVAL; + } + + if (!codec_shape_eq(&uni_shape, &bcast_shape)) { + ESP_LOGE(TAG, "Codec config differs on sink %u: " + "unicast freq %d dur %d octets %u blocks %u vs " + "broadcast freq %d dur %d octets %u blocks %u", + (unsigned)i, + uni_shape.freq, uni_shape.frame_dur, + uni_shape.octets_per_frame, uni_shape.frame_blocks, + bcast_shape.freq, bcast_shape.frame_dur, + bcast_shape.octets_per_frame, bcast_shape.frame_blocks); + return -EINVAL; + } + + /* Same SDU rate and framing, or the paths cannot carry the same frame + * sequence. ISO_Interval is the controller's and only observable once + * the streams run - cap_handover_diag_sample() prints it. + */ + if (uni->qos.interval != bcast->qos.interval || + uni->qos.framing != bcast->qos.framing) { + ESP_LOGE(TAG, "QoS differs on sink %u: unicast interval %lu framing %d vs " + "broadcast interval %lu framing %d", (unsigned)i, + (unsigned long)uni->qos.interval, uni->qos.framing, + (unsigned long)bcast->qos.interval, bcast->qos.framing); + return -EINVAL; + } + + /* PD may legitimately differ: it is how a rendering-point offset would + * be absorbed. Nothing uses that yet, so warn rather than fail. + */ + if (uni->qos.pd != bcast->qos.pd) { + ESP_LOGW(TAG, "Presentation delay differs on sink %u: %lu vs %lu us", + (unsigned)i, (unsigned long)uni->qos.pd, (unsigned long)bcast->qos.pd); + } + } + + return 0; +} + +static int handover_unicast_to_broadcast(void) +{ + esp_ble_audio_cap_handover_unicast_to_broadcast_param_t param = {0}; + uint8_t *ext_data = NULL; + uint8_t ext_len = 0; + size_t count = 0; + int err; + + err = handover_audio_config_check(); + if (err) { + return err; + } + + /* All of them: CAP 7.3.1.10 applies to every CIS carrying Initiator-to-Acceptor audio. */ + for (size_t i = 0; i < peer.sink_ep_count && count < ARRAY_SIZE(bcast_stream_params); i++) { + esp_ble_audio_cap_stream_t *stream = &peer.sink_streams[i]; + esp_ble_audio_bap_ep_info_t ep_info = {0}; + esp_ble_audio_location_t loc; + + if (stream->bap_stream.ep == NULL || + esp_ble_audio_bap_ep_get_info(stream->bap_stream.ep, &ep_info) != ESP_OK || + ep_info.state != ESP_BLE_AUDIO_BAP_EP_STATE_STREAMING) { + continue; + } + + /* Shared subgroup codec cfg, so the channel allocation goes per BIS. */ + loc = broadcast_bis_location_get(count); + bcast_bis_data[count][0] = 5; + bcast_bis_data[count][1] = ESP_BLE_AUDIO_CODEC_CFG_CHAN_ALLOC; + bcast_bis_data[count][2] = (uint8_t)loc; + bcast_bis_data[count][3] = (uint8_t)(loc >> 8); + bcast_bis_data[count][4] = (uint8_t)(loc >> 16); + bcast_bis_data[count][5] = (uint8_t)(loc >> 24); + + bcast_stream_params[count].stream = stream; + bcast_stream_params[count].data = bcast_bis_data[count]; + bcast_stream_params[count].data_len = sizeof(bcast_bis_data[count]); + count++; + } + + if (count == 0) { + ESP_LOGW(TAG, "No streaming sink stream to hand over"); + return -ENODEV; + } + + bcast_subgroup_param.stream_count = count; + bcast_subgroup_param.stream_params = bcast_stream_params; + bcast_subgroup_param.codec_cfg = &broadcast_preset_get()->codec_cfg; + + bcast_create_param.subgroup_count = 1; + bcast_create_param.subgroup_params = &bcast_subgroup_param; + bcast_create_param.qos = &broadcast_preset_get()->qos; + + /* PA stays stopped: the BASE only exists once the procedure created the source. */ + ext_data = broadcast_ext_adv_data_get(&ext_len); + if (ext_data == NULL) { + ESP_LOGE(TAG, "No adv data, cannot hand over to broadcast"); + return -ENOMEM; + } + + err = ext_adv_start_without_base(ext_data, ext_len); + free(ext_data); + if (err) { + ESP_LOGE(TAG, "Failed to start advertising for handover, err %d", err); + return err; + } + + if (adv_registered == false) { + adv_info.adv_handle = ADV_HANDLE; + adv_info.addr_type = 0; /* public */ + adv_info.sid = ADV_SID; + + if (local_public_addr_get(adv_info.addr) != 0) { + ESP_LOGE(TAG, "No local address for the BASS Add Source"); + return -EIO; + } + + err = esp_ble_audio_bap_broadcast_adv_add(&adv_info); + if (err) { + ESP_LOGE(TAG, "Failed to add adv for broadcast source, err %d", err); + return err; + } + + adv_registered = true; + } + + param.type = ESP_BLE_AUDIO_CAP_SET_TYPE_AD_HOC; + param.unicast_group = unicast_group_get(); + param.adv_handle = ADV_HANDLE; + param.pa_interval = HANDOVER_PA_INTERVAL; + param.broadcast_id = LOCAL_BROADCAST_ID; + param.broadcast_create_param = &bcast_create_param; + + ESP_LOGI(TAG, "Handover: unicast -> broadcast (%u stream(s))", (unsigned)count); + + cap_handover_diag_sample("u2b-pre", true); + + err = esp_ble_audio_cap_handover_unicast_to_broadcast(¶m); + if (err) { + ESP_LOGE(TAG, "Failed to hand over to broadcast, err %d", err); + return err; + } + + return 0; +} + +static int handover_broadcast_to_unicast(void) +{ + esp_ble_audio_cap_handover_broadcast_to_unicast_param_t param = {0}; + size_t count = 0; + int err; + + if (recv_state_valid == false) { + ESP_LOGW(TAG, "Acceptor has no receive state for our broadcast"); + return -ENODEV; + } + + /* Must be the broadcast source's own streams. Sink only: a BIS has no return path. */ + for (size_t i = 0; i < peer.sink_ep_count && count < ARRAY_SIZE(uni_stream_params); i++) { + esp_ble_audio_bap_lc3_preset_t *preset = sink_preset_get(i); + + if (preset == NULL || peer.sink_eps[i] == NULL) { + continue; + } + + uni_stream_params[count].qos_cfg = &preset->qos; + uni_stream_params[count].stream = &peer.sink_streams[i]; + + uni_pair_params[count].rx_param = NULL; + uni_pair_params[count].tx_param = &uni_stream_params[count]; + + uni_start_stream_params[count].member.member = peer.conn; + uni_start_stream_params[count].stream = &peer.sink_streams[i]; + uni_start_stream_params[count].ep = peer.sink_eps[i]; + uni_start_stream_params[count].codec_cfg = &preset->codec_cfg; + count++; + } + + if (count == 0) { + ESP_LOGW(TAG, "No endpoint to hand back to unicast"); + return -ENODEV; + } + + uni_group_param.params_count = count; + uni_group_param.params = uni_pair_params; + + uni_start_param.type = ESP_BLE_AUDIO_CAP_SET_TYPE_AD_HOC; + uni_start_param.count = count; + uni_start_param.stream_params = uni_start_stream_params; + + stop_member_param.member.member = peer.conn; + stop_member_param.src_id = recv_state_src_id; + stop_member_param.num_subgroups = bcast_create_param.subgroup_count; + + stop_param.type = ESP_BLE_AUDIO_CAP_SET_TYPE_AD_HOC; + stop_param.param = &stop_member_param; + stop_param.count = 1; + + /* Documented as ignored once reception_stop_param is set, but the procedure + * still matches receive state notifications against this triple. Leaving it + * zero stalls the handover after the broadcast source stops. + */ + param.broadcast_id = LOCAL_BROADCAST_ID; + param.adv_sid = ADV_SID; + param.adv_type = 0; /* public */ + + param.reception_stop_param = &stop_param; + param.broadcast_source = broadcast_source; + param.unicast_group_param = &uni_group_param; + param.unicast_start_param = &uni_start_param; + + ESP_LOGI(TAG, "Handover: broadcast -> unicast (%u stream(s))", (unsigned)count); + + cap_handover_diag_sample("b2u-pre", true); + + err = esp_ble_audio_cap_handover_broadcast_to_unicast(¶m); + if (err) { + ESP_LOGE(TAG, "Failed to hand back to unicast, err %d", err); + return err; + } + + /* Stop feeding the broadcast now, not from the stream callbacks: the ISO + * channel goes down with the BIG terminate, but the disconnected callback + * trails the controller's event by ~100 ms and every SDU in between is + * rejected. Unicast re-registers from its own started callback. The other + * direction does not need this - ASCS delivers CIS callbacks promptly. + */ + for (size_t i = 0; i < ARRAY_SIZE(peer.sink_streams); i++) { + (void)cap_handover_tx_unregister_stream(&peer.sink_streams[i]); + } + + return 0; +} + +static void switch_timer_cb(void *arg) +{ + int err; + + if (proc_active) { + /* CAP runs one procedure at a time; retry on the next tick. */ + switch_timer_arm(); + return; + } + + if (peer.conn == NULL) { + return; + } + + proc_active = true; + + err = on_broadcast ? handover_broadcast_to_unicast() + : handover_unicast_to_broadcast(); + if (err) { + proc_active = false; + switch_timer_arm(); + } +} + +static void unicast_to_broadcast_created_cb(esp_ble_audio_cap_broadcast_source_t *source) +{ + uint8_t *per_data; + uint8_t per_len = 0; + int err; + + /* The source exists now, so publish its BASE before the Acceptor syncs. */ + per_data = broadcast_base_data_get(source, &per_len); + if (per_data == NULL) { + ESP_LOGE(TAG, "Failed to build BASE for handover"); + return; + } + + err = per_adv_data_start(per_data, per_len); + free(per_data); + if (err) { + ESP_LOGE(TAG, "Failed to publish BASE, err %d", err); + } +} + +static void unicast_to_broadcast_complete_cb(int err, esp_ble_conn_t *conn, + esp_ble_audio_cap_unicast_group_t *group, + esp_ble_audio_cap_broadcast_source_t *source) +{ + /* On success the procedure deleted the unicast group. */ + unicast_group_set(group); + broadcast_source = source; + proc_active = false; + + if (err) { + ESP_LOGE(TAG, "Handover to broadcast failed, err %d", err); + + /* The group is already gone, so a failure leaves us broadcasting to + * nobody. The stop callback deletes the source and restores unicast. + */ + if (source != NULL && + esp_ble_audio_cap_initiator_broadcast_audio_stop(source) != 0) { + ESP_LOGE(TAG, "Failed to stop broadcast source"); + } + } else { + on_broadcast = true; + + ESP_LOGI(TAG, "Handover to broadcast completed"); + + /* BIG terms are readable now; its timestamp is not, until the first + * SDU. The next b2u-pre sample catches that side with real data. + */ + cap_handover_diag_sample("u2b-post", false); + } + + switch_timer_arm(); +} + +static void broadcast_to_unicast_complete_cb(int err, esp_ble_conn_t *conn, + esp_ble_audio_cap_broadcast_source_t *source, + esp_ble_audio_cap_unicast_group_t *group) +{ + /* On success the procedure deleted the broadcast source. */ + unicast_group_set(group); + broadcast_source = source; + proc_active = false; + + if (err) { + ESP_LOGE(TAG, "Handover to unicast failed, err %d", err); + } else { + on_broadcast = false; + + /* Nothing left to announce: drop the announcement and the periodic + * advertising carrying its BASE before declaring the handover done. + */ + if (adv_stop() != 0) { + ESP_LOGW(TAG, "Failed to stop advertising after handover"); + } + + ESP_LOGI(TAG, "Handover to unicast completed"); + + /* Mirror of the above: CIS terms readable, timestamp not yet. */ + cap_handover_diag_sample("b2u-post", false); + } + + switch_timer_arm(); +} + +static esp_ble_audio_cap_handover_cb_t handover_cb = { + .unicast_to_broadcast_created = unicast_to_broadcast_created_cb, + .unicast_to_broadcast_complete = unicast_to_broadcast_complete_cb, + .broadcast_to_unicast_complete = broadcast_to_unicast_complete_cb, +}; + +/* Discovery only subscribes, so read the receive states once to see what the + * Acceptor already holds. They outlive our reboot, and BAP 6.5.4 rejects an Add + * Source duplicating the {address, SID, Broadcast_ID} triple - an entry left by + * an earlier run of ours would fail every handover. + */ +static void recv_state_sweep_next(void) +{ + while (recv_state_next < recv_state_cnt) { + uint8_t idx = recv_state_next++; + int err; + + err = esp_ble_audio_bap_broadcast_assistant_read_recv_state(peer.conn_handle, idx); + if (err == 0) { + return; /* Continues in assistant_recv_state_cb */ + } + + ESP_LOGW(TAG, "Failed to read receive state %u, err %d", idx, err); + } + + recv_state_sweeping = false; + + unicast_start_defer(); +} + +static void assistant_discover_cb(esp_ble_conn_t *conn, int err, uint8_t recv_state_count) +{ + if (err) { + ESP_LOGE(TAG, "BASS discovery failed, err %d", err); + return; + } + + ESP_LOGI(TAG, "BASS discovered (%u receive state(s))", recv_state_count); + + recv_state_cnt = recv_state_count; + recv_state_next = 0; + recv_state_sweeping = true; + + recv_state_sweep_next(); +} + +static void assistant_recv_state_cb(esp_ble_conn_t *conn, int err, + const esp_ble_audio_bap_scan_delegator_recv_state_t *state) +{ + bool ours = (err == 0 && state != NULL && state->broadcast_id == LOCAL_BROADCAST_ID); + uint32_t bis_sync = 0; + + if (recv_state_sweeping) { + if (ours) { + ESP_LOGW(TAG, "Removing leftover receive state (src_id %u)", state->src_id); + + if (esp_ble_audio_bap_broadcast_assistant_rem_src(peer.conn_handle, + state->src_id) == 0) { + return; /* Continues in assistant_rem_src_cb */ + } + + ESP_LOGE(TAG, "Failed to remove leftover receive state"); + } + + recv_state_sweep_next(); + return; + } + + if (!ours) { + return; + } + + recv_state_src_id = state->src_id; + recv_state_valid = true; + + /* The Acceptor notifies per field change, so the same pa_sync arrives more + * than once (PA synced, then BIG synced). bis_sync tells them apart, and is + * also what says whether audio is flowing. + */ + for (uint8_t i = 0; i < state->num_subgroups; i++) { + bis_sync |= state->subgroups[i].bis_sync; + } + + ESP_LOGI(TAG, "Acceptor receive state: src_id %u pa_sync %u bis_sync 0x%08lx", + state->src_id, state->pa_sync_state, (unsigned long)bis_sync); + + /* The Acceptor asks for the periodic advertising train it was told about. + * We are the advertiser, so hand it over instead of making it scan. + */ + if (state->pa_sync_state == ESP_BLE_AUDIO_BAP_PA_STATE_INFO_REQ) { + /* The Source ID goes in the high octet of the service data; the Acceptor + * reads it from there to match the transfer to a receive state. */ + err = pa_set_info_transfer(peer.conn_handle, peer.dst, + (uint16_t)state->src_id << 8); + if (err) { + ESP_LOGE(TAG, "Failed to transfer PA sync info, err %d", err); + } else { + ESP_LOGI(TAG, "PA sync info transferred (src_id %u)", state->src_id); + } + } +} + +static void assistant_recv_state_removed_cb(esp_ble_conn_t *conn, uint8_t src_id) +{ + if (recv_state_valid && src_id == recv_state_src_id) { + recv_state_valid = false; + } +} + +static void assistant_rem_src_cb(esp_ble_conn_t *conn, int err) +{ + if (err) { + ESP_LOGE(TAG, "Remove source failed, err %d", err); + } + + if (recv_state_sweeping) { + recv_state_sweep_next(); + } +} + +static esp_ble_audio_bap_broadcast_assistant_cb_t assistant_cb = { + .discover = assistant_discover_cb, + .recv_state = assistant_recv_state_cb, + .recv_state_removed = assistant_recv_state_removed_cb, + .rem_src = assistant_rem_src_cb, +}; + +int cap_handover_proc_discover(uint16_t conn_handle) +{ + int err; + + err = esp_ble_audio_bap_broadcast_assistant_discover(conn_handle); + if (err) { + ESP_LOGE(TAG, "Failed to discover BASS, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Discovering BASS"); + return 0; +} + +void cap_handover_proc_unicast_started(void) +{ + if (on_broadcast) { + return; + } + + switch_timer_arm(); +} + +void cap_handover_proc_broadcast_stopped(esp_ble_audio_cap_broadcast_source_t *source) +{ + if (source != broadcast_source) { + return; + } + + /* A source is only deletable once its BIG is down, so finish it here. */ + if (esp_ble_audio_cap_initiator_broadcast_audio_delete(source) != 0) { + ESP_LOGE(TAG, "Failed to delete broadcast source"); + return; + } + + broadcast_source = NULL; + on_broadcast = false; + + (void)adv_stop(); + + /* A failed handover left the ASEs released; restore unicast while connected. */ + if (peer.conn != NULL) { + unicast_start_defer(); + } +} + +void cap_handover_proc_reset(void) +{ + if (switch_timer != NULL) { + (void)esp_timer_stop(switch_timer); + } + + if (start_timer != NULL) { + (void)esp_timer_stop(start_timer); + } + + if (broadcast_source != NULL) { + /* Deleted by cap_handover_proc_broadcast_stopped once the BIG is down. */ + if (esp_ble_audio_cap_initiator_broadcast_audio_stop(broadcast_source) != 0) { + ESP_LOGE(TAG, "Failed to stop broadcast source"); + } + } + + proc_active = false; + on_broadcast = false; + recv_state_valid = false; + recv_state_sweeping = false; +} + +int cap_handover_proc_init(void) +{ + const esp_timer_create_args_t timer_args = { + .callback = switch_timer_cb, + .name = "cap_handover", + }; + const esp_timer_create_args_t start_timer_args = { + .callback = start_timer_cb, + .name = "cap_uni_start", + }; + int err; + + err = esp_ble_audio_cap_handover_register_cb(&handover_cb); + if (err) { + ESP_LOGE(TAG, "Failed to register handover callbacks, err %d", err); + return err; + } + + err = esp_ble_audio_bap_broadcast_assistant_register_cb(&assistant_cb); + if (err) { + ESP_LOGE(TAG, "Failed to register broadcast assistant callbacks, err %d", err); + return err; + } + + err = esp_timer_create(&timer_args, &switch_timer); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to create handover timer, err %d", err); + return err; + } + + err = esp_timer_create(&start_timer_args, &start_timer); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to create unicast start timer, err %d", err); + return err; + } + + ESP_LOGI(TAG, "CAP initiator handover initialized (period %u s)", + (unsigned)CONFIG_EXAMPLE_HANDOVER_PERIOD_S); + + return 0; +} diff --git a/examples/bluetooth/esp_ble_audio/cap/handover/main/cap_handover_tx.c b/examples/bluetooth/esp_ble_audio/cap/handover/main/cap_handover_tx.c new file mode 100644 index 00000000000..0d4d4793528 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/handover/main/cap_handover_tx.c @@ -0,0 +1,225 @@ +/* + * SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +#include "cap_handover.h" + +/* The same stream objects move between unicast and broadcast, so one slot each. */ +static struct tx_stream tx_streams[SINK_STREAM_COUNT]; + +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) { + 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 (tx_stream == NULL || tx_stream->stream == NULL) { + return false; + } + + /* 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(tx_stream->stream->bap_stream.ep, &ep_info); + if (err) { + return false; + } + + return (ep_info.state == ESP_BLE_AUDIO_BAP_EP_STATE_STREAMING); +} + +static void cap_handover_tx_send(struct tx_stream *tx_stream) +{ + const char *label; + int err; + + if (tx_stream == NULL || tx_stream->stream == NULL) { + return; + } + + 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, "[%s] Invalid QoS", label); + return; + } + + if (tx_stream->data == NULL) { + ESP_LOGE(TAG, "[%s] Buffer unavailable (SDU %u)", + label, tx_stream->stream->bap_stream.qos->sdu); + return; + } + + memset(tx_stream->data, (uint8_t)tx_stream->seq_num, tx_stream->stream->bap_stream.qos->sdu); + + err = esp_ble_audio_cap_stream_send(tx_stream->stream, tx_stream->data, + tx_stream->stream->bap_stream.qos->sdu, + tx_stream->seq_num); + if (err) { + ESP_LOGD(TAG, "[%s] send failed, err %d", label, err); + return; + } + + tx_stream->seq_num++; +} + +void cap_handover_tx_stream_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 && &tx_streams[i].stream->bap_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; + } + } +} + +static void tx_scheduler_cb(void *arg) +{ + struct tx_stream *tx_stream = arg; + + cap_handover_tx_send(tx_stream); +} + +int cap_handover_tx_register_stream(esp_ble_audio_cap_stream_t *cap_stream, bool is_broadcast) +{ + int err; + + if (cap_stream == NULL) { + return -EINVAL; + } + + 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, "[%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, "[%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, "[%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_handover_tx_send(&tx_streams[i]); + return 0; + } + } + + ESP_LOGE(TAG, "No free TX stream slot"); + + return -ENOMEM; +} + +int cap_handover_tx_unregister_stream(esp_ble_audio_cap_stream_t *cap_stream) +{ + int err; + + if (cap_stream == NULL) { + return -EINVAL; + } + + 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, "[%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; + } + } + + return -ENODATA; +} + +void cap_handover_tx_init(void) +{ + int err; + + memset(tx_streams, 0, sizeof(tx_streams)); + + for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) { + err = example_audio_tx_scheduler_init(&tx_streams[i].scheduler, + tx_scheduler_cb, + &tx_streams[i]); + if (err) { + ESP_LOGE(TAG, "Failed to initialize tx scheduler[%zu], err %d", i, err); + return; + } + } +} diff --git a/examples/bluetooth/esp_ble_audio/cap/handover/main/cap_handover_unicast.c b/examples/bluetooth/esp_ble_audio/cap/handover/main/cap_handover_unicast.c new file mode 100644 index 00000000000..02b3cebf2f9 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/handover/main/cap_handover_unicast.c @@ -0,0 +1,693 @@ +/* + * SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "cap_handover.h" + +/* One preset per sink stream: each owns its codec cfg buffer, which is what + * lets the streams carry different channel allocations. + */ +HANDOVER_LC3_PRESET_DEFINE(unicast_preset_left, ESP_BLE_AUDIO_LOCATION_FRONT_LEFT); +HANDOVER_LC3_PRESET_DEFINE(unicast_preset_right, ESP_BLE_AUDIO_LOCATION_FRONT_RIGHT); +static esp_ble_audio_bap_lc3_preset_t *const sink_presets[] = { + &unicast_preset_left, + &unicast_preset_right, +}; + +_Static_assert(ARRAY_SIZE(sink_presets) >= SINK_STREAM_COUNT, + "Need one preset (one channel allocation) per sink stream"); + +static esp_ble_audio_cap_unicast_group_t *unicast_group; + +struct peer_config peer = { + .conn_handle = CONN_HANDLE_INIT, +}; + +esp_ble_audio_cap_unicast_group_t *unicast_group_get(void) +{ + return unicast_group; +} + +void unicast_group_set(esp_ble_audio_cap_unicast_group_t *group) +{ + unicast_group = group; +} + +esp_ble_audio_bap_lc3_preset_t *sink_preset_get(size_t idx) +{ + return (idx < ARRAY_SIZE(sink_presets)) ? sink_presets[idx] : NULL; +} + +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) +{ + for (size_t i = 0; i < ARRAY_SIZE(peer.sink_streams); i++) { + if (stream == &peer.sink_streams[i].bap_stream) { + return "SNK"; + } + } + + return "???"; +} + +static int stream_index(const esp_ble_audio_bap_stream_t *stream) +{ + /* Index within its own direction's pool, so logs read "SNK #0" / "SNK #1". */ + for (size_t i = 0; i < ARRAY_SIZE(peer.sink_streams); i++) { + if (stream == &peer.sink_streams[i].bap_stream) { + return (int)i; + } + } + + return -1; +} + +/* Ask whether the endpoint transmits; do not derive it from dir. These streams + * migrate, and dir means opposite things on the two endpoint kinds: the peer's + * direction on a unicast ASE (SINK), our own on a broadcast source (SOURCE). + * Testing dir == SINK therefore goes silent the moment a stream moves to a BIS. + */ +static bool is_tx_stream(esp_ble_audio_bap_stream_t *stream) +{ + esp_ble_audio_bap_ep_info_t ep_info = {0}; + esp_err_t err; + + /* Detached already: only teardown sees this, and unregistering is a no-op. */ + if (stream->ep == NULL) { + return true; + } + + err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info); + if (err) { + ESP_LOGE(TAG, "Failed to get ep info, err %d", err); + return false; + } + + return ep_info.can_send; +} + +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, "[%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, "[%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, "[%s #%d] Stream enabled", + stream_dir_str(stream), stream_index(stream)); +} + +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, "[%s #%d] Stream started", + stream_dir_str(stream), stream_index(stream)); + + if (is_tx_stream(stream)) { + cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream); + + err = cap_handover_tx_register_stream(cap_stream, false); + if (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, "[%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, "[%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, "[%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); + + (void)cap_handover_tx_unregister_stream(cap_stream); + } +} + +static void unicast_stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason) +{ + esp_ble_audio_cap_stream_t *cap_stream; + + 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); + + (void)cap_handover_tx_unregister_stream(cap_stream); + } +} + +static void unicast_stream_released_cb(esp_ble_audio_bap_stream_t *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) +{ + cap_handover_tx_stream_sent(stream, user_data); +} + +static esp_ble_audio_bap_stream_ops_t unicast_stream_ops = { + .configured = unicast_stream_configured_cb, + .qos_set = unicast_stream_qos_set_cb, + .enabled = unicast_stream_enabled_cb, + .started = unicast_stream_started_cb, + .metadata_updated = unicast_stream_metadata_updated_cb, + .disabled = unicast_stream_disabled_cb, + .stopped = unicast_stream_stopped_cb, + .released = unicast_stream_released_cb, + .sent = unicast_stream_sent_cb, + .disconnected = unicast_stream_disconnected_cb, +}; + +static int discover_cas(void) +{ + int err; + + err = esp_ble_audio_cap_initiator_unicast_discover(peer.conn_handle); + if (err) { + ESP_LOGE(TAG, "Failed to discover CAS, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Discovering CAS"); + + return 0; +} + +static int discover_sinks(void) +{ + int err; + + for (size_t i = 0; i < ARRAY_SIZE(peer.sink_streams); i++) { + esp_ble_audio_cap_stream_ops_register(&peer.sink_streams[i], &unicast_stream_ops); + } + + err = esp_ble_audio_bap_unicast_client_discover(peer.conn_handle, ESP_BLE_AUDIO_DIR_SINK); + if (err) { + ESP_LOGE(TAG, "Failed to discover sinks, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Discovering sinks"); + + return 0; +} + +static int unicast_group_create(void) +{ + /* Referenced by the group while it exists, so they outlive this call. */ + static esp_ble_audio_cap_unicast_group_stream_param_t sink_stream_params[SINK_STREAM_COUNT]; + static esp_ble_audio_cap_unicast_group_stream_pair_param_t pair_params[SINK_STREAM_COUNT]; + esp_ble_audio_cap_unicast_group_param_t group_param = {0}; + size_t pair_count = 0; + int err; + + /* One CIS per sink stream. */ + for (size_t i = 0; i < peer.sink_ep_count; i++) { + sink_stream_params[i].qos_cfg = &sink_presets[i]->qos; + sink_stream_params[i].stream = &peer.sink_streams[i]; + + pair_params[pair_count].rx_param = NULL; + pair_params[pair_count].tx_param = &sink_stream_params[i]; + pair_count++; + } + + if (pair_count == 0) { + ESP_LOGW(TAG, "No endpoints available, skip creating unicast group"); + return -ENODEV; + } + + group_param.params_count = pair_count; + group_param.params = pair_params; + + err = esp_ble_audio_cap_unicast_group_create(&group_param, &unicast_group); + if (err) { + ESP_LOGE(TAG, "Failed to create unicast group, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Created unicast group"); + + return 0; +} + +int unicast_group_delete(void) +{ + int err; + + if (unicast_group == NULL) { + return 0; + } + + err = esp_ble_audio_cap_unicast_group_delete(unicast_group); + if (err) { + ESP_LOGE(TAG, "Failed to delete unicast group, err %d", err); + return err; + } + + unicast_group = NULL; + + ESP_LOGI(TAG, "Deleted unicast group"); + + return 0; +} + +static int unicast_audio_start(void) +{ + /* codec_cfg has to stay valid while the stream is non-idle. */ + static esp_ble_audio_cap_unicast_audio_start_stream_param_t stream_param[SINK_STREAM_COUNT]; + esp_ble_audio_cap_unicast_audio_start_param_t param = {0}; + int err; + + for (size_t i = 0; i < peer.sink_ep_count; i++) { + stream_param[param.count].member.member = peer.conn; + stream_param[param.count].stream = &peer.sink_streams[i]; + stream_param[param.count].ep = peer.sink_eps[i]; + stream_param[param.count].codec_cfg = &sink_presets[i]->codec_cfg; + param.count++; + } + + if (param.count == 0) { + ESP_LOGW(TAG, "No endpoints available, skip starting unicast audio"); + return 0; + } + + param.type = ESP_BLE_AUDIO_CAP_SET_TYPE_AD_HOC; + param.stream_params = stream_param; + + err = esp_ble_audio_cap_initiator_unicast_audio_start(¶m); + if (err) { + ESP_LOGE(TAG, "Failed to start unicast audio, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Starting unicast streams"); + + return 0; +} + +int cap_handover_unicast_setup_and_start(void) +{ + int err; + + err = unicast_group_create(); + if (err) { + return err; + } + + err = unicast_audio_start(); + if (err) { + unicast_group_delete(); + return err; + } + + return 0; +} + +static void discover_cb(esp_ble_conn_t *conn, int err, esp_ble_audio_dir_t dir) +{ + if (conn->handle != peer.conn_handle) { + return; + } + + peer.conn = conn; + + /* Sink only: a broadcast Audio Stream has no return path, so the Acceptor's + * source ASEs take no part in a handover. + */ + if (dir != ESP_BLE_AUDIO_DIR_SINK) { + return; + } + + if (err) { + ESP_LOGE(TAG, "Discovery sinks failed, err %d", err); + return; + } + + ESP_LOGI(TAG, "Discover sinks complete"); + + /* The collocated Commander writes the broadcast source into the Acceptor's + * BASS, so discover that too. Its callback continues the unicast setup. + */ + (void)cap_handover_proc_discover(peer.conn_handle); +} + +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(TAG, codec_cap); +} + +static void endpoint_cb(esp_ble_conn_t *conn, + esp_ble_audio_dir_t dir, + esp_ble_audio_bap_ep_t *ep) +{ + /* Keep every sink endpoint: a stereo Acceptor exposes one per channel and + * they are handed over together. + */ + if (dir == ESP_BLE_AUDIO_DIR_SINK && + peer.sink_ep_count < ARRAY_SIZE(peer.sink_eps)) { + ESP_LOGI(TAG, "[%s #%zu] Endpoint discovered", dir_str(dir), peer.sink_ep_count); + peer.sink_eps[peer.sink_ep_count++] = ep; + } +} + +static esp_ble_audio_bap_unicast_client_cb_t unicast_client_cbs = { + .discover = discover_cb, + .pac_record = pac_record_cb, + .endpoint = endpoint_cb, +}; + +static void unicast_discovery_complete_cb(esp_ble_conn_t *conn, int err, + const esp_ble_audio_csip_set_coordinator_set_member_t *member, + const esp_ble_audio_csip_set_coordinator_csis_inst_t *csis_inst) +{ + if (err) { + ESP_LOGE(TAG, "Unicast discovery completed, err %d", err); + return; + } + + if (IS_ENABLED(CONFIG_BT_CAP_ACCEPTOR_SET_MEMBER)) { + if (csis_inst == NULL) { + ESP_LOGW(TAG, "Failed to discover CAS CSIS"); + return; + } + + ESP_LOGI(TAG, "Found CAS with CSIS"); + + /* TODO: Do set member discovery */ + } else { + ESP_LOGI(TAG, "Found CAS"); + } + + (void)discover_sinks(); +} + +static void unicast_start_complete_cb(int err, esp_ble_conn_t *conn) +{ + if (err) { + ESP_LOGE(TAG, "Unicast start completed, err %d", err); + return; + } + + ESP_LOGI(TAG, "Unicast start completed"); + + cap_handover_proc_unicast_started(); +} + +static void broadcast_stopped_cb(esp_ble_audio_cap_broadcast_source_t *source, uint8_t reason) +{ + ESP_LOGI(TAG, "Broadcast source stopped, reason 0x%02x", reason); + + cap_handover_proc_broadcast_stopped(source); +} + +static esp_ble_audio_cap_initiator_cb_t cap_cb = { + .unicast_discovery_complete = unicast_discovery_complete_cb, + .unicast_start_complete = unicast_start_complete_cb, + .broadcast_stopped = broadcast_stopped_cb, +}; + +static bool check_and_connect(uint8_t type, const uint8_t *data, + uint8_t data_len, void *user_data) +{ + esp_ble_audio_gap_app_event_t *event; + uint16_t uuid_val; + int err; + + event = user_data; + assert(event); + + if (type != EXAMPLE_AD_TYPE_SERVICE_DATA16) { + return true; /* Continue parsing to next AD data type */ + } + + if (data_len < sizeof(uuid_val)) { + ESP_LOGW(TAG, "Invalid ad size %u (cas uuid)", data_len); + return true; /* Continue parsing to next AD data type */ + } + + uuid_val = sys_get_le16(data); + + if (uuid_val != ESP_BLE_AUDIO_UUID_CAS_VAL) { + /* We are looking for the TMAS service data */ + return true; /* Continue parsing to next AD data type */ + } + + ESP_LOGI(TAG, "Found CAS in peer adv data!"); + + /* Stop scanning before connect — NimBLE rejects ble_gap_connect while + * a discovery procedure is running. On failure restart scanning so we + * don't stall in no-scan no-conn state. */ + err = ext_scan_stop(); + if (err) { + ESP_LOGE(TAG, "Failed to stop scanning, err %d", err); + return false; + } + + err = conn_create(event->ext_scan_recv.addr.type, + event->ext_scan_recv.addr.val); + if (err) { + ESP_LOGE(TAG, "Failed to create conn, err %d", err); + cap_handover_unicast_start(); + } + + return false; /* Stop parsing */ +} + +static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event) +{ + if (peer.conn_handle != CONN_HANDLE_INIT) { + return; + } + + /* Check if the advertising is connectable and if TMAS is supported */ + if (event->ext_scan_recv.event_type & EXAMPLE_ADV_PROP_CONNECTABLE) { + esp_ble_audio_data_parse(event->ext_scan_recv.data, + event->ext_scan_recv.data_len, + check_and_connect, (void *)event); + } +} + +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); + /* Scanning was stopped before conn_create and acl_disconnect only fires on + * an established connection, so resume here or nothing runs. */ + cap_handover_unicast_start(); + return; + } + + ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x", + event->acl_connect.conn_handle, event->acl_connect.role, + EXAMPLE_BT_ADDR_PRINT_ARGS(event->acl_connect.dst.val)); + + peer.conn_handle = event->acl_connect.conn_handle; + memcpy(peer.dst, event->acl_connect.dst.val, sizeof(peer.dst)); + + err = pairing_start(event->acl_connect.conn_handle); + if (err) { + ESP_LOGE(TAG, "Failed to initiate security, err %d", err); + return; + } +} + +static void acl_disconnect(esp_ble_audio_gap_app_event_t *event) +{ + ESP_LOGI(TAG, "Disconnected: handle %u reason 0x%02x", + event->acl_disconnect.conn_handle, event->acl_disconnect.reason); + + peer.conn_handle = CONN_HANDLE_INIT; + peer.conn = NULL; + memset(peer.dst, 0, sizeof(peer.dst)); + memset(peer.sink_eps, 0, sizeof(peer.sink_eps)); + peer.sink_ep_count = 0; + peer.disc_completed = false; + peer.mtu_exchanged = false; + + /* Drop a broadcast source left running before reusing the stream objects. */ + cap_handover_proc_reset(); + + unicast_group_delete(); + + cap_handover_unicast_start(); +} + +static void security_change(esp_ble_audio_gap_app_event_t *event) +{ + int err; + + if (event->security_change.status) { + security_failed_recover(event->security_change.conn_handle, + event->security_change.status); + return; + } + + ESP_LOGI(TAG, "Security: handle %u level %u bonded %u", + event->security_change.conn_handle, event->security_change.sec_level, + event->security_change.bonded); + + err = exchange_mtu(event->security_change.conn_handle); + if (err) { + ESP_LOGE(TAG, "Failed to exchange MTU, err %d", err); + return; + } +} + +void cap_handover_unicast_gap_cb(esp_ble_audio_gap_app_event_t *event) +{ + switch (event->type) { + case ESP_BLE_AUDIO_GAP_EVENT_EXT_SCAN_RECV: + ext_scan_recv(event); + break; + case ESP_BLE_AUDIO_GAP_EVENT_ACL_CONNECT: + acl_connect(event); + break; + case ESP_BLE_AUDIO_GAP_EVENT_ACL_DISCONNECT: + acl_disconnect(event); + break; + case ESP_BLE_AUDIO_GAP_EVENT_SECURITY_CHANGE: + security_change(event); + break; + default: + break; + } +} + +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, "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", + event->gatt_mtu_change.mtu, ESP_BLE_AUDIO_ATT_MTU_MIN); + return; + } + + 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, "Service discovery started: handle %u", conn_handle); + + /* Note: + * MTU exchanged event may arrived after discover completed event. + */ + peer.mtu_exchanged = true; + + if (peer.disc_completed) { + (void)discover_cas(); + } +} + +static void gattc_disc_cmpl(esp_ble_audio_gatt_app_event_t *event) +{ + 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); + return; + } + + /* Note: + * Discover completed event may arrived before MTU exchanged event. + */ + peer.disc_completed = true; + + if (peer.mtu_exchanged) { + (void)discover_cas(); + } +} + +void cap_handover_unicast_gatt_cb(esp_ble_audio_gatt_app_event_t *event) +{ + switch (event->type) { + case ESP_BLE_AUDIO_GATT_EVENT_GATT_MTU_CHANGE: + gatt_mtu_change(event); + break; + case ESP_BLE_AUDIO_GATT_EVENT_GATTC_DISC_CMPL: + gattc_disc_cmpl(event); + break; + default: + break; + } +} + +int cap_handover_unicast_start(void) +{ + return ext_scan_start(); +} + +int cap_handover_unicast_init(void) +{ + int err; + + err = esp_ble_audio_cap_initiator_register_cb(&cap_cb); + if (err) { + ESP_LOGE(TAG, "Failed to register CAP callbacks, err %d", err); + return err; + } + + err = esp_ble_audio_bap_unicast_client_register_cb(&unicast_client_cbs); + if (err) { + ESP_LOGE(TAG, "Failed to register BAP unicast client callbacks, err %d", err); + return err; + } + + ESP_LOGI(TAG, "CAP initiator unicast initialized"); + + return 0; +} diff --git a/examples/bluetooth/esp_ble_audio/cap/handover/main/idf_component.yml b/examples/bluetooth/esp_ble_audio/cap/handover/main/idf_component.yml new file mode 100644 index 00000000000..bb02c0922aa --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/handover/main/idf_component.yml @@ -0,0 +1,5 @@ +dependencies: + example_init: + path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_init + example_utils: + path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_utils diff --git a/examples/bluetooth/esp_ble_audio/cap/handover/main/main.c b/examples/bluetooth/esp_ble_audio/cap/handover/main/main.c new file mode 100644 index 00000000000..96430ade8b3 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/handover/main/main.c @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA + * SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "nvs_flash.h" + +#include "cap_handover.h" + +void app_main(void) +{ + esp_ble_audio_init_info_t info = { + .gap_cb = cap_handover_unicast_gap_cb, + .gatt_cb = cap_handover_unicast_gatt_cb, + }; + esp_err_t err; + + /* Initialize NVS — it is used to store PHY calibration data */ + err = nvs_flash_init(); + if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + err = nvs_flash_init(); + } + ESP_ERROR_CHECK(err); + + err = bluetooth_init(); + if (err) { + ESP_LOGE(TAG, "Failed to initialize BLE, err %d", err); + return; + } + + err = app_host_init(); + if (err) { + ESP_LOGE(TAG, "Failed to init host, err %d", err); + return; + } + + err = esp_ble_audio_common_init(&info); + if (err) { + ESP_LOGE(TAG, "Failed to initialize audio, err %d", err); + return; + } + + err = cap_handover_unicast_init(); + if (err) { + return; + } + + err = cap_handover_proc_init(); + if (err) { + return; + } + + cap_handover_tx_init(); + + err = esp_ble_audio_common_start(NULL); + if (err) { + ESP_LOGE(TAG, "Failed to start audio, err %d", err); + return; + } + + err = set_device_name(); + if (err) { + ESP_LOGE(TAG, "Failed to set device name, err %d", err); + return; + } + + /* Scanning only: the broadcast source is created by the handover procedure. */ + err = cap_handover_unicast_start(); + if (err) { + return; + } +} diff --git a/examples/bluetooth/esp_ble_audio/cap/handover/main/nimble/adv.c b/examples/bluetooth/esp_ble_audio/cap/handover/main/nimble/adv.c new file mode 100644 index 00000000000..7c594dd865d --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/handover/main/nimble/adv.c @@ -0,0 +1,170 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "esp_log.h" + +#include "host/ble_gap.h" +#include "host/ble_hs.h" + +#include "os/os_mbuf.h" + +#include "cap_handover.h" + +static int gap_event_cb(struct ble_gap_event *event, void *arg) +{ + return 0; +} + +static int adv_data_set(const uint8_t *payload, uint8_t payload_len, bool periodic) +{ + struct os_mbuf *data; + int err; + + data = os_msys_get_pkthdr(payload_len, 0); + if (data == NULL) { + ESP_LOGE(TAG, "Failed to get %s adv mbuf", periodic ? "per" : "ext"); + return -1; + } + + err = os_mbuf_append(data, payload, payload_len); + if (err) { + ESP_LOGE(TAG, "Failed to append %s adv data, err %d", periodic ? "per" : "ext", err); + os_mbuf_free_chain(data); + return err; + } + + err = periodic ? ble_gap_periodic_adv_set_data(ADV_HANDLE, data) + : ble_gap_ext_adv_set_data(ADV_HANDLE, data); + if (err) { + ESP_LOGE(TAG, "Failed to set %s adv data, err %d", periodic ? "per" : "ext", err); + return err; + } + + return 0; +} + +static int adv_set_configure(const uint8_t *ext_data, uint8_t ext_len) +{ + struct ble_gap_periodic_adv_params per_params = {0}; + struct ble_gap_ext_adv_params ext_params = {0}; + int err; + + ext_params.connectable = 0; + ext_params.scannable = 0; + ext_params.legacy_pdu = 0; + ext_params.own_addr_type = BLE_OWN_ADDR_PUBLIC; + ext_params.primary_phy = BLE_HCI_LE_PHY_1M; + ext_params.secondary_phy = BLE_HCI_LE_PHY_2M; + ext_params.tx_power = ADV_TX_POWER; + ext_params.sid = ADV_SID; + ext_params.itvl_min = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS); + ext_params.itvl_max = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS); + + err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL, + gap_event_cb, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err); + return err; + } + + err = adv_data_set(ext_data, ext_len, false); + if (err) { + return err; + } + + per_params.include_tx_power = 0; + per_params.itvl_min = BLE_GAP_PERIODIC_ITVL_MS(PER_ADV_INTERVAL_MS); + per_params.itvl_max = BLE_GAP_PERIODIC_ITVL_MS(PER_ADV_INTERVAL_MS); + + err = ble_gap_periodic_adv_configure(ADV_HANDLE, &per_params); + if (err) { + ESP_LOGE(TAG, "Failed to configure per adv params, err %d", err); + return err; + } + + return 0; +} + +int per_adv_data_start(const uint8_t *per_data, uint8_t per_len) +{ + int err; + + err = adv_data_set(per_data, per_len, true); + if (err) { + return err; + } + + err = ble_gap_periodic_adv_start(ADV_HANDLE); + if (err) { + ESP_LOGE(TAG, "Failed to start per advertising, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Periodic advertising started (handle %u)", ADV_HANDLE); + return 0; +} + + +int ext_adv_start_without_base(const uint8_t *ext_data, uint8_t ext_len) +{ + int err; + + err = adv_set_configure(ext_data, ext_len); + if (err) { + return err; + } + + err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0); + if (err) { + ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Advertising started, BASE pending (handle %u)", ADV_HANDLE); + return 0; +} + +int adv_stop(void) +{ + int err; + + err = ble_gap_periodic_adv_stop(ADV_HANDLE); + if (err) { + ESP_LOGE(TAG, "Failed to stop per advertising, err %d", err); + return err; + } + + err = ble_gap_ext_adv_stop(ADV_HANDLE); + if (err) { + ESP_LOGE(TAG, "Failed to stop ext advertising, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Advertising stopped (handle %u)", ADV_HANDLE); + return 0; +} + +int local_public_addr_get(uint8_t addr[6]) +{ + int err = ble_hs_id_copy_addr(BLE_ADDR_PUBLIC, addr, NULL); + + if (err) { + ESP_LOGE(TAG, "Local BD address unavailable, err %d", err); + return err; + } + + return 0; +} + +int pa_set_info_transfer(uint16_t conn_handle, const uint8_t peer_addr[6], + uint16_t service_data) +{ + (void)peer_addr; + + return ble_gap_periodic_adv_sync_set_info(ADV_HANDLE, conn_handle, service_data); +} diff --git a/examples/bluetooth/esp_ble_audio/cap/handover/main/nimble/central.c b/examples/bluetooth/esp_ble_audio/cap/handover/main/nimble/central.c new file mode 100644 index 00000000000..ca5fa4a640f --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/handover/main/nimble/central.c @@ -0,0 +1,136 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "esp_log.h" + +#include "host/ble_gap.h" +#include "host/ble_hs.h" +#include "host/ble_store.h" +#include "services/gap/ble_svc_gap.h" + +#include "esp_ble_audio_common_api.h" + +#include "cap_handover.h" + +/* Init/conn parameters are shared with the bluedroid wrapper via cap_handover.h. + * CONN_DURATION is NimBLE-specific (ble_gap_connect's discovery timeout). */ +#define CONN_DURATION 10000 /* 10s */ + +static int gap_event_cb(struct ble_gap_event *event, void *arg) +{ + switch (event->type) { + case BLE_GAP_EVENT_EXT_DISC: + case BLE_GAP_EVENT_CONNECT: + case BLE_GAP_EVENT_DISCONNECT: + case BLE_GAP_EVENT_ENC_CHANGE: + esp_ble_audio_gap_app_post_event(event->type, event); + break; + case BLE_GAP_EVENT_MTU: + case BLE_GAP_EVENT_NOTIFY_RX: + case BLE_GAP_EVENT_NOTIFY_TX: + case BLE_GAP_EVENT_SUBSCRIBE: + esp_ble_audio_gatt_app_post_event(event->type, event); + break; + case BLE_GAP_EVENT_REPEAT_PAIRING: { + struct ble_gap_conn_desc desc = {0}; + int rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc); + if (rc == 0) { + ble_store_util_delete_peer(&desc.peer_id_addr); + } + return BLE_GAP_REPEAT_PAIRING_RETRY; + } + default: + break; + } + + return 0; +} + +int app_host_init(void) +{ + return 0; +} + +int set_device_name(void) +{ + return ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME); +} + +#define OWN_ADDR_TYPE BLE_OWN_ADDR_PUBLIC + +int ext_scan_start(void) +{ + struct ble_gap_disc_params params = {0}; + int err; + + params.passive = 1; + params.itvl = SCAN_INTERVAL; + params.window = SCAN_WINDOW; + + err = ble_gap_disc(OWN_ADDR_TYPE, BLE_HS_FOREVER, ¶ms, + gap_event_cb, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to start scanning, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Scanning for CAP Acceptor..."); + return 0; +} + +int ext_scan_stop(void) +{ + return ble_gap_disc_cancel(); +} + +int conn_create(uint8_t addr_type, const uint8_t addr[6]) +{ + struct ble_gap_conn_params params = {0}; + ble_addr_t dst = {0}; + + params.scan_itvl = INIT_SCAN_INTERVAL; + params.scan_window = INIT_SCAN_WINDOW; + params.itvl_min = CONN_INTERVAL; + params.itvl_max = CONN_INTERVAL; + params.latency = CONN_LATENCY; + params.supervision_timeout = CONN_TIMEOUT; + params.max_ce_len = CONN_MAX_CE_LEN; + params.min_ce_len = CONN_MIN_CE_LEN; + + dst.type = addr_type; + memcpy(dst.val, addr, sizeof(dst.val)); + + return ble_gap_connect(OWN_ADDR_TYPE, &dst, CONN_DURATION, + ¶ms, gap_event_cb, NULL); +} + +int pairing_start(uint16_t conn_handle) +{ + return ble_gap_security_initiate(conn_handle); +} + +int exchange_mtu(uint16_t conn_handle) +{ + return ble_gattc_exchange_mtu(conn_handle, NULL, NULL); +} + +void security_failed_recover(uint16_t conn_handle, uint8_t status) +{ + struct ble_gap_conn_desc desc = {0}; + int rc; + + ESP_LOGE(TAG, "Security change failed, status %u, clearing local bond and reconnecting", status); + + rc = ble_gap_conn_find(conn_handle, &desc); + if (rc == 0) { + ble_store_util_delete_peer(&desc.peer_id_addr); + } + + ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM); +} diff --git a/examples/bluetooth/esp_ble_audio/cap/handover/sdkconfig.defaults b/examples/bluetooth/esp_ble_audio/cap/handover/sdkconfig.defaults new file mode 100644 index 00000000000..36c2f59a70e --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/handover/sdkconfig.defaults @@ -0,0 +1,54 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# + +CONFIG_BT_ENABLED=y +CONFIG_BT_NIMBLE_ENABLED=n +CONFIG_BT_BLUEDROID_ENABLED=y +CONFIG_BT_CLASSIC_ENABLED=n +CONFIG_BT_CONTROLLER_ENABLED=y +CONFIG_BT_BLE_ENABLED=y +CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y +CONFIG_BT_ACL_CONNECTIONS=1 +CONFIG_BT_GATTC_NOTIF_REG_MAX=20 +CONFIG_BT_BLE_FEAT_ISO_EN=y +CONFIG_BT_BLE_FEAT_PERIODIC_ADV_SYNC_TRANSFER=y + +CONFIG_BT_ISO_MAX_CHAN=4 + +CONFIG_BT_CAP_INITIATOR=y +CONFIG_BT_BAP_UNICAST_CLIENT=y +CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT=2 +CONFIG_BT_BAP_BROADCAST_SOURCE=y +CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT=2 + +CONFIG_BT_CSIP_SET_COORDINATOR=y +CONFIG_BT_BAP_SCAN_DELEGATOR=y +CONFIG_BT_BAP_BROADCAST_ASSISTANT=y +CONFIG_BT_CAP_COMMANDER=y +CONFIG_BT_CAP_HANDOVER=y + +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y + +CONFIG_FREERTOS_HZ=1000 + +# To place LE Audio .bss + control-plane heap in PSRAM, uncomment the +# lines below (needs a PSRAM-capable target, e.g. esp32s31/esp32h4). +# CONFIG_SPIRAM=y +# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_HEAP_EXTERNAL_MEMORY=y +# CONFIG_BT_AUDIO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_AUDIO_HEAP_EXTERNAL_MEMORY=y + +# Bandwidth-optimized (critical-only) compressed BLE Audio/ISO logging over +# UART DMA. Pins shown are for esp32s31 (UART0 TX = GPIO58, 3000000 baud); set +# CONFIG_BLE_LOG_PRPH_UART_DMA_* values to the port/pin your capture reads. +# CONFIG_BLE_LOG_ENABLED=y +# CONFIG_BT_LOG_CRITICAL_ONLY=y +# CONFIG_BLE_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_LOG_PRPH_UART_DMA=y +# CONFIG_BLE_LOG_PRPH_UART_DMA_PORT=0 +# CONFIG_BLE_LOG_PRPH_UART_DMA_BAUD_RATE=3000000 +# CONFIG_BLE_LOG_PRPH_UART_DMA_TX_IO_NUM=58 diff --git a/examples/bluetooth/esp_ble_audio/cap/handover/sdkconfig.defaults.esp32h4 b/examples/bluetooth/esp_ble_audio/cap/handover/sdkconfig.defaults.esp32h4 new file mode 100644 index 00000000000..5920ce3f853 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/handover/sdkconfig.defaults.esp32h4 @@ -0,0 +1,6 @@ +# Override some defaults so BT stack is enabled +# by default in this example + +CONFIG_IDF_TARGET="esp32h4" + +CONFIG_BT_LE_ISO_SUPPORT=y diff --git a/examples/bluetooth/esp_ble_audio/cap/handover/sdkconfig.defaults.esp32s31 b/examples/bluetooth/esp_ble_audio/cap/handover/sdkconfig.defaults.esp32s31 new file mode 100644 index 00000000000..1f18ae5d6a9 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/handover/sdkconfig.defaults.esp32s31 @@ -0,0 +1,6 @@ +# Override some defaults so BT stack is enabled +# by default in this example + +CONFIG_IDF_TARGET="esp32s31" + +CONFIG_BT_LE_ISO_SUPPORT=y diff --git a/examples/bluetooth/esp_ble_audio/cap/handover/sdkconfig.defaults.nimble b/examples/bluetooth/esp_ble_audio/cap/handover/sdkconfig.defaults.nimble new file mode 100644 index 00000000000..0c3c84403ac --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/handover/sdkconfig.defaults.nimble @@ -0,0 +1,13 @@ +# NimBLE host overlay for this example. +# Use with: +# idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.$IDF_TARGET;sdkconfig.defaults.nimble" build + +CONFIG_BT_BLUEDROID_ENABLED=n +CONFIG_BT_NIMBLE_ENABLED=y +CONFIG_BT_NIMBLE_EXT_ADV=y +CONFIG_BT_NIMBLE_NVS_PERSIST=y +CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 +CONFIG_BT_NIMBLE_MAX_CCCDS=20 +CONFIG_BT_NIMBLE_ISO=y +CONFIG_BT_NIMBLE_PERIODIC_ADV_SYNC_TRANSFER=y +CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y 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 76fd734f957..5b8963c8d66 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 @@ -24,6 +24,17 @@ #define CONN_HANDLE_INIT 0xFFFF +/* Number of sink streams the sample drives towards the Acceptor (one per sink ASE, + * so a stereo Acceptor gets one stream per channel). + * + * The same stream objects are reused for unicast and, after a CAP handover, for + * broadcast, so the count is bounded by both the number of sink ASEs that can be + * discovered on the Acceptor and the number of broadcast source streams that can + * be created locally. + */ +#define SINK_STREAM_COUNT MIN(CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT, \ + CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT) + #if CONFIG_EXAMPLE_UNICAST #define LOCAL_DEVICE_NAME "CAP Initiator" 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 dabd5e8e1e1..7e2c4c648c1 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 @@ -18,20 +18,44 @@ ESP_BLE_AUDIO_BAP_LC3_BROADCAST_PRESET_16_2_1_DEFINE(broadcast_preset_16_2_1, ESP_BLE_AUDIO_CONTEXT_TYPE_UNSPECIFIED); static esp_ble_audio_cap_broadcast_source_t *broadcast_source; -static esp_ble_audio_cap_stream_t broadcast_stream; +static esp_ble_audio_cap_stream_t broadcast_streams[SINK_STREAM_COUNT]; + +/* One BIS per channel. The subgroup codec configuration is shared, so the channel + * allocation is carried per BIS as Codec Specific Configuration. + */ +static const esp_ble_audio_location_t bis_locations[] = { + ESP_BLE_AUDIO_LOCATION_FRONT_LEFT, + ESP_BLE_AUDIO_LOCATION_FRONT_RIGHT, +}; +_Static_assert(ARRAY_SIZE(bis_locations) >= SINK_STREAM_COUNT, + "Need one channel allocation per broadcast stream"); + +static uint8_t bis_data[SINK_STREAM_COUNT][6]; + +static int broadcast_stream_index(const esp_ble_audio_bap_stream_t *stream) +{ + for (size_t i = 0; i < ARRAY_SIZE(broadcast_streams); i++) { + if (stream == &broadcast_streams[i].bap_stream) { + return (int)i; + } + } + + return -1; +} 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, "[SRC #0] Stream started"); + ESP_LOGI(TAG, "[SRC #%d] Stream started", broadcast_stream_index(stream)); cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream); err = cap_initiator_tx_register_stream(cap_stream, true); if (err) { - ESP_LOGE(TAG, "[SRC #0] Failed to register TX, err %d", err); + ESP_LOGE(TAG, "[SRC #%d] Failed to register TX, err %d", + broadcast_stream_index(stream), err); } } @@ -39,7 +63,8 @@ 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, "[SRC #0] Stream stopped, reason 0x%02x", reason); + ESP_LOGI(TAG, "[SRC #%d] Stream stopped, reason 0x%02x", + broadcast_stream_index(stream), reason); cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream); @@ -50,7 +75,8 @@ static void broadcast_stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, { esp_ble_audio_cap_stream_t *cap_stream; - ESP_LOGI(TAG, "[SRC #0] ISO disconnected, reason 0x%02x", reason); + ESP_LOGI(TAG, "[SRC #%d] ISO disconnected, reason 0x%02x", + broadcast_stream_index(stream), reason); cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream); @@ -135,13 +161,11 @@ static uint8_t *per_adv_data_get(uint8_t *data_len) int cap_initiator_broadcast_start(void) { - esp_ble_audio_cap_initiator_broadcast_stream_param_t stream_params = { - .stream = &broadcast_stream, - }; + esp_ble_audio_cap_initiator_broadcast_stream_param_t stream_params[SINK_STREAM_COUNT] = {0}; esp_ble_audio_cap_initiator_broadcast_subgroup_param_t subgroup_param = { .codec_cfg = &broadcast_preset_16_2_1.codec_cfg, - .stream_params = &stream_params, - .stream_count = 1, + .stream_params = stream_params, + .stream_count = ARRAY_SIZE(stream_params), }; const esp_ble_audio_cap_initiator_broadcast_create_param_t create_param = { .qos = &broadcast_preset_16_2_1.qos, @@ -160,6 +184,22 @@ int cap_initiator_broadcast_start(void) ESP_LOGI(TAG, "Creating broadcast source"); + for (size_t i = 0; i < ARRAY_SIZE(stream_params); i++) { + const esp_ble_audio_location_t loc = bis_locations[i]; + + /* LTV: length, type, 4-octet Audio_Channel_Allocation (little endian) */ + bis_data[i][0] = 5; + bis_data[i][1] = ESP_BLE_AUDIO_CODEC_CFG_CHAN_ALLOC; + bis_data[i][2] = (uint8_t)loc; + bis_data[i][3] = (uint8_t)(loc >> 8); + bis_data[i][4] = (uint8_t)(loc >> 16); + bis_data[i][5] = (uint8_t)(loc >> 24); + + stream_params[i].stream = &broadcast_streams[i]; + stream_params[i].data = bis_data[i]; + stream_params[i].data_len = sizeof(bis_data[i]); + } + err = esp_ble_audio_cap_initiator_broadcast_audio_create(&create_param, &broadcast_source); if (err) { ESP_LOGE(TAG, "Failed to create broadcast source, err %d", err); @@ -212,7 +252,9 @@ end: int cap_initiator_broadcast_init(void) { - esp_ble_audio_cap_stream_ops_register(&broadcast_stream, &broadcast_stream_ops); + for (size_t i = 0; i < ARRAY_SIZE(broadcast_streams); i++) { + esp_ble_audio_cap_stream_ops_register(&broadcast_streams[i], &broadcast_stream_ops); + } ESP_LOGI(TAG, "CAP initiator broadcast initialized"); 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 1966901d8e0..c33c295afb8 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 @@ -13,7 +13,12 @@ #include "cap_initiator.h" -static struct tx_stream tx_streams[IS_ENABLED(CONFIG_EXAMPLE_UNICAST) + IS_ENABLED(CONFIG_EXAMPLE_BROADCAST)]; +/* One slot per stream the sample can transmit on, in every enabled mode: the + * unicast sink streams and the broadcast source streams are distinct objects. + */ +static struct tx_stream tx_streams[SINK_STREAM_COUNT * + (IS_ENABLED(CONFIG_EXAMPLE_UNICAST) + + IS_ENABLED(CONFIG_EXAMPLE_BROADCAST))]; static const char *cap_stream_tx_label(const esp_ble_audio_cap_stream_t *cap_stream) { 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 127b8a875a3..75238ebe58d 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 @@ -7,20 +7,41 @@ #include #include +#include #include #include "cap_initiator.h" -ESP_BLE_AUDIO_BAP_LC3_UNICAST_PRESET_16_2_1_DEFINE(unicast_preset_16_2_1, +/* One preset per sink stream: each ..._DEFINE allocates its own codec configuration + * buffer, which is what lets the streams carry different Audio_Channel_Allocation + * values. Extend both this list and sink_presets[] to drive more than two channels. + */ +ESP_BLE_AUDIO_BAP_LC3_UNICAST_PRESET_16_2_1_DEFINE(unicast_preset_left, + ESP_BLE_AUDIO_LOCATION_FRONT_LEFT, + ESP_BLE_AUDIO_CONTEXT_TYPE_UNSPECIFIED); +ESP_BLE_AUDIO_BAP_LC3_UNICAST_PRESET_16_2_1_DEFINE(unicast_preset_right, + ESP_BLE_AUDIO_LOCATION_FRONT_RIGHT, + ESP_BLE_AUDIO_CONTEXT_TYPE_UNSPECIFIED); +/* The return direction carries a single microphone channel. */ +ESP_BLE_AUDIO_BAP_LC3_UNICAST_PRESET_16_2_1_DEFINE(unicast_preset_mono, ESP_BLE_AUDIO_LOCATION_MONO_AUDIO, ESP_BLE_AUDIO_CONTEXT_TYPE_UNSPECIFIED); + +static esp_ble_audio_bap_lc3_preset_t *const sink_presets[] = { + &unicast_preset_left, + &unicast_preset_right, +}; +_Static_assert(ARRAY_SIZE(sink_presets) >= SINK_STREAM_COUNT, + "Need one preset (one channel allocation) per sink stream"); + static esp_ble_audio_cap_unicast_group_t *unicast_group; static struct peer_config { esp_ble_audio_cap_stream_t source_stream; - esp_ble_audio_cap_stream_t sink_stream; + esp_ble_audio_cap_stream_t sink_streams[SINK_STREAM_COUNT]; esp_ble_audio_bap_ep_t *source_ep; - esp_ble_audio_bap_ep_t *sink_ep; + esp_ble_audio_bap_ep_t *sink_eps[SINK_STREAM_COUNT]; + size_t sink_ep_count; esp_ble_conn_t *conn; uint16_t conn_handle; @@ -39,19 +60,35 @@ static const char *dir_str(esp_ble_audio_dir_t dir) 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) { + for (size_t i = 0; i < ARRAY_SIZE(peer.sink_streams); i++) { + if (stream == &peer.sink_streams[i].bap_stream) { + return "SNK"; + } + } + + 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; + /* Index within the pool of its own direction, so logs read as "SNK #0" / + * "SNK #1" for the two channels of a stereo Acceptor. + */ + for (size_t i = 0; i < ARRAY_SIZE(peer.sink_streams); i++) { + if (stream == &peer.sink_streams[i].bap_stream) { + return (int)i; + } + } + + if (stream == &peer.source_stream.bap_stream) { + return 0; + } + + return -1; } static bool is_tx_stream(esp_ble_audio_bap_stream_t *stream) @@ -97,9 +134,10 @@ static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *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)) { + /* Only the source stream receives, so resetting the metrics here would let + * a sink stream starting later wipe the counters of a running source. + */ cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream); err = cap_initiator_tx_register_stream(cap_stream, false); @@ -107,6 +145,8 @@ static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream) ESP_LOGE(TAG, "[%s #%d] Failed to register TX, err %d", stream_dir_str(stream), stream_index(stream), err); } + } else { + example_audio_rx_metrics_reset(&rx_metrics); } } @@ -206,7 +246,9 @@ static int discover_sinks(void) { int err; - esp_ble_audio_cap_stream_ops_register(&peer.sink_stream, &unicast_stream_ops); + for (size_t i = 0; i < ARRAY_SIZE(peer.sink_streams); i++) { + esp_ble_audio_cap_stream_ops_register(&peer.sink_streams[i], &unicast_stream_ops); + } err = esp_ble_audio_bap_unicast_client_discover(peer.conn_handle, ESP_BLE_AUDIO_DIR_SINK); if (err) { @@ -238,28 +280,44 @@ static int discover_sources(void) static int unicast_group_create(void) { - esp_ble_audio_cap_unicast_group_stream_param_t source_stream_param = { - .qos_cfg = &unicast_preset_16_2_1.qos, - .stream = &peer.source_stream, - }; - esp_ble_audio_cap_unicast_group_stream_param_t sink_stream_param = { - .qos_cfg = &unicast_preset_16_2_1.qos, - .stream = &peer.sink_stream, - }; - esp_ble_audio_cap_unicast_group_stream_pair_param_t pair_params = {0}; + /* The group keeps referencing these while it exists, so they outlive this call. */ + static esp_ble_audio_cap_unicast_group_stream_param_t source_stream_param; + static esp_ble_audio_cap_unicast_group_stream_param_t sink_stream_params[SINK_STREAM_COUNT]; + static esp_ble_audio_cap_unicast_group_stream_pair_param_t pair_params[SINK_STREAM_COUNT]; esp_ble_audio_cap_unicast_group_param_t group_param = {0}; + size_t pair_count = 0; int err; + /* One CIS per sink stream. */ + for (size_t i = 0; i < peer.sink_ep_count; i++) { + sink_stream_params[i].qos_cfg = &sink_presets[i]->qos; + sink_stream_params[i].stream = &peer.sink_streams[i]; + + pair_params[pair_count].rx_param = NULL; + pair_params[pair_count].tx_param = &sink_stream_params[i]; + pair_count++; + } + + /* The return direction shares the first CIS, making it bidirectional. */ if (peer.source_ep) { - pair_params.rx_param = &source_stream_param; + source_stream_param.qos_cfg = &unicast_preset_mono.qos; + source_stream_param.stream = &peer.source_stream; + + if (pair_count == 0) { + pair_params[0].tx_param = NULL; + pair_count = 1; + } + + pair_params[0].rx_param = &source_stream_param; } - if (peer.sink_ep) { - pair_params.tx_param = &sink_stream_param; + if (pair_count == 0) { + ESP_LOGW(TAG, "No endpoints available, skip creating unicast group"); + return -ENODEV; } - group_param.params_count = 1; - group_param.params = &pair_params; + group_param.params_count = pair_count; + group_param.params = pair_params; err = esp_ble_audio_cap_unicast_group_create(&group_param, &unicast_group); if (err) { @@ -295,15 +353,19 @@ int unicast_group_delete(void) static int unicast_audio_start(void) { - esp_ble_audio_cap_unicast_audio_start_stream_param_t stream_param[2] = {0}; + /* codec_cfg is assigned to the stream and has to stay valid while it is + * non-idle, so the parameters cannot live on the stack. + */ + static esp_ble_audio_cap_unicast_audio_start_stream_param_t + stream_param[SINK_STREAM_COUNT + 1]; esp_ble_audio_cap_unicast_audio_start_param_t param = {0}; int err; - if (peer.sink_ep) { + for (size_t i = 0; i < peer.sink_ep_count; i++) { stream_param[param.count].member.member = peer.conn; - stream_param[param.count].stream = &peer.sink_stream; - stream_param[param.count].ep = peer.sink_ep; - stream_param[param.count].codec_cfg = &unicast_preset_16_2_1.codec_cfg; + stream_param[param.count].stream = &peer.sink_streams[i]; + stream_param[param.count].ep = peer.sink_eps[i]; + stream_param[param.count].codec_cfg = &sink_presets[i]->codec_cfg; param.count++; } @@ -311,7 +373,7 @@ static int unicast_audio_start(void) stream_param[param.count].member.member = peer.conn; stream_param[param.count].stream = &peer.source_stream; stream_param[param.count].ep = peer.source_ep; - stream_param[param.count].codec_cfg = &unicast_preset_16_2_1.codec_cfg; + stream_param[param.count].codec_cfg = &unicast_preset_mono.codec_cfg; param.count++; } @@ -383,13 +445,16 @@ static void endpoint_cb(esp_ble_conn_t *conn, { if (dir == ESP_BLE_AUDIO_DIR_SOURCE) { if (peer.source_ep == NULL) { - ESP_LOGI(TAG, "[%s] Endpoint discovered", dir_str(dir)); + ESP_LOGI(TAG, "[%s #0] 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, "[%s] Endpoint discovered", dir_str(dir)); - peer.sink_ep = ep; + /* Keep every sink endpoint, up to one per stream: a stereo Acceptor + * exposes one sink ASE per channel and all of them are used. + */ + if (peer.sink_ep_count < ARRAY_SIZE(peer.sink_eps)) { + ESP_LOGI(TAG, "[%s #%zu] Endpoint discovered", dir_str(dir), peer.sink_ep_count); + peer.sink_eps[peer.sink_ep_count++] = ep; } } } @@ -534,7 +599,8 @@ static void acl_disconnect(esp_ble_audio_gap_app_event_t *event) peer.conn_handle = CONN_HANDLE_INIT; peer.conn = NULL; peer.source_ep = NULL; - peer.sink_ep = NULL; + memset(peer.sink_eps, 0, sizeof(peer.sink_eps)); + peer.sink_ep_count = 0; peer.disc_completed = false; peer.mtu_exchanged = false; diff --git a/examples/bluetooth/esp_ble_audio/cap/initiator/sdkconfig.defaults b/examples/bluetooth/esp_ble_audio/cap/initiator/sdkconfig.defaults index 63c1276ecd2..6fdaed04ee9 100644 --- a/examples/bluetooth/esp_ble_audio/cap/initiator/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/cap/initiator/sdkconfig.defaults @@ -13,7 +13,7 @@ CONFIG_BT_ACL_CONNECTIONS=1 CONFIG_BT_GATTC_NOTIF_REG_MAX=20 CONFIG_BT_BLE_FEAT_ISO_EN=y -CONFIG_BT_ISO_MAX_CHAN=2 +CONFIG_BT_ISO_MAX_CHAN=4 CONFIG_BT_CAP_INITIATOR=y CONFIG_BT_BAP_UNICAST_CLIENT=y @@ -26,3 +26,24 @@ CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y CONFIG_EXAMPLE_UNICAST=y CONFIG_FREERTOS_HZ=1000 + +# To place LE Audio .bss + control-plane heap in PSRAM, uncomment the +# lines below (needs a PSRAM-capable target, e.g. esp32s31/esp32h4). +# CONFIG_SPIRAM=y +# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_HEAP_EXTERNAL_MEMORY=y +# CONFIG_BT_AUDIO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_AUDIO_HEAP_EXTERNAL_MEMORY=y + +# Bandwidth-optimized (critical-only) compressed BLE Audio/ISO logging over +# UART DMA. Pins shown are for esp32s31 (UART0 TX = GPIO58, 3000000 baud); set +# CONFIG_BLE_LOG_PRPH_UART_DMA_* values to the port/pin your capture reads. +# CONFIG_BLE_LOG_ENABLED=y +# CONFIG_BT_LOG_CRITICAL_ONLY=y +# CONFIG_BLE_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_LOG_PRPH_UART_DMA=y +# CONFIG_BLE_LOG_PRPH_UART_DMA_PORT=0 +# CONFIG_BLE_LOG_PRPH_UART_DMA_BAUD_RATE=3000000 +# CONFIG_BLE_LOG_PRPH_UART_DMA_TX_IO_NUM=58 diff --git a/examples/bluetooth/esp_ble_audio/tmap/bmr/README.md b/examples/bluetooth/esp_ble_audio/tmap/bmr/README.md index 641d8e7887f..c1c74256c64 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/bmr/README.md +++ b/examples/bluetooth/esp_ble_audio/tmap/bmr/README.md @@ -59,7 +59,7 @@ For `esp32s31`, replace the chip overlay accordingly. 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. +8. On `PA_SYNC_LOST` matching the active sync handle, the sink is deleted — by `broadcast_sink_stopped_cb` once BASS clears `bis_sync` (deleting from `stream_stopped_cb` races `rem_src` while `bis_sync` is still set), or directly here if the sink never reached BIG sync — and scanning resumes. ## Expected Log @@ -90,6 +90,7 @@ I (xxx) TMAP_BMR: [SNK #0] Stream started Stop / sync loss: ``` I (xxx) TMAP_BMR: [SNK #0] Stream stopped, reason 0x.. +I (xxx) TMAP_BMR: Broadcast sink stopped, reason 0x.. I (xxx) TMAP_BMR: PA sync lost: sync_handle .. reason 0x.. I (xxx) TMAP_BMR: PA sync .. lost with reason .. ``` 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 2828bfb3c08..249bf79f4e2 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 @@ -21,6 +21,10 @@ static bool tmap_bms_found; static esp_ble_audio_bap_broadcast_sink_t *broadcast_sink; static uint32_t bcast_id; +/* True after a BIS stream stops until broadcast_sink_stopped_cb runs — + * pa_lost must not delete in that window (EPs idle, bis_sync not yet cleared). + */ +static bool stop_pending; static esp_ble_audio_bap_stream_t streams[CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT]; static esp_ble_audio_bap_stream_t *streams_p[ARRAY_SIZE(streams)]; @@ -72,6 +76,11 @@ static void stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason { ESP_LOGI(TAG, "[SNK #%d] Stream stopped, reason 0x%02x", stream_index(stream), reason); + /* Sink delete is deferred to broadcast_sink_stopped_cb (after BASS + * bis_sync clear). Deleting while EPs are idle but bis_sync is still + * set races rem_src and leaves WRNs. + */ + stop_pending = true; } static void stream_recv_cb(esp_ble_audio_bap_stream_t *stream, @@ -129,9 +138,35 @@ static void syncable_cb(esp_ble_audio_bap_broadcast_sink_t *sink, } } +static void broadcast_sink_stopped_cb(esp_ble_audio_bap_broadcast_sink_t *sink, + uint8_t reason) +{ + esp_err_t err; + + ESP_LOGI(TAG, "Broadcast sink stopped, reason 0x%02x", reason); + + stop_pending = false; + + /* Called from big_stopped after update_recv_state_big_cleared(), so + * BASS bis_sync is already 0 and rem_src inside delete can succeed. + */ + if (broadcast_sink == NULL) { + return; + } + + err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink); + if (err) { + ESP_LOGE(TAG, "Failed to delete broadcast sink, err %d", err); + return; + } + + broadcast_sink = NULL; +} + static esp_ble_audio_bap_broadcast_sink_cb_t broadcast_sink_cbs = { .base_recv = base_recv_cb, .syncable = syncable_cb, + .stopped = broadcast_sink_stopped_cb, }; static esp_ble_audio_bap_scan_delegator_cb_t scan_delegator_cbs; @@ -254,6 +289,8 @@ void bap_broadcast_pa_sync(esp_ble_audio_gap_app_event_t *event) void bap_broadcast_pa_lost(esp_ble_audio_gap_app_event_t *event) { + esp_err_t err; + if (sync_handle == event->pa_sync_lost.sync_handle) { ESP_LOGI(TAG, "PA sync %u lost with reason %u", sync_handle, event->pa_sync_lost.reason); @@ -261,9 +298,17 @@ void bap_broadcast_pa_lost(esp_ble_audio_gap_app_event_t *event) sync_handle = PA_SYNC_HANDLE_INIT; pa_syncing = false; /* allow the rescan below to sync a fresh broadcaster */ - if (broadcast_sink != NULL) { - esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink); - broadcast_sink = NULL; + /* Prefer delete from broadcast_sink_stopped_cb after bis_sync clear. + * Skip while BIG teardown is in flight; if the sink never reached + * BIG sync, delete here. + */ + if (broadcast_sink != NULL && !stop_pending) { + err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink); + if (err) { + ESP_LOGW(TAG, "Sink delete deferred to stopped_cb, err %d", err); + } else { + broadcast_sink = NULL; + } } bap_broadcast_sink_scan(); diff --git a/examples/bluetooth/esp_ble_audio/tmap/bmr/sdkconfig.defaults b/examples/bluetooth/esp_ble_audio/tmap/bmr/sdkconfig.defaults index 5febc040e4e..fd69ac63aa4 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/bmr/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/tmap/bmr/sdkconfig.defaults @@ -26,3 +26,24 @@ CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=60 CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y CONFIG_FREERTOS_HZ=1000 + +# To place LE Audio .bss + control-plane heap in PSRAM, uncomment the +# lines below (needs a PSRAM-capable target, e.g. esp32s31/esp32h4). +# CONFIG_SPIRAM=y +# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_HEAP_EXTERNAL_MEMORY=y +# CONFIG_BT_AUDIO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_AUDIO_HEAP_EXTERNAL_MEMORY=y + +# Bandwidth-optimized (critical-only) compressed BLE Audio/ISO logging over +# UART DMA. Pins shown are for esp32s31 (UART0 TX = GPIO58, 3000000 baud); set +# CONFIG_BLE_LOG_PRPH_UART_DMA_* values to the port/pin your capture reads. +# CONFIG_BLE_LOG_ENABLED=y +# CONFIG_BT_LOG_CRITICAL_ONLY=y +# CONFIG_BLE_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_LOG_PRPH_UART_DMA=y +# CONFIG_BLE_LOG_PRPH_UART_DMA_PORT=0 +# CONFIG_BLE_LOG_PRPH_UART_DMA_BAUD_RATE=3000000 +# CONFIG_BLE_LOG_PRPH_UART_DMA_TX_IO_NUM=58 diff --git a/examples/bluetooth/esp_ble_audio/tmap/bms/sdkconfig.defaults b/examples/bluetooth/esp_ble_audio/tmap/bms/sdkconfig.defaults index 31c481d6a80..cfab3744e76 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/bms/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/tmap/bms/sdkconfig.defaults @@ -21,3 +21,24 @@ CONFIG_BT_BAP_BROADCAST_SOURCE=y CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y CONFIG_FREERTOS_HZ=1000 + +# To place LE Audio .bss + control-plane heap in PSRAM, uncomment the +# lines below (needs a PSRAM-capable target, e.g. esp32s31/esp32h4). +# CONFIG_SPIRAM=y +# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_HEAP_EXTERNAL_MEMORY=y +# CONFIG_BT_AUDIO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_AUDIO_HEAP_EXTERNAL_MEMORY=y + +# Bandwidth-optimized (critical-only) compressed BLE Audio/ISO logging over +# UART DMA. Pins shown are for esp32s31 (UART0 TX = GPIO58, 3000000 baud); set +# CONFIG_BLE_LOG_PRPH_UART_DMA_* values to the port/pin your capture reads. +# CONFIG_BLE_LOG_ENABLED=y +# CONFIG_BT_LOG_CRITICAL_ONLY=y +# CONFIG_BLE_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_LOG_PRPH_UART_DMA=y +# CONFIG_BLE_LOG_PRPH_UART_DMA_PORT=0 +# CONFIG_BLE_LOG_PRPH_UART_DMA_BAUD_RATE=3000000 +# CONFIG_BLE_LOG_PRPH_UART_DMA_TX_IO_NUM=58 diff --git a/examples/bluetooth/esp_ble_audio/tmap/central/README.md b/examples/bluetooth/esp_ble_audio/tmap/central/README.md index 975acfcd944..020597da3fd 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/central/README.md +++ b/examples/bluetooth/esp_ble_audio/tmap/central/README.md @@ -7,14 +7,64 @@ ## Overview -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. +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, pairs, exchanges MTU, and then drives TMAP, CSIP and VCP discovery before bringing up unicast audio. -The example uses the ESP-BLE-AUDIO library pieces for: CAP initiator with the BAP Unicast Client (LC3 preset 48_2_1, sink direction, FRONT_LEFT, MEDIA context), VCP Volume Controller, MCP server backed by the media proxy player, and CCP server registering a single GTBS bearer (`Generic TBS`, UCI `un000`, `tel,wechat` URI schemes, 5G technology). A periodic TX scheduler in the ISO task feeds dummy ISO SDUs filled with the sequence number. Device name is set to `TMAP Central`. +The example uses the ESP-BLE-AUDIO library pieces for: CAP initiator with the BAP Unicast Client (LC3 preset 48_2_1, MEDIA context), CSIP Set Coordinator, 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). Each transmitting stream runs its own TX scheduler in the ISO task, feeding dummy ISO SDUs filled with that stream's sequence number. Device name is set to `TMAP Central`. + +## Topology + +The number of peripherals and the number of CIS is a build-time choice. The first CIS is always **bidirectional** — one sink and one source ASE of the same peripheral share it — and a second CIS, when present, is **transmit-only**: + +| Topology | Peripherals | CIS #0 | CIS #1 | +| --- | --- | --- | --- | +| `1P_1CIS` | 1 | TX+RX — sink #0 + source #0 | — | +| `1P_2CIS` (default) | 1 | TX+RX — sink #0 + source #0 | TX — sink #1 of the same peer | +| `2P_2CIS` | 2 (CSIP coordinated set) | TX+RX — rank 1 member | TX — rank 2 member | + +All CIS live in a single CIG. Which endpoint goes on which CIS is expressed by one table (`bindings[]` in `main/cap_initiator.c`); everything else in the state machine is shared between the three topologies. + +> **The topology picked here must match how [tmap_peripheral](../peripheral/) is built.** The central does not negotiate the topology — it looks for a fixed number of peers and a fixed number of sink ASEs on each. A mismatch does not fall back gracefully: it stalls in discovery or refuses to build the group. + +| Central **Unicast topology** | Peripheral boards | **Earbuds type** | **Earbud Location** | +| --- | --- | --- | --- | +| `1 peripheral, 1 CIS (TX+RX)` | 1 | `Single headset` | `Mono` | +| `1 peripheral, 2 CISes (TX+RX, TX)` **(default)** | 1 | `Single headset` **(default)** | `Stereo` **(default)** | +| `2 peripherals (coordinated set), 2 CISes (TX+RX, TX)` | 2 | `Duo headset` on both | `Left` on one, `Right` on the other | + +What a mismatch looks like: + +* central built for 2 peripherals, only one flashed → stops after `Peer 1/2 connected, scanning for the rest` and never starts audio +* central built for 2 CISes, peripheral left on `Mono` → `[SNK #1] Peer 0 has no sink endpoint 1` and `Failed to create unicast group` +* peripheral built with `CONFIG_BT_ASCS_MAX_ACTIVE_ASES` below 3 → the third codec configuration is rejected with `[SNK #1] Config response, rsp_code 13` (Insufficient Resources) and `Unicast start completed, err -77` +* two set members flashed with the same ear → same rank on both, `both report rank 1`, streams come up unlocked + +The peripheral's Kconfig only offers the location options that suit its earbuds type — mono/stereo for a single headset, left/right for a duo — so the two peripheral settings on their own cannot express an unsupported combination. What still has to be kept in step is picking the *right* one of the three rows above. + +### Channel allocation + +The rendered channel comes from the peer's **PACS Sink Audio Locations**, not from its CSIP rank — CSIS only requires a rank to be unique and non-zero, it says nothing about which ear a device is. `sink_preset()` in `main/cap_initiator.c` resolves it as: + +* peer carries **one** sink stream → its own advertised location (`FRONT_LEFT` / `FRONT_RIGHT`), or mono when it advertises an empty bitmap or both channels +* peer carries **two** sink streams → front left for the first, front right for the second + +An acceptor that publishes no location therefore gets a mono stream, which is what the peripheral example's `Mono` location produces. + +Rank still decides *which* member gets the bidirectional CIS in `2P_2CIS`, which is a separate question from which ear it is. + +### Coordinated set (`2P_2CIS`) + +The central resolves each advertised RSI against the SIRK hardcoded in `main/csip_set_coordinator.c`, connects only members of that set, orders them by CSIS rank, and runs the CSIP ordered access procedure — locking the set ascending by rank before stream setup and releasing it once `unicast_start_complete` fires. + +CAP names the members by their CSIS instance (`ESP_BLE_AUDIO_CAP_SET_TYPE_CSIP`) as soon as every member exposes one, and falls back to naming them by connection (`..._AD_HOC`) otherwise — that fallback is what lets the same code path serve a peripheral without CSIS. Both forms resolve to the same connections; the set lock itself is driven by the CSIP layer, not by the CAP set type. + +Volume is treated as a property of the whole set: every member gets its own VCP volume controller, and volume / mute go through the **CAP commander** (`esp_ble_audio_cap_commander_change_volume*`) so both earbuds change together. The commander refuses to run until each member has a discovered volume controller, so the example reads each member's VCS state first — that also syncs the VCS change counter. + +To make that visible, the example keeps driving the set once the streams are up: every 5 minutes it issues one commander procedure, walking the volume through 32 / 96 / 160 / 224 and then muting and unmuting. Each step is a single procedure covering every member, so both earbuds report the same value in the same tick rather than being written one at a time. It runs on its own application task — not the audio task and not the esp_timer one, which the stack's own timers share — and the audio APIs take the host lock themselves, so calling them from there is safe. A tick is skipped rather than queued if the previous procedure has not reported back, and the cycle pauses as soon as the set is no longer whole. ## Requirements * A board with BLE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31) -* Peer device running the paired example +* One or two boards running [tmap_peripheral](../peripheral/), depending on the topology ## Configuration @@ -22,7 +72,11 @@ The example uses the ESP-BLE-AUDIO library pieces for: CAP initiator with the BA idf.py menuconfig ``` -No build-time options — runtime defaults are baked into source. +Under **Example: TMAP Central (CG & UMS)** → **Unicast topology**, pick one — and build the peripheral to match, per the table in [Topology](#topology): + +* **1 peripheral, 1 CIS (TX+RX)** (`EXAMPLE_TMAP_CEN_TOPO_1P_1CIS`) — peripheral: `Single headset` + `Mono`. +* **1 peripheral, 2 CISes (TX+RX, TX)** (`EXAMPLE_TMAP_CEN_TOPO_1P_2CIS`, **default**) — peripheral: `Single headset` + `Stereo`, which is also its default. It must expose two sink ASEs (`CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT`, 2 in its `sdkconfig.defaults`). +* **2 peripherals (coordinated set), 2 CISes (TX+RX, TX)** (`EXAMPLE_TMAP_CEN_TOPO_2P_2CIS`) — selects `BT_CSIP_SET_COORDINATOR`. Peripheral: `Duo headset` on both boards, one `Left` and one `Right`; their ranks default from that and must stay distinct. Rank picks which member gets the bidirectional CIS, the location picks the channel. ### Security & Pairing @@ -53,65 +107,86 @@ For `esp32s31`, replace the chip overlay accordingly. ## Example Flow 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). +2. Sub-modules initialize in order: CAP initiator (one TX scheduler per sink stream), CSIP set coordinator, VCP volume controller plus CAP commander callbacks, 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. +4. Each connectable scan result is parsed for TMAS service data and, in the two-peripheral topology, for the CSIS RSI. A peer is taken when its TMAP role contains UMR and — for a coordinated set build — its RSI resolves against our SIRK. Peers already connected are skipped by address. 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. +6. After the MTU exchange and GATT discovery both complete, TMAP, VCP and CSIP discovery start for that peer, and scanning resumes if more peers are still wanted. VCP discovery runs on **every** member, not just the first. +7. Once every peer has been through set discovery, the members are ordered by rank, the set is locked (two-peripheral topology only) and `cap_initiator_setup` runs CAS discover → sink and source ASE discovery on every member. The CAS discovery also records each peer's CSIS instance, and PACS reports its Sink Audio Locations. +8. When the last member reports back, the unicast group is created from `bindings[]` — each sink's channel resolved from that peer's advertised location — and unicast audio starts. The set lock is released when the start procedure completes. +9. Each sink stream that reaches "started" arms its own TX scheduler at the QoS interval and sends ISO SDUs filled with its own sequence number; the source stream's incoming SDUs are counted as RX metrics. +10. Once the streams are up and every member's VCS state has been read, a periodic demo starts driving the set through the CAP commander — one procedure every 5 minutes, cycling volume and then mute, each applying to the whole set at once. +11. Losing one member costs the others nothing: the CIG stays up, their CIS keep streaming, and the departed member's slot in the group is held for whatever comes back to fill it. Only that member is scanned for and rediscovered, and starting it again reuses its slot — releasing a stream leaves it in the group, and configuring it again rebinds the new endpoint to the CIS the slot already owns. The group is deleted only when the last member goes. ## Expected Log Initialization phase: ``` -TMAP_CEN: CAP initiator initialized +TMAP_CEN: CAP initiator initialized: 2 peer(s), 2 CIS +TMAP_CEN: CSIP set coordinator initialized TMAP_CEN: VCP volume controller initialized TMAP_CEN: MCP server initialized TMAP_CEN: Registered gtbs bearer -TMAP_CEN: Scanning for peripheral... ``` -Scan and connect: +Scan and connect (the `2/2` count and the `set member` note only appear in a coordinated-set build): ``` -TMAP_CEN: Found TMAS in peer adv data! +TMAP_CEN: Found peer 1/2 (TMAS UMR, set member) 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_CEN: TMAP discovery done: handle +TMAP_CEN: Peer 1/2 connected, scanning for the rest +TMAP_CEN: CSIP discovered: handle set_size 2 rank 1 lockable 1 ``` -TMAP / VCP / CAP setup: +Set lock and 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: All 2 peer(s) ready +TMAP_CEN: Set locked +TMAP_CEN: CAP initiator setup for 2 member(s) +TMAP_CEN: Found CAS with CSIS: handle +TMAP_CEN: [SNK #0] Endpoint discovered on handle +TMAP_CEN: [SRC #0] Endpoint discovered on handle +TMAP_CEN: Sink discover complete: handle +TMAP_CEN: Source discover complete: handle +TMAP_CEN: [SNK #0] Peer 0 (loc 0x00000001) renders front left +TMAP_CEN: [SNK #1] Peer 1 (loc 0x00000002) renders front right +TMAP_CEN: Created unicast group: 2 CIS +TMAP_CEN: Started 3 unicast stream(s) as a coordinated set TMAP_CEN: Unicast start completed +TMAP_CEN: Set released +``` + +Volume, applied to the whole set through the CAP commander: + +``` +TMAP_CEN: VCP discovery done, vocs aics +TMAP_CEN: VCP state cb done, volume mute +TMAP_CEN: Driving the volume of 2 member(s) every 300 s +TMAP_CEN: Changing volume of 2 member(s) to 32 +TMAP_CEN: Set volume changed +TMAP_CEN: Changing volume of 2 member(s) to 96 +TMAP_CEN: Set volume changed +TMAP_CEN: Changing volume mute state of 2 member(s) to 1 +TMAP_CEN: Set volume mute changed +``` + +Streaming phase. Each stream reports its own count, so the two sinks advance independently and the source counts what the rank 1 peripheral sends back: + +``` +TMAP_CEN: [SNK #0] Stream started +TMAP_CEN: [SNK #0] Streaming, interval , length +TMAP_CEN: [SNK #1] Stream started +TMAP_CEN: [SNK #1] Streaming, interval , length +TMAP_CEN: [SNK #0] TX: packets +TMAP_CEN: [SNK #1] TX: packets +TMAP_CEN: [SRC #0] RX: packets ``` Call control / disconnect: @@ -129,11 +204,22 @@ Tag is `TMAP_CEN`. ## Peer Pairing -Run [tmap_peripheral](../peripheral/) on a second board. +Run [tmap_peripheral](../peripheral/) on one or two boards, matching the topology. -1. Flash the peripheral and let it start extended advertising. +### 1 peripheral (`1P_1CIS` / `1P_2CIS`) + +1. Flash the peripheral and let it start extended advertising. `1P_2CIS` works with its defaults (`Single headset` + `Stereo`); for `1P_1CIS` switch its location to **Mono**. 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. +5. The central starts the sink stream(s) and feeds ISO SDUs at the QoS interval; the peripheral transmits back on the bidirectional CIS. 6. The central can drive the peripheral's volume via VCP and accept TBS originate/terminate from the peripheral. + +### 2 peripherals (`2P_2CIS`) + +1. Build the peripheral twice as **Duo headset**, once with **Earbud Location** = **Left** and once with **Right**. The rank default follows the location (left → 1, right → 2), so the two boards get distinct ranks without touching **Device rank in set**. Flash one image per board. +2. Flash and start this central. It connects the first member it finds, resumes scanning, then connects the second. +3. After both members finish CSIP discovery the central locks the set by rank, sets up one CIG with two CIS, and releases the lock when the procedure completes. +4. The rank 1 board gets the bidirectional CIS and transmits back; the rank 2 board gets a transmit-only CIS. Which channel each renders comes from its **Earbud Location**, so a rank/ear mismatch shows up in the `renders front left` / `renders front right` log rather than silently sending the wrong channel. +5. Once both streams are up and both members' VCS state is read, the volume demo starts: every 5 minutes one CAP commander procedure moves both boards to the same volume, and the mute steps at the end of the cycle mute and unmute them together. Both boards log the same value within one tick — that is the point of routing it through the commander instead of writing each VCS separately. +6. Losing either board leaves the other one streaming: its CIS is untouched, and the central just rescans for the missing one and re-enters it into the slot it left. The surviving earbud logs nothing at all — no `Disable request`, no `ISO disconnected` — while the central logs `Lost a member; 1 still streaming` and later `[SNK #n] Already streaming, left alone`. diff --git a/examples/bluetooth/esp_ble_audio/tmap/central/main/CMakeLists.txt b/examples/bluetooth/esp_ble_audio/tmap/central/main/CMakeLists.txt index e851db1810b..d46876efc1d 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/central/main/CMakeLists.txt +++ b/examples/bluetooth/esp_ble_audio/tmap/central/main/CMakeLists.txt @@ -1,5 +1,6 @@ set(srcs "cap_initiator.c" "ccp_server.c" + "csip_set_coordinator.c" "mcp_server.c" "vcp_vol_ctlr.c" "main.c") diff --git a/examples/bluetooth/esp_ble_audio/tmap/central/main/Kconfig.projbuild b/examples/bluetooth/esp_ble_audio/tmap/central/main/Kconfig.projbuild new file mode 100644 index 00000000000..f2a22ae8556 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/tmap/central/main/Kconfig.projbuild @@ -0,0 +1,51 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 + +menu "Example: TMAP Central (CG & UMS)" + + choice EXAMPLE_TMAP_CEN_TOPOLOGY + prompt "Unicast topology" + default EXAMPLE_TMAP_CEN_TOPO_1P_2CIS + help + How many TMAP peripherals to connect and how many CIS carry the + streams. The first CIS is always bidirectional — one sink and one + source endpoint of the same peripheral share it — and a second CIS, + when present, is transmit-only. + + config EXAMPLE_TMAP_CEN_TOPO_1P_1CIS + bool "1 peripheral, 1 CIS (TX+RX)" + help + One peripheral. Its sink and source ASEs are paired into a single + bidirectional CIS. + + Build the peripheral as "Single headset" with the "Mono" location, + which is its default. + + config EXAMPLE_TMAP_CEN_TOPO_1P_2CIS + bool "1 peripheral, 2 CISes (TX+RX, TX)" + help + One peripheral exposing two sink ASEs. The first sink shares a + bidirectional CIS with the source ASE; the second sink gets its + own transmit-only CIS, and the two are rendered front left and + front right. + + Build the peripheral as "Single headset" with the "Stereo" + location, so its PACS advertises both channels, and with at least + two sink ASEs (CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT). + + config EXAMPLE_TMAP_CEN_TOPO_2P_2CIS + bool "2 peripherals (coordinated set), 2 CISes (TX+RX, TX)" + select BT_CSIP_SET_COORDINATOR + help + Two peripherals that belong to one CSIP coordinated set, both + served by a single CIG. The rank 1 member gets the bidirectional + CIS, the rank 2 member a transmit-only one; which channel each + renders comes from its own PACS Audio Locations, not from rank. + + Build the peripheral twice as "Duo headset", one with the "Left" + location and one with "Right" — their ranks default from that and + must stay distinct. Both share the SIRK hardcoded in the example. + + endchoice + +endmenu diff --git a/examples/bluetooth/esp_ble_audio/tmap/central/main/bluedroid/central.c b/examples/bluetooth/esp_ble_audio/tmap/central/main/bluedroid/central.c index 2abf8deba6a..792fdfbb716 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/central/main/bluedroid/central.c +++ b/examples/bluetooth/esp_ble_audio/tmap/central/main/bluedroid/central.c @@ -28,11 +28,6 @@ static esp_bt_status_t scan_op_status; #define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, scan_sem, portMAX_DELAY, scan_op_status) -/* Cached peer address. Bluedroid's pairing and disconnect APIs key off - * bd_addr rather than conn_handle, so we stash the addr at conn_create - * time and reuse it in pairing_start / security_failed_recover. */ -static esp_bd_addr_t peer_bda; - static esp_ble_ext_scan_params_t ext_scan_params = { .own_addr_type = BLE_ADDR_TYPE_PUBLIC, .filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, @@ -138,6 +133,9 @@ int conn_create(uint8_t addr_type, const uint8_t addr[6]) esp_gatt_if_t gattc_if; esp_err_t err; + esp_bd_addr_t peer_bda; + + /* Local copy: the Bluedroid APIs below take a non-const esp_bd_addr_t. */ memcpy(peer_bda, addr, sizeof(peer_bda)); err = esp_ble_gap_prefer_ext_connect_params_set( @@ -165,8 +163,14 @@ int conn_create(uint8_t addr_type, const uint8_t addr[6]) int pairing_start(uint16_t conn_handle) { - (void)conn_handle; - return esp_ble_set_encryption(peer_bda, ESP_BLE_SEC_ENCRYPT_NO_MITM); + const uint8_t *addr = set_member_addr(conn_handle); + + if (addr == NULL) { + ESP_LOGE(TAG, "No address for handle %u; not starting security", conn_handle); + return ESP_ERR_INVALID_STATE; + } + + return esp_ble_set_encryption((uint8_t *)addr, ESP_BLE_SEC_ENCRYPT_NO_MITM); } int exchange_mtu(uint16_t conn_handle) @@ -181,13 +185,20 @@ int exchange_mtu(uint16_t conn_handle) void security_failed_recover(uint16_t conn_handle, uint8_t status) { - (void)conn_handle; + const uint8_t *addr = set_member_addr(conn_handle); + + if (addr == NULL) { + ESP_LOGE(TAG, "Security change failed on handle %u, status %u; member already gone", + conn_handle, status); + return; + } /* Asymmetric bond state: we still hold an LTK for this peer but it * cleared its side, so encrypt-with-cached-key times out. Drop the bond * and tear down the link; the next reconnect runs fresh pairing. */ - ESP_LOGE(TAG, "Security change failed, status %u, clearing local bond and reconnecting", status); + ESP_LOGE(TAG, "Security change failed on handle %u, status %u, clearing local bond and reconnecting", + conn_handle, status); - esp_ble_remove_bond_device(peer_bda); - esp_ble_gap_disconnect(peer_bda); + esp_ble_remove_bond_device((uint8_t *)addr); + esp_ble_gap_disconnect((uint8_t *)addr); } 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 3bb47dc307d..265790c037c 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 @@ -10,27 +10,140 @@ #include #include #include +#include #include "esp_ble_audio_bap_lc3_preset_defs.h" #include "tmap_central.h" -static esp_ble_audio_cap_stream_t unicast_sink_streams[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT]; -static esp_ble_audio_cap_stream_t unicast_source_streams[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT]; -static esp_ble_audio_bap_ep_t *unicast_sink_eps[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT]; -static esp_ble_audio_bap_ep_t *unicast_source_eps[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT]; +/* ASE ids repeat across peripherals: an endpoint only means something with its + * connection. */ +struct peer_eps { + esp_ble_conn_t *conn; + uint16_t conn_handle; + uint8_t sink_ep_count; + esp_ble_audio_bap_ep_t *sink_eps[TMAP_CEN_SINK_EP_MAX]; + esp_ble_audio_bap_ep_t *source_ep; + /* PACS Sink Audio Locations, i.e. which ear this peripheral actually is. */ + esp_ble_audio_location_t sink_loc; + /* CSIS instance inside the peer's CAS, NULL when it is not a set member. */ + esp_ble_audio_csip_set_coordinator_csis_inst_t *csis_inst; +}; + +static struct peer_eps peers[TMAP_CEN_PEER_COUNT]; + +/* Which CIS carries which endpoint — the only place the topologies differ. */ +struct cis_binding { + uint8_t peer; /* index into peers[], ordered by set rank */ + uint8_t sink_idx; /* which sink endpoint of that peer */ + bool with_rx; /* pair the peer's source endpoint into the same CIS */ +}; + +static const struct cis_binding bindings[TMAP_CEN_CIS_COUNT] = { + { .peer = 0, .sink_idx = 0, .with_rx = true }, +#if CONFIG_EXAMPLE_TMAP_CEN_TOPO_1P_2CIS + { .peer = 0, .sink_idx = 1, .with_rx = false }, +#elif CONFIG_EXAMPLE_TMAP_CEN_TOPO_2P_2CIS + { .peer = 1, .sink_idx = 0, .with_rx = false }, +#endif +}; + +static esp_ble_audio_cap_stream_t unicast_sink_streams[TMAP_CEN_CIS_COUNT]; +static esp_ble_audio_cap_stream_t unicast_source_streams[1]; static esp_ble_audio_cap_unicast_group_t *unicast_group; -static example_audio_tx_scheduler_t tx_scheduler; -static uint16_t tx_seq_num; -static uint8_t *iso_data; +/* Counts the discoveries this round actually dispatched, so a member that kept + * its endpoints through a partial teardown is not waited for again. */ +static size_t discover_pending; -static void unicast_audio_tx(void); +/* Per stream: a shared sequence number jumps by the stream count. */ +struct sink_tx { + example_audio_tx_scheduler_t scheduler; + uint16_t seq_num; + uint8_t *data; + uint8_t index; + bool running; +}; -ESP_BLE_AUDIO_BAP_LC3_UNICAST_PRESET_48_2_1_DEFINE(unicast_preset_48_2_1, +static struct sink_tx sink_tx[TMAP_CEN_CIS_COUNT]; + +/* Only CIS #0 is bidirectional, so one RX metrics block is enough. */ +static example_audio_rx_metrics_t rx_metrics; + +static void unicast_audio_tx(struct sink_tx *tx); + +static void peer_discovery_done(void); + +/* One preset per rendered channel; only the channel allocation differs. */ +ESP_BLE_AUDIO_BAP_LC3_UNICAST_PRESET_48_2_1_DEFINE(unicast_preset_mono, + ESP_BLE_AUDIO_LOCATION_MONO_AUDIO, + ESP_BLE_AUDIO_CONTEXT_TYPE_MEDIA); +ESP_BLE_AUDIO_BAP_LC3_UNICAST_PRESET_48_2_1_DEFINE(unicast_preset_left, ESP_BLE_AUDIO_LOCATION_FRONT_LEFT, ESP_BLE_AUDIO_CONTEXT_TYPE_MEDIA); +ESP_BLE_AUDIO_BAP_LC3_UNICAST_PRESET_48_2_1_DEFINE(unicast_preset_right, + ESP_BLE_AUDIO_LOCATION_FRONT_RIGHT, + ESP_BLE_AUDIO_CONTEXT_TYPE_MEDIA); + +/* How many sink streams this binding's peer carries, and which of them this is. */ +static void binding_sink_position(size_t binding, size_t *nth, size_t *total) +{ + *nth = 0; + *total = 0; + + for (size_t i = 0; i < ARRAY_SIZE(bindings); i++) { + if (bindings[i].peer != bindings[binding].peer) { + continue; + } + + if (i < binding) { + (*nth)++; + } + + (*total)++; + } +} + +/* Channel follows the peer's PACS Sink Audio Locations — rank says nothing about + * which ear a device is. Two streams on one peer still split L/R. */ +static esp_ble_audio_bap_lc3_preset_t *sink_preset(size_t binding) +{ + esp_ble_audio_location_t loc = peers[bindings[binding].peer].sink_loc; + size_t nth; + size_t total; + + binding_sink_position(binding, &nth, &total); + + if (total > 1) { + return (nth == 0) ? &unicast_preset_left : &unicast_preset_right; + } + + if (loc == ESP_BLE_AUDIO_LOCATION_FRONT_LEFT) { + return &unicast_preset_left; + } + + if (loc == ESP_BLE_AUDIO_LOCATION_FRONT_RIGHT) { + return &unicast_preset_right; + } + + return &unicast_preset_mono; +} + +static const char *sink_channel_str(size_t binding) +{ + const esp_ble_audio_bap_lc3_preset_t *preset = sink_preset(binding); + + if (preset == &unicast_preset_left) { + return "front left"; + } + + if (preset == &unicast_preset_right) { + return "front right"; + } + + return "mono"; +} static const char *dir_str(esp_ble_audio_dir_t dir) { @@ -67,6 +180,160 @@ static int stream_index(const esp_ble_audio_bap_stream_t *stream) return -1; } +static struct sink_tx *sink_tx_of(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 &sink_tx[i]; + } + } + + return NULL; +} + +/* Still attached to an endpoint that has not gone back to idle. Streams of a + * peer that dropped are released by the stack before its disconnect is + * reported, so this only ever picks out the ones still worth stopping. */ +static bool stream_is_live(const esp_ble_audio_cap_stream_t *cap_stream) +{ + const esp_ble_audio_bap_stream_t *bap_stream = &cap_stream->bap_stream; + esp_ble_audio_bap_ep_info_t ep_info = {0}; + + if (bap_stream->conn == NULL || bap_stream->ep == NULL) { + return false; + } + + if (esp_ble_audio_bap_ep_get_info(bap_stream->ep, &ep_info) != 0) { + return false; + } + + return ep_info.state != ESP_BLE_AUDIO_BAP_EP_STATE_IDLE; +} + +/* Handle 0 is valid, so free slots are CONN_HANDLE_INIT rather than zeroed. */ +static void peers_reset(void) +{ + memset(peers, 0, sizeof(peers)); + + for (size_t i = 0; i < ARRAY_SIZE(peers); i++) { + peers[i].conn_handle = CONN_HANDLE_INIT; + } +} + +static struct peer_eps *peer_by_handle(uint16_t conn_handle) +{ + if (conn_handle == CONN_HANDLE_INIT) { + return NULL; + } + + for (size_t i = 0; i < ARRAY_SIZE(peers); i++) { + if (peers[i].conn_handle == conn_handle) { + return &peers[i]; + } + } + + return NULL; +} + +/* Everything recorded for a peer dies with its connection: the endpoints, the + * conn reference and the CSIS instance are all owned by the link. */ +void peer_forget(uint16_t conn_handle) +{ + struct peer_eps *peer = peer_by_handle(conn_handle); + + if (peer == NULL) { + return; + } + + memset(peer, 0, sizeof(*peer)); + peer->conn_handle = CONN_HANDLE_INIT; +} + +/* A peer that can be streamed on as-is, so a rebuild does not rediscover it. */ +static bool peer_is_ready(const struct peer_eps *peer) +{ + return peer != NULL && peer->conn != NULL && peer->sink_ep_count > 0; +} + +/* Re-seat into the rank order main.c settled on; collection is by handle. */ +static void peers_reorder(void) +{ + struct peer_eps ordered[TMAP_CEN_PEER_COUNT]; + + memset(ordered, 0, sizeof(ordered)); + + for (size_t i = 0; i < ARRAY_SIZE(ordered); i++) { + struct peer_eps *peer = peer_by_handle(set_member_handle(i)); + + if (peer != NULL) { + ordered[i] = *peer; + } else { + ordered[i].conn_handle = CONN_HANDLE_INIT; + } + } + + memcpy(peers, ordered, sizeof(peers)); +} + +/* CSIP once every member exposes a CSIS instance; ad-hoc is the only form that + * works without one. Both resolve to the same connections. */ +esp_ble_audio_cap_set_type_t set_type_get(void) +{ + size_t count = set_member_count(); + + if (count == 0) { + return ESP_BLE_AUDIO_CAP_SET_TYPE_AD_HOC; + } + + for (size_t i = 0; i < count; i++) { + const struct peer_eps *peer = peer_by_handle(set_member_handle(i)); + + if (peer == NULL || peer->csis_inst == NULL) { + return ESP_BLE_AUDIO_CAP_SET_TYPE_AD_HOC; + } + } + + return ESP_BLE_AUDIO_CAP_SET_TYPE_CSIP; +} + +bool set_member_fill(esp_ble_audio_cap_set_member_t *member, + esp_ble_audio_cap_set_type_t type, + uint16_t conn_handle) +{ + const struct peer_eps *peer = peer_by_handle(conn_handle); + + if (peer == NULL) { + return false; + } + + if (type == ESP_BLE_AUDIO_CAP_SET_TYPE_CSIP) { + member->csip = peer->csis_inst; + return member->csip != NULL; + } + + member->member = peer->conn; + + return member->member != NULL; +} + +static struct peer_eps *peer_slot_alloc(uint16_t conn_handle) +{ + struct peer_eps *peer = peer_by_handle(conn_handle); + + if (peer != NULL) { + return peer; + } + + for (size_t i = 0; i < ARRAY_SIZE(peers); i++) { + if (peers[i].conn_handle == CONN_HANDLE_INIT) { + peers[i].conn_handle = conn_handle; + return &peers[i]; + } + } + + return NULL; +} + static void unicast_stream_configured_cb(esp_ble_audio_bap_stream_t *stream, const esp_ble_audio_bap_qos_cfg_pref_t *pref) { @@ -89,51 +356,49 @@ static void unicast_stream_enabled_cb(esp_ble_audio_bap_stream_t *stream) static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream) { - esp_ble_audio_bap_ep_info_t ep_info = {0}; + struct sink_tx *tx; esp_err_t err; 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) { - ESP_LOGE(TAG, "Failed to get ep info, err %d", err); + tx = sink_tx_of(stream); + if (tx == NULL) { + /* Source stream: the peripheral transmits, we only count. */ + example_audio_rx_metrics_reset(&rx_metrics); return; } - /* Note: - * For a BAP Unicast Client, this is considered outgoing audio (TX). - */ - if (ep_info.dir == ESP_BLE_AUDIO_DIR_SINK) { - if (stream->qos == NULL || stream->qos->sdu == 0) { - ESP_LOGE(TAG, "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); - return; - } - } - - assert(stream->qos->interval); - - ESP_LOGI(TAG, "Streaming, interval %u, length %u", stream->qos->interval, stream->qos->sdu); - - tx_seq_num = 0; - example_audio_tx_scheduler_reset(&tx_scheduler); - - /* 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); - return; - } - - unicast_audio_tx(); + if (stream->qos == NULL || stream->qos->sdu == 0 || stream->qos->interval == 0) { + ESP_LOGE(TAG, "[SNK #%u] Invalid stream qos", tx->index); + return; } + + if (tx->data == NULL) { + tx->data = calloc(1, stream->qos->sdu); + if (tx->data == NULL) { + ESP_LOGE(TAG, "[SNK #%u] Failed to alloc TX buffer, SDU %u", + tx->index, stream->qos->sdu); + return; + } + } + + ESP_LOGI(TAG, "[SNK #%u] Streaming, interval %u, length %u", + tx->index, stream->qos->interval, stream->qos->sdu); + + tx->seq_num = 0; + example_audio_tx_scheduler_reset(&tx->scheduler); + + /* Note: esp timer is not accurate enough */ + err = example_audio_tx_scheduler_start(&tx->scheduler, stream->qos->interval); + if (err) { + ESP_LOGE(TAG, "[SNK #%u] Failed to start tx scheduler, err %d", tx->index, err); + return; + } + + tx->running = true; + + unicast_audio_tx(tx); } static void unicast_stream_metadata_updated_cb(esp_ble_audio_bap_stream_t *stream) @@ -148,31 +413,37 @@ static void unicast_stream_disabled_cb(esp_ble_audio_bap_stream_t *stream) stream_dir_str(stream), stream_index(stream)); } -static void unicast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason) +/* Keyed on the stream, not the endpoint state: the ISO disconnected callback runs + * before the endpoint leaves streaming, so a state query would still match. */ +static void sink_tx_stop(esp_ble_audio_bap_stream_t *stream) { - esp_ble_audio_bap_ep_info_t ep_info = {0}; + struct sink_tx *tx; esp_err_t err; - 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) { - ESP_LOGE(TAG, "Failed to get ep info, err %d", err); + tx = sink_tx_of(stream); + if (tx == NULL || !tx->running) { return; } - if (ep_info.dir == ESP_BLE_AUDIO_DIR_SINK) { - err = example_audio_tx_scheduler_stop(&tx_scheduler); - if (err) { - ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err); - } - - if (iso_data != NULL) { - free(iso_data); - iso_data = NULL; - } + err = example_audio_tx_scheduler_stop(&tx->scheduler); + if (err) { + ESP_LOGE(TAG, "[SNK #%u] Failed to stop tx scheduler, err %d", tx->index, err); } + + tx->running = false; + + if (tx->data != NULL) { + free(tx->data); + tx->data = NULL; + } +} + +static void unicast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason) +{ + ESP_LOGI(TAG, "[%s #%d] Stream stopped, reason 0x%02x", + stream_dir_str(stream), stream_index(stream), reason); + + sink_tx_stop(stream); } static void unicast_stream_released_cb(esp_ble_audio_bap_stream_t *stream) @@ -181,13 +452,31 @@ static void unicast_stream_released_cb(esp_ble_audio_bap_stream_t *stream) stream_dir_str(stream), stream_index(stream)); } -static void unicast_stream_sent_cb(esp_ble_audio_bap_stream_t *stream, void *user_data) +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)); - example_audio_tx_scheduler_on_sent(&tx_scheduler, user_data, TAG, name); + rx_metrics.last_sdu_len = len; + 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) +{ + struct sink_tx *tx; + char name[24]; + + tx = sink_tx_of(stream); + if (tx == NULL) { + return; + } + + 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) @@ -198,29 +487,10 @@ static void unicast_stream_connected_cb(esp_ble_audio_bap_stream_t *stream) static void unicast_stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason) { - esp_ble_audio_bap_ep_info_t ep_info = {0}; - esp_err_t err; - 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) { - ESP_LOGE(TAG, "Failed to get ep info, err %d", err); - return; - } - - if (ep_info.dir == ESP_BLE_AUDIO_DIR_SINK) { - err = example_audio_tx_scheduler_stop(&tx_scheduler); - if (err) { - ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err); - } - - if (iso_data != NULL) { - free(iso_data); - iso_data = NULL; - } - } + sink_tx_stop(stream); } static esp_ble_audio_bap_stream_ops_t unicast_stream_ops = { @@ -232,6 +502,7 @@ static esp_ble_audio_bap_stream_ops_t unicast_stream_ops = { .disabled = unicast_stream_disabled_cb, .stopped = unicast_stream_stopped_cb, .released = unicast_stream_released_cb, + .recv = unicast_stream_recv_cb, .sent = unicast_stream_sent_cb, .connected = unicast_stream_connected_cb, .disconnected = unicast_stream_disconnected_cb, @@ -241,30 +512,33 @@ static void unicast_discovery_complete_cb(esp_ble_conn_t *conn, int err, const esp_ble_audio_csip_set_coordinator_set_member_t *member, const esp_ble_audio_csip_set_coordinator_csis_inst_t *csis_inst) { + struct peer_eps *peer; + if (err) { ESP_LOGE(TAG, "Unicast discovery completed, err %d", err); + peer_discovery_done(); return; } - if (default_conn_handle_get() != conn->handle) { - ESP_LOGE(TAG, "%s, not connected", __func__); - return; - } - - if (IS_ENABLED(CONFIG_BT_CAP_ACCEPTOR_SET_MEMBER)) { - if (csis_inst == NULL) { - ESP_LOGW(TAG, "Failed to discover CAS CSIS"); - return; - } - - ESP_LOGI(TAG, "Found CAS with CSIS"); + if (csis_inst != NULL) { + ESP_LOGI(TAG, "Found CAS with CSIS: handle %u", conn->handle); + } else if (TMAP_CEN_PEER_COUNT > 1) { + ESP_LOGW(TAG, "Failed to discover CAS CSIS on handle %u", conn->handle); } else { - ESP_LOGI(TAG, "Found CAS"); + ESP_LOGI(TAG, "Found CAS: handle %u", conn->handle); + } + + /* Keep the CSIS instance: it is how CAP names a coordinated set member. */ + peer = peer_slot_alloc(conn->handle); + if (peer != NULL) { + peer->conn = conn; + peer->csis_inst = (esp_ble_audio_csip_set_coordinator_csis_inst_t *)csis_inst; } err = esp_ble_audio_bap_unicast_client_discover(conn->handle, ESP_BLE_AUDIO_DIR_SINK); if (err) { ESP_LOGE(TAG, "Failed to discover sink, err %d", err); + peer_discovery_done(); } } @@ -275,6 +549,15 @@ static void unicast_start_complete_cb(int err, esp_ble_conn_t *conn) } else { ESP_LOGI(TAG, "Unicast start completed"); } + + /* The lock covers the stream setup procedure, so give it back either way. */ + set_release_lock(); + + /* Latching this on a failed start would drive the volume of a set that + * never came up: the flag stays set until a disconnect clears it. */ + if (!err) { + vcp_vol_ctlr_streams_started(); + } } static void unicast_update_complete_cb(int err, esp_ble_conn_t *conn) @@ -304,24 +587,49 @@ static esp_ble_audio_cap_initiator_cb_t cap_cb = { static int unicast_group_create(void) { - esp_ble_audio_cap_unicast_group_stream_param_t group_stream_params = {0}; - esp_ble_audio_cap_unicast_group_stream_pair_param_t pair_params = {0}; + esp_ble_audio_cap_unicast_group_stream_param_t tx_params[TMAP_CEN_CIS_COUNT] = {0}; + esp_ble_audio_cap_unicast_group_stream_param_t rx_params[TMAP_CEN_CIS_COUNT] = {0}; + esp_ble_audio_cap_unicast_group_stream_pair_param_t pair_params[TMAP_CEN_CIS_COUNT] = {0}; esp_ble_audio_cap_unicast_group_param_t group_param = {0}; int err; + /* Reused rather than rebuilt: the CIG stays up while a member is away, so + * the one that comes back re-enters the slot it left. Releasing a stream + * keeps it in the group (bt_bap_stream_detach() leaves stream->group and + * stream->iso alone), and configuring it again rebinds the new endpoint to + * the CIS the slot already owns. */ if (unicast_group != NULL) { return 0; } - group_stream_params.qos_cfg = &unicast_preset_48_2_1.qos; - group_stream_params.stream = &unicast_sink_streams[0]; + /* One pair is one CIS. tx_param only -> TX-only CIS; both -> bidirectional, + * which only works for a sink and source on the same peripheral. */ + for (size_t i = 0; i < ARRAY_SIZE(bindings); i++) { + const struct peer_eps *peer = &peers[bindings[i].peer]; - pair_params.tx_param = &group_stream_params; - pair_params.rx_param = NULL; + if (bindings[i].sink_idx >= peer->sink_ep_count) { + ESP_LOGE(TAG, "[SNK #%zu] Peer %u has no sink endpoint %u", + i, bindings[i].peer, bindings[i].sink_idx); + return -ENODEV; + } + + ESP_LOGI(TAG, "[SNK #%zu] Peer %u (loc 0x%08lx) renders %s", + i, bindings[i].peer, peer->sink_loc, sink_channel_str(i)); + + tx_params[i].qos_cfg = &sink_preset(i)->qos; + tx_params[i].stream = &unicast_sink_streams[i]; + pair_params[i].tx_param = &tx_params[i]; + + if (bindings[i].with_rx && peer->source_ep != NULL) { + rx_params[i].qos_cfg = &unicast_preset_mono.qos; + rx_params[i].stream = &unicast_source_streams[0]; + pair_params[i].rx_param = &rx_params[i]; + } + } group_param.packing = ESP_BLE_ISO_PACKING_SEQUENTIAL; - group_param.params_count = 1; - group_param.params = &pair_params; + group_param.params_count = ARRAY_SIZE(pair_params); + group_param.params = pair_params; err = esp_ble_audio_cap_unicast_group_create(&group_param, &unicast_group); if (err) { @@ -329,62 +637,85 @@ static int unicast_group_create(void) return err; } - ESP_LOGI(TAG, "Created unicast group"); + ESP_LOGI(TAG, "Created unicast group: %u CIS", (unsigned)ARRAY_SIZE(pair_params)); return 0; } -int unicast_group_delete(void) +/* Nothing is connected any more, or the session is being abandoned. The CIG only + * ever goes away here: while a member is merely absent it stays up, carrying the + * others and holding that member's slot. */ +void cap_initiator_reset(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; - } + /* Endpoints go stale with the connection; stale slots warn "Unhandled endpoint". */ + peers_reset(); + + discover_pending = 0; if (unicast_group == NULL) { - return 0; + return; } + /* Every stream has to be released first, which the departing links did. */ err = esp_ble_audio_cap_unicast_group_delete(unicast_group); if (err) { ESP_LOGE(TAG, "Failed to delete unicast group, err %d", err); - return err; + return; } unicast_group = NULL; ESP_LOGI(TAG, "Deleted unicast group"); - - return 0; } -static int unicast_audio_start(esp_ble_conn_t *conn) +static int unicast_audio_start(void) { - esp_ble_audio_cap_unicast_audio_start_stream_param_t stream_param = {0}; + esp_ble_audio_cap_unicast_audio_start_stream_param_t stream_params[TMAP_CEN_CIS_COUNT + 1] = {0}; esp_ble_audio_cap_unicast_audio_start_param_t param = {0}; int err; - if (unicast_sink_eps[0] == NULL) { - ESP_LOGE(TAG, "No sink endpoint available"); - return -ENODEV; + param.type = set_type_get(); + + /* Endpoint and connection must match, or CAP reports an ep conn mismatch. */ + for (size_t i = 0; i < ARRAY_SIZE(bindings); i++) { + const struct peer_eps *peer = &peers[bindings[i].peer]; + + /* A member that never left is still on air in this slot. Its endpoint + * would be rejected as not startable, and restarting it would break + * the audio this whole path exists to preserve. */ + if (stream_is_live(&unicast_sink_streams[i])) { + ESP_LOGI(TAG, "[SNK #%zu] Already streaming, left alone", i); + continue; + } + + if (!set_member_fill(&stream_params[param.count].member, param.type, + peer->conn_handle)) { + ESP_LOGE(TAG, "No set member for handle %u", peer->conn_handle); + return -ENOENT; + } + + stream_params[param.count].stream = &unicast_sink_streams[i]; + stream_params[param.count].ep = peer->sink_eps[bindings[i].sink_idx]; + stream_params[param.count].codec_cfg = &sink_preset(i)->codec_cfg; + param.count++; + + if (bindings[i].with_rx && peer->source_ep != NULL) { + stream_params[param.count].member = stream_params[param.count - 1].member; + stream_params[param.count].stream = &unicast_source_streams[0]; + stream_params[param.count].ep = peer->source_ep; + stream_params[param.count].codec_cfg = &unicast_preset_mono.codec_cfg; + param.count++; + } } - stream_param.member.member = conn; - stream_param.stream = &unicast_sink_streams[0]; - stream_param.ep = unicast_sink_eps[0]; - stream_param.codec_cfg = &unicast_preset_48_2_1.codec_cfg; + if (param.count == 0) { + ESP_LOGW(TAG, "Nothing left to start, every stream is already up"); + return -EALREADY; + } - param.type = ESP_BLE_AUDIO_CAP_SET_TYPE_AD_HOC; - param.count = 1; - param.stream_params = &stream_param; + param.stream_params = stream_params; err = esp_ble_audio_cap_initiator_unicast_audio_start(¶m); if (err) { @@ -392,7 +723,8 @@ static int unicast_audio_start(esp_ble_conn_t *conn) return err; } - ESP_LOGI(TAG, "Started unicast audio"); + ESP_LOGI(TAG, "Started %u unicast stream(s) as %s set", (unsigned)param.count, + (param.type == ESP_BLE_AUDIO_CAP_SET_TYPE_CSIP) ? "a coordinated" : "an ad-hoc"); return 0; } @@ -401,7 +733,20 @@ 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); + struct peer_eps *peer; + + ESP_LOGI(TAG, "Location, handle %u dir %u loc 0x%08lx", conn->handle, dir, loc); + + if (dir != ESP_BLE_AUDIO_DIR_SINK) { + return; + } + + /* Remember which ear this peripheral is; it decides the channel allocation. */ + peer = peer_slot_alloc(conn->handle); + if (peer != NULL) { + peer->conn = conn; + peer->sink_loc = loc; + } } static void available_contexts_cb(esp_ble_conn_t *conn, @@ -475,40 +820,125 @@ static void release_cb(esp_ble_audio_bap_stream_t *stream, stream_dir_str(stream), stream_index(stream), rsp_code, reason); } +/* peers_reorder() must not move a member that is still on air into another + * slot: bindings[] would then aim its live CIS at the wrong peripheral. Rank + * ordering is stable, so this only ever fires on an unranked set. */ +static bool live_streams_match_peers(void) +{ + for (size_t i = 0; i < ARRAY_SIZE(bindings); i++) { + const struct peer_eps *peer = &peers[bindings[i].peer]; + + if (stream_is_live(&unicast_sink_streams[i]) && + unicast_sink_streams[i].bap_stream.conn != peer->conn) { + ESP_LOGE(TAG, "[SNK #%zu] Streaming member moved slots, cannot rebuild", i); + return false; + } + + if (!bindings[i].with_rx) { + continue; + } + + if (stream_is_live(&unicast_source_streams[0]) && + unicast_source_streams[0].bap_stream.conn != peer->conn) { + ESP_LOGE(TAG, "[SRC #0] Streaming member moved slots, cannot rebuild"); + return false; + } + } + + return true; +} + +/* One CIG covers every peripheral, so it is filled only once the last + * outstanding discovery is in. The set lock is held until the start procedure + * reports back, so every failure that returns early has to give it back. */ +static void peers_ready(void) +{ + /* A member can go away between cap_initiator_setup() dispatching the + * discoveries and the last one reporting back, leaving bindings[] pointing + * at an empty slot. The next member event runs this again. */ + if (set_member_count() < ARRAY_SIZE(peers)) { + ESP_LOGW(TAG, "Set no longer whole, %zu of %u member(s); not starting", + set_member_count(), TMAP_CEN_PEER_COUNT); + set_release_lock(); + + return; + } + + peers_reorder(); + + if (!live_streams_match_peers()) { + set_release_lock(); + return; + } + + if (unicast_group_create() != 0) { + set_release_lock(); + return; + } + + /* The group is left in place on failure: a member still on air owns a CIS + * in it, and the next member event reuses it rather than rebuilding. */ + if (unicast_audio_start() != 0) { + set_release_lock(); + } +} + +/* Single exit for every peer, whether its discovery succeeded or not: a peer that + * contributed no endpoint just makes the group build fail instead of stalling the + * others. */ +static void peer_discovery_done(void) +{ + if (discover_pending > 0) { + discover_pending--; + } + + if (discover_pending > 0) { + return; + } + + peers_ready(); +} + static void discover_cb(esp_ble_conn_t *conn, int err, esp_ble_audio_dir_t dir) { - if (err) { - ESP_LOGE(TAG, "Discovery failed, err %d", err); + struct peer_eps *peer; + + peer = peer_slot_alloc(conn->handle); + if (peer == NULL) { return; } - if (default_conn_handle_get() != conn->handle) { - ESP_LOGE(TAG, "%s, not connected", __func__); - return; - } + peer->conn = conn; if (dir == ESP_BLE_AUDIO_DIR_SINK) { - ESP_LOGI(TAG, "Sink discover complete"); + if (err) { + ESP_LOGE(TAG, "Sink discovery failed on handle %u, err %d", conn->handle, err); + peer_discovery_done(); + return; + } + + ESP_LOGI(TAG, "Sink discover complete: handle %u", conn->handle); err = esp_ble_audio_bap_unicast_client_discover(conn->handle, ESP_BLE_AUDIO_DIR_SOURCE); if (err) { ESP_LOGE(TAG, "Failed to discover source, err %d", err); - return; - } - } else if (dir == ESP_BLE_AUDIO_DIR_SOURCE) { - ESP_LOGI(TAG, "Source discover complete"); - - err = unicast_group_create(); - if (err) { - return; + peer_discovery_done(); } - err = unicast_audio_start(conn); - if (err) { - unicast_group_delete(); - return; - } + return; } + + if (dir != ESP_BLE_AUDIO_DIR_SOURCE) { + return; + } + + if (err) { + ESP_LOGE(TAG, "Source discovery failed on handle %u, err %d", conn->handle, err); + } else { + ESP_LOGI(TAG, "Source discover complete: handle %u", conn->handle); + } + + peer_discovery_done(); } static void pac_record_cb(esp_ble_conn_t *conn, esp_ble_audio_dir_t dir, @@ -521,25 +951,38 @@ static void endpoint_cb(esp_ble_conn_t *conn, esp_ble_audio_dir_t dir, esp_ble_audio_bap_ep_t *ep) { - 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, "[SRC #%zu] Endpoint discovered", i); - unicast_source_eps[i] = ep; - return; - } - } - } 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, "[SNK #%zu] Endpoint discovered", i); - unicast_sink_eps[i] = ep; - return; - } - } + struct peer_eps *peer; + + /* Record every endpoint; bindings[] picks which ones are streamed on. */ + peer = peer_slot_alloc(conn->handle); + if (peer == NULL) { + ESP_LOGW(TAG, "[%s] Unhandled endpoint, peer table full", dir_str(dir)); + return; } - ESP_LOGW(TAG, "[%s] Unhandled endpoint", dir_str(dir)); + peer->conn = conn; + + if (dir == ESP_BLE_AUDIO_DIR_SOURCE) { + if (peer->source_ep == NULL) { + ESP_LOGI(TAG, "[%s #0] Endpoint discovered on handle %u", + dir_str(dir), conn->handle); + peer->source_ep = ep; + return; + } + } else if (dir == ESP_BLE_AUDIO_DIR_SINK) { + if (peer->sink_ep_count < ARRAY_SIZE(peer->sink_eps)) { + ESP_LOGI(TAG, "[%s #%u] Endpoint discovered on handle %u", + dir_str(dir), peer->sink_ep_count, conn->handle); + peer->sink_eps[peer->sink_ep_count++] = ep; + return; + } + } else { + ESP_LOGW(TAG, "Endpoint with unknown dir %u on handle %u", dir, conn->handle); + return; + } + + /* A peer may expose more ASEs than this topology streams on. */ + ESP_LOGI(TAG, "[%s] Spare endpoint on handle %u", dir_str(dir), conn->handle); } static esp_ble_audio_bap_unicast_client_cb_t unicast_client_cbs = { @@ -558,15 +1001,15 @@ static esp_ble_audio_bap_unicast_client_cb_t unicast_client_cbs = { .discover = discover_cb, }; -static void unicast_audio_tx(void) +static void unicast_audio_tx(struct sink_tx *tx) { esp_ble_audio_bap_ep_info_t ep_info = {0}; esp_ble_audio_cap_stream_t *cap_stream; esp_ble_audio_bap_stream_t *bap_stream; esp_err_t err; - cap_stream = &unicast_sink_streams[0]; - bap_stream = &unicast_sink_streams[0].bap_stream; + cap_stream = &unicast_sink_streams[tx->index]; + bap_stream = &cap_stream->bap_stream; if (bap_stream->ep == NULL) { return; @@ -582,38 +1025,41 @@ static void unicast_audio_tx(void) } if (bap_stream->qos == NULL || bap_stream->qos->sdu == 0) { - ESP_LOGE(TAG, "Invalid stream qos"); + ESP_LOGE(TAG, "[SNK #%u] Invalid stream qos", tx->index); return; } - if (iso_data == NULL) { - ESP_LOGE(TAG, "TX buffer unavailable, SDU %u", bap_stream->qos->sdu); + if (tx->data == NULL) { + ESP_LOGE(TAG, "[SNK #%u] TX buffer unavailable, SDU %u", + tx->index, bap_stream->qos->sdu); return; } - memset(iso_data, (uint8_t)tx_seq_num, bap_stream->qos->sdu); + memset(tx->data, (uint8_t)tx->seq_num, bap_stream->qos->sdu); - err = esp_ble_audio_cap_stream_send(cap_stream, iso_data, + err = esp_ble_audio_cap_stream_send(cap_stream, tx->data, bap_stream->qos->sdu, - tx_seq_num); + tx->seq_num); if (err) { - ESP_LOGD(TAG, "Failed to transmit data on streams, err %d", err); + /* Backpressure is normal; a stuck stream shows up as its TX count stalling. */ return; } - tx_seq_num++; + tx->seq_num++; } +/* Each stream has its own timer, so this only ever feeds its own stream. */ static void tx_scheduler_cb(void *arg) { - (void)arg; - unicast_audio_tx(); + unicast_audio_tx(arg); } int cap_initiator_init(void) { int err; + peers_reset(); + err = esp_ble_audio_cap_initiator_register_cb(&cap_cb); if (err) { ESP_LOGE(TAG, "Failed to register CAP callbacks (err %d)", err); @@ -628,43 +1074,68 @@ int cap_initiator_init(void) for (size_t i = 0; i < ARRAY_SIZE(unicast_sink_streams); i++) { esp_ble_audio_cap_stream_ops_register(&unicast_sink_streams[i], &unicast_stream_ops); + + /* Own timer per stream, fed with its own context. */ + sink_tx[i].index = (uint8_t)i; + + err = example_audio_tx_scheduler_init(&sink_tx[i].scheduler, + tx_scheduler_cb, + &sink_tx[i]); + if (err) { + ESP_LOGE(TAG, "Failed to init tx scheduler[%zu], err %d", i, err); + return err; + } } for (size_t i = 0; i < ARRAY_SIZE(unicast_source_streams); i++) { esp_ble_audio_cap_stream_ops_register(&unicast_source_streams[i], &unicast_stream_ops); } - err = example_audio_tx_scheduler_init(&tx_scheduler, - tx_scheduler_cb, - NULL); - if (err) { - ESP_LOGE(TAG, "Failed to init tx scheduler, err %d", err); - return err; - } - - ESP_LOGI(TAG, "CAP initiator initialized"); + ESP_LOGI(TAG, "CAP initiator initialized: %u peer(s), %u CIS", + TMAP_CEN_PEER_COUNT, TMAP_CEN_CIS_COUNT); return 0; } int cap_initiator_setup(void) { - uint16_t conn_handle; + size_t count; int err; - conn_handle = default_conn_handle_get(); - if (conn_handle == CONN_HANDLE_INIT) { + count = set_member_count(); + if (count == 0) { ESP_LOGE(TAG, "%s, not connected", __func__); return -ENOTCONN; } - err = esp_ble_audio_cap_initiator_unicast_discover(conn_handle); - if (err) { - ESP_LOGE(TAG, "Failed to discover CAS, err %d", err); - return err; + discover_pending = 0; + + /* Per member; the group is built when the last one reports back. A member + * that kept its link through a partial teardown kept its endpoints too, so + * only the one that came back is rediscovered. */ + for (size_t i = 0; i < count; i++) { + uint16_t conn_handle = set_member_handle(i); + + if (peer_is_ready(peer_by_handle(conn_handle))) { + continue; + } + + err = esp_ble_audio_cap_initiator_unicast_discover(conn_handle); + if (err) { + ESP_LOGE(TAG, "Failed to discover CAS on handle %u, err %d", conn_handle, err); + return err; + } + + discover_pending++; } - ESP_LOGI(TAG, "CAP initiator setup"); + ESP_LOGI(TAG, "CAP initiator setup for %zu member(s), %zu to discover", + count, discover_pending); + + /* Everything was already discovered, so nothing will report back. */ + if (discover_pending == 0) { + peers_ready(); + } return 0; } diff --git a/examples/bluetooth/esp_ble_audio/tmap/central/main/csip_set_coordinator.c b/examples/bluetooth/esp_ble_audio/tmap/central/main/csip_set_coordinator.c new file mode 100644 index 00000000000..12c07c350fc --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/tmap/central/main/csip_set_coordinator.c @@ -0,0 +1,187 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "tmap_central.h" + +#if TMAP_CEN_PEER_COUNT > 1 + +/* Must match the SIRK the peripherals register (peripheral/main/csip_set_member.c). */ +#define CSIP_SIRK_DEBUG { 0xcd, 0xcc, 0x72, 0xdd, 0x86, 0x8c, 0xcd, 0xce, \ + 0x22, 0xfd, 0xa1, 0x21, 0x09, 0x7d, 0x7d, 0x45 } + +static const uint8_t set_sirk[ESP_BLE_AUDIO_CSIP_SIRK_SIZE] = CSIP_SIRK_DEBUG; + +/* Look up by the caller's handles: the library keys on the controller-assigned + * ACL handle, not a slot number. */ +static uint8_t collect_members(const uint16_t *conn_handles, size_t count, + const esp_ble_audio_csip_set_coordinator_set_member_t *out[]) +{ + uint8_t found = 0; + + for (size_t i = 0; i < count; i++) { + const esp_ble_audio_csip_set_coordinator_set_member_t *member = + esp_ble_audio_csip_set_coordinator_set_member_by_conn(conn_handles[i]); + + if (member != NULL) { + out[found++] = member; + } + } + + return found; +} + +static void csip_discover_cb(struct bt_conn *conn, + const esp_ble_audio_csip_set_coordinator_set_member_t *member, + int err, size_t set_count) +{ + uint16_t conn_handle = (conn != NULL) ? conn->handle : CONN_HANDLE_INIT; + + if (err) { + ESP_LOGE(TAG, "CSIP discovery failed on handle %u, err %d", conn_handle, err); + set_member_discovered(conn_handle, err); + return; + } + + if (set_count == 0 || member == NULL) { + ESP_LOGE(TAG, "No coordinated set on handle %u", conn_handle); + set_member_discovered(conn_handle, -ENOENT); + return; + } + + ESP_LOGI(TAG, "CSIP discovered: handle %u set_size %u rank %u lockable %u", + conn_handle, member->insts[0].info.set_size, + member->insts[0].info.rank, member->insts[0].info.lockable); + + if (member->insts[0].info.set_size != TMAP_CEN_PEER_COUNT) { + ESP_LOGW(TAG, "Peer reports set size %u, this build drives %u member(s)", + member->insts[0].info.set_size, TMAP_CEN_PEER_COUNT); + } + + set_member_discovered(conn_handle, 0); +} + +static void csip_lock_set_cb(int err) +{ + /* A failure is reported by set_lock_complete(), which also says what it + * does about it. */ + if (!err) { + ESP_LOGI(TAG, "Set locked"); + } + + set_lock_complete(err); +} + +static void csip_release_set_cb(int err) +{ + if (err) { + ESP_LOGE(TAG, "Failed to release set, err %d", err); + } else { + ESP_LOGI(TAG, "Set released"); + } +} + +static esp_ble_audio_csip_set_coordinator_cb_t csip_cb = { + .discover = csip_discover_cb, + .lock_set = csip_lock_set_cb, + .release_set = csip_release_set_cb, +}; + +int csip_set_coordinator_init(void) +{ + int err; + + err = esp_ble_audio_csip_set_coordinator_register_cb(&csip_cb); + if (err) { + ESP_LOGE(TAG, "Failed to register csip callbacks, err %d", err); + return err; + } + + ESP_LOGI(TAG, "CSIP set coordinator initialized"); + + return 0; +} + +bool csip_adv_is_set_member(uint8_t ad_type, const uint8_t *data, uint8_t data_len) +{ + return esp_ble_audio_csip_set_coordinator_is_set_member(set_sirk, ad_type, data, data_len); +} + +int csip_discover_start(uint16_t conn_handle) +{ + int err; + + err = esp_ble_audio_csip_set_coordinator_discover(conn_handle); + if (err) { + ESP_LOGE(TAG, "Failed to start csip discovery on handle %u, err %d", conn_handle, err); + return err; + } + + return 0; +} + +uint8_t csip_member_rank_get(uint16_t conn_handle) +{ + const esp_ble_audio_csip_set_coordinator_set_member_t *member = + esp_ble_audio_csip_set_coordinator_set_member_by_conn(conn_handle); + + return (member != NULL) ? member->insts[0].info.rank : 0; +} + +int csip_lock_set(const uint16_t *conn_handles, size_t count) +{ + const esp_ble_audio_csip_set_coordinator_set_member_t *members[TMAP_CEN_PEER_COUNT]; + uint8_t found; + int err; + + if (conn_handles == NULL || count == 0 || count > ARRAY_SIZE(members)) { + return -EINVAL; + } + + found = collect_members(conn_handles, count, members); + if (found == 0) { + ESP_LOGE(TAG, "No set member to lock"); + return -ENOENT; + } + + /* The library locks ascending by rank, releases descending. */ + err = esp_ble_audio_csip_set_coordinator_lock(members, found, &members[0]->insts[0].info); + if (err) { + ESP_LOGE(TAG, "Failed to issue set lock, err %d", err); + return err; + } + + return 0; +} + +int csip_release_set(const uint16_t *conn_handles, size_t count) +{ + const esp_ble_audio_csip_set_coordinator_set_member_t *members[TMAP_CEN_PEER_COUNT]; + uint8_t found; + int err; + + if (conn_handles == NULL || count == 0 || count > ARRAY_SIZE(members)) { + return -EINVAL; + } + + found = collect_members(conn_handles, count, members); + if (found == 0) { + return -ENOENT; + } + + err = esp_ble_audio_csip_set_coordinator_release(members, found, &members[0]->insts[0].info); + if (err) { + ESP_LOGE(TAG, "Failed to issue set release, err %d", err); + return err; + } + + return 0; +} + +#endif /* TMAP_CEN_PEER_COUNT > 1 */ 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 40820630e56..fb3954d0462 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/central/main/main.c +++ b/examples/bluetooth/esp_ble_audio/tmap/central/main/main.c @@ -13,13 +13,128 @@ #include "tmap_central.h" -static uint16_t default_conn_handle = CONN_HANDLE_INIT; -static bool disc_completed; -static bool mtu_exchanged; +/* MTU exchange and service discovery complete independently, so both are tracked. + * set_discovered is per member rather than a count, so a member that keeps its + * link while another one is replaced does not have to be discovered again. */ +struct set_member { + uint16_t handle; + uint8_t dst[6]; + bool mtu_exchanged; + bool disc_completed; + bool set_discovered; +}; -uint16_t default_conn_handle_get(void) +static struct set_member members[TMAP_CEN_PEER_COUNT]; +static size_t member_count; + +/* One connect at a time. members[] only learns a peer at acl_connect, and + * ext_scan_stop() does not discard reports already queued for us, so without + * this a second report starts an overlapping conn_create that the stack + * rejects ("L2CAP - LE - cannot start new connection at conn st: 1"). */ +static bool conn_pending; + +/* Lock state: pending until the callback, held until stream setup completes. */ +static bool lock_pending; +static bool set_locked; + +size_t set_member_count(void) { - return default_conn_handle; + return member_count; +} + +uint16_t set_member_handle(size_t index) +{ + return (index < member_count) ? members[index].handle : CONN_HANDLE_INIT; +} + +const uint8_t *set_member_addr(uint16_t conn_handle) +{ + for (size_t i = 0; i < member_count; i++) { + if (members[i].handle == conn_handle) { + return members[i].dst; + } + } + + return NULL; +} + +static struct set_member *member_by_handle(uint16_t handle) +{ + for (size_t i = 0; i < member_count; i++) { + if (members[i].handle == handle) { + return &members[i]; + } + } + + return NULL; +} + +static void member_remove(uint16_t handle) +{ + for (size_t i = 0; i < member_count; i++) { + if (members[i].handle != handle) { + continue; + } + + for (size_t j = i + 1; j < member_count; j++) { + members[j - 1] = members[j]; + } + + member_count--; + memset(&members[member_count], 0, sizeof(members[member_count])); + + return; + } +} + +static bool member_is_known(const uint8_t addr[6]) +{ + for (size_t i = 0; i < member_count; i++) { + if (memcmp(members[i].dst, addr, sizeof(members[i].dst)) == 0) { + return true; + } + } + + return false; +} + +/* The library looks members up by handle, not by slot. */ +static const uint16_t *lock_handles(void) +{ + static uint16_t handles[TMAP_CEN_PEER_COUNT]; + + for (size_t i = 0; i < member_count; i++) { + handles[i] = members[i].handle; + } + + return handles; +} + +/* Index 0 must be the rank 1 device; unranked members keep connection order. */ +static void set_members_sort_by_rank(void) +{ + for (size_t i = 1; i < member_count; i++) { + struct set_member tmp = members[i]; + uint8_t rank = csip_member_rank_get(tmp.handle); + size_t j = i; + + if (rank == 0) { + continue; + } + + while (j > 0) { + uint8_t prev = csip_member_rank_get(members[j - 1].handle); + + if (prev != 0 && prev <= rank) { + break; + } + + members[j] = members[j - 1]; + j--; + } + + members[j] = tmp; + } } static void tmap_discovery_complete(esp_ble_audio_tmap_role_t role, @@ -30,32 +145,41 @@ static void tmap_discovery_complete(esp_ble_audio_tmap_role_t role, return; } - if (conn->handle != default_conn_handle) { + if (member_by_handle(conn->handle) == NULL) { return; } - ESP_LOGI(TAG, "TMAP discovery done"); - - err = cap_initiator_setup(); - if (err) { - return; - } + /* Audio setup waits for set_lock_complete(). */ + ESP_LOGI(TAG, "TMAP discovery done: handle %u", conn->handle); } static esp_ble_audio_tmap_cb_t tmap_callbacks = { .discovery_complete = tmap_discovery_complete, }; -static bool check_and_connect(uint8_t type, const uint8_t *data, +/* TMAP role and set identity are separate AD elements; decide after parsing. */ +struct adv_match { + bool tmap_umr; + bool rsi_present; + bool rsi_ours; +}; + +static bool collect_adv_match(uint8_t type, const uint8_t *data, uint8_t data_len, void *user_data) { - esp_ble_audio_gap_app_event_t *event; + struct adv_match *match; uint16_t tmap_role; uint16_t uuid_val; - int err; - event = user_data; - assert(event); + match = user_data; + assert(match); + + if (type == EXAMPLE_AD_TYPE_CSIS_RSI) { + /* Resolves the advertised RSI against our SIRK. */ + match->rsi_present = true; + match->rsi_ours = csip_adv_is_set_member(type, data, data_len); + return true; /* Continue parsing to next AD data type */ + } if (type != EXAMPLE_AD_TYPE_SERVICE_DATA16) { return true; /* Continue parsing to next AD data type */ @@ -73,24 +197,54 @@ static bool check_and_connect(uint8_t type, const uint8_t *data, return true; /* Continue parsing to next AD data type */ } - ESP_LOGI(TAG, "Found TMAS in peer adv data!"); - if (data_len < sizeof(uuid_val) + sizeof(tmap_role)) { ESP_LOGW(TAG, "Invalid ad size %u (tmap role)", data_len); - return false; /* Stop parsing */ + return true; /* Continue parsing to next AD data type */ } tmap_role = sys_get_le16(data + sizeof(uuid_val)); + match->tmap_umr = (tmap_role & ESP_BLE_AUDIO_TMAP_ROLE_UMR) != 0; - if ((tmap_role & ESP_BLE_AUDIO_TMAP_ROLE_UMR) == 0) { - ESP_LOGW(TAG, "No TMAS UMR support!"); - return false; /* Stop parsing */ + return true; /* Continue parsing to next AD data type */ +} + +static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event) +{ + struct adv_match match = {0}; + int err; + + if (member_count >= ARRAY_SIZE(members) || conn_pending) { + return; } + if ((event->ext_scan_recv.event_type & EXAMPLE_ADV_PROP_CONNECTABLE) == 0) { + return; + } + + if (member_is_known(event->ext_scan_recv.addr.val)) { + return; + } + + esp_ble_audio_data_parse(event->ext_scan_recv.data, + event->ext_scan_recv.data_len, + collect_adv_match, &match); + + if (!match.tmap_umr) { + return; + } + + /* A coordinated set build only accepts members carrying our SIRK. */ + if (TMAP_CEN_PEER_COUNT > 1 && !(match.rsi_present && match.rsi_ours)) { + return; + } + + ESP_LOGI(TAG, "Found peer %zu/%u (TMAS UMR%s)", member_count + 1, TMAP_CEN_PEER_COUNT, + match.rsi_present ? ", set member" : ""); + err = ext_scan_stop(); if (err) { ESP_LOGE(TAG, "Failed to stop scanning, err %d", err); - return false; + return; } err = conn_create(event->ext_scan_recv.addr.type, @@ -98,31 +252,21 @@ static bool check_and_connect(uint8_t type, const uint8_t *data, if (err) { ESP_LOGE(TAG, "Failed to create conn, err %d", err); ext_scan_start(); - } - - return false; /* Stop parsing */ -} - -static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event) -{ - if (default_conn_handle != CONN_HANDLE_INIT) { return; } - /* Check if the advertising is connectable and if TMAS is supported */ - if (event->ext_scan_recv.event_type & EXAMPLE_ADV_PROP_CONNECTABLE) { - esp_ble_audio_data_parse(event->ext_scan_recv.data, - event->ext_scan_recv.data_len, - check_and_connect, (void *)event); - } + conn_pending = true; } static void acl_connect(esp_ble_audio_gap_app_event_t *event) { int err; + conn_pending = false; + if (event->acl_connect.status) { ESP_LOGE(TAG, "Connection failed, status %d", event->acl_connect.status); + ext_scan_start(); return; } @@ -130,7 +274,16 @@ static void acl_connect(esp_ble_audio_gap_app_event_t *event) event->acl_connect.conn_handle, event->acl_connect.role, EXAMPLE_BT_ADDR_PRINT_ARGS(event->acl_connect.dst.val)); - default_conn_handle = event->acl_connect.conn_handle; + if (member_count >= ARRAY_SIZE(members)) { + ESP_LOGE(TAG, "Set member table full"); + return; + } + + memset(&members[member_count], 0, sizeof(members[member_count])); + members[member_count].handle = event->acl_connect.conn_handle; + memcpy(members[member_count].dst, event->acl_connect.dst.val, + sizeof(members[member_count].dst)); + member_count++; err = pairing_start(event->acl_connect.conn_handle); if (err) { @@ -139,16 +292,50 @@ static void acl_connect(esp_ble_audio_gap_app_event_t *event) } } +/* Nothing is connected any more: forget the whole session. */ +static void set_teardown_finish(void) +{ + lock_pending = false; + set_locked = false; + + cap_initiator_reset(); + + vcp_vol_ctlr_reset(); + + ext_scan_start(); +} + static void acl_disconnect(esp_ble_audio_gap_app_event_t *event) { ESP_LOGI(TAG, "Disconnected: handle %u reason 0x%02x", event->acl_disconnect.conn_handle, event->acl_disconnect.reason); - default_conn_handle = CONN_HANDLE_INIT; - disc_completed = false; - mtu_exchanged = false; + if (member_by_handle(event->acl_disconnect.conn_handle) == NULL) { + return; + } - unicast_group_delete(); + member_remove(event->acl_disconnect.conn_handle); + peer_forget(event->acl_disconnect.conn_handle); + vcp_vol_ctlr_forget(event->acl_disconnect.conn_handle); + + /* An incomplete set is no longer something to drive as one. */ + vcp_vol_ctlr_streams_stopped(); + + /* Give the lock back on whatever is still connected, and drop a lock that + * is still in flight: the set it was taken for no longer exists. */ + set_release_lock(); + lock_pending = false; + + if (member_count == 0) { + set_teardown_finish(); + return; + } + + /* Nothing else to undo: the members that are left keep their links and + * keep streaming. Only the departed member's own streams were released, + * by the stack, and its slot in the CIG stays reserved for whatever comes + * back to fill it. */ + ESP_LOGI(TAG, "Lost a member; %zu left, scanning for the other", member_count); ext_scan_start(); } @@ -195,8 +382,40 @@ static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event) } } +/* MTU exchange and service discovery complete in either order. */ +static void member_discover_profiles(struct set_member *member) +{ + esp_err_t err; + + if (!member->mtu_exchanged || !member->disc_completed) { + return; + } + + err = esp_ble_audio_tmap_discover(member->handle, &tmap_callbacks); + if (err) { + ESP_LOGE(TAG, "Failed to discover tmap, err %d", err); + return; + } + + /* The CAP commander needs a volume controller on every member. */ + vcp_vol_ctlr_discover(member->handle); + + /* Look for the remaining members while this one is discovered. */ + if (member_count < ARRAY_SIZE(members)) { + ESP_LOGI(TAG, "Peer %zu/%u connected, scanning for the rest", + member_count, TMAP_CEN_PEER_COUNT); + ext_scan_start(); + } + + /* Rank before endpoints, so members can be ordered. No CSIS, no callback. */ + if (csip_discover_start(member->handle) != 0) { + set_member_discovered(member->handle, -ENOTSUP); + } +} + static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event) { + struct set_member *member; esp_err_t err; ESP_LOGI(TAG, "MTU updated: handle %u mtu %u", @@ -208,6 +427,11 @@ static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event) return; } + member = member_by_handle(event->gatt_mtu_change.conn_handle); + if (member == NULL) { + return; + } + err = esp_ble_audio_gattc_disc_start(event->gatt_mtu_change.conn_handle); if (err) { ESP_LOGE(TAG, "Failed to start service discovery, err %d", err); @@ -217,25 +441,14 @@ static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event) ESP_LOGI(TAG, "Service discovery started: handle %u", event->gatt_mtu_change.conn_handle); - /* Note: - * MTU exchanged event may arrived after discover completed event. - */ - mtu_exchanged = true; + member->mtu_exchanged = true; - if (disc_completed) { - err = esp_ble_audio_tmap_discover(default_conn_handle, &tmap_callbacks); - if (err) { - ESP_LOGE(TAG, "Failed to discover tmap, err %d", err); - return; - } - - vcp_vol_ctlr_discover(); - } + member_discover_profiles(member); } static void gattc_disc_cmpl(esp_ble_audio_gatt_app_event_t *event) { - esp_err_t err; + struct set_member *member; ESP_LOGI(TAG, "Service discovery complete: handle %u status %u", event->gattc_disc_cmpl.conn_handle, @@ -245,19 +458,125 @@ static void gattc_disc_cmpl(esp_ble_audio_gatt_app_event_t *event) return; } - /* Note: - * Discover completed event may arrived before MTU exchanged event. - */ - disc_completed = true; + member = member_by_handle(event->gattc_disc_cmpl.conn_handle); + if (member == NULL) { + return; + } - if (mtu_exchanged) { - err = esp_ble_audio_tmap_discover(default_conn_handle, &tmap_callbacks); - if (err) { - ESP_LOGE(TAG, "Failed to discover tmap, err %d", err); - return; + member->disc_completed = true; + + member_discover_profiles(member); +} + +/* Ordered access needs all ranks 0 or all unique: a duplicate locks one member + * twice and skips the other. */ +static bool set_ranks_are_valid(void) +{ + size_t unranked = 0; + + for (size_t i = 0; i < member_count; i++) { + uint8_t rank = csip_member_rank_get(members[i].handle); + + if (rank == 0) { + unranked++; + continue; } - vcp_vol_ctlr_discover(); + for (size_t j = i + 1; j < member_count; j++) { + if (csip_member_rank_get(members[j].handle) == rank) { + ESP_LOGE(TAG, "Handle %u and handle %u both report rank %u; flash the " + "peripherals for different ears so their ranks differ", + members[i].handle, members[j].handle, rank); + return false; + } + } + } + + if (unranked != 0 && unranked != member_count) { + ESP_LOGE(TAG, "Set mixes %zu ranked and %zu unranked member(s)", + member_count - unranked, unranked); + return false; + } + + return true; +} + +/* One peripheral finished set discovery, or has no CSIS at all. */ +void set_member_discovered(uint16_t conn_handle, int err) +{ + struct set_member *member = member_by_handle(conn_handle); + + /* A standalone headset exposes no CSIS; keep going with what is connected. */ + if (err) { + ESP_LOGI(TAG, "Handle %u is not a coordinated set member (err %d)", conn_handle, err); + } + + if (member == NULL) { + return; + } + + member->set_discovered = true; + + if (member_count < ARRAY_SIZE(members)) { + return; + } + + for (size_t i = 0; i < member_count; i++) { + if (!members[i].set_discovered) { + return; + } + } + + ESP_LOGI(TAG, "All %zu peer(s) ready", member_count); + + set_members_sort_by_rank(); + + /* Streams still work unlocked, so report and carry on. */ + if (!set_ranks_are_valid()) { + set_lock_complete(-EINVAL); + return; + } + + /* Nothing to lock: go straight to audio setup. */ + if (csip_lock_set(lock_handles(), member_count) != 0) { + set_lock_complete(-ENOTSUP); + return; + } + + lock_pending = true; +} + +void set_lock_complete(int err) +{ + /* A set that cannot be locked may still be operated on. */ + if (err && lock_pending) { + ESP_LOGW(TAG, "Proceeding without the set lock, err %d", err); + } + + set_locked = (err == 0) && lock_pending; + lock_pending = false; + + /* A member may have gone while the lock was in flight; the set that is left + * is not the one to set up. */ + if (member_count < ARRAY_SIZE(members)) { + set_release_lock(); + return; + } + + if (cap_initiator_setup() != 0) { + set_release_lock(); + } +} + +/* Give the lock back once stream setup is done. Called from cap_initiator.c. */ +void set_release_lock(void) +{ + if (!set_locked) { + return; + } + + if (csip_release_set(lock_handles(), member_count) == 0) { + set_locked = false; } } @@ -321,6 +640,11 @@ void app_main(void) return; } + err = csip_set_coordinator_init(); + if (err) { + return; + } + err = vcp_vol_ctlr_init(); if (err) { return; diff --git a/examples/bluetooth/esp_ble_audio/tmap/central/main/tmap_central.h b/examples/bluetooth/esp_ble_audio/tmap/central/main/tmap_central.h index a8b44f463af..5d8c68649ef 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/central/main/tmap_central.h +++ b/examples/bluetooth/esp_ble_audio/tmap/central/main/tmap_central.h @@ -6,7 +6,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include #include +#include #include #include @@ -37,6 +39,28 @@ #define LOCAL_DEVICE_NAME "TMAP Central" +/* Topology from EXAMPLE_TMAP_CEN_TOPOLOGY. CIS #0 is bidirectional, a second is + * TX-only. Note: CONFIG_BT_ACL_CONNECTIONS is Bluedroid-only. */ +#if CONFIG_EXAMPLE_TMAP_CEN_TOPO_1P_1CIS +#define TMAP_CEN_PEER_COUNT 1 +#define TMAP_CEN_CIS_COUNT 1 +#elif CONFIG_EXAMPLE_TMAP_CEN_TOPO_1P_2CIS +#define TMAP_CEN_PEER_COUNT 1 +#define TMAP_CEN_CIS_COUNT 2 +#else /* CONFIG_EXAMPLE_TMAP_CEN_TOPO_2P_2CIS */ +#define TMAP_CEN_PEER_COUNT 2 +#define TMAP_CEN_CIS_COUNT 2 +#endif + +/* Sink ASEs recorded per peripheral: as many as the client caches, so a peer + * offering more than this topology streams on is not flagged as unhandled. */ +#define TMAP_CEN_SINK_EP_MAX CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT + +#if TMAP_CEN_PEER_COUNT == 1 +_Static_assert(TMAP_CEN_SINK_EP_MAX >= TMAP_CEN_CIS_COUNT, + "One peripheral must expose a sink ASE per CIS"); +#endif + #define SCAN_INTERVAL 160 /* 100ms */ #define SCAN_WINDOW 160 /* 100ms */ @@ -65,7 +89,70 @@ int exchange_mtu(uint16_t conn_handle); void security_failed_recover(uint16_t conn_handle, uint8_t status); -uint16_t default_conn_handle_get(void); +size_t set_member_count(void); + +uint16_t set_member_handle(size_t index); + +const uint8_t *set_member_addr(uint16_t conn_handle); + +void set_member_discovered(uint16_t conn_handle, int err); + +void set_lock_complete(int err); + +void set_release_lock(void); + +#if TMAP_CEN_PEER_COUNT > 1 +int csip_set_coordinator_init(void); + +bool csip_adv_is_set_member(uint8_t ad_type, const uint8_t *data, uint8_t data_len); + +int csip_discover_start(uint16_t conn_handle); + +uint8_t csip_member_rank_get(uint16_t conn_handle); + +int csip_lock_set(const uint16_t *conn_handles, size_t count); + +int csip_release_set(const uint16_t *conn_handles, size_t count); +#else /* TMAP_CEN_PEER_COUNT > 1 */ +static inline int csip_set_coordinator_init(void) +{ + return 0; +} + +static inline bool csip_adv_is_set_member(uint8_t ad_type, const uint8_t *data, uint8_t data_len) +{ + (void)ad_type; + (void)data; + (void)data_len; + return false; +} + +static inline int csip_discover_start(uint16_t conn_handle) +{ + (void)conn_handle; + return -ENOTSUP; +} + +static inline uint8_t csip_member_rank_get(uint16_t conn_handle) +{ + (void)conn_handle; + return 0; +} + +static inline int csip_lock_set(const uint16_t *conn_handles, size_t count) +{ + (void)conn_handles; + (void)count; + return -ENOTSUP; +} + +static inline int csip_release_set(const uint16_t *conn_handles, size_t count) +{ + (void)conn_handles; + (void)count; + return -ENOTSUP; +} +#endif /* TMAP_CEN_PEER_COUNT > 1 */ int mcp_server_init(void); @@ -73,10 +160,28 @@ int ccp_server_init(void); int vcp_vol_ctlr_init(void); -int vcp_vol_ctlr_discover(void); +int vcp_vol_ctlr_discover(uint16_t conn_handle); + +void vcp_vol_ctlr_streams_started(void); + +void vcp_vol_ctlr_streams_stopped(void); + +void vcp_vol_ctlr_forget(uint16_t conn_handle); + +void vcp_vol_ctlr_reset(void); + +int vcp_vol_ctlr_set_volume(uint8_t volume); + +esp_ble_audio_cap_set_type_t set_type_get(void); + +bool set_member_fill(esp_ble_audio_cap_set_member_t *member, + esp_ble_audio_cap_set_type_t type, + uint16_t conn_handle); int cap_initiator_init(void); int cap_initiator_setup(void); -int unicast_group_delete(void); +void peer_forget(uint16_t conn_handle); + +void cap_initiator_reset(void); 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 8ef8aaccff3..694cf8e1d31 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 @@ -7,17 +7,191 @@ */ #include +#include #include +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" #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; +/* A TWS pair must change volume on both earbuds together, so commands go through + * the CAP commander. It refuses until every member has a volume controller, and a + * VCS write needs the change counter synced, hence the per-member state read. + * + * Kept per member rather than as a count: a member that keeps its link while + * another one is replaced keeps its controller and its synced counter too. */ +struct vol_ctlr_slot { + uint16_t conn_handle; + esp_ble_audio_vcp_vol_ctlr_t *ctlr; + bool state_read; +}; + +static struct vol_ctlr_slot vol_ctlrs[TMAP_CEN_PEER_COUNT]; +static bool streams_started; + +/* Walks the set through one commander procedure per tick, so the log shows both + * earbuds following every step of a single procedure rather than being written + * one at a time. + * + * This is application behaviour, so it gets an application task rather than + * borrowing one of the stack's. Not an esp_timer callback either: that task is + * shared with the BLE stack's own timers — the ISO TX schedulers post from it + * every SDU interval — and it is serial, so a GATT write chain dispatched there + * would sit in front of them. The audio APIs take the host lock themselves + * (esp_ble_audio_cap_commander_* -> bt_cap_commander_*_safe -> k_lock), which is + * what makes calling them from here safe. */ +#define VOLUME_PERIOD_MS (5 * 60 * 1000) /* 5 minutes */ +#define VOLUME_TASK_STACK 3072 +#define VOLUME_TASK_PRIO 5 + +static const struct { + bool is_mute; + uint8_t value; /* volume level, or mute on/off */ +} volume_steps[] = { + { false, 32 }, + { false, 96 }, + { false, 160 }, + { false, 224 }, + { true, 1 }, + { true, 0 }, +}; + +static size_t volume_index; + +/* Written here, cleared from the commander callback in the audio task. */ +static volatile bool volume_busy; +static volatile bool volume_running; + +static struct vol_ctlr_slot *slot_by_handle(uint16_t conn_handle) +{ + for (size_t i = 0; i < ARRAY_SIZE(vol_ctlrs); i++) { + if (vol_ctlrs[i].ctlr != NULL && vol_ctlrs[i].conn_handle == conn_handle) { + return &vol_ctlrs[i]; + } + } + + return NULL; +} + +static struct vol_ctlr_slot *slot_by_ctlr(const esp_ble_audio_vcp_vol_ctlr_t *ctlr) +{ + for (size_t i = 0; i < ARRAY_SIZE(vol_ctlrs); i++) { + if (vol_ctlrs[i].ctlr == ctlr) { + return &vol_ctlrs[i]; + } + } + + return NULL; +} + +static int set_volume_mute(bool mute) +{ + esp_ble_audio_cap_set_member_t members[TMAP_CEN_PEER_COUNT] = {0}; + esp_ble_audio_cap_commander_change_volume_mute_state_param_t param = {0}; + size_t count; + int err; + + count = set_member_count(); + if (count == 0) { + return -ENOTCONN; + } + + param.type = set_type_get(); + + for (size_t i = 0; i < count; i++) { + if (!set_member_fill(&members[i], param.type, set_member_handle(i))) { + ESP_LOGE(TAG, "No set member for handle %u", set_member_handle(i)); + return -ENOENT; + } + } + + param.members = members; + param.count = count; + param.mute = mute; + + err = esp_ble_audio_cap_commander_change_volume_mute_state(¶m); + if (err) { + ESP_LOGE(TAG, "Failed to %s the set, err %d", mute ? "mute" : "unmute", err); + return err; + } + + ESP_LOGI(TAG, "Changing volume mute state of %zu member(s) to %u", count, mute); + + return 0; +} + +static void volume_tick(void) +{ + size_t step = volume_index; + int err; + + /* The commander runs one procedure at a time, so a tick is skipped rather + * than piled on top of a set that has not answered yet. */ + if (volume_busy) { + ESP_LOGW(TAG, "Previous volume procedure still running, skipping a step"); + return; + } + + volume_index = (volume_index + 1) % ARRAY_SIZE(volume_steps); + + /* Marked busy before issuing: a procedure that fails early reports back + * from inside the call, and that callback must not be overwritten here. */ + volume_busy = true; + + if (volume_steps[step].is_mute) { + err = set_volume_mute(volume_steps[step].value != 0); + } else { + err = vcp_vol_ctlr_set_volume(volume_steps[step].value); + } + + if (err) { + volume_busy = false; + } +} + +static void volume_task(void *arg) +{ + for (;;) { + vTaskDelay(pdMS_TO_TICKS(VOLUME_PERIOD_MS)); + + if (volume_running) { + volume_tick(); + } + } +} + +static void volume_stop(void) +{ + volume_running = false; + volume_busy = false; +} + +/* Needs every VCS state read and the streams up, in either order. */ +static void volume_try_start(void) +{ + size_t count = set_member_count(); + + if (volume_running || !streams_started || count == 0) { + return; + } + + for (size_t i = 0; i < count; i++) { + const struct vol_ctlr_slot *slot = slot_by_handle(set_member_handle(i)); + + if (slot == NULL || !slot->state_read) { + return; + } + } + + volume_index = 0; + volume_busy = false; + volume_running = true; + + ESP_LOGI(TAG, "Driving the volume of %zu member(s) every %u s", + count, VOLUME_PERIOD_MS / 1000); +} static void vcs_discover_cb(esp_ble_audio_vcp_vol_ctlr_t *vol_ctlr, int err, uint8_t vocs_count, uint8_t aics_count) @@ -27,13 +201,12 @@ static void vcs_discover_cb(esp_ble_audio_vcp_vol_ctlr_t *vol_ctlr, int err, return; } - ESP_LOGI(TAG, "VCP discovery done"); + ESP_LOGI(TAG, "VCP discovery done, vocs %u aics %u", vocs_count, aics_count); - initial_state_read_pending = true; + /* Control-point writes carry a change counter: read the state to sync it. */ err = esp_ble_audio_vcp_vol_ctlr_read_state(vol_ctlr); if (err) { ESP_LOGE(TAG, "Failed to read VCS state, err %d", err); - initial_state_read_pending = false; } } @@ -49,22 +222,21 @@ static void vcs_write_cb(esp_ble_audio_vcp_vol_ctlr_t *vol_ctlr, int err) static void vcs_state_cb(esp_ble_audio_vcp_vol_ctlr_t *vol_ctlr, int err, uint8_t volume, uint8_t mute) { + struct vol_ctlr_slot *slot; + if (err) { ESP_LOGE(TAG, "VCP state cb failed, err %d", err); - 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); - } + slot = slot_by_ctlr(vol_ctlr); + if (slot != NULL) { + slot->state_read = true; } + + volume_try_start(); } static void vcs_flags_cb(esp_ble_audio_vcp_vol_ctlr_t *vol_ctlr, @@ -90,24 +262,131 @@ static esp_ble_audio_vcp_vol_ctlr_cb_t vcp_cbs = { .flags = vcs_flags_cb, }; -int vcp_vol_ctlr_discover(void) +/* One callback per procedure, not per member: conn names the member that failed. */ +static void volume_changed_cb(esp_ble_conn_t *conn, int err) { - uint16_t conn_handle; + if (err) { + ESP_LOGE(TAG, "Set volume failed on handle %u, err %d", + (conn != NULL) ? conn->handle : CONN_HANDLE_INIT, err); + } else { + ESP_LOGI(TAG, "Set volume changed"); + } + + volume_busy = false; +} + +static void volume_mute_changed_cb(esp_ble_conn_t *conn, int err) +{ + if (err) { + ESP_LOGE(TAG, "Set volume mute failed on handle %u, err %d", + (conn != NULL) ? conn->handle : CONN_HANDLE_INIT, err); + } else { + ESP_LOGI(TAG, "Set volume mute changed"); + } + + volume_busy = false; +} + +static esp_ble_audio_cap_commander_cb_t cap_commander_cbs = { + .volume_changed = volume_changed_cb, + .volume_mute_changed = volume_mute_changed_cb, +}; + +int vcp_vol_ctlr_discover(uint16_t conn_handle) +{ + esp_ble_audio_vcp_vol_ctlr_t *vol_ctlr; + struct vol_ctlr_slot *slot; int err; - conn_handle = default_conn_handle_get(); if (conn_handle == CONN_HANDLE_INIT) { ESP_LOGE(TAG, "%s, not connected", __func__); return -ENOTCONN; } - err = esp_ble_audio_vcp_vol_ctlr_discover(conn_handle, &vcp_vol_ctlr); + slot = slot_by_ctlr(NULL); + if (slot == NULL) { + ESP_LOGE(TAG, "No volume controller slot for handle %u", conn_handle); + return -ENOMEM; + } + + err = esp_ble_audio_vcp_vol_ctlr_discover(conn_handle, &vol_ctlr); if (err) { - ESP_LOGE(TAG, "Failed to discover vcp vol ctlr, err %d", err); + ESP_LOGE(TAG, "Failed to discover vcp vol ctlr on handle %u, err %d", conn_handle, err); return err; } - ESP_LOGI(TAG, "VCP volume controller discovering"); + slot->conn_handle = conn_handle; + slot->ctlr = vol_ctlr; + slot->state_read = false; + + ESP_LOGI(TAG, "VCP volume controller discovering: handle %u", conn_handle); + + return 0; +} + +void vcp_vol_ctlr_forget(uint16_t conn_handle) +{ + struct vol_ctlr_slot *slot = slot_by_handle(conn_handle); + + if (slot != NULL) { + memset(slot, 0, sizeof(*slot)); + } +} + +void vcp_vol_ctlr_streams_started(void) +{ + streams_started = true; + + volume_try_start(); +} + +/* The set is no longer whole, so it is no longer something to drive as one. */ +void vcp_vol_ctlr_streams_stopped(void) +{ + streams_started = false; + + volume_stop(); +} + +void vcp_vol_ctlr_reset(void) +{ + volume_stop(); + + memset(vol_ctlrs, 0, sizeof(vol_ctlrs)); + streams_started = false; +} + +int vcp_vol_ctlr_set_volume(uint8_t volume) +{ + esp_ble_audio_cap_set_member_t members[TMAP_CEN_PEER_COUNT] = {0}; + esp_ble_audio_cap_commander_change_volume_param_t param = {0}; + size_t count; + int err; + + count = set_member_count(); + if (count == 0) { + return -ENOTCONN; + } + + param.type = set_type_get(); + + for (size_t i = 0; i < count; i++) { + if (!set_member_fill(&members[i], param.type, set_member_handle(i))) { + return -ENOENT; + } + } + + param.members = members; + param.count = count; + param.volume = volume; + + err = esp_ble_audio_cap_commander_change_volume(¶m); + if (err) { + ESP_LOGE(TAG, "Failed to set set volume, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Changing volume of %zu member(s) to %u", count, volume); return 0; } @@ -122,6 +401,18 @@ int vcp_vol_ctlr_init(void) return err; } + err = esp_ble_audio_cap_commander_register_cb(&cap_commander_cbs); + if (err) { + ESP_LOGE(TAG, "Failed to register cap commander cb, err %d", err); + return err; + } + + if (xTaskCreate(volume_task, "volume", VOLUME_TASK_STACK, NULL, + VOLUME_TASK_PRIO, NULL) != pdPASS) { + ESP_LOGE(TAG, "Failed to create the volume task"); + return -ENOMEM; + } + ESP_LOGI(TAG, "VCP volume controller initialized"); return 0; diff --git a/examples/bluetooth/esp_ble_audio/tmap/central/sdkconfig.defaults b/examples/bluetooth/esp_ble_audio/tmap/central/sdkconfig.defaults index c0d383c7c12..9e52c35ee96 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/central/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/tmap/central/sdkconfig.defaults @@ -9,18 +9,21 @@ CONFIG_BT_CLASSIC_ENABLED=n CONFIG_BT_CONTROLLER_ENABLED=y CONFIG_BT_BLE_ENABLED=y CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y -CONFIG_BT_ACL_CONNECTIONS=1 +CONFIG_BT_ACL_CONNECTIONS=2 CONFIG_BT_GATT_MAX_SR_PROFILES=12 CONFIG_BT_GATTC_NOTIF_REG_MAX=64 CONFIG_BT_BLE_FEAT_ISO_EN=y CONFIG_BT_ISO_MAX_CHAN=2 +CONFIG_BT_ISO_MAX_CIG=1 CONFIG_BT_TMAP=y CONFIG_BT_CAP_INITIATOR=y CONFIG_BT_CAP_COMMANDER=y CONFIG_BT_CSIP_SET_COORDINATOR=y CONFIG_BT_BAP_UNICAST_CLIENT=y +CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT=2 +CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT=2 CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT=2 CONFIG_BT_VCP_VOL_CTLR=y CONFIG_BT_MPL=y @@ -34,3 +37,24 @@ CONFIG_BT_TBS_SUPPORTED_FEATURES=3 CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y CONFIG_FREERTOS_HZ=1000 + +# To place LE Audio .bss + control-plane heap in PSRAM, uncomment the +# lines below (needs a PSRAM-capable target, e.g. esp32s31/esp32h4). +# CONFIG_SPIRAM=y +# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_HEAP_EXTERNAL_MEMORY=y +# CONFIG_BT_AUDIO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_AUDIO_HEAP_EXTERNAL_MEMORY=y + +# Bandwidth-optimized (critical-only) compressed BLE Audio/ISO logging over +# UART DMA. Pins shown are for esp32s31 (UART0 TX = GPIO58, 3000000 baud); set +# CONFIG_BLE_LOG_PRPH_UART_DMA_* values to the port/pin your capture reads. +# CONFIG_BLE_LOG_ENABLED=y +# CONFIG_BT_LOG_CRITICAL_ONLY=y +# CONFIG_BLE_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_LOG_PRPH_UART_DMA=y +# CONFIG_BLE_LOG_PRPH_UART_DMA_PORT=0 +# CONFIG_BLE_LOG_PRPH_UART_DMA_BAUD_RATE=3000000 +# CONFIG_BLE_LOG_PRPH_UART_DMA_TX_IO_NUM=58 diff --git a/examples/bluetooth/esp_ble_audio/tmap/central/sdkconfig.defaults.nimble b/examples/bluetooth/esp_ble_audio/tmap/central/sdkconfig.defaults.nimble index 615c7722d8d..f2740186c18 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/central/sdkconfig.defaults.nimble +++ b/examples/bluetooth/esp_ble_audio/tmap/central/sdkconfig.defaults.nimble @@ -6,7 +6,7 @@ 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_CONNECTIONS=2 CONFIG_BT_NIMBLE_MAX_CCCDS=64 CONFIG_BT_NIMBLE_ISO=y CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y diff --git a/examples/bluetooth/esp_ble_audio/tmap/peripheral/README.md b/examples/bluetooth/esp_ble_audio/tmap/peripheral/README.md index f5fb6895384..b685e5e1327 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/peripheral/README.md +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/README.md @@ -22,11 +22,29 @@ The example uses the ESP-BLE-AUDIO library pieces for: BAP Unicast Server with P idf.py menuconfig ``` +> **This board must be built to match the topology [tmap_central](../central/) is built for.** Neither side negotiates it: the central looks for a fixed number of peers, each exposing a fixed number of sink ASEs with a specific channel. A mismatch stalls in discovery or fails to build the unicast group. + +| Central **Unicast topology** | Boards of this example | **Earbuds type** | **Earbud Location** | +| --- | --- | --- | --- | +| `1 peripheral, 1 CIS (TX+RX)` | 1 | `Single headset` | `Mono` | +| `1 peripheral, 2 CISes (TX+RX, TX)` **(default)** | 1 | `Single headset` **(default)** | `Stereo` **(default)** | +| `2 peripherals (coordinated set), 2 CISes (TX+RX, TX)` | 2 | `Duo headset` on both | `Left` on one, `Right` on the other | + Under **Example: TMAP Peripheral (CT & UMR)**: -* **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. +* **Earbuds type** — `Single headset` (standalone: no CSIS, no RSI) or `Duo headset` (selects `BT_CSIP_SET_MEMBER` and `BT_CAP_ACCEPTOR_SET_MEMBER`, enabling CSIS and adding RSI to the advertisement). +* **Earbud Location** — sets the PACS sink and source location bitmap, and **this is what the central uses to pick the channel it sends here**. The options offered follow the earbuds type, so the two choices together can only express a topology the central supports: + * with `Single headset` — a whole device, so mono or stereo: + * `Mono` — empty bitmap; the central sends one mono stream. Pair with the central's `1 peripheral, 1 CIS` topology. + * `Stereo` (default) — `FRONT_LEFT | FRONT_RIGHT`, so one board renders both channels. Matches the central's default `1 peripheral, 2 CISes` topology. + * with `Duo headset` — one of a pair, so left or right: + * `Left` (default) — `FRONT_LEFT`, and rank 1. + * `Right` — `FRONT_RIGHT`, and rank 2. +* **Device rank in set** — integer 1–2, only when Duo is selected; written into the CSIS register parameters. The CSIS instance is registered as lockable, which requires a non-zero rank that is **unique** within the set: duplicate ranks break the CSIP ordered access procedure the central locks the set with. The default follows Earbud Location (left → 1, right → 2), and a set member can only be left or right, so building the two boards for their respective ears already gives them distinct ranks. Rank orders the set lock and decides which member the central gives its bidirectional CIS to; it does **not** say which ear this board is. + +The ASE counts come from `sdkconfig.defaults`: two sink ASEs and one source ASE, which is what the central's topologies need (two sinks on one device, or one sink per device, plus a source for the bidirectional CIS). Each ASE gets its own stream slot, RX metrics and — for source ASEs — its own TX scheduler, buffer and sequence number. + +`CONFIG_BT_ASCS_MAX_ACTIVE_ASES` is pinned to 3 there as well, and it is **not** the same thing as the CIS count. It bounds how many ASEs may be non-idle at once, and a bidirectional CIS carries two ASEs on one CIS — so `1 peripheral, 2 CISes` needs 3 active ASEs (two sinks plus one source) over only 2 CIS. Its Kconfig default is `BT_ISO_MAX_CHAN`, i.e. 2, which is one short; leaving it at the default makes the third codec configuration fail with ASCS response `0x0D` (Insufficient Resources) and `AscsAseNewFail` in the library log, and the central reports `Unicast start completed, err -77`. ### Security & Pairing @@ -52,6 +70,9 @@ idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkco For `esp32s31`, replace the chip overlay accordingly. +A coordinated set needs this example on two boards, one built for the `Left` +ear and the other for `Right`. + (Exit serial monitor with `Ctrl-]`.) ## Example Flow @@ -64,9 +85,10 @@ For `esp32s31`, replace the chip overlay accordingly. 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. +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, per sink ASE. +10. A source ASE that reaches "started" arms its own TX scheduler at the QoS interval and transmits dummy SDUs back to the central — this is the uplink half of the central's bidirectional CIS. The scheduler and buffer are released when the stream stops, when its CIS disconnects, or when the ASE is released. +11. Helpers `initiate_call`/`terminate_call` (gated on peer CG) and `play_media`/`pause_media` (gated on peer UMS) issue TBS and MCC commands. +12. On disconnect, the connection handle is cleared and advertising restarts. ## Expected Log @@ -78,7 +100,7 @@ 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: BAP unicast server initialized: 2 sink / 1 source ASE TMAP_PER: CCP call controller initialized TMAP_PER: MCP controller initialized TMAP_PER: Advertising started (handle 0) @@ -102,7 +124,7 @@ TMAP_PER: Read media state succeeded, state TMAP_PER: Read content control id succeeded, ccid ``` -ASCS / unicast streaming: +ASCS / unicast streaming. Which streams appear depends on the topology the central was built for — a second `SNK #1` when it drives two CISes onto this device, and `SRC #0` whenever it pairs a source into the bidirectional CIS: ``` TMAP_PER: [SNK] Config request: @@ -111,12 +133,17 @@ TMAP_PER: [SNK #0] Enable request (meta_len ) TMAP_PER: [SNK #0] Stream enabled TMAP_PER: [SNK #0] Start request TMAP_PER: [SNK #0] Stream started +TMAP_PER: [SNK #0] RX: packets +TMAP_PER: [SRC #0] Stream started +TMAP_PER: [SRC #0] TX: packets TMAP_PER: [SNK #0] Stop request TMAP_PER: [SNK #0] Stream stopped, reason 0x TMAP_PER: [SNK #0] Disable request TMAP_PER: [SNK #0] Release request ``` +Each ASE keeps its own counters, so `[SNK #0]` and `[SNK #1]` advance independently rather than sharing one total. + VCP / call control / disconnect: ``` @@ -144,11 +171,19 @@ Tag is `TMAP_PER`. ## Peer Pairing -Run [tmap_central](../central/) on a second board. +Run [tmap_central](../central/) on another board. How many boards of this example are needed, and how they must be configured, follows the topology the central is built for: + +| Central topology | Boards of this example | Configuration | +| --- | --- | --- | +| `1P_1CIS` | 1 | `Single headset` with the `Mono` location, so the central sends one mono stream | +| `1P_2CIS` | 1 | `Stereo` location, so PACS advertises both channels | +| `2P_2CIS` | 2 | `Duo headset` on both, `Left` on one and `Right` on the other — the rank default follows, giving them 1 and 2 | 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. +2. Flash and start the central; it scans for TMAS+UMR (plus a matching CSIS RSI in a coordinated-set build) 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. +4. The MCC chain reads the central's media proxy state; the central's CAP initiator configures and starts the streams. +5. The peripheral auto-starts each sink stream from the `enabled` callback and logs its received SDU metrics; a configured source stream transmits back on the bidirectional CIS. 6. Use `initiate_call`/`terminate_call` to drive the central's TBS, and `play_media`/`pause_media` to send PLAY/PAUSE via MCC. + +For the two-board set, the CSIS instance is lockable, so the central takes the set lock by rank before configuring the streams and releases it afterwards. Each board logs `Client

locked the lock` / `... released the lock`. 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 6c72a87e40f..8208cbd5cb2 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/Kconfig.projbuild +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/Kconfig.projbuild @@ -7,11 +7,17 @@ menu "Example: TMAP Peripheral (CT & UMR)" choice EXAMPLE_TMAP_PER_TYPE_CHOICE prompt "Earbuds type" + default EXAMPLE_TMAP_PER_SINGLE help - Select the Earbuds Type to compile. + Select the Earbuds Type to compile. Together with Earbud Location this + has to match the topology tmap_central is built for; the defaults of + both examples pair up as "1 peripheral, 2 CISes". config EXAMPLE_TMAP_PER_SINGLE - bool "Single ear headset" + bool "Single headset" + help + A standalone acceptor: no CSIS instance and no RSI. Which channels + it renders is the separate choice below. config EXAMPLE_TMAP_PER_DUO bool "Duo headset" @@ -20,26 +26,66 @@ menu "Example: TMAP Peripheral (CT & UMR)" endchoice + choice EXAMPLE_TMAP_PER_LOCATION + + prompt "Earbud Location" + default EXAMPLE_TMAP_PER_STEREO if EXAMPLE_TMAP_PER_SINGLE + default EXAMPLE_TMAP_PER_LEFT + help + Which channels this device renders, published as the PACS Sink and + Source Audio Locations, and what the central reads to decide the + channel it sends here. The options follow the earbuds type: a single + headset is a whole device, so it is mono or stereo, while a set member + is one of a pair, so it is left or right. + + config EXAMPLE_TMAP_PER_MONO + bool "Mono" + depends on !EXAMPLE_TMAP_PER_DUO + help + Publish an empty Audio Locations bitmap, which BAP reads as "mono, + no preference", so the central sends one mono stream. This is what + a one-ear headset renders; pair it with the central's + "1 peripheral, 1 CIS" topology. + + config EXAMPLE_TMAP_PER_STEREO + bool "Stereo" + depends on !EXAMPLE_TMAP_PER_DUO + help + Publish front left and front right, so one device renders both + channels. This is the default, matching the central's default + "1 peripheral, 2 CISes" topology, which configures both channels + on this board. + + config EXAMPLE_TMAP_PER_LEFT + bool "Left" + depends on EXAMPLE_TMAP_PER_DUO + + config EXAMPLE_TMAP_PER_RIGHT + bool "Right" + depends on EXAMPLE_TMAP_PER_DUO + + endchoice + if EXAMPLE_TMAP_PER_DUO config EXAMPLE_TMAP_PER_SET_RANK int "Device rank in set" range 1 2 + default 1 if EXAMPLE_TMAP_PER_LEFT + default 2 if EXAMPLE_TMAP_PER_RIGHT + default 1 help - Rank of this device in set. + Rank of this device in set. A lockable coordinated set requires a + non-zero rank that is unique among its members, so the two boards + must end up with rank 1 and rank 2 — duplicate ranks break the + CSIP ordered access procedure the central locks the set with. + + The default follows Earbud Location (left is 1, right is 2), and a + set member can only be left or right, so building the two boards + for their respective ears already gives them distinct ranks. + + Rank orders the set lock, and decides which member the central's + "2 peripherals" topology gives the bidirectional CIS to. It does + not say which ear this board is — that comes from Earbud Location. endif - choice EXAMPLE_TMAP_PER_LOCATION - - prompt "Earbud Location" - help - Select the Earbud location. - - config EXAMPLE_TMAP_PER_LEFT - bool "Left Ear" - - config EXAMPLE_TMAP_PER_RIGHT - bool "Right Ear" - - endchoice - endmenu 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 659920c05d6..83c674f9bef 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 @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -33,7 +34,11 @@ static uint8_t codec_meta[] = static const esp_ble_audio_codec_cap_t lc3_codec_cap = ESP_BLE_AUDIO_CODEC_CAP_LC3(codec_data, codec_meta); -static esp_ble_audio_pacs_cap_t cap = { +static esp_ble_audio_pacs_cap_t cap_sink = { + .codec_cap = &lc3_codec_cap, +}; + +static esp_ble_audio_pacs_cap_t cap_source = { .codec_cap = &lc3_codec_cap, }; @@ -47,13 +52,22 @@ static struct audio_sink { static size_t configured_sink_stream_count; +/* Per source ASE: a shared sequence number jumps by the stream count. */ static struct audio_source { - esp_ble_audio_bap_stream_t *stream; - uint16_t seq_num; + esp_ble_audio_bap_stream_t *stream; + example_audio_tx_scheduler_t scheduler; + uint16_t seq_num; + uint8_t *data; + uint8_t index; + bool running; } source_streams[CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT]; static size_t configured_source_stream_count; +static void unicast_server_tx(struct audio_source *src); + +static void source_tx_stop(esp_ble_audio_bap_stream_t *stream); + static const esp_ble_audio_bap_qos_cfg_pref_t qos_pref = ESP_BLE_AUDIO_BAP_QOS_CFG_PREF(true, /* Unframed PDUs supported */ ESP_BLE_ISO_PHY_2M, /* Preferred Target PHY */ @@ -112,6 +126,17 @@ static int stream_index(const esp_ble_audio_bap_stream_t *stream) return -1; } +static struct audio_source *source_by_stream(const esp_ble_audio_bap_stream_t *stream) +{ + for (size_t i = 0; i < ARRAY_SIZE(source_streams); i++) { + if (source_streams[i].stream == stream) { + return &source_streams[i]; + } + } + + return NULL; +} + static int config_cb(esp_ble_conn_t *conn, const esp_ble_audio_bap_ep_t *ep, esp_ble_audio_dir_t dir, @@ -360,7 +385,10 @@ static int release_cb(esp_ble_audio_bap_stream_t *stream, for (size_t i = 0; i < ARRAY_SIZE(source_streams); i++) { if (source_streams[i].stream == stream) { - memset(&source_streams[i], 0, sizeof(source_streams[i])); + /* Per-session fields only; the work item and index persist. */ + source_tx_stop(stream); + source_streams[i].stream = NULL; + source_streams[i].seq_num = 0; if (configured_source_stream_count > 0) { configured_source_stream_count--; } @@ -412,21 +440,155 @@ static void stream_enabled(esp_ble_audio_bap_stream_t *stream) static void stream_started(esp_ble_audio_bap_stream_t *stream) { + struct audio_source *src; + int err; + 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) { example_audio_rx_metrics_reset(&sink_streams[i].rx_metrics); - break; + return; } } + + /* Source stream: feed the uplink half of the bidirectional CIS. */ + src = source_by_stream(stream); + if (src == NULL) { + return; + } + + if (stream->qos == NULL || stream->qos->sdu == 0 || stream->qos->interval == 0) { + ESP_LOGE(TAG, "[SRC #%u] Invalid stream qos", src->index); + return; + } + + if (src->data == NULL) { + src->data = calloc(1, stream->qos->sdu); + if (src->data == NULL) { + ESP_LOGE(TAG, "[SRC #%u] Failed to alloc TX buffer, SDU %u", + src->index, stream->qos->sdu); + return; + } + } + + src->seq_num = 0; + example_audio_tx_scheduler_reset(&src->scheduler); + + /* Note: esp timer is not accurate enough */ + err = example_audio_tx_scheduler_start(&src->scheduler, stream->qos->interval); + if (err) { + ESP_LOGE(TAG, "[SRC #%u] Failed to start tx scheduler, err %d", src->index, err); + return; + } + + src->running = true; + + unicast_server_tx(src); +} + +/* Keyed on the stream, not the endpoint state: the ISO disconnected callback runs + * before the endpoint leaves streaming. */ +static void source_tx_stop(esp_ble_audio_bap_stream_t *stream) +{ + struct audio_source *src; + int err; + + src = source_by_stream(stream); + if (src == NULL || !src->running) { + return; + } + + err = example_audio_tx_scheduler_stop(&src->scheduler); + if (err) { + ESP_LOGE(TAG, "[SRC #%u] Failed to stop tx scheduler, err %d", src->index, err); + } + + src->running = false; + + if (src->data != NULL) { + free(src->data); + src->data = NULL; + } } static void stream_stopped(esp_ble_audio_bap_stream_t *stream, uint8_t reason) { ESP_LOGI(TAG, "[%s #%d] Stream stopped, reason 0x%02x", stream_dir_str(stream), stream_index(stream), reason); + + source_tx_stop(stream); +} + +static void stream_disconnected(esp_ble_audio_bap_stream_t *stream, uint8_t reason) +{ + ESP_LOGI(TAG, "[%s #%d] ISO disconnected, reason 0x%02x", + stream_dir_str(stream), stream_index(stream), reason); + + source_tx_stop(stream); +} + +static void stream_sent(esp_ble_audio_bap_stream_t *stream, void *user_data) +{ + struct audio_source *src; + char name[24]; + + src = source_by_stream(stream); + if (src == NULL) { + return; + } + + snprintf(name, sizeof(name), "SRC #%u", src->index); + example_audio_tx_scheduler_on_sent(&src->scheduler, user_data, TAG, name); +} + +static void unicast_server_tx(struct audio_source *src) +{ + esp_ble_audio_bap_ep_info_t ep_info = {0}; + esp_ble_audio_bap_stream_t *stream; + esp_err_t err; + + stream = src->stream; + + if (stream == NULL || stream->ep == NULL) { + return; + } + + err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info); + if (err) { + return; + } + + if (ep_info.state != ESP_BLE_AUDIO_BAP_EP_STATE_STREAMING) { + return; + } + + if (stream->qos == NULL || stream->qos->sdu == 0) { + ESP_LOGE(TAG, "[SRC #%u] Invalid stream qos", src->index); + return; + } + + if (src->data == NULL) { + ESP_LOGE(TAG, "[SRC #%u] TX buffer unavailable, SDU %u", src->index, stream->qos->sdu); + return; + } + + memset(src->data, (uint8_t)src->seq_num, stream->qos->sdu); + + err = esp_ble_audio_bap_stream_send(stream, src->data, stream->qos->sdu, src->seq_num); + if (err) { + /* Backpressure is normal; a stuck stream shows up as its TX count stalling. */ + return; + } + + src->seq_num++; +} + +/* Each stream has its own timer, so this only ever feeds its own stream. */ +static void tx_scheduler_cb(void *arg) +{ + unicast_server_tx(arg); } static void stream_recv(esp_ble_audio_bap_stream_t *stream, @@ -451,10 +613,12 @@ static void stream_recv(esp_ble_audio_bap_stream_t *stream, } static esp_ble_audio_bap_stream_ops_t stream_ops = { - .enabled = stream_enabled, - .started = stream_started, - .stopped = stream_stopped, - .recv = stream_recv, + .enabled = stream_enabled, + .started = stream_started, + .stopped = stream_stopped, + .recv = stream_recv, + .sent = stream_sent, + .disconnected = stream_disconnected, }; static esp_ble_audio_bap_unicast_server_register_param_t param = { @@ -493,16 +657,16 @@ int bap_unicast_sr_init(void) return err; } -#if CONFIG_EXAMPLE_TMAP_PER_LEFT +#if CONFIG_EXAMPLE_TMAP_PER_LEFT || CONFIG_EXAMPLE_TMAP_PER_STEREO location |= ESP_BLE_AUDIO_LOCATION_FRONT_LEFT; -#endif /* CONFIG_EXAMPLE_TMAP_PER_LEFT */ -#if CONFIG_EXAMPLE_TMAP_PER_RIGHT +#endif /* CONFIG_EXAMPLE_TMAP_PER_LEFT || CONFIG_EXAMPLE_TMAP_PER_STEREO */ +#if CONFIG_EXAMPLE_TMAP_PER_RIGHT || CONFIG_EXAMPLE_TMAP_PER_STEREO location |= ESP_BLE_AUDIO_LOCATION_FRONT_RIGHT; -#endif /* CONFIG_EXAMPLE_TMAP_PER_RIGHT */ +#endif /* CONFIG_EXAMPLE_TMAP_PER_RIGHT || CONFIG_EXAMPLE_TMAP_PER_STEREO */ #if CONFIG_BT_PAC_SNK /* Register CT required capabilities */ - err = esp_ble_audio_pacs_cap_register(ESP_BLE_AUDIO_DIR_SINK, &cap); + err = esp_ble_audio_pacs_cap_register(ESP_BLE_AUDIO_DIR_SINK, &cap_sink); if (err) { ESP_LOGE(TAG, "Failed to register pacs capabilities, err %d", err); return err; @@ -529,7 +693,7 @@ int bap_unicast_sr_init(void) #if CONFIG_BT_PAC_SRC /* Register CT required capabilities */ - err = esp_ble_audio_pacs_cap_register(ESP_BLE_AUDIO_DIR_SOURCE, &cap); + err = esp_ble_audio_pacs_cap_register(ESP_BLE_AUDIO_DIR_SOURCE, &cap_source); if (err) { ESP_LOGE(TAG, "Failed to register pacs capabilities, err %d", err); return err; @@ -558,7 +722,21 @@ int bap_unicast_sr_init(void) esp_ble_audio_bap_stream_cb_register(&streams[i], &stream_ops); } - ESP_LOGI(TAG, "BAP unicast server initialized"); + for (size_t i = 0; i < ARRAY_SIZE(source_streams); i++) { + /* Own timer per stream, fed with its own context. */ + source_streams[i].index = (uint8_t)i; + + err = example_audio_tx_scheduler_init(&source_streams[i].scheduler, + tx_scheduler_cb, + &source_streams[i]); + if (err) { + ESP_LOGE(TAG, "Failed to init tx scheduler[%zu], err %d", i, err); + return err; + } + } + + ESP_LOGI(TAG, "BAP unicast server initialized: %u sink / %u source ASE", + CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT, CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT); return 0; } diff --git a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/bluedroid/peripheral.c b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/bluedroid/peripheral.c index 6f28f783052..7cd82c2bfc8 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/bluedroid/peripheral.c +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/bluedroid/peripheral.c @@ -36,7 +36,7 @@ static esp_ble_gap_ext_adv_params_t ext_adv_params = { .secondary_phy = ESP_BLE_GAP_PHY_2M, .sid = ADV_SID, .scan_req_notif = false, - .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .own_addr_type = BLE_ADDR_TYPE_RANDOM, .tx_power = ADV_TX_POWER, }; @@ -48,6 +48,10 @@ static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch (event) { + case ESP_GAP_BLE_EXT_ADV_SET_RAND_ADDR_COMPLETE_EVT: + adv_op_status = param->ext_adv_set_rand_addr.status; + xSemaphoreGive(adv_sem); + break; case ESP_GAP_BLE_EXT_ADV_SET_PARAMS_COMPLETE_EVT: adv_op_status = param->ext_adv_set_params.status; xSemaphoreGive(adv_sem); @@ -107,10 +111,14 @@ int set_device_name(void) int ext_adv_start(const uint8_t *ext_data, uint16_t ext_len) { + esp_bd_addr_t addr = ADV_ADDR; + WAIT_API(esp_ble_gap_ext_adv_set_params(ADV_HANDLE, &ext_adv_params)); + WAIT_API(esp_ble_gap_ext_adv_set_rand_addr(ADV_HANDLE, addr)); WAIT_API(esp_ble_gap_config_ext_adv_data_raw(ADV_HANDLE, ext_len, ext_data)); WAIT_API(esp_ble_gap_ext_adv_start(1, ext_adv_inst)); - ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); + ESP_LOGI(TAG, "Advertising started (handle %u) as %02x:%02x:%02x:%02x:%02x:%02x", + ADV_HANDLE, addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]); return 0; } diff --git a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/csip_set_member.c b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/csip_set_member.c index cc42a9d6819..a71f8fdcb95 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/csip_set_member.c +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/csip_set_member.c @@ -21,7 +21,15 @@ static void csip_lock_changed_cb(esp_ble_conn_t *conn, esp_ble_audio_csip_set_member_svc_inst_t *inst, bool locked) { - ESP_LOGI(TAG, "Client %p %s the lock", conn, locked ? "locked" : "released"); + /* No conn means the server released it itself, on the lock timeout or from + * bt_csip_set_member_lock(), rather than a client asking for it. */ + if (conn == NULL) { + ESP_LOGI(TAG, "Lock %s locally", locked ? "taken" : "released"); + return; + } + + ESP_LOGI(TAG, "Client on handle %u %s the lock", conn->handle, + locked ? "locked" : "released"); } static uint8_t sirk_read_req_cb(esp_ble_conn_t *conn, @@ -40,7 +48,9 @@ int csip_set_member_init(void) esp_ble_audio_csip_set_member_register_param_t param = { .set_size = 2, .rank = CONFIG_EXAMPLE_TMAP_PER_SET_RANK, - .lockable = false, + /* Lockable so the coordinator can run the CSIP ordered access procedure + * (rank-ordered lock) around stream setup, as a real CAP initiator does. */ + .lockable = true, .sirk = CSIP_SIRK_DEBUG, .cb = &csip_cb, }; diff --git a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/nimble/peripheral.c b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/nimble/peripheral.c index b7c305bec42..02841d74db0 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/nimble/peripheral.c +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/nimble/peripheral.c @@ -62,12 +62,25 @@ int ext_adv_start(const uint8_t *ext_data, uint16_t ext_len) { struct ble_gap_ext_adv_params ext_params = {0}; struct os_mbuf *data = NULL; + const uint8_t addr[6] = ADV_ADDR; + uint8_t addr_le[6]; int err; + /* NimBLE takes the address least significant byte first. */ + for (size_t i = 0; i < sizeof(addr); i++) { + addr_le[i] = addr[sizeof(addr) - 1 - i]; + } + + err = ble_hs_id_set_rnd(addr_le); + if (err) { + ESP_LOGE(TAG, "Failed to set the static random address, err %d", err); + return err; + } + ext_params.connectable = 1; ext_params.scannable = 0; ext_params.legacy_pdu = 0; - ext_params.own_addr_type = BLE_OWN_ADDR_PUBLIC; + ext_params.own_addr_type = BLE_OWN_ADDR_RANDOM; ext_params.primary_phy = BLE_HCI_LE_PHY_1M; ext_params.secondary_phy = BLE_HCI_LE_PHY_2M; ext_params.tx_power = ADV_TX_POWER; diff --git a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/tmap_peripheral.h b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/tmap_peripheral.h index 6baa3058354..5b071949548 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/tmap_peripheral.h +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/tmap_peripheral.h @@ -37,6 +37,22 @@ #define ADV_HANDLE 0 #define ADV_SID 0 + +/* Advertise a static random address rather than whatever the factory + * programmed: two boards sharing an address corrupt every address-keyed lookup + * in the stack — bonding, the CSIS lock holder, the central's member dedup. + * The last byte is the set rank, which CSIS already requires to be unique + * within the set. The two most significant bits are 1, as static random needs. + */ +#if defined(CONFIG_EXAMPLE_TMAP_PER_SET_RANK) +#define ADV_ADDR_ID CONFIG_EXAMPLE_TMAP_PER_SET_RANK +#else +#define ADV_ADDR_ID 1 +#endif + +/* Most significant byte first, as esp_bd_addr_t and the logs print it. */ +#define ADV_ADDR { 0xC0, 0xDE, 0x00, 0x00, 0x00, ADV_ADDR_ID } + #define ADV_TX_POWER 127 #define ADV_INTERVAL_MS 200 diff --git a/examples/bluetooth/esp_ble_audio/tmap/peripheral/sdkconfig.defaults b/examples/bluetooth/esp_ble_audio/tmap/peripheral/sdkconfig.defaults index bfd6b33c04b..2e9c2c2f928 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/peripheral/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/sdkconfig.defaults @@ -19,6 +19,9 @@ CONFIG_BT_ISO_MAX_CHAN=2 CONFIG_BT_TMAP=y CONFIG_BT_CAP_ACCEPTOR=y CONFIG_BT_BAP_UNICAST_SERVER=y +CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT=2 +CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT=1 +CONFIG_BT_ASCS_MAX_ACTIVE_ASES=3 CONFIG_BT_PAC_SNK_NOTIFIABLE=y CONFIG_BT_PAC_SNK_LOC_WRITEABLE=y CONFIG_BT_PAC_SNK_LOC_NOTIFIABLE=y @@ -40,8 +43,27 @@ CONFIG_BT_TBS_CLIENT_TERMINATE_CALL=y CONFIG_BT_TBS_CLIENT_BEARER_URI_SCHEMES_SUPPORTED_LIST=y CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=60 -CONFIG_EXAMPLE_TMAP_PER_DUO=y - CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y CONFIG_FREERTOS_HZ=1000 + +# To place LE Audio .bss + control-plane heap in PSRAM, uncomment the +# lines below (needs a PSRAM-capable target, e.g. esp32s31/esp32h4). +# CONFIG_SPIRAM=y +# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_HEAP_EXTERNAL_MEMORY=y +# CONFIG_BT_AUDIO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_AUDIO_HEAP_EXTERNAL_MEMORY=y + +# Bandwidth-optimized (critical-only) compressed BLE Audio/ISO logging over +# UART DMA. Pins shown are for esp32s31 (UART0 TX = GPIO58, 3000000 baud); set +# CONFIG_BLE_LOG_PRPH_UART_DMA_* values to the port/pin your capture reads. +# CONFIG_BLE_LOG_ENABLED=y +# CONFIG_BT_LOG_CRITICAL_ONLY=y +# CONFIG_BLE_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_LOG_PRPH_UART_DMA=y +# CONFIG_BLE_LOG_PRPH_UART_DMA_PORT=0 +# CONFIG_BLE_LOG_PRPH_UART_DMA_BAUD_RATE=3000000 +# CONFIG_BLE_LOG_PRPH_UART_DMA_TX_IO_NUM=58 diff --git a/examples/bluetooth/esp_ble_iso/big_broadcaster/sdkconfig.defaults b/examples/bluetooth/esp_ble_iso/big_broadcaster/sdkconfig.defaults index 8cad6879b3c..0b754ed2a1c 100644 --- a/examples/bluetooth/esp_ble_iso/big_broadcaster/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_iso/big_broadcaster/sdkconfig.defaults @@ -17,3 +17,22 @@ CONFIG_BT_ISO_MAX_CHAN=2 CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y CONFIG_FREERTOS_HZ=1000 + +# To place BLE ISO .bss + control-plane heap in PSRAM, uncomment the +# lines below (needs a PSRAM-capable target, e.g. esp32s31/esp32h4). +# CONFIG_SPIRAM=y +# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_HEAP_EXTERNAL_MEMORY=y + +# Bandwidth-optimized (critical-only) compressed BLE Audio/ISO logging over +# UART DMA. Pins shown are for esp32s31 (UART0 TX = GPIO58, 3000000 baud); set +# CONFIG_BLE_LOG_PRPH_UART_DMA_* values to the port/pin your capture reads. +# CONFIG_BLE_LOG_ENABLED=y +# CONFIG_BT_LOG_CRITICAL_ONLY=y +# CONFIG_BLE_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_LOG_PRPH_UART_DMA=y +# CONFIG_BLE_LOG_PRPH_UART_DMA_PORT=0 +# CONFIG_BLE_LOG_PRPH_UART_DMA_BAUD_RATE=3000000 +# CONFIG_BLE_LOG_PRPH_UART_DMA_TX_IO_NUM=58 diff --git a/examples/bluetooth/esp_ble_iso/big_receiver/sdkconfig.defaults b/examples/bluetooth/esp_ble_iso/big_receiver/sdkconfig.defaults index bb0d86ba91f..adf1860d501 100644 --- a/examples/bluetooth/esp_ble_iso/big_receiver/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_iso/big_receiver/sdkconfig.defaults @@ -17,3 +17,22 @@ CONFIG_BT_ISO_MAX_CHAN=2 CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y CONFIG_FREERTOS_HZ=1000 + +# To place BLE ISO .bss + control-plane heap in PSRAM, uncomment the +# lines below (needs a PSRAM-capable target, e.g. esp32s31/esp32h4). +# CONFIG_SPIRAM=y +# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_HEAP_EXTERNAL_MEMORY=y + +# Bandwidth-optimized (critical-only) compressed BLE Audio/ISO logging over +# UART DMA. Pins shown are for esp32s31 (UART0 TX = GPIO58, 3000000 baud); set +# CONFIG_BLE_LOG_PRPH_UART_DMA_* values to the port/pin your capture reads. +# CONFIG_BLE_LOG_ENABLED=y +# CONFIG_BT_LOG_CRITICAL_ONLY=y +# CONFIG_BLE_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_LOG_PRPH_UART_DMA=y +# CONFIG_BLE_LOG_PRPH_UART_DMA_PORT=0 +# CONFIG_BLE_LOG_PRPH_UART_DMA_BAUD_RATE=3000000 +# CONFIG_BLE_LOG_PRPH_UART_DMA_TX_IO_NUM=58 diff --git a/examples/bluetooth/esp_ble_iso/cis_central/README.md b/examples/bluetooth/esp_ble_iso/cis_central/README.md index 47dc7eddd0a..4a6f4d8a2f6 100644 --- a/examples/bluetooth/esp_ble_iso/cis_central/README.md +++ b/examples/bluetooth/esp_ble_iso/cis_central/README.md @@ -9,7 +9,9 @@ This is a raw BLE Connected Isochronous Stream (CIS) example operating directly at the ISO transport layer over either the NimBLE or Bluedroid host (selected at build time via Kconfig). 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 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 **two-CIS** CIG (10 ms SDU interval, 2M PHY, RTN 2, 120-byte SDU, sequential/unframed), connects both CIS, configures each input data path to the HCI in transparent format, and then drives one software TX scheduler per CIS that submits one SDU every 10 ms. + +Both CIS ride the same ACL connection to the same peer, which is what `esp_ble_iso_chan_connect()` expresses: it takes one ACL handle plus a channel count. Each stream is otherwise independent — its own timer, sequence numbering and TX counters — because the controller establishes them one after another, so a shared scheduler would send on a CIS that is not up yet and would report both streams under a single name. 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 via the `esp_ble_iso_*` APIs. @@ -26,7 +28,7 @@ The transmitted payload is a dummy buffer filled with the current sequence numbe idf.py menuconfig ``` -No build-time options — runtime defaults are baked into source. +No menuconfig options — runtime defaults are baked into source. The number of CIS is the `CIS_COUNT` macro in `main/main.c`; raising it also requires `CONFIG_BT_ISO_MAX_CHAN` in `sdkconfig.defaults` to be at least that value (the host default is 1, so a second CIS is rejected without it). ### Security & Pairing @@ -60,9 +62,9 @@ For `esp32s31`, replace the chip overlay accordingly. 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. +5. After the security change, call `esp_ble_iso_cig_create` (two CIS, 10 ms latencies and SDU interval, sequential/unframed, SCA unknown) and `esp_ble_iso_chan_connect` once for both channels over the same ACL handle. +6. The controller establishes the CIS one at a time. On each CIS connect, set up that channel's input data path (HCI / transparent) and start its own periodic TX scheduler. +7. Each scheduler invokes `esp_ble_iso_chan_send` every 10 ms on its own channel, with a per-CIS incrementing sequence number on a 120-byte dummy SDU. ## Expected Log @@ -76,17 +78,22 @@ I CIS_CEN: Connected: handle role peer XX:XX:XX:XX:XX:XX I CIS_CEN: Security: handle level bonded ``` -CIS setup and streaming phase (TX log emitted every `LOG_INTERVAL_PACKETS` SDUs by the shared utility): +CIS setup and streaming phase. The two CIS come up a couple of hundred milliseconds apart, and each reports its own count (TX log emitted every `LOG_INTERVAL_PACKETS` SDUs by the shared utility): ``` I CIS_CEN: [CIS #0] Connected +I CIS_CEN: [CIS #1] Connected I CIS_CEN: [CIS #0] TX: packets +I CIS_CEN: [CIS #1] TX: packets ``` -Disconnect path: +There should be no `IsoSendChanNotConn` errors in the gap between the two `Connected` lines: each CIS only starts its timer once it is established, so nothing is submitted to a stream that is still connecting. + +Disconnect path (per CIS, then the ACL): ``` I CIS_CEN: [CIS #0] Disconnected, reason 0x +I CIS_CEN: [CIS #1] Disconnected, reason 0x I CIS_CEN: Disconnected: handle reason 0x ``` @@ -97,6 +104,6 @@ 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: packets` periodically. +4. After the security change, the central creates the CIG and connects both CIS over that one ACL. +5. Both sides set up a data path per CIS (input on central, output on peripheral). +6. The central streams 120-byte SDUs every 10 ms on each CIS; the peripheral reports `RX: packets` per CIS 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 76ced1a872c..e882ecf1aed 100644 --- a/examples/bluetooth/esp_ble_iso/cis_central/main/main.c +++ b/examples/bluetooth/esp_ble_iso/cis_central/main/main.c @@ -5,6 +5,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include #include #include @@ -31,17 +32,37 @@ #define CIS_PHY ESP_BLE_ISO_PHY_2M #define CIS_RTN 2 #define CIS_SDU_SIZE 120 +#define CIS_COUNT 2 static bool acl_connected; -static uint16_t iso_seq_num; -static uint8_t iso_data[CIS_SDU_SIZE]; - static esp_ble_iso_cig_t *out_cig; -static example_iso_tx_scheduler_t tx_scheduler; +/* Each CIS is streamed independently: it comes up at its own time and carries + * its own sequence numbering, so it gets its own timer and TX counters instead + * of sharing one scheduler (which would report both under a single name). */ +struct cis_ctx { + esp_ble_iso_chan_t chan; + example_iso_tx_scheduler_t scheduler; + uint16_t seq_num; + uint8_t data[CIS_SDU_SIZE]; + char name[8]; +}; -static void iso_chan_send(void); +static struct cis_ctx cis_ctxs[CIS_COUNT]; + +static void iso_chan_send(struct cis_ctx *ctx); + +static struct cis_ctx *ctx_by_chan(const esp_ble_iso_chan_t *chan) +{ + for (size_t i = 0; i < ARRAY_SIZE(cis_ctxs); i++) { + if (&cis_ctxs[i].chan == chan) { + return &cis_ctxs[i]; + } + } + + return NULL; +} static void iso_connected_cb(esp_ble_iso_chan_t *chan) { @@ -49,38 +70,40 @@ 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, }; + struct cis_ctx *ctx = ctx_by_chan(chan); esp_err_t err; - ESP_LOGI(TAG, "[CIS #0] Connected"); + ESP_LOGI(TAG, "[%s] Connected", ctx->name); err = esp_ble_iso_setup_data_path(chan, ESP_BLE_ISO_DATA_PATH_DIR_INPUT, &data_path); if (err) { - ESP_LOGE(TAG, "[CIS #0] Failed to setup data path, err %d", err); + ESP_LOGE(TAG, "[%s] Failed to setup data path, err %d", ctx->name, err); return; } - iso_seq_num = 0; - example_iso_tx_scheduler_reset(&tx_scheduler); + ctx->seq_num = 0; + example_iso_tx_scheduler_reset(&ctx->scheduler); /* Note: esp timer is not accurate enough */ - err = example_iso_tx_scheduler_start(&tx_scheduler, CIG_SDU_INTERVAL_US); + err = example_iso_tx_scheduler_start(&ctx->scheduler, CIG_SDU_INTERVAL_US); if (err) { - ESP_LOGE(TAG, "[CIS #0] Scheduler start failed, err %d", err); + ESP_LOGE(TAG, "[%s] Scheduler start failed, err %d", ctx->name, err); return; } - iso_chan_send(); + iso_chan_send(ctx); } static void iso_disconnected_cb(esp_ble_iso_chan_t *chan, uint8_t reason) { + struct cis_ctx *ctx = ctx_by_chan(chan); esp_err_t err; - ESP_LOGI(TAG, "[CIS #0] Disconnected, reason 0x%02x", reason); + ESP_LOGI(TAG, "[%s] Disconnected, reason 0x%02x", ctx->name, reason); - err = example_iso_tx_scheduler_stop(&tx_scheduler); + err = example_iso_tx_scheduler_stop(&ctx->scheduler); if (err) { - ESP_LOGE(TAG, "[CIS #0] Scheduler stop failed, err %d", err); + ESP_LOGE(TAG, "[%s] Scheduler stop failed, err %d", ctx->name, err); } /* Per BT Core 6.0 §7.7.5: a CIS on the Central retains its handle and @@ -91,13 +114,15 @@ static void iso_disconnected_cb(esp_ble_iso_chan_t *chan, uint8_t reason) */ 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); + ESP_LOGE(TAG, "[%s] Failed to remove data path, err %d", ctx->name, 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, "CIS #0"); + struct cis_ctx *ctx = ctx_by_chan(chan); + + example_iso_tx_scheduler_on_sent(&ctx->scheduler, user_data, TAG, ctx->name); } static esp_ble_iso_chan_ops_t iso_ops = { @@ -117,24 +142,21 @@ static esp_ble_iso_chan_qos_t iso_qos = { .rx = NULL, }; -static esp_ble_iso_chan_t iso_chan = { - .ops = &iso_ops, - .qos = &iso_qos, -}; - static void create_cig_and_cis(uint16_t acl_handle) { - esp_ble_iso_connect_param_t connect_param = {0}; + esp_ble_iso_connect_param_t connect_param[CIS_COUNT] = {0}; esp_ble_iso_cig_param_t cig_param = {0}; - esp_ble_iso_chan_t *channels[1] = {0}; + esp_ble_iso_chan_t *channels[CIS_COUNT] = {0}; int err; + for (size_t i = 0; i < ARRAY_SIZE(cis_ctxs); i++) { + channels[i] = &cis_ctxs[i].chan; + } + if (out_cig) { goto connect; } - channels[0] = &iso_chan; - cig_param.cis_channels = channels; cig_param.num_cis = ARRAY_SIZE(channels); cig_param.sca = CIG_SCA; @@ -152,37 +174,41 @@ static void create_cig_and_cis(uint16_t acl_handle) } connect: - connect_param.iso_chan = &iso_chan; + /* Both CIS ride the same ACL to the peer. */ + for (size_t i = 0; i < ARRAY_SIZE(connect_param); i++) { + connect_param[i].iso_chan = &cis_ctxs[i].chan; + } - err = esp_ble_iso_chan_connect(&connect_param, acl_handle, 1); + err = esp_ble_iso_chan_connect(connect_param, acl_handle, ARRAY_SIZE(connect_param)); if (err) { ESP_LOGE(TAG, "Failed to create CIS, err %d", err); return; } } -static void iso_chan_send(void) +static void iso_chan_send(struct cis_ctx *ctx) { int err; - memset(iso_data, (uint8_t)iso_seq_num, sizeof(iso_data)); + memset(ctx->data, (uint8_t)ctx->seq_num, sizeof(ctx->data)); - err = esp_ble_iso_chan_send(&iso_chan, - iso_data, - sizeof(iso_data), - iso_seq_num); + err = esp_ble_iso_chan_send(&ctx->chan, + ctx->data, + sizeof(ctx->data), + ctx->seq_num); if (err) { - ESP_LOGD(TAG, "[CIS #0] Send failed, err %d", err); + ESP_LOGD(TAG, "[%s] Send failed, err %d", ctx->name, err); return; } - iso_seq_num++; + ctx->seq_num++; } +/* Each CIS has its own timer, so the scheduler only ever feeds its own channel; + * a CIS that is not established yet has no timer running and is never sent to. */ static void tx_scheduler_cb(void *arg) { - (void)arg; - iso_chan_send(); + iso_chan_send(arg); } static bool data_cb(uint8_t type, const uint8_t *data, @@ -343,12 +369,21 @@ void app_main(void) return; } - err = example_iso_tx_scheduler_init(&tx_scheduler, - tx_scheduler_cb, - NULL); - if (err) { - ESP_LOGE(TAG, "Failed to init tx scheduler, err %d", err); - return; + /* All CIS share the callbacks and TX QoS; each keeps its own timer, fed + * with its own context so the scheduler knows which channel to send on. */ + for (size_t i = 0; i < ARRAY_SIZE(cis_ctxs); i++) { + cis_ctxs[i].chan.ops = &iso_ops; + cis_ctxs[i].chan.qos = &iso_qos; + snprintf(cis_ctxs[i].name, sizeof(cis_ctxs[i].name), "CIS #%zu", i); + + err = example_iso_tx_scheduler_init(&cis_ctxs[i].scheduler, + tx_scheduler_cb, + &cis_ctxs[i]); + if (err) { + ESP_LOGE(TAG, "[%s] Failed to init tx scheduler, err %d", + cis_ctxs[i].name, err); + return; + } } err = set_device_name(); diff --git a/examples/bluetooth/esp_ble_iso/cis_central/sdkconfig.defaults b/examples/bluetooth/esp_ble_iso/cis_central/sdkconfig.defaults index 6da20d7e9c1..3b0a180854b 100644 --- a/examples/bluetooth/esp_ble_iso/cis_central/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_iso/cis_central/sdkconfig.defaults @@ -12,7 +12,27 @@ CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y CONFIG_BT_BLE_FEAT_ISO_EN=y CONFIG_BT_ISO_CENTRAL=y +CONFIG_BT_ISO_MAX_CHAN=2 CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y CONFIG_FREERTOS_HZ=1000 + +# To place BLE ISO .bss + control-plane heap in PSRAM, uncomment the +# lines below (needs a PSRAM-capable target, e.g. esp32s31/esp32h4). +# CONFIG_SPIRAM=y +# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_HEAP_EXTERNAL_MEMORY=y + +# Bandwidth-optimized (critical-only) compressed BLE Audio/ISO logging over +# UART DMA. Pins shown are for esp32s31 (UART0 TX = GPIO58, 3000000 baud); set +# CONFIG_BLE_LOG_PRPH_UART_DMA_* values to the port/pin your capture reads. +# CONFIG_BLE_LOG_ENABLED=y +# CONFIG_BT_LOG_CRITICAL_ONLY=y +# CONFIG_BLE_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_LOG_PRPH_UART_DMA=y +# CONFIG_BLE_LOG_PRPH_UART_DMA_PORT=0 +# CONFIG_BLE_LOG_PRPH_UART_DMA_BAUD_RATE=3000000 +# CONFIG_BLE_LOG_PRPH_UART_DMA_TX_IO_NUM=58 diff --git a/examples/bluetooth/esp_ble_iso/cis_peripheral/README.md b/examples/bluetooth/esp_ble_iso/cis_peripheral/README.md index d1da73f0924..8bb24898ad5 100644 --- a/examples/bluetooth/esp_ble_iso/cis_peripheral/README.md +++ b/examples/bluetooth/esp_ble_iso/cis_peripheral/README.md @@ -9,7 +9,9 @@ This is a raw BLE Connected Isochronous Stream (CIS) example operating directly at the ISO transport layer over either the NimBLE or Bluedroid host (selected at build time via Kconfig). 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. +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 next free CIS channel slot, sets up the output data path to the HCI in transparent format when each CIS connects, and tallies received SDUs through the shared RX-metrics helper. + +It accepts **two CIS** from the central, both on the same ACL connection. The central establishes them one after another, so the accept callback is invoked twice and returns a different channel each time; each channel keeps its own RX metrics so the two streams are reported separately. 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 via the `esp_ble_iso_*` APIs. @@ -24,7 +26,7 @@ There is no audio decoding — incoming SDUs are simply counted (valid / error / idf.py menuconfig ``` -No build-time options — runtime defaults are baked into source. +No menuconfig options — runtime defaults are baked into source. The number of CIS accepted is the `CIS_COUNT` macro in `main/main.c`; it must be matched by `CONFIG_BT_ISO_MAX_CHAN` in `sdkconfig.defaults` (the host default is 1, so a second CIS would be rejected) and by the central's own `CIS_COUNT`. ### Security & Pairing @@ -55,11 +57,11 @@ For `esp32s31`, replace the chip overlay accordingly. ## Example Flow 1. Initialize NVS, the selected BLE 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. +2. Register an ISO server (`esp_ble_iso_server_register`) whose accept callback returns the next CIS channel that is not already carrying one. 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. +5. Each CIS request invokes the accept callback, which supplies a free channel; on CIS connect, set up that channel's output data path (HCI / transparent) and reset its RX metrics. +6. Each received SDU updates the valid/error/lost counters of the channel it arrived on; periodic per-CIS RX summaries are logged by the shared utility. 7. On ACL disconnect, restart extended advertising. ## Expected Log @@ -74,18 +76,24 @@ I CIS_PER: Connected: handle role peer XX:XX:XX:XX:XX:XX I CIS_PER: Security: handle level bonded ``` -CIS accept and streaming phase (RX log emitted every `LOG_INTERVAL_PACKETS` SDUs by the shared utility): +CIS accept and streaming phase. Both requests arrive on the same ACL handle, one after the other, and each CIS reports its own count (RX log emitted every `LOG_INTERVAL_PACKETS` SDUs by the shared utility): ``` I CIS_PER: Incoming CIS request from handle I CIS_PER: [CIS #0] Connected +I CIS_PER: Incoming CIS request from handle +I CIS_PER: [CIS #1] Connected I CIS_PER: [CIS #0] RX: packets +I CIS_PER: [CIS #1] RX: packets ``` -Disconnect path: +A `No channels available` error on a request means more CIS were offered than `CIS_COUNT` provides for. + +Disconnect path (per CIS, then the ACL): ``` I CIS_PER: [CIS #0] Disconnected, reason 0x +I CIS_PER: [CIS #1] Disconnected, reason 0x I CIS_PER: Disconnected: handle reason 0x ``` @@ -96,6 +104,6 @@ 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: packets` is logged periodically. +4. The central creates the CIG and connects both CIS over that one ACL; the peripheral's accept callback returns a free local channel for each. +5. The peripheral configures an output data path per CIS as each one connects. +6. Incoming 120-byte SDUs are counted per CIS and `[CIS #] RX: packets` is logged periodically for each. 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 d4e6dd0286f..a94a18b08ce 100644 --- a/examples/bluetooth/esp_ble_iso/cis_peripheral/main/main.c +++ b/examples/bluetooth/esp_ble_iso/cis_peripheral/main/main.c @@ -6,6 +6,7 @@ */ #include +#include #include #include @@ -20,10 +21,29 @@ #include "peripheral.h" #define CIS_SDU_SIZE 120 +#define CIS_COUNT 2 static uint8_t ext_adv_data[3 + 2 + LOCAL_DEVICE_NAME_LEN]; -static example_iso_rx_metrics_t rx_metrics; +/* Each CIS is received and reported independently, so it keeps its own metrics. */ +struct cis_ctx { + esp_ble_iso_chan_t chan; + example_iso_rx_metrics_t rx_metrics; + char name[8]; +}; + +static struct cis_ctx cis_ctxs[CIS_COUNT]; + +static struct cis_ctx *ctx_by_chan(const esp_ble_iso_chan_t *chan) +{ + for (size_t i = 0; i < ARRAY_SIZE(cis_ctxs); i++) { + if (&cis_ctxs[i].chan == chan) { + return &cis_ctxs[i]; + } + } + + return NULL; +} static void iso_connected_cb(esp_ble_iso_chan_t *chan) { @@ -31,30 +51,35 @@ 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, }; + struct cis_ctx *ctx = ctx_by_chan(chan); esp_err_t err; - ESP_LOGI(TAG, "[CIS #0] Connected"); + ESP_LOGI(TAG, "[%s] Connected", ctx->name); err = esp_ble_iso_setup_data_path(chan, ESP_BLE_ISO_DATA_PATH_DIR_OUTPUT, &data_path); if (err) { - ESP_LOGE(TAG, "[CIS #0] Failed to setup data path, err %d", err); + ESP_LOGE(TAG, "[%s] Failed to setup data path, err %d", ctx->name, err); return; } - example_iso_rx_metrics_reset(&rx_metrics); + example_iso_rx_metrics_reset(&ctx->rx_metrics); } static void iso_disconnected_cb(esp_ble_iso_chan_t *chan, uint8_t reason) { - ESP_LOGI(TAG, "[CIS #0] Disconnected, reason 0x%02x", reason); + struct cis_ctx *ctx = ctx_by_chan(chan); + + ESP_LOGI(TAG, "[%s] Disconnected, reason 0x%02x", ctx->name, 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, "CIS #0"); + struct cis_ctx *ctx = ctx_by_chan(chan); + + ctx->rx_metrics.last_sdu_len = len; + example_iso_rx_metrics_on_recv(info, &ctx->rx_metrics, TAG, ctx->name); } static esp_ble_iso_chan_ops_t iso_ops = { @@ -72,24 +97,23 @@ static esp_ble_iso_chan_qos_t iso_qos = { .tx = NULL, }; -static esp_ble_iso_chan_t iso_chan = { - .ops = &iso_ops, - .qos = &iso_qos, -}; - static int iso_accept(const esp_ble_iso_accept_info_t *info, esp_ble_iso_chan_t **chan) { ESP_LOGI(TAG, "Incoming CIS request from handle %u", info->acl->handle); - if (iso_chan.iso) { - ESP_LOGE(TAG, "No channels available"); - return -ENOMEM; + /* The central establishes the CIS one at a time, so hand out the next + * channel that is not already carrying one. */ + for (size_t i = 0; i < ARRAY_SIZE(cis_ctxs); i++) { + if (cis_ctxs[i].chan.iso == NULL) { + *chan = &cis_ctxs[i].chan; + return 0; + } } - *chan = &iso_chan; + ESP_LOGE(TAG, "No channels available"); - return 0; + return -ENOMEM; } static esp_ble_iso_server_t iso_server = { @@ -189,6 +213,14 @@ void app_main(void) return; } + /* All CIS share the same callbacks and RX QoS; they differ only by which + * channel iso_accept() hands out and where its metrics are kept. */ + for (size_t i = 0; i < ARRAY_SIZE(cis_ctxs); i++) { + cis_ctxs[i].chan.ops = &iso_ops; + cis_ctxs[i].chan.qos = &iso_qos; + snprintf(cis_ctxs[i].name, sizeof(cis_ctxs[i].name), "CIS #%zu", i); + } + err = esp_ble_iso_server_register(&iso_server); if (err) { ESP_LOGE(TAG, "Failed to register ISO server, err %d", err); diff --git a/examples/bluetooth/esp_ble_iso/cis_peripheral/sdkconfig.defaults b/examples/bluetooth/esp_ble_iso/cis_peripheral/sdkconfig.defaults index b412ca65563..cd5c479f421 100644 --- a/examples/bluetooth/esp_ble_iso/cis_peripheral/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_iso/cis_peripheral/sdkconfig.defaults @@ -12,7 +12,27 @@ CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y CONFIG_BT_BLE_FEAT_ISO_EN=y CONFIG_BT_ISO_PERIPHERAL=y +CONFIG_BT_ISO_MAX_CHAN=2 CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y CONFIG_FREERTOS_HZ=1000 + +# To place BLE ISO .bss + control-plane heap in PSRAM, uncomment the +# lines below (needs a PSRAM-capable target, e.g. esp32s31/esp32h4). +# CONFIG_SPIRAM=y +# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_BSS_SEG_EXTERNAL_MEMORY=y +# CONFIG_BT_ISO_HEAP_EXTERNAL_MEMORY=y + +# Bandwidth-optimized (critical-only) compressed BLE Audio/ISO logging over +# UART DMA. Pins shown are for esp32s31 (UART0 TX = GPIO58, 3000000 baud); set +# CONFIG_BLE_LOG_PRPH_UART_DMA_* values to the port/pin your capture reads. +# CONFIG_BLE_LOG_ENABLED=y +# CONFIG_BT_LOG_CRITICAL_ONLY=y +# CONFIG_BLE_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE=y +# CONFIG_BLE_LOG_PRPH_UART_DMA=y +# CONFIG_BLE_LOG_PRPH_UART_DMA_PORT=0 +# CONFIG_BLE_LOG_PRPH_UART_DMA_BAUD_RATE=3000000 +# CONFIG_BLE_LOG_PRPH_UART_DMA_TX_IO_NUM=58