From 8dbef3bad409e2682d0a0dd29bc4e7f518b46577 Mon Sep 17 00:00:00 2001 From: Liu Linyan Date: Mon, 18 May 2026 20:31:24 +0800 Subject: [PATCH] feat(ble_audio): Support running ISO & LE Audio with Bluedroid Host --- components/bt/esp_ble_audio/CMakeLists.txt | 41 +- .../bt/esp_ble_audio/Kconfig/Kconfig.mcs.in | 4 +- .../bt/esp_ble_audio/Kconfig/Kconfig.mpl.in | 5 +- .../api/esp_ble_audio_common_api.c | 9 + .../api/include/esp_ble_audio_common_api.h | 22 +- .../bluedroid/include/bluedroid/init.h | 43 + .../include/bluedroid/profiles/ascs.h | 25 + .../include/bluedroid/profiles/bass.h | 25 + .../include/bluedroid/profiles/cas.h | 25 + .../include/bluedroid/profiles/csis.h | 25 + .../include/bluedroid/profiles/has.h | 25 + .../include/bluedroid/profiles/mcs.h | 25 + .../include/bluedroid/profiles/mics.h | 25 + .../include/bluedroid/profiles/pacs.h | 25 + .../include/bluedroid/profiles/tbs.h | 25 + .../include/bluedroid/profiles/tmas.h | 25 + .../include/bluedroid/profiles/vcs.h | 25 + .../bluedroid/include/bluedroid/server.h | 172 + .../host/adapter/bluedroid/init.c | 342 ++ .../host/adapter/bluedroid/profiles/ascs.c | 56 + .../host/adapter/bluedroid/profiles/bass.c | 56 + .../host/adapter/bluedroid/profiles/cas.c | 105 + .../host/adapter/bluedroid/profiles/csis.c | 76 + .../host/adapter/bluedroid/profiles/has.c | 56 + .../host/adapter/bluedroid/profiles/mcs.c | 52 + .../host/adapter/bluedroid/profiles/mics.c | 136 + .../host/adapter/bluedroid/profiles/pacs.c | 56 + .../host/adapter/bluedroid/profiles/tbs.c | 52 + .../host/adapter/bluedroid/profiles/tmas.c | 56 + .../host/adapter/bluedroid/profiles/vcs.c | 169 + .../host/adapter/bluedroid/server.c | 596 ++++ .../include/nimble/{profiles => }/server.h | 0 .../esp_ble_audio/host/adapter/nimble/init.c | 9 +- .../host/adapter/nimble/profiles/ascs.c | 12 +- .../host/adapter/nimble/profiles/bass.c | 12 +- .../host/adapter/nimble/profiles/cas.c | 10 +- .../host/adapter/nimble/profiles/csis.c | 2 +- .../host/adapter/nimble/profiles/has.c | 14 +- .../host/adapter/nimble/profiles/mcs.c | 12 +- .../host/adapter/nimble/profiles/mics.c | 17 +- .../host/adapter/nimble/profiles/pacs.c | 12 +- .../host/adapter/nimble/profiles/tbs.c | 12 +- .../host/adapter/nimble/profiles/tmas.c | 12 +- .../host/adapter/nimble/profiles/vcs.c | 38 +- .../adapter/nimble/{profiles => }/server.c | 0 .../bt/esp_ble_audio/host/common/init.c | 44 + components/bt/esp_ble_iso/CMakeLists.txt | 44 +- components/bt/esp_ble_iso/Kconfig.in | 13 +- .../esp_ble_iso/api/esp_ble_iso_common_api.c | 7 + .../api/include/esp_ble_iso_common_api.h | 15 + .../esp_ble_iso/host/adapter/bluedroid/gap.c | 506 +++ .../host/adapter/bluedroid/gatt/gatt.c | 3092 +++++++++++++++++ .../esp_ble_iso/host/adapter/bluedroid/hci.c | 198 ++ .../bluedroid/include/bluedroid/btm_error.h | 47 + .../adapter/bluedroid/include/bluedroid/gap.h | 32 + .../bluedroid/include/bluedroid/gatt.h | 133 + .../adapter/bluedroid/include/bluedroid/hci.h | 56 + .../adapter/bluedroid/include/bluedroid/iso.h | 30 + .../esp_ble_iso/host/adapter/bluedroid/iso.c | 1276 +++++++ .../bt/esp_ble_iso/host/adapter/nimble/gap.c | 41 +- .../host/adapter/nimble/gatt/gatt.c | 22 +- .../host/adapter/nimble/gatt/gatt.db.c | 56 +- .../host/adapter/nimble/gatt/gatt.nrp.c | 25 +- .../host/adapter/nimble/include/nimble/iso.h | 2 + .../bt/esp_ble_iso/host/adapter/nimble/iso.c | 20 +- .../esp_ble_iso/host/adapter/nimble/l2cap.c | 8 +- .../bt/esp_ble_iso/host/common/app/gap.c | 7 +- .../bt/esp_ble_iso/host/common/app/gatt.c | 22 + components/bt/esp_ble_iso/host/common/conn.c | 24 +- components/bt/esp_ble_iso/host/common/gatt.c | 93 +- components/bt/esp_ble_iso/host/common/hci.c | 4 + components/bt/esp_ble_iso/host/common/host.c | 32 +- .../host/common/include/common/app/gap.h | 4 + .../host/common/include/common/app/gatt.h | 25 + .../host/common/include/common/conn.h | 8 + .../host/common/include/common/gatt.h | 350 +- .../host/common/include/common/iso.h | 6 + .../host/common/include/common/l2cap.h | 4 + .../host/common/include/common/scan.h | 4 + .../host/common/include/common/task.h | 14 + components/bt/esp_ble_iso/host/common/iso.c | 20 + components/bt/esp_ble_iso/host/common/l2cap.c | 35 +- components/bt/esp_ble_iso/host/common/scan.c | 9 + .../bt/esp_ble_iso/include/zephyr/autoconf.h | 45 + .../bt/esp_ble_iso/include/zephyr/kernel.h | 89 + .../bap/broadcast_sink/README.md | 23 +- .../bap/broadcast_sink/main/CMakeLists.txt | 9 +- .../bap/broadcast_sink/main/bluedroid/scan.c | 117 + .../bap/broadcast_sink/main/main.c | 100 +- .../bap/broadcast_sink/main/nimble/scan.c | 90 + .../bap/broadcast_sink/main/scan.h | 27 + .../bap/broadcast_sink/sdkconfig.defaults | 20 +- .../broadcast_sink/sdkconfig.defaults.nimble | 12 + .../bap/broadcast_source/README.md | 19 +- .../bap/broadcast_source/main/CMakeLists.txt | 9 +- .../bap/broadcast_source/main/adv.h | 24 + .../bap/broadcast_source/main/bluedroid/adv.c | 122 + .../bap/broadcast_source/main/main.c | 157 +- .../bap/broadcast_source/main/nimble/adv.c | 115 + .../bap/broadcast_source/sdkconfig.defaults | 15 +- .../sdkconfig.defaults.nimble | 9 + .../bap/unicast_client/README.md | 25 +- .../bap/unicast_client/main/CMakeLists.txt | 9 +- .../unicast_client/main/bluedroid/central.c | 193 + .../bap/unicast_client/main/central.h | 46 + .../bap/unicast_client/main/main.c | 134 +- .../bap/unicast_client/main/nimble/central.c | 152 + .../bap/unicast_client/sdkconfig.defaults | 19 +- .../unicast_client/sdkconfig.defaults.nimble | 12 + .../bap/unicast_server/README.md | 33 +- .../bap/unicast_server/main/CMakeLists.txt | 9 +- .../main/bluedroid/peripheral.c | 117 + .../bap/unicast_server/main/main.c | 110 +- .../unicast_server/main/nimble/peripheral.c | 115 + .../bap/unicast_server/main/peripheral.h | 27 + .../bap/unicast_server/sdkconfig.defaults | 19 +- .../unicast_server/sdkconfig.defaults.nimble | 12 + .../esp_ble_audio/cap/acceptor/README.md | 30 +- .../cap/acceptor/main/CMakeLists.txt | 14 +- .../cap/acceptor/main/bluedroid/peripheral.c | 148 + .../cap/acceptor/main/bluedroid/scan.c | 130 + .../cap/acceptor/main/cap_acceptor.h | 44 +- .../acceptor/main/cap_acceptor_broadcast.c | 165 +- .../esp_ble_audio/cap/acceptor/main/main.c | 106 +- .../cap/acceptor/main/nimble/peripheral.c | 112 + .../cap/acceptor/main/nimble/scan.c | 125 + .../cap/acceptor/sdkconfig.defaults | 22 +- .../cap/acceptor/sdkconfig.defaults.nimble | 13 + .../esp_ble_audio/cap/initiator/README.md | 15 +- .../cap/initiator/main/CMakeLists.txt | 13 +- .../cap/initiator/main/bluedroid/adv.c | 127 + .../cap/initiator/main/bluedroid/central.c | 189 + .../cap/initiator/main/cap_initiator.h | 52 +- .../initiator/main/cap_initiator_broadcast.c | 161 +- .../cap/initiator/main/cap_initiator_tx.c | 1 - .../initiator/main/cap_initiator_unicast.c | 115 +- .../esp_ble_audio/cap/initiator/main/main.c | 22 +- .../cap/initiator/main/nimble/adv.c | 123 + .../cap/initiator/main/nimble/central.c | 149 + .../cap/initiator/sdkconfig.defaults | 19 +- .../cap/initiator/sdkconfig.defaults.nimble | 12 + .../example_init/ble_audio_example_init.c | 114 +- .../example_utils/ble_audio_example_utils.c | 60 - .../example_utils/ble_audio_example_utils.h | 63 +- .../esp_ble_audio/tmap/bmr/README.md | 17 +- .../tmap/bmr/main/CMakeLists.txt | 7 + .../tmap/bmr/main/bap_broadcast_sink.c | 70 +- .../tmap/bmr/main/bluedroid/scan.c | 117 + .../esp_ble_audio/tmap/bmr/main/main.c | 15 +- .../esp_ble_audio/tmap/bmr/main/nimble/scan.c | 90 + .../esp_ble_audio/tmap/bmr/main/tmap_bmr.h | 18 +- .../tmap/bmr/main/vcp_vol_renderer.c | 7 - .../esp_ble_audio/tmap/bmr/sdkconfig.defaults | 22 +- .../tmap/bmr/sdkconfig.defaults.nimble | 12 + .../esp_ble_audio/tmap/bms/README.md | 17 +- .../tmap/bms/main/CMakeLists.txt | 7 + .../tmap/bms/main/bluedroid/adv.c | 146 + .../tmap/bms/main/cap_initiator.c | 198 +- .../esp_ble_audio/tmap/bms/main/main.c | 17 +- .../esp_ble_audio/tmap/bms/main/nimble/adv.c | 147 + .../esp_ble_audio/tmap/bms/main/tmap_bms.h | 23 +- .../esp_ble_audio/tmap/bms/sdkconfig.defaults | 17 +- .../tmap/bms/sdkconfig.defaults.nimble | 10 + .../esp_ble_audio/tmap/central/README.md | 15 +- .../tmap/central/main/CMakeLists.txt | 7 + .../tmap/central/main/bluedroid/central.c | 193 + .../tmap/central/main/cap_initiator.c | 2 - .../tmap/central/main/ccp_server.c | 2 - .../esp_ble_audio/tmap/central/main/main.c | 125 +- .../tmap/central/main/mcp_server.c | 3 - .../tmap/central/main/nimble/central.c | 149 + .../tmap/central/main/tmap_central.h | 34 +- .../tmap/central/main/vcp_vol_ctlr.c | 1 - .../tmap/central/sdkconfig.defaults | 23 +- .../tmap/central/sdkconfig.defaults.nimble | 12 + .../esp_ble_audio/tmap/peripheral/README.md | 29 +- .../tmap/peripheral/main/CMakeLists.txt | 7 + .../peripheral/main/bluedroid/peripheral.c | 116 + .../tmap/peripheral/main/ccp_call_ctrl.c | 1 - .../tmap/peripheral/main/csip_set_member.c | 5 - .../esp_ble_audio/tmap/peripheral/main/main.c | 123 +- .../tmap/peripheral/main/mcp_ctlr.c | 6 +- .../tmap/peripheral/main/nimble/peripheral.c | 112 + .../tmap/peripheral/main/tmap_ct_umr.c | 3 - .../tmap/peripheral/main/tmap_peripheral.h | 20 +- .../tmap/peripheral/main/vcp_vol_renderer.c | 3 - .../tmap/peripheral/sdkconfig.defaults | 24 +- .../tmap/peripheral/sdkconfig.defaults.nimble | 12 + .../esp_ble_iso/big_broadcaster/README.md | 19 +- .../big_broadcaster/main/CMakeLists.txt | 9 +- .../esp_ble_iso/big_broadcaster/main/adv.h | 24 + .../big_broadcaster/main/bluedroid/adv.c | 121 + .../esp_ble_iso/big_broadcaster/main/main.c | 145 +- .../big_broadcaster/main/nimble/adv.c | 118 + .../big_broadcaster/sdkconfig.defaults | 14 +- .../big_broadcaster/sdkconfig.defaults.nimble | 9 + .../esp_ble_iso/big_receiver/README.md | 25 +- .../big_receiver/main/CMakeLists.txt | 9 +- .../big_receiver/main/bluedroid/scan.c | 116 + .../esp_ble_iso/big_receiver/main/main.c | 69 +- .../big_receiver/main/nimble/scan.c | 84 + .../esp_ble_iso/big_receiver/main/scan.h | 26 + .../big_receiver/sdkconfig.defaults | 14 +- .../big_receiver/sdkconfig.defaults.nimble | 9 + .../esp_ble_iso/cis_central/README.md | 23 +- .../cis_central/main/CMakeLists.txt | 9 +- .../cis_central/main/bluedroid/central.c | 184 + .../esp_ble_iso/cis_central/main/central.h | 44 + .../esp_ble_iso/cis_central/main/main.c | 128 +- .../cis_central/main/nimble/central.c | 150 + .../cis_central/sdkconfig.defaults | 16 +- .../cis_central/sdkconfig.defaults.nimble | 11 + .../esp_ble_iso/cis_peripheral/README.md | 21 +- .../cis_peripheral/main/CMakeLists.txt | 9 +- .../main/bluedroid/peripheral.c | 118 + .../esp_ble_iso/cis_peripheral/main/main.c | 110 +- .../cis_peripheral/main/nimble/peripheral.c | 108 + .../cis_peripheral/main/peripheral.h | 27 + .../cis_peripheral/sdkconfig.defaults | 16 +- .../cis_peripheral/sdkconfig.defaults.nimble | 11 + .../example_init/ble_iso_example_init.c | 114 +- .../example_utils/ble_iso_example_utils.c | 57 - .../example_utils/ble_iso_example_utils.h | 63 +- 223 files changed, 14714 insertions(+), 2347 deletions(-) create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/init.h create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/ascs.h create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/bass.h create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/cas.h create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/csis.h create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/has.h create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/mcs.h create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/mics.h create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/pacs.h create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/tbs.h create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/tmas.h create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/vcs.h create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/server.h create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/init.c create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/ascs.c create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/bass.c create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/cas.c create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/csis.c create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/has.c create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/mcs.c create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/mics.c create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/pacs.c create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/tbs.c create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/tmas.c create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/vcs.c create mode 100644 components/bt/esp_ble_audio/host/adapter/bluedroid/server.c rename components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/{profiles => }/server.h (100%) rename components/bt/esp_ble_audio/host/adapter/nimble/{profiles => }/server.c (100%) create mode 100644 components/bt/esp_ble_iso/host/adapter/bluedroid/gap.c create mode 100644 components/bt/esp_ble_iso/host/adapter/bluedroid/gatt/gatt.c create mode 100644 components/bt/esp_ble_iso/host/adapter/bluedroid/hci.c create mode 100644 components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/btm_error.h create mode 100644 components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/gap.h create mode 100644 components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/gatt.h create mode 100644 components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/hci.h create mode 100644 components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/iso.h create mode 100644 components/bt/esp_ble_iso/host/adapter/bluedroid/iso.c create mode 100644 examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/bluedroid/scan.c create mode 100644 examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/nimble/scan.c create mode 100644 examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/scan.h create mode 100644 examples/bluetooth/esp_ble_audio/bap/broadcast_sink/sdkconfig.defaults.nimble create mode 100644 examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/adv.h create mode 100644 examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/bluedroid/adv.c create mode 100644 examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/nimble/adv.c create mode 100644 examples/bluetooth/esp_ble_audio/bap/broadcast_source/sdkconfig.defaults.nimble create mode 100644 examples/bluetooth/esp_ble_audio/bap/unicast_client/main/bluedroid/central.c create mode 100644 examples/bluetooth/esp_ble_audio/bap/unicast_client/main/central.h create mode 100644 examples/bluetooth/esp_ble_audio/bap/unicast_client/main/nimble/central.c create mode 100644 examples/bluetooth/esp_ble_audio/bap/unicast_client/sdkconfig.defaults.nimble create mode 100644 examples/bluetooth/esp_ble_audio/bap/unicast_server/main/bluedroid/peripheral.c create mode 100644 examples/bluetooth/esp_ble_audio/bap/unicast_server/main/nimble/peripheral.c create mode 100644 examples/bluetooth/esp_ble_audio/bap/unicast_server/main/peripheral.h create mode 100644 examples/bluetooth/esp_ble_audio/bap/unicast_server/sdkconfig.defaults.nimble create mode 100644 examples/bluetooth/esp_ble_audio/cap/acceptor/main/bluedroid/peripheral.c create mode 100644 examples/bluetooth/esp_ble_audio/cap/acceptor/main/bluedroid/scan.c create mode 100644 examples/bluetooth/esp_ble_audio/cap/acceptor/main/nimble/peripheral.c create mode 100644 examples/bluetooth/esp_ble_audio/cap/acceptor/main/nimble/scan.c create mode 100644 examples/bluetooth/esp_ble_audio/cap/acceptor/sdkconfig.defaults.nimble create mode 100644 examples/bluetooth/esp_ble_audio/cap/initiator/main/bluedroid/adv.c create mode 100644 examples/bluetooth/esp_ble_audio/cap/initiator/main/bluedroid/central.c create mode 100644 examples/bluetooth/esp_ble_audio/cap/initiator/main/nimble/adv.c create mode 100644 examples/bluetooth/esp_ble_audio/cap/initiator/main/nimble/central.c create mode 100644 examples/bluetooth/esp_ble_audio/cap/initiator/sdkconfig.defaults.nimble create mode 100644 examples/bluetooth/esp_ble_audio/tmap/bmr/main/bluedroid/scan.c create mode 100644 examples/bluetooth/esp_ble_audio/tmap/bmr/main/nimble/scan.c create mode 100644 examples/bluetooth/esp_ble_audio/tmap/bmr/sdkconfig.defaults.nimble create mode 100644 examples/bluetooth/esp_ble_audio/tmap/bms/main/bluedroid/adv.c create mode 100644 examples/bluetooth/esp_ble_audio/tmap/bms/main/nimble/adv.c create mode 100644 examples/bluetooth/esp_ble_audio/tmap/bms/sdkconfig.defaults.nimble create mode 100644 examples/bluetooth/esp_ble_audio/tmap/central/main/bluedroid/central.c create mode 100644 examples/bluetooth/esp_ble_audio/tmap/central/main/nimble/central.c create mode 100644 examples/bluetooth/esp_ble_audio/tmap/central/sdkconfig.defaults.nimble create mode 100644 examples/bluetooth/esp_ble_audio/tmap/peripheral/main/bluedroid/peripheral.c create mode 100644 examples/bluetooth/esp_ble_audio/tmap/peripheral/main/nimble/peripheral.c create mode 100644 examples/bluetooth/esp_ble_audio/tmap/peripheral/sdkconfig.defaults.nimble create mode 100644 examples/bluetooth/esp_ble_iso/big_broadcaster/main/adv.h create mode 100644 examples/bluetooth/esp_ble_iso/big_broadcaster/main/bluedroid/adv.c create mode 100644 examples/bluetooth/esp_ble_iso/big_broadcaster/main/nimble/adv.c create mode 100644 examples/bluetooth/esp_ble_iso/big_broadcaster/sdkconfig.defaults.nimble create mode 100644 examples/bluetooth/esp_ble_iso/big_receiver/main/bluedroid/scan.c create mode 100644 examples/bluetooth/esp_ble_iso/big_receiver/main/nimble/scan.c create mode 100644 examples/bluetooth/esp_ble_iso/big_receiver/main/scan.h create mode 100644 examples/bluetooth/esp_ble_iso/big_receiver/sdkconfig.defaults.nimble create mode 100644 examples/bluetooth/esp_ble_iso/cis_central/main/bluedroid/central.c create mode 100644 examples/bluetooth/esp_ble_iso/cis_central/main/central.h create mode 100644 examples/bluetooth/esp_ble_iso/cis_central/main/nimble/central.c create mode 100644 examples/bluetooth/esp_ble_iso/cis_central/sdkconfig.defaults.nimble create mode 100644 examples/bluetooth/esp_ble_iso/cis_peripheral/main/bluedroid/peripheral.c create mode 100644 examples/bluetooth/esp_ble_iso/cis_peripheral/main/nimble/peripheral.c create mode 100644 examples/bluetooth/esp_ble_iso/cis_peripheral/main/peripheral.h create mode 100644 examples/bluetooth/esp_ble_iso/cis_peripheral/sdkconfig.defaults.nimble diff --git a/components/bt/esp_ble_audio/CMakeLists.txt b/components/bt/esp_ble_audio/CMakeLists.txt index 14008acf50c..f710811f627 100644 --- a/components/bt/esp_ble_audio/CMakeLists.txt +++ b/components/bt/esp_ble_audio/CMakeLists.txt @@ -29,13 +29,24 @@ endif() list(APPEND ble_audio_include_dirs "${CMAKE_CURRENT_LIST_DIR}/api/include" "${CMAKE_CURRENT_LIST_DIR}/host/common/include" - "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/include" "${CMAKE_CURRENT_LIST_DIR}/include" ) +if(CONFIG_BT_NIMBLE_ENABLED) + set(audio_adapter "host/adapter/nimble") +elseif(CONFIG_BT_BLUEDROID_ENABLED) + set(audio_adapter "host/adapter/bluedroid") +endif() + +list(APPEND ble_audio_include_dirs + "${CMAKE_CURRENT_LIST_DIR}/${audio_adapter}/include" +) +list(APPEND ble_audio_srcs + "${CMAKE_CURRENT_LIST_DIR}/${audio_adapter}/server.c" + "${CMAKE_CURRENT_LIST_DIR}/${audio_adapter}/init.c" +) + list(APPEND ble_audio_srcs - "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/profiles/server.c" - "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/init.c" "${CMAKE_CURRENT_LIST_DIR}/host/common/init.c" "${CMAKE_CURRENT_LIST_DIR}/api/esp_ble_audio_aics_api.c" "${CMAKE_CURRENT_LIST_DIR}/api/esp_ble_audio_bap_api.c" @@ -57,69 +68,71 @@ list(APPEND ble_audio_srcs "${CMAKE_CURRENT_LIST_DIR}/api/esp_ble_audio_vocs_api.c" ) +# Profile glue files live under the host adapter; same set of profile sources +# for either host, dispatched via ${audio_adapter}. if(CONFIG_BT_ASCS) list(APPEND ble_audio_srcs - "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/profiles/ascs.c" + "${CMAKE_CURRENT_LIST_DIR}/${audio_adapter}/profiles/ascs.c" ) endif() if(CONFIG_BT_PACS) list(APPEND ble_audio_srcs - "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/profiles/pacs.c" + "${CMAKE_CURRENT_LIST_DIR}/${audio_adapter}/profiles/pacs.c" ) endif() if(CONFIG_BT_BAP_SCAN_DELEGATOR) list(APPEND ble_audio_srcs - "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/profiles/bass.c" + "${CMAKE_CURRENT_LIST_DIR}/${audio_adapter}/profiles/bass.c" ) endif() if(CONFIG_BT_CAP_ACCEPTOR) list(APPEND ble_audio_srcs - "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/profiles/cas.c" + "${CMAKE_CURRENT_LIST_DIR}/${audio_adapter}/profiles/cas.c" ) endif() if(CONFIG_BT_CSIP_SET_MEMBER) list(APPEND ble_audio_srcs - "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/profiles/csis.c" + "${CMAKE_CURRENT_LIST_DIR}/${audio_adapter}/profiles/csis.c" ) endif() if(CONFIG_BT_TBS) list(APPEND ble_audio_srcs - "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/profiles/tbs.c" + "${CMAKE_CURRENT_LIST_DIR}/${audio_adapter}/profiles/tbs.c" ) endif() if(CONFIG_BT_TMAP) list(APPEND ble_audio_srcs - "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/profiles/tmas.c" + "${CMAKE_CURRENT_LIST_DIR}/${audio_adapter}/profiles/tmas.c" ) endif() if(CONFIG_BT_VCP_VOL_REND) list(APPEND ble_audio_srcs - "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/profiles/vcs.c" + "${CMAKE_CURRENT_LIST_DIR}/${audio_adapter}/profiles/vcs.c" ) endif() if(CONFIG_BT_MICP_MIC_DEV) list(APPEND ble_audio_srcs - "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/profiles/mics.c" + "${CMAKE_CURRENT_LIST_DIR}/${audio_adapter}/profiles/mics.c" ) endif() if(CONFIG_BT_MCS) list(APPEND ble_audio_srcs - "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/profiles/mcs.c" + "${CMAKE_CURRENT_LIST_DIR}/${audio_adapter}/profiles/mcs.c" ) endif() if(CONFIG_BT_HAS) list(APPEND ble_audio_srcs - "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/profiles/has.c" + "${CMAKE_CURRENT_LIST_DIR}/${audio_adapter}/profiles/has.c" ) endif() diff --git a/components/bt/esp_ble_audio/Kconfig/Kconfig.mcs.in b/components/bt/esp_ble_audio/Kconfig/Kconfig.mcs.in index ef8bb91f05f..001a6552e80 100644 --- a/components/bt/esp_ble_audio/Kconfig/Kconfig.mcs.in +++ b/components/bt/esp_ble_audio/Kconfig/Kconfig.mcs.in @@ -61,8 +61,8 @@ if BT_MCC it. config BT_MCC_OTS - bool "Media Control Client support for Object Transfer Service (depends on OTS Client)" - depends on BT_OTS_CLIENT + bool "Media Control Client support for Object Transfer Service" + select BT_OTS_CLIENT help Use this option to configure support in the Media Control Client for an included Object Transfer Service in the Media Control Service. diff --git a/components/bt/esp_ble_audio/Kconfig/Kconfig.mpl.in b/components/bt/esp_ble_audio/Kconfig/Kconfig.mpl.in index c50f4f868f2..3ed39fc6158 100644 --- a/components/bt/esp_ble_audio/Kconfig/Kconfig.mpl.in +++ b/components/bt/esp_ble_audio/Kconfig/Kconfig.mpl.in @@ -71,9 +71,10 @@ if BT_MPL config BT_MPL_OBJECTS bool "Support for media player objects" - depends on BT_OTS + select BT_OTS # TODO: Temporarily depends also on BT_MCS, to avoid issues with the - # bt_mcs_get_ots() call + # bt_mcs_get_ots() call. BT_MCS stays as `depends on` because it has + # non-leaf prerequisites (BT_MCTL_LOCAL_PLAYER_REMOTE_CONTROL ...). depends on BT_MCS # OTS shall be instantiated as a Secondary Service and shall be # included in MCS or GMCS. 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 b47db6b776e..6fd5765848f 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 @@ -70,10 +70,12 @@ void esp_ble_audio_gap_app_post_event(uint8_t type, void *param) bt_le_gap_app_post_event(type, param); } +#if !CONFIG_BT_BLUEDROID_ENABLED void esp_ble_audio_gatt_app_post_event(uint8_t type, void *param) { bt_le_gatt_app_post_event(type, param); } +#endif /* !CONFIG_BT_BLUEDROID_ENABLED */ esp_err_t esp_ble_audio_common_init(esp_ble_audio_init_info_t *info) { @@ -156,3 +158,10 @@ esp_err_t esp_ble_audio_common_start(esp_ble_audio_start_info_t *info) return ESP_OK; } + +#if CONFIG_BT_BLUEDROID_ENABLED +uint8_t esp_ble_audio_bluedroid_get_gattc_if(void) +{ + return bt_le_bluedroid_gattc_get_if(); +} +#endif /* CONFIG_BT_BLUEDROID_ENABLED */ 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 80df0d8a669..6ff3cc7e64a 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 @@ -128,15 +128,20 @@ typedef struct { */ void esp_ble_audio_gap_app_post_event(uint8_t type, void *param); +#if !CONFIG_BT_BLUEDROID_ENABLED /** * @brief Post an application-layer GATT event for audio internal usage. * - * @note This function is only needed while using NimBLE Host. + * @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. * * @param type Event type. * @param param Event parameters. */ void esp_ble_audio_gatt_app_post_event(uint8_t type, void *param); +#endif /* !CONFIG_BT_BLUEDROID_ENABLED */ /** * @brief Initialize BLE Audio common functionality. @@ -167,6 +172,21 @@ typedef struct { */ esp_err_t esp_ble_audio_common_start(esp_ble_audio_start_info_t *info); +#if CONFIG_BT_BLUEDROID_ENABLED +/** + * @brief Get the engine's internal GATTC interface handle (Bluedroid only). + * + * Pass this to esp_ble_gattc_aux_open() / esp_ble_gattc_open() so the + * resulting ACL events route back to the engine, avoiding the need for the + * application to register a second BTA GATTC app for connection initiation. + * + * @return Engine's gattc_if (ABI-compatible with esp_gatt_if_t), or + * ESP_GATT_IF_NONE (0xFF) if GATTC registration has not completed — + * callers must bail rather than pass it to aux_open. + */ +uint8_t esp_ble_audio_bluedroid_get_gattc_if(void); +#endif /* CONFIG_BT_BLUEDROID_ENABLED */ + #ifdef __cplusplus } #endif 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 new file mode 100644 index 00000000000..c684349b846 --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/init.h @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef HOST_BLUEDROID_INIT_H_ +#define HOST_BLUEDROID_INIT_H_ + +#include + +#include "bluedroid/server.h" +#include "bluedroid/profiles/ascs.h" +#include "bluedroid/profiles/bass.h" +#include "bluedroid/profiles/cas.h" +#include "bluedroid/profiles/csis.h" +#include "bluedroid/profiles/mcs.h" +#include "bluedroid/profiles/mics.h" +#include "bluedroid/profiles/pacs.h" +#include "bluedroid/profiles/tbs.h" +#include "bluedroid/profiles/tmas.h" +#include "bluedroid/profiles/vcs.h" +#include "bluedroid/profiles/has.h" + +#ifdef __cplusplus +extern "C" { +#endif + +int bt_le_bluedroid_audio_init(void); + +int bt_le_bluedroid_media_proxy_pl_init(void); + +int bt_le_bluedroid_vcp_vol_rend_init(void); + +int bt_le_bluedroid_micp_mic_dev_init(void); + +int bt_le_bluedroid_audio_start(void *info); + +#ifdef __cplusplus +} +#endif + +#endif /* HOST_BLUEDROID_INIT_H_ */ 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 new file mode 100644 index 00000000000..97f0996a21d --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/ascs.h @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef HOST_BLUEDROID_PROFILE_ASCS_H_ +#define HOST_BLUEDROID_PROFILE_ASCS_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int bt_le_bluedroid_ascs_init(void); + +int bt_le_bluedroid_ascs_start(void); + +#ifdef __cplusplus +} +#endif + +#endif /* HOST_BLUEDROID_PROFILE_ASCS_H_ */ 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 new file mode 100644 index 00000000000..c4073352610 --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/bass.h @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef HOST_BLUEDROID_PROFILE_BASS_H_ +#define HOST_BLUEDROID_PROFILE_BASS_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int bt_le_bluedroid_bass_init(void); + +int bt_le_bluedroid_bass_start(void); + +#ifdef __cplusplus +} +#endif + +#endif /* HOST_BLUEDROID_PROFILE_BASS_H_ */ diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/cas.h b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/cas.h new file mode 100644 index 00000000000..5645507fa80 --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/cas.h @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef HOST_BLUEDROID_PROFILE_CAS_H_ +#define HOST_BLUEDROID_PROFILE_CAS_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int bt_le_bluedroid_cas_init(void *csis_svc_p); + +int bt_le_bluedroid_cas_start(void); + +#ifdef __cplusplus +} +#endif + +#endif /* HOST_BLUEDROID_PROFILE_CAS_H_ */ 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 new file mode 100644 index 00000000000..8038bd8a4d3 --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/csis.h @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef HOST_BLUEDROID_PROFILE_CSIS_H_ +#define HOST_BLUEDROID_PROFILE_CSIS_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int bt_le_bluedroid_csis_init(void *csis_svc, uint8_t count); + +int bt_le_bluedroid_csis_start(void); + +#ifdef __cplusplus +} +#endif + +#endif /* HOST_BLUEDROID_PROFILE_CSIS_H_ */ diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/has.h b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/has.h new file mode 100644 index 00000000000..3ba9004f47a --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/has.h @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef HOST_BLUEDROID_PROFILE_HAS_H_ +#define HOST_BLUEDROID_PROFILE_HAS_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int bt_le_bluedroid_has_init(void); + +int bt_le_bluedroid_has_start(void); + +#ifdef __cplusplus +} +#endif + +#endif /* HOST_BLUEDROID_PROFILE_HAS_H_ */ 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 new file mode 100644 index 00000000000..e4d89cc9ef2 --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/mcs.h @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef HOST_BLUEDROID_PROFILE_MCS_H_ +#define HOST_BLUEDROID_PROFILE_MCS_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int bt_le_bluedroid_gmcs_init(void); + +int bt_le_bluedroid_gmcs_start(void); + +#ifdef __cplusplus +} +#endif + +#endif /* HOST_BLUEDROID_PROFILE_MCS_H_ */ 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 new file mode 100644 index 00000000000..f5bcca9ce65 --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/mics.h @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef HOST_BLUEDROID_PROFILE_MICS_H_ +#define HOST_BLUEDROID_PROFILE_MICS_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int bt_le_bluedroid_mics_init(void *micp_inc); + +int bt_le_bluedroid_mics_start(void); + +#ifdef __cplusplus +} +#endif + +#endif /* HOST_BLUEDROID_PROFILE_MICS_H_ */ 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 new file mode 100644 index 00000000000..f36e8e76e80 --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/pacs.h @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef HOST_BLUEDROID_PROFILE_PACS_H_ +#define HOST_BLUEDROID_PROFILE_PACS_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int bt_le_bluedroid_pacs_init(void); + +int bt_le_bluedroid_pacs_start(void); + +#ifdef __cplusplus +} +#endif + +#endif /* HOST_BLUEDROID_PROFILE_PACS_H_ */ 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 new file mode 100644 index 00000000000..7e5e6e76962 --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/tbs.h @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef HOST_BLUEDROID_PROFILE_TBS_H_ +#define HOST_BLUEDROID_PROFILE_TBS_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int bt_le_bluedroid_gtbs_init(void); + +int bt_le_bluedroid_gtbs_start(void); + +#ifdef __cplusplus +} +#endif + +#endif /* HOST_BLUEDROID_PROFILE_TBS_H_ */ diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/tmas.h b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/tmas.h new file mode 100644 index 00000000000..dfd55384959 --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/tmas.h @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef HOST_BLUEDROID_PROFILE_TMAS_H_ +#define HOST_BLUEDROID_PROFILE_TMAS_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int bt_le_bluedroid_tmas_init(void); + +int bt_le_bluedroid_tmas_start(void); + +#ifdef __cplusplus +} +#endif + +#endif /* HOST_BLUEDROID_PROFILE_TMAS_H_ */ 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 new file mode 100644 index 00000000000..a7c797dd6e2 --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/profiles/vcs.h @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef HOST_BLUEDROID_PROFILE_VCS_H_ +#define HOST_BLUEDROID_PROFILE_VCS_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int bt_le_bluedroid_vcs_init(void *vcp_inc); + +int bt_le_bluedroid_vcs_start(void); + +#ifdef __cplusplus +} +#endif + +#endif /* HOST_BLUEDROID_PROFILE_VCS_H_ */ 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 new file mode 100644 index 00000000000..bf9e6c6e043 --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/include/bluedroid/server.h @@ -0,0 +1,172 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef HOST_BLUEDROID_SERVER_H_ +#define HOST_BLUEDROID_SERVER_H_ + +#include +#include + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +static inline char *audio_svc_uuid_to_str(uint16_t uuid) +{ + switch (uuid) { + case BT_UUID_AICS_VAL: return "AICS"; + case BT_UUID_CAS_VAL: return "CAS"; + case BT_UUID_VCS_VAL: return "VCS"; + case BT_UUID_VOCS_VAL: return "VOCS"; + case BT_UUID_CSIS_VAL: return "CSIS"; + case BT_UUID_MCS_VAL: return "MCS"; + case BT_UUID_GMCS_VAL: return "GMCS"; + case BT_UUID_TBS_VAL: return "TBS"; + case BT_UUID_GTBS_VAL: return "GTBS"; + case BT_UUID_MICS_VAL: return "MICS"; + case BT_UUID_ASCS_VAL: return "ASCS"; + case BT_UUID_BASS_VAL: return "BASS"; + case BT_UUID_PACS_VAL: return "PACS"; + case BT_UUID_BASIC_AUDIO_VAL: return "BASIC_AUDIO"; + case BT_UUID_HAS_VAL: return "HAS"; + case BT_UUID_TMAS_VAL: return "TMAS"; + case BT_UUID_PBA_VAL: return "PBA"; + case BT_UUID_GMAS_VAL: return "GMAS"; + case BT_UUID_OTS_VAL: return "OTS"; + default: return "UnknownSvc"; + } +} + +static inline char *audio_chrc_uuid_to_str(uint16_t uuid) +{ + switch (uuid) { + case BT_UUID_OTS_FEATURE_VAL: return "OTS_FEATURE"; + case BT_UUID_OTS_NAME_VAL: return "OTS_NAME"; + case BT_UUID_OTS_TYPE_VAL: return "OTS_TYPE"; + case BT_UUID_OTS_SIZE_VAL: return "OTS_SIZE"; + case BT_UUID_OTS_FIRST_CREATED_VAL: return "OTS_FIRST_CREATED"; + case BT_UUID_OTS_LAST_MODIFIED_VAL: return "OTS_LAST_MODIFIED"; + case BT_UUID_OTS_ID_VAL: return "OTS_ID"; + case BT_UUID_OTS_PROPERTIES_VAL: return "OTS_PROPERTIES"; + case BT_UUID_OTS_ACTION_CP_VAL: return "OTS_ACTION_CP"; + case BT_UUID_OTS_LIST_CP_VAL: return "OTS_LIST_CP"; + case BT_UUID_OTS_LIST_FILTER_VAL: return "OTS_LIST_FILTER"; + case BT_UUID_OTS_CHANGED_VAL: return "OTS_CHANGED"; + case BT_UUID_GATT_TMAPR_VAL: return "TMAP_ROLE"; + case BT_UUID_AICS_STATE_VAL: return "AICS_STATE"; + case BT_UUID_AICS_GAIN_SETTINGS_VAL: return "AICS_GAIN_SETTINGS"; + case BT_UUID_AICS_INPUT_TYPE_VAL: return "AICS_INPUT_TYPE"; + case BT_UUID_AICS_INPUT_STATUS_VAL: return "AICS_INPUT_STATUS"; + case BT_UUID_AICS_CONTROL_VAL: return "AICS_CONTROL"; + case BT_UUID_AICS_DESCRIPTION_VAL: return "AICS_DESCRIPTION"; + case BT_UUID_VCS_STATE_VAL: return "VCS_STATE"; + case BT_UUID_VCS_CONTROL_VAL: return "VCS_CONTROL"; + case BT_UUID_VCS_FLAGS_VAL: return "VCS_FLAGS"; + case BT_UUID_VOCS_STATE_VAL: return "VOCS_STATE"; + case BT_UUID_VOCS_LOCATION_VAL: return "VOCS_LOCATION"; + case BT_UUID_VOCS_CONTROL_VAL: return "VOCS_CONTROL"; + case BT_UUID_VOCS_DESCRIPTION_VAL: return "VOCS_DESCRIPTION"; + case BT_UUID_CSIS_SIRK_VAL: return "CSIS_SIRK"; + case BT_UUID_CSIS_SET_SIZE_VAL: return "CSIS_SET_SIZE"; + case BT_UUID_CSIS_SET_LOCK_VAL: return "CSIS_SET_LOCK"; + case BT_UUID_CSIS_RANK_VAL: return "CSIS_RANK"; + case BT_UUID_MCS_PLAYER_NAME_VAL: return "MCS_PLAYER_NAME"; + case BT_UUID_MCS_ICON_OBJ_ID_VAL: return "MCS_ICON_OBJ_ID"; + case BT_UUID_MCS_ICON_URL_VAL: return "MCS_ICON_URL"; + case BT_UUID_MCS_TRACK_CHANGED_VAL: return "MCS_TRACK_CHANGED"; + case BT_UUID_MCS_TRACK_TITLE_VAL: return "MCS_TRACK_TITLE"; + case BT_UUID_MCS_TRACK_DURATION_VAL: return "MCS_TRACK_DURATION"; + case BT_UUID_MCS_TRACK_POSITION_VAL: return "MCS_TRACK_POSITION"; + case BT_UUID_MCS_PLAYBACK_SPEED_VAL: return "MCS_PLAYBACK_SPEED"; + case BT_UUID_MCS_SEEKING_SPEED_VAL: return "MCS_SEEKING_SPEED"; + case BT_UUID_MCS_TRACK_SEGMENTS_OBJ_ID_VAL: return "MCS_TRACK_SEGMENTS_OBJ_ID"; + case BT_UUID_MCS_CURRENT_TRACK_OBJ_ID_VAL: return "MCS_CURRENT_TRACK_OBJ_ID"; + case BT_UUID_MCS_NEXT_TRACK_OBJ_ID_VAL: return "MCS_NEXT_TRACK_OBJ_ID"; + case BT_UUID_MCS_PARENT_GROUP_OBJ_ID_VAL: return "MCS_PARENT_GROUP_OBJ_ID"; + case BT_UUID_MCS_CURRENT_GROUP_OBJ_ID_VAL: return "MCS_CURRENT_GROUP_OBJ_ID"; + case BT_UUID_MCS_PLAYING_ORDER_VAL: return "MCS_PLAYING_ORDER"; + case BT_UUID_MCS_PLAYING_ORDERS_VAL: return "MCS_PLAYING_ORDERS"; + case BT_UUID_MCS_MEDIA_STATE_VAL: return "MCS_MEDIA_STATE"; + case BT_UUID_MCS_MEDIA_CONTROL_POINT_VAL: return "MCS_MEDIA_CONTROL_POINT"; + case BT_UUID_MCS_MEDIA_CONTROL_OPCODES_VAL: return "MCS_MEDIA_CONTROL_OPCODES"; + case BT_UUID_MCS_SEARCH_RESULTS_OBJ_ID_VAL: return "MCS_SEARCH_RESULTS_OBJ_ID"; + case BT_UUID_MCS_SEARCH_CONTROL_POINT_VAL: return "MCS_SEARCH_CONTROL_POINT"; + case BT_UUID_TBS_PROVIDER_NAME_VAL: return "TBS_PROVIDER_NAME"; + case BT_UUID_TBS_UCI_VAL: return "TBS_UCI"; + case BT_UUID_TBS_TECHNOLOGY_VAL: return "TBS_TECHNOLOGY"; + case BT_UUID_TBS_URI_LIST_VAL: return "TBS_URI_LIST"; + case BT_UUID_TBS_SIGNAL_STRENGTH_VAL: return "TBS_SIGNAL_STRENGTH"; + case BT_UUID_TBS_SIGNAL_INTERVAL_VAL: return "TBS_SIGNAL_INTERVAL"; + case BT_UUID_TBS_LIST_CURRENT_CALLS_VAL: return "TBS_LIST_CURRENT_CALLS"; + case BT_UUID_CCID_VAL: return "CCID"; + case BT_UUID_TBS_STATUS_FLAGS_VAL: return "TBS_STATUS_FLAGS"; + case BT_UUID_TBS_INCOMING_URI_VAL: return "TBS_INCOMING_URI"; + case BT_UUID_TBS_CALL_STATE_VAL: return "TBS_CALL_STATE"; + case BT_UUID_TBS_CALL_CONTROL_POINT_VAL: return "TBS_CALL_CONTROL_POINT"; + case BT_UUID_TBS_OPTIONAL_OPCODES_VAL: return "TBS_OPTIONAL_OPCODES"; + case BT_UUID_TBS_TERMINATE_REASON_VAL: return "TBS_TERMINATE_REASON"; + case BT_UUID_TBS_INCOMING_CALL_VAL: return "TBS_INCOMING_CALL"; + case BT_UUID_TBS_FRIENDLY_NAME_VAL: return "TBS_FRIENDLY_NAME"; + case BT_UUID_MICS_MUTE_VAL: return "MICS_MUTE"; + case BT_UUID_ASCS_ASE_SNK_VAL: return "ASCS_ASE_SNK"; + case BT_UUID_ASCS_ASE_SRC_VAL: return "ASCS_ASE_SRC"; + case BT_UUID_ASCS_ASE_CP_VAL: return "ASCS_ASE_CP"; + case BT_UUID_BASS_CONTROL_POINT_VAL: return "BASS_CP"; + case BT_UUID_BASS_RECV_STATE_VAL: return "BASS_RECV_STATE"; + case BT_UUID_PACS_SNK_VAL: return "PACS_SNK"; + case BT_UUID_PACS_SNK_LOC_VAL: return "PACS_SNK_LOC"; + case BT_UUID_PACS_SRC_VAL: return "PACS_SRC"; + case BT_UUID_PACS_SRC_LOC_VAL: return "PACS_SRC_LOC"; + case BT_UUID_PACS_AVAILABLE_CONTEXT_VAL: return "PACS_AVAILABLE_CONTEXT"; + case BT_UUID_PACS_SUPPORTED_CONTEXT_VAL: return "PACS_SUPPORTED_CONTEXT"; + case BT_UUID_HAS_HEARING_AID_FEATURES_VAL: return "HAS_HEARING_AID_FEATURES"; + case BT_UUID_HAS_PRESET_CONTROL_POINT_VAL: return "HAS_PRESET_CONTROL_POINT"; + case BT_UUID_HAS_ACTIVE_PRESET_INDEX_VAL: return "HAS_ACTIVE_PRESET_INDEX"; + case BT_UUID_GMAP_ROLE_VAL: return "GMAP_ROLE"; + case BT_UUID_GMAP_UGG_FEAT_VAL: return "GMAP_UGG_FEAT"; + case BT_UUID_GMAP_UGT_FEAT_VAL: return "GMAP_UGT_FEAT"; + case BT_UUID_GMAP_BGS_FEAT_VAL: return "GMAP_BGS_FEAT"; + case BT_UUID_GMAP_BGR_FEAT_VAL: return "GMAP_BGR_FEAT"; + default: return "UnknownChrc"; + } +} + +enum { + AICS_IN_PROGRESS, + ASCS_IN_PROGRESS, + BASS_IN_PROGRESS, + CAS_IN_PROGRESS, + CSIS_IN_PROGRESS, + HAS_IN_PROGRESS, + GMCS_IN_PROGRESS, + MCS_IN_PROGRESS, + MICS_IN_PROGRESS, + PACS_IN_PROGRESS, + GTBS_IN_PROGRESS, + TBS_IN_PROGRESS, + TMAS_IN_PROGRESS, + VCS_IN_PROGRESS, + VOCS_IN_PROGRESS, + + MAX_IN_PROGRESS, +}; + +void bt_le_bluedroid_audio_gatts_init(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_start(struct bt_gatt_service *svc); + +#ifdef __cplusplus +} +#endif + +#endif /* HOST_BLUEDROID_SERVER_H_ */ diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/init.c b/components/bt/esp_ble_audio/host/adapter/bluedroid/init.c new file mode 100644 index 00000000000..424c9a9eafb --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/init.c @@ -0,0 +1,342 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "sdkconfig.h" + +#include "bta/bta_gatt_common.h" + +#include +#include +#include +#include + +#include "bluedroid/init.h" +#include "common/init.h" + +#include "../../../lib/include/audio.h" + +LOG_MODULE_REGISTER(LEA_BINIT, CONFIG_BT_ISO_LOG_LEVEL); + +#if CONFIG_BT_CSIP_SET_MEMBER +#define CSIS_SVC_COUNT CONFIG_BT_CSIP_SET_MEMBER_MAX_INSTANCE_COUNT +#else /* CONFIG_BT_CSIP_SET_MEMBER */ +#define CSIS_SVC_COUNT 0 +#endif /* CONFIG_BT_CSIP_SET_MEMBER */ + +/* 3 is reserved for other GATT services */ +#define TOTAL_SERVICE_COUNT (3 + \ + (IS_ENABLED(CONFIG_BT_ASCS) ? 1 : 0) + \ + (IS_ENABLED(CONFIG_BT_PACS) ? 1 : 0) + \ + (IS_ENABLED(CONFIG_BT_BAP_SCAN_DELEGATOR) ? 1 : 0) + \ + (IS_ENABLED(CONFIG_BT_TMAP) ? 1 : 0) + \ + (IS_ENABLED(CONFIG_BT_MCS) ? 1 : 0) + \ + (IS_ENABLED(CONFIG_BT_CSIP_SET_MEMBER) ? CSIS_SVC_COUNT : 0) + \ + (IS_ENABLED(CONFIG_BT_CAP_ACCEPTOR) ? 1 : 0) + \ + (IS_ENABLED(CONFIG_BT_VCP_VOL_REND) ? 1 : 0) + \ + (IS_ENABLED(CONFIG_BT_MICP_MIC_DEV) ? 1 : 0) + \ + CONFIG_BT_VOCS_MAX_INSTANCE_COUNT + \ + CONFIG_BT_AICS_MAX_INSTANCE_COUNT + \ + (IS_ENABLED(CONFIG_BT_TBS) ? 1 : 0) + \ + (IS_ENABLED(CONFIG_BT_HAS) ? 1 : 0)) + +_Static_assert(TOTAL_SERVICE_COUNT <= CONFIG_BT_GATT_MAX_SR_PROFILES, "Too small BT_GATT_MAX_SR_PROFILES"); + +int bt_le_bluedroid_audio_init(void) +{ + int err = 0; + + BTA_GATT_SetLocalMTU(BLE_AUDIO_ATT_MTU_MIN); + + bt_le_bluedroid_audio_gatts_init(); + +#if CONFIG_BT_PACS + err = bt_le_bluedroid_pacs_init(); + if (err) { + return err; + } +#endif /* CONFIG_BT_PACS */ + +#if (BLE_AUDIO_SVC_DEFERRED_ADD == 0) +#if CONFIG_BT_ASCS + err = bt_le_bluedroid_ascs_init(); + if (err) { + return err; + } +#endif /* CONFIG_BT_ASCS */ + +#if CONFIG_BT_BAP_SCAN_DELEGATOR + err = bt_le_bluedroid_bass_init(); + if (err) { + return err; + } +#endif /* CONFIG_BT_BAP_SCAN_DELEGATOR */ + +#if CONFIG_BT_TMAP + err = bt_le_bluedroid_tmas_init(); + if (err) { + return err; + } +#endif /* CONFIG_BT_TMAP */ + +#if CONFIG_BT_TBS + err = bt_le_bluedroid_gtbs_init(); + if (err) { + return err; + } +#endif /* CONFIG_BT_TBS */ + +#if CONFIG_BT_HAS + err = bt_le_bluedroid_has_init(); + if (err) { + return err; + } +#endif /* CONFIG_BT_HAS */ +#endif /* (BLE_AUDIO_SVC_DEFERRED_ADD == 0) */ + + return err; +} + +#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) +{ + struct bt_gatt_service *csis_svc[CONFIG_BT_CSIP_SET_MEMBER_MAX_INSTANCE_COUNT]; + uint8_t count; + int err; + + if (info == NULL) { + return 0; + } + + *inc_csis_svc = NULL; + count = 0; + + for (size_t i = 0; i < ARRAY_SIZE(info->csis_insts); i++) { + if (info->csis_insts[i].svc_inst == NULL) { + continue; + } + + csis_svc[count] = lib_csip_set_member_svc_get(info->csis_insts[i].svc_inst); + if (!csis_svc[count]) { + LOG_ERR("[B]CsisSvcGetFail[%u]", i); + return -ENODEV; + } + + if (info->csis_insts[i].included_by_cas) { + if (*inc_csis_svc == NULL) { + *inc_csis_svc = csis_svc[count]; + } else { + /* CAS may include at most one CSIS — caller misconfigured. */ + LOG_ERR("[B]CsisMultiIncByCas"); + return -EINVAL; + } + } + + count++; + } + + err = bt_le_bluedroid_csis_init(csis_svc, count); + if (err) { + return err; + } + + return 0; +} +#endif /* CONFIG_BT_CSIP_SET_MEMBER */ + +#if CONFIG_BT_MCS +int bt_le_bluedroid_media_proxy_pl_init(void) +{ + int err; + +#if CONFIG_BT_MPL_OBJECTS + +#endif /* CONFIG_BT_MPL_OBJECTS */ + + err = bt_le_bluedroid_gmcs_init(); + if (err) { + return err; + } + + return 0; +} +#endif /* CONFIG_BT_MCS */ + +#if CONFIG_BT_VCP_VOL_REND +int bt_le_bluedroid_vcp_vol_rend_init(void) +{ + struct bt_vcp_included vcp_included; + int err; + + memset(&vcp_included, 0, sizeof(vcp_included)); + + err = bt_vcp_vol_rend_included_get_safe(&vcp_included); + if (err) { + return err; + } + + err = bt_le_bluedroid_vcs_init(&vcp_included); + if (err) { + return err; + } + + return 0; +} +#endif /* CONFIG_BT_VCP_VOL_REND */ + +#if CONFIG_BT_MICP_MIC_DEV +int bt_le_bluedroid_micp_mic_dev_init(void) +{ + struct bt_micp_included micp_included; + int err; + + memset(&micp_included, 0, sizeof(micp_included)); + + err = bt_micp_mic_dev_included_get_safe(&micp_included); + if (err) { + return err; + } + + err = bt_le_bluedroid_mics_init(&micp_included); + if (err) { + return err; + } + + return 0; +} +#endif /* CONFIG_BT_MICP_MIC_DEV */ + +int bt_le_bluedroid_audio_start(void *info) +{ + struct bt_gatt_service *inc_csis_svc = NULL; + int err = 0; + +#if CONFIG_BT_CSIP_SET_MEMBER + /* Note: + * Should add CSIS before CAS in case one CSIS instance + * is included by CAS. + */ + err = bluedroid_gatt_csis_init(info, &inc_csis_svc); + if (err) { + return err; + } +#endif /* CONFIG_BT_CSIP_SET_MEMBER */ + +#if CONFIG_BT_CAP_ACCEPTOR + err = bt_le_bluedroid_cas_init(inc_csis_svc); + if (err) { + return err; + } +#else /* CONFIG_BT_CAP_ACCEPTOR */ + ARG_UNUSED(inc_csis_svc); +#endif /* CONFIG_BT_CAP_ACCEPTOR */ + +#if (BLE_AUDIO_SVC_DEFERRED_ADD == 0) +#if CONFIG_BT_MCS + err = bt_le_bluedroid_media_proxy_pl_init(); + if (err) { + return err; + } +#endif /* CONFIG_BT_MCS */ + +#if CONFIG_BT_VCP_VOL_REND + err = bt_le_bluedroid_vcp_vol_rend_init(); + if (err) { + return err; + } +#endif /* CONFIG_BT_VCP_VOL_REND */ + +#if CONFIG_BT_MICP_MIC_DEV + err = bt_le_bluedroid_micp_mic_dev_init(); + if (err) { + return err; + } +#endif /* CONFIG_BT_MICP_MIC_DEV */ +#endif /* (BLE_AUDIO_SVC_DEFERRED_ADD == 0) */ + +#if CONFIG_BT_ASCS + err = bt_le_bluedroid_ascs_start(); + if (err) { + return err; + } +#endif /* CONFIG_BT_ASCS */ + +#if CONFIG_BT_PACS + err = bt_le_bluedroid_pacs_start(); + if (err) { + return err; + } +#endif /* CONFIG_BT_PACS */ + +#if CONFIG_BT_BAP_SCAN_DELEGATOR + err = bt_le_bluedroid_bass_start(); + if (err) { + return err; + } +#endif /* CONFIG_BT_BAP_SCAN_DELEGATOR */ + +#if CONFIG_BT_TMAP + err = bt_le_bluedroid_tmas_start(); + if (err) { + return err; + } +#endif /* CONFIG_BT_TMAP */ + +#if CONFIG_BT_CSIP_SET_MEMBER + err = bt_le_bluedroid_csis_start(); + if (err) { + return err; + } +#endif /* CONFIG_BT_CSIP_SET_MEMBER */ + +#if CONFIG_BT_CAP_ACCEPTOR + err = bt_le_bluedroid_cas_start(); + if (err) { + return err; + } +#endif /* CONFIG_BT_CAP_ACCEPTOR */ + +#if CONFIG_BT_VCP_VOL_REND + err = bt_le_bluedroid_vcs_start(); + if (err) { + return err; + } +#endif /* CONFIG_BT_VCP_VOL_REND */ + +#if CONFIG_BT_MICP_MIC_DEV + err = bt_le_bluedroid_mics_start(); + if (err) { + return err; + } +#endif /* CONFIG_BT_MICP_MIC_DEV */ + +#if CONFIG_BT_MCS + err = bt_le_bluedroid_gmcs_start(); + if (err) { + return err; + } +#endif /* CONFIG_BT_MCS */ + +#if CONFIG_BT_TBS + err = bt_le_bluedroid_gtbs_start(); + if (err) { + return err; + } +#endif /* CONFIG_BT_TBS */ + +#if CONFIG_BT_HAS + err = bt_le_bluedroid_has_start(); + if (err) { + return err; + } +#endif /* CONFIG_BT_HAS */ + + return err; +} 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 new file mode 100644 index 00000000000..646ae4aeb9e --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/ascs.c @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include + +#include "bluedroid/server.h" + +#include "common/host.h" + +#include "../../../lib/include/audio.h" + +LOG_MODULE_REGISTER(LEA_ASCS, CONFIG_BT_ISO_LOG_LEVEL); + +int bt_le_bluedroid_ascs_init(void) +{ + struct bt_gatt_service *ascs_svc; + + LOG_DBG("[B]AscsInit"); + + ascs_svc = lib_ascs_svc_get(); + if (!ascs_svc) { + LOG_ERR("[B]AscsSvcGetFail"); + return -ENODEV; + } + + bt_le_bluedroid_set_svc_in_progress(ASCS_IN_PROGRESS); + + return bt_le_bluedroid_svc_init(ascs_svc); +} + +int bt_le_bluedroid_ascs_start(void) +{ + struct bt_gatt_service *ascs_svc; + + LOG_DBG("[B]AscsStart"); + + ascs_svc = lib_ascs_svc_get(); + if (!ascs_svc) { + LOG_ERR("[B]AscsSvcGetFail"); + return -ENODEV; + } + + bt_le_bluedroid_set_svc_in_progress(ASCS_IN_PROGRESS); + + return bt_le_bluedroid_svc_start(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 new file mode 100644 index 00000000000..36726ddb237 --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/bass.c @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include + +#include "bluedroid/server.h" + +#include "common/host.h" + +#include "../../../lib/include/audio.h" + +LOG_MODULE_REGISTER(LEA_BASS, CONFIG_BT_ISO_LOG_LEVEL); + +int bt_le_bluedroid_bass_init(void) +{ + struct bt_gatt_service *bass_svc; + + LOG_DBG("[B]BassInit"); + + bass_svc = lib_bap_bass_svc_get(); + if (!bass_svc) { + LOG_ERR("[B]BassSvcGetFail"); + return -ENODEV; + } + + bt_le_bluedroid_set_svc_in_progress(BASS_IN_PROGRESS); + + return bt_le_bluedroid_svc_init(bass_svc); +} + +int bt_le_bluedroid_bass_start(void) +{ + struct bt_gatt_service *bass_svc; + + LOG_DBG("[B]BassStart"); + + bass_svc = lib_bap_bass_svc_get(); + if (!bass_svc) { + LOG_ERR("[B]BassSvcGetFail"); + return -ENODEV; + } + + bt_le_bluedroid_set_svc_in_progress(BASS_IN_PROGRESS); + + return bt_le_bluedroid_svc_start(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 new file mode 100644 index 00000000000..2a93afc5512 --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/cas.c @@ -0,0 +1,105 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include + +#include "bluedroid/server.h" + +#include "common/host.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; + +struct inc_svc_inst *cas_not_included_inst(void) +{ + if (inc_csis_inst.included == false) { + return &inc_csis_inst; + } + + return NULL; +} +#endif /* CONFIG_BT_CAP_ACCEPTOR_SET_MEMBER */ + +int bt_le_bluedroid_cas_init(void *csis_svc_p) +{ + struct bt_gatt_service *cas_svc; + int err = 0; + + LOG_DBG("[B]CasInit"); + +#if CONFIG_BT_CAP_ACCEPTOR_SET_MEMBER + /* csis_svc_p is non-NULL in practice (SET_MEMBER depends on CSIP_SET_MEMBER, + * which populates it), but guard for parity with the nimble adapter. With + * no CSIS, mark included=true so cas_not_included_inst() skips it and + * svc_init never feeds a NULL svc_p into BTA_GATTS_AddIncludeService. */ + if (csis_svc_p) { + /* The instance of CSIS is included by CAS */ + inc_csis_inst.svc_p = csis_svc_p; + /* Reset included before svc_init; see mics.c for rationale. */ + inc_csis_inst.included = false; + } else { + inc_csis_inst.included = true; + } +#else /* CONFIG_BT_CAP_ACCEPTOR_SET_MEMBER */ + ARG_UNUSED(csis_svc_p); +#endif /* CONFIG_BT_CAP_ACCEPTOR_SET_MEMBER */ + + cas_svc = lib_cas_svc_get(); + if (!cas_svc) { + LOG_ERR("[B]CasSvcGetFail"); + return -ENODEV; + } + + bt_le_bluedroid_set_svc_in_progress(CAS_IN_PROGRESS); + + err = bt_le_bluedroid_svc_init(cas_svc); + if (err) { + return err; + } + +#if CONFIG_BT_CAP_ACCEPTOR_SET_MEMBER + if (cas_not_included_inst()) { + LOG_ERR("[B]CasCsisNotInc"); + return -EIO; + } +#else /* CONFIG_BT_CAP_ACCEPTOR_SET_MEMBER */ + /* Insert CAS to the GATT db list */ + err = bt_gatt_service_register_safe(cas_svc); + if (err) { + LOG_ERR("[B]CasSvcRegFail[%d]", err); + } +#endif /* CONFIG_BT_CAP_ACCEPTOR_SET_MEMBER */ + + return err; +} + +int bt_le_bluedroid_cas_start(void) +{ + struct bt_gatt_service *cas_svc; + + LOG_DBG("[B]CasStart"); + + cas_svc = lib_cas_svc_get(); + if (!cas_svc) { + LOG_ERR("[B]CasSvcGetFail"); + return -ENODEV; + } + + bt_le_bluedroid_set_svc_in_progress(CAS_IN_PROGRESS); + + return bt_le_bluedroid_svc_start(cas_svc); +} 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 new file mode 100644 index 00000000000..19e2be17869 --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/csis.c @@ -0,0 +1,76 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include + +#include "bluedroid/server.h" + +#include "common/host.h" + +#include "../../../lib/include/audio.h" + +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 { + struct bt_gatt_service *svc_p; +} csis_insts[CSIS_SVC_COUNT]; + +static uint8_t csis_svc_count; + +int bt_le_bluedroid_csis_init(void *svc, uint8_t count) +{ + int err; + + LOG_DBG("[B]CsisInit"); + + if (count > CSIS_SVC_COUNT) { + return -1; + } + + bt_le_bluedroid_set_svc_in_progress(CSIS_IN_PROGRESS); + + for (size_t i = 0; i < count; i++) { + csis_insts[i].svc_p = ((struct bt_gatt_service **)svc)[i]; + + err = bt_le_bluedroid_svc_init(csis_insts[i].svc_p); + if (err) { + return err; + } + } + + /* Commit the count only after the loop succeeds, so a mid-loop failure + * doesn't leave csis_start() iterating uninitialized entries. */ + csis_svc_count = count; + + return 0; +} + +int bt_le_bluedroid_csis_start(void) +{ + int err; + + LOG_DBG("[B]CsisStart"); + + bt_le_bluedroid_set_svc_in_progress(CSIS_IN_PROGRESS); + + for (size_t i = 0; i < csis_svc_count; i++) { + err = bt_le_bluedroid_svc_start(csis_insts[i].svc_p); + if (err) { + return err; + } + } + + return 0; +} diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/has.c b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/has.c new file mode 100644 index 00000000000..3516c7c9406 --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/has.c @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include + +#include "bluedroid/server.h" + +#include "common/host.h" + +#include "../../../lib/include/audio.h" + +LOG_MODULE_REGISTER(LEA_HAS, CONFIG_BT_ISO_LOG_LEVEL); + +int bt_le_bluedroid_has_init(void) +{ + struct bt_gatt_service *has_svc; + + LOG_DBG("[B]HasInit"); + + has_svc = lib_has_svc_get(); + if (!has_svc) { + LOG_ERR("[B]HasSvcGetFail"); + return -ENODEV; + } + + bt_le_bluedroid_set_svc_in_progress(HAS_IN_PROGRESS); + + return bt_le_bluedroid_svc_init(has_svc); +} + +int bt_le_bluedroid_has_start(void) +{ + struct bt_gatt_service *has_svc; + + LOG_DBG("[B]HasStart"); + + has_svc = lib_has_svc_get(); + if (!has_svc) { + LOG_ERR("[B]HasSvcGetFail"); + return -ENODEV; + } + + bt_le_bluedroid_set_svc_in_progress(HAS_IN_PROGRESS); + + return bt_le_bluedroid_svc_start(has_svc); +} 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 new file mode 100644 index 00000000000..a19c56ebe95 --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/mcs.c @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include + +#include "bluedroid/server.h" + +#include "common/host.h" + +#include "../../../lib/include/audio.h" + +LOG_MODULE_REGISTER(LEA_MCS, CONFIG_BT_ISO_LOG_LEVEL); + +int bt_le_bluedroid_gmcs_init(void) +{ + struct bt_gatt_service *gmcs_svc; + + gmcs_svc = lib_mcs_svc_get(); + if (!gmcs_svc) { + LOG_ERR("[B]GmcsSvcGetFail"); + return -ENODEV; + } + + bt_le_bluedroid_set_svc_in_progress(GMCS_IN_PROGRESS); + + return bt_le_bluedroid_svc_init(gmcs_svc); +} + +int bt_le_bluedroid_gmcs_start(void) +{ + struct bt_gatt_service *gmcs_svc; + + gmcs_svc = lib_mcs_svc_get(); + if (!gmcs_svc) { + LOG_ERR("[B]GmcsSvcGetFail"); + return -ENODEV; + } + + bt_le_bluedroid_set_svc_in_progress(GMCS_IN_PROGRESS); + + return bt_le_bluedroid_svc_start(gmcs_svc); +} 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 new file mode 100644 index 00000000000..b8941321600 --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/mics.c @@ -0,0 +1,136 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "bluedroid/server.h" + +#include "common/host.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 struct inc_svc_inst inc_aics_insts[AICS_INST_COUNT]; + +struct inc_svc_inst *mics_not_included_inst(void) +{ + for (size_t i = 0; i < inc_aics_svc_count; i++) { + if (inc_aics_insts[i].included == false) { + return &inc_aics_insts[i]; + } + } + + return NULL; +} + +int bt_le_bluedroid_mics_init(void *micp_inc) +{ + struct bt_micp_included *micp_included; + struct bt_gatt_service *mics_svc; + int err; + + LOG_DBG("[B]MicsInit"); + + mics_svc = lib_mics_svc_get(); + if (!mics_svc) { + LOG_ERR("[B]MicsSvcGetFail"); + return -ENODEV; + } + + /* Reset before the NULL check so a re-init with micp_included == NULL + * doesn't iterate over stale entries from a previous non-NULL call. */ + inc_aics_svc_count = 0; + + micp_included = micp_inc; + + if (micp_included) { + if (micp_included->aics_cnt > AICS_INST_COUNT) { + return -EINVAL; + } + + bt_le_bluedroid_set_svc_in_progress(AICS_IN_PROGRESS); + + /* MICS may include zero or more instances of AICS */ + for (size_t i = 0; i < micp_included->aics_cnt; i++) { + inc_aics_insts[i].svc_p = lib_aics_svc_get(micp_included->aics[i]); + if (!inc_aics_insts[i].svc_p) { + LOG_ERR("[B]AicsSvcGetFail[%u]", i); + return -ENODEV; + } + + /* Reset included before svc_init: server.c sets it to true when + * the include attribute is processed. Without this reset, a + * re-init where the include attribute is missing would leave + * the stale true from a previous init and mics_not_included_inst() + * would falsely report all instances as included. */ + inc_aics_insts[i].included = false; + + err = bt_le_bluedroid_svc_init(inc_aics_insts[i].svc_p); + if (err) { + return err; + } + } + + /* Commit the count only after every instance initialized — a mid-loop + * failure above leaves it at 0 (reset on entry) so mics_start() never + * iterates partially-initialized entries. */ + inc_aics_svc_count = micp_included->aics_cnt; + } + + bt_le_bluedroid_set_svc_in_progress(MICS_IN_PROGRESS); + + err = bt_le_bluedroid_svc_init(mics_svc); + if (err) { + return err; + } + + if (mics_not_included_inst()) { + LOG_ERR("[B]MicsAicsNotAllInc"); + return -EIO; + } + + return 0; +} + +int bt_le_bluedroid_mics_start(void) +{ + struct bt_gatt_service *mics_svc; + int err; + + LOG_DBG("[B]MicsStart"); + + mics_svc = lib_mics_svc_get(); + if (!mics_svc) { + LOG_ERR("[B]MicsSvcGetFail"); + return -ENODEV; + } + + bt_le_bluedroid_set_svc_in_progress(AICS_IN_PROGRESS); + + for (size_t i = 0; i < inc_aics_svc_count; i++) { + err = bt_le_bluedroid_svc_start(inc_aics_insts[i].svc_p); + if (err) { + return err; + } + } + + bt_le_bluedroid_set_svc_in_progress(MICS_IN_PROGRESS); + + return bt_le_bluedroid_svc_start(mics_svc); +} 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 new file mode 100644 index 00000000000..8dc151dbf67 --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/pacs.c @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include + +#include "bluedroid/server.h" + +#include "common/host.h" + +#include "../../../lib/include/audio.h" + +LOG_MODULE_REGISTER(LEA_PACS, CONFIG_BT_ISO_LOG_LEVEL); + +int bt_le_bluedroid_pacs_init(void) +{ + struct bt_gatt_service *pacs_svc; + + LOG_DBG("[B]PacsInit"); + + pacs_svc = lib_pacs_svc_get(); + if (!pacs_svc) { + LOG_ERR("[B]PacsSvcGetFail"); + return -ENODEV; + } + + bt_le_bluedroid_set_svc_in_progress(PACS_IN_PROGRESS); + + return bt_le_bluedroid_svc_init(pacs_svc); +} + +int bt_le_bluedroid_pacs_start(void) +{ + struct bt_gatt_service *pacs_svc; + + LOG_DBG("[B]PacsStart"); + + pacs_svc = lib_pacs_svc_get(); + if (!pacs_svc) { + LOG_ERR("[B]PacsSvcGetFail"); + return -ENODEV; + } + + bt_le_bluedroid_set_svc_in_progress(PACS_IN_PROGRESS); + + return bt_le_bluedroid_svc_start(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 new file mode 100644 index 00000000000..6d0d2e70acb --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/tbs.c @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include + +#include "bluedroid/server.h" + +#include "common/host.h" + +#include "../../../lib/include/audio.h" + +LOG_MODULE_REGISTER(LEA_TBS, CONFIG_BT_ISO_LOG_LEVEL); + +int bt_le_bluedroid_gtbs_init(void) +{ + struct bt_gatt_service *gtbs_svc; + + gtbs_svc = lib_gtbs_svc_get(); + if (!gtbs_svc) { + LOG_ERR("[B]GtbsSvcGetFail"); + return -ENODEV; + } + + bt_le_bluedroid_set_svc_in_progress(GTBS_IN_PROGRESS); + + return bt_le_bluedroid_svc_init(gtbs_svc); +} + +int bt_le_bluedroid_gtbs_start(void) +{ + struct bt_gatt_service *gtbs_svc; + + gtbs_svc = lib_gtbs_svc_get(); + if (!gtbs_svc) { + LOG_ERR("[B]GtbsSvcGetFail"); + return -ENODEV; + } + + bt_le_bluedroid_set_svc_in_progress(GTBS_IN_PROGRESS); + + return bt_le_bluedroid_svc_start(gtbs_svc); +} diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/tmas.c b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/tmas.c new file mode 100644 index 00000000000..11258136b50 --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/tmas.c @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include + +#include "bluedroid/server.h" + +#include "common/host.h" + +#include "../../../lib/include/audio.h" + +LOG_MODULE_REGISTER(LEA_TMAS, CONFIG_BT_ISO_LOG_LEVEL); + +int bt_le_bluedroid_tmas_init(void) +{ + struct bt_gatt_service *tmas_svc; + + LOG_DBG("[B]TmasInit"); + + tmas_svc = lib_tmas_svc_get(); + if (!tmas_svc) { + LOG_ERR("[B]TmasSvcGetFail"); + return -ENODEV; + } + + bt_le_bluedroid_set_svc_in_progress(TMAS_IN_PROGRESS); + + return bt_le_bluedroid_svc_init(tmas_svc); +} + +int bt_le_bluedroid_tmas_start(void) +{ + struct bt_gatt_service *tmas_svc; + + LOG_DBG("[B]TmasStart"); + + tmas_svc = lib_tmas_svc_get(); + if (!tmas_svc) { + LOG_ERR("[B]TmasSvcGetFail"); + return -ENODEV; + } + + bt_le_bluedroid_set_svc_in_progress(TMAS_IN_PROGRESS); + + return bt_le_bluedroid_svc_start(tmas_svc); +} 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 new file mode 100644 index 00000000000..af5674646d6 --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/profiles/vcs.c @@ -0,0 +1,169 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "bluedroid/server.h" + +#include "common/host.h" + +#include "../../../lib/include/audio.h" + +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 struct inc_svc_inst inc_vocs_insts[VOCS_INST_COUNT]; +static struct inc_svc_inst inc_aics_insts[AICS_INST_COUNT]; + +struct inc_svc_inst *vcs_not_included_inst(void) +{ + for (size_t i = 0; i < inc_vocs_svc_count; i++) { + if (inc_vocs_insts[i].included == false) { + return &inc_vocs_insts[i]; + } + } + + for (size_t i = 0; i < inc_aics_svc_count; i++) { + if (inc_aics_insts[i].included == false) { + return &inc_aics_insts[i]; + } + } + + return NULL; +} + +int bt_le_bluedroid_vcs_init(void *vcp_inc) +{ + struct bt_vcp_included *vcp_included; + struct bt_gatt_service *vcs_svc; + int err; + + LOG_DBG("[B]VcsInit"); + + vcs_svc = lib_vcs_svc_get(); + if (!vcs_svc) { + LOG_ERR("[B]VcsSvcGetFail"); + return -ENODEV; + } + + /* Reset before the NULL check so a re-init with vcp_included == NULL + * doesn't iterate over stale entries from a previous non-NULL call. */ + inc_vocs_svc_count = 0; + inc_aics_svc_count = 0; + + vcp_included = vcp_inc; + + if (vcp_included) { + if (vcp_included->vocs_cnt > VOCS_INST_COUNT || + vcp_included->aics_cnt > AICS_INST_COUNT) { + 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++) { + 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); + return -ENODEV; + } + + /* Reset included before svc_init; see mics.c for rationale. */ + inc_vocs_insts[i].included = false; + + err = bt_le_bluedroid_svc_init(inc_vocs_insts[i].svc_p); + if (err) { + return err; + } + } + + 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++) { + 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); + return -ENODEV; + } + + /* Reset included before svc_init; see mics.c for rationale. */ + inc_aics_insts[i].included = false; + + err = bt_le_bluedroid_svc_init(inc_aics_insts[i].svc_p); + if (err) { + return err; + } + } + } + + bt_le_bluedroid_set_svc_in_progress(VCS_IN_PROGRESS); + + err = bt_le_bluedroid_svc_init(vcs_svc); + if (err) { + return err; + } + + if (vcs_not_included_inst()) { + LOG_ERR("[B]VcsVocsAicsNotAllInc"); + return -EIO; + } + + return 0; +} + +int bt_le_bluedroid_vcs_start(void) +{ + struct bt_gatt_service *vcs_svc; + int err; + + LOG_DBG("[B]VcsStart"); + + vcs_svc = lib_vcs_svc_get(); + if (!vcs_svc) { + LOG_ERR("[B]VcsSvcGetFail"); + return -ENODEV; + } + + bt_le_bluedroid_set_svc_in_progress(VOCS_IN_PROGRESS); + + for (size_t i = 0; i < inc_vocs_svc_count; i++) { + err = bt_le_bluedroid_svc_start(inc_vocs_insts[i].svc_p); + if (err) { + return err; + } + } + + bt_le_bluedroid_set_svc_in_progress(AICS_IN_PROGRESS); + + for (size_t i = 0; i < inc_aics_svc_count; i++) { + err = bt_le_bluedroid_svc_start(inc_aics_insts[i].svc_p); + if (err) { + return err; + } + } + + bt_le_bluedroid_set_svc_in_progress(VCS_IN_PROGRESS); + + return bt_le_bluedroid_svc_start(vcs_svc); +} diff --git a/components/bt/esp_ble_audio/host/adapter/bluedroid/server.c b/components/bt/esp_ble_audio/host/adapter/bluedroid/server.c new file mode 100644 index 00000000000..4870f3204d9 --- /dev/null +++ b/components/bt/esp_ble_audio/host/adapter/bluedroid/server.c @@ -0,0 +1,596 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include <../host/conn_internal.h> + +#include "bta/bta_gatt_api.h" + +#include "bluedroid/gatt.h" +#include "bluedroid/server.h" + +#include "common/host.h" + +LOG_MODULE_REGISTER(LEA_GSRV, CONFIG_BT_ISO_LOG_LEVEL); + +static uint8_t svc_in_progress; + +static uint16_t inc_svc_handle; +static uint16_t svc_handle; +static uint16_t chrc_handle; + +static bool is_primary_svc(void) +{ + if (svc_in_progress == ASCS_IN_PROGRESS || + svc_in_progress == BASS_IN_PROGRESS || + svc_in_progress == CAS_IN_PROGRESS || + svc_in_progress == CSIS_IN_PROGRESS || + svc_in_progress == HAS_IN_PROGRESS || + svc_in_progress == GMCS_IN_PROGRESS || + svc_in_progress == MCS_IN_PROGRESS || + svc_in_progress == MICS_IN_PROGRESS || + svc_in_progress == PACS_IN_PROGRESS || + svc_in_progress == GTBS_IN_PROGRESS || + svc_in_progress == TBS_IN_PROGRESS || + svc_in_progress == TMAS_IN_PROGRESS || + svc_in_progress == VCS_IN_PROGRESS) { + return true; + } + + return false; +} + +static bool is_secondary_svc(void) +{ + if (svc_in_progress == AICS_IN_PROGRESS || + svc_in_progress == VOCS_IN_PROGRESS) { + return true; + } + + return false; +} + +static bool any_included_svc(void) +{ + if (svc_in_progress == CAS_IN_PROGRESS || + svc_in_progress == MICS_IN_PROGRESS || + svc_in_progress == VCS_IN_PROGRESS) { + return true; + } + + return false; +} + +static bool is_svc_uuid_valid(uint16_t uuid) +{ + switch (svc_in_progress) { + case AICS_IN_PROGRESS: + return (uuid == BT_UUID_AICS_VAL); + + case ASCS_IN_PROGRESS: + return (uuid == BT_UUID_ASCS_VAL); + + case BASS_IN_PROGRESS: + return (uuid == BT_UUID_BASS_VAL); + + case CAS_IN_PROGRESS: + return (uuid == BT_UUID_CAS_VAL); + + case CSIS_IN_PROGRESS: + return (uuid == BT_UUID_CSIS_VAL); + + case HAS_IN_PROGRESS: + return (uuid == BT_UUID_HAS_VAL); + + case GMCS_IN_PROGRESS: + return (uuid == BT_UUID_GMCS_VAL); + + case MCS_IN_PROGRESS: + return (uuid == BT_UUID_MCS_VAL); + + case MICS_IN_PROGRESS: + return (uuid == BT_UUID_MICS_VAL); + + case PACS_IN_PROGRESS: + return (uuid == BT_UUID_PACS_VAL); + + case GTBS_IN_PROGRESS: + return (uuid == BT_UUID_GTBS_VAL); + + case TBS_IN_PROGRESS: + return (uuid == BT_UUID_TBS_VAL); + + case TMAS_IN_PROGRESS: + return (uuid == BT_UUID_TMAS_VAL); + + case VCS_IN_PROGRESS: + return (uuid == BT_UUID_VCS_VAL); + + case VOCS_IN_PROGRESS: + return (uuid == BT_UUID_VOCS_VAL); + + default: + return false; + } +} + +static void svc_create_cb(uint16_t service_id, uint16_t svc_instance, + bool is_primary, uint8_t status, void *uuid) +{ + int result = status; + + if ((is_primary_svc() && is_primary == false) || + (is_secondary_svc() && is_primary == true) || + ((tBT_UUID *)uuid)->len != 2 || + is_svc_uuid_valid(((tBT_UUID *)uuid)->uu.uuid16) == false) { + /* 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 (((tBT_UUID *)uuid)->len == 2) { + LOG_ERR("[B]SvcCreateMismatch[%u][%u][2][0x%04x]", + svc_in_progress, is_primary, + ((tBT_UUID *)uuid)->uu.uuid16); + } else { + LOG_ERR("[B]SvcCreateMismatch[%u][%u][%u][nonU16]", + svc_in_progress, is_primary, + ((tBT_UUID *)uuid)->len); + } + result = -1; + goto end; + } + + svc_handle = service_id; + +end: + bt_le_bluedroid_gatts_sem_give(result); +} + +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) { + LOG_ERR("[B]IncSvcAddMismatch[%u][%u][%u]", + svc_in_progress, service_id, svc_handle); + result = -1; + goto end; + } + + inc_svc_handle = attr_id; + +end: + bt_le_bluedroid_gatts_sem_give(result); +} + +static void chrc_add_cb(uint16_t service_id, uint16_t attr_id, + uint8_t status, void *uuid) +{ + 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 (((tBT_UUID *)uuid)->len == 2) { + LOG_ERR("[B]ChrcAddMismatch[%u][%u][%u][2][0x%04x]", + svc_in_progress, service_id, svc_handle, + ((tBT_UUID *)uuid)->uu.uuid16); + } else { + LOG_ERR("[B]ChrcAddMismatch[%u][%u][%u][%u][nonU16]", + svc_in_progress, service_id, svc_handle, + ((tBT_UUID *)uuid)->len); + } + result = -1; + goto end; + } + + chrc_handle = attr_id; + +end: + bt_le_bluedroid_gatts_sem_give(result); +} + +static void svc_start_cb(uint16_t service_id, uint8_t status) +{ + int result = status; + + if (service_id != svc_handle) { + LOG_ERR("[B]SvcStartMismatch[%u][%u]", service_id, svc_handle); + result = -1; + } + + bt_le_bluedroid_gatts_sem_give(result); +} + +static struct gatts_svc_cb svc_cb = { + .svc_create_cb = svc_create_cb, + .inc_svc_add_cb = inc_svc_add_cb, + .chrc_add_cb = chrc_add_cb, + .svc_start_cb = svc_start_cb, +}; + +void bt_le_bluedroid_audio_gatts_init(void) +{ + bt_le_bluedroid_gatts_svc_cb_register(&svc_cb); +} + +int bt_le_bluedroid_set_svc_in_progress(uint8_t value) +{ + if (value >= MAX_IN_PROGRESS) { + LOG_ERR("[B]SvcInProgressInv[%u]", value); + return -1; + } + + svc_in_progress = value; + return 0; +} + +static bool is_svc_attr_uuid_valid(uint16_t attr_uuid) +{ + switch (svc_in_progress) { + case AICS_IN_PROGRESS: + return (attr_uuid == BT_UUID_AICS_STATE_VAL || + attr_uuid == BT_UUID_AICS_GAIN_SETTINGS_VAL || + attr_uuid == BT_UUID_AICS_INPUT_TYPE_VAL || + attr_uuid == BT_UUID_AICS_INPUT_STATUS_VAL || + attr_uuid == BT_UUID_AICS_CONTROL_VAL || + attr_uuid == BT_UUID_AICS_DESCRIPTION_VAL); + + case ASCS_IN_PROGRESS: + return (attr_uuid == BT_UUID_ASCS_ASE_SNK_VAL || + attr_uuid == BT_UUID_ASCS_ASE_SRC_VAL || + attr_uuid == BT_UUID_ASCS_ASE_CP_VAL); + + case BASS_IN_PROGRESS: + return (attr_uuid == BT_UUID_BASS_CONTROL_POINT_VAL || + attr_uuid == BT_UUID_BASS_RECV_STATE_VAL); + + case CAS_IN_PROGRESS: + /* No characteristic in CAS */ + return false; + + case CSIS_IN_PROGRESS: + return (attr_uuid == BT_UUID_CSIS_SIRK_VAL || + attr_uuid == BT_UUID_CSIS_SET_SIZE_VAL || + attr_uuid == BT_UUID_CSIS_SET_LOCK_VAL || + attr_uuid == BT_UUID_CSIS_RANK_VAL); + + case HAS_IN_PROGRESS: + return (attr_uuid == BT_UUID_HAS_HEARING_AID_FEATURES_VAL || + attr_uuid == BT_UUID_HAS_PRESET_CONTROL_POINT_VAL || + attr_uuid == BT_UUID_HAS_ACTIVE_PRESET_INDEX_VAL); + + case GMCS_IN_PROGRESS: + case MCS_IN_PROGRESS: + return (attr_uuid == BT_UUID_MCS_PLAYER_NAME_VAL || + attr_uuid == BT_UUID_MCS_ICON_OBJ_ID_VAL || + attr_uuid == BT_UUID_MCS_ICON_URL_VAL || + attr_uuid == BT_UUID_MCS_TRACK_CHANGED_VAL || + attr_uuid == BT_UUID_MCS_TRACK_TITLE_VAL || + attr_uuid == BT_UUID_MCS_TRACK_DURATION_VAL || + attr_uuid == BT_UUID_MCS_TRACK_POSITION_VAL || + attr_uuid == BT_UUID_MCS_PLAYBACK_SPEED_VAL || + attr_uuid == BT_UUID_MCS_SEEKING_SPEED_VAL || + attr_uuid == BT_UUID_MCS_TRACK_SEGMENTS_OBJ_ID_VAL || + attr_uuid == BT_UUID_MCS_CURRENT_TRACK_OBJ_ID_VAL || + attr_uuid == BT_UUID_MCS_NEXT_TRACK_OBJ_ID_VAL || + attr_uuid == BT_UUID_MCS_PARENT_GROUP_OBJ_ID_VAL || + attr_uuid == BT_UUID_MCS_CURRENT_GROUP_OBJ_ID_VAL || + attr_uuid == BT_UUID_MCS_PLAYING_ORDER_VAL || + attr_uuid == BT_UUID_MCS_PLAYING_ORDERS_VAL || + attr_uuid == BT_UUID_MCS_MEDIA_STATE_VAL || + attr_uuid == BT_UUID_MCS_MEDIA_CONTROL_POINT_VAL || + attr_uuid == BT_UUID_MCS_MEDIA_CONTROL_OPCODES_VAL || + attr_uuid == BT_UUID_MCS_SEARCH_RESULTS_OBJ_ID_VAL || + attr_uuid == BT_UUID_MCS_SEARCH_CONTROL_POINT_VAL || + attr_uuid == BT_UUID_CCID_VAL); + + case MICS_IN_PROGRESS: + return (attr_uuid == BT_UUID_MICS_MUTE_VAL); + + case PACS_IN_PROGRESS: + return (attr_uuid == BT_UUID_PACS_SNK_VAL || + attr_uuid == BT_UUID_PACS_SNK_LOC_VAL || + attr_uuid == BT_UUID_PACS_SRC_VAL || + attr_uuid == BT_UUID_PACS_SRC_LOC_VAL || + attr_uuid == BT_UUID_PACS_AVAILABLE_CONTEXT_VAL || + attr_uuid == BT_UUID_PACS_SUPPORTED_CONTEXT_VAL); + + case GTBS_IN_PROGRESS: + case TBS_IN_PROGRESS: + return (attr_uuid == BT_UUID_TBS_PROVIDER_NAME_VAL || + attr_uuid == BT_UUID_TBS_UCI_VAL || + attr_uuid == BT_UUID_TBS_TECHNOLOGY_VAL || + attr_uuid == BT_UUID_TBS_URI_LIST_VAL || + attr_uuid == BT_UUID_TBS_SIGNAL_STRENGTH_VAL || + attr_uuid == BT_UUID_TBS_SIGNAL_INTERVAL_VAL || + attr_uuid == BT_UUID_TBS_LIST_CURRENT_CALLS_VAL || + attr_uuid == BT_UUID_CCID_VAL || + attr_uuid == BT_UUID_TBS_STATUS_FLAGS_VAL || + attr_uuid == BT_UUID_TBS_INCOMING_URI_VAL || + attr_uuid == BT_UUID_TBS_CALL_STATE_VAL || + attr_uuid == BT_UUID_TBS_CALL_CONTROL_POINT_VAL || + attr_uuid == BT_UUID_TBS_OPTIONAL_OPCODES_VAL || + attr_uuid == BT_UUID_TBS_TERMINATE_REASON_VAL || + attr_uuid == BT_UUID_TBS_INCOMING_CALL_VAL || + attr_uuid == BT_UUID_TBS_FRIENDLY_NAME_VAL); + + case TMAS_IN_PROGRESS: + return (attr_uuid == BT_UUID_GATT_TMAPR_VAL); + + case VCS_IN_PROGRESS: + return (attr_uuid == BT_UUID_VCS_STATE_VAL || + attr_uuid == BT_UUID_VCS_CONTROL_VAL || + attr_uuid == BT_UUID_VCS_FLAGS_VAL); + + case VOCS_IN_PROGRESS: + return (attr_uuid == BT_UUID_VOCS_STATE_VAL || + attr_uuid == BT_UUID_VOCS_LOCATION_VAL || + attr_uuid == BT_UUID_VOCS_CONTROL_VAL || + attr_uuid == BT_UUID_VOCS_DESCRIPTION_VAL); + + default: + return false; + } +} + +static struct inc_svc_inst *get_not_included_inst(void) +{ + switch (svc_in_progress) { +#if CONFIG_BT_CAP_ACCEPTOR_SET_MEMBER + case CAS_IN_PROGRESS: + extern struct inc_svc_inst *cas_not_included_inst(void); + return cas_not_included_inst(); +#endif /* CONFIG_BT_CAP_ACCEPTOR_SET_MEMBER */ + +#if CONFIG_BT_MICP_MIC_DEV + case MICS_IN_PROGRESS: + extern struct inc_svc_inst *mics_not_included_inst(void); + return mics_not_included_inst(); +#endif /* CONFIG_BT_MICP_MIC_DEV */ + +#if CONFIG_BT_VCP_VOL_REND + case VCS_IN_PROGRESS: + extern struct inc_svc_inst *vcs_not_included_inst(void); + return vcs_not_included_inst(); +#endif /* CONFIG_BT_VCP_VOL_REND */ + + default: + return NULL; + } +} + +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++; + + case BT_UUID_CSIS_VAL: + return csis_count++; + + case BT_UUID_VOCS_VAL: + return vocs_count++; + + case BT_UUID_MCS_VAL: + return mcs_count++; + + case BT_UUID_OTS_VAL: + return ots_count++; + + case BT_UUID_TBS_VAL: + return tbs_count++; + + default: + return 0; + } +} + +int bt_le_bluedroid_svc_init(struct bt_gatt_service *svc) +{ + struct bt_gatt_attr *curr_attr; + struct bt_gatt_attr *next_attr; + struct inc_svc_inst *inc_inst; + struct bt_gatt_chrc *chrc; + tBTA_GATT_PERM perm; + uint16_t attr_uuid; + uint8_t inst_id; + tBT_UUID uuid; + + assert(svc); + + for (size_t i = 0; i < svc->attr_count; i++) { + curr_attr = &svc->attrs[i]; + + attr_uuid = BT_UUID_16(curr_attr->uuid)->val; + + switch (attr_uuid) { + case BT_UUID_GATT_PRIMARY_VAL: + if (is_primary_svc() == false) { + LOG_ERR("[B]NotCreatingPrimary"); + return -1; + } + + 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); + + bt_le_bluedroid_gatts_sem_reset(); + BTA_GATTS_CreateService(bt_le_bluedroid_gatts_get_if(), &uuid, inst_id, svc->attr_count, true); + + if (bt_le_bluedroid_gatts_sem_take()) { + LOG_ERR("[B]PrimaryCreateFail"); + return -1; + } + + curr_attr->handle = svc_handle; + + LOG_INF("[B]PrimarySvc[%s][0x%04x][%u][0x%04x][%d]", + audio_svc_uuid_to_str(uuid.uu.uuid16), uuid.uu.uuid16, + inst_id, curr_attr->perm, svc_handle); + break; + + case BT_UUID_GATT_SECONDARY_VAL: + if (is_secondary_svc() == false) { + LOG_ERR("[B]NotCreatingSecondary"); + return -1; + } + + 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); + + bt_le_bluedroid_gatts_sem_reset(); + BTA_GATTS_CreateService(bt_le_bluedroid_gatts_get_if(), &uuid, inst_id, svc->attr_count, false); + + if (bt_le_bluedroid_gatts_sem_take()) { + LOG_ERR("[B]SecondaryCreateFail"); + return -1; + } + + curr_attr->handle = svc_handle; + + LOG_INF("[B]SecondarySvc[%s][0x%04x][%u][0x%04x][%d]", + audio_svc_uuid_to_str(uuid.uu.uuid16), uuid.uu.uuid16, + inst_id, curr_attr->perm, svc_handle); + break; + + case BT_UUID_GATT_INCLUDE_VAL: + if (any_included_svc() == false) { + LOG_ERR("[B]NoSvcToInc[%u]", svc_in_progress); + return -1; + } + + inc_inst = get_not_included_inst(); + + if (inc_inst == NULL) { + LOG_ERR("[B]IncSvcNotFound[%u]", svc_in_progress); + return -1; + } + + bt_le_bluedroid_gatts_sem_reset(); + BTA_GATTS_AddIncludeService(svc_handle, inc_inst->svc_p->attrs[0].handle); + + if (bt_le_bluedroid_gatts_sem_take()) { + LOG_ERR("[B]IncSvcAddFail"); + return -1; + } + + curr_attr->handle = inc_svc_handle; + inc_inst->included = true; /* Mark the svc as included */ + + LOG_INF("[B]IncSvc[%s][0x%04x][0x%04x][%d]", + audio_svc_uuid_to_str(BT_UUID_16(inc_inst->svc_p->attrs[0].user_data)->val), + BT_UUID_16(inc_inst->svc_p->attrs[0].user_data)->val, + curr_attr->perm, inc_svc_handle); + break; + + case BT_UUID_GATT_CHRC_VAL: + /* Chrc declaration must be followed by its value attribute. + * Standard BT_GATT_CHARACTERISTIC pairs them, but guard against + * a malformed table where it is the last entry. */ + if (i + 1 >= svc->attr_count) { + LOG_ERR("[B]ChrcDeclLastAttr[%u]", svc_in_progress); + return -1; + } + + next_attr = &svc->attrs[i + 1]; + perm = bt_le_bluedroid_gatt_perm_convert(next_attr->perm); + + assert(curr_attr->user_data); + + chrc = curr_attr->user_data; + bt_le_bluedroid_gatt_uuid_convert(chrc->uuid, &uuid); + bt_le_bluedroid_gatts_sem_reset(); + BTA_GATTS_AddCharacteristic(svc_handle, &uuid, perm, chrc->properties, NULL, NULL); + + if (bt_le_bluedroid_gatts_sem_take()) { + LOG_ERR("[B]ChrcAddFail"); + return -1; + } + + /* Characteristic declaration handle and Characteristic value handle */ + curr_attr->handle = chrc_handle - 1; + next_attr->handle = chrc_handle; + + LOG_INF("[B]Chrc[%s][0x%04x][%u][%u][0x%04x][0x%02x]", + audio_chrc_uuid_to_str(uuid.uu.uuid16), uuid.uu.uuid16, + chrc_handle - 1, chrc_handle, next_attr->perm, chrc->properties); + break; + + case BT_UUID_GATT_CCC_VAL: + perm = bt_le_bluedroid_gatt_perm_convert(curr_attr->perm); + bt_le_bluedroid_gatt_uuid_convert(curr_attr->uuid, &uuid); + bt_le_bluedroid_gatts_sem_reset(); + BTA_GATTS_AddCharDescriptor(svc_handle, perm, &uuid, NULL, NULL); + + if (bt_le_bluedroid_gatts_sem_take()) { + LOG_ERR("[B]ChrcCccdAddFail"); + return -1; + } + + curr_attr->handle = chrc_handle; + + LOG_INF("[B]ChrcCccd[%u][0x%04x]", chrc_handle, curr_attr->perm); + break; + + default: + if (is_svc_attr_uuid_valid(attr_uuid) == false) { + LOG_ERR("[B]InvSvcAttrUuid[0x%04x][%u]", attr_uuid, svc_in_progress); + return -1; + } + break; + } + } + + return 0; +} + +int bt_le_bluedroid_svc_start(struct bt_gatt_service *svc) +{ + assert(svc); + + 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; + } + + bt_le_bluedroid_gatts_sem_reset(); + BTA_GATTS_StartService(svc_handle, BTA_GATT_TRANSPORT_LE); + + if (bt_le_bluedroid_gatts_sem_take()) { + LOG_ERR("[B]SvcStartFail"); + return -1; + } + + return 0; +} diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/server.h b/components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/server.h similarity index 100% rename from components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/profiles/server.h rename to components/bt/esp_ble_audio/host/adapter/nimble/include/nimble/server.h 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 8a8346eeceb..aac2efd5df8 100644 --- a/components/bt/esp_ble_audio/host/adapter/nimble/init.c +++ b/components/bt/esp_ble_audio/host/adapter/nimble/init.c @@ -313,13 +313,18 @@ static int nimble_gatt_csis_init(struct bt_le_audio_start_info *info, } csis_svc[count] = lib_csip_set_member_svc_get(info->csis_insts[i].svc_inst); - assert(csis_svc[count]); + if (!csis_svc[count]) { + LOG_ERR("[N]CsisSvcGetFail[%u]", i); + return -ENODEV; + } if (info->csis_insts[i].included_by_cas) { if (*inc_csis_svc == NULL) { *inc_csis_svc = csis_svc[count]; } else { - assert(0); + /* CAS may include at most one CSIS — caller misconfigured. */ + LOG_ERR("[N]CsisMultiIncByCas"); + return -EINVAL; } } 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 830a20e243c..2b82fdd6145 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 @@ -23,7 +23,7 @@ #include "host/ble_gatt.h" #include "host/ble_hs_mbuf.h" -#include "nimble/profiles/server.h" +#include "nimble/server.h" #include "common/host.h" @@ -122,7 +122,10 @@ int bt_le_nimble_ascs_attr_handle_set(void) } ascs_svc = lib_ascs_svc_get(); - assert(ascs_svc); + if (!ascs_svc) { + LOG_ERR("[N]AscsSvcGetFail"); + return -ENODEV; + } assert(ase_control_point_handle >= 2); start_handle = ase_control_point_handle - 2; /* server attr handle & char def handle */ @@ -164,7 +167,10 @@ static int ascs_svc_check(void) */ ascs_svc = lib_ascs_svc_get(); - assert(ascs_svc); + if (!ascs_svc) { + LOG_ERR("[N]AscsSvcGetFail"); + return -ENODEV; + } LOG_DBG("[N]AscsSvcCheck"); 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 f123d0c99bd..e0499d5e2d0 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 @@ -23,7 +23,7 @@ #include "host/ble_gatt.h" #include "host/ble_hs_mbuf.h" -#include "nimble/profiles/server.h" +#include "nimble/server.h" #include "common/host.h" @@ -98,7 +98,10 @@ int bt_le_nimble_bass_attr_handle_set(void) } bass_svc = lib_bap_bass_svc_get(); - assert(bass_svc); + if (!bass_svc) { + LOG_ERR("[N]BassSvcGetFail"); + return -ENODEV; + } assert(bass_control_point_handle >= 2); start_handle = bass_control_point_handle - 2; /* server attr handle & char def handle */ @@ -134,7 +137,10 @@ static int bass_svc_check(void) */ bass_svc = lib_bap_bass_svc_get(); - assert(bass_svc); + if (!bass_svc) { + LOG_ERR("[N]BassSvcGetFail"); + return -ENODEV; + } LOG_DBG("[N]BassSvcCheck"); 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 3cee66578f6..db2f25527c7 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 @@ -60,7 +60,10 @@ int bt_le_nimble_cas_attr_handle_set(void) } cas_svc = lib_cas_svc_get(); - assert(cas_svc); + if (!cas_svc) { + LOG_ERR("[N]CasSvcGetFail"); + return -ENODEV; + } LOG_DBG("[N]CasAttrHdlSet[%u][%u]", handle, cas_svc->attr_count); @@ -113,7 +116,10 @@ int bt_le_nimble_cas_init(void *csis_svc_p) struct bt_gatt_service *cas_svc; cas_svc = lib_cas_svc_get(); - assert(cas_svc); + if (!cas_svc) { + LOG_ERR("[N]CasSvcGetFail"); + return -ENODEV; + } /* Insert CAS to the GATT db list */ rc = bt_gatt_service_register_safe(cas_svc); 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 939976e415c..238c1296b47 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 @@ -25,7 +25,7 @@ #include "host/ble_gatt.h" #include "host/ble_hs_mbuf.h" -#include "nimble/profiles/server.h" +#include "nimble/server.h" #include "common/host.h" 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 2c94637c8ad..4eeff2d4343 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 @@ -23,7 +23,7 @@ #include "host/ble_gatt.h" #include "host/ble_hs_mbuf.h" -#include "nimble/profiles/server.h" +#include "nimble/server.h" #include "common/host.h" @@ -120,7 +120,10 @@ int bt_le_nimble_has_attr_handle_set(void) } has_svc = lib_has_svc_get(); - assert(has_svc); + if (!has_svc) { + LOG_ERR("[N]HasSvcGetFail"); + return -ENODEV; + } assert(has_svc->attr_count > 0); end_handle = start_handle + has_svc->attr_count - 1; @@ -136,7 +139,7 @@ int bt_le_nimble_has_attr_handle_set(void) attr = has_svc->attrs + has_svc->attr_count - 1; if (attr->handle != end_handle) { - LOG_ERR("[N]HasMismatchAttrHdl (%u %u %u %u)", + LOG_ERR("[N]HasMismatchAttrHdl[%u][%u][%u][%u]", start_handle, end_handle, attr->handle, has_svc->attr_count); return -1; } @@ -155,7 +158,10 @@ static int has_svc_check(void) */ has_svc = lib_has_svc_get(); - assert(has_svc); + if (!has_svc) { + LOG_ERR("[N]HasSvcGetFail"); + return -ENODEV; + } LOG_DBG("[N]HasSvcCheck"); 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 c0386353aef..ed04817e0b4 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 @@ -28,7 +28,7 @@ #include "host/ble_gatt.h" #include "host/ble_hs_mbuf.h" -#include "nimble/profiles/server.h" +#include "nimble/server.h" #include "common/host.h" @@ -403,7 +403,10 @@ int bt_le_nimble_gmcs_attr_handle_set(void) } gmcs_svc = lib_mcs_svc_get(); - assert(gmcs_svc); + if (!gmcs_svc) { + LOG_ERR("[N]GmcsSvcGetFail"); + return -ENODEV; + } end_handle = start_handle + gmcs_svc->attr_count - 1; @@ -446,7 +449,10 @@ static int gmcs_svc_check(void) */ gmcs_svc = lib_mcs_svc_get(); - assert(gmcs_svc); + if (!gmcs_svc) { + LOG_ERR("[N]GmcsSvcGetFail"); + return -ENODEV; + } LOG_DBG("[N]GmcsSvcCheck"); 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 36828699ea4..472729d2a3e 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 @@ -23,7 +23,7 @@ #include "host/ble_gatt.h" #include "host/ble_hs_mbuf.h" -#include "nimble/profiles/server.h" +#include "nimble/server.h" #include "common/host.h" @@ -159,7 +159,10 @@ static int mics_svc_check(void) bool chr_found; mics_svc = lib_mics_svc_get(); - assert(mics_svc); + if (!mics_svc) { + LOG_ERR("[N]MicsSvcGetFail"); + return -ENODEV; + } LOG_DBG("[N]MicsSvcCheck"); @@ -244,7 +247,10 @@ int bt_le_nimble_mics_attr_handle_set(void) } mics_svc = lib_mics_svc_get(); - assert(mics_svc); + if (!mics_svc) { + LOG_ERR("[N]MicsSvcGetFail"); + return -ENODEV; + } end_handle = start_handle + mics_svc->attr_count - 1; @@ -379,6 +385,11 @@ int bt_le_nimble_mics_init(void *micp_inc) inc_aics_svc_init(&inc_aics_insts[i], &gatt_svc_inc_aics[i]); inc_aics_insts[i].svc_p = lib_aics_svc_get(micp_included->aics[i]); + if (!inc_aics_insts[i].svc_p) { + LOG_ERR("[N]AicsSvcGetFail[%u]", i); + rc = -ENODEV; + goto free; + } mics_inc_svcs[i] = &gatt_svc_inc_aics[i]; } diff --git a/components/bt/esp_ble_audio/host/adapter/nimble/profiles/pacs.c b/components/bt/esp_ble_audio/host/adapter/nimble/profiles/pacs.c index 615b9271d8e..c93107a5150 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 @@ -21,7 +21,7 @@ #include "host/ble_gatt.h" #include "host/ble_hs_mbuf.h" -#include "nimble/profiles/server.h" +#include "nimble/server.h" #include "common/host.h" @@ -166,7 +166,10 @@ int bt_le_nimble_pacs_attr_handle_set(void) uint16_t end_handle = 0; pacs_svc = lib_pacs_svc_get(); - assert(pacs_svc); + if (!pacs_svc) { + LOG_ERR("[N]PacsSvcGetFail"); + return -ENODEV; + } #if CONFIG_BT_PAC_SNK assert(pacs_snk_handle >= 2); @@ -214,7 +217,10 @@ static int pacs_svc_check(void) */ pacs_svc = lib_pacs_svc_get(); - assert(pacs_svc); + if (!pacs_svc) { + LOG_ERR("[N]PacsSvcGetFail"); + return -ENODEV; + } LOG_DBG("[N]PacsSvcCheck"); 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 b56a93823b0..49551baefd3 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 @@ -21,7 +21,7 @@ #include "host/ble_gatt.h" #include "host/ble_hs_mbuf.h" -#include "nimble/profiles/server.h" +#include "nimble/server.h" #include "common/host.h" @@ -178,7 +178,10 @@ int bt_le_nimble_gtbs_attr_handle_set(void) } gtbs_svc = lib_gtbs_svc_get(); - assert(gtbs_svc); + if (!gtbs_svc) { + LOG_ERR("[N]GtbsSvcGetFail"); + return -ENODEV; + } LOG_DBG("[N]GtbsAttrHdlSet[%u][%u]", handle, gtbs_svc->attr_count); @@ -200,7 +203,10 @@ static int gtbs_svc_check(void) */ gtbs_svc = lib_gtbs_svc_get(); - assert(gtbs_svc); + if (!gtbs_svc) { + LOG_ERR("[N]GtbsSvcGetFail"); + return -ENODEV; + } LOG_DBG("[N]GtbsSvcCheck"); 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 c97c3ab1bc8..6e861c8013d 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 @@ -21,7 +21,7 @@ #include "host/ble_gatt.h" #include "host/ble_hs_mbuf.h" -#include "nimble/profiles/server.h" +#include "nimble/server.h" #include "common/host.h" @@ -71,7 +71,10 @@ int bt_le_nimble_tmas_attr_handle_set(void) } tmas_svc = lib_tmas_svc_get(); - assert(tmas_svc); + if (!tmas_svc) { + LOG_ERR("[N]TmasSvcGetFail"); + return -ENODEV; + } LOG_DBG("[N]TmasAttrHdlSet[%u][%u]", handle, tmas_svc->attr_count); @@ -93,7 +96,10 @@ static int tmas_svc_check(void) */ tmas_svc = lib_tmas_svc_get(); - assert(tmas_svc); + if (!tmas_svc) { + LOG_ERR("[N]TmasSvcGetFail"); + return -ENODEV; + } LOG_DBG("[N]TmasSvcCheck"); 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 58e7e8c4751..2c775fe6f5e 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 @@ -23,7 +23,7 @@ #include "host/ble_gatt.h" #include "host/ble_hs_mbuf.h" -#include "nimble/profiles/server.h" +#include "nimble/server.h" #include "common/host.h" @@ -261,7 +261,10 @@ static int vcs_svc_check(void) */ vcs_svc = lib_vcs_svc_get(); - assert(vcs_svc); + if (!vcs_svc) { + LOG_ERR("[N]VcsSvcGetFail"); + return -ENODEV; + } LOG_DBG("[N]VCSSvcCheck"); @@ -380,7 +383,10 @@ int bt_le_nimble_vcs_attr_handle_set(void) } vcs_svc = lib_vcs_svc_get(); - assert(vcs_svc); + if (!vcs_svc) { + LOG_ERR("[N]VcsSvcGetFail"); + return -ENODEV; + } end_handle = start_handle + vcs_svc->attr_count - 1; @@ -576,6 +582,11 @@ int bt_le_nimble_vcs_init(void *vcp_inc) inc_vocs_svc_init(&inc_vocs_insts[i], &gatt_svc_inc_vocs[i]); inc_vocs_insts[i].svc_p = lib_vocs_svc_get(vcp_included->vocs[i]); + if (!inc_vocs_insts[i].svc_p) { + LOG_ERR("[N]VocsSvcGetFail[%u]", i); + rc = -ENODEV; + goto free; + } vcs_inc_svcs[i] = &gatt_svc_inc_vocs[i]; } @@ -608,6 +619,11 @@ int bt_le_nimble_vcs_init(void *vcp_inc) inc_aics_svc_init(&inc_aics_insts[i], &gatt_svc_inc_aics[i]); inc_aics_insts[i].svc_p = lib_aics_svc_get(vcp_included->aics[i]); + if (!inc_aics_insts[i].svc_p) { + LOG_ERR("[N]AicsSvcGetFail[%u]", i); + rc = -ENODEV; + goto free; + } vcs_inc_svcs[inc_vocs_svc_count + i] = &gatt_svc_inc_aics[i]; } @@ -670,13 +686,17 @@ free: } if (inc_aics_svc_count) { - 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; - } + /* 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) { + 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/server.c b/components/bt/esp_ble_audio/host/adapter/nimble/server.c similarity index 100% rename from components/bt/esp_ble_audio/host/adapter/nimble/profiles/server.c rename to components/bt/esp_ble_audio/host/adapter/nimble/server.c diff --git a/components/bt/esp_ble_audio/host/common/init.c b/components/bt/esp_ble_audio/host/common/init.c index 8330e483334..e179e1eabb6 100644 --- a/components/bt/esp_ble_audio/host/common/init.c +++ b/components/bt/esp_ble_audio/host/common/init.c @@ -44,7 +44,11 @@ #include <../host/conn_internal.h> #include <../host/hci_core.h> +#if CONFIG_BT_BLUEDROID_ENABLED +#include "bluedroid/init.h" +#else #include "nimble/init.h" +#endif #include "../../../lib/include/audio.h" @@ -2091,7 +2095,11 @@ int bt_le_audio_init(void) return err; } +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_audio_init(); +#else return bt_le_nimble_audio_init(); +#endif } #if BLE_AUDIO_SVC_DEFERRED_ADD @@ -2100,7 +2108,11 @@ int bt_le_ascs_init(void) { LOG_DBG("AscsInit"); +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_ascs_init(); +#else return bt_le_nimble_ascs_init(); +#endif } #endif /* CONFIG_BT_ASCS */ @@ -2109,7 +2121,11 @@ int bt_le_bass_init(void) { LOG_DBG("BassInit"); +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_bass_init(); +#else return bt_le_nimble_bass_init(); +#endif } #endif /* CONFIG_BT_BAP_SCAN_DELEGATOR */ @@ -2118,7 +2134,11 @@ int bt_le_tmas_init(void) { LOG_DBG("TmasInit"); +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_tmas_init(); +#else return bt_le_nimble_tmas_init(); +#endif } #endif /* CONFIG_BT_TMAP */ @@ -2127,7 +2147,11 @@ int bt_le_gtbs_init(void) { LOG_DBG("GtbsInit"); +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_gtbs_init(); +#else return bt_le_nimble_gtbs_init(); +#endif } #endif /* CONFIG_BT_TBS */ @@ -2136,7 +2160,11 @@ int bt_le_has_init(void) { LOG_DBG("HasInit"); +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_has_init(); +#else return bt_le_nimble_has_init(); +#endif } #endif /* CONFIG_BT_HAS */ @@ -2145,7 +2173,11 @@ int bt_le_media_proxy_pl_init(void) { LOG_DBG("MprxPlInit"); +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_media_proxy_pl_init(); +#else return bt_le_nimble_media_proxy_pl_init(); +#endif } #endif /* CONFIG_BT_MCS */ @@ -2154,7 +2186,11 @@ int bt_le_vcp_vol_rend_init(void) { LOG_DBG("VcpVolRendInit"); +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_vcp_vol_rend_init(); +#else return bt_le_nimble_vcp_vol_rend_init(); +#endif } #endif /* CONFIG_BT_VCP_VOL_REND */ @@ -2163,7 +2199,11 @@ int bt_le_micp_mic_dev_init(void) { LOG_DBG("MicpMicDevInit"); +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_micp_mic_dev_init(); +#else return bt_le_nimble_micp_mic_dev_init(); +#endif } #endif /* CONFIG_BT_MICP_MIC_DEV */ #endif /* BLE_AUDIO_SVC_DEFERRED_ADD */ @@ -2172,7 +2212,11 @@ int bt_le_audio_start(void *info) { LOG_DBG("AudioStart"); +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_audio_start(info); +#else return bt_le_nimble_audio_start(info); +#endif } void ble_audio_lib_compressed_out(uint8_t log_level, uint32_t log_index, size_t arg_cnt, ...) diff --git a/components/bt/esp_ble_iso/CMakeLists.txt b/components/bt/esp_ble_iso/CMakeLists.txt index 567ec615ef3..6c3e32f58a0 100644 --- a/components/bt/esp_ble_iso/CMakeLists.txt +++ b/components/bt/esp_ble_iso/CMakeLists.txt @@ -15,12 +15,24 @@ if(CONFIG_BT_CONTROLLER_ONLY OR NOT CONFIG_BT_ISO) return() endif() +if(CONFIG_BT_NIMBLE_ENABLED) + list(APPEND ble_iso_srcs + "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/gatt/gatt.c" + "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/gatt/gatt.db.c" + "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/gatt/gatt.nrp.c" + "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/gap.c" + "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/iso.c" + ) +elseif(CONFIG_BT_BLUEDROID_ENABLED) + list(APPEND ble_iso_srcs + "${CMAKE_CURRENT_LIST_DIR}/host/adapter/bluedroid/hci.c" + "${CMAKE_CURRENT_LIST_DIR}/host/adapter/bluedroid/iso.c" + "${CMAKE_CURRENT_LIST_DIR}/host/adapter/bluedroid/gap.c" + "${CMAKE_CURRENT_LIST_DIR}/host/adapter/bluedroid/gatt/gatt.c" + ) +endif() + list(APPEND ble_iso_srcs - "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/gatt/gatt.c" - "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/gatt/gatt.db.c" - "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/gatt/gatt.nrp.c" - "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/gap.c" - "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/iso.c" "${CMAKE_CURRENT_LIST_DIR}/host/common/adv.c" "${CMAKE_CURRENT_LIST_DIR}/host/common/conn.c" "${CMAKE_CURRENT_LIST_DIR}/host/common/gatt.c" @@ -45,23 +57,37 @@ list(APPEND ble_iso_srcs ) # L2CAP host shim wiring is currently only consumed by the OTS service -# (which lives in esp_ble_audio). Keep the same gate so build output -# matches the pre-split behavior; the .c files belong to the host shim. +# (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" - "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/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/adapter/nimble/include" "${CMAKE_CURRENT_LIST_DIR}/include/subsys/bluetooth" "${CMAKE_CURRENT_LIST_DIR}/include/subsys/bluetooth/host" "${CMAKE_CURRENT_LIST_DIR}/include" ) +if(CONFIG_BT_NIMBLE_ENABLED) + list(APPEND ble_iso_include_dirs + "${CMAKE_CURRENT_LIST_DIR}/host/adapter/nimble/include" + ) +elseif(CONFIG_BT_BLUEDROID_ENABLED) + list(APPEND ble_iso_include_dirs + "${CMAKE_CURRENT_LIST_DIR}/host/adapter/bluedroid/include" + ) +endif() + set(ble_iso_srcs "${ble_iso_srcs}" PARENT_SCOPE) set(ble_iso_include_dirs "${ble_iso_include_dirs}" PARENT_SCOPE) diff --git a/components/bt/esp_ble_iso/Kconfig.in b/components/bt/esp_ble_iso/Kconfig.in index 6461c3e224a..4e977e7718d 100644 --- a/components/bt/esp_ble_iso/Kconfig.in +++ b/components/bt/esp_ble_iso/Kconfig.in @@ -6,6 +6,7 @@ config BT_ISO bool select BT_NIMBLE_ISO if BT_NIMBLE_ENABLED + select BT_BLE_FEAT_ISO_EN if BT_BLUEDROID_ENABLED config BT_ISO_TX bool @@ -44,7 +45,6 @@ config BT_ISO_BROADCASTER bool "Bluetooth Isochronous Broadcaster Support" select BT_ISO_BROADCAST select BT_ISO_TX - select BT_BROADCASTER help This option enables support for the Bluetooth Isochronous Broadcaster. @@ -68,6 +68,7 @@ if BT_ISO config BT_ISO_TEST_PARAMS bool "ISO test parameters support" + default y if BT_BLUEDROID_ENABLED default BT_NIMBLE_ISO_TEST if BT_NIMBLE_ENABLED help Enabling advanced ISO parameters will allow the use of the ISO test @@ -79,7 +80,10 @@ if BT_ISO config BT_ISO_MAX_CIG int "Maximum number of Connected Isochronous Groups (CIGs) to support" - range 1 BT_NIMBLE_ISO_CIG + range 1 1 if BT_BLUEDROID_ENABLED + range 1 BT_NIMBLE_ISO_CIG if BT_NIMBLE_ENABLED + default 1 if BT_BLUEDROID_ENABLED + default BT_NIMBLE_ISO_CIG if BT_NIMBLE_ENABLED help Maximum number of CIGs that are supported by the host. A CIG can be used for either transmitting or receiving. @@ -90,7 +94,10 @@ if BT_ISO config BT_ISO_MAX_BIG int "Maximum number of Broadcast Isochronous Groups (BIGs) to support" - range 1 BT_NIMBLE_ISO_BIG + range 1 1 if BT_BLUEDROID_ENABLED + range 1 BT_NIMBLE_ISO_BIG if BT_NIMBLE_ENABLED + default 1 if BT_BLUEDROID_ENABLED + default BT_NIMBLE_ISO_BIG if BT_NIMBLE_ENABLED help Maximum number of BIGs that are supported by the host. A BIG can be used for either transmitting or receiving, but not at the same time. 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 62e7f6f6a66..5dcf4516f95 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 @@ -430,3 +430,10 @@ unregister_gap: } return ESP_FAIL; } + +#if CONFIG_BT_BLUEDROID_ENABLED +uint8_t esp_ble_iso_bluedroid_get_gattc_if(void) +{ + return bt_le_bluedroid_gattc_get_if(); +} +#endif /* CONFIG_BT_BLUEDROID_ENABLED */ 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 52ec59ac08e..3e0fdc0cdf7 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 @@ -591,6 +591,21 @@ 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); +#if CONFIG_BT_BLUEDROID_ENABLED +/** + * @brief Get the engine's internal GATTC interface handle (Bluedroid only). + * + * Pass this to esp_ble_gattc_aux_open() / esp_ble_gattc_open() so the + * resulting ACL events route back to the engine, avoiding the need for the + * application to register a second BTA GATTC app for connection initiation. + * + * @return Engine's gattc_if (ABI-compatible with esp_gatt_if_t), or + * ESP_GATT_IF_NONE (0xFF) if GATTC registration has not completed — + * callers must bail rather than pass it to aux_open. + */ +uint8_t esp_ble_iso_bluedroid_get_gattc_if(void); +#endif /* CONFIG_BT_BLUEDROID_ENABLED */ + #ifdef __cplusplus } #endif diff --git a/components/bt/esp_ble_iso/host/adapter/bluedroid/gap.c b/components/bt/esp_ble_iso/host/adapter/bluedroid/gap.c new file mode 100644 index 00000000000..4f34eff91fc --- /dev/null +++ b/components/bt/esp_ble_iso/host/adapter/bluedroid/gap.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 + +#include <../host/hci_core.h> +#include <../host/iso_internal.h> + +#include "bta/bta_api.h" +#include "stack/btm_ble_api.h" +#include "stack/hcidefs.h" + +#include "esp_gap_ble_api.h" +#include "esp_gattc_api.h" +#include "esp_gatts_api.h" + +#include "bluedroid/btm_error.h" +#include "bluedroid/hci.h" + +#include "common/host.h" +#include "common/app/gap.h" + +LOG_MODULE_REGISTER(ISO_BGAP, CONFIG_BT_ISO_LOG_LEVEL); + +extern void btc_ble_5_gap_callback(tBTA_DM_BLE_5_GAP_EVENT event, + tBTA_DM_BLE_5_GAP_CB_PARAMS *params); + +#if (BLE_50_EXTEND_SYNC_EN == TRUE) +/* Active PA sync handle tracker. Used to synthesize a PA_SYNC_LOST event + * to the iso task when the application calls + * esp_ble_gap_periodic_adv_sync_terminate(): Bluedroid's + * BTM_BLE_5_GAP_PERIODIC_ADV_SYNC_TERMINATE_COMPLETE_EVT only carries a + * status byte and lacks sync_handle, so we recover it from here. This + * mirrors NimBLE host behavior, where ble_gap_periodic_adv_sync_terminate + * internally enqueues a sync_lost event. + * + * The tracker is populated by both PA_SYNC_ESTAB (direct scan-and-sync) + * and PA_SYNC_TRANS_RECV (PAST-delivered sync) on success, and cleared + * by real PA_SYNC_LOST or by the synthesized terminate path below. + * + * Single-slot — assumes one active PA sync (and at most one in-flight + * terminate) at a time. The audio examples never hold multiple PA syncs + * concurrently. Multi-sync acceptors / Broadcast Assistants will need an + * N-slot tracker plus per-terminate intent capture. + * + * TODO(bluedroid): drop this once + * BTM_BLE_5_GAP_PERIODIC_ADV_SYNC_TERMINATE_COMPLETE_EVT also surfaces + * sync_handle (BTM_BlePeriodicAdvSyncTerm in btm_ble_5_gap.c knows the + * handle, it just isn't propagated into cb_params). Ideally Bluedroid + * would directly synthesize a SYNC_LOST event on successful terminate, + * 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; +#endif /* BLE_50_EXTEND_SYNC_EN == TRUE */ + +/* Fast-path BTA → iso-queue post. + * + * BTU calls gap_app_cb directly; this function bypasses the BTC dispatch + * hop for the audio-critical events so they reach the iso task in the same + * order BTU received them. BIGINFO_ADV_REPORT already posts directly from + * iso_evt_handler; routing PA_SYNC_ESTAB / PA_SYNC_LOST / EXT_ADV_REPORT / + * PAST_RECV the same way keeps both event streams ordered. */ +static void bt_le_bluedroid_gap_post_event_bta(tBTA_DM_BLE_5_GAP_EVENT event, + tBTA_DM_BLE_5_GAP_CB_PARAMS *params); + +static void gap_app_cb(tBTA_DM_BLE_5_GAP_EVENT event, tBTA_DM_BLE_5_GAP_CB_PARAMS *params) +{ + /* Audio-critical data events: post directly to iso task here to keep + * ordering with BIGINFO_ADV_REPORT (which iso_evt_handler also posts + * directly). The BTC dispatch below still runs for non-audio recipients + * of esp_ble_gap_register_callback. */ + switch (event) { +#if (BLE_50_EXTEND_SCAN_EN == TRUE) + case BTM_BLE_5_GAP_EXT_ADV_REPORT_EVT: +#endif +#if (BLE_50_EXTEND_SYNC_EN == TRUE) + case BTM_BLE_5_GAP_PERIODIC_ADV_SYNC_ESTAB_EVT: + case BTM_BLE_5_GAP_PERIODIC_ADV_SYNC_LOST_EVT: + case BTM_BLE_5_GAP_PERIODIC_ADV_SYNC_TERMINATE_COMPLETE_EVT: + case BTM_BLE_5_GAP_PERIODIC_ADV_REPORT_EVT: +#endif +#if (BLE_FEAT_PERIODIC_ADV_SYNC_TRANSFER == TRUE) + case BTM_BLE_GAP_PERIODIC_ADV_SYNC_TRANS_RECV_EVT: +#endif + bt_le_bluedroid_gap_post_event_bta(event, params); + break; + default: + break; + } + + /* Forward to BTC so esp_ble_gap_register_callback() recipients see it. */ + btc_ble_5_gap_callback(event, params); +} + +void bt_le_bluedroid_gap_post_event(uint16_t event, void *param) +{ + const esp_ble_gap_cb_param_t *p = param; + struct bt_le_gap_app_param *qev = NULL; + int err; + + qev = calloc(1, sizeof(*qev)); + assert(qev); + + /* Only AUTH_CMPL reaches here from the application's + * esp_ble_gap_register_callback path. EXT_ADV_REPORT / PA_SYNC_ESTAB / + * PA_SYNC_LOST / PAST_RECV are posted directly by gap_app_cb on the BTM + * BLE 5 channel; SMP events have no such channel so AUTH_CMPL is the + * only one the application must forward. */ + 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); + + if (qev->security_change.status == 0) { + /* Derive level from auth_mode bits (mirrors NimBLE's sec_state + * mapping). esp_ble_auth_cmpl_t exposes neither encrypted/ + * authenticated flags nor a key_size, so MITM presence is the + * only authenticated-vs-Just-Works signal we have. */ + if (!(a->auth_mode & ESP_LE_AUTH_REQ_MITM)) { + sec_level = BT_SECURITY_L2; + } else if (!(a->auth_mode & ESP_LE_AUTH_REQ_SC_ONLY)) { + sec_level = BT_SECURITY_L3; + } else { + sec_level = BT_SECURITY_L4; + } + + qev->security_change.sec_level = sec_level; + qev->security_change.bonded = (a->auth_mode & ESP_LE_AUTH_BOND) ? 1 : 0; + } + break; + } + + default: + LOG_WRN("[B]GapPostEvtUnexp[%u]", event); + free(qev); + return; + } + + 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); + free(qev); + } +} + +static void bt_le_bluedroid_gap_post_event_bta(tBTA_DM_BLE_5_GAP_EVENT event, + tBTA_DM_BLE_5_GAP_CB_PARAMS *params) +{ + struct bt_le_gap_app_param *qev = NULL; + int err; + + qev = calloc(1, sizeof(*qev)); + assert(qev); + + switch (event) { +#if (BLE_50_EXTEND_SCAN_EN == TRUE) + case BTM_BLE_5_GAP_EXT_ADV_REPORT_EVT: { + const tBTM_BLE_EXT_ADV_REPORT *r = ¶ms->ext_adv_report; + + qev->type = BT_LE_GAP_APP_PARAM_EXT_SCAN_RECV; + + qev->ext_scan_recv.event_type = (r->data_status << 5) | r->event_type; + qev->ext_scan_recv.addr.type = r->addr_type; + memcpy(qev->ext_scan_recv.addr.val, r->addr, BT_ADDR_SIZE); + /* BTM declares rssi/tx_power as UINT8; the bytes are spec-defined as + * signed (RSSI typically negative). Cast preserves the bit pattern. */ + qev->ext_scan_recv.rssi = (int8_t)r->rssi; + qev->ext_scan_recv.tx_power = (int8_t)r->tx_power; + qev->ext_scan_recv.sid = r->sid; + qev->ext_scan_recv.pri_phy = r->primary_phy; + qev->ext_scan_recv.sec_phy = r->secondry_phy; + qev->ext_scan_recv.per_adv_itvl = r->per_adv_interval; + 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); + memcpy(qev->ext_scan_recv.data, r->adv_data, qev->ext_scan_recv.data_len); + } + break; + } +#endif /* BLE_50_EXTEND_SCAN_EN == TRUE */ + +#if (BLE_50_EXTEND_SYNC_EN == TRUE) + case BTM_BLE_5_GAP_PERIODIC_ADV_SYNC_ESTAB_EVT: { + const tBTM_BLE_PERIOD_ADV_SYNC_ESTAB *e = ¶ms->sync_estab; + + qev->type = BT_LE_GAP_APP_PARAM_PA_SYNC; + + qev->pa_sync.status = e->status; + qev->pa_sync.sync_handle = e->sync_handle; + qev->pa_sync.addr.type = e->adv_addr_type; + memcpy(qev->pa_sync.addr.val, e->adv_addr, BT_ADDR_SIZE); + qev->pa_sync.sid = e->sid; + qev->pa_sync.adv_phy = e->adv_phy; + qev->pa_sync.per_adv_itvl = e->period_adv_interval; + qev->pa_sync.adv_ca = e->adv_clk_accuracy; + + if (e->status == 0) { + if (active_pa_sync_handle != ISO_PA_SYNC_HANDLE_NONE) { + /* Concurrent PA sync: single-slot tracker overwrites the + * existing handle, so a later terminate on the first sync + * would synthesize SYNC_LOST against the wrong one. + * See ISO_PA_SYNC_HANDLE_NONE doc for the multi-sync TODO. */ + LOG_WRN("[B]PaSyncEstabOverwrite[%04x->%04x]", + active_pa_sync_handle, e->sync_handle); + } else { + LOG_INF("[B]PaSyncEstab[%04x]", e->sync_handle); + } + active_pa_sync_handle = e->sync_handle; + } + break; + } + + case BTM_BLE_5_GAP_PERIODIC_ADV_SYNC_LOST_EVT: + qev->type = BT_LE_GAP_APP_PARAM_PA_SYNC_LOST; + + /* Public API exposes only sync_handle; reason isn't surfaced. */ + qev->pa_sync_lost.sync_handle = params->sync_lost.sync_handle; + qev->pa_sync_lost.reason = 0; + + if (active_pa_sync_handle == qev->pa_sync_lost.sync_handle) { + LOG_INF("[B]PaSyncLost[%04x]", qev->pa_sync_lost.sync_handle); + active_pa_sync_handle = ISO_PA_SYNC_HANDLE_NONE; + } + break; + + case BTM_BLE_5_GAP_PERIODIC_ADV_SYNC_TERMINATE_COMPLETE_EVT: + /* Host-commanded terminate: synthesize PA_SYNC_LOST so the iso + * task sees a unified sync-end event regardless of whether the + * sync was lost on-air or torn down via + * esp_ble_gap_periodic_adv_sync_terminate(). The complete event + * only carries status; recover sync_handle from the tracker. + * See ISO_PA_SYNC_HANDLE_NONE definition for limitations and + * the Bluedroid TODO. */ + if (params->per_adv_sync_term.status != BTM_SUCCESS || + active_pa_sync_handle == ISO_PA_SYNC_HANDLE_NONE) { + LOG_WRN("[B]PaSyncTermNoSynth[%02x][%04x]", + params->per_adv_sync_term.status, active_pa_sync_handle); + free(qev); + return; + } + + qev->type = BT_LE_GAP_APP_PARAM_PA_SYNC_LOST; + qev->pa_sync_lost.sync_handle = active_pa_sync_handle; + qev->pa_sync_lost.reason = 0; + + LOG_INF("[B]PaSyncTermSynth[%04x]", active_pa_sync_handle); + active_pa_sync_handle = ISO_PA_SYNC_HANDLE_NONE; + break; + + case BTM_BLE_5_GAP_PERIODIC_ADV_REPORT_EVT: { + const tBTM_PERIOD_ADV_REPORT *r = ¶ms->period_adv_report; + + qev->type = BT_LE_GAP_APP_PARAM_PA_SYNC_RECV; + + qev->pa_sync_recv.sync_handle = r->sync_handle; + qev->pa_sync_recv.tx_power = r->tx_power; + qev->pa_sync_recv.rssi = r->rssi; + qev->pa_sync_recv.data_status = r->data_status; + 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); + memcpy(qev->pa_sync_recv.data, r->data, qev->pa_sync_recv.data_len); + } + break; + } +#endif /* BLE_50_EXTEND_SYNC_EN == TRUE */ + +#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; + + qev->pa_sync_past.status = r->status; + qev->pa_sync_past.sync_handle = r->sync_handle; + qev->pa_sync_past.addr.type = r->adv_addr_type; + memcpy(qev->pa_sync_past.addr.val, r->adv_addr, BT_ADDR_SIZE); + qev->pa_sync_past.sid = r->adv_sid; + 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; + +#if (BLE_50_EXTEND_SYNC_EN == TRUE) + /* PAST-established syncs need tracker too: a later app-initiated + * terminate hits TERMINATE_COMPLETE_EVT regardless of how the sync + * was originally created. Gated by BLE_50_EXTEND_SYNC_EN since + * that's where the tracker decl + LOST/TERMINATE consumers live. */ + if (r->status == 0) { + if (active_pa_sync_handle != ISO_PA_SYNC_HANDLE_NONE) { + LOG_WRN("[B]PaSyncTransOverwrite[%04x->%04x]", + active_pa_sync_handle, r->sync_handle); + } else { + LOG_INF("[B]PaSyncTrans[%04x]", r->sync_handle); + } + active_pa_sync_handle = r->sync_handle; + } +#endif /* BLE_50_EXTEND_SYNC_EN == TRUE */ + break; + } +#endif /* BLE_FEAT_PERIODIC_ADV_SYNC_TRANSFER == TRUE */ + + default: + free(qev); + return; + } + + err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GAP_EVENT, qev, sizeof(*qev)); + if (err) { + LOG_ERR("[B]GapPostEvtBtaFail[%d][%u]", err, qev->type); + goto free; + } + + 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); +} + +int bt_le_bluedroid_scan_start(const struct bt_le_scan_param *param) +{ + tBTM_STATUS status; + + LOG_DBG("[B]ScanStart[%u][%u][%u]", param->type, param->interval, param->window); + +#if USE_DIRECT_HCI + { + /* HCI LE Set Extended Scan Parameters (uncoded only): + * own_addr_type(1) | scan_filter_policy(1) | scanning_phys(1) + * | per-phy { scan_type(1) | scan_interval(2) | scan_window(2) } */ + uint8_t cmd_params[8]; + + cmd_params[0] = BLE_ADDR_PUBLIC; + cmd_params[1] = 0; /* filter_policy: accept all */ + cmd_params[2] = 0x01; /* scanning_phys: LE 1M only */ + cmd_params[3] = param->type; + sys_put_le16(param->interval, cmd_params + 4); + sys_put_le16(param->window, cmd_params + 6); + + status = bt_le_bluedroid_hci_send_sync(HCI_BLE_SET_EXT_SCAN_PARAMS, + cmd_params, sizeof(cmd_params), + NULL, 0); + } +#else /* USE_DIRECT_HCI */ + { + tBTM_BLE_EXT_SCAN_PARAMS scan_params = {0}; + + scan_params.own_addr_type = BLE_ADDR_PUBLIC; + scan_params.filter_policy = 0; + scan_params.scan_duplicate = 0; + scan_params.cfg_mask = BTM_BLE_GAP_EXT_SCAN_UNCODE_MASK; + scan_params.uncoded_cfg.scan_type = param->type; + scan_params.uncoded_cfg.scan_interval = param->interval; + scan_params.uncoded_cfg.scan_window = param->window; + + bt_le_host_lock(); + status = BTM_BleSetExtendedScanParams(&scan_params); + bt_le_host_unlock(); + } +#endif /* USE_DIRECT_HCI */ + + if (status != BTM_SUCCESS) { + LOG_ERR("[B]SetScanParamsFail[%02x]", status); + return bluedroid_err_to_errno(status); + } + +#if USE_DIRECT_HCI + { + /* HCI LE Set Extended Scan Enable: + * enable(1) | filter_duplicates(1) | duration(2) | period(2) + * duration=period=0 → continuous scan. */ + uint8_t cmd_params[6] = { 1, 0, 0, 0, 0, 0 }; + + status = bt_le_bluedroid_hci_send_sync(HCI_BLE_SET_EXT_SCAN_ENABLE, + cmd_params, sizeof(cmd_params), + NULL, 0); + } +#else /* USE_DIRECT_HCI */ + bt_le_host_lock(); + status = BTM_BleExtendedScan(true, 0, 0); + bt_le_host_unlock(); +#endif /* USE_DIRECT_HCI */ + + if (status != BTM_SUCCESS) { + LOG_ERR("[B]ScanStartFail[%02x]", status); + } + + return bluedroid_err_to_errno(status); +} + +int bt_le_bluedroid_scan_stop(void) +{ + tBTM_STATUS status; + + LOG_DBG("[B]ScanStop"); + +#if USE_DIRECT_HCI + { + /* HCI LE Set Extended Scan Enable with enable=0; other fields + * are ignored by the controller per spec but must be present. */ + uint8_t cmd_params[6] = { 0, 0, 0, 0, 0, 0 }; + + status = bt_le_bluedroid_hci_send_sync(HCI_BLE_SET_EXT_SCAN_ENABLE, + cmd_params, sizeof(cmd_params), + NULL, 0); + } +#else /* USE_DIRECT_HCI */ + bt_le_host_lock(); + status = BTM_BleExtendedScan(false, 0, 0); + bt_le_host_unlock(); +#endif /* USE_DIRECT_HCI */ + + if (status != BTM_SUCCESS) { + LOG_ERR("[B]ScanStopFail[%02x]", status); + } + + return bluedroid_err_to_errno(status); +} + +int bt_le_bluedroid_iso_disconnect(uint16_t conn_handle, uint8_t reason) +{ + tBTM_STATUS status; + + 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. */ + status = BTM_BleDisconCis(conn_handle, reason); + + if (status != BTM_SUCCESS) { + LOG_ERR("[B]IsoDisconnFail[0x%03x][%02x]", conn_handle, status); + } + + return bluedroid_err_to_errno(status); +} + +int bt_le_bluedroid_gap_init(void) +{ + 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 new file mode 100644 index 00000000000..fa6864909aa --- /dev/null +++ b/components/bt/esp_ble_iso/host/adapter/bluedroid/gatt/gatt.c @@ -0,0 +1,3092 @@ +/* + * 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 "bta/bta_api.h" +#include "bta/bta_gatt_api.h" +#include "bta/bta_gatt_common.h" +#include "bta_gattc_int.h" +#include "btc_gatt_util.h" +#include "stack/btm_ble_api.h" + +#include "common/host.h" +#include "common/app/gap.h" +#include "common/app/gatt.h" + +LOG_MODULE_REGISTER(ISO_BGAT, CONFIG_BT_ISO_LOG_LEVEL); + +/* Use UUID with a fixed pattern 0x98 for ISO & LE Audio GATT Server. + * 0x96/0x97 are reserved by BLE Mesh; pick a distinct byte for any new + * 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; + +/* 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 struct gatts_svc_cb *gatts_svc_cb; + +static 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; + +/* 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; + +enum { + GATTC_OP_READ, + GATTC_OP_WRITE, +}; + +/* One in-flight GATTC op (read or write). FIFO matches BTA p_cmd_list. */ +struct gattc_list_node { + sys_snode_t node; + uint8_t type; + union { + struct bt_gatt_read_params *read_params; + struct bt_gatt_write_params *write_params; + }; +}; + +static struct gattc_list_node *gattc_list_node_alloc(uint8_t type, void *params) +{ + struct gattc_list_node *op; + + op = calloc(1, sizeof(*op)); + if (op == NULL) { + return NULL; + } + + op->type = type; + /* Both arms of the union are pointer-typed and same size — assigning via + * either field is equivalent. */ + op->read_params = params; + + return op; +} + +static void gattc_list_drain(struct gatt_conn *gatt_conn) +{ + struct gattc_list_node *op; + sys_snode_t *snode; + + while ((snode = sys_slist_get(&gatt_conn->gattc_list)) != NULL) { + op = CONTAINER_OF(snode, struct gattc_list_node, node); + + LOG_DBG("[B]GattcOpDrain[%u]", op->type); + /* lib clears its own state via disconn cb; don't invoke params->func + * here (BTA also doesn't deliver cmpl_evt for queued ops on close). */ + free(op); + } +} + +/* One pending indication. Deep-copies params + data because the caller may + * reuse the params slot or pass a stack-backed data buffer while we are + * queued behind another in-flight indication. Mirrors NimBLE NRP. */ +struct gatts_list_node { + sys_snode_t node; + struct bt_gatt_indicate_params params_copy; + uint16_t value_handle; /* resolved value handle for BTA dispatch */ + uint8_t *data_copy; /* NULL if params->len == 0 */ +}; + +/* Resolve attr (possibly by uuid lookup), validate INDICATE property for + * CHRC declarations, and emit the value handle to dispatch on. */ +static int gatts_indicate_resolve(const struct bt_gatt_attr *attr_in, + const struct bt_uuid *uuid, + const struct bt_gatt_attr **resolved_attr, + uint16_t *value_handle) +{ + struct bt_gatt_chrc *chrc; + struct notify_data data; + uint16_t handle; + + memset(&data, 0, sizeof(data)); + data.attr = attr_in; + data.handle = bt_gatt_attr_get_handle(data.attr); + + if (uuid) { + if (bt_gatts_find_attr_by_uuid(&data, uuid) == false) { + return -ENOENT; + } + } else if (data.handle == 0) { + return -ENOENT; + } + + handle = data.handle; + + if (bt_uuid_cmp(data.attr->uuid, BT_UUID_GATT_CHRC) == 0) { + assert(data.attr->user_data); + chrc = data.attr->user_data; + + if ((chrc->properties & BT_GATT_CHRC_INDICATE) == 0) { + return -EINVAL; + } + + handle = bt_gatt_attr_value_handle(data.attr); + } + + *resolved_attr = data.attr; + *value_handle = handle; + + return 0; +} + +static struct gatts_list_node *gatts_list_node_alloc(struct bt_gatt_indicate_params *ip, + const struct bt_gatt_attr *attr, + uint16_t value_handle) +{ + struct gatts_list_node *n; + + n = calloc(1, sizeof(*n)); + if (n == NULL) { + return NULL; + } + + n->params_copy = *ip; + n->params_copy.attr = attr; /* freeze the resolved attr in the copy */ + n->value_handle = value_handle; + n->data_copy = NULL; + + if (ip->len > 0) { + assert(ip->data); + + n->data_copy = malloc(ip->len); + if (n->data_copy == NULL) { + free(n); + return NULL; + } + + memcpy(n->data_copy, ip->data, ip->len); + } + /* lib sees the copy in func cb, so point copy's data at our backing buf */ + n->params_copy.data = n->data_copy; + + return n; +} + +static void gatts_list_node_free(struct gatts_list_node *n) +{ + if (n == NULL) { + return; + } + + free(n->data_copy); + free(n); +} + +/* Marker for one in-flight notify. BTA fires CONF_EVT for notify too — we + * push one of these per send and pop in handle_gatts_notify_tx_event to keep + * the indication queue (gatts_list) unmolested. */ +struct gatts_notify_node { + sys_snode_t node; + uint16_t value_handle; +}; + +static void gatts_notify_list_drain(struct gatt_conn *gatt_conn) +{ + struct gatts_notify_node *n; + sys_snode_t *snode; + + while ((snode = sys_slist_get(&gatt_conn->gatts_notify_list)) != NULL) { + n = CONTAINER_OF(snode, struct gatts_notify_node, node); + free(n); + } +} + +static void gatts_indicate_dispatch(struct bt_conn *conn, + struct gatts_list_node *n) +{ + uint16_t conn_id = BTC_GATT_CREATE_CONN_ID(gatts_if, conn->handle); + + LOG_DBG("[B]GattsIndDispatch[%u][%u]", conn->handle, n->value_handle); + + BTA_GATTS_HandleValueIndication(conn_id, n->value_handle, + n->params_copy.len, + n->data_copy, true); +} + +/* Silent drain — used by reset_gatt_conn as a safety net. The disconnect + * handler is expected to have already fired func for each pending node + * (mirrors NimBLE stack's ble_gatts_indicate_fail_notconn on disconn). */ +static void gatts_list_drain(struct gatt_conn *gatt_conn) +{ + 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); + + LOG_DBG("[B]GattsIndDrain[%u]", n->value_handle); + gatts_list_node_free(n); + } +} + +uint8_t bt_le_bluedroid_gattc_get_if(void) +{ + /* BTA assigns a positive client_if on successful registration; 0 means + * the engine hasn't completed BTA_GATTC_AppRegister yet. Returning + * 0xFF (ESP_GATT_IF_NONE) signals the caller to bail rather than feed + * a junk handle into esp_ble_gattc_aux_open() / etc. */ + if (gattc_if == 0) { + LOG_WRN("[B]GetGattcIfBeforeInit"); + return 0xFF; /* ESP_GATT_IF_NONE */ + } + + return gattc_if; +} + +uint8_t bt_le_bluedroid_gatts_get_if(void) +{ + return gatts_if; +} + +void bt_le_bluedroid_gatts_sem_reset(void) +{ + /* Must run BEFORE the BTA op that triggers the producer. Drains any + * stale give from a prior timed-out op and clears .result so the + * next take reads exactly what this op's producer writes. */ + k_sem_reset(&gatts_sem); +} + +int bt_le_bluedroid_gatts_sem_take(void) +{ + int rc; + + rc = k_sem_take(&gatts_sem, K_SEM_SHORT); + if (rc) { + return rc; + } + + return gatts_sem.result; +} + +void bt_le_bluedroid_gatts_sem_give(int result) +{ + gatts_sem.result = result; + k_sem_give(&gatts_sem); +} + +struct gatt_conn *bt_le_bluedroid_find_gatt_conn_with_addr(uint8_t addr_type, + const uint8_t addr[6], + bool ignore_type) +{ + for (size_t i = 0; i < ARRAY_SIZE(gatt_conns); i++) { + if (gatt_conns[i].used && + (ignore_type || gatt_conns[i].peer.type == addr_type) && + memcmp(gatt_conns[i].peer.val, addr, BT_ADDR_SIZE) == 0) { + return &gatt_conns[i]; + } + } + + return NULL; +} + +struct gatt_conn *bt_le_bluedroid_find_gatt_conn_with_handle(uint16_t conn_handle) +{ + for (size_t i = 0; i < ARRAY_SIZE(gatt_conns); i++) { + if (gatt_conns[i].used && + gatt_conns[i].conn_handle == conn_handle) { + return &gatt_conns[i]; + } + } + + return NULL; +} + +struct gatt_conn *bt_le_bluedroid_find_free_gatt_conn(void) +{ + for (size_t i = 0; i < ARRAY_SIZE(gatt_conns); i++) { + if (gatt_conns[i].used == 0) { + memset(&gatt_conns[i], 0, sizeof(gatt_conns[i])); + return &gatt_conns[i]; + } + } + + return NULL; +} + +static void reset_gatt_conn(struct gatt_conn *gatt_conn) +{ + gattc_list_drain(gatt_conn); + gatts_list_drain(gatt_conn); + gatts_notify_list_drain(gatt_conn); + memset(gatt_conn, 0, sizeof(*gatt_conn)); + gatt_conn->conn_handle = UINT16_MAX; +} + +/* Bluedroid surfaces a 16-bit disconnect reason mixing 8-bit HCI codes with + * host-side extensions (gatt_api.h: GATT_CONN_CANCEL/_NONE). Downstream cb + * mirrors Zephyr's uint8_t signature — map the extensions to the closest HCI + * Vol 1 Part F equivalent rather than letting them silently truncate to 0x00 + * (which would look like a graceful disconnect). */ +static uint8_t bta_disconn_reason_to_hci(uint16_t reason) +{ + if (reason <= 0xFF) { + return (uint8_t)reason; + } + + switch (reason) { + case 0x0100: /* GATT_CONN_CANCEL — local-initiated cancel. + * Macro aliases L2CAP_CONN_CANCEL whose def + * lives in l2cdefs.h (not transitively + * included), so use the numeric literal. */ + return HCI_ERR_CONN_CAUSE_LOCAL_HOST; /* 0x16 */ + case BTA_GATT_CONN_NONE: /* 0x0101 — no link to cancel */ + return HCI_ERR_NO_CONNECTION; /* 0x02 */ + default: + LOG_WRN("[B]UnknownDisconnReason[0x%04x]", reason); + return HCI_ERR_UNSPECIFIED; /* 0x1F */ + } +} + +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->type = BT_LE_GATTC_CONNECT_EVENT; + + qev->gattc_connect.conn_handle = BTC_GATT_GET_CONN_ID(connect->conn_id); + qev->gattc_connect.role = connect->link_role; + qev->gattc_connect.peer.type = connect->ble_addr_type; + memcpy(qev->gattc_connect.peer.val, connect->remote_bda, BT_ADDR_SIZE); + + err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); + if (err) { + LOG_ERR("[B]GattcConnPostFail[%d]", err); + free(qev); + } +} + +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->type = BT_LE_GATTC_DISCONNECT_EVENT; + + qev->gattc_disconnect.conn_handle = BTC_GATT_GET_CONN_ID(disconnect->conn_id); + qev->gattc_disconnect.reason = bta_disconn_reason_to_hci(disconnect->reason); + + err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); + if (err) { + LOG_ERR("[B]GattcDiscPostFail[%d]", err); + free(qev); + } +} + +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->type = BT_LE_GATTC_OPEN_EVENT; + + qev->gattc_open.status = open->status; + qev->gattc_open.conn_handle = BTC_GATT_GET_CONN_ID(open->conn_id); + + err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); + if (err) { + LOG_ERR("[B]GattcOpenPostFail[%d]", err); + free(qev); + } +} + +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->type = BT_LE_GATTC_MTU_EVENT; + + qev->gattc_mtu.status = cfg_mtu->status; + qev->gattc_mtu.conn_handle = BTC_GATT_GET_CONN_ID(cfg_mtu->conn_id); + qev->gattc_mtu.mtu = cfg_mtu->mtu; + + err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); + if (err) { + LOG_ERR("[B]GattcMtuPostFail[%d]", err); + free(qev); + } +} + +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->type = BT_LE_GATTC_DISC_CMPL_EVENT; + + qev->gattc_disc_cmpl.status = disc_cmpl->status; + qev->gattc_disc_cmpl.conn_handle = BTC_GATT_GET_CONN_ID(disc_cmpl->conn_id); + + err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); + if (err) { + LOG_ERR("[B]GattcDiscCmplPostFail[%d]", err); + free(qev); + } +} + +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->type = BT_LE_GATTC_READ_CHRC_EVENT; + + qev->gattc_read_chrc.status = read->status; + qev->gattc_read_chrc.conn_handle = BTC_GATT_GET_CONN_ID(read->conn_id); + qev->gattc_read_chrc.attr_handle = read->handle; + + if (read->p_value && + read->p_value->len && + 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); + + 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); + } +} + +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->type = BT_LE_GATTC_WRITE_CHRC_EVENT; + + qev->gattc_write_chrc.status = write->status; + qev->gattc_write_chrc.conn_handle = BTC_GATT_GET_CONN_ID(write->conn_id); + qev->gattc_write_chrc.attr_handle = write->handle; + qev->gattc_write_chrc.offset = write->offset; + + err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); + if (err) { + LOG_ERR("[B]GattcWriteChrcPostFail[%d]", err); + free(qev); + } +} + +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->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. */ + qev->gatts_notify_tx.is_notify = false; + qev->gatts_notify_tx.conn_handle = BTC_GATT_GET_CONN_ID(req->conn_id); + qev->gatts_notify_tx.attr_handle = req->handle; + qev->gatts_notify_tx.status = req->status; + + err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); + if (err) { + LOG_ERR("[B]GattsNotifyTxPostFail[%d]", err); + free(qev); + } +} + +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->type = BT_LE_GATTC_NOTIFY_RX_EVENT; + + qev->gattc_notify_rx.is_notify = notify->is_notify; + qev->gattc_notify_rx.conn_handle = BTC_GATT_GET_CONN_ID(notify->conn_id); + qev->gattc_notify_rx.attr_handle = notify->handle; + + 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); + + 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); + if (qev->gattc_notify_rx.value) { + free(qev->gattc_notify_rx.value); + } + free(qev); + } +} + +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->type = BT_LE_GATTS_CONNECT_EVENT; + + qev->gatts_connect.conn_handle = BTC_GATT_GET_CONN_ID(connect->conn_id); + qev->gatts_connect.role = connect->link_role; + qev->gatts_connect.peer.type = connect->ble_addr_type; + memcpy(qev->gatts_connect.peer.val, connect->remote_bda, BT_ADDR_SIZE); + + err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); + if (err) { + LOG_ERR("[B]GattsConnPostFail[%d]", err); + free(qev); + } +} + +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->type = BT_LE_GATTS_DISCONNECT_EVENT; + + qev->gatts_disconnect.conn_handle = BTC_GATT_GET_CONN_ID(disconnect->conn_id); + qev->gatts_disconnect.reason = bta_disconn_reason_to_hci(disconnect->reason); + + err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); + if (err) { + LOG_ERR("[B]GattsDiscPostFail[%d]", err); + free(qev); + } +} + +static void gatts_mtu_event_handler(tBTA_GATTS_REQ *req) +{ + struct bt_le_gatt_event_param *qev; + int err; + + /* req->p_data is non-NULL here: BTA always passes a stack object. */ + qev = calloc(1, sizeof(*qev)); + assert(qev); + + qev->type = BT_LE_GATTS_MTU_EVENT; + + qev->gatts_mtu.conn_handle = BTC_GATT_GET_CONN_ID(req->conn_id); + qev->gatts_mtu.mtu = req->p_data->mtu; + + err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); + if (err) { + LOG_ERR("[B]GattsMtuPostFail[%d]", err); + free(qev); + } +} + +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->type = BT_LE_GATTS_READ_EVENT; + + qev->gatts_read.conn_handle = BTC_GATT_GET_CONN_ID(req->conn_id); + qev->gatts_read.trans_id = req->trans_id; + memcpy(qev->gatts_read.peer, req->remote_bda, BT_ADDR_SIZE); + qev->gatts_read.attr_handle = req->p_data->read_req.handle; + qev->gatts_read.offset = req->p_data->read_req.offset; + qev->gatts_read.is_long = req->p_data->read_req.is_long; + qev->gatts_read.need_rsp = req->p_data->read_req.need_rsp; + + err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); + if (err) { + LOG_ERR("[B]GattsReadPostFail[%d]", err); + free(qev); + } +} + +static void gatts_write_req_handler(tBTA_GATTS_REQ *req) +{ + struct bt_le_gatt_event_param *qev; + int err; + + /* req->p_data is non-NULL here: BTA always passes a stack object. */ + qev = calloc(1, sizeof(*qev)); + assert(qev); + + qev->type = BT_LE_GATTS_WRITE_EVENT; + + qev->gatts_write.conn_handle = BTC_GATT_GET_CONN_ID(req->conn_id); + qev->gatts_write.trans_id = req->trans_id; + memcpy(qev->gatts_write.peer, req->remote_bda, BT_ADDR_SIZE); + qev->gatts_write.attr_handle = req->p_data->write_req.handle; + qev->gatts_write.offset = req->p_data->write_req.offset; + qev->gatts_write.is_prep = req->p_data->write_req.is_prep; + qev->gatts_write.need_rsp = req->p_data->write_req.need_rsp; + + 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); + + 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); + } +} + +static void gatts_app_cb(tBTA_GATTS_EVT event, tBTA_GATTS *p_data) +{ + switch (event) { + case BTA_GATTS_REG_EVT: + LOG_DBG("[B]GattsRegEvt[%u][%u]", + p_data->reg_oper.status, p_data->reg_oper.server_if); + LOG_DBG("[B]GattsRegUuid[%u][%02x][%02x]", + p_data->reg_oper.uuid.len, + 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; + } + + gatts_sem.result = p_data->reg_oper.status; + k_sem_give(&gatts_sem); + break; + + case BTA_GATTS_DEREG_EVT: + LOG_DBG("[B]GattsDeregEvt[%u]", p_data->reg_oper.server_if); + break; + + case BTA_GATTS_CONNECT_EVT: + LOG_INF("[B]GattsConnEvt[%u][0x%04x][0x%03x]", + p_data->conn.server_if, p_data->conn.conn_id, p_data->conn.conn_handle); + LOG_DBG("[B]GattsConnBda[%u][%02x:%02x:%02x:%02x:%02x:%02x]", + p_data->conn.ble_addr_type, + p_data->conn.remote_bda[0], p_data->conn.remote_bda[1], + p_data->conn.remote_bda[2], p_data->conn.remote_bda[3], + p_data->conn.remote_bda[4], p_data->conn.remote_bda[5]); + LOG_DBG("[B]GattsConnParams[%u][%u][%u][%u][%u]", + p_data->conn.transport, p_data->conn.link_role, + p_data->conn.conn_params.interval, p_data->conn.conn_params.latency, + p_data->conn.conn_params.timeout); + + if (p_data->conn.server_if != gatts_if) { + LOG_ERR("[B]GattsConnUnknownIf[%u]", p_data->conn.server_if); + break; + } + + if (BTC_GATT_GET_CONN_ID(p_data->conn.conn_id) != p_data->conn.conn_handle) { + LOG_ERR("[B]GattsConnHdlMismatch[%u][%u]", + BTC_GATT_GET_CONN_ID(p_data->conn.conn_id), p_data->conn.conn_handle); + break; + } + + gatts_connect_event_handler(&p_data->conn); + break; + + case BTA_GATTS_DISCONNECT_EVT: + LOG_INF("[B]GattsDisconnEvt[%u][0x%04x][%u]", + p_data->conn.server_if, p_data->conn.conn_id, p_data->conn.reason); + LOG_DBG("[B]GattsDisconnBda[%02x:%02x:%02x:%02x:%02x:%02x]", + p_data->conn.remote_bda[0], p_data->conn.remote_bda[1], + p_data->conn.remote_bda[2], p_data->conn.remote_bda[3], + 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); + break; + } + + gatts_disconnect_event_handler(&p_data->conn); + break; + + case BTA_GATTS_OPEN_EVT: + LOG_INF("[B]GattsOpenEvt[%u][%u]", + p_data->open.status, p_data->open.server_if); + break; + + case BTA_GATTS_CANCEL_OPEN_EVT: + LOG_DBG("[B]GattsCancelOpenEvt[%u][%u]", + p_data->cancel_open.status, p_data->cancel_open.server_if); + break; + + case BTA_GATTS_CLOSE_EVT: + LOG_INF("[B]GattsCloseEvt[%u][0x%04x]", + p_data->close.status, p_data->close.conn_id); + break; + + case BTA_GATTS_MTU_EVT: + LOG_INF("[B]GattsMtuEvt[0x%04x][%u][%u]", + p_data->req_data.conn_id, p_data->req_data.trans_id, + 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)); + break; + } + + gatts_mtu_event_handler(&p_data->req_data); + break; + + case BTA_GATTS_DELELTE_EVT: + LOG_DBG("[B]GattsDeleteEvt[%u][%u][%u]", + p_data->srvc_oper.status, p_data->srvc_oper.server_if, + p_data->srvc_oper.service_id); + break; + + case BTA_GATTS_START_EVT: + LOG_DBG("[B]GattsStartEvt[%u][%u][%u]", + p_data->srvc_oper.status, p_data->srvc_oper.server_if, + p_data->srvc_oper.service_id); + + if (gatts_svc_cb && gatts_svc_cb->svc_start_cb) { + gatts_svc_cb->svc_start_cb(p_data->srvc_oper.service_id, p_data->srvc_oper.status); + } + break; + + case BTA_GATTS_STOP_EVT: + LOG_DBG("[B]GattsStopEvt[%u][%u][%u]", + p_data->srvc_oper.status, p_data->srvc_oper.server_if, + p_data->srvc_oper.service_id); + break; + + case BTA_GATTS_CREATE_EVT: + LOG_DBG("[B]GattsCreateEvt[%u][%u]", + p_data->create.status, p_data->create.server_if); + LOG_DBG("[B]GattsCreateSvc[%u][%u][%u]", + p_data->create.service_id, p_data->create.svc_instance, + p_data->create.is_primary); + LOG_DBG("[B]GattsCreateUuid[%u][%02x][%02x]", + p_data->create.uuid.len, + p_data->create.uuid.uu.uuid128[0], + 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); + break; + } + + if (gatts_svc_cb && gatts_svc_cb->svc_create_cb) { + gatts_svc_cb->svc_create_cb(p_data->create.service_id, p_data->create.svc_instance, + p_data->create.is_primary, p_data->create.status, + &p_data->create.uuid); + } + break; + + case BTA_GATTS_ADD_INCL_SRVC_EVT: + LOG_DBG("[B]GattsAddInclSvcEvt[%u][%u][%u][%u]", + p_data->add_result.status, p_data->add_result.server_if, + 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); + break; + } + + if (gatts_svc_cb && gatts_svc_cb->inc_svc_add_cb) { + gatts_svc_cb->inc_svc_add_cb(p_data->add_result.service_id, p_data->add_result.attr_id, + p_data->add_result.status); + } + break; + + case BTA_GATTS_ADD_CHAR_EVT: + LOG_DBG("[B]GattsAddCharEvt[%u][%u][%u][%u]", + p_data->add_result.status, p_data->add_result.server_if, + p_data->add_result.attr_id, p_data->add_result.service_id); + LOG_DBG("[B]GattsAddCharUuid[%u][0x%04x]", + 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); + break; + } + + if (gatts_svc_cb && gatts_svc_cb->chrc_add_cb) { + gatts_svc_cb->chrc_add_cb(p_data->add_result.service_id, p_data->add_result.attr_id, + p_data->add_result.status, &p_data->add_result.char_uuid); + } + break; + + case BTA_GATTS_ADD_CHAR_DESCR_EVT: + LOG_DBG("[B]GattsAddDescrEvt[%u][%u][%u][%u]", + p_data->add_result.status, p_data->add_result.server_if, + p_data->add_result.attr_id, p_data->add_result.service_id); + LOG_DBG("[B]GattsAddDescrUuid[%u][0x%04x]", + 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); + break; + } + + if (gatts_svc_cb && gatts_svc_cb->chrc_add_cb) { + gatts_svc_cb->chrc_add_cb(p_data->add_result.service_id, p_data->add_result.attr_id, + p_data->add_result.status, &p_data->add_result.char_uuid); + } + break; + + case BTA_GATTS_READ_EVT: + LOG_DBG("[B]GattsReadEvt[0x%04x][%u]", + p_data->req_data.conn_id, p_data->req_data.trans_id); + LOG_DBG("[B]GattsReadBda[%02x:%02x:%02x:%02x:%02x:%02x]", + p_data->req_data.remote_bda[0], p_data->req_data.remote_bda[1], + p_data->req_data.remote_bda[2], p_data->req_data.remote_bda[3], + p_data->req_data.remote_bda[4], p_data->req_data.remote_bda[5]); + + /* req_data.p_data is non-NULL here: BTA always passes a stack object. */ + LOG_DBG("[B]GattsReadReq[%u][%u][%u][%u]", + p_data->req_data.p_data->read_req.handle, + p_data->req_data.p_data->read_req.offset, + p_data->req_data.p_data->read_req.is_long, + 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)); + break; + } + + gatts_read_req_handler(&p_data->req_data); + break; + + case BTA_GATTS_WRITE_EVT: + LOG_DBG("[B]GattsWriteEvt[0x%04x][%u]", + p_data->req_data.conn_id, p_data->req_data.trans_id); + LOG_DBG("[B]GattsWriteBda[%02x:%02x:%02x:%02x:%02x:%02x]", + p_data->req_data.remote_bda[0], p_data->req_data.remote_bda[1], + p_data->req_data.remote_bda[2], p_data->req_data.remote_bda[3], + p_data->req_data.remote_bda[4], p_data->req_data.remote_bda[5]); + if (p_data->req_data.p_data) { + LOG_DBG("[B]GattsWriteReq[%u][%u][%u][%u]", + p_data->req_data.p_data->write_req.handle, + p_data->req_data.p_data->write_req.offset, + p_data->req_data.p_data->write_req.is_prep, + p_data->req_data.p_data->write_req.need_rsp); + } + + 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)); + break; + } + + gatts_write_req_handler(&p_data->req_data); + break; + + case BTA_GATTS_EXEC_WRITE_EVT: + LOG_DBG("[B]GattsExecWriteEvt[0x%04x][%u]", + p_data->req_data.conn_id, p_data->req_data.trans_id); + LOG_DBG("[B]GattsExecWriteBda[%02x:%02x:%02x:%02x:%02x:%02x]", + p_data->req_data.remote_bda[0], p_data->req_data.remote_bda[1], + p_data->req_data.remote_bda[2], p_data->req_data.remote_bda[3], + p_data->req_data.remote_bda[4], p_data->req_data.remote_bda[5]); + if (p_data->req_data.p_data) { + LOG_DBG("[B]GattsExecFlag[%u]", p_data->req_data.p_data->exec_write); + } + break; + + case BTA_GATTS_CONF_EVT: + LOG_DBG("[B]GattsConfEvt[0x%04x][%u][%u][%u]", + p_data->req_data.conn_id, p_data->req_data.status, + p_data->req_data.handle, p_data->req_data.data_len); + + gatts_notify_tx_event_handler(&p_data->req_data); + break; + + case BTA_GATTS_SET_ATTR_VAL_EVT: + LOG_DBG("[B]GattsSetAttrValEvt[%u][%u][%u][%u]", + p_data->attr_val.status, p_data->attr_val.server_if, + p_data->attr_val.service_id, p_data->attr_val.attr_id); + break; + + case BTA_GATTS_CONGEST_EVT: + LOG_DBG("[B]GattsCongestEvt[0x%04x][%u]", + p_data->congest.conn_id, p_data->congest.congested); + break; + + case BTA_GATTS_SEND_SERVICE_CHANGE_EVT: + LOG_DBG("[B]GattsSvcChgEvt[%u][%u]", + p_data->service_change.status, p_data->service_change.server_if); + break; + + default: + break; + } +} + +static void gattc_app_cb(tBTA_GATTC_EVT event, tBTA_GATTC *p_data) +{ + switch (event) { + case BTA_GATTC_REG_EVT: + LOG_DBG("[B]GattcRegEvt[%u][%u]", + p_data->reg_oper.status, p_data->reg_oper.client_if); + LOG_DBG("[B]GattcRegUuid[%u][%02x][%02x]", + p_data->reg_oper.app_uuid.len, + 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; + } + + gattc_sem.result = p_data->reg_oper.status; + k_sem_give(&gattc_sem); + break; + + case BTA_GATTC_DEREG_EVT: + LOG_DBG("[B]GattcDeregEvt[%u]", p_data->reg_oper.client_if); + break; + + case BTA_GATTC_CONNECT_EVT: + LOG_INF("[B]GattcConnEvt[%u][0x%04x][0x%03x]", + p_data->connect.client_if, p_data->connect.conn_id, p_data->connect.conn_handle); + LOG_DBG("[B]GattcConnBda[%u][%02x:%02x:%02x:%02x:%02x:%02x]", + p_data->connect.ble_addr_type, + p_data->connect.remote_bda[0], p_data->connect.remote_bda[1], + p_data->connect.remote_bda[2], p_data->connect.remote_bda[3], + p_data->connect.remote_bda[4], p_data->connect.remote_bda[5]); + LOG_DBG("[B]GattcConnParams[%u][%u][%u][%u]", + p_data->connect.link_role, p_data->connect.conn_params.interval, + 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); + break; + } + + if (BTC_GATT_GET_CONN_ID(p_data->connect.conn_id) != p_data->connect.conn_handle) { + LOG_ERR("[B]GattcConnHdlMismatch[%u][%u]", + BTC_GATT_GET_CONN_ID(p_data->connect.conn_id), p_data->connect.conn_handle); + break; + } + + gattc_connect_event_handler(&p_data->connect); + break; + + case BTA_GATTC_DISCONNECT_EVT: + LOG_INF("[B]GattcDisconnEvt[%u][0x%04x][%u]", + p_data->disconnect.client_if, p_data->disconnect.conn_id, p_data->disconnect.reason); + LOG_DBG("[B]GattcDisconnBda[%02x:%02x:%02x:%02x:%02x:%02x]", + p_data->disconnect.remote_bda[0], p_data->disconnect.remote_bda[1], + p_data->disconnect.remote_bda[2], p_data->disconnect.remote_bda[3], + 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); + break; + } + + gattc_disconnect_event_handler(&p_data->disconnect); + break; + + case BTA_GATTC_OPEN_EVT: + LOG_INF("[B]GattcOpenEvt[%u][%u][0x%04x][%u]", + p_data->open.status, p_data->open.client_if, + p_data->open.conn_id, p_data->open.mtu); + LOG_DBG("[B]GattcOpenBda[%02x:%02x:%02x:%02x:%02x:%02x]", + p_data->open.remote_bda[0], p_data->open.remote_bda[1], + p_data->open.remote_bda[2], p_data->open.remote_bda[3], + 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); + break; + } + + gattc_open_event_handler(&p_data->open); + break; + + case BTA_GATTC_CLOSE_EVT: + LOG_INF("[B]GattcCloseEvt[%u][%u][0x%04x][%u]", + p_data->close.status, p_data->close.client_if, + p_data->close.conn_id, p_data->close.reason); + LOG_DBG("[B]GattcCloseBda[%02x:%02x:%02x:%02x:%02x:%02x]", + p_data->close.remote_bda[0], p_data->close.remote_bda[1], + p_data->close.remote_bda[2], p_data->close.remote_bda[3], + p_data->close.remote_bda[4], p_data->close.remote_bda[5]); + + bta_gattc_clcb_dealloc_by_conn_id(p_data->close.conn_id); + break; + + case BTA_GATTC_CFG_MTU_EVT: + LOG_INF("[B]GattcCfgMtuEvt[%u][0x%04x][%u]", + 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)); + break; + } + + if (p_data->cfg_mtu.status != BTA_GATT_OK) { + LOG_WRN("[B]GattcMtuExchFail[%u]", p_data->cfg_mtu.status); + /* Spec: when MTU exchange fails the link keeps the default + * MTU. Override here so handle_gattc_mtu_event records 23 in + * gatt_conn->mtu instead of leaving it at 0, which would + * make get_mtu() fall back to the local-configured max. */ + p_data->cfg_mtu.mtu = GATT_DEF_BLE_MTU_SIZE; + } + + gattc_mtu_event_handler(&p_data->cfg_mtu); + break; + + case BTA_GATTC_DIS_SRVC_CMPL_EVT: + LOG_INF("[B]GattcDiscSvcCmplEvt[%u][0x%04x]", + 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)); + break; + } + + gattc_disc_cmpl_event_handler(&p_data->dis_cmpl); + break; + + case BTA_GATTC_SEARCH_RES_EVT: + LOG_INF("[B]GattcSearchResEvt[0x%04x]", p_data->srvc_res.conn_id); + LOG_DBG("[B]GattcSvcResHdls[%u][%u][%u]", + p_data->srvc_res.start_handle, p_data->srvc_res.end_handle, + p_data->srvc_res.is_primary); + LOG_DBG("[B]GattcSvcResUuid[%u][0x%04x][%u]", + p_data->srvc_res.service_uuid.uuid.len, + p_data->srvc_res.service_uuid.uuid.uu.uuid16, + 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)); + break; + } + break; + + case BTA_GATTC_SEARCH_CMPL_EVT: + LOG_INF("[B]GattcSearchCmplEvt[%u][0x%04x][%s]", + p_data->search_cmpl.status, p_data->search_cmpl.conn_id, + (p_data->search_cmpl.searched_service_source == + 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)); + break; + } + break; + + case BTA_GATTC_READ_CHAR_EVT: + LOG_INF("[B]GattcReadCharEvt[%u][0x%04x][%u]", + 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)); + break; + } + + gattc_read_chrc_event_handler(&p_data->read); + break; + + case BTA_GATTC_READ_DESCR_EVT: + LOG_DBG("[B]GattcReadDescrEvt[%u][0x%04x][%u]", + p_data->read.status, p_data->read.conn_id, p_data->read.handle); + break; + + case BTA_GATTC_READ_MULTIPLE_EVT: + LOG_DBG("[B]GattcReadMultiEvt[%u][0x%04x][%u]", + p_data->read.status, p_data->read.conn_id, p_data->read.handle); + break; + + case BTA_GATTC_READ_MULTI_VAR_EVT: + LOG_DBG("[B]GattcReadMultiVarEvt[%u][0x%04x][%u]", + p_data->read.status, p_data->read.conn_id, p_data->read.handle); + break; + + case BTA_GATTC_WRITE_CHAR_EVT: + LOG_INF("[B]GattcWriteCharEvt[%u][0x%04x][%u][%u]", + p_data->write.status, p_data->write.conn_id, + 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)); + break; + } + + gattc_write_chrc_event_handler(&p_data->write); + break; + + case BTA_GATTC_WRITE_DESCR_EVT: + LOG_DBG("[B]GattcWriteDescrEvt[%u][0x%04x][%u]", + p_data->write.status, p_data->write.conn_id, p_data->write.handle); + break; + + case BTA_GATTC_PREP_WRITE_EVT: + LOG_DBG("[B]GattcPrepWriteEvt[%u][0x%04x][%u][%u]", + p_data->write.status, p_data->write.conn_id, + p_data->write.handle, p_data->write.offset); + break; + + case BTA_GATTC_EXEC_EVT: + LOG_DBG("[B]GattcExecEvt[%u][0x%04x]", + p_data->exec_cmpl.status, p_data->exec_cmpl.conn_id); + break; + + case BTA_GATTC_NOTIF_EVT: + LOG_INF("[B]GattcNotifEvt[0x%04x][%u][%u]", + p_data->notify.conn_id, p_data->notify.handle, p_data->notify.is_notify); + LOG_DBG("[B]GattcNotifBda[%02x:%02x:%02x:%02x:%02x:%02x]", + p_data->notify.bda[0], p_data->notify.bda[1], + p_data->notify.bda[2], p_data->notify.bda[3], + 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)); + break; + } + + gattc_notify_rx_event_handler(&p_data->notify); + break; + + case BTA_GATTC_SRVC_CHG_EVT: + LOG_INF("[B]GattcSvcChgEvt[0x%04x]", p_data->srvc_chg.conn_id); + LOG_DBG("[B]GattcSvcChgBda[%02x:%02x:%02x:%02x:%02x:%02x]", + p_data->srvc_chg.remote_bda[0], p_data->srvc_chg.remote_bda[1], + p_data->srvc_chg.remote_bda[2], p_data->srvc_chg.remote_bda[3], + p_data->srvc_chg.remote_bda[4], p_data->srvc_chg.remote_bda[5]); + break; + + case BTA_GATTC_CONGEST_EVT: + LOG_DBG("[B]GattcCongestEvt[0x%04x][%u]", + p_data->congest.conn_id, p_data->congest.congested); + break; + + case BTA_GATTC_QUEUE_FULL_EVT: + /* BTA p_cmd_list hit GATTC_COMMAND_QUEUE_SIZE_MAX (30); the rejected op + * gets no cmpl_evt, so its gattc_list node would orphan until disconnect. + * lib profiles are sequential and should never reach this; flag loudly + * if it ever fires. */ + LOG_ERR("[B]GattcQueueFullEvt[%u][0x%04x][%u]", + p_data->queue_full.status, p_data->queue_full.conn_id, + p_data->queue_full.is_full); + break; + + case BTA_GATTC_ASSOC_EVT: + LOG_DBG("[B]GattcAssocEvt[%u][%u]", + p_data->set_assoc.status, p_data->set_assoc.client_if); + break; + + case BTA_GATTC_GET_ADDR_LIST_EVT: + LOG_DBG("[B]GattcGetAddrListEvt[%u][%u][%u]", + p_data->get_addr_list.status, p_data->get_addr_list.client_if, + p_data->get_addr_list.num_addr); + break; + + default: + break; + } +} + +/* Bluedroid has no GAP-level ACL event; we synthesize one from BTA's GATTS/ + * GATTC connect/disconnect callbacks and post it through the iso GAP queue + * — same path bt_le_nimble_gap_post_event() uses for BLE_GAP_EVENT_CONNECT/ + * DISCONNECT. The iso task then runs handle_acl_connect_event_safe() / the + * disconnect equivalent inside common/app/gap.c. */ +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->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. */ + 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; + memcpy(qev->acl_connect.dst.val, gatt_conn->peer.val, BT_ADDR_SIZE); + + err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GAP_EVENT, qev, sizeof(*qev)); + if (err) { + LOG_ERR("[B]AclConnPostFail[%d]", err); + free(qev); + } +} + +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->type = BT_LE_GAP_APP_PARAM_ACL_DISCONNECT; + + qev->acl_disconnect.conn_handle = conn_handle; + qev->acl_disconnect.reason = reason; + + err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GAP_EVENT, qev, sizeof(*qev)); + if (err) { + LOG_ERR("[B]AclDiscPostFail[%d]", err); + free(qev); + } +} + +static void handle_gattc_connect_event(struct bt_le_gattc_connect_event *event) +{ + struct gatt_conn *gatt_conn; + + /* GATTS_CONNECT (the SLAVE path) owns peripheral-side gatt_conn; if BTA + * ever dispatched a SLAVE-role event here we'd double-allocate. */ + if (event->role != BTM_ROLE_MASTER) { + return; + } + + 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; + } + + /* App initiated via esp_ble_gattc_aux_open(engine_gattc_if, ...). BTA + * dispatches CONNECT here first, OPEN follows. We allocate the slot now + * so the OPEN handler finds it and posts ACL_CONNECT. */ + gatt_conn = bt_le_bluedroid_find_free_gatt_conn(); + if (gatt_conn == NULL) { + LOG_ERR("[B]NoFreeConnInfo"); + return; + } + + gatt_conn->used = 1; + gatt_conn->conn_create = 0; + gatt_conn->status = 0x00; + gatt_conn->gatt_if = gattc_if; + gatt_conn->conn_handle = event->conn_handle; + gatt_conn->role = event->role; + gatt_conn->peer.type = event->peer.type; + memcpy(gatt_conn->peer.val, event->peer.val, BT_ADDR_SIZE); +} + +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 = bt_le_bluedroid_find_gatt_conn_with_handle(event->conn_handle); + if (gatt_conn == NULL) { + LOG_DBG("[B]GattcDisconnUnknownDev"); + return; + } + + 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); + } + + post_acl_disconnect_app_event(gatt_conn->conn_handle, event->reason); + + /* Reset the corresponding gatt_conn */ + reset_gatt_conn(gatt_conn); + } +} + +static void handle_gattc_open_event(struct bt_le_gattc_open_event *event) +{ + struct gatt_conn *gatt_conn; + uint16_t conn_id; + + gatt_conn = bt_le_bluedroid_find_gatt_conn_with_handle(event->conn_handle); + if (gatt_conn == NULL) { + LOG_ERR("[B]GattcOpenUnknownDev"); + return; + } + + gatt_conn->status = event->status; + + if (gatt_conn->role == BTM_ROLE_MASTER) { + 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; + } + } + + /* At this moment, the peer device may has not initiated MTU exchange */ + if (gatt_conn->mtu == 0) { + conn_id = BTC_GATT_CREATE_CONN_ID(gattc_if, gatt_conn->conn_handle); + + BTA_GATTC_ConfigureMTU(conn_id); + + /* Mark MTU exchange in progress. The MTU change event will be + * posted later when BTA_GATTC_CFG_MTU_EVT arrives (the open event + * below still goes out unconditionally). */ + 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; + + gatt_conn = bt_le_bluedroid_find_gatt_conn_with_handle(event->conn_handle); + if (gatt_conn == NULL) { + LOG_ERR("[B]GattcMtuUnknownConn[%u]", event->conn_handle); + return; + } + + /* The device may works as GATT client or server or both, which is not + * 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. + */ + if (gatt_conn->mtu != 0) { + LOG_INF("[B]GattcMtuExchanged[%u][%u][%u]", + gatt_conn->conn_handle, gatt_conn->mtu, event->mtu); + + gatt_conn->mtu = MIN(event->mtu, gatt_conn->mtu); + } else { + gatt_conn->mtu = event->mtu; + } + + 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). */ + bt_le_gatt_app_mtu_change_event(event->conn_handle, gatt_conn->mtu); + + gatt_conn->mtu_posted = 1; /* Mark MTU event as posted */ + } +} + +static void handle_gattc_disc_cmpl_event(struct bt_le_gattc_disc_cmpl_event *event) +{ + struct gatt_conn *gatt_conn; + + gatt_conn = bt_le_bluedroid_find_gatt_conn_with_handle(event->conn_handle); + if (gatt_conn == NULL) { + LOG_ERR("[B]GattcDiscSvcUnknownConn[%u]", event->conn_handle); + return; + } + + bt_le_gattc_app_disc_cmpl_event(event); +} + +static void handle_gattc_read_chrc_event(struct bt_le_gattc_read_chrc_event *event) +{ + struct bt_gatt_read_params *params; + struct gatt_conn *gatt_conn; + struct gattc_list_node *op; + struct bt_conn *conn; + const uint8_t *val; + sys_snode_t *snode; + uint16_t vlen; + uint16_t off; + uint8_t ret; + + conn = bt_le_acl_conn_find(event->conn_handle); + if (conn == NULL || conn->state != BT_CONN_CONNECTED) { + LOG_WRN("[B]GattcRdCharNotConn"); + goto end; + } + + gatt_conn = bt_le_bluedroid_find_gatt_conn_with_handle(event->conn_handle); + if (gatt_conn == NULL) { + LOG_WRN("[B]GattcRdCharUnknownConn[%u]", event->conn_handle); + goto end; + } + + snode = sys_slist_get(&gatt_conn->gattc_list); + if (snode == NULL) { + LOG_ERR("[B]GattcRdCharNoOp[%u]", event->conn_handle); + goto end; + } + + op = CONTAINER_OF(snode, struct gattc_list_node, node); + if (op->type != GATTC_OP_READ) { + LOG_ERR("[B]GattcOpHeadMismatch[%u]exp_rd_got[%u]", + event->conn_handle, op->type); + /* BTA delivers cmpl_evt in p_cmd_list FIFO order; type mismatch + * means this EVT is spurious — put the head back (still a legitimate + * in-flight op) and drop the spurious EVT. */ + sys_slist_prepend(&gatt_conn->gattc_list, &op->node); + goto end; + } + + params = op->read_params; + free(op); + + if (params == NULL || params->func == NULL) { + LOG_ERR("[B]GattcRdCharNoFunc"); + goto end; + } + + if ((params->handle_count == 0 && (event->attr_handle < params->by_uuid.start_handle || + event->attr_handle > params->by_uuid.end_handle)) || + (params->handle_count == 1 && event->attr_handle != params->single.handle)) { + LOG_ERR("[B]GattcRdCharInvRsp[%u][%u][%u][%u][%u]", + params->handle_count, event->attr_handle, params->by_uuid.start_handle, + params->by_uuid.end_handle, params->single.handle); + /* BTA fires READ_CHAR_EVT exactly once per read; the op was just + * popped above so no further EVT will arrive. Fire func with err + * so caller's state machine doesn't stall waiting forever. */ + params->func(conn, BT_ATT_ERR_UNLIKELY, params, NULL, 0); + goto end; + } + + val = event->value; + vlen = event->len; + + /* Long read: BTA's auto Read-Blob already fetched the full value on the + * BTU task; hand the caller only the requested [offset:] tail. */ + if (params->handle_count == 1 && params->single.offset != 0 && event->status == 0) { + off = params->single.offset; + val = (off < event->len) ? event->value + off : NULL; + vlen = (off < event->len) ? (event->len - off) : 0; + } + + ret = params->func(conn, event->status, params, val, vlen); + + /* (0, NULL, 0) is the success-completion signal per bt_gatt_read_func_t. + * Skip it on error paths — the error call above is already terminal; + * sending a follow-up "success" would let lib treat the read as OK and + * keep stale data. Mirrors NimBLE nrp.c, which fires func(err, NULL, 0) + * once on the default branch and never the success-completion. */ + if (ret == BT_GATT_ITER_CONTINUE && event->status == 0) { + params->func(conn, 0, params, NULL, 0); + } + +end: + if (event->value) { + free(event->value); + } +} + +static void handle_gattc_write_chrc_event(struct bt_le_gattc_write_chrc_event *event) +{ + struct bt_gatt_write_params *params; + struct gatt_conn *gatt_conn; + struct gattc_list_node *op; + struct bt_conn *conn; + sys_snode_t *snode; + + conn = bt_le_acl_conn_find(event->conn_handle); + if (conn == NULL || conn->state != BT_CONN_CONNECTED) { + LOG_WRN("[B]GattcWrCharNotConn"); + return; + } + + gatt_conn = bt_le_bluedroid_find_gatt_conn_with_handle(event->conn_handle); + if (gatt_conn == NULL) { + LOG_WRN("[B]GattcWrCharUnknownConn[%u]", event->conn_handle); + return; + } + + snode = sys_slist_get(&gatt_conn->gattc_list); + if (snode == NULL) { + /* write_without_rsp does not enqueue (no cmpl_evt expected); if BTA + * still fires one (e.g. internal write-cmd path uses cmpl_evt), there + * is no head to consume. */ + LOG_DBG("[B]GattcWrCharNoOp[%u]", event->conn_handle); + return; + } + + op = CONTAINER_OF(snode, struct gattc_list_node, node); + if (op->type != GATTC_OP_WRITE) { + LOG_ERR("[B]GattcOpHeadMismatch[%u]exp_wr_got[%u]", + event->conn_handle, op->type); + /* Put it back at head — this EVT doesn't belong to us, leave the + * read op for handle_gattc_read_chrc_event to consume. */ + sys_slist_prepend(&gatt_conn->gattc_list, &op->node); + return; + } + + params = op->write_params; + free(op); + + assert(params && params->func); + + if (event->attr_handle != params->handle) { + LOG_ERR("[B]GattcWrCharInvRsp[%u][%u]", event->attr_handle, params->handle); + /* Op was already popped and freed; fire func with err so caller's + * state machine doesn't stall waiting for a completion that won't + * come. Mirrors handle_gattc_read_chrc_event's mismatch path. */ + params->func(conn, BT_ATT_ERR_UNLIKELY, params); + return; + } + + params->func(conn, event->status, params); +} + +static void handle_gattc_notify_event(struct bt_le_gattc_notify_rx_event *event) +{ + struct bt_gatt_subscribe_params *params; + struct bt_gatt_subscribe_params *tmp; + struct gatt_conn *gatt_conn; + struct gattc_sub *sub; + struct bt_conn *conn; + + conn = bt_le_acl_conn_find(event->conn_handle); + if (conn == NULL || conn->state != BT_CONN_CONNECTED) { + LOG_WRN("[B]GattcNotifNotConn"); + goto end; + } + + gatt_conn = bt_le_bluedroid_find_gatt_conn_with_handle(event->conn_handle); + if (gatt_conn == NULL) { + LOG_WRN("[B]GattcNotifUnknownConn[%u]", event->conn_handle); + goto end; + } + + sub = bt_gattc_sub_find(conn); + if (sub == NULL) { + LOG_WRN("[B]GattcNotifNoSub[%u]", event->conn_handle); + goto end; + } + + /* Caller bt_le_bluedroid_gatt_handle_event already holds host_lock + * across the whole switch — sub->list iteration runs under that. + * Zephyr bt_gatt_subscribe_params API permits the notify cb to + * unsubscribe inline, which NULLs the current node's next pointer + * and breaks the unsafe walker; use _SAFE to pre-cache next. */ + SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&sub->list, params, tmp, node) { + if (params->ccc_handle != BT_GATT_AUTO_DISCOVER_CCC_HANDLE && + params->value_handle == event->attr_handle) { + if (params->notify) { + /* Return value intentionally ignored. Zephyr unsubscribes on + * BT_GATT_ITER_STOP, but lib clients return STOP on a single + * malformed/short notify (e.g. unicast_client_cp_notify); + * 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); + } + } + } + +end: + if (event->value) { + free(event->value); + } +} + +static void handle_gatts_connect_event(struct bt_le_gatts_connect_event *event) +{ + struct gatt_conn *gatt_conn; + + /* GATTS_CONNECT is broadcast to every registered GATTS app; central side + * (MASTER role) also receives it. Central-side gatt_conn creation is + * owned by handle_gattc_connect_event so we only handle SLAVE here. */ + if (event->role != BTM_ROLE_SLAVE) { + return; + } + + 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; + } + + gatt_conn = bt_le_bluedroid_find_free_gatt_conn(); + if (gatt_conn == NULL) { + LOG_ERR("[B]NoFreeConnInfo"); + return; + } + + gatt_conn->used = 1; + gatt_conn->conn_create = 0; + gatt_conn->status = 0x00; + gatt_conn->gatt_if = gatts_if; + gatt_conn->conn_handle = event->conn_handle; + gatt_conn->role = event->role; + gatt_conn->peer.type = event->peer.type; + memcpy(gatt_conn->peer.val, event->peer.val, BT_ADDR_SIZE); + + post_acl_connect_app_event(gatt_conn); +} + +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 = bt_le_bluedroid_find_gatt_conn_with_handle(event->conn_handle); + if (gatt_conn == NULL) { + LOG_WRN("[B]GattsDisconnUnknownDev"); + return; + } + + /* Mirror handle_gatts_connect_event: SLAVE-side teardown happens here; + * MASTER teardown is in handle_gattc_disconnect_event. */ + if (gatt_conn->role != BTM_ROLE_SLAVE) { + 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); + } + + 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; + + gatt_conn = bt_le_bluedroid_find_gatt_conn_with_handle(event->conn_handle); + if (gatt_conn == NULL) { + LOG_ERR("[B]GattsMtuUnknownConn[%u]", event->conn_handle); + return; + } + + /* The device may works as GATT client or server or both, which is not + * 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. + */ + if (gatt_conn->mtu != 0) { + LOG_INF("[B]GattsMtuExchanged[%u][%u][%u]", + gatt_conn->conn_handle, gatt_conn->mtu, event->mtu); + + gatt_conn->mtu = MIN(event->mtu, gatt_conn->mtu); + } else { + gatt_conn->mtu = event->mtu; + } + + if (gatt_conn->mtu_posted == 0) { + /* 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 */ + } +} + +static void handle_gatts_read_event(struct bt_le_gatts_read_event *event) +{ + struct bt_gatt_attr *attr; + tBTA_GATT_STATUS status; + struct bt_conn *conn; + tBTA_GATTS_RSP *rsp; + uint16_t conn_id; + ssize_t ret; + + status = BTA_GATT_OK; + rsp = NULL; + + conn = bt_le_acl_conn_find(event->conn_handle); + if (conn == NULL || conn->state != BT_CONN_CONNECTED) { + LOG_WRN("[B]GattsRdEvtNotConn"); + return; + } + + attr = bt_gatts_find_attr_by_handle(event->attr_handle); + if (attr == NULL) { + LOG_ERR("[B]GattsRdAttrNotFound[%u]", event->attr_handle); + status = GATT_INVALID_HANDLE; + goto end; + } + + /* The tBTA_GATT_STATUS structure is too large, hence use + * dynamic memory here to avoid stack overflow. + */ + rsp = calloc(1, sizeof(*(rsp))); + assert(rsp); + + rsp->attr_value.handle = event->attr_handle; + + if (attr->read) { + if (event->offset) { + LOG_WRN("[B]GattsRdEvtNotSupp"); + + status = GATT_REQ_NOT_SUPPORTED; + } else { + ret = attr->read(conn, attr, (void *)rsp, GATT_MAX_ATTR_LEN, 0); + if (ret < 0) { + LOG_ERR("[B]GattsRdEvtFail[%u][%d]", event->attr_handle, ret); + + status = BT_GATT_ERR(ret); + } + } + } else { + status = GATT_READ_NOT_PERMIT; + } + +end: + conn_id = BTC_GATT_CREATE_CONN_ID(gatts_if, conn->handle); + + BTA_GATTS_SendRsp(conn_id, event->trans_id, status, rsp); + + if (rsp) { + free(rsp); + } +} + +static void handle_gatts_write_event(struct bt_le_gatts_write_event *event) +{ + struct bt_le_gatts_subscribe_event sub_event; + struct bt_gatt_attr *attr; + tBTA_GATT_STATUS status; + struct bt_conn *conn; + uint16_t conn_id; + ssize_t ret; + + status = BTA_GATT_OK; + + conn = bt_le_acl_conn_find(event->conn_handle); + if (conn == NULL || conn->state != BT_CONN_CONNECTED) { + LOG_WRN("[B]GattsWrEvtNotConn"); + if (event->value) { + free(event->value); + } + return; + } + + attr = bt_gatts_find_attr_by_handle(event->attr_handle); + if (attr == NULL) { + LOG_ERR("[B]GattsWrAttrNotFound[%u]", event->attr_handle); + status = GATT_INVALID_HANDLE; + 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); + status = GATT_INVALID_PDU; + goto end; + } + + uint16_t value = sys_get_le16(event->value); + + /* The CCC cfg pool is BT_GATT_CCC_MAX deep; a full pool returns + * nonzero here. Don't ACK OK in that case, else the client believes + * notify is enabled while the server tracks nothing. */ + if (bt_gatts_sub_changed(conn->handle, event->attr_handle, + value & 0x01, (value >> 1) & 0x01, 0x00) != 0) { + LOG_ERR("[B]GattsSubChangedFail[%u]", event->attr_handle); + status = GATT_INSUF_RESOURCE; + goto end; + } + + /* Mirror NimBLE adapter's subscribe path so the lib's subscribe cb + * fires on Bluedroid too. attr_handle follows NimBLE convention + * (chrc value handle = CCCD handle - 1). Bluedroid stack does not + * track previous CCCD value, so prev_* are reported as 0. + */ + sub_event.conn_handle = conn->handle; + sub_event.attr_handle = event->attr_handle - 1; + sub_event.prev_notify = 0; + sub_event.cur_notify = value & 0x01; + sub_event.prev_indicate = 0; + sub_event.cur_indicate = (value >> 1) & 0x01; + sub_event.reason = 0; + bt_le_gatts_app_subscribe_event(&sub_event); + + goto end; + } + + if (attr->write) { + if (event->offset) { + LOG_WRN("[B]GattsWrEvtNotSupp"); + + status = GATT_REQ_NOT_SUPPORTED; + } else { + ret = attr->write(conn, attr, event->value, event->len, 0, 0); + if (ret < 0) { + LOG_ERR("[B]GattsWrEvtFail[%u][%d]", event->attr_handle, ret); + + status = BT_GATT_ERR(ret); + } + } + } else { + status = GATT_WRITE_NOT_PERMIT; + } + +end: + if (event->need_rsp) { + conn_id = BTC_GATT_CREATE_CONN_ID(gatts_if, conn->handle); + + BTA_GATTS_SendRsp(conn_id, event->trans_id, status, NULL); + } + + if (event->value) { + free(event->value); + } +} + +static void handle_gatts_notify_tx_event(struct bt_le_gatts_notify_tx_event *event) +{ + struct gatts_list_node *n; + struct gatt_conn *gatt_conn; + struct bt_conn *conn; + sys_snode_t *snode; + + /* 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 + * list — disconnect drain will fire its func with BT_ATT_ERR_UNLIKELY. */ + conn = bt_le_acl_conn_find(event->conn_handle); + if (conn == NULL) { + LOG_WRN("[B]NotifyTxNoConn[%u]", event->conn_handle); + return; + } + + gatt_conn = bt_le_bluedroid_find_gatt_conn_with_handle(event->conn_handle); + if (gatt_conn == NULL) { + LOG_WRN("[B]NotifyTxUnknownConn[%u]", event->conn_handle); + 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); + if (snode != NULL) { + struct gatts_notify_node *nn = CONTAINER_OF(snode, struct gatts_notify_node, node); + + if (nn->value_handle != event->attr_handle) { + LOG_DBG("[B]NotifyTxHdlSkew[%u][%u]", event->attr_handle, nn->value_handle); + } + free(nn); + 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); + + /* Submit next pending BEFORE firing the completion cb. host_mutex is + * recursive, so a cb that re-enters bt_gatt_indicate isn't blocked; + * if dispatch_next ran AFTER the cb, gatts_indicate_enqueue would + * see an empty list (head just popped) and dispatch the new node + * inline — then this post-cb dispatch would fire it again, sending + * the same indication twice on the wire. Doing the peek-and-dispatch + * first leaves the head in place so re-entrant enqueue sees a + * non-empty list and skips its inline dispatch. Keep the node in the + * list — its own CONF_EVT will pop it later. */ + { + sys_snode_t *next_snode = sys_slist_peek_head(&gatt_conn->gatts_list); + if (next_snode != NULL) { + struct gatts_list_node *next_n = + CONTAINER_OF(next_snode, struct gatts_list_node, node); + gatts_indicate_dispatch(conn, next_n); + } + } + + 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); + } + + gatts_list_node_free(n); +} + +void bt_le_bluedroid_gatt_handle_event(uint8_t *data, size_t data_len) +{ + struct bt_le_gatt_event_param *param; + + param = (struct bt_le_gatt_event_param *)data; + + bt_le_host_lock(); + + switch (param->type) { + case BT_LE_GATTC_CONNECT_EVENT: + handle_gattc_connect_event(¶m->gattc_connect); + break; + + case BT_LE_GATTC_DISCONNECT_EVENT: + handle_gattc_disconnect_event(¶m->gattc_disconnect); + break; + + case BT_LE_GATTC_OPEN_EVENT: + handle_gattc_open_event(¶m->gattc_open); + break; + + case BT_LE_GATTC_MTU_EVENT: + handle_gattc_mtu_event(¶m->gattc_mtu); + break; + + case BT_LE_GATTC_DISC_CMPL_EVENT: + handle_gattc_disc_cmpl_event(¶m->gattc_disc_cmpl); + break; + + case BT_LE_GATTC_READ_CHRC_EVENT: + handle_gattc_read_chrc_event(¶m->gattc_read_chrc); + break; + + case BT_LE_GATTC_WRITE_CHRC_EVENT: + handle_gattc_write_chrc_event(¶m->gattc_write_chrc); + break; + + case BT_LE_GATTC_NOTIFY_RX_EVENT: + handle_gattc_notify_event(¶m->gattc_notify_rx); + break; + + case BT_LE_GATTS_CONNECT_EVENT: + handle_gatts_connect_event(¶m->gatts_connect); + break; + + case BT_LE_GATTS_DISCONNECT_EVENT: + handle_gatts_disconnect_event(¶m->gatts_disconnect); + break; + + case BT_LE_GATTS_MTU_EVENT: + handle_gatts_mtu_event(¶m->gatts_mtu); + break; + + case BT_LE_GATTS_READ_EVENT: + handle_gatts_read_event(¶m->gatts_read); + break; + + case BT_LE_GATTS_WRITE_EVENT: + handle_gatts_write_event(¶m->gatts_write); + break; + + case BT_LE_GATTS_NOTIFY_TX_EVENT: + handle_gatts_notify_tx_event(¶m->gatts_notify_tx); + break; + + default: + assert(0); + break; + } + + bt_le_host_unlock(); + + free(data); +} + +void bt_le_bluedroid_gatts_svc_cb_register(struct gatts_svc_cb *cb) +{ + bt_le_host_lock(); + gatts_svc_cb = cb; + bt_le_host_unlock(); +} + +void bt_le_bluedroid_gatt_uuid_convert(const struct bt_uuid *uuid_in, void *uuid_out) +{ + assert(uuid_out && uuid_in); + + if (uuid_in->type == BT_UUID_TYPE_16) { + ((tBT_UUID *)uuid_out)->len = LEN_UUID_16; + ((tBT_UUID *)uuid_out)->uu.uuid16 = BT_UUID_16(uuid_in)->val; + } else if (uuid_in->type == BT_UUID_TYPE_32) { + ((tBT_UUID *)uuid_out)->len = LEN_UUID_32; + ((tBT_UUID *)uuid_out)->uu.uuid32 = BT_UUID_32(uuid_in)->val; + } else if (uuid_in->type == BT_UUID_TYPE_128) { + ((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); + } +} + +uint16_t bt_le_bluedroid_gatt_perm_convert(uint16_t perm_in) +{ + tBTA_GATT_PERM perm_out = 0; + + if ((perm_in & BT_GATT_PERM_READ) == BT_GATT_PERM_READ) { + perm_out |= BTA_GATT_PERM_READ; + } + + if ((perm_in & BT_GATT_PERM_WRITE) == BT_GATT_PERM_WRITE) { + perm_out |= BTA_GATT_PERM_WRITE; + } + + if ((perm_in & BT_GATT_PERM_READ_ENCRYPT) == BT_GATT_PERM_READ_ENCRYPT) { + perm_out |= BTA_GATT_PERM_READ_ENCRYPTED; + } + + if ((perm_in & BT_GATT_PERM_WRITE_ENCRYPT) == BT_GATT_PERM_WRITE_ENCRYPT) { + perm_out |= BTA_GATT_PERM_WRITE_ENCRYPTED; + } + + if ((perm_in & BT_GATT_PERM_READ_AUTHEN) == BT_GATT_PERM_READ_AUTHEN) { + perm_out |= BTA_GATT_PERM_READ_ENC_MITM; + } + + if ((perm_in & BT_GATT_PERM_WRITE_AUTHEN) == BT_GATT_PERM_WRITE_AUTHEN) { + perm_out |= BTA_GATT_PERM_WRITE_ENC_MITM; + } + + return perm_out; +} + +uint16_t bt_le_bluedroid_gatt_get_mtu(struct bt_conn *conn) +{ + struct gatt_conn *gatt_conn; + + gatt_conn = bt_le_bluedroid_find_gatt_conn_with_handle(conn->handle); + if (gatt_conn && gatt_conn->mtu) { + return gatt_conn->mtu; + } + + /* Not yet negotiated — return local advertised MTU as best-effort. */ + return BTA_GATT_GetLocalMTU(); +} + +ssize_t bt_le_bluedroid_gatts_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) +{ + ssize_t len; + + if (value == NULL || value_len == 0) { + len = 0; + } else if (offset >= value_len) { + /* Without this guard `value_len - offset` would underflow uint16 and + * MIN would feed memcpy an out-of-bounds length. + */ + LOG_WRN("[B]OffsetTooLarge[%u][%u]", offset, value_len); + len = 0; + } else { + len = MIN(buf_len, value_len - offset); + + LOG_DBG("[B]AttrRd[%u][%u][%u]", attr->handle, offset, len); + + /* Dispatch by caller: a real peer GATT read comes via handle_gatts_read_event + * with conn != NULL and buf = tBTA_GATTS_RSP*; internal raw-byte reads + * (bt_gatt_is_subscribed, ccid.c, etc.) come with conn == NULL and buf = a + * plain byte buffer. Without this branch the raw-byte case would clobber + * stack memory by writing through rsp->attr_value.value past the caller's + * 1-byte storage. */ + if (conn != NULL) { + tBTA_GATTS_RSP *rsp = buf; + + memcpy(rsp->attr_value.value, (uint8_t *)value + offset, len); + rsp->attr_value.len = len; + } else { + memcpy(buf, (uint8_t *)value + offset, len); + } + } + + 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 + * 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) +{ + struct gatts_notify_node *n; + struct gatt_conn *gatt_conn; + + /* Common during ACL teardown — lib drains its own send queue racing with + * the disconnect event walking up. Not actionable, keep at DBG. */ + gatt_conn = bt_le_bluedroid_find_gatt_conn_with_handle(conn->handle); + if (gatt_conn == NULL) { + LOG_INF("[B]GattsNotifyAfterDisconn[%u]", conn->handle); + return -ENOTCONN; + } + + n = calloc(1, sizeof(*n)); + if (n == NULL) { + LOG_ERR("[B]GattsNotifyNodeAllocFail[%u]", conn->handle); + return -ENOMEM; + } + + n->value_handle = value_handle; + sys_slist_append(&gatt_conn->gatts_notify_list, &n->node); + + return 0; +} + +static int gatts_notify(struct bt_conn *conn, + const struct bt_gatt_attr **attr, + const struct bt_uuid *uuid, + const void *value, uint16_t len, + bool need_cfm) +{ + struct notify_data data; + struct bt_conn *conns; + uint8_t conns_count; + uint16_t conn_id; + + /* BTA_GATTS_HandleValueIndication is void and silently drops payloads + * larger than BTA_GATT_MAX_ATTR_LEN. Without this guard, the notify + * marker / indicate node would be enqueued but no CONF_EVT would ever + * arrive, permanently stalling the queue. Catch oversize here so the + * marker is never pushed in that case. (osi_malloc OOM inside BTA is + * still possible but not catchable from this layer.) */ + if (len > BTA_GATT_MAX_ATTR_LEN) { + LOG_ERR("[B]GattsValTooBig[%u]", len); + return -EMSGSIZE; + } + + memset(&data, 0, sizeof(data)); + + 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; + } + + *attr = data.attr; + } else { + if (data.handle == 0) { + return -ENOENT; + } + } + + /* 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); + chrc = data.attr->user_data; + + required = need_cfm ? BT_GATT_CHRC_INDICATE : BT_GATT_CHRC_NOTIFY; + if ((chrc->properties & required) == 0) { + return -EINVAL; + } + + data.handle = bt_gatt_attr_value_handle(data.attr); + } + + if (conn) { + /* For notify, enqueue marker first; any non-zero return means the + * marker is not on the list (gatt_conn gone or alloc failure) so + * skip the BTA send too — otherwise BTA's CONF_EVT pop would have + * no matching marker and corrupt the indication queue head. */ + if (!need_cfm) { + int rc = gatts_notify_enqueue(conn, data.handle); + if (rc) { + return rc; + } + } + + conn_id = BTC_GATT_CREATE_CONN_ID(gatts_if, conn->handle); + BTA_GATTS_HandleValueIndication(conn_id, data.handle, len, + (uint8_t *)value, need_cfm); + } else { + bt_conn_get_acl_conns(&conns, &conns_count); + + for (size_t i = 0; i < conns_count; i++) { + if (conns[i].state != BT_CONN_CONNECTED) { + continue; + } + + if (!need_cfm && gatts_notify_enqueue(&conns[i], data.handle) != 0) { + continue; + } + + conn_id = BTC_GATT_CREATE_CONN_ID(gatts_if, conns[i].handle); + BTA_GATTS_HandleValueIndication(conn_id, data.handle, len, + (uint8_t *)value, need_cfm); + } + } + + return 0; +} + +int bt_le_bluedroid_gatts_notify(struct bt_conn *conn, struct bt_gatt_notify_params *params) +{ + return gatts_notify(conn, &(params->attr), params->uuid, params->data, params->len, false); +} + +/* Enqueue one node onto a conn's gatts_list; dispatch if it became head. + * BTA spec: per-conn single outstanding indication — queueing here lets the + * caller pipeline; we serialize dispatch one-at-a-time via CONF_EVT path. */ +static int gatts_indicate_enqueue(struct bt_conn *conn, + struct bt_gatt_indicate_params *params, + const struct bt_gatt_attr *attr, + uint16_t value_handle) +{ + struct gatts_list_node *n; + struct gatt_conn *gatt_conn; + bool was_empty; + + gatt_conn = bt_le_bluedroid_find_gatt_conn_with_handle(conn->handle); + if (gatt_conn == NULL) { + LOG_ERR("[B]GattsIndNoConnInfo[%u]", conn->handle); + return -ENODEV; + } + + n = gatts_list_node_alloc(params, attr, value_handle); + if (n == NULL) { + LOG_ERR("[B]GattsIndNodeAllocFail[%u]", conn->handle); + return -ENOMEM; + } + + was_empty = sys_slist_is_empty(&gatt_conn->gatts_list); + sys_slist_append(&gatt_conn->gatts_list, &n->node); + + if (was_empty) { + gatts_indicate_dispatch(conn, n); + } + + return 0; +} + +int bt_le_bluedroid_gatts_indicate(struct bt_conn *conn, struct bt_gatt_indicate_params *params) +{ + const struct bt_gatt_attr *attr; + struct bt_conn *conns; + uint16_t value_handle; + uint8_t conns_count; + int rc; + + /* Same BTA silent-drop hazard as gatts_notify: an oversize payload is + * dropped with no CONF_EVT, permanently stalling the per-conn queue. */ + if (params->len > BTA_GATT_MAX_ATTR_LEN) { + LOG_ERR("[B]GattsIndValTooBig[%u]", params->len); + return -EMSGSIZE; + } + + rc = gatts_indicate_resolve(params->attr, params->uuid, &attr, &value_handle); + if (rc) { + LOG_ERR("[B]GattsIndResolveFail[%d]", rc); + return rc; + } + + /* lib reads params->attr after this call (e.g. to obtain the matched + * attr for uuid lookups). Mirror the legacy gatts_notify behavior. */ + params->attr = attr; + + if (conn) { + return gatts_indicate_enqueue(conn, params, attr, value_handle); + } + + /* Broadcast: expand to each CONNECTED ACL conn; deep copy happens per + * node so each conn's queue owns its own data. Mirrors NimBLE NRP. */ + bt_conn_get_acl_conns(&conns, &conns_count); + + for (size_t i = 0; i < conns_count; i++) { + if (conns[i].state != BT_CONN_CONNECTED) { + continue; + } + + rc = gatts_indicate_enqueue(&conns[i], params, attr, value_handle); + if (rc) { + /* Per-conn failure (e.g. -ENODEV for a torn-down conn) — skip + * and keep delivering to remaining conns. Partial-success + * semantics, same as NimBLE NRP broadcast and the sibling + * gatts_notify broadcast path above. */ + LOG_ERR("[B]GattsIndEnqueueFail[%u][%d]", conns[i].handle, rc); + continue; + } + } + + return 0; +} + +int bt_le_bluedroid_gattc_disc_start(uint16_t conn_handle) +{ + struct gatt_conn *gatt_conn; + + gatt_conn = bt_le_bluedroid_find_gatt_conn_with_handle(conn_handle); + if (gatt_conn == NULL) { + LOG_ERR("[B]NoConnInfo[%u]", conn_handle); + return -ENODEV; + } + + /* Central path: BTA_GATTC already opened + cached the peer DB inside + * esp_ble_gattc_aux_open. Re-issuing Enh_Open here would either be a + * no-op or trigger a duplicate open — warn the caller and skip so + * role-agnostic callers can invoke this unconditionally. + */ + if (gatt_conn->role == BTM_ROLE_MASTER) { + LOG_INF("[B]DiscStartCentralSkip[%u]", conn_handle); + return 0; + } + + /* Peripheral path: BTA_GATTC has no prior knowledge of the peer (we did + * not initiate the link). Enh_Open establishes the client view so the + * stack can auto-discover the remote DB. + */ + /* Non-PAWR connection: is_pawr_synced=false, adv_handle/subevent=0xff + * sentinel (matches btc_gattc_open non-PAWR branch). phy_mask=0 means + * "no PHY preference"; phy_*_conn_params NULL → controller defaults. */ + BTA_GATTC_Enh_Open(gattc_if, gatt_conn->peer.val, gatt_conn->peer.type, + 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; +} + +static int gattc_disc_primary_svc(struct bt_conn *conn, struct bt_gatt_discover_params *params) +{ + struct bt_gatt_service_val svc = {0}; + struct bt_uuid_16 svc_uuid = {0}; + struct bt_gatt_attr attr = {0}; + btgatt_db_element_t *db = NULL; + tBT_UUID uuid = {0}; + uint16_t count = 0; + uint16_t conn_id; + uint8_t ret; + + if (params->uuid == NULL) { + LOG_ERR("[B]DiscAllPrimarySvcsNotSupp"); + return -ENOTSUP; + } + + conn_id = BTC_GATT_CREATE_CONN_ID(gattc_if, conn->handle); + bt_le_bluedroid_gatt_uuid_convert(params->uuid, &uuid); + + BTA_GATTC_GetServiceWithUUID(conn_id, &uuid, &db, &count); + + LOG_INF("[B]FoundPrimarySvc[%u][0x%04x][%u][%u]", + count, uuid.uu.uuid16, + params->start_handle, params->end_handle); + + if (params->func == NULL) { + goto end; + } + + if (db == NULL || count == 0) { + params->func(conn, NULL, params); + goto end; + } + + ret = BT_GATT_ITER_CONTINUE; + + for (size_t i = 0; i < count; i++) { + LOG_INF("[B]SvcInst[%u][%u][%u][%u][%u][%02x]", + i, db[i].type, db[i].start_handle, db[i].end_handle, + db[i].attribute_handle, db[i].properties); + + /* GetServiceWithUUID returns both primary and secondary services with + * a matching UUID; this is primary-service discovery, so skip + * secondaries. */ + if (db[i].type != BTGATT_DB_PRIMARY_SERVICE) { + continue; + } + + /* BTA's GetServiceWithUUID has no handle-range filter, so honor + * the caller's [start_handle, end_handle] manually. Sibling + * gattc_disc_included_svc / gattc_disc_chrc pass the range into + * BTA so they don't need this. */ + if (db[i].start_handle < params->start_handle || + db[i].end_handle > params->end_handle) { + continue; + } + + svc_uuid.uuid.type = BT_UUID_TYPE_16; + svc_uuid.val = BT_UUID_16(params->uuid)->val; + + svc.uuid = &svc_uuid.uuid; + svc.end_handle = db[i].end_handle; + + attr.user_data = &svc; + attr.handle = db[i].start_handle; + attr.uuid = params->uuid; + + ret = params->func(conn, &attr, params); + if (ret == BT_GATT_ITER_STOP) { + break; + } + } + + if (ret == BT_GATT_ITER_CONTINUE) { + params->func(conn, NULL, params); + } + +end: + if (db) { + free(db); + } + return 0; +} + +static int gattc_disc_included_svc(struct bt_conn *conn, struct bt_gatt_discover_params *params) +{ + struct bt_gatt_include inc_svc = {0}; + struct bt_uuid_16 svc_uuid = {0}; + struct bt_gatt_attr attr = {0}; + btgatt_db_element_t *db = NULL; + tBT_UUID uuid = {0}; + uint16_t count = 0; + uint16_t conn_id; + uint8_t ret; + + conn_id = BTC_GATT_CREATE_CONN_ID(gattc_if, conn->handle); + if (params->uuid) { + bt_le_bluedroid_gatt_uuid_convert(params->uuid, &uuid); + } + + BTA_GATTC_GetIncludeService(conn_id, params->start_handle, params->end_handle, + params->uuid ? &uuid : NULL, &db, &count); + + if (params->uuid) { + LOG_INF("[B]FoundInclSvc[%u][0x%04x][%u][%u]", + count, uuid.uu.uuid16, + params->start_handle, params->end_handle); + } else { + LOG_INF("[B]FoundInclSvc[%u][All][%u][%u]", + count, params->start_handle, params->end_handle); + } + + if (params->func == NULL) { + goto end; + } + + if (db == NULL || count == 0) { + params->func(conn, NULL, params); + goto end; + } + + ret = BT_GATT_ITER_CONTINUE; + + for (size_t i = 0; i < count; i++) { + LOG_INF("[B]InclSvcInst[%u][%u][%u][%u][%u][%02x]", + i, db[i].type, db[i].start_handle, db[i].end_handle, + db[i].attribute_handle, db[i].properties); + + 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; + inc_svc.end_handle = db[i].end_handle; + + /* Include declaration's own attribute type. Caller may dereference + * attr->uuid (Zephyr bt_gatt_attr contract); leaving it NULL would + * crash discovery callbacks. The included service's own UUID is + * conveyed via attr.user_data (bt_gatt_include). */ + attr.uuid = BT_UUID_GATT_INCLUDE; + attr.user_data = &inc_svc; + /* Real include declaration handle from BTA cache. Callers (e.g. + * vcp_vol_ctlr / cap_common) use attr->handle + 1 to advance the + * next disc range; reporting params->start_handle would let BTA + * re-emit the same include on every continuation pass. + * + * NimBLE side still reports params->start_handle (TODO until + * CONFIG_BT_NIMBLE_INCL_SVC_DISCOVERY is wired). Lib tolerates + * that via inst_cnt gates, so the dual-host divergence stays + * functional — Bluedroid is just the more efficient path. */ + attr.handle = db[i].attribute_handle; + + ret = params->func(conn, &attr, params); + if (ret == BT_GATT_ITER_STOP) { + break; + } + } + + if (ret == BT_GATT_ITER_CONTINUE) { + params->func(conn, NULL, params); + } + +end: + if (db) { + free(db); + } + return 0; +} + +static int gattc_disc_chrc(struct bt_conn *conn, struct bt_gatt_discover_params *params) +{ + struct bt_uuid_16 chrc_uuid = {0}; + struct bt_gatt_attr attr = {0}; + struct bt_gatt_chrc chrc = {0}; + btgatt_db_element_t *db = NULL; + tBT_UUID uuid = {0}; + uint16_t count = 0; + uint16_t conn_id; + uint8_t ret; + + conn_id = BTC_GATT_CREATE_CONN_ID(gattc_if, conn->handle); + + /* Note: + * Increment start_handle with 1 here because, for example, if 2 characteristics + * are found with characteristic value handle equals to A and B. After handling + * the characteristic with value handle A, the LE Audio stack may try to find the + * 2nd characteristic with value handle B with the start_handle set to A. + * In this case, the characteristic with value handle A will still be found which + * will cause infinite discovery loop. + */ + /* The +1 above is intentionally disabled: the LE Audio lib callers already + * re-invoke discovery with start_handle = attr->handle + 1 (e.g. cap_common.c, + * mcc.c, vcp_vol_ctlr.c), and BTA's '>= start_handle' filter (bta_gattc_cache.c) + * skips the previously found characteristic as-is. The nimble adapter uses the + * same '>=' filter (gatt.db.c::handle_gattc_disc_chrs) and runs without issue. + * Adding another +1 here would double-skip the attribute adjacent to the value + * handle (e.g. the next chrc declaration). + */ + /* params->start_handle += (params->start_handle == BT_ATT_FIRST_ATTRIBUTE_HANDLE ? 0 : 1); */ + + if (params->uuid) { + bt_le_bluedroid_gatt_uuid_convert(params->uuid, &uuid); + + BTA_GATTC_GetCharByUUID(conn_id, params->start_handle, params->end_handle, uuid, &db, &count); + } else { + BTA_GATTC_GetAllChar(conn_id, params->start_handle, params->end_handle, &db, &count); + } + + if (params->uuid) { + LOG_INF("[B]FoundChrc[%u][0x%04x][%u][%u]", + count, uuid.uu.uuid16, + params->start_handle, params->end_handle); + } else { + LOG_INF("[B]FoundChrc[%u][All][%u][%u]", + count, params->start_handle, params->end_handle); + } + + if (params->func == NULL) { + goto end; + } + + if (db == NULL || count == 0) { + params->func(conn, NULL, params); + goto end; + } + + ret = BT_GATT_ITER_CONTINUE; + + for (size_t i = 0; i < count; i++) { + LOG_INF("[B]ChrcInst[%u][%u][%u][%u][%u][%02x]", + i, db[i].type, db[i].start_handle, db[i].end_handle, + db[i].attribute_handle, db[i].properties); + + chrc_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. + */ + chrc_uuid.val = sys_get_le16(db[i].uuid.uu + 12); + + chrc.uuid = &chrc_uuid.uuid; + chrc.value_handle = db[i].attribute_handle; /* Char val handle */ + chrc.properties = db[i].properties; + + /* Char declaration's own attribute type (Zephyr attr->uuid contract, + * same as gattc_disc_included_svc); the char's UUID is in chrc.uuid. */ + attr.uuid = BT_UUID_GATT_CHRC; + attr.user_data = &chrc; + attr.handle = db[i].attribute_handle - 1; /* Char def handle */ + + ret = params->func(conn, &attr, params); + if (ret == BT_GATT_ITER_STOP) { + break; + } + } + + if (ret == BT_GATT_ITER_CONTINUE) { + params->func(conn, NULL, params); + } + +end: + if (db) { + free(db); + } + return 0; +} + +static int gattc_disc_chrc_desc(struct bt_conn *conn, struct bt_gatt_discover_params *params) +{ + struct bt_gatt_attr attr = {0}; + btgatt_db_element_t *db = NULL; + tBTA_GATT_UNFMT write = {0}; + tBTA_GATT_STATUS status; + uint16_t chrc_handle; + tBT_UUID uuid = {0}; + uint16_t count = 0; + uint16_t conn_id; + int err = 0; + + assert(params); + assert(params->uuid); + assert(params->func); + assert(params->sub_params); + + /* Only descriptors can be filtered */ + if (bt_uuid_cmp(params->uuid, BT_UUID_GATT_PRIMARY) == 0 || + bt_uuid_cmp(params->uuid, BT_UUID_GATT_SECONDARY) == 0 || + bt_uuid_cmp(params->uuid, BT_UUID_GATT_INCLUDE) == 0 || + bt_uuid_cmp(params->uuid, BT_UUID_GATT_CHRC) == 0) { + LOG_ERR("[B]InvDescUuidToDisc[%02x]", params->uuid->type); + return -EINVAL; + } + + conn_id = BTC_GATT_CREATE_CONN_ID(gattc_if, conn->handle); + chrc_handle = params->sub_params->value_handle; + bt_le_bluedroid_gatt_uuid_convert(params->uuid, &uuid); + + BTA_GATTC_GetDescrByCharHandle(conn_id, chrc_handle, uuid, &db, &count); + + LOG_INF("[B]FoundChrcDesc[%u][%u][%u]", + count, chrc_handle, params->end_handle); + + if (db == NULL || count == 0) { + params->func(conn, NULL, params); + + err = -ENODEV; + goto end; + } + + if (count != 1) { + LOG_ERR("[B]TooMuchChrcDesc[%u][%u]", + count, chrc_handle); + params->func(conn, NULL, params); + + err = -EINVAL; + goto end; + } + + LOG_INF("[B]DescInst[%u][%u][%u][%u][%02x]", + db[0].type, db[0].start_handle, db[0].end_handle, + db[0].attribute_handle, db[0].properties); + + attr.handle = db[0].attribute_handle; + + status = BTA_GATTC_RegisterForNotifications(gattc_if, conn->le.dst.a.val, chrc_handle); + if (status != BTA_GATT_OK) { + LOG_ERR("[B]EnableNtfFail[%u][%u]", chrc_handle, attr.handle); + + params->func(conn, NULL, params); + + err = -EIO; + goto end; + } + + write.len = sizeof(params->sub_params->value); + write.p_value = (uint8_t *)¶ms->sub_params->value; + + LOG_INF("[B]EnableCcc[%u][%u]", attr.handle, write.len); + + BTA_GATTC_WriteCharDescr(conn_id, attr.handle, BTA_GATTC_TYPE_WRITE, + &write, BTA_GATT_AUTH_REQ_NONE); + + params->func(conn, &attr, params); + +end: + if (db) { + free(db); + } + return err; +} + +int bt_le_bluedroid_gattc_discover(struct bt_conn *conn, struct bt_gatt_discover_params *params) +{ + struct gatt_conn *gatt_conn; + + LOG_DBG("[B]GattcDisc[%u][%u]", conn->handle, params->type); + + gatt_conn = bt_le_bluedroid_find_gatt_conn_with_handle(conn->handle); + if (gatt_conn == NULL) { + LOG_ERR("[B]NoConnInfo[%u]", conn->handle); + return -ENODEV; + } + + /* Reject non-16-bit caller UUIDs up front — sibling disc helpers all + * read the result UUID via sys_get_le16(db[i].uuid.uu + 12), which only + * yields a valid value for 16-bit Bluetooth Base UUIDs; 32/128-bit + * would silently truncate. BTA itself supports all three widths, but + * the callback contract here is 16-bit only. Matches NimBLE's gate in + * bt_le_nimble_gattc_discover. */ + if (params->uuid && params->uuid->type != BT_UUID_TYPE_16) { + LOG_ERR("[B]DiscNon16BitUuid[%u]", params->uuid->type); + return -ENOTSUP; + } + + switch (params->type) { + case BT_GATT_DISCOVER_PRIMARY: + return gattc_disc_primary_svc(conn, params); + + case BT_GATT_DISCOVER_INCLUDE: + return gattc_disc_included_svc(conn, params); + + case BT_GATT_DISCOVER_CHARACTERISTIC: + return gattc_disc_chrc(conn, params); + + case BT_GATT_DISCOVER_DESCRIPTOR: + return gattc_disc_chrc_desc(conn, params); + + default: + LOG_ERR("[B]DiscTypeNotSupp[%u]", params->type); + return -ENOTSUP; + } +} + +int bt_le_bluedroid_gattc_read(struct bt_conn *conn, struct bt_gatt_read_params *params) +{ + struct gatt_conn *gatt_conn; + struct gattc_list_node *op; + tBT_UUID uuid = {0}; + uint16_t conn_id; + + LOG_DBG("[B]GattcRd[%u]", conn->handle); + + gatt_conn = bt_le_bluedroid_find_gatt_conn_with_handle(conn->handle); + if (gatt_conn == NULL) { + LOG_ERR("[B]NoConnInfo[%u]", conn->handle); + return -ENODEV; + } + + /* 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. */ + op = gattc_list_node_alloc(GATTC_OP_READ, params); + if (op == NULL) { + LOG_ERR("[B]GattcRdOpAllocFail[%u]", conn->handle); + return -ENOMEM; + } + + sys_slist_append(&gatt_conn->gattc_list, &op->node); + + conn_id = BTC_GATT_CREATE_CONN_ID(gattc_if, conn->handle); + + if (params->handle_count == 0) { + LOG_INF("[B]RdByTypeReq[0x%04x]", BT_UUID_16(params->by_uuid.uuid)->val); + + bt_le_bluedroid_gatt_uuid_convert(params->by_uuid.uuid, &uuid); + + BTA_GATTC_Read_by_type(conn_id, params->by_uuid.start_handle, + params->by_uuid.end_handle, &uuid, + BTA_GATT_AUTH_REQ_NONE); + } else { + LOG_INF("[B]RdReq[%u][%u]", params->single.handle, params->single.offset); + + /* A non-zero offset is a long read: the lib re-reads the remainder of + * an ASE/BASS value whose notification was MTU-truncated. BTA's + * ReadLongChar would run GATTC_Read inline on the ISO task, racing the + * BTU task that owns gatt_cb. Always issue ReadCharacteristic instead + * (GATT_READ_BY_HANDLE) — BTA auto-continues it with Read Blob on the + * BTU task to fetch the full value, and handle_gattc_read_chrc_event + * slices the requested [offset:] tail back to the caller. */ + BTA_GATTC_ReadCharacteristic(conn_id, params->single.handle, + BTA_GATT_AUTH_REQ_NONE); + } + + return 0; +} + +int bt_le_bluedroid_gattc_write(struct bt_conn *conn, struct bt_gatt_write_params *params) +{ + struct gatt_conn *gatt_conn; + struct gattc_list_node *op; + uint16_t conn_id; + + LOG_DBG("[B]GattcWr[%u]", conn->handle); + + gatt_conn = bt_le_bluedroid_find_gatt_conn_with_handle(conn->handle); + if (gatt_conn == NULL) { + LOG_ERR("[B]NoConnInfo[%u]", conn->handle); + return -ENODEV; + } + + op = gattc_list_node_alloc(GATTC_OP_WRITE, params); + if (op == NULL) { + LOG_ERR("[B]GattcWrOpAllocFail[%u]", conn->handle); + return -ENOMEM; + } + + sys_slist_append(&gatt_conn->gattc_list, &op->node); + + conn_id = BTC_GATT_CREATE_CONN_ID(gattc_if, conn->handle); + + BTA_GATTC_WriteCharValue(conn_id, params->handle, BTA_GATTC_TYPE_WRITE, + params->length, (void *)params->data, + BTA_GATT_AUTH_REQ_NONE); + + return 0; +} + +int bt_le_bluedroid_gattc_write_without_rsp(struct bt_conn *conn, uint16_t handle, + const void *data, uint16_t length) +{ + struct gatt_conn *gatt_conn; + uint16_t conn_id; + + LOG_DBG("[B]GattcWrCmd[%u][%u][%u]", conn->handle, handle, length); + + /* 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). */ + gatt_conn = bt_le_bluedroid_find_gatt_conn_with_handle(conn->handle); + if (gatt_conn == NULL) { + LOG_ERR("[B]NoConnInfo[%u]", conn->handle); + return -ENODEV; + } + + conn_id = BTC_GATT_CREATE_CONN_ID(gattc_if, conn->handle); + + /* TODO: BTA_GATTC_WriteCharValue is void and silently drops the cmd on + * osi_malloc failure. NimBLE returns the host status here, so the API + * contract documents -ENOMEM as reachable, but this path can never + * report it. Acceptable today because write_without_rsp is wire-level + * best-effort anyway; revisit if BTA gains a status-returning variant. */ + BTA_GATTC_WriteCharValue(conn_id, handle, BTA_GATTC_TYPE_WRITE_NO_RSP, + length, (void *)data, BTA_GATT_AUTH_REQ_NONE); + + return 0; +} + +int bt_le_bluedroid_gattc_write_ccc(struct bt_conn *conn, struct bt_gatt_subscribe_params *params) +{ + tBTA_GATT_UNFMT write = {0}; + tBTA_GATT_STATUS status; + uint16_t chrc_handle; + uint16_t conn_id; + + conn_id = BTC_GATT_CREATE_CONN_ID(gattc_if, conn->handle); + chrc_handle = params->value_handle; + + /* Unsubscribe path: caller (bt_gatt_unsubscribe) forces params->value to + * 0x0000. BTA's notif_reg[] is a purely local routing table — Dereg only + * clears our callback slot for (bda, handle); a not-found error here + * (BTA_GATT_ERROR) just means the slot was already gone, e.g.: + * - disconnect cleanup wiped notif_reg before the host caught up; + * - a previous unsubscribe already ran; + * - BTA bookkeeping diverged from ours for any reason. + * It does NOT mean the CCC write to the peer can be skipped: the peer + * still needs CCC=0 to stop notifications on the wire, and the upper + * layer needs success so it removes the node from its subscription + * list — otherwise the node is stuck forever. + */ + status = BTA_GATTC_DeregisterForNotifications(gattc_if, conn->le.dst.a.val, chrc_handle); + if (status != BTA_GATT_OK) { + LOG_WRN("[B]DeregNtfFail[%u][%u][%d]", chrc_handle, params->ccc_handle, status); + } + + write.len = sizeof(params->value); + write.p_value = (uint8_t *)¶ms->value; + + BTA_GATTC_WriteCharDescr(conn_id, params->ccc_handle, BTA_GATTC_TYPE_WRITE, + &write, BTA_GATT_AUTH_REQ_NONE); + + return 0; +} + +int bt_le_bluedroid_gatt_init(void) +{ + gatt_shutting_down = false; + k_sem_create(&gatts_sem); + k_sem_create(&gattc_sem); + + /* Threading: gatts_app_cb / gattc_app_cb run in BTU (BTA dispatches + * inline, no BTC hop). BTU is shared with classic BT, so handlers only + * marshal the event (calloc + memcpy) and post to the ISO task — heavy + * work stays off BTU. Classic BT is unaffected: GATT is LE-only, no + * shared BTA module or BTC path. + * + * BTU task (single task) layout: + * ├─ HCI event dispatch classic + LE + * ├─ BTM ble_*, btm_* LE + * ├─ BTM btm_acl_*, sec classic + LE + * ├─ BTA_AV/AG/SPP… classic + * ├─ BTA_GATTS/GATTC LE (iso) + * ├─ BTA_DM classic + LE + * └─ btc_transfer_context classic app cb hops to BTC task here + */ + + k_sem_reset(&gatts_sem); + BTA_GATTS_AppRegister(&gatts_app_uuid, gatts_app_cb); + + if (k_sem_take(&gatts_sem, K_SEM_SHORT) || gatts_sem.result) { + LOG_ERR("[B]GattsRegFail"); + return -1; + } + + k_sem_reset(&gattc_sem); + BTA_GATTC_AppRegister(&gattc_app_uuid, gattc_app_cb); + + if (k_sem_take(&gattc_sem, K_SEM_SHORT) || gattc_sem.result) { + LOG_ERR("[B]GattcRegFail"); + return -1; + } + + for (size_t i = 0; i < ARRAY_SIZE(gatt_conns); i++) { + reset_gatt_conn(&gatt_conns[i]); + } + + return 0; +} + +void bt_le_bluedroid_gatt_deinit(void) +{ + /* Block late REG_EVT gives before tearing down the sems. */ + gatt_shutting_down = true; + + if (gattc_if != 0) { + BTA_GATTC_AppDeregister(gattc_if); + gattc_if = 0; + } + + if (gatts_if != 0) { + BTA_GATTS_AppDeregister(gatts_if); + gatts_if = 0; + } + + /* Free any operation nodes still queued on per-conn lists (gattc_list, + * gatts_list, gatts_notify_list). Mirrors the reset in init so an + * init/deinit/re-init cycle doesn't accumulate leaks. */ + for (size_t i = 0; i < ARRAY_SIZE(gatt_conns); i++) { + reset_gatt_conn(&gatt_conns[i]); + } + + k_sem_delete(&gattc_sem); + k_sem_delete(&gatts_sem); +} diff --git a/components/bt/esp_ble_iso/host/adapter/bluedroid/hci.c b/components/bt/esp_ble_iso/host/adapter/bluedroid/hci.c new file mode 100644 index 00000000000..6dffd4aa135 --- /dev/null +++ b/components/bt/esp_ble_iso/host/adapter/bluedroid/hci.c @@ -0,0 +1,198 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include +#include + +#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 + * Zephyr definition before pulling in osi's. */ +#undef CONCAT +#include "hci/hci_layer.h" + +#include "stack/hcimsgs.h" +#include "stack/btm_ble_api.h" +#include "bluedroid/btm_error.h" + +#include "common/host.h" + +LOG_MODULE_REGISTER(ISO_BHCI, CONFIG_BT_ISO_LOG_LEVEL); + +/* 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, + * not ble_sync_info.sync_sem. Two-task race on sync_info->opcode disappears. + * + * Caller sets direct_hci_rsp.opcode before transmit; cb verifies the + * response opcode matches before giving the sem (guards against unexpected + * stray Command_Complete dispatches). + * + * direct_hci_complete_cb runs on the hci_layer task — keep it tiny and + * non-blocking, no host_lock, no further dispatch. + * + * 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; + +/* 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 struct { + uint16_t opcode; /* expected — set by caller, verified by cb */ + uint8_t status; /* HCI status from Command_Complete */ +} direct_hci_rsp; + +/* Internal sink for the cmd-specific return payload (bytes after the status + * byte). The cb copies HERE, never into the caller's stack buffer — a + * timed-out caller may already have freed its stack frame by the time a late + * cb runs on the hci_layer task. The caller copies this out only after a + * successful take. Static storage: a late cb always has a valid target, so no + * 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 void direct_hci_complete_cb(BT_HDR *response, void *context) +{ + /* Command_Complete event layout after BT_HDR data/offset: + * [0] event code (0x0E) [1] param len [2] num_cmd_packets + * [3..4] opcode (LE) [5] status [6..] cmd-specific params + */ + uint8_t *stream = response->data + response->offset + 3; + uint16_t opcode; + uint8_t status; + uint8_t event_param_len; + + ARG_UNUSED(context); + + event_param_len = response->data[response->offset + 1]; + + STREAM_TO_UINT16(opcode, stream); + STREAM_TO_UINT8(status, stream); + + if (opcode != direct_hci_rsp.opcode) { + LOG_ERR("[B]DirectHciOpcodeMismatch[exp=0x%04x][got=0x%04x]", + direct_hci_rsp.opcode, opcode); + /* Drop — don't give sem, caller times out. */ + osi_free(response); + return; + } + + direct_hci_rsp.status = status; + + /* Copy cmd-specific return params (everything after the status byte) + * into the caller's buffer. event_param_len counts: + * num_cmd_packets(1) + opcode(2) + status(1) + cmd-specific(N) + * so the payload available to caller is event_param_len - 4. */ + if (event_param_len > 4) { + direct_hci_rsp_data_len = MIN(event_param_len - 4, sizeof(direct_hci_rsp_data)); + memcpy(direct_hci_rsp_data, stream, direct_hci_rsp_data_len); + } + + osi_free(response); + + /* 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) { + return; + } + + k_sem_give(&direct_hci_sem); +} + +tBTM_STATUS bt_le_bluedroid_hci_send_sync(uint16_t opcode, + const uint8_t *cmd_params, + uint8_t cmd_params_len, + uint8_t *rsp_buf, + uint8_t rsp_buf_len) +{ + BT_HDR *p; + UINT8 *pp; + hci_cmd_metadata_t *metadata; + + p = HCI_GET_CMD_BUF(cmd_params_len); + if (p == NULL) { + return BTM_NO_RESOURCES; + } + + pp = p->data; + UINT16_TO_STREAM(pp, opcode); + UINT8_TO_STREAM(pp, cmd_params_len); + if (cmd_params_len > 0 && cmd_params != NULL) { + memcpy(pp, cmd_params, cmd_params_len); + } + + metadata = HCI_GET_CMD_METAMSG(p); + metadata->command_complete_cb = direct_hci_complete_cb; + metadata->command_status_cb = NULL; + metadata->opcode = opcode; + metadata->context = NULL; + + /* Set expected opcode and rsp sink BEFORE transmit — cb may run + * before transmit returns if response is already queued. */ + direct_hci_rsp.opcode = opcode; + direct_hci_rsp.status = 0; + direct_hci_rsp_data_len = 0; + + /* Drop any stale give from a previous timed-out cmd whose cb arrived + * late. Must run AFTER opcode update (so any cb arriving after this + * drain hits the mismatch path and doesn't give) and BEFORE transmit + * (so this cmd's own cb-give isn't accidentally consumed here). */ + k_sem_reset(&direct_hci_sem); + + hci_layer_get_interface()->transmit_command(p, direct_hci_complete_cb, + NULL, NULL); + + if (k_sem_take(&direct_hci_sem, K_SEM_SHORT) != 0) { + LOG_ERR("[B]DirectHciTimeout[0x%04x]", opcode); + return BTM_ERR_PROCESSING; + } + + /* The cb gives the sem only after its memcpy, so the payload is complete; + * copy it into the caller's (now-confirmed-live) sink. */ + if (rsp_buf != NULL && rsp_buf_len > 0 && direct_hci_rsp_data_len > 0) { + memcpy(rsp_buf, direct_hci_rsp_data, MIN(direct_hci_rsp_data_len, rsp_buf_len)); + } + + if (direct_hci_rsp.status != HCI_SUCCESS) { + LOG_ERR("[B]DirectHciStatus[0x%04x][%02x]", + opcode, direct_hci_rsp.status); + return BTM_HCI_ERROR | direct_hci_rsp.status; + } + + return BTM_SUCCESS; +} + +int bt_le_bluedroid_hci_init(void) +{ + direct_hci_shutting_down = false; + k_sem_create(&direct_hci_sem); + return 0; +} + +void bt_le_bluedroid_hci_deinit(void) +{ + /* Flip the gate first so any cb past this point skips the give. + * Drain any give that was already posted before the flag flip so + * the sem is clean before deletion. */ + direct_hci_shutting_down = true; + k_sem_reset(&direct_hci_sem); + k_sem_delete(&direct_hci_sem); +} + +#endif /* USE_DIRECT_HCI */ diff --git a/components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/btm_error.h b/components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/btm_error.h new file mode 100644 index 00000000000..01202df645f --- /dev/null +++ b/components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/btm_error.h @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "stack/btm_api.h" + +/** + * Translate a Bluedroid BTM status (tBTM_STATUS, see stack/btm_api.h) into + * a standard negative POSIX errno. + * + * Counterpart to nimble_err_to_errno() — both adapters return errno values + * to common/ and lib code so the audio layer can branch on -EAGAIN / + * -EBUSY / -ENOMEM etc. without knowing which host it ran on. + * + * Unknown codes map to -EIO so caller logic doesn't accidentally match a + * known errno. + */ +static inline int bluedroid_err_to_errno(tBTM_STATUS status) +{ + switch (status) { + case BTM_SUCCESS: return 0; + case BTM_CMD_STARTED: return 0; + case BTM_CMD_STORED: return 0; + case BTM_SUCCESS_NO_SECURITY: return 0; + case BTM_BUSY: return -EBUSY; + case BTM_NO_RESOURCES: return -ENOMEM; + case BTM_MODE_UNSUPPORTED: return -ENOTSUP; + case BTM_ILLEGAL_VALUE: return -EINVAL; + case BTM_WRONG_MODE: return -EPERM; + case BTM_UNKNOWN_ADDR: return -ENOENT; + case BTM_DEVICE_TIMEOUT: return -ETIMEDOUT; + case BTM_BAD_VALUE_RET: return -EBADMSG; + case BTM_ERR_PROCESSING: return -EIO; + case BTM_NOT_AUTHORIZED: return -EACCES; + case BTM_DEV_RESET: return -ECONNRESET; + case BTM_ILLEGAL_ACTION: return -EPERM; + case BTM_FAILED_ON_SECURITY: return -EPERM; + case BTM_REPEATED_ATTEMPTS: return -EAGAIN; + default: return -EIO; + } +} diff --git a/components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/gap.h b/components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/gap.h new file mode 100644 index 00000000000..03e475f52b4 --- /dev/null +++ b/components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/gap.h @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef HOST_BLUEDROID_APP_GAP_H_ +#define HOST_BLUEDROID_APP_GAP_H_ + +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void bt_le_bluedroid_gap_post_event(uint16_t event, void *param); + +int bt_le_bluedroid_scan_start(const struct bt_le_scan_param *param); + +int bt_le_bluedroid_scan_stop(void); + +int bt_le_bluedroid_iso_disconnect(uint16_t conn_handle, uint8_t reason); + +int bt_le_bluedroid_gap_init(void); + +#ifdef __cplusplus +} +#endif + +#endif /* HOST_BLUEDROID_APP_GAP_H_ */ diff --git a/components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/gatt.h b/components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/gatt.h new file mode 100644 index 00000000000..7abfbcd7d37 --- /dev/null +++ b/components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/gatt.h @@ -0,0 +1,133 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef HOST_BLUEDROID_GATT_H_ +#define HOST_BLUEDROID_GATT_H_ + +#include +#include + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct gatt_conn { + uint8_t used : 1; + uint8_t conn_create : 1; + uint8_t gattc_open : 1; + uint8_t cfg_mtu : 1; + uint8_t mtu_posted : 1; + + uint8_t status; + uint8_t gatt_if; + uint16_t conn_handle; + uint8_t role; + struct { + uint8_t type; + uint8_t val[6]; + } peer; + + uint16_t mtu; + + /* FIFO of in-flight GATTC read/write ops; mirrors BTA p_cmd_list ordering. + * BTA EVTs deliver attr_handle/status only — we need this to recover the + * caller's params on each cmpl. See struct gattc_list_node in gatt.c. */ + sys_slist_t gattc_list; + + /* FIFO of pending GATTS indications. GATT spec restricts per-conn to a + * single outstanding indication; this queue absorbs that limit so callers + * never see -EBUSY. See struct gatts_list_node in gatt.c. Mirrors + * NimBLE adapter NRP indicate behavior. */ + sys_slist_t gatts_list; + + /* FIFO of in-flight GATTS notify markers. BTA fires CONF_EVT for notify + * too (immediately after send) — tBTA_GATTS_REQ carries no notify/indicate + * flag so the handler pops this list first to disambiguate from the + * indication acks tracked in gatts_list. See struct gatts_notify_node. */ + sys_slist_t gatts_notify_list; +}; + +uint8_t bt_le_bluedroid_gattc_get_if(void); + +uint8_t bt_le_bluedroid_gatts_get_if(void); + +void bt_le_bluedroid_gatts_sem_reset(void); + +int bt_le_bluedroid_gatts_sem_take(void); + +void bt_le_bluedroid_gatts_sem_give(int result); + +struct gatt_conn *bt_le_bluedroid_find_gatt_conn_with_addr(uint8_t addr_type, + const uint8_t addr[6], + bool ignore_type); + +struct gatt_conn *bt_le_bluedroid_find_gatt_conn_with_handle(uint16_t conn_handle); + +struct gatt_conn *bt_le_bluedroid_find_free_gatt_conn(void); + +void bt_le_bluedroid_gatt_handle_event(uint8_t *data, size_t data_len); + +void bt_le_bluedroid_gatt_uuid_convert(const struct bt_uuid *uuid_in, void *uuid_out); + +uint16_t bt_le_bluedroid_gatt_perm_convert(uint16_t perm_in); + +uint16_t bt_le_bluedroid_gatt_get_mtu(struct bt_conn *conn); + +ssize_t bt_le_bluedroid_gatts_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); + +int bt_le_bluedroid_gatts_notify(struct bt_conn *conn, struct bt_gatt_notify_params *params); + +int bt_le_bluedroid_gatts_indicate(struct bt_conn *conn, struct bt_gatt_indicate_params *params); + +int bt_le_bluedroid_gattc_disc_start(uint16_t conn_handle); + +int bt_le_bluedroid_gattc_discover(struct bt_conn *conn, struct bt_gatt_discover_params *params); + +int bt_le_bluedroid_gattc_read(struct bt_conn *conn, struct bt_gatt_read_params *params); + +int bt_le_bluedroid_gattc_write(struct bt_conn *conn, struct bt_gatt_write_params *params); + +int bt_le_bluedroid_gattc_write_without_rsp(struct bt_conn *conn, uint16_t handle, + const void *data, uint16_t length); + +int bt_le_bluedroid_gattc_write_ccc(struct bt_conn *conn, struct bt_gatt_subscribe_params *params); + +int bt_le_bluedroid_gatt_init(void); + +void bt_le_bluedroid_gatt_deinit(void); + +struct inc_svc_inst { + struct bt_gatt_service *svc_p; + bool included; +}; + +struct gatts_svc_cb { + void (*svc_create_cb)(uint16_t service_id, uint16_t svc_instance, + bool is_primary, uint8_t status, void *uuid); + + void (*inc_svc_add_cb)(uint16_t service_id, uint16_t attr_id, uint8_t status); + + void (*chrc_add_cb)(uint16_t service_id, uint16_t attr_id, + uint8_t status, void *uuid); + + void (*svc_start_cb)(uint16_t service_id, uint8_t status); +}; + +void bt_le_bluedroid_gatts_svc_cb_register(struct gatts_svc_cb *cb); + +#ifdef __cplusplus +} +#endif + +#endif /* HOST_BLUEDROID_GATT_H_ */ 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 new file mode 100644 index 00000000000..92e208f6b21 --- /dev/null +++ b/components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/hci.h @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef HOST_BLUEDROID_APP_HCI_H_ +#define HOST_BLUEDROID_APP_HCI_H_ + +#include + +#include "stack/btm_ble_api.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* When set to 1, sync HCI commands issued by the iso adapter bypass BTM + * and the global ble_sync_info to avoid the sync_info opcode race with + * concurrent BTC/BTA sync cmds. Experimental. Toggle is shared with + * iso.c and gap.c via this header so the BTM fallback paths in both + * files stay in sync. */ +#define USE_DIRECT_HCI 1 + +int bt_le_bluedroid_hci_init(void); + +void bt_le_bluedroid_hci_deinit(void); + +/* Send a sync HCI command, bypassing BTM and ble_sync_info. `cmd_params` + * is the cmd parameter payload (without opcode/length preamble). + * + * Returns BTM_SUCCESS, BTM_HCI_ERROR | hci_status on controller failure, + * or BTM_ERR_PROCESSING on host-side timeout. + * + * If `rsp_buf` is non-NULL, up to `rsp_buf_len` bytes of the + * cmd-specific return params (the bytes after the status byte in the + * HCI Command_Complete event) are copied in. If the actual payload is + * shorter, the tail of rsp_buf is left untouched; if longer, the excess + * is silently dropped. Callers parse opcode-specific structure. + * + * Serialization: NOT thread-safe — internally uses single-slot static + * state (sem, opcode latch, rsp buffer pointer). Callers must serialize + * externally; in practice all current callers (iso.c hci_cmd_* and + * gap.c scan_start/stop) run on the iso task, so single-task affinity + * provides serialization without explicit locking. */ +tBTM_STATUS bt_le_bluedroid_hci_send_sync(uint16_t opcode, + const uint8_t *cmd_params, + uint8_t cmd_params_len, + uint8_t *rsp_buf, + uint8_t rsp_buf_len); + +#ifdef __cplusplus +} +#endif + +#endif /* HOST_BLUEDROID_APP_HCI_H_ */ diff --git a/components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/iso.h b/components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/iso.h new file mode 100644 index 00000000000..4bced9c259c --- /dev/null +++ b/components/bt/esp_ble_iso/host/adapter/bluedroid/include/bluedroid/iso.h @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef HOST_BLUEDROID_ISO_H_ +#define HOST_BLUEDROID_ISO_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct net_buf; + +int bt_le_bluedroid_hci_iso_cmd_send_sync(uint16_t opcode, + struct net_buf *buf, + struct net_buf **rsp); + +int bt_le_bluedroid_iso_init(void); + +void bt_le_bluedroid_iso_deinit(void); + +#ifdef __cplusplus +} +#endif + +#endif /* HOST_BLUEDROID_ISO_H_ */ diff --git a/components/bt/esp_ble_iso/host/adapter/bluedroid/iso.c b/components/bt/esp_ble_iso/host/adapter/bluedroid/iso.c new file mode 100644 index 00000000000..e9c7bd94912 --- /dev/null +++ b/components/bt/esp_ble_iso/host/adapter/bluedroid/iso.c @@ -0,0 +1,1276 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include + +#include <../host/conn_internal.h> +#include <../host/iso_internal.h> + +#include "stack/hcimsgs.h" +#include "stack/btm_ble_api.h" + +#include "bluedroid/btm_error.h" +#include "bluedroid/hci.h" + +#include "common/host.h" + +LOG_MODULE_REGISTER(ISO_BISO, CONFIG_BT_ISO_LOG_LEVEL); + +/* All adapter entrypoints (hci_cmd_* helpers, bt_le_bluedroid_iso_*) are + * invoked from the iso task — single caller. Neither the DIRECT_HCI + * 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. + * + * 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 + * sem; the caller takes the sem and reads the buffers. Under + * USE_DIRECT_HCI=1 (default) BTU emits no synthesized + * SET_CIG_PARAMS_EVT / READ_TX_SYNC_EVT for our cmds (they bypass + * BTM), so iso_sem is created but never given/taken. + * + * Lock order on the BTM fallback path: + * bt_le_host_lock → k_sem_take(iso_sem) + * iso_evt_rx runs on BTU and MUST NOT acquire bt_le_host_lock — the + * blocked taker still holds it. + * + * iso_sem timeout (K_SEM_SHORT) is fatal: the caller sets + * status = BTM_ERR_PROCESSING and does NOT read the response (memset- + * 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 struct { + uint8_t status; + uint16_t conn_handle; + uint16_t packet_seq_num; + uint32_t tx_time_stamp; + uint32_t time_offset; +} tx_sync; + +static struct { + uint8_t status; + uint8_t cig_id; + uint8_t cis_count; + uint16_t cis_handle[CONFIG_BT_ISO_MAX_CHAN]; +} set_cig_params; + +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); + + conn_handle = sys_get_le16(buf->data + 3); + + memset(&tx_sync, 0, sizeof(tx_sync)); + +#if USE_DIRECT_HCI + /* Return params layout: Connection_Handle(2) | Packet_Seq_Num(2) | + * TX_Time_Stamp(4) | Time_Offset(3, 24-bit). Mirror BTM mask of + * high bits on the 16-bit / 24-bit fields. */ + uint8_t rsp_buf[11]; + + status = bt_le_bluedroid_hci_send_sync(HCI_BLE_ISO_READ_TX_SYNC, + buf->data + 3, 2, + rsp_buf, sizeof(rsp_buf)); + if (status == BTM_SUCCESS) { + tx_sync.conn_handle = sys_get_le16(rsp_buf) & 0x0FFF; + 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); + } +#else /* USE_DIRECT_HCI */ + bt_le_host_lock(); + status = BTM_BleIsoReadTxSync(conn_handle); + if (status != BTM_SUCCESS) { + LOG_ERR("[B]RdIsoTxSyncFail[0x%03x][%02x]", conn_handle, status); + } else if (k_sem_take(&iso_sem, K_SEM_SHORT) != 0) { + LOG_ERR("[B]RdIsoTxSyncRspTimeout[0x%03x]", conn_handle); + status = BTM_ERR_PROCESSING; + } else { + assert(tx_sync.conn_handle == conn_handle); + status = tx_sync.status; + } + bt_le_host_unlock(); +#endif /* USE_DIRECT_HCI */ + + /* On failure return BTM status verbatim and leave *rsp NULL so the caller + * short-circuits via bluedroid_err_to_errno() before parsing rsp — BTM + * encodes errors as BTM_HCI_ERROR|hci_status, which is not a valid HCI + * status byte. Mirrors NimBLE adapter pattern. */ + if (status != BTM_SUCCESS) { + net_buf_unref(buf); + *rsp = NULL; + return status; + } + + net_buf_reset(buf); + net_buf_add_u8(buf, status); + net_buf_add_le16(buf, conn_handle); + net_buf_add_le16(buf, tx_sync.packet_seq_num); + net_buf_add_le32(buf, tx_sync.tx_time_stamp); + net_buf_add_le24(buf, tx_sync.time_offset); + + *rsp = buf; + + return 0; +} + +static int hci_cmd_set_cig_params(struct net_buf *buf, struct net_buf **rsp) +{ + tBTM_STATUS status; + uint8_t cis_count; + uint8_t cig_id; + + assert(rsp); + + cig_id = buf->data[3]; + cis_count = buf->data[17]; + + memset(&set_cig_params, 0, sizeof(set_cig_params)); + +#if USE_DIRECT_HCI + /* Forward lib-constructed params verbatim. Return params layout: + * CIG_ID(1) | CIS_Count(1) | Connection_Handle[CIS_Count](2*N). */ + uint8_t rsp_buf[2 + 2 * CONFIG_BT_ISO_MAX_CHAN]; + uint8_t rsp_cis_count; + + status = bt_le_bluedroid_hci_send_sync(HCI_BLE_ISO_SET_CIG_PARAMS, + buf->data + 3, buf->data[2], + rsp_buf, sizeof(rsp_buf)); + if (status == BTM_SUCCESS) { + set_cig_params.cig_id = rsp_buf[0]; + rsp_cis_count = rsp_buf[1]; + if (rsp_cis_count > CONFIG_BT_ISO_MAX_CHAN) { + LOG_ERR("[B]CigParamsCisCountOverflow[%u>%d]", + rsp_cis_count, CONFIG_BT_ISO_MAX_CHAN); + rsp_cis_count = CONFIG_BT_ISO_MAX_CHAN; + } + set_cig_params.cis_count = rsp_cis_count; + 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); + } +#else /* USE_DIRECT_HCI */ + struct ble_hci_le_cis_params *cis_params; + uint32_t sdu_interval_c_to_p; + uint32_t sdu_interval_p_to_c; + uint8_t worst_case_sca; + uint16_t mtl_c_to_p; + uint16_t mtl_p_to_c; + uint8_t packing; + uint8_t framing; + + sdu_interval_c_to_p = sys_get_le24(buf->data + 4); + sdu_interval_p_to_c = sys_get_le24(buf->data + 7); + worst_case_sca = buf->data[10]; + packing = buf->data[11]; + framing = buf->data[12]; + 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); + + 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)]; + cis_params[i].max_sdu_c_to_p = sys_get_le16(buf->data + 19 + i * sizeof(struct ble_hci_le_cis_params)); + cis_params[i].max_sdu_p_to_c = sys_get_le16(buf->data + 21 + i * sizeof(struct ble_hci_le_cis_params)); + cis_params[i].phy_c_to_p = buf->data[23 + i * sizeof(struct ble_hci_le_cis_params)]; + cis_params[i].phy_p_to_c = buf->data[24 + i * sizeof(struct ble_hci_le_cis_params)]; + cis_params[i].rtn_c_to_p = buf->data[25 + i * sizeof(struct ble_hci_le_cis_params)]; + cis_params[i].rtn_p_to_c = buf->data[26 + i * sizeof(struct ble_hci_le_cis_params)]; + } + + bt_le_host_lock(); + status = BTM_BleSetCigParams(cig_id, + sdu_interval_c_to_p, + sdu_interval_p_to_c, + worst_case_sca, + packing, + framing, + mtl_c_to_p, + mtl_p_to_c, + cis_count, + (void *)cis_params); + if (status != BTM_SUCCESS) { + LOG_ERR("[B]SetCigParamsFail[%u][%02x]", cig_id, status); + } else if (k_sem_take(&iso_sem, K_SEM_SHORT) != 0) { + 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); + status = set_cig_params.status; + if (status) { + LOG_ERR("[B]SetCigParamsCtrlFail[%u][%02x]", cig_id, status); + } + } + bt_le_host_unlock(); + free(cis_params); +#endif /* USE_DIRECT_HCI */ + + /* See hci_cmd_read_iso_tx_sync — same fail-path contract. */ + if (status != BTM_SUCCESS) { + net_buf_unref(buf); + *rsp = NULL; + return status; + } + + net_buf_reset(buf); + net_buf_add_u8(buf, status); + net_buf_add_u8(buf, cig_id); + net_buf_add_u8(buf, cis_count); + for (size_t i = 0; i < cis_count; i++) { + net_buf_add_le16(buf, set_cig_params.cis_handle[i]); + } + + *rsp = buf; + + return 0; +} + +static int hci_cmd_set_cig_params_test(struct net_buf *buf, struct net_buf **rsp) +{ + tBTM_STATUS status; + uint8_t cis_count; + uint8_t cig_id; + + assert(rsp); + + cig_id = buf->data[3]; + cis_count = buf->data[17]; + + memset(&set_cig_params, 0, sizeof(set_cig_params)); + +#if USE_DIRECT_HCI + /* Return params layout matches SET_CIG_PARAMS: + * CIG_ID(1) | CIS_Count(1) | Connection_Handle[CIS_Count](2*N). */ + uint8_t rsp_buf[2 + 2 * CONFIG_BT_ISO_MAX_CHAN]; + uint8_t rsp_cis_count; + + status = bt_le_bluedroid_hci_send_sync(HCI_BLE_ISO_SET_CIG_PARAMS_TEST, + buf->data + 3, buf->data[2], + rsp_buf, sizeof(rsp_buf)); + if (status == BTM_SUCCESS) { + set_cig_params.cig_id = rsp_buf[0]; + rsp_cis_count = rsp_buf[1]; + if (rsp_cis_count > CONFIG_BT_ISO_MAX_CHAN) { + LOG_ERR("[B]CigParamsCisCountOverflow[%u>%d]", + rsp_cis_count, CONFIG_BT_ISO_MAX_CHAN); + rsp_cis_count = CONFIG_BT_ISO_MAX_CHAN; + } + set_cig_params.cis_count = rsp_cis_count; + 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); + } +#else /* USE_DIRECT_HCI */ + struct ble_hci_le_cis_params_test *cis_params; + uint32_t sdu_interval_c_to_p; + uint32_t sdu_interval_p_to_c; + uint8_t worst_case_sca; + uint16_t iso_interval; + uint8_t ft_c_to_p; + uint8_t ft_p_to_c; + uint8_t packing; + uint8_t framing; + + sdu_interval_c_to_p = sys_get_le24(buf->data + 4); + sdu_interval_p_to_c = sys_get_le24(buf->data + 7); + ft_c_to_p = buf->data[10]; + ft_p_to_c = buf->data[11]; + iso_interval = sys_get_le16(buf->data + 12); + worst_case_sca = buf->data[14]; + 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); + + 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)]; + cis_params[i].nse = buf->data[19 + i * sizeof(struct ble_hci_le_cis_params_test)]; + cis_params[i].max_sdu_c_to_p = sys_get_le16(buf->data + 20 + i * sizeof(struct ble_hci_le_cis_params_test)); + cis_params[i].max_sdu_p_to_c = sys_get_le16(buf->data + 22 + i * sizeof(struct ble_hci_le_cis_params_test)); + cis_params[i].max_pdu_c_to_p = sys_get_le16(buf->data + 24 + i * sizeof(struct ble_hci_le_cis_params_test)); + cis_params[i].max_pdu_p_to_c = sys_get_le16(buf->data + 26 + i * sizeof(struct ble_hci_le_cis_params_test)); + cis_params[i].phy_c_to_p = buf->data[28 + i * sizeof(struct ble_hci_le_cis_params_test)]; + cis_params[i].phy_p_to_c = buf->data[29 + i * sizeof(struct ble_hci_le_cis_params_test)]; + cis_params[i].bn_c_to_p = buf->data[30 + i * sizeof(struct ble_hci_le_cis_params_test)]; + cis_params[i].bn_p_to_c = buf->data[31 + i * sizeof(struct ble_hci_le_cis_params_test)]; + } + + bt_le_host_lock(); + status = BTM_BleSetCigParamsTest(cig_id, + sdu_interval_c_to_p, + sdu_interval_p_to_c, + ft_c_to_p, + ft_p_to_c, + iso_interval, + worst_case_sca, + packing, + framing, + cis_count, + (void *)cis_params); + if (status != BTM_SUCCESS) { + LOG_ERR("[B]SetCigParamsTestFail[%u][%02x]", cig_id, status); + } else if (k_sem_take(&iso_sem, K_SEM_SHORT) != 0) { + 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); + status = set_cig_params.status; + if (status) { + LOG_ERR("[B]SetCigParamsTestCtrlFail[%u][%02x]", cig_id, status); + } + } + bt_le_host_unlock(); + free(cis_params); +#endif /* USE_DIRECT_HCI */ + + /* See hci_cmd_read_iso_tx_sync — same fail-path contract. */ + if (status != BTM_SUCCESS) { + net_buf_unref(buf); + *rsp = NULL; + return status; + } + + net_buf_reset(buf); + net_buf_add_u8(buf, status); + net_buf_add_u8(buf, cig_id); + net_buf_add_u8(buf, cis_count); + for (size_t i = 0; i < cis_count; i++) { + net_buf_add_le16(buf, set_cig_params.cis_handle[i]); + } + + *rsp = buf; + + return 0; +} + +static int hci_cmd_create_cis(struct net_buf *buf, struct net_buf **rsp) +{ + struct ble_hci_cis_hdls *cis_params; + tBTM_STATUS status; + uint8_t cis_count; + + ARG_UNUSED(rsp); + + cis_count = buf->data[3]; + + cis_params = calloc(1, cis_count * sizeof(struct ble_hci_cis_hdls)); + 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].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. */ + status = BTM_BleCreateCis(cis_count, (void *)cis_params); + + net_buf_unref(buf); + free(cis_params); + + return status; +} + +static int hci_cmd_remove_cig(struct net_buf *buf, struct net_buf **rsp) +{ + tBTM_STATUS status; + + ARG_UNUSED(rsp); + +#if USE_DIRECT_HCI + status = bt_le_bluedroid_hci_send_sync(HCI_BLE_ISO_REMOVE_CIG, + buf->data + 3, 1, NULL, 0); +#else /* USE_DIRECT_HCI */ + uint8_t cig_id = buf->data[3]; + status = BTM_BleRemoveCig(cig_id); +#endif /* USE_DIRECT_HCI */ + + net_buf_unref(buf); + + return status; +} + +static int hci_cmd_accept_cis_req(struct net_buf *buf, struct net_buf **rsp) +{ + uint16_t cis_handle; + tBTM_STATUS status; + + ARG_UNUSED(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. */ + status = BTM_BleAcceptCisReq(cis_handle); + + net_buf_unref(buf); + + return status; +} + +static int hci_cmd_reject_cis_req(struct net_buf *buf, struct net_buf **rsp) +{ + tBTM_STATUS status; + + ARG_UNUSED(rsp); + +#if USE_DIRECT_HCI + status = bt_le_bluedroid_hci_send_sync(HCI_BLE_ISO_REJECT_CIS_REQ, + buf->data + 3, 3, NULL, 0); +#else /* USE_DIRECT_HCI */ + uint16_t cis_handle = sys_get_le16(buf->data + 3); + uint8_t reason = buf->data[5]; + status = BTM_BleRejectCisReq(cis_handle, reason); +#endif /* USE_DIRECT_HCI */ + + net_buf_unref(buf); + + return status; +} + +static int hci_cmd_create_big(struct net_buf *buf, struct net_buf **rsp) +{ + uint8_t big_handle; + uint8_t adv_handle; + uint8_t num_bis; + uint32_t sdu_interval; + uint16_t max_sdu; + uint16_t mtl; + uint8_t rtn; + uint8_t phy; + uint8_t packing; + uint8_t framing; + uint8_t encryption; + uint8_t *bst_code; + tBTM_STATUS status; + + ARG_UNUSED(rsp); + + big_handle = buf->data[3]; + adv_handle = buf->data[4]; + num_bis = buf->data[5]; + sdu_interval = sys_get_le24(buf->data + 6); + max_sdu = sys_get_le16(buf->data + 9); + mtl = sys_get_le16(buf->data + 11); + rtn = buf->data[13]; + phy = buf->data[14]; + packing = buf->data[15]; + framing = buf->data[16]; + encryption = buf->data[17]; + 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. */ + status = BTM_BleBigCreate(big_handle, + adv_handle, + num_bis, + sdu_interval, + max_sdu, + mtl, + rtn, + phy, + packing, + framing, + encryption, + bst_code); + + net_buf_unref(buf); + + return status; +} + +static int hci_cmd_create_big_test(struct net_buf *buf, struct net_buf **rsp) +{ + uint8_t big_handle; + uint8_t adv_handle; + uint8_t num_bis; + uint32_t sdu_interval; + uint16_t iso_interval; + uint16_t max_sdu; + uint16_t max_pdu; + uint8_t nse; + uint8_t phy; + uint8_t packing; + uint8_t framing; + uint8_t bn; + uint8_t irc; + uint8_t pto; + uint8_t encryption; + uint8_t *bst_code; + tBTM_STATUS status; + + ARG_UNUSED(rsp); + + big_handle = buf->data[3]; + adv_handle = buf->data[4]; + num_bis = buf->data[5]; + sdu_interval = sys_get_le24(buf->data + 6); + iso_interval = sys_get_le16(buf->data + 9); + nse = buf->data[11]; + max_sdu = sys_get_le16(buf->data + 12); + max_pdu = sys_get_le16(buf->data + 14); + phy = buf->data[16]; + packing = buf->data[17]; + framing = buf->data[18]; + bn = buf->data[19]; + irc = buf->data[20]; + pto = buf->data[21]; + encryption = buf->data[22]; + 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. */ + status = BTM_BleBigCreateTest(big_handle, + adv_handle, + num_bis, + sdu_interval, + iso_interval, + nse, + max_sdu, + max_pdu, + phy, + packing, + framing, + bn, + irc, + pto, + encryption, + bst_code); + + net_buf_unref(buf); + + return status; +} + +static int hci_cmd_terminate_big(struct net_buf *buf, struct net_buf **rsp) +{ + uint8_t big_handle; + tBTM_STATUS status; + uint8_t reason; + + ARG_UNUSED(rsp); + + big_handle = buf->data[3]; + reason = buf->data[4]; + + /* No direct_hci variant: HCI Terminate BIG returns Command_Status; + * outcome arrives via BTM_BLE_ISO_BIG_TERMINATE_COMPLETE_EVT. */ + status = BTM_BleBigTerminate(big_handle, reason); + + net_buf_unref(buf); + + return status; +} + +static int hci_cmd_big_create_sync(struct net_buf *buf, struct net_buf **rsp) +{ + uint16_t sync_timeout; + uint16_t sync_handle; + tBTM_STATUS status; + uint8_t big_handle; + uint8_t encryption; + uint8_t *bst_code; + uint8_t num_bis; + uint8_t *bis; + uint8_t mse; + + ARG_UNUSED(rsp); + + big_handle = buf->data[3]; + sync_handle = sys_get_le16(buf->data + 4); + encryption = buf->data[6]; + bst_code = buf->data + 7; + mse = buf->data[23]; + sync_timeout = sys_get_le16(buf->data + 24); + num_bis = buf->data[26]; + 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. */ + status = BTM_BleBigSyncCreate(big_handle, + sync_handle, + encryption, + bst_code, + mse, + sync_timeout, + num_bis, + bis); + + net_buf_unref(buf); + + return status; +} + +static int hci_cmd_big_terminate_sync(struct net_buf *buf, struct net_buf **rsp) +{ + uint8_t big_handle; + tBTM_STATUS status; + + assert(rsp); + + big_handle = buf->data[3]; + +#if USE_DIRECT_HCI + status = bt_le_bluedroid_hci_send_sync(HCI_BLE_BIG_TERMINATE_SYNC, + &big_handle, 1, NULL, 0); +#else /* USE_DIRECT_HCI */ + status = BTM_BleBigSyncTerminate(big_handle); +#endif /* USE_DIRECT_HCI */ + + /* See hci_cmd_read_iso_tx_sync — same fail-path contract. */ + if (status != BTM_SUCCESS) { + net_buf_unref(buf); + *rsp = NULL; + return status; + } + + net_buf_reset(buf); + net_buf_add_u8(buf, status); + net_buf_add_u8(buf, big_handle); + + *rsp = buf; + + return 0; +} + +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); + + conn_handle = sys_get_le16(buf->data + 3); + +#if USE_DIRECT_HCI + /* params are variable-length (13 + codec_cfg_len); use lib-supplied + * preamble length byte rather than recomputing. */ + status = bt_le_bluedroid_hci_send_sync(HCI_BLE_ISO_SET_DATA_PATH, + buf->data + 3, buf->data[2], + NULL, 0); +#else /* USE_DIRECT_HCI */ + uint8_t data_path_direction = buf->data[5]; + uint8_t data_path_id = buf->data[6]; + uint8_t coding_format = buf->data[7]; + uint16_t company_id = sys_get_le16(buf->data + 8); + uint16_t vs_codec_id = sys_get_le16(buf->data + 10); + uint32_t controller_delay = sys_get_le24(buf->data + 12); + uint8_t codec_cfg_len = buf->data[15]; + uint8_t *codec_cfg = buf->data + 16; + + /* TODO: this branch is dead today (USE_DIRECT_HCI is hardcoded to 1). + * If re-enabled, BTM_BleIsoSetDataPath is fire-and-forget — its + * return is the queueing status, NOT the controller Command_Complete. + * Real status arrives later via BTM_BLE_ISO_DATA_PATH_UPFATE_EVT in + * iso_evt_rx. Need a data_path static + k_sem_give in the cb + sem + * wait here (mirror tx_sync/set_cig_params pattern) before treating + * `status` as the final result. Same fix applies to + * hci_cmd_remove_iso_data_path below. */ + status = BTM_BleIsoSetDataPath(conn_handle, + data_path_direction, + data_path_id, + coding_format, + company_id, + vs_codec_id, + controller_delay, + codec_cfg_len, + codec_cfg); +#endif /* USE_DIRECT_HCI */ + + /* See hci_cmd_read_iso_tx_sync — same fail-path contract. */ + if (status != BTM_SUCCESS) { + net_buf_unref(buf); + *rsp = NULL; + return status; + } + + net_buf_reset(buf); + net_buf_add_u8(buf, status); + net_buf_add_le16(buf, conn_handle); + + *rsp = buf; + + return 0; +} + +static int hci_cmd_remove_iso_data_path(struct net_buf *buf, struct net_buf **rsp) +{ + uint16_t conn_handle; + tBTM_STATUS status; + + assert(rsp); + + conn_handle = sys_get_le16(buf->data + 3); + +#if USE_DIRECT_HCI + status = bt_le_bluedroid_hci_send_sync(HCI_BLE_ISO_REMOVE_DATA_PATH, + buf->data + 3, 3, NULL, 0); +#else /* USE_DIRECT_HCI */ + uint8_t data_path_direction = buf->data[5]; + status = BTM_BleIsoRemoveDataPath(conn_handle, data_path_direction); +#endif /* USE_DIRECT_HCI */ + + /* See hci_cmd_read_iso_tx_sync — same fail-path contract. */ + if (status != BTM_SUCCESS) { + net_buf_unref(buf); + *rsp = NULL; + return status; + } + + net_buf_reset(buf); + net_buf_add_u8(buf, status); + net_buf_add_le16(buf, conn_handle); + + *rsp = buf; + + return 0; +} + +int bt_le_bluedroid_hci_iso_cmd_send_sync(uint16_t opcode, + struct net_buf *buf, + struct net_buf **rsp) +{ + int rc; + + switch (opcode) { + case BT_HCI_OP_LE_READ_ISO_TX_SYNC: + rc = hci_cmd_read_iso_tx_sync(buf, rsp); + break; + + case BT_HCI_OP_LE_SET_CIG_PARAMS: + rc = hci_cmd_set_cig_params(buf, rsp); + break; + + case BT_HCI_OP_LE_SET_CIG_PARAMS_TEST: + rc = hci_cmd_set_cig_params_test(buf, rsp); + break; + + case BT_HCI_OP_LE_CREATE_CIS: + rc = hci_cmd_create_cis(buf, rsp); + break; + + case BT_HCI_OP_LE_REMOVE_CIG: + rc = hci_cmd_remove_cig(buf, rsp); + break; + + case BT_HCI_OP_LE_ACCEPT_CIS: + rc = hci_cmd_accept_cis_req(buf, rsp); + break; + + case BT_HCI_OP_LE_REJECT_CIS: + rc = hci_cmd_reject_cis_req(buf, rsp); + break; + + case BT_HCI_OP_LE_CREATE_BIG: + rc = hci_cmd_create_big(buf, rsp); + break; + + case BT_HCI_OP_LE_CREATE_BIG_TEST: + rc = hci_cmd_create_big_test(buf, rsp); + break; + + case BT_HCI_OP_LE_TERMINATE_BIG: + rc = hci_cmd_terminate_big(buf, rsp); + break; + + case BT_HCI_OP_LE_BIG_CREATE_SYNC: + rc = hci_cmd_big_create_sync(buf, rsp); + break; + + case BT_HCI_OP_LE_BIG_TERMINATE_SYNC: + rc = hci_cmd_big_terminate_sync(buf, rsp); + break; + + case BT_HCI_OP_LE_SETUP_ISO_PATH: + rc = hci_cmd_setup_iso_data_path(buf, rsp); + break; + + case BT_HCI_OP_LE_REMOVE_ISO_PATH: + rc = hci_cmd_remove_iso_data_path(buf, rsp); + break; + + default: + /* All other case branches consume buf via their helper (net_buf_unref + * inside fire-and-forget helpers, or transparent reuse as response in + * sync helpers). The default path is the only one that returns without + * a helper, so it must unref buf itself — hci_cmd_pool is fixed at + * size 1 (see host/common/hci.c), a single leak permanently exhausts + * the pool and blocks all subsequent HCI commands. */ + LOG_ERR("[B]IsoCmdNotSupp[0x%04x]", opcode); + net_buf_unref(buf); + return -ENOTSUP; + } + + return bluedroid_err_to_errno(rc); +} + +static void iso_evt_handler(tBTM_BLE_ISO_EVENT event, tBTM_BLE_ISO_CB_PARAMS *params) +{ + uint8_t *qdata = NULL; + size_t qdata_len = 0; + int err; + + /* Note: + * For NimBLE Host, the LE meta event structures contain the subevent_code, + * but for Zephyr, the subevent_codes are not included. + * Hence while allocating buffer for holding each event, one more octet + * will be allocated for each LE meta event. + */ + + if (event != BTM_BLE_ISO_BIGINFO_ADV_REPORT_EVT) { + LOG_DBG("[B]ISOEvtRx[%02x]", event); + } + + switch (event) { + case BTM_BLE_ISO_CIS_DISCONNECTED_EVT: { + struct bt_hci_evt_disconn_complete ev = {0}; + + qdata_len = 2 + sizeof(ev); + qdata = calloc(1, qdata_len); + assert(qdata); + + ev.status = 0x00; + ev.handle = params->btm_cis_disconnectd_evt.cis_handle; + ev.reason = params->btm_cis_disconnectd_evt.reason; + + qdata[0] = false; /* Not LE meta event */ + qdata[1] = BT_HCI_EVT_DISCONN_COMPLETE; + memcpy(qdata + 2, &ev, sizeof(ev)); + break; + } + + case BTM_BLE_ISO_CIS_ESTABLISHED_EVT: { + struct bt_hci_evt_le_cis_established ev = {0}; + + qdata_len = 2 + 1 + sizeof(ev); + qdata = calloc(1, qdata_len); + assert(qdata); + + ev.status = params->btm_cis_established_evt.status; + ev.conn_handle = params->btm_cis_established_evt.conn_handle; + sys_put_le24(params->btm_cis_established_evt.cig_sync_delay, ev.cig_sync_delay); + sys_put_le24(params->btm_cis_established_evt.cis_sync_delay, ev.cis_sync_delay); + sys_put_le24(params->btm_cis_established_evt.trans_lat_c_to_p, ev.c_latency); + sys_put_le24(params->btm_cis_established_evt.trans_lat_p_to_c, ev.p_latency); + ev.c_phy = params->btm_cis_established_evt.phy_c_to_p; + ev.p_phy = params->btm_cis_established_evt.phy_p_to_c; + ev.nse = params->btm_cis_established_evt.nse; + ev.c_bn = params->btm_cis_established_evt.bn_c_to_p; + ev.p_bn = params->btm_cis_established_evt.bn_p_to_c; + ev.c_ft = params->btm_cis_established_evt.ft_c_to_p; + ev.p_ft = params->btm_cis_established_evt.ft_p_to_c; + ev.c_max_pdu = params->btm_cis_established_evt.max_pdu_c_to_p; + ev.p_max_pdu = params->btm_cis_established_evt.max_pdu_p_to_c; + ev.interval = params->btm_cis_established_evt.iso_interval; + + qdata[0] = true; /* LE meta event */ + qdata[1] = BT_HCI_EVT_LE_CIS_ESTABLISHED; + qdata[2] = BT_HCI_EVT_LE_CIS_ESTABLISHED; + memcpy(qdata + 3, &ev, sizeof(ev)); + break; + } + + case BTM_BLE_ISO_CIS_REQUEST_EVT: { + struct bt_hci_evt_le_cis_req ev = {0}; + + qdata_len = 2 + 1 + sizeof(ev); + qdata = calloc(1, qdata_len); + assert(qdata); + + ev.acl_handle = params->btm_cis_request_evt.acl_handle; + ev.cis_handle = params->btm_cis_request_evt.cis_handle; + ev.cig_id = params->btm_cis_request_evt.cig_id; + ev.cis_id = params->btm_cis_request_evt.cis_id; + + qdata[0] = true; /* LE meta event */ + qdata[1] = BT_HCI_EVT_LE_CIS_REQ; + qdata[2] = BT_HCI_EVT_LE_CIS_REQ; + memcpy(qdata + 3, &ev, sizeof(ev)); + break; + } + + case BTM_BLE_ISO_BIG_CREATE_COMPLETE_EVT: { + 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); + + ev.status = params->btm_big_cmpl.status; + ev.big_handle = params->btm_big_cmpl.big_handle; + sys_put_le24(params->btm_big_cmpl.big_sync_delay, ev.sync_delay); + sys_put_le24(params->btm_big_cmpl.transport_latency, ev.latency); + ev.phy = params->btm_big_cmpl.phy; + ev.nse = params->btm_big_cmpl.nse; + ev.bn = params->btm_big_cmpl.bn; + ev.pto = params->btm_big_cmpl.pto; + ev.irc = params->btm_big_cmpl.irc; + ev.max_pdu = params->btm_big_cmpl.max_pdu; + ev.iso_interval = params->btm_big_cmpl.iso_interval; + ev.num_bis = params->btm_big_cmpl.num_bis; + + qdata[0] = true; /* LE meta event */ + qdata[1] = BT_HCI_EVT_LE_BIG_COMPLETE; + qdata[2] = BT_HCI_EVT_LE_BIG_COMPLETE; + memcpy(qdata + 3, &ev, sizeof(ev)); + + for (size_t i = 0; i < ev.num_bis; i++) { + sys_put_le16(params->btm_big_cmpl.bis_handle[i], qdata + 3 + sizeof(ev) + i * 2); + } + break; + } + + case BTM_BLE_ISO_BIG_TERMINATE_COMPLETE_EVT: { + struct bt_hci_evt_le_big_terminate ev = {0}; + + qdata_len = 2 + 1 + sizeof(ev); + qdata = calloc(1, qdata_len); + assert(qdata); + + ev.big_handle = params->btm_big_term.big_handle; + ev.reason = params->btm_big_term.reason; + + qdata[0] = true; /* LE meta event */ + qdata[1] = BT_HCI_EVT_LE_BIG_TERMINATE; + qdata[2] = BT_HCI_EVT_LE_BIG_TERMINATE; + memcpy(qdata + 3, &ev, sizeof(ev)); + break; + } + + case BTM_BLE_ISO_BIG_SYNC_ESTABLISHED_EVT: { + 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); + + ev.status = params->btm_big_sync_estab.status; + ev.big_handle = params->btm_big_sync_estab.big_handle; + sys_put_le24(params->btm_big_sync_estab.transport_latency_big, ev.latency); + ev.nse = params->btm_big_sync_estab.nse; + ev.bn = params->btm_big_sync_estab.bn; + ev.pto = params->btm_big_sync_estab.pto; + ev.irc = params->btm_big_sync_estab.irc; + ev.max_pdu = params->btm_big_sync_estab.max_pdu; + ev.iso_interval = params->btm_big_sync_estab.iso_interval; + ev.num_bis = params->btm_big_sync_estab.num_bis; + + qdata[0] = true; /* LE meta event */ + qdata[1] = BT_HCI_EVT_LE_BIG_SYNC_ESTABLISHED; + qdata[2] = BT_HCI_EVT_LE_BIG_SYNC_ESTABLISHED; + memcpy(qdata + 3, &ev, sizeof(ev)); + + for (size_t i = 0; i < ev.num_bis; i++) { + sys_put_le16(params->btm_big_sync_estab.bis_handle[i], qdata + 3 + sizeof(ev) + i * 2); + } + break; + } + + case BTM_BLE_ISO_BIG_SYNC_LOST_EVT: { + struct bt_hci_evt_le_big_sync_lost ev = {0}; + + qdata_len = 2 + 1 + sizeof(ev); + qdata = calloc(1, qdata_len); + assert(qdata); + + ev.big_handle = params->btm_big_sync_lost.big_handle; + ev.reason = params->btm_big_sync_lost.reason; + + qdata[0] = true; /* LE meta event */ + qdata[1] = BT_HCI_EVT_LE_BIG_SYNC_LOST; + qdata[2] = BT_HCI_EVT_LE_BIG_SYNC_LOST; + memcpy(qdata + 3, &ev, sizeof(ev)); + break; + } + + case BTM_BLE_ISO_BIGINFO_ADV_REPORT_EVT: { + struct bt_hci_evt_le_biginfo_adv_report ev = {0}; + + qdata_len = 2 + 1 + sizeof(ev); + qdata = calloc(1, qdata_len); + assert(qdata); + + ev.sync_handle = params->btm_biginfo_report.sync_handle; + ev.num_bis = params->btm_biginfo_report.num_bis; + ev.nse = params->btm_biginfo_report.nse; + ev.iso_interval = params->btm_biginfo_report.iso_interval; + ev.bn = params->btm_biginfo_report.bn; + ev.pto = params->btm_biginfo_report.pto; + ev.irc = params->btm_biginfo_report.irc; + ev.max_pdu = params->btm_biginfo_report.max_pdu; + sys_put_le24(params->btm_biginfo_report.sdu_interval, ev.sdu_interval); + ev.max_sdu = params->btm_biginfo_report.max_sdu; + ev.phy = params->btm_biginfo_report.phy; + ev.framing = params->btm_biginfo_report.framing; + ev.encryption = params->btm_biginfo_report.encryption; + + qdata[0] = true; /* LE meta event */ + qdata[1] = BT_HCI_EVT_LE_BIGINFO_ADV_REPORT; + qdata[2] = BT_HCI_EVT_LE_BIGINFO_ADV_REPORT; + memcpy(qdata + 3, &ev, sizeof(ev)); + break; + } + + default: + LOG_WRN("[B]IsoEvtUnexp[%u]", event); + return; + } + + err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_ISO_HCI_EVENT, qdata, qdata_len); + if (err) { + LOG_ERR("[B]IsoPostEvtFail[%d][%02x]", err, event); + free(qdata); + } +} + +/* BTM callback — runs on the BTU task. MUST NOT acquire bt_le_host_lock: + * sync-response callers (read_iso_tx_sync / set_cig_params*) hold host_lock + * while blocked on iso_sem, which is given from this function — taking + * host_lock here would deadlock. The same applies to iso_evt_handler below. + * See file-top "Lock order" notes. */ +static void iso_evt_rx(tBTM_BLE_ISO_EVENT event, tBTM_BLE_ISO_CB_PARAMS *params) +{ + if (event == BTM_BLE_ISO_CIS_DISCONNECTED_EVT || + event == BTM_BLE_ISO_CIS_ESTABLISHED_EVT || + event == BTM_BLE_ISO_CIS_REQUEST_EVT || + event == BTM_BLE_ISO_BIG_CREATE_COMPLETE_EVT || + event == BTM_BLE_ISO_BIG_TERMINATE_COMPLETE_EVT || + event == BTM_BLE_ISO_BIG_SYNC_ESTABLISHED_EVT || + event == BTM_BLE_ISO_BIG_SYNC_LOST_EVT || + event == BTM_BLE_ISO_BIGINFO_ADV_REPORT_EVT) { + iso_evt_handler(event, params); + return; + } + + switch (event) { + case BTM_BLE_ISO_SET_CIG_PARAMS_EVT: { + uint8_t cis_count = params->btm_set_cig_params.cis_count; + + LOG_DBG("[B]SetCigParamsEvt[%u][%u][%u][%u]", + params->status, params->btm_set_cig_params.status, + params->btm_set_cig_params.cig_id, cis_count); + for (size_t i = 0; i < cis_count; i++) { + LOG_DBG("[B]CisHdl[%u][0x%03x]", i, params->btm_set_cig_params.conn_hdl[i]); + } + + /* BTM's source array is sized BLE_ISO_CIS_MAX_COUNT, which can + * exceed CONFIG_BT_ISO_MAX_CHAN per the Static_assert. Runtime + * clamp so the copy below doesn't overrun set_cig_params.cis_handle; + * assert alone is unsafe (NDEBUG no-ops it). Mirrors DIRECT_HCI + * path clamp in hci_cmd_set_cig_params. */ + if (cis_count > CONFIG_BT_ISO_MAX_CHAN) { + LOG_ERR("[B]CigParamsCisCountOverflow[%u>%d]", + cis_count, CONFIG_BT_ISO_MAX_CHAN); + cis_count = CONFIG_BT_ISO_MAX_CHAN; + } + + set_cig_params.status = params->btm_set_cig_params.status; + set_cig_params.cig_id = params->btm_set_cig_params.cig_id; + set_cig_params.cis_count = cis_count; + for (size_t i = 0; i < cis_count; i++) { + set_cig_params.cis_handle[i] = params->btm_set_cig_params.conn_hdl[i]; + } + + k_sem_give(&iso_sem); + break; + } + + case BTM_BLE_ISO_REMOVE_CIG_EVT: + LOG_DBG("[B]RemoveCigEvt[%u][%u][%u]", + params->status, params->btm_remove_cig.status, + params->btm_remove_cig.cig_id); + break; + + case BTM_BLE_ISO_CREATE_CIS_EVT: + LOG_DBG("[B]CreateCisEvt[%u]", params->status); + break; + + case BTM_BLE_ISO_ACCEPT_CIS_REQ_EVT: + LOG_DBG("[B]AcceptCisReqEvt[%u]", params->status); + break; + + case BTM_BLE_ISO_REJECT_CIS_REQ_EVT: + LOG_DBG("[B]RejectCisReqEvt[%u][%u][0x%03x]", + params->status, params->btm_reject_cis_req.status, + params->btm_reject_cis_req.cis_handle); + break; + + case BTM_BLE_ISO_BIG_SYNC_TERMINATE_COMPLETE_EVT: + LOG_DBG("[B]BigSyncTermCmpl[%u][%u][%u]", + params->status, params->btm_big_sync_ter.status, + params->btm_big_sync_ter.big_hdl); + break; + + case BTM_BLE_ISO_DATA_PATH_UPFATE_EVT: + LOG_DBG("[B]DataPathUpdate[%u][%u][%s][0x%03x]", + params->status, params->btm_data_path_update.status, + (params->btm_data_path_update.op_type == BTM_BLE_ISO_DATA_PATH_UNKNOWN ? "unknown" : + (params->btm_data_path_update.op_type == BTM_BLE_ISO_DATA_PATH_SETUP ? "setup" : "remove")), + params->btm_data_path_update.conn_hdl); + break; + + case BTM_BLE_ISO_READ_TX_SYNC_EVT: + LOG_DBG("[B]RdTxSyncEvt[%u][%u][0x%03x]", + params->status, params->btm_read_tx_sync.status, + params->btm_read_tx_sync.conn_hdl); + LOG_DBG("[B]RdTxSyncData[%u][%u][%u]", + params->btm_read_tx_sync.pkt_seq_num, + params->btm_read_tx_sync.tx_time_stamp, + params->btm_read_tx_sync.time_offset); + + tx_sync.status = params->btm_read_tx_sync.status; + tx_sync.conn_handle = params->btm_read_tx_sync.conn_hdl; + tx_sync.packet_seq_num = params->btm_read_tx_sync.pkt_seq_num; + tx_sync.tx_time_stamp = params->btm_read_tx_sync.tx_time_stamp; + tx_sync.time_offset = params->btm_read_tx_sync.time_offset; + + k_sem_give(&iso_sem); + break; + + case BTM_BLE_ISO_READ_LINK_QUALITY_EVT: + LOG_DBG("[B]RdLinkQuality[%u][%u][0x%04x]", + params->status, params->btm_read_link_quality.status, + params->btm_read_link_quality.conn_hdl); + LOG_DBG("[B]RdLqTx[%u][%u][%u][%u]", + params->btm_read_link_quality.tx_unacked_pkts, + params->btm_read_link_quality.tx_flushed_pkts, + params->btm_read_link_quality.tx_last_subevt_pkts, + params->btm_read_link_quality.retransmitted_pkts); + LOG_DBG("[B]RdLqRx[%u][%u][%u]", + params->btm_read_link_quality.crc_error_pkts, + params->btm_read_link_quality.rx_unreceived_pkts, + params->btm_read_link_quality.duplicate_pkts); + break; + + default: + LOG_WRN("[B]IsoEvtRxUnexp[%u]", event); + break; + } +} + +#if CONFIG_BT_ISO_RX +static void iso_pkt_rx(uint8_t *data, uint16_t len) +{ + bt_le_iso_rx(data, len, NULL); +} +#endif /* CONFIG_BT_ISO_RX */ + +#if CONFIG_BT_ISO_UNICAST +static int iso_enable_cis(void) +{ + tBTM_STATUS status; + +#if USE_DIRECT_HCI + /* HCI LE Set Host Feature: Bit_Number(1) | Bit_Value(1). + * bit 32 = LE ISO Channels (Host Support). */ + uint8_t cmd_params[2] = { 32, 1 }; + + status = bt_le_bluedroid_hci_send_sync(HCI_BLE_SET_HOST_FEATURE, + cmd_params, sizeof(cmd_params), + NULL, 0); +#else /* USE_DIRECT_HCI */ + status = BTM_BleSetHostFeature(32, 1); +#endif /* USE_DIRECT_HCI */ + + return bluedroid_err_to_errno(status); +} +#endif /* CONFIG_BT_ISO_UNICAST */ + +int bt_le_bluedroid_iso_init(void) +{ +#if CONFIG_BT_ISO_UNICAST + int err; +#endif /* CONFIG_BT_ISO_UNICAST */ + + k_sem_create(&iso_sem); + +#if USE_DIRECT_HCI + bt_le_bluedroid_hci_init(); +#endif /* USE_DIRECT_HCI */ + + BTM_BleIsoRegisterCallback(iso_evt_rx); + +#if CONFIG_BT_ISO_RX + extern void ble_host_register_rx_iso_data_cb(void *cb); + ble_host_register_rx_iso_data_cb(iso_pkt_rx); +#endif /* CONFIG_BT_ISO_RX */ + +#if CONFIG_BT_ISO_UNICAST + /* Set Connected Isochronous Streams - Host support */ + 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 + * disable_cis step since enable never took effect. */ +#if CONFIG_BT_ISO_RX + ble_host_register_rx_iso_data_cb(NULL); +#endif /* CONFIG_BT_ISO_RX */ + /* iso_evt_rx stays installed: BTM_BleIsoRegisterCallback rejects NULL + * (see deinit) and a retry just re-installs it (no EALREADY guard). */ +#if USE_DIRECT_HCI + bt_le_bluedroid_hci_deinit(); +#endif /* USE_DIRECT_HCI */ + k_sem_delete(&iso_sem); + return err; + } +#endif /* CONFIG_BT_ISO_UNICAST */ + + return 0; +} + +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. */ +#if USE_DIRECT_HCI + { + uint8_t cmd_params[2] = { 32, 0 }; + + bt_le_bluedroid_hci_send_sync(HCI_BLE_SET_HOST_FEATURE, + cmd_params, sizeof(cmd_params), + NULL, 0); + } +#else /* USE_DIRECT_HCI */ + BTM_BleSetHostFeature(32, 0); +#endif /* USE_DIRECT_HCI */ +#endif /* CONFIG_BT_ISO_UNICAST */ + +#if CONFIG_BT_ISO_RX + extern void ble_host_register_rx_iso_data_cb(void *cb); + ble_host_register_rx_iso_data_cb(NULL); +#endif /* CONFIG_BT_ISO_RX */ + + /* BTM_BleIsoRegisterCallback rejects NULL — iso_evt_rx stays installed + * for the Bluedroid stack lifetime (same upstream constraint applies + * to gap_app_cb installed via BTM_BleGapRegisterCallback, which is + * why gap.c provides no deinit counterpart). */ + +#if USE_DIRECT_HCI + bt_le_bluedroid_hci_deinit(); +#endif /* USE_DIRECT_HCI */ + + k_sem_delete(&iso_sem); +} 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 341fd6ff152..b64d6847a0c 100644 --- a/components/bt/esp_ble_iso/host/adapter/nimble/gap.c +++ b/components/bt/esp_ble_iso/host/adapter/nimble/gap.c @@ -217,9 +217,9 @@ void bt_le_nimble_gap_post_event(void *param) break; default: + LOG_WRN("[N]GapPostEvtUnexp[%u]", ev->type); free(qev); - assert(0); - break; + return; } err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GAP_EVENT, qev, sizeof(*qev)); @@ -253,22 +253,39 @@ free: int bt_le_nimble_scan_start(const struct bt_le_scan_param *param, ble_gap_event_fn *cb) { - struct ble_gap_disc_params scan_param = {0}; + struct ble_gap_ext_disc_params uncoded = {0}; + int rc; - scan_param.itvl = param->interval; - scan_param.window = param->window; - scan_param.filter_policy = 0; - scan_param.limited = 0; - scan_param.passive = !param->type; - scan_param.filter_duplicates = 0; + LOG_DBG("[N]ScanStart[%u][%u][%u]", param->type, param->interval, param->window); - return nimble_err_to_errno(ble_gap_disc(BLE_OWN_ADDR_PUBLIC, BLE_HS_FOREVER, - &scan_param, cb, NULL)); + uncoded.itvl = param->interval; + uncoded.window = param->window; + uncoded.passive = !param->type; + + /* LE Audio sources broadcast via extended advertising; legacy + * ble_gap_disc would miss them. Uncoded-only mirrors the Bluedroid + * side which sets BTM_BLE_GAP_EXT_SCAN_UNCODE_MASK. */ + rc = ble_gap_ext_disc(BLE_OWN_ADDR_PUBLIC, 0, 0, 0, 0, 0, + &uncoded, NULL, cb, NULL); + if (rc) { + LOG_ERR("[N]ScanStartFail[%d]", rc); + } + + return nimble_err_to_errno(rc); } int bt_le_nimble_scan_stop(void) { - return nimble_err_to_errno(ble_gap_disc_cancel()); + int rc; + + LOG_DBG("[N]ScanStop"); + + rc = ble_gap_disc_cancel(); + if (rc) { + LOG_ERR("[N]ScanStopFail[%d]", rc); + } + + return nimble_err_to_errno(rc); } int bt_le_nimble_iso_disconnect(uint16_t conn_handle, uint8_t reason) 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 8fe8311ef1f..4c582efbf64 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 @@ -95,9 +95,9 @@ void bt_le_nimble_gatt_post_event(void *param) break; default: + LOG_WRN("[N]GattPostEvtUnexp[%u]", ev->type); free(qev); - assert(0); - break; + return; } err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GATT_EVENT, qev, sizeof(*qev)); @@ -209,7 +209,7 @@ static void handle_gattc_notify_rx_event_safe(struct bt_le_gattc_notify_rx_event conn = bt_le_acl_conn_find(event->conn_handle); if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_ERR("[N]NotConn[%d]", __LINE__); + LOG_INF("[N]GattcNtfRxNotConn[%u][%u]", event->conn_handle, BT_CONN_STATE_GET(conn)); goto end; } @@ -222,6 +222,12 @@ static void handle_gattc_notify_rx_event_safe(struct bt_le_gattc_notify_rx_event if (params->ccc_handle != BT_GATT_AUTO_DISCOVER_CCC_HANDLE && params->value_handle == event->attr_handle) { if (params->notify) { + /* Return value intentionally ignored. Zephyr unsubscribes on + * BT_GATT_ITER_STOP, but lib clients return STOP on a single + * malformed/short notify (e.g. unicast_client_cp_notify); + * 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); } } @@ -446,6 +452,16 @@ int bt_le_nimble_gattc_discover(struct bt_conn *conn, struct bt_gatt_discover_pa LOG_DBG("[N]GattcDisc[%u]", conn->handle); if (params->uuid) { + /* Downstream gattc_db_disc_*_by_uuid takes ble_uuid16_t — reject + * non-16-bit caller UUIDs explicitly. BT_UUID_16 would otherwise + * reinterpret a 32/128-bit struct and feed garbage to the lookup. + * LE Audio profiles only use SIG-assigned 16-bit UUIDs, so this + * gate stays a compile-time-style guard in practice. */ + if (params->uuid->type != BT_UUID_TYPE_16) { + LOG_ERR("[N]DiscNon16BitUuid[%u]", params->uuid->type); + return -ENOTSUP; + } + uuid.u.type = BLE_UUID_TYPE_16; uuid.value = BT_UUID_16(params->uuid)->val; } 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 1881ea1078b..7e28339e5aa 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 @@ -529,9 +529,11 @@ static void gattc_db_disc(struct gattc_db *adb, uint8_t status) * to the upper layer. */ if (status == DISC_FAIL) { + uint16_t conn_handle = adb->conn_handle; + gattc_db_del(adb); - bt_le_nimble_gatt_post_disc_cmpl_event(adb->conn_handle, 0x01); + bt_le_nimble_gatt_post_disc_cmpl_event(conn_handle, 0x01); return; } @@ -553,9 +555,11 @@ static void gattc_db_disc(struct gattc_db *adb, uint8_t status) * we will delete the gatt client database and post the * discovery completion event. */ + uint16_t conn_handle = adb->conn_handle; + gattc_db_del(adb); - bt_le_nimble_gatt_post_disc_cmpl_event(adb->conn_handle, 0x01); + bt_le_nimble_gatt_post_disc_cmpl_event(conn_handle, 0x01); break; } @@ -590,8 +594,8 @@ static int gattc_db_disc_all_svcs_cb_safe(uint16_t conn_handle, case 0: assert(svc); - LOG_DBG("[N]GattcDbDiscAllSvcs[%s][%u][%u]", - audio_svc_uuid_to_str(svc->uuid.u16.value), + LOG_DBG("[N]GattcDbDiscAllSvcs[0x%04x][%u][%u]", + svc->uuid.u16.value, svc->start_handle, svc->end_handle); gattc_db_svc_insert(adb, svc); @@ -637,7 +641,7 @@ static int gattc_db_find_inc_svcs_cb_safe(uint16_t conn_handle, adb = gattc_db_find(conn_handle); if (adb == NULL) { - LOG_ERR("[N]GattcDbNotFound"); + LOG_WRN("[N]GattcDbFindIncSvcsCbNotFound"); rc = -ENODEV; goto end; } @@ -646,8 +650,8 @@ static int gattc_db_find_inc_svcs_cb_safe(uint16_t conn_handle, case 0: assert(svc); - LOG_DBG("[N]GattcDbFindIncSvcs[%s][%u][%u]", - audio_svc_uuid_to_str(svc->uuid.u16.value), + LOG_DBG("[N]GattcDbFindIncSvcs[0x%04x][%u][%u]", + svc->uuid.u16.value, svc->start_handle, svc->end_handle); gattc_db_inc_svc_insert(asvc, svc); @@ -725,7 +729,7 @@ static int gattc_db_disc_all_inc_chrs_cb_safe(uint16_t conn_handle, adb = gattc_db_find(conn_handle); if (adb == NULL) { - LOG_ERR("[N]GattcDbNotFound"); + LOG_WRN("[N]GattcDbDiscAllIncChrsCbNotFound"); rc = -ENODEV; goto end; } @@ -783,7 +787,7 @@ static int gattc_db_disc_all_chrs_cb_safe(uint16_t conn_handle, adb = gattc_db_find(conn_handle); if (adb == NULL) { - LOG_ERR("[N]GattcDbNotFound"); + LOG_WRN("[N]GattcDbDiscAllChrsCbNotFound"); rc = -ENODEV; goto end; } @@ -843,7 +847,7 @@ static int gattc_db_disc_all_inc_dscs_cb_safe(uint16_t conn_handle, adb = gattc_db_find(conn_handle); if (adb == NULL) { - LOG_ERR("[N]GattcDbNotFound"); + LOG_WRN("[N]GattcDbDiscAllIncDscsCbNotFound"); rc = -ENODEV; goto end; } @@ -906,7 +910,7 @@ static int gattc_db_disc_all_dscs_cb_safe(uint16_t conn_handle, adb = gattc_db_find(conn_handle); if (adb == NULL) { - LOG_ERR("[N]GattcDbNotFound"); + LOG_WRN("[N]GattcDbDiscAllDscsCbNotFound"); rc = -ENODEV; goto end; } @@ -967,7 +971,7 @@ static int gattc_db_enable_notify_cb_safe(uint16_t conn_handle, adb = gattc_db_find(conn_handle); if (adb == NULL) { - LOG_ERR("[N]GattcDbNotFound"); + LOG_WRN("[N]GattcDbEnableNotifyCbNotFound"); rc = -ENODEV; goto end; } @@ -987,10 +991,17 @@ static int gattc_db_enable_notify_cb_safe(uint16_t conn_handle, break; } - /* Mark the CCCD write as completed */ - achrc->cccd_write = 1; - - gattc_db_disc(adb, DISC_SUCCESS); + if (rc) { + /* Terminal failure (ENOTCONN/ETIMEOUT) — bail via DISC_FAIL so the + * next CCCD write isn't queued; it would otherwise sit on NimBLE's + * pending list and only resolve after the 30s ATT timeout, by which + * time the adb has been removed. */ + gattc_db_disc(adb, DISC_FAIL); + } else { + /* Mark the CCCD write as completed */ + achrc->cccd_write = 1; + gattc_db_disc(adb, DISC_SUCCESS); + } end: bt_le_host_unlock(); @@ -1014,7 +1025,7 @@ static int handle_gattc_disc_svc_by_uuid(struct bt_conn *conn, ble_uuid16_t *uui adb = gattc_db_find(conn->handle); if (adb == NULL) { - LOG_ERR("[N]GattcDbNotFound"); + LOG_WRN("[N]GattcDiscSvcByUuidNotFound"); goto end; } @@ -1068,7 +1079,7 @@ static int handle_gattc_find_inc_svcs(struct bt_conn *conn, adb = gattc_db_find(conn->handle); if (adb == NULL) { - LOG_ERR("[N]GattcDbNotFound"); + LOG_WRN("[N]GattcFindIncSvcsNotFound"); goto end; } @@ -1087,6 +1098,9 @@ static int handle_gattc_find_inc_svcs(struct bt_conn *conn, inc_svc.start_handle = ainc_svc->svc.start_handle; inc_svc.end_handle = ainc_svc->svc.end_handle; + /* Include declaration's own attribute type — same rationale + * as the Bluedroid side: caller may dereference attr->uuid. */ + attr.uuid = BT_UUID_GATT_INCLUDE; attr.user_data = &inc_svc; /* TODO: * When CONFIG_BT_NIMBLE_INCL_SVC_DISCOVERY is enabled, use ainc_svc->attr_handle here. @@ -1158,7 +1172,7 @@ static int handle_gattc_disc_chrs(struct bt_conn *conn, ble_uuid16_t *uuid, adb = gattc_db_find(conn->handle); if (adb == NULL) { - LOG_ERR("[N]GattcDbNotFound"); + LOG_WRN("[N]GattcDiscChrsNotFound"); goto end; } @@ -1234,7 +1248,7 @@ void handle_gattc_db_disc_event_safe(struct bt_le_gattc_discover_event *event) conn = bt_le_acl_conn_find(event->conn_handle); if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_ERR("[N]GattcDbNotConn[%d]", __LINE__); + LOG_INF("[N]GattcDbDiscNotConn[%u][%u]", event->conn_handle, BT_CONN_STATE_GET(conn)); goto end; } @@ -1341,7 +1355,7 @@ static int handle_gattc_disc_all_dscs(struct bt_conn *conn, adb = gattc_db_find(conn->handle); if (adb == NULL) { - LOG_ERR("[N]GattcDbNotFound"); + LOG_WRN("[N]GattcDiscAllDscsNotFound"); rc = -ENODEV; goto end; } 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 d21413d9728..1561afef952 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 @@ -117,7 +117,7 @@ static int gattc_nrp_read_by_uuid_cb_safe(uint16_t conn_handle, conn = bt_le_acl_conn_find(conn_handle); if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_ERR("[N]GattcNrpNotConn[%d]", __LINE__); + LOG_INF("[N]GattcNrpRdByUuidNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); rc = -ENOTCONN; goto end; } @@ -207,7 +207,7 @@ static int gattc_nrp_read_long_cb_safe(uint16_t conn_handle, conn = bt_le_acl_conn_find(conn_handle); if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_ERR("[N]GattcNrpNotConn[%d]", __LINE__); + LOG_INF("[N]GattcNrpRdLongNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); rc = -ENOTCONN; goto end; } @@ -287,7 +287,7 @@ static int gattc_nrp_read_single_cb_safe(uint16_t conn_handle, conn = bt_le_acl_conn_find(conn_handle); if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_ERR("[N]GattcNrpNotConn[%d]", __LINE__); + LOG_INF("[N]GattcNrpRdSingleNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); rc = -ENOTCONN; goto end; } @@ -404,7 +404,7 @@ static int gattc_nrp_write_cb_safe(uint16_t conn_handle, conn = bt_le_acl_conn_find(conn_handle); if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_ERR("[N]GattcNrpNotConn[%d]", __LINE__); + LOG_INF("[N]GattcNrpWrNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); rc = -ENOTCONN; goto end; } @@ -480,7 +480,7 @@ static int gattc_nrp_subscribe_cb_safe(uint16_t conn_handle, conn = bt_le_acl_conn_find(conn_handle); if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_ERR("[N]GattcNrpNotConn[%d]", __LINE__); + LOG_INF("[N]GattcNrpSubNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); rc = -ENOTCONN; goto end; } @@ -560,7 +560,7 @@ void bt_le_nimble_gatts_nrp_indicate_cb(uint16_t conn_handle, conn = bt_le_acl_conn_find(conn_handle); if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_ERR("[N]GattsNrpNotConn[%d]", __LINE__); + LOG_INF("[N]GattsNrpIndNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); return; } @@ -902,17 +902,22 @@ int bt_le_nimble_gatt_nrp_remove(struct bt_conn *conn, uint8_t type, void *param case GATTS_NRP_INDICATE: 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). + * On match: err propagates the mapped NimBLE status (0 on EDONE, + * else err). On handle mismatch the cb event doesn't correspond + * to this queue head — we cannot report this head's real outcome, + * so fire with BT_ATT_ERR_UNLIKELY to signal failure rather than + * silently passing the wrong op's status (could be 0/success and + * mislead the lib state machine). */ if (nrp_head->indicate.params->attr->handle != ((struct bt_gatt_indicate_params *)params)->attr->handle) { LOG_ERR("[N]GattNrpMismatchIndHdl[%u][%u]", ((struct bt_gatt_indicate_params *)params)->attr->handle, nrp_head->indicate.params->attr->handle); - assert(0); /* Should not happen */ + err = BT_ATT_ERR_UNLIKELY; } - /* 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). - * err propagates the mapped NimBLE status (0 on EDONE, else err). */ nrp_head->indicate.params->func(conn, nrp_head->indicate.params, err); free(nrp_head->indicate.data_copy); diff --git a/components/bt/esp_ble_iso/host/adapter/nimble/include/nimble/iso.h b/components/bt/esp_ble_iso/host/adapter/nimble/include/nimble/iso.h index 06c33a77bd5..89e508fa5c1 100644 --- a/components/bt/esp_ble_iso/host/adapter/nimble/include/nimble/iso.h +++ b/components/bt/esp_ble_iso/host/adapter/nimble/include/nimble/iso.h @@ -13,6 +13,8 @@ extern "C" { #endif +struct net_buf; + int bt_le_nimble_iso_cmd_send_sync(uint16_t opcode, struct net_buf *buf, struct net_buf **rsp); 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 774466622e7..16f75c10a9f 100644 --- a/components/bt/esp_ble_iso/host/adapter/nimble/iso.c +++ b/components/bt/esp_ble_iso/host/adapter/nimble/iso.c @@ -842,15 +842,20 @@ int bt_le_nimble_iso_init(void) { int err; + /* nimble's setters reject NULL and refuse re-registration (EALREADY), and + * expose no unregister path — so deinit cannot clear these. On a host + * re-enable the callback is still our own iso_evt_rx / bt_le_iso_rx, so + * treat EALREADY as success instead of failing the whole init. (evt setter + * returns -BLE_HS_EALREADY, pkt setter returns BLE_HS_EALREADY.) */ err = ble_hs_iso_evt_rx_cb_set(iso_evt_rx); - if (err) { + if (err && err != -BLE_HS_EALREADY) { LOG_ERR("[N]IsoEvtRxCbSetFail[%d]", err); return err; } #if CONFIG_BT_ISO_RX err = ble_hs_iso_pkt_rx_cb_set(bt_le_iso_rx); - if (err) { + if (err && err != BLE_HS_EALREADY) { LOG_ERR("[N]IsoPktRxCbSetFail[%d]", err); return err; } @@ -869,14 +874,11 @@ int bt_le_nimble_iso_init(void) void bt_le_nimble_iso_deinit(void) { - LOG_DBG("IsoDeinit"); - - ble_hs_iso_evt_rx_cb_set(NULL); - -#if CONFIG_BT_ISO_RX - ble_hs_iso_pkt_rx_cb_set(NULL); -#endif /* CONFIG_BT_ISO_RX */ + LOG_DBG("[N]IsoDeinit"); + /* nimble exposes no way to unregister iso_evt_rx / bt_le_iso_rx (the + * setters reject NULL); they harmlessly persist until the next init, + * which tolerates the resulting EALREADY. */ #if CONFIG_BT_ISO_UNICAST iso_disable_cis(); #endif /* CONFIG_BT_ISO_UNICAST */ diff --git a/components/bt/esp_ble_iso/host/adapter/nimble/l2cap.c b/components/bt/esp_ble_iso/host/adapter/nimble/l2cap.c index 4b9ce128f1d..f184dfcae69 100644 --- a/components/bt/esp_ble_iso/host/adapter/nimble/l2cap.c +++ b/components/bt/esp_ble_iso/host/adapter/nimble/l2cap.c @@ -87,7 +87,11 @@ static int ots_l2cap_event_cb(struct ble_l2cap_event *event, void *arg) ots_chan = event->connect.chan; if (ble_l2cap_get_chan_info(event->connect.chan, &chan_info)) { - assert(0); + LOG_ERR("[N]CocGetChanInfoFail"); + /* 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_DBG("[N]CocConnect[%u][%04x][%04x][%04x][%u][%u][%u][%u]", @@ -320,7 +324,7 @@ int bt_le_nimble_l2cap_init(void) void bt_le_nimble_l2cap_deinit(void) { - LOG_DBG("NimbleL2capDeinit"); + LOG_DBG("[N]L2capDeinit"); /* TODO: free the ots_mbuf_pool and ots_mbuf_mempool */ 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 77defe11ac6..75a07d3267c 100644 --- a/components/bt/esp_ble_iso/host/common/app/gap.c +++ b/components/bt/esp_ble_iso/host/common/app/gap.c @@ -517,8 +517,11 @@ 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) { - /* Currently type is not used */ +#if CONFIG_BT_BLUEDROID_ENABLED + /* For Bluedroid, post the typed event to the ISO task instead. */ + bt_le_bluedroid_gap_post_event(type, param); +#else ARG_UNUSED(type); - bt_le_nimble_gap_post_event(param); +#endif } 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 d90b9a75446..36015841dfa 100644 --- a/components/bt/esp_ble_iso/host/common/app/gatt.c +++ b/components/bt/esp_ble_iso/host/common/app/gatt.c @@ -106,6 +106,7 @@ void bt_le_gatts_app_subscribe_event(struct bt_le_gatts_subscribe_event *param) bt_le_gatt_app_cb_evt(&event); } +#if !CONFIG_BT_BLUEDROID_ENABLED void bt_le_gatt_app_post_event(uint8_t type, void *param) { /* Currently type is not used */ @@ -113,3 +114,24 @@ void bt_le_gatt_app_post_event(uint8_t type, void *param) bt_le_nimble_gatt_post_event(param); } +#endif /* !CONFIG_BT_BLUEDROID_ENABLED */ + +#if CONFIG_BT_BLUEDROID_ENABLED +/* Bluedroid-only: GATTC open completion carries the BTA gattc_if, which has + * no NimBLE analogue. MTU and disc-cmpl events reuse the host-agnostic + * helpers above (bt_le_gatt_app_mtu_change_event / bt_le_gattc_app_disc_cmpl_event). */ +void bt_le_gattc_app_open_event(struct bt_le_gattc_open_event *param, uint8_t gattc_if) +{ + struct bt_le_gatt_app_event event = { + .type = BT_LE_GATT_APP_EVENT_GATTC_OPEN, + }; + + LOG_DBG("GattcOpenAppEvt[%u][%02x][%u]", param->conn_handle, param->status, gattc_if); + + event.gattc_open.status = param->status; + event.gattc_open.conn_handle = param->conn_handle; + event.gattc_open.gattc_if = gattc_if; + + bt_le_gatt_app_cb_evt(&event); +} +#endif /* CONFIG_BT_BLUEDROID_ENABLED */ diff --git a/components/bt/esp_ble_iso/host/common/conn.c b/components/bt/esp_ble_iso/host/common/conn.c index 191ebbc4fbc..4a8b940fa37 100644 --- a/components/bt/esp_ble_iso/host/common/conn.c +++ b/components/bt/esp_ble_iso/host/common/conn.c @@ -382,11 +382,12 @@ int bt_le_acl_conn_delete(uint16_t conn_handle) { struct bt_conn *conn; - LOG_DBG("AclConnDel[%u]", conn_handle); + LOG_INF("AclConnDel[%u]", conn_handle); + /* disconnected_listener already flipped state to DISCONNECTED. */ conn = bt_le_acl_conn_find(conn_handle); - if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_ERR("NotConn[%d]", __LINE__); + if (conn == NULL || conn->state != BT_CONN_DISCONNECTED) { + LOG_ERR("AclConnDelNotDisc[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); return -ENOTCONN; } @@ -406,7 +407,7 @@ int bt_le_acl_conn_update(uint16_t conn_handle, conn = bt_le_acl_conn_find(conn_handle); if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_ERR("NotConn[%d]", __LINE__); + LOG_ERR("AclConnUpdNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); return -ENOTCONN; } @@ -543,7 +544,7 @@ int bt_le_acl_conn_connected_listener(uint16_t conn_handle) conn = bt_le_acl_conn_find(conn_handle); if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_ERR("NotConn[%d]", __LINE__); + LOG_ERR("AclConnConnectedListenerNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); return -ENOTCONN; } @@ -566,10 +567,15 @@ int bt_le_acl_conn_disconnected_listener(uint16_t conn_handle, uint8_t reason) conn = bt_le_acl_conn_find(conn_handle); if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_ERR("NotConn[%d]", __LINE__); + LOG_ERR("AclConnDisconnectedListenerNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); return -ENOTCONN; } + /* Flip before dispatch — lib disconnect cbs guard late notifies on + * bt_conn_get_info().state. Otherwise BTA queues the send and the conn + * is gone by the time BTU drains it ("Unknown connection ID"). */ + conn->state = BT_CONN_DISCONNECTED; + SYS_SLIST_FOR_EACH_CONTAINER(&conn_cbs, listener, _node) { if (listener->disconnected) { listener->disconnected(conn, reason); @@ -593,7 +599,7 @@ int bt_le_acl_conn_security_changed_listener(uint16_t conn_handle, bt_security_t conn = bt_le_acl_conn_find(conn_handle); if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_ERR("NotConn[%d]", __LINE__); + LOG_ERR("AclConnSecChgListenerNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); return -ENOTCONN; } @@ -621,7 +627,7 @@ int bt_le_acl_conn_identity_resolved_listener(uint16_t conn_handle, conn = bt_le_acl_conn_find(conn_handle); if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_ERR("NotConn[%d]", __LINE__); + LOG_ERR("AclConnIdResolvedListenerNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); return -ENOTCONN; } @@ -644,7 +650,7 @@ int bt_le_acl_conn_pairing_completed_listener(uint16_t conn_handle, bool bonded) conn = bt_le_acl_conn_find(conn_handle); if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_ERR("NotConn[%d]", __LINE__); + LOG_ERR("AclConnPairingCompletedListenerNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); return -ENOTCONN; } diff --git a/components/bt/esp_ble_iso/host/common/gatt.c b/components/bt/esp_ble_iso/host/common/gatt.c index fbe167c904c..20b082ae2c7 100644 --- a/components/bt/esp_ble_iso/host/common/gatt.c +++ b/components/bt/esp_ble_iso/host/common/gatt.c @@ -100,7 +100,11 @@ uint16_t bt_gatt_get_mtu(struct bt_conn *conn) assert(conn); +#if CONFIG_BT_BLUEDROID_ENABLED + mtu = bt_le_bluedroid_gatt_get_mtu(conn); +#else mtu = bt_le_nimble_gatt_get_mtu(conn); +#endif LOG_DBG("GattGetMtu[%u]", mtu); @@ -141,13 +145,12 @@ uint16_t bt_gatt_attr_get_handle(const struct bt_gatt_attr *attr) { uint16_t handle = 0; - LOG_DBG("GattAttrGetHdl"); - if (attr && attr->handle) { handle = attr->handle; } - LOG_DBG("Hdl[%u]", handle); + LOG_DBG("GattAttrGetHdl[%u]", handle); + return handle; } @@ -162,32 +165,29 @@ uint16_t bt_gatt_attr_value_handle(const struct bt_gatt_attr *attr) * Currently this function is only used by TMAP. */ - LOG_DBG("GattAttrValHdl"); - if (attr) { + LOG_DBG("GattAttrUuid[%s]", attr->uuid ? bt_uuid_str(attr->uuid) : "Null"); + if (attr->uuid == NULL) { handle = (attr->handle + 1); } else { - LOG_DBG("Uuid[%s]", bt_uuid_str(attr->uuid)); - if (bt_uuid_cmp(attr->uuid, BT_UUID_GATT_CHRC) == 0) { struct bt_gatt_chrc *chrc = attr->user_data; assert(chrc); handle = chrc->value_handle; - LOG_DBG("ValHdl[%u]", handle); - if (handle == 0) { /* Fall back to Zephyr value handle policy */ handle = bt_gatt_attr_get_handle(attr) + 1; - LOG_INF("ValHdlNew[%u]", handle); + LOG_INF("GattAttrValHdlNew[%u]", handle); } } } } - LOG_DBG("Hdl[%u]", handle); + LOG_DBG("GattAttrValHdl[%u]", handle); + return handle; } @@ -401,8 +401,13 @@ ssize_t bt_gatt_attr_read(struct bt_conn *conn, const struct bt_gatt_attr *attr, return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); } +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_gatts_attr_read(conn, attr, buf, buf_len, + offset, value, value_len); +#else return bt_le_nimble_gatts_attr_read(conn, attr, buf, buf_len, offset, value, value_len); +#endif } _LIB_ONLY @@ -415,11 +420,15 @@ int bt_gatt_notify_cb(struct bt_conn *conn, struct bt_gatt_notify_params *params LOG_DBG("GattNtfCb[%u]", params->len); if (conn && conn->state != BT_CONN_CONNECTED) { - LOG_ERR("NotConn[%d]", __LINE__); + LOG_ERR("GattNtfNotConn[%u][%u]", conn->handle, conn->state); return -ENOTCONN; } +#if CONFIG_BT_BLUEDROID_ENABLED + err = bt_le_bluedroid_gatts_notify(conn, params); +#else 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 @@ -440,7 +449,7 @@ int bt_gatt_indicate(struct bt_conn *conn, struct bt_gatt_indicate_params *param LOG_DBG("GattInd[%s]", bt_uuid_str(params->attr->uuid)); if (conn && conn->state != BT_CONN_CONNECTED) { - LOG_ERR("NotConn[%d]", __LINE__); + LOG_ERR("GattIndNotConn[%u][%u]", conn->handle, conn->state); return -ENOTCONN; } @@ -454,7 +463,11 @@ int bt_gatt_indicate(struct bt_conn *conn, struct bt_gatt_indicate_params *param return -ENOTSUP; } +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_gatts_indicate(conn, params); +#else return bt_le_nimble_gatts_indicate(conn, params); +#endif } _LIB_IDF @@ -774,7 +787,8 @@ bool bt_gatt_is_subscribed(struct bt_conn *conn, LOG_DBG("GattIsSub[%04x][%s]", ccc_type, bt_uuid_str(attr->uuid)); if (conn->state != BT_CONN_CONNECTED) { - LOG_ERR("NotConn[%d]", __LINE__); + /* Query — disconnected conn is "not subscribed", not an error. */ + LOG_INF("GattIsSubNotConn[%u][%u]", conn->handle, conn->state); return false; } @@ -952,7 +966,7 @@ int bt_gatts_sub_changed(uint16_t conn_handle, conn = bt_le_acl_conn_find(conn_handle); if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_ERR("NotConn[%d]", __LINE__); + LOG_ERR("GattsSubChgNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); return -ENOTCONN; } @@ -975,7 +989,11 @@ int bt_gattc_disc_start_safe(uint16_t conn_handle) int err; LOG_DBG("GattcDiscStart[%u]", conn_handle); bt_le_host_lock(); +#if CONFIG_BT_BLUEDROID_ENABLED + err = bt_le_bluedroid_gattc_disc_start(conn_handle); +#else err = bt_le_nimble_gattc_disc_start(conn_handle); +#endif bt_le_host_unlock(); return err; } @@ -991,7 +1009,7 @@ int bt_gatt_discover(struct bt_conn *conn, struct bt_gatt_discover_params *param LOG_DBG("GattDisc[%u][%u]", params->start_handle, params->end_handle); if (conn->state != BT_CONN_CONNECTED) { - LOG_ERR("NotConn[%d]", __LINE__); + LOG_ERR("GattDiscNotConn[%u][%u]", conn->handle, conn->state); return -ENOTCONN; } @@ -1000,7 +1018,11 @@ int bt_gatt_discover(struct bt_conn *conn, struct bt_gatt_discover_params *param return -ENOTSUP; } +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_gattc_discover(conn, params); +#else return bt_le_nimble_gattc_discover(conn, params); +#endif } static struct gattc_sub *gattc_sub_find(struct bt_conn *conn) @@ -1141,7 +1163,11 @@ static int gattc_write_ccc(struct bt_conn *conn, struct bt_gatt_subscribe_params { LOG_DBG("GattcWrCcc[%u][%04x]", params->ccc_handle, params->value); +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_gattc_write_ccc(conn, params); +#else return bt_le_nimble_gattc_write_ccc(conn, params); +#endif } _LIB_ONLY @@ -1167,7 +1193,7 @@ int bt_gatt_subscribe(struct bt_conn *conn, struct bt_gatt_subscribe_params *par * can resubscribe. */ if (conn->state != BT_CONN_CONNECTED) { - LOG_ERR("NotConn[%d]", __LINE__); + LOG_ERR("GattSubNotConn[%u][%u]", conn->handle, conn->state); params->value_handle = 0; /* unlinked: clear retry guard */ return -ENOTCONN; } @@ -1251,7 +1277,7 @@ int bt_gatt_unsubscribe(struct bt_conn *conn, struct bt_gatt_subscribe_params *p params->value, params->value_handle, params->ccc_handle, params->end_handle); if (conn->state != BT_CONN_CONNECTED) { - LOG_ERR("NotConn[%d]", __LINE__); + LOG_ERR("GattUnsubNotConn[%u][%u]", conn->handle, conn->state); return -ENOTCONN; } @@ -1327,7 +1353,7 @@ int bt_gatt_read(struct bt_conn *conn, struct bt_gatt_read_params *params) LOG_DBG("GattRd[%u]", params->handle_count); if (conn->state != BT_CONN_CONNECTED) { - LOG_ERR("NotConn[%d]", __LINE__); + LOG_ERR("GattRdNotConn[%u][%u]", conn->handle, conn->state); return -ENOTCONN; } @@ -1345,7 +1371,11 @@ int bt_gatt_read(struct bt_conn *conn, struct bt_gatt_read_params *params) } } +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_gattc_read(conn, params); +#else return bt_le_nimble_gattc_read(conn, params); +#endif } _LIB_IDF @@ -1360,7 +1390,7 @@ int bt_gatt_write(struct bt_conn *conn, struct bt_gatt_write_params *params) LOG_DBG("GattWr[%u][%u][%u]", params->handle, params->length, params->offset); if (conn->state != BT_CONN_CONNECTED) { - LOG_ERR("NotConn[%d]", __LINE__); + LOG_ERR("GattWrNotConn[%u][%u]", conn->handle, conn->state); return -ENOTCONN; } @@ -1369,7 +1399,11 @@ int bt_gatt_write(struct bt_conn *conn, struct bt_gatt_write_params *params) return -ENOTSUP; } +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_gattc_write(conn, params); +#else return bt_le_nimble_gattc_write(conn, params); +#endif } _LIB_ONLY @@ -1388,7 +1422,7 @@ int bt_gatt_write_without_response_cb(struct bt_conn *conn, uint16_t handle, LOG_DBG("GattWrCmd[%u][%u][%u]", handle, length, sign); if (conn->state != BT_CONN_CONNECTED) { - LOG_ERR("NotConn[%d]", __LINE__); + LOG_ERR("GattWrCmdNotConn[%u][%u]", conn->handle, conn->state); return -ENOTCONN; } @@ -1397,7 +1431,11 @@ int bt_gatt_write_without_response_cb(struct bt_conn *conn, uint16_t handle, return -ENOTSUP; } +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_gattc_write_without_rsp(conn, handle, data, length); +#else return bt_le_nimble_gattc_write_without_rsp(conn, handle, data, length); +#endif } _LIB_ONLY @@ -1407,6 +1445,7 @@ void bt_le_acl_conn_disconnected_gatt_listener(uint16_t conn_handle) LOG_DBG("AclConnDisconnectedGattListener[%u]", conn_handle); +#if !CONFIG_BT_BLUEDROID_ENABLED bt_le_nimble_gatt_nrp_clear(conn_handle); /* Drop the cached GATT client database so the next connection runs a @@ -1420,10 +1459,12 @@ void bt_le_acl_conn_disconnected_gatt_listener(uint16_t conn_handle) * and Service Changed handling end-to-end. */ bt_le_nimble_gattc_db_remove(conn_handle); +#endif - /* The cached DB is dropped above, so the app will rediscover and call - * bt_gatt_subscribe() again on reconnect with the BAP library's reused - * `bt_gatt_subscribe_params` pointers. Drop those nodes from the + /* On NimBLE the cached DB was dropped above; on Bluedroid no client-side + * DB cache cleanup is performed here. Either way the app will rediscover + * and call bt_gatt_subscribe() again on reconnect with the BAP library's + * reused `bt_gatt_subscribe_params` pointers. Drop those nodes from the * tracking list (and zero their value) so the duplicate check in * bt_gatt_subscribe() doesn't short-circuit with -EALREADY, which * would otherwise skip the CCCD write and starve notifications. @@ -1438,5 +1479,9 @@ void bt_le_gatt_handle_event(uint8_t *data, size_t data_len) { assert(data && data_len); +#if CONFIG_BT_BLUEDROID_ENABLED + bt_le_bluedroid_gatt_handle_event(data, data_len); +#else bt_le_nimble_gatt_handle_event(data, data_len); +#endif } diff --git a/components/bt/esp_ble_iso/host/common/hci.c b/components/bt/esp_ble_iso/host/common/hci.c index b2d1bd1bb68..83e1a4d4d9a 100644 --- a/components/bt/esp_ble_iso/host/common/hci.c +++ b/components/bt/esp_ble_iso/host/common/hci.c @@ -70,5 +70,9 @@ int bt_hci_cmd_send_sync(uint16_t opcode, { LOG_DBG("HciCmdSendSync[%04x]", opcode); +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_hci_iso_cmd_send_sync(opcode, buf, rsp); +#else return bt_le_nimble_iso_cmd_send_sync(opcode, buf, rsp); +#endif } diff --git a/components/bt/esp_ble_iso/host/common/host.c b/components/bt/esp_ble_iso/host/common/host.c index cf5491dee81..127542a3ec7 100644 --- a/components/bt/esp_ble_iso/host/common/host.c +++ b/components/bt/esp_ble_iso/host/common/host.c @@ -17,12 +17,15 @@ #include "common/app/gap.h" #include "common/app/gatt.h" +#if CONFIG_BT_BLUEDROID_ENABLED +#include "bluedroid/gap.h" +#include "bluedroid/gatt.h" +#endif + LOG_MODULE_REGISTER(ISO_HOST, CONFIG_BT_ISO_LOG_LEVEL); static struct k_mutex host_mutex; -#define TIMEOUT_MS (5000 / portTICK_PERIOD_MS) /* 5s */ - #if HOST_LOCK_DEBUG void bt_le_host_lock_debug(const char *func, int line) #else /* HOST_LOCK_DEBUG */ @@ -31,9 +34,9 @@ void bt_le_host_lock(void) { /* LOG_DBG("%s: %d", func, line); */ - int err = k_mutex_lock(&host_mutex, TIMEOUT_MS); + int err = k_mutex_lock(&host_mutex, K_MUTEX_SHORT); if (err) { - /* 5s wait failed: the host stack is wedged. k_mutex_lock has + /* 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 * let the caller enter the critical section without the mutex @@ -84,11 +87,23 @@ int bt_le_host_init(void) } #endif /* CONFIG_BT_OTS || CONFIG_BT_OTS_CLIENT */ - err = bt_le_iso_init(); +#if CONFIG_BT_BLUEDROID_ENABLED + err = bt_le_bluedroid_gap_init(); if (err) { goto deinit_l2cap; } + err = bt_le_bluedroid_gatt_init(); + if (err) { + goto deinit_bluedroid_gatt; + } +#endif /* CONFIG_BT_BLUEDROID_ENABLED */ + + err = bt_le_iso_init(); + if (err) { + goto deinit_bluedroid_gatt; + } + err = bt_le_iso_task_init(); if (err) { goto deinit_iso; @@ -98,7 +113,11 @@ int bt_le_host_init(void) deinit_iso: bt_le_iso_deinit(); +deinit_bluedroid_gatt: +#if CONFIG_BT_BLUEDROID_ENABLED + bt_le_bluedroid_gatt_deinit(); deinit_l2cap: +#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 */ @@ -116,6 +135,9 @@ void bt_le_host_deinit(void) bt_le_iso_task_deinit(); bt_le_iso_deinit(); +#if CONFIG_BT_BLUEDROID_ENABLED + bt_le_bluedroid_gatt_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 */ 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 28cb301fa17..fff7cf2e51d 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 @@ -11,7 +11,11 @@ #include "sdkconfig.h" +#if CONFIG_BT_BLUEDROID_ENABLED +#include "bluedroid/gap.h" +#else #include "nimble/gap.h" +#endif #ifdef __cplusplus extern "C" { diff --git a/components/bt/esp_ble_iso/host/common/include/common/app/gatt.h b/components/bt/esp_ble_iso/host/common/include/common/app/gatt.h index 22c2271a83c..2e9273163f4 100644 --- a/components/bt/esp_ble_iso/host/common/include/common/app/gatt.h +++ b/components/bt/esp_ble_iso/host/common/include/common/app/gatt.h @@ -11,7 +11,11 @@ #include "sdkconfig.h" +#if CONFIG_BT_BLUEDROID_ENABLED +/* ref bluedroid/app/gatt.h was an empty header; nothing to include here. */ +#else #include "nimble/gatt.h" +#endif #ifdef __cplusplus extern "C" { @@ -37,6 +41,14 @@ struct bt_le_gatt_app_event_gatts_subscribe { uint8_t reason; }; +/* Bluedroid-only: surfaces the GATTC open completion (carries the gattc_if + * the BTA dispatcher used, which NimBLE has no analogue of). */ +struct bt_le_gatt_app_event_gattc_open { + uint8_t status; + uint16_t conn_handle; + uint8_t gattc_if; +}; + struct bt_le_gatt_app_event { uint8_t type; @@ -44,6 +56,7 @@ struct bt_le_gatt_app_event { struct bt_le_gatt_app_event_gatt_mtu_change gatt_mtu_change; struct bt_le_gatt_app_event_gattc_disc_cmpl gattc_disc_cmpl; struct bt_le_gatt_app_event_gatts_subscribe gatts_subscribe; + struct bt_le_gatt_app_event_gattc_open gattc_open; }; }; @@ -51,6 +64,7 @@ enum bt_le_gatt_app_event_type { BT_LE_GATT_APP_EVENT_GATT_MTU_CHANGE, BT_LE_GATT_APP_EVENT_GATTC_DISC_CMPL, BT_LE_GATT_APP_EVENT_GATTS_SUBSCRIBE, + BT_LE_GATT_APP_EVENT_GATTC_OPEN, BT_LE_GATT_APP_EVENT_MAX, }; @@ -67,7 +81,18 @@ void bt_le_gattc_app_disc_cmpl_event(struct bt_le_gattc_disc_cmpl_event *param); void bt_le_gatts_app_subscribe_event(struct bt_le_gatts_subscribe_event *param); +#if CONFIG_BT_BLUEDROID_ENABLED +void bt_le_gattc_app_open_event(struct bt_le_gattc_open_event *param, uint8_t gattc_if); +#endif /* CONFIG_BT_BLUEDROID_ENABLED */ + +#if !CONFIG_BT_BLUEDROID_ENABLED +/* NimBLE-only: callers post the raw ble_gap_event so the adapter can + * translate it into the host-agnostic bt_le_gatt_app_event. Bluedroid has + * no analogous app-level event stream (BTA dispatches directly to BTC), + * so this is hidden from the API surface to make misuse a compile error. + */ void bt_le_gatt_app_post_event(uint8_t type, void *param); +#endif /* !CONFIG_BT_BLUEDROID_ENABLED */ #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 7860d8f0928..a924df21942 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 @@ -13,13 +13,21 @@ #include "sdkconfig.h" +#if CONFIG_BT_BLUEDROID_ENABLED +/* TODO */ +#else #include "nimble/gap.h" #include "nimble/iso.h" +#endif #ifdef __cplusplus extern "C" { #endif +/* 0xff is out of bt_conn_state_t range — distinguishes NULL conn from any + * real state in logs. */ +#define BT_CONN_STATE_GET(_c) ((_c) ? (_c)->state : 0xff) + 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); 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 55ac31b74c5..28dc6dc4bbb 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 @@ -14,7 +14,11 @@ #include #include +#if CONFIG_BT_BLUEDROID_ENABLED +#include "bluedroid/gatt.h" +#else #include "nimble/gatt.h" +#endif #ifdef __cplusplus extern "C" { @@ -73,16 +77,115 @@ struct bt_le_gatts_notify_tx_event { uint8_t status; }; +/* Bluedroid-side adapter events. NimBLE produces ACL connect/disconnect via + * the GAP event path (BT_LE_GAP_APP_PARAM_ACL_*), but Bluedroid's BTA + * surfaces these through GATTS/GATTC callbacks. The bluedroid adapter posts + * these typed events into the iso task and translates them into ACL/MTU/ + * discovery events for the LE Audio profile dispatcher. */ + +struct bt_le_addr_simple { + uint8_t type; + uint8_t val[6]; +}; + +struct bt_le_gattc_connect_event { + uint16_t conn_handle; + uint8_t role; + struct bt_le_addr_simple peer; +}; + +struct bt_le_gattc_disconnect_event { + uint16_t conn_handle; + uint8_t reason; +}; + +struct bt_le_gattc_open_event { + uint8_t status; + uint16_t conn_handle; +}; + +struct bt_le_gattc_mtu_event { + uint8_t status; + uint16_t conn_handle; + uint16_t mtu; +}; + +struct bt_le_gattc_read_chrc_event { + uint8_t status; + uint16_t conn_handle; + uint16_t attr_handle; + uint16_t len; + uint8_t *value; +}; + +struct bt_le_gattc_write_chrc_event { + uint8_t status; + uint16_t conn_handle; + uint16_t attr_handle; + uint16_t offset; +}; + +struct bt_le_gatts_connect_event { + uint16_t conn_handle; + uint8_t role; + struct bt_le_addr_simple peer; +}; + +struct bt_le_gatts_disconnect_event { + uint16_t conn_handle; + uint8_t reason; +}; + +struct bt_le_gatts_mtu_event { + uint16_t conn_handle; + uint16_t mtu; +}; + +struct bt_le_gatts_read_event { + uint16_t conn_handle; + uint32_t trans_id; + uint8_t peer[6]; + uint16_t attr_handle; + uint16_t offset; + bool is_long; + bool need_rsp; +}; + +struct bt_le_gatts_write_event { + uint16_t conn_handle; + uint32_t trans_id; + uint8_t peer[6]; + uint16_t attr_handle; + uint16_t offset; + bool is_prep; + bool need_rsp; + uint16_t len; + uint8_t *value; +}; + struct bt_le_gatt_event_param { uint8_t type; union { - struct bt_le_gatt_mtu_change_event gatt_mtu_change; - struct bt_le_gattc_discover_event gattc_discover; - struct bt_le_gattc_disc_cmpl_event gattc_disc_cmpl; - struct bt_le_gatts_subscribe_event gatts_subscribe; - struct bt_le_gattc_notify_rx_event gattc_notify_rx; - struct bt_le_gatts_notify_tx_event gatts_notify_tx; + struct bt_le_gatt_mtu_change_event gatt_mtu_change; + struct bt_le_gattc_discover_event gattc_discover; + struct bt_le_gattc_disc_cmpl_event gattc_disc_cmpl; + struct bt_le_gatts_subscribe_event gatts_subscribe; + struct bt_le_gattc_notify_rx_event gattc_notify_rx; + struct bt_le_gatts_notify_tx_event gatts_notify_tx; + + /* Bluedroid-only paths (see comment above). */ + struct bt_le_gattc_connect_event gattc_connect; + struct bt_le_gattc_disconnect_event gattc_disconnect; + struct bt_le_gattc_open_event gattc_open; + struct bt_le_gattc_mtu_event gattc_mtu; + struct bt_le_gattc_read_chrc_event gattc_read_chrc; + struct bt_le_gattc_write_chrc_event gattc_write_chrc; + struct bt_le_gatts_connect_event gatts_connect; + struct bt_le_gatts_disconnect_event gatts_disconnect; + struct bt_le_gatts_mtu_event gatts_mtu; + struct bt_le_gatts_read_event gatts_read; + struct bt_le_gatts_write_event gatts_write; }; }; @@ -94,231 +197,22 @@ enum { BT_LE_GATTC_NOTIFY_RX_EVENT, BT_LE_GATTS_NOTIFY_TX_EVENT, + /* Bluedroid-only paths (see comment above). */ + BT_LE_GATTC_CONNECT_EVENT, + BT_LE_GATTC_DISCONNECT_EVENT, + BT_LE_GATTC_OPEN_EVENT, + BT_LE_GATTC_MTU_EVENT, + BT_LE_GATTC_READ_CHRC_EVENT, + BT_LE_GATTC_WRITE_CHRC_EVENT, + BT_LE_GATTS_CONNECT_EVENT, + BT_LE_GATTS_DISCONNECT_EVENT, + BT_LE_GATTS_MTU_EVENT, + BT_LE_GATTS_READ_EVENT, + BT_LE_GATTS_WRITE_EVENT, + BT_LE_GATT_EVENT_MAX, }; -static inline char *audio_svc_uuid_to_str(uint16_t uuid) -{ - switch (uuid) { - case BT_UUID_GAP_VAL: - return "GAP"; - case BT_UUID_GATT_VAL: - return "GATT"; - case BT_UUID_AICS_VAL: - return "AICS"; - case BT_UUID_CAS_VAL: - return "CAS"; - case BT_UUID_VCS_VAL: - return "VCS"; - case BT_UUID_VOCS_VAL: - return "VOCS"; - case BT_UUID_CSIS_VAL: - return "CSIS"; - case BT_UUID_MCS_VAL: - return "MCS"; - case BT_UUID_GMCS_VAL: - return "GMCS"; - case BT_UUID_TBS_VAL: - return "TBS"; - case BT_UUID_GTBS_VAL: - return "GTBS"; - case BT_UUID_MICS_VAL: - return "MICS"; - case BT_UUID_ASCS_VAL: - return "ASCS"; - case BT_UUID_BASS_VAL: - return "BASS"; - case BT_UUID_PACS_VAL: - return "PACS"; - case BT_UUID_BASIC_AUDIO_VAL: - return "BASIC_AUDIO"; - case BT_UUID_HAS_VAL: - return "HAS"; - case BT_UUID_TMAS_VAL: - return "TMAS"; - case BT_UUID_PBA_VAL: - return "PBA"; - case BT_UUID_GMAS_VAL: - return "GMAS"; - case BT_UUID_OTS_VAL: - return "OTS"; - default: - return "Unknown"; - } -} - -static inline char *audio_chrc_uuid_to_str(uint16_t uuid) -{ - switch (uuid) { - case BT_UUID_OTS_FEATURE_VAL: - return "OTS_FEATURE"; - case BT_UUID_OTS_NAME_VAL: - return "OTS_NAME"; - case BT_UUID_OTS_TYPE_VAL: - return "OTS_TYPE"; - case BT_UUID_OTS_SIZE_VAL: - return "OTS_SIZE"; - case BT_UUID_OTS_FIRST_CREATED_VAL: - return "OTS_FIRST_CREATED"; - case BT_UUID_OTS_LAST_MODIFIED_VAL: - return "OTS_LAST_MODIFIED"; - case BT_UUID_OTS_ID_VAL: - return "OTS_ID"; - case BT_UUID_OTS_PROPERTIES_VAL: - return "OTS_PROPERTIES"; - case BT_UUID_OTS_ACTION_CP_VAL: - return "OTS_ACTION_CP"; - case BT_UUID_OTS_LIST_CP_VAL: - return "OTS_LIST_CP"; - case BT_UUID_OTS_LIST_FILTER_VAL: - return "OTS_LIST_FILTER"; - case BT_UUID_OTS_CHANGED_VAL: - return "OTS_CHANGED"; - case BT_UUID_GATT_TMAPR_VAL: - return "TMAP_ROLE"; - case BT_UUID_AICS_STATE_VAL: - return "AICS_STATE"; - case BT_UUID_AICS_GAIN_SETTINGS_VAL: - return "AICS_GAIN_SETTINGS"; - case BT_UUID_AICS_INPUT_TYPE_VAL: - return "AICS_INPUT_TYPE"; - case BT_UUID_AICS_INPUT_STATUS_VAL: - return "AICS_INPUT_STATUS"; - case BT_UUID_AICS_CONTROL_VAL: - return "AICS_CONTROL"; - case BT_UUID_AICS_DESCRIPTION_VAL: - return "AICS_DESCRIPTION"; - case BT_UUID_VCS_STATE_VAL: - return "VCS_STATE"; - case BT_UUID_VCS_CONTROL_VAL: - return "VCS_CONTROL"; - case BT_UUID_VCS_FLAGS_VAL: - return "VCS_FLAGS"; - case BT_UUID_VOCS_STATE_VAL: - return "VOCS_STATE"; - case BT_UUID_VOCS_LOCATION_VAL: - return "VOCS_LOCATION"; - case BT_UUID_VOCS_CONTROL_VAL: - return "VOCS_CONTROL"; - case BT_UUID_VOCS_DESCRIPTION_VAL: - return "VOCS_DESCRIPTION"; - case BT_UUID_CSIS_SIRK_VAL: - return "CSIS_SIRK"; - case BT_UUID_CSIS_SET_SIZE_VAL: - return "CSIS_SET_SIZE"; - case BT_UUID_CSIS_SET_LOCK_VAL: - return "CSIS_SET_LOCK"; - case BT_UUID_CSIS_RANK_VAL: - return "CSIS_RANK"; - case BT_UUID_MCS_PLAYER_NAME_VAL: - return "MCS_PLAYER_NAME"; - case BT_UUID_MCS_ICON_OBJ_ID_VAL: - return "MCS_ICON_OBJ_ID"; - case BT_UUID_MCS_ICON_URL_VAL: - return "MCS_ICON_URL"; - case BT_UUID_MCS_TRACK_CHANGED_VAL: - return "MCS_TRACK_CHANGED"; - case BT_UUID_MCS_TRACK_TITLE_VAL: - return "MCS_TRACK_TITLE"; - case BT_UUID_MCS_TRACK_DURATION_VAL: - return "MCS_TRACK_DURATION"; - case BT_UUID_MCS_TRACK_POSITION_VAL: - return "MCS_TRACK_POSITION"; - case BT_UUID_MCS_PLAYBACK_SPEED_VAL: - return "MCS_PLAYBACK_SPEED"; - case BT_UUID_MCS_SEEKING_SPEED_VAL: - return "MCS_SEEKING_SPEED"; - case BT_UUID_MCS_TRACK_SEGMENTS_OBJ_ID_VAL: - return "MCS_TRACK_SEGMENTS_OBJ_ID"; - case BT_UUID_MCS_CURRENT_TRACK_OBJ_ID_VAL: - return "MCS_CURRENT_TRACK_OBJ_ID"; - case BT_UUID_MCS_NEXT_TRACK_OBJ_ID_VAL: - return "MCS_NEXT_TRACK_OBJ_ID"; - case BT_UUID_MCS_PARENT_GROUP_OBJ_ID_VAL: - return "MCS_PARENT_GROUP_OBJ_ID"; - case BT_UUID_MCS_CURRENT_GROUP_OBJ_ID_VAL: - return "MCS_CURRENT_GROUP_OBJ_ID"; - case BT_UUID_MCS_PLAYING_ORDER_VAL: - return "MCS_PLAYING_ORDER"; - case BT_UUID_MCS_PLAYING_ORDERS_VAL: - return "MCS_PLAYING_ORDERS"; - case BT_UUID_MCS_MEDIA_STATE_VAL: - return "MCS_MEDIA_STATE"; - case BT_UUID_MCS_MEDIA_CONTROL_POINT_VAL: - return "MCS_MEDIA_CONTROL_POINT"; - case BT_UUID_MCS_MEDIA_CONTROL_OPCODES_VAL: - return "MCS_MEDIA_CONTROL_OPCODES"; - case BT_UUID_MCS_SEARCH_RESULTS_OBJ_ID_VAL: - return "MCS_SEARCH_RESULTS_OBJ_ID"; - case BT_UUID_MCS_SEARCH_CONTROL_POINT_VAL: - return "MCS_SEARCH_CONTROL_POINT"; - case BT_UUID_TBS_PROVIDER_NAME_VAL: - return "TBS_PROVIDER_NAME"; - case BT_UUID_TBS_UCI_VAL: - return "TBS_UCI"; - case BT_UUID_TBS_TECHNOLOGY_VAL: - return "TBS_TECHNOLOGY"; - case BT_UUID_TBS_URI_LIST_VAL: - return "TBS_URI_LIST"; - case BT_UUID_TBS_SIGNAL_STRENGTH_VAL: - return "TBS_SIGNAL_STRENGTH"; - case BT_UUID_TBS_SIGNAL_INTERVAL_VAL: - return "TBS_SIGNAL_INTERVAL"; - case BT_UUID_TBS_LIST_CURRENT_CALLS_VAL: - return "TBS_LIST_CURRENT_CALLS"; - case BT_UUID_CCID_VAL: - return "CCID"; - case BT_UUID_TBS_STATUS_FLAGS_VAL: - return "TBS_STATUS_FLAGS"; - case BT_UUID_TBS_INCOMING_URI_VAL: - return "TBS_INCOMING_URI"; - case BT_UUID_TBS_CALL_STATE_VAL: - return "TBS_CALL_STATE"; - case BT_UUID_TBS_CALL_CONTROL_POINT_VAL: - return "TBS_CALL_CONTROL_POINT"; - case BT_UUID_TBS_OPTIONAL_OPCODES_VAL: - return "TBS_OPTIONAL_OPCODES"; - case BT_UUID_TBS_TERMINATE_REASON_VAL: - return "TBS_TERMINATE_REASON"; - case BT_UUID_TBS_INCOMING_CALL_VAL: - return "TBS_INCOMING_CALL"; - case BT_UUID_TBS_FRIENDLY_NAME_VAL: - return "TBS_FRIENDLY_NAME"; - case BT_UUID_MICS_MUTE_VAL: - return "MICS_MUTE"; - case BT_UUID_ASCS_ASE_SNK_VAL: - return "ASCS_ASE_SNK"; - case BT_UUID_ASCS_ASE_SRC_VAL: - return "ASCS_ASE_SRC"; - case BT_UUID_ASCS_ASE_CP_VAL: - return "ASCS_ASE_CP"; - case BT_UUID_BASS_CONTROL_POINT_VAL: - return "BASS_CP"; - case BT_UUID_BASS_RECV_STATE_VAL: - return "BASS_RECV_STATE"; - case BT_UUID_PACS_SNK_VAL: - return "PACS_SNK"; - case BT_UUID_PACS_SNK_LOC_VAL: - return "PACS_SNK_LOC"; - case BT_UUID_PACS_SRC_VAL: - return "PACS_SRC"; - case BT_UUID_PACS_SRC_LOC_VAL: - return "PACS_SRC_LOC"; - case BT_UUID_PACS_AVAILABLE_CONTEXT_VAL: - return "PACS_AVAILABLE_CONTEXT"; - case BT_UUID_PACS_SUPPORTED_CONTEXT_VAL: - return "PACS_SUPPORTED_CONTEXT"; - case BT_UUID_HAS_HEARING_AID_FEATURES_VAL: - return "HAS_HEARING_AID_FEATURES"; - case BT_UUID_HAS_PRESET_CONTROL_POINT_VAL: - return "HAS_PRESET_CONTROL_POINT"; - case BT_UUID_HAS_ACTIVE_PRESET_INDEX_VAL: - return "HAS_ACTIVE_PRESET_INDEX"; - default: - return "Unknown"; - } -} - struct gattc_sub { uint8_t id; bt_addr_le_t peer; 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 2c7ff8cf6ca..0e2d81c1fc7 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 @@ -13,12 +13,18 @@ #include "sdkconfig.h" +#if CONFIG_BT_BLUEDROID_ENABLED +#include "bluedroid/iso.h" +#else #include "nimble/iso.h" +#endif #ifdef __cplusplus extern "C" { #endif +struct net_buf; + struct bt_le_iso_cb { void (*cis_dis)(struct net_buf *buf); void (*cis_est)(struct net_buf *buf); 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 index 53cda7ff8ef..f071ab4afdd 100644 --- a/components/bt/esp_ble_iso/host/common/include/common/l2cap.h +++ b/components/bt/esp_ble_iso/host/common/include/common/l2cap.h @@ -13,7 +13,11 @@ #include "sdkconfig.h" +#if CONFIG_BT_BLUEDROID_ENABLED +/* TODO */ +#else #include "nimble/l2cap.h" +#endif #ifdef __cplusplus extern "C" { 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 27aeaedc234..2a1d7b13edf 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 @@ -13,7 +13,11 @@ #include "sdkconfig.h" +#if CONFIG_BT_BLUEDROID_ENABLED +#include "bluedroid/gap.h" +#else #include "nimble/gap.h" +#endif #ifdef __cplusplus extern "C" { 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 92c7ef0cdab..ff00f17d40f 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 @@ -20,18 +20,32 @@ extern "C" { #endif +#if CONFIG_BT_BLUEDROID_ENABLED +#if 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) +#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 #else /* CONFIG_BT_NIMBLE_PINNED_TO_CORE */ #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" /* Ref: + * - Bluedroid BTC task: configMAX_PRIORITIES - 6 + * - Bluedroid BTU task: configMAX_PRIORITIES - 5 * - NimBLE Host task: configMAX_PRIORITIES - 4 */ +#if CONFIG_BT_BLUEDROID_ENABLED +#define ISO_TASK_PRIO (configMAX_PRIORITIES - 5) +#else #define ISO_TASK_PRIO (configMAX_PRIORITIES - 4) +#endif enum iso_queue_item_type { ISO_QUEUE_ITEM_TYPE_TIMER_EVENT, diff --git a/components/bt/esp_ble_iso/host/common/iso.c b/components/bt/esp_ble_iso/host/common/iso.c index 41c5b1834f8..b1047b99a28 100644 --- a/components/bt/esp_ble_iso/host/common/iso.c +++ b/components/bt/esp_ble_iso/host/common/iso.c @@ -25,11 +25,19 @@ LOG_MODULE_REGISTER(ISO_SHIM, CONFIG_BT_ISO_LOG_LEVEL); +#if CONFIG_BT_BLUEDROID_ENABLED +#ifdef CONFIG_BT_BLE_ISO_STD_FLOW_CTRL +#define ISO_STD_FLOW_CTRL true +#else /* CONFIG_BT_BLE_ISO_STD_FLOW_CTRL */ +#define ISO_STD_FLOW_CTRL false +#endif /* CONFIG_BT_BLE_ISO_STD_FLOW_CTRL */ +#else /* CONFIG_BT_BLUEDROID_ENABLED */ #ifdef CONFIG_BT_NIMBLE_ISO_STD_FLOW_CTRL #define ISO_STD_FLOW_CTRL true #else /* CONFIG_BT_NIMBLE_ISO_STD_FLOW_CTRL */ #define ISO_STD_FLOW_CTRL false #endif /* CONFIG_BT_NIMBLE_ISO_STD_FLOW_CTRL */ +#endif /* CONFIG_BT_BLUEDROID_ENABLED */ #define ISO_PKT_FIRST_FRAG (0b00) #define ISO_PKT_CONT_FRAG (0b01) @@ -757,7 +765,11 @@ void bt_le_iso_handle_rx_data(uint8_t *data, size_t data_len) int bt_le_iso_disconnect(uint16_t conn_handle, uint8_t reason) { +#if CONFIG_BT_BLUEDROID_ENABLED + return bt_le_bluedroid_iso_disconnect(conn_handle, reason); +#else return bt_le_nimble_iso_disconnect(conn_handle, reason); +#endif } static void iso_features_set(void) @@ -819,7 +831,11 @@ int bt_le_iso_init(void) r_ble_ll_isoal_tx_comp_cb_set(iso_tx_comp_cb); #endif /* CONFIG_BT_ISO_TX */ +#if CONFIG_BT_BLUEDROID_ENABLED + err = bt_le_bluedroid_iso_init(); +#else err = bt_le_nimble_iso_init(); +#endif if (err) { return err; } @@ -839,5 +855,9 @@ void bt_le_iso_deinit(void) r_ble_ll_isoal_tx_comp_cb_set(NULL); #endif /* CONFIG_BT_ISO_TX */ +#if CONFIG_BT_BLUEDROID_ENABLED + bt_le_bluedroid_iso_deinit(); +#else bt_le_nimble_iso_deinit(); +#endif } diff --git a/components/bt/esp_ble_iso/host/common/l2cap.c b/components/bt/esp_ble_iso/host/common/l2cap.c index 580f46cb4d8..79dc4256964 100644 --- a/components/bt/esp_ble_iso/host/common/l2cap.c +++ b/components/bt/esp_ble_iso/host/common/l2cap.c @@ -146,7 +146,7 @@ int bt_le_l2cap_accept(uint16_t conn_handle, uint16_t psm, conn = bt_le_acl_conn_find(conn_handle); if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_ERR("NotConn[%d]", __LINE__); + LOG_INF("L2capAcceptNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); *result = L2CAP_LE_ERR_INVALID_PARAMS; return -ENOTCONN; @@ -219,7 +219,7 @@ void bt_le_l2cap_connected(uint16_t conn_handle, uint16_t psm, conn = bt_le_acl_conn_find(conn_handle); if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_ERR("NotConn[%d]", __LINE__); + LOG_INF("L2capConnectedNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); return; } @@ -255,7 +255,7 @@ void bt_le_l2cap_disconnected(uint16_t conn_handle, uint16_t psm) conn = bt_le_acl_conn_find(conn_handle); if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_ERR("NotConn[%d]", __LINE__); + LOG_INF("L2capDisconnectedNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); return; } @@ -294,7 +294,7 @@ void bt_le_l2cap_received(uint16_t conn_handle, uint16_t psm, conn = bt_le_acl_conn_find(conn_handle); if (conn == NULL || conn->state != BT_CONN_CONNECTED) { - LOG_ERR("NotConn[%d]", __LINE__); + LOG_INF("L2capReceivedNotConn[%u][%u]", conn_handle, BT_CONN_STATE_GET(conn)); return; } @@ -315,7 +315,9 @@ void bt_le_l2cap_received(uint16_t conn_handle, uint16_t psm, _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); @@ -334,6 +336,12 @@ int bt_l2cap_chan_connect(struct bt_conn *conn, struct bt_l2cap_chan *chan, uint 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; @@ -342,12 +350,15 @@ int bt_l2cap_chan_connect(struct bt_conn *conn, struct bt_l2cap_chan *chan, uint 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"); @@ -361,6 +372,9 @@ int bt_l2cap_chan_disconnect(struct bt_l2cap_chan *chan) 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. @@ -371,6 +385,7 @@ int bt_l2cap_chan_disconnect(struct bt_l2cap_chan *chan) } return err; +#endif } _IDF_ONLY @@ -393,7 +408,11 @@ int bt_l2cap_chan_send(struct bt_l2cap_chan *chan, struct net_buf *buf) return -EMSGSIZE; } +#if CONFIG_BT_BLUEDROID_ENABLED + return -ENOTSUP; +#else return bt_le_nimble_l2cap_chan_send(chan, buf); +#endif } _IDF_ONLY @@ -490,10 +509,18 @@ int bt_le_l2cap_init(void) } #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; } diff --git a/components/bt/esp_ble_iso/host/common/scan.c b/components/bt/esp_ble_iso/host/common/scan.c index 5d72d9d15ab..f0d1f663282 100644 --- a/components/bt/esp_ble_iso/host/common/scan.c +++ b/components/bt/esp_ble_iso/host/common/scan.c @@ -484,7 +484,12 @@ int bt_le_scan_start(const struct bt_le_scan_param *param, void *cb) int err = 0; if (atomic_test_bit(bt_dev.flags, BT_DEV_SCANNING) == false) { +#if CONFIG_BT_BLUEDROID_ENABLED + ARG_UNUSED(cb); + err = bt_le_bluedroid_scan_start(param); +#else err = bt_le_nimble_scan_start(param, cb); +#endif if (err == 0) { atomic_set_bit(bt_dev.flags, BT_DEV_SCANNING); } @@ -501,7 +506,11 @@ int bt_le_scan_stop(void) int err = 0; if (atomic_test_bit(bt_dev.flags, BT_DEV_SCANNING)) { +#if CONFIG_BT_BLUEDROID_ENABLED + err = bt_le_bluedroid_scan_stop(); +#else err = bt_le_nimble_scan_stop(); +#endif if (err == 0) { atomic_clear_bit(bt_dev.flags, BT_DEV_SCANNING); } diff --git a/components/bt/esp_ble_iso/include/zephyr/autoconf.h b/components/bt/esp_ble_iso/include/zephyr/autoconf.h index 086b9d1d09a..5767bc700f5 100644 --- a/components/bt/esp_ble_iso/include/zephyr/autoconf.h +++ b/components/bt/esp_ble_iso/include/zephyr/autoconf.h @@ -13,6 +13,49 @@ #define CONFIG_LITTLE_ENDIAN 1 #define CONFIG_BT_CONN_TX_USER_DATA_SIZE 8 +#if CONFIG_BT_BLUEDROID_ENABLED + +#define CONFIG_BT_ISO_EXT_ADV CONFIG_BT_BLE_50_EXTEND_ADV_EN +#define CONFIG_BT_ISO_PER_ADV CONFIG_BT_BLE_50_PERIODIC_ADV_EN + +/* Bluedroid host does not expose ext-adv/periodic-sync slot counts; + * keep using the controller-side limits as the source of truth. */ +#if CONFIG_BT_ISO_EXT_ADV +#define CONFIG_BT_EXT_ADV_MAX_ADV_SET CONFIG_BT_LE_MAX_EXT_ADV_INSTANCES +#else /* CONFIG_BT_ISO_EXT_ADV */ +#define CONFIG_BT_EXT_ADV_MAX_ADV_SET 0 +#endif /* CONFIG_BT_ISO_EXT_ADV */ + +#if CONFIG_BT_ISO_PER_ADV +#define CONFIG_BT_PER_ADV_SYNC_MAX CONFIG_BT_LE_MAX_PERIODIC_SYNCS +#else /* CONFIG_BT_ISO_PER_ADV */ +#define CONFIG_BT_PER_ADV_SYNC_MAX 0 +#endif /* CONFIG_BT_ISO_PER_ADV */ + +#if CONFIG_BT_BLE_FEAT_PERIODIC_ADV_SYNC_TRANSFER +#define CONFIG_BT_PER_ADV_SYNC_TRANSFER_SENDER CONFIG_BT_BLE_FEAT_PERIODIC_ADV_SYNC_TRANSFER +#define CONFIG_BT_PER_ADV_SYNC_TRANSFER_RECEIVER CONFIG_BT_BLE_FEAT_PERIODIC_ADV_SYNC_TRANSFER +#else /* CONFIG_BT_BLE_FEAT_PERIODIC_ADV_SYNC_TRANSFER */ +#define CONFIG_BT_PER_ADV_SYNC_TRANSFER_SENDER 0 +#define CONFIG_BT_PER_ADV_SYNC_TRANSFER_RECEIVER 0 +#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 +_Static_assert(CONFIG_BT_ISO_MAX_CHAN <= CONFIG_BT_BLE_ISO_CIS_MAX_COUNT + CONFIG_BT_BLE_ISO_BIS_MAX_COUNT, "Too large ISO channels"); +#elif CONFIG_BT_ISO_UNICAST && !CONFIG_BT_ISO_BROADCAST +_Static_assert(CONFIG_BT_ISO_MAX_CHAN <= CONFIG_BT_BLE_ISO_CIS_MAX_COUNT, "Too large ISO channels"); +#elif !CONFIG_BT_ISO_UNICAST && CONFIG_BT_ISO_BROADCAST +_Static_assert(CONFIG_BT_ISO_MAX_CHAN <= CONFIG_BT_BLE_ISO_BIS_MAX_COUNT, "Too large ISO channels"); +#else /* CONFIG_BT_ISO_UNICAST && CONFIG_BT_ISO_BROADCAST */ +_Static_assert(CONFIG_BT_ISO_MAX_CHAN == 0, "Too large ISO channels"); +#endif /* CONFIG_BT_ISO_UNICAST && CONFIG_BT_ISO_BROADCAST */ + +#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 @@ -46,3 +89,5 @@ _Static_assert(CONFIG_BT_ISO_MAX_CHAN <= CONFIG_BT_NIMBLE_ISO_BIS, "Too large IS #else /* CONFIG_BT_ISO_UNICAST && CONFIG_BT_ISO_BROADCAST */ _Static_assert(CONFIG_BT_ISO_MAX_CHAN == 0, "Too large ISO channels"); #endif /* CONFIG_BT_ISO_UNICAST && CONFIG_BT_ISO_BROADCAST */ + +#endif /* CONFIG_BT_BLUEDROID_ENABLED */ diff --git a/components/bt/esp_ble_iso/include/zephyr/kernel.h b/components/bt/esp_ble_iso/include/zephyr/kernel.h index ec6f0e82f10..840868b04da 100644 --- a/components/bt/esp_ble_iso/include/zephyr/kernel.h +++ b/components/bt/esp_ble_iso/include/zephyr/kernel.h @@ -28,6 +28,7 @@ extern "C" { /* Mutex */ #define K_MUTEX_FOREVER portMAX_DELAY +#define K_MUTEX_SHORT (5000 / portTICK_PERIOD_MS) struct k_mutex { SemaphoreHandle_t handle; @@ -99,6 +100,94 @@ static inline int k_mutex_unlock(struct k_mutex *mutex) return 0; } +/* Semaphore */ + +#define K_SEM_FOREVER portMAX_DELAY +#define K_SEM_SHORT (5000 / portTICK_PERIOD_MS) + +struct k_sem { + SemaphoreHandle_t handle; + int result; +}; + +static inline void k_sem_create(struct k_sem *sem) +{ + assert(sem); + assert(sem->handle == NULL); + + sem->handle = xSemaphoreCreateBinary(); + assert(sem->handle); + sem->result = 0; +} + +static inline void k_sem_delete(struct k_sem *sem) +{ + assert(sem); + assert(sem->handle); + + vSemaphoreDelete(sem->handle); + sem->handle = NULL; +} + +/* Inline log helper with a fixed tag, mirroring K_MUTEX_LOG_ERR. The sem + * helpers are inlined across many TUs, not all of which call + * LOG_MODULE_REGISTER, so we cannot reference the per-TU __iso_log_tag. + * Hardcode the "ISO_SEM" tag and keep the same level gating. */ +#if CONFIG_BT_ISO_NO_LOG || (CONFIG_BT_ISO_LOG_LEVEL < BT_ISO_LOG_ERROR) +#define K_SEM_LOG_ERR(fmt, args...) +#else +#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) +{ + assert(sem); + 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 + * higher-priority task). Clearing now would race-overwrite the + * producer's value. Caller must k_sem_reset() before initiating the + * async op that will produce the result. */ + + if (xSemaphoreTake(sem->handle, timeout) == pdTRUE) { + return 0; + } + +#if !CONFIG_BT_ISO_NO_LOG && (CONFIG_BT_ISO_LOG_LEVEL >= BT_ISO_LOG_ERROR) + K_SEM_LOG_ERR("TakeFail[self=%s]", pcTaskGetName(NULL)); +#else + K_SEM_LOG_ERR("TakeFail"); +#endif + return -EIO; +} + +static inline int k_sem_give(struct k_sem *sem) +{ + assert(sem); + assert(sem->handle); + + if (xSemaphoreGive(sem->handle) != pdTRUE) { + K_SEM_LOG_ERR("GiveFail"); + return -EIO; + } + + return 0; +} + +/* Discard any pending give. Used by callers that reuse the same sem across + * cmd/response cycles where a previous cycle timed out — without this, a + * late give from the timed-out cycle would unblock the next take and the + * caller would see uninitialized response data. */ +static inline void k_sem_reset(struct k_sem *sem) +{ + assert(sem); + assert(sem->handle); + + xQueueReset(sem->handle); + sem->result = 0; +} + /* Timer */ typedef uint32_t k_timeout_t; 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 a2580a5806b..109f05c6b86 100644 --- a/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/README.md +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/README.md @@ -9,9 +9,9 @@ This example acts as a **BAP Broadcast Sink**. It scans for extended advertisements that carry the Broadcast Audio Announcement Service UUID and whose complete-name AD matches the hard-coded `"BAP Broadcast Source"` string; on a hit, it creates a periodic-advertising sync, builds a BAP broadcast sink for that PA handle and broadcast ID, decodes the BASE / BIGInfo from the PA channel, and then synchronizes to the BIG to receive BIS streams. -The build runs on top of the NimBLE host stack and the ESP BLE Audio component set. Sink-side APIs used: `esp_ble_audio_common_init` / `_start`, `esp_ble_audio_pacs_register` + `esp_ble_audio_pacs_cap_register` (sink PAC and sink location enabled), `esp_ble_audio_bap_scan_delegator_register` (so a Broadcast Assistant can drive PA-sync, broadcast-code, and BIS-sync requests via BASS), `esp_ble_audio_bap_broadcast_sink_register_cb`, `esp_ble_audio_bap_broadcast_sink_create` / `_sync` / `_stop` / `_delete`, and `esp_ble_audio_bap_base_get_subgroup_count` / `_get_bis_indexes`. PAC capabilities are LC3 with sample rates 16 kHz + 24 kHz, frame duration 10 ms, 1 channel, 40–60 octets/frame, 1 frame/SDU. The fallback broadcast code is `"1234"`; if a Broadcast Assistant has supplied one through BASS it is used instead. +The build runs on top of the selected BLE host stack (Bluedroid by default; NimBLE via the `sdkconfig.defaults.nimble` overlay) and the ESP BLE Audio component set. Sink-side APIs used: `esp_ble_audio_common_init` / `_start`, `esp_ble_audio_pacs_register` + `esp_ble_audio_pacs_cap_register` (sink PAC and sink location enabled), `esp_ble_audio_bap_scan_delegator_register` (so a Broadcast Assistant can drive PA-sync, broadcast-code, and BIS-sync requests via BASS), `esp_ble_audio_bap_broadcast_sink_register_cb`, `esp_ble_audio_bap_broadcast_sink_create` / `_sync` / `_stop` / `_delete`, and `esp_ble_audio_bap_base_get_subgroup_count` / `_get_bis_indexes`. PAC capabilities are LC3 with sample rates 16 kHz + 24 kHz, frame duration 10 ms, 1 channel, 40–60 octets/frame, 1 frame/SDU. The fallback broadcast code is `"1234"`; if a Broadcast Assistant has supplied one through BASS it is used instead. -After PA sync is established, `ble_gap_disc_cancel()` stops the extended scanner — BASE/BIGInfo arrive over the PA channel — and `pa_sync_lost()` re-arms the scanner. +After PA sync is established, `scan_stop()` halts the extended scanner — BASE/BIGInfo arrive over the PA channel — and `pa_sync_lost()` calls `scan_start()` to re-arm. The host-specific GAP/PA-sync plumbing lives in `main/bluedroid/scan.c` and `main/nimble/scan.c`; `main.c` only sees the host-agnostic interface in `scan.h`. ## Requirements @@ -34,11 +34,24 @@ The example inherits a Just-Works pairing model (LE Secure Connections, no MITM, ## Build & Flash +The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`. + +### Bluedroid host (default) + ```bash -idf.py set-target esp32h4 # or esp32s31 +idf.py set-target esp32h4 idf.py -p PORT flash monitor ``` +### NimBLE host + +```bash +idf.py set-target esp32h4 +idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor +``` + +For `esp32s31`, replace the chip overlay accordingly. + (Exit serial monitor with `Ctrl-]`.) ## Example Flow @@ -46,8 +59,8 @@ idf.py -p PORT flash monitor 1. `app_main` initializes NVS, calls `bluetooth_init()`, and calls `esp_ble_audio_common_init(&info)` with `info.gap_cb = iso_gap_app_cb`. 2. PACS is registered (`snk_pac` + `snk_loc`), each stream's `ops` field is wired to `stream_ops`, and the LC3 sink capability is registered via `esp_ble_audio_pacs_cap_register(ESP_BLE_AUDIO_DIR_SINK, ...)`. 3. The scan delegator (`scan_delegator_cbs`: `recv_state_updated`, `pa_sync_req`, `pa_sync_term_req`, `broadcast_code`, `bis_sync_req`) and broadcast-sink callbacks (`base_recv`, `syncable`) are registered, then `esp_ble_audio_common_start(NULL)` runs. -4. `ext_scan_start()` runs passive extended discovery (`itvl=window=160`). For each `ESP_BLE_AUDIO_GAP_EVENT_EXT_SCAN_RECV`, `data_cb` matches the complete/short/broadcast name AD type against `"BAP Broadcast Source"` and records the Broadcast ID from the Broadcast Audio Service Data. -5. On match (and only when not already PA-syncing and no scan-delegator state is pinned), `pa_sync_create()` calls `ble_gap_periodic_adv_sync_create` with `skip=0`, `sync_timeout=10s`. +4. `scan_init()` performs host-specific GAP setup (Bluedroid: registers the GAP callback for `*_COMPLETE_EVT` semaphore signalling + posts `EXT_ADV_REPORT` / `PERIODIC_ADV_SYNC_ESTAB` / `PERIODIC_ADV_REPORT` / `PERIODIC_ADV_SYNC_LOST` to the audio engine; NimBLE: no-op — the scan-instance callback is passed at `ble_gap_disc` / `ble_gap_periodic_adv_sync_create` time). `scan_start()` then runs passive extended discovery (`itvl=window=160`). For each `ESP_BLE_AUDIO_GAP_EVENT_EXT_SCAN_RECV`, `data_cb` matches the complete/short/broadcast name AD type against `"BAP Broadcast Source"` and records the Broadcast ID from the Broadcast Audio Service Data. +5. On match (and only when not already PA-syncing and no scan-delegator state is pinned), `pa_sync_create(addr_type, addr, sid)` invokes the host-specific PA-sync create routine (`ble_gap_periodic_adv_sync_create` / `esp_ble_gap_periodic_adv_create_sync`) with `skip=0`, `sync_timeout=10s`. 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`. diff --git a/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/CMakeLists.txt b/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/CMakeLists.txt index 721967052c7..8b18ad60330 100644 --- a/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/CMakeLists.txt +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/CMakeLists.txt @@ -1,4 +1,11 @@ set(srcs "main.c") -idf_component_register(SRCS "${srcs}" +if(CONFIG_BT_BLUEDROID_ENABLED) + list(APPEND srcs "bluedroid/scan.c") +else() + list(APPEND srcs "nimble/scan.c") +endif() + +idf_component_register(SRCS ${srcs} + INCLUDE_DIRS "." REQUIRES bt nvs_flash) diff --git a/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/bluedroid/scan.c b/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/bluedroid/scan.c new file mode 100644 index 00000000000..c055e7f81a6 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/bluedroid/scan.c @@ -0,0 +1,117 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "esp_log.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +#include "esp_bt_defs.h" +#include "esp_gap_ble_api.h" + +#include "scan.h" + +static SemaphoreHandle_t scan_sem; + +/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */ +static esp_bt_status_t scan_op_status; + +#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, scan_sem, portMAX_DELAY, scan_op_status) + +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; + + /* PA sync / ext adv events: forwarded by adapter's BTA path, not here. */ + default: + break; + } +} + +int app_host_init(void) +{ + esp_err_t err; + + scan_sem = xSemaphoreCreateBinary(); + if (scan_sem == NULL) { + ESP_LOGE(TAG, "Failed to create scan semaphore"); + return -1; + } + + err = esp_ble_gap_register_callback(gap_event_handler); + if (err) { + ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err); + vSemaphoreDelete(scan_sem); + return err; + } + + return 0; +} + +int ext_scan_start(void) +{ + WAIT_API(esp_ble_gap_set_ext_scan_params(&ext_scan_params)); + WAIT_API(esp_ble_gap_start_ext_scan(0, 0)); + + ESP_LOGI(TAG, "Scanning for broadcast source..."); + return ESP_OK; +} + +int ext_scan_stop(void) +{ + WAIT_API(esp_ble_gap_stop_ext_scan()); + return ESP_OK; +} + +int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid) +{ + esp_ble_gap_periodic_adv_sync_params_t params = { + .filter_policy = 0, + .sid = sid, + .addr_type = addr_type, + .skip = PA_SYNC_SKIP, + .sync_timeout = PA_SYNC_TIMEOUT, + }; + + memcpy(params.addr, addr, sizeof(params.addr)); + + /* Fire-and-forget: sync establishment (PERIODIC_ADV_SYNC_ESTAB_EVT) is + * air-dependent and surfaces asynchronously. */ + return esp_ble_gap_periodic_adv_create_sync(¶ms); +} + +int pa_sync_terminate(uint16_t sync_handle) +{ + return esp_ble_gap_periodic_adv_sync_terminate(sync_handle); +} 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 e8d76f2d332..1516d846817 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 @@ -13,10 +13,6 @@ #include "esp_log.h" #include "nvs_flash.h" -#include "esp_system.h" - -#include "host/ble_hs.h" -#include "services/gap/ble_svc_gap.h" #include "esp_ble_audio_lc3_defs.h" #include "esp_ble_audio_bap_api.h" @@ -25,18 +21,13 @@ #include "ble_audio_example_init.h" #include "ble_audio_example_utils.h" -#define TAG "BAP_BSNK" +#include "scan.h" #define TARGET_DEVICE_NAME "BAP Broadcast Source" #define TARGET_DEVICE_NAME_LEN (sizeof(TARGET_DEVICE_NAME) - 1) #define TARGET_BROADCAST_CODE "1234" -#define SCAN_INTERVAL 160 /* 100ms */ -#define SCAN_WINDOW 160 /* 100ms */ - -#define PA_SYNC_SKIP 0 -#define PA_SYNC_TIMEOUT 1000 /* 1000 * 10ms = 10s */ #define PA_SYNC_HANDLE_INIT UINT16_MAX #define CONN_HANDLE_INIT UINT16_MAX @@ -90,61 +81,6 @@ static esp_ble_audio_pacs_cap_t cap = { .codec_cap = &codec_cap, }; -static void ext_scan_start(void) -{ - struct ble_gap_disc_params params = {0}; - uint8_t own_addr_type; - int err; - - err = ble_hs_id_infer_auto(0, &own_addr_type); - if (err) { - ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err); - return; - } - - params.passive = 1; - params.itvl = SCAN_INTERVAL; - params.window = SCAN_WINDOW; - - err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms, - example_audio_gap_event_cb, NULL); - if (err) { - ESP_LOGE(TAG, "Failed to start scanning, err %d", err); - return; - } - - ESP_LOGI(TAG, "Scanning for broadcast source..."); -} - -static int pa_sync_create(const bt_addr_le_t *addr, uint8_t adv_sid) -{ - struct ble_gap_periodic_sync_params params = {0}; - ble_addr_t sync_addr = {0}; - - sync_addr.type = addr->type; - memcpy(sync_addr.val, addr->a.val, sizeof(sync_addr.val)); - params.skip = PA_SYNC_SKIP; - params.sync_timeout = PA_SYNC_TIMEOUT; - - return ble_gap_periodic_adv_sync_create(&sync_addr, adv_sid, ¶ms, - example_audio_gap_event_cb, NULL); -} - -static int pa_sync_terminate(void) -{ - int err; - - err = ble_gap_periodic_adv_sync_terminate(sync_handle); - if (err) { - ESP_LOGE(TAG, "Failed to terminate PA sync, err %d", err); - return err; - } - - ESP_LOGI(TAG, "PA sync terminated"); - - return 0; -} - static void recv_state_updated_cb(esp_ble_conn_t *conn, const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state) { @@ -199,13 +135,27 @@ static int pa_sync_term_req_cb(esp_ble_conn_t *conn, req_recv_state = recv_state; - err = pa_sync_terminate(); + /* Nothing to terminate if PAST/PA-sync never landed (e.g., PAST setup failed + * earlier). Issuing the HCI terminate with the sentinel handle gets 0x12 + * (Invalid HCI Command Parameters) back from the controller. Mirrors + * cap_acceptor_broadcast.c. */ + if (sync_handle == PA_SYNC_HANDLE_INIT) { + ESP_LOGI(TAG, "PA sync never established, skip terminate"); + return 0; + } + + err = pa_sync_terminate(sync_handle); if (err) { + ESP_LOGE(TAG, "Failed to terminate PA sync, err %d", err); return -EIO; } - sync_handle = PA_SYNC_HANDLE_INIT; + ESP_LOGI(TAG, "PA sync terminated"); + /* Let the synthesized PA_SYNC_LOST event drive cleanup via pa_sync_lost + * (it gates on the original sync_handle). Resetting sync_handle here would + * make that gate miss and leak broadcast_sink. Mirrors + * cap_acceptor_broadcast.c. */ return 0; } @@ -481,7 +431,6 @@ static bool data_cb(uint8_t type, const uint8_t *data, static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event) { struct scan_recv_data sr = {0}; - bt_addr_le_t addr; int err; /* Periodic advertising interval. 0 if no periodic advertising. */ @@ -500,10 +449,9 @@ static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event) if (pa_syncing == false && req_recv_state == NULL) { broadcaster_broadcast_id = sr.broadcast_id; - addr.type = event->ext_scan_recv.addr.type; - memcpy(addr.a.val, event->ext_scan_recv.addr.val, sizeof(addr.a.val)); - - err = pa_sync_create(&addr, event->ext_scan_recv.sid); + err = pa_sync_create(event->ext_scan_recv.addr.type, + event->ext_scan_recv.addr.val, + event->ext_scan_recv.sid); if (err) { ESP_LOGE(TAG, "Failed to create PA sync, err %d", err); return; @@ -533,7 +481,7 @@ static void pa_sync(esp_ble_audio_gap_app_event_t *event) * via the PA sync channel, so the extended scanner is no longer * needed. Stop it now — pa_sync_lost() will restart it on loss. */ - rc = ble_gap_disc_cancel(); + rc = ext_scan_stop(); if (rc) { ESP_LOGW(TAG, "Failed to stop scanning, err %d", rc); } @@ -618,6 +566,12 @@ void app_main(void) return; } + err = app_host_init(); + if (err) { + ESP_LOGE(TAG, "Failed to init host, err %d", err); + return; + } + err = esp_ble_audio_common_init(&info); if (err) { ESP_LOGE(TAG, "Failed to initialize audio, err %d", err); diff --git a/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/nimble/scan.c b/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/nimble/scan.c new file mode 100644 index 00000000000..19da6974a96 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/nimble/scan.c @@ -0,0 +1,90 @@ +/* + * 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 "esp_ble_audio_common_api.h" + +#include "scan.h" + +/* Forward only the GAP events the application consumes. */ +static int gap_event_cb(struct ble_gap_event *event, void *arg) +{ + switch (event->type) { + case BLE_GAP_EVENT_EXT_DISC: + case BLE_GAP_EVENT_PERIODIC_SYNC: + case BLE_GAP_EVENT_PERIODIC_REPORT: + case BLE_GAP_EVENT_PERIODIC_SYNC_LOST: + esp_ble_audio_gap_app_post_event(event->type, event); + break; + default: + break; + } + + return 0; +} + +int app_host_init(void) +{ + return 0; +} + +int ext_scan_start(void) +{ + struct ble_gap_disc_params params = {0}; + uint8_t own_addr_type; + int err; + + err = ble_hs_id_infer_auto(0, &own_addr_type); + if (err) { + ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err); + return err; + } + + params.passive = 1; + params.itvl = SCAN_INTERVAL; + params.window = SCAN_WINDOW; + + err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms, + gap_event_cb, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to start scanning, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Scanning for broadcast source..."); + return 0; +} + +int ext_scan_stop(void) +{ + return ble_gap_disc_cancel(); +} + +int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid) +{ + struct ble_gap_periodic_sync_params params = {0}; + ble_addr_t sync_addr = {0}; + + sync_addr.type = addr_type; + memcpy(sync_addr.val, addr, sizeof(sync_addr.val)); + params.skip = PA_SYNC_SKIP; + params.sync_timeout = PA_SYNC_TIMEOUT; + + return ble_gap_periodic_adv_sync_create(&sync_addr, sid, ¶ms, + gap_event_cb, NULL); +} + +int pa_sync_terminate(uint16_t sync_handle) +{ + return ble_gap_periodic_adv_sync_terminate(sync_handle); +} diff --git a/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/scan.h b/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/scan.h new file mode 100644 index 00000000000..03f9bb8220e --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/main/scan.h @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "ble_audio_example_utils.h" + +#define TAG "BAP_BSNK" + +#define SCAN_INTERVAL 160 /* 100ms */ +#define SCAN_WINDOW 160 /* 100ms */ + +#define PA_SYNC_SKIP 0 +#define PA_SYNC_TIMEOUT 1000 /* 1000 * 10ms = 10s */ + +int app_host_init(void); + +int ext_scan_start(void); +int ext_scan_stop(void); + +int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid); +int pa_sync_terminate(uint16_t sync_handle); 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 8a2bff9147b..85ae6dd730f 100644 --- a/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/sdkconfig.defaults @@ -3,14 +3,14 @@ # CONFIG_BT_ENABLED=y -CONFIG_BT_BLUEDROID_ENABLED=n -CONFIG_BT_NIMBLE_ENABLED=y -CONFIG_BT_NIMBLE_EXT_ADV=y -CONFIG_BT_NIMBLE_NVS_PERSIST=y -CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 -CONFIG_BT_NIMBLE_MAX_CCCDS=20 -CONFIG_BT_NIMBLE_ISO=y -CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y +CONFIG_BT_NIMBLE_ENABLED=n +CONFIG_BT_BLUEDROID_ENABLED=y +CONFIG_BT_CLASSIC_ENABLED=n +CONFIG_BT_CONTROLLER_ENABLED=y +CONFIG_BT_BLE_ENABLED=y +CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y +CONFIG_BT_ACL_CONNECTIONS=1 +CONFIG_BT_BLE_FEAT_ISO_EN=y CONFIG_BT_ISO_MAX_CHAN=2 @@ -24,6 +24,8 @@ CONFIG_BT_PAC_SRC_NOTIFIABLE=y CONFIG_BT_PAC_SRC_LOC_WRITEABLE=y CONFIG_BT_PAC_SRC_LOC_NOTIFIABLE=y CONFIG_BT_PACS_SUPPORTED_CONTEXT_NOTIFIABLE=y -CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=30 +CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=60 + +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y CONFIG_FREERTOS_HZ=1000 diff --git a/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/sdkconfig.defaults.nimble b/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/sdkconfig.defaults.nimble new file mode 100644 index 00000000000..ea3029ef03f --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_sink/sdkconfig.defaults.nimble @@ -0,0 +1,12 @@ +# NimBLE host overlay for this example. +# Use with: +# idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.$IDF_TARGET;sdkconfig.defaults.nimble" build + +CONFIG_BT_BLUEDROID_ENABLED=n +CONFIG_BT_NIMBLE_ENABLED=y +CONFIG_BT_NIMBLE_EXT_ADV=y +CONFIG_BT_NIMBLE_NVS_PERSIST=y +CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 +CONFIG_BT_NIMBLE_MAX_CCCDS=20 +CONFIG_BT_NIMBLE_ISO=y +CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y diff --git a/examples/bluetooth/esp_ble_audio/bap/broadcast_source/README.md b/examples/bluetooth/esp_ble_audio/bap/broadcast_source/README.md index d840b865d01..c8bd4da72b0 100644 --- a/examples/bluetooth/esp_ble_audio/bap/broadcast_source/README.md +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_source/README.md @@ -9,7 +9,7 @@ This example acts as a **BAP Broadcast Source**. It creates a BAP broadcast source from the LC3 `16_2_1` broadcast preset, encodes the BASE into the periodic advertising payload, places the Broadcast Audio Announcement Service UUID and a fixed 24-bit Broadcast ID (`0x123456`) in the extended advertising payload, and then starts the BIG so that BIS streams transmit synthetic SDU data on a fixed cadence. -The build runs on top of the NimBLE host stack and the ESP BLE Audio component set. Source-side APIs used: `esp_ble_audio_common_init` / `esp_ble_audio_common_start` (common layer), `esp_ble_audio_bap_broadcast_source_create` / `_get_base` / `_start` (BAP), `esp_ble_audio_bap_broadcast_adv_add` (BAP/ISO glue), and the BAP stream callbacks (`started`, `stopped`, `sent`, `disconnected`). PACS, GAP scanner, and the scan delegator are **not** used on this side. Encryption is enabled because the broadcast code `"1234"` is non-empty; packing is `ESP_BLE_ISO_PACKING_SEQUENTIAL`. Channel allocation per stream is hard-coded as `FRONT_LEFT` for stream 0 and `FRONT_RIGHT` for stream 1. +The build runs on top of the selected BLE host stack (Bluedroid by default; NimBLE via the `sdkconfig.defaults.nimble` overlay) and the ESP BLE Audio component set. Source-side APIs used: `esp_ble_audio_common_init` / `esp_ble_audio_common_start` (common layer), `esp_ble_audio_bap_broadcast_source_create` / `_get_base` / `_start` (BAP), `esp_ble_audio_bap_broadcast_adv_add` (BAP/ISO glue), and the BAP stream callbacks (`started`, `stopped`, `sent`, `disconnected`). PACS, GAP scanner, and the scan delegator are **not** used on this side. Encryption is enabled because the broadcast code `"1234"` is non-empty; packing is `ESP_BLE_ISO_PACKING_SEQUENTIAL`. Channel allocation per stream is hard-coded as `FRONT_LEFT` for stream 0 and `FRONT_RIGHT` for stream 1. The TX scheduler is built on `example_audio_tx_scheduler_*` helpers; the source comment notes that ESP timer resolution is not accurate enough for the SDU interval, so a `k_work_delayable`-based scheduler is used instead. @@ -34,11 +34,24 @@ The example inherits a Just-Works pairing model (LE Secure Connections, no MITM, ## Build & Flash +The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`. + +### Bluedroid host (default) + ```bash -idf.py set-target esp32h4 # or esp32s31 +idf.py set-target esp32h4 idf.py -p PORT flash monitor ``` +### NimBLE host + +```bash +idf.py set-target esp32h4 +idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor +``` + +For `esp32s31`, replace the chip overlay accordingly. + (Exit serial monitor with `Ctrl-]`.) ## Example Flow @@ -46,7 +59,7 @@ idf.py -p PORT flash monitor 1. `app_main` initializes NVS, calls `bluetooth_init()`, and calls `esp_ble_audio_common_init(NULL)` (no GAP callback for this role). 2. `broadcast_source_setup()` registers `source_started` / `source_stopped` callbacks, populates per-stream channel-allocation BIS metadata (left/right), registers `stream_ops` on each stream, and calls `esp_ble_audio_bap_broadcast_source_create()` with the encrypted preset. 3. `esp_ble_audio_common_start(NULL)` starts the audio stack; each stream's `example_audio_tx_scheduler_init()` is wired with `tx_scheduler_cb`. -4. `ext_adv_start()` configures non-connectable extended adv (1M primary / 2M secondary, 200 ms interval), writes Broadcast Audio Service Data + Broadcast ID + complete name, configures periodic adv (100 ms), writes the BASE returned by `esp_ble_audio_bap_broadcast_source_get_base()`, then starts periodic and extended advertising. +4. `adv_init()` performs host-specific GAP setup (Bluedroid: registers the GAP callback for `*_COMPLETE_EVT` semaphore signalling; NimBLE: no-op — adv-instance callback is passed at configure time). `ext_adv_start()` builds the extended (Broadcast Audio Service Data + Broadcast ID + complete name) and periodic (BASE from `esp_ble_audio_bap_broadcast_source_get_base()`) payloads in host-agnostic bytes, then calls `adv_start(ext_data, ext_len, per_data, per_len)`. The host-specific implementation in `main/bluedroid/adv.c` or `main/nimble/adv.c` configures non-connectable extended adv (1M primary / 2M secondary, 200 ms interval), configures periodic adv (100 ms), writes both payloads, and starts periodic and extended advertising. 5. `broadcast_start()` calls `esp_ble_audio_bap_broadcast_adv_add()` and `esp_ble_audio_bap_broadcast_source_start()` on the same `ADV_HANDLE`, which kicks off BIGInfo + BIS creation. 6. When each BIS stream goes streaming, `stream_started_cb` allocates an SDU-sized buffer and calls `example_audio_tx_scheduler_start()` at `preset_active.qos.interval`; the scheduler invokes `broadcast_source_tx()`, which fills the buffer with the low byte of `seq_num` and calls `esp_ble_audio_bap_stream_send()`. 7. `stream_sent_cb` forwards completions to `example_audio_tx_scheduler_on_sent()` for drift accounting; `stream_stopped_cb` and `stream_disconnected_cb` stop the scheduler; `source_stopped_cb` frees all per-stream buffers. diff --git a/examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/CMakeLists.txt b/examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/CMakeLists.txt index 721967052c7..fb840295253 100644 --- a/examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/CMakeLists.txt +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/CMakeLists.txt @@ -1,4 +1,11 @@ set(srcs "main.c") -idf_component_register(SRCS "${srcs}" +if(CONFIG_BT_BLUEDROID_ENABLED) + list(APPEND srcs "bluedroid/adv.c") +else() + list(APPEND srcs "nimble/adv.c") +endif() + +idf_component_register(SRCS ${srcs} + INCLUDE_DIRS "." REQUIRES bt nvs_flash) diff --git a/examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/adv.h b/examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/adv.h new file mode 100644 index 00000000000..db2ff97c023 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/adv.h @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "ble_audio_example_utils.h" + +#define TAG "BAP_BSRC" + +#define ADV_HANDLE 0 +#define ADV_SID 0 +#define ADV_TX_POWER 127 +#define ADV_INTERVAL_MS 200 +#define PER_ADV_INTERVAL_MS 100 + +int app_host_init(void); + +int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len, + const uint8_t *per_data, uint8_t per_len); diff --git a/examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/bluedroid/adv.c b/examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/bluedroid/adv.c new file mode 100644 index 00000000000..bd744a079bc --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/bluedroid/adv.c @@ -0,0 +1,122 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#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 "adv.h" + +static SemaphoreHandle_t adv_sem; + +/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */ +static esp_bt_status_t adv_op_status; + +#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, adv_sem, portMAX_DELAY, adv_op_status) + +static esp_ble_gap_ext_adv_params_t ext_adv_params = { + .type = ESP_BLE_GAP_SET_EXT_ADV_PROP_NONCONN_NONSCANNABLE_UNDIRECTED, + .interval_min = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS), + .interval_max = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS), + .channel_map = ADV_CHNL_ALL, + .filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, + .primary_phy = ESP_BLE_GAP_PHY_1M, + .max_skip = 0, + .secondary_phy = ESP_BLE_GAP_PHY_2M, + .sid = ADV_SID, + .scan_req_notif = false, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .tx_power = ADV_TX_POWER, +}; + +static esp_ble_gap_periodic_adv_params_t periodic_adv_params = { + .interval_min = ESP_BLE_GAP_PERIODIC_ADV_ITVL_MS(PER_ADV_INTERVAL_MS), + .interval_max = ESP_BLE_GAP_PERIODIC_ADV_ITVL_MS(PER_ADV_INTERVAL_MS), + .properties = 0, +}; + +static esp_ble_gap_ext_adv_t ext_adv_inst[1] = { + [0] = { ADV_HANDLE, 0, 0 }, +}; + +static void gap_event_handler(esp_gap_ble_cb_event_t event, + esp_ble_gap_cb_param_t *param) +{ + switch (event) { + case ESP_GAP_BLE_EXT_ADV_SET_PARAMS_COMPLETE_EVT: + adv_op_status = param->ext_adv_set_params.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_EXT_ADV_DATA_SET_COMPLETE_EVT: + adv_op_status = param->ext_adv_data_set.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_EXT_ADV_START_COMPLETE_EVT: + adv_op_status = param->ext_adv_start.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_PERIODIC_ADV_SET_PARAMS_COMPLETE_EVT: + adv_op_status = param->peroid_adv_set_params.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_PERIODIC_ADV_DATA_SET_COMPLETE_EVT: + adv_op_status = param->period_adv_data_set.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_PERIODIC_ADV_START_COMPLETE_EVT: + adv_op_status = param->period_adv_start.status; + xSemaphoreGive(adv_sem); + break; + default: + break; + } +} + +int app_host_init(void) +{ + esp_err_t err; + + adv_sem = xSemaphoreCreateBinary(); + if (adv_sem == NULL) { + ESP_LOGE(TAG, "Failed to create adv semaphore"); + return -1; + } + + err = esp_ble_gap_register_callback(gap_event_handler); + if (err) { + ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err); + vSemaphoreDelete(adv_sem); + return err; + } + + return 0; +} + +int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len, + const uint8_t *per_data, uint8_t per_len) +{ + WAIT_API(esp_ble_gap_ext_adv_set_params(ADV_HANDLE, &ext_adv_params)); + WAIT_API(esp_ble_gap_config_ext_adv_data_raw(ADV_HANDLE, ext_len, ext_data)); + WAIT_API(esp_ble_gap_periodic_adv_set_params(ADV_HANDLE, &periodic_adv_params)); +#if CONFIG_BT_BLE_FEAT_PERIODIC_ADV_ENH + WAIT_API(esp_ble_gap_config_periodic_adv_data_raw(ADV_HANDLE, per_len, per_data, false)); + WAIT_API(esp_ble_gap_periodic_adv_start(ADV_HANDLE, true)); +#else + WAIT_API(esp_ble_gap_config_periodic_adv_data_raw(ADV_HANDLE, per_len, per_data)); + WAIT_API(esp_ble_gap_periodic_adv_start(ADV_HANDLE)); +#endif + WAIT_API(esp_ble_gap_ext_adv_start(1, ext_adv_inst)); + + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); + return 0; +} diff --git a/examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/main.c b/examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/main.c index f2260ce9821..a51380ed30b 100644 --- a/examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/main.c +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/main.c @@ -9,17 +9,10 @@ #include #include #include -#include #include #include "esp_log.h" #include "nvs_flash.h" -#include "esp_system.h" -#include "esp_random.h" -#include "esp_timer.h" - -#include "host/ble_hs.h" -#include "services/gap/ble_svc_gap.h" #include "esp_ble_audio_lc3_defs.h" #include "esp_ble_audio_bap_api.h" @@ -29,7 +22,7 @@ #include "ble_audio_example_init.h" #include "ble_audio_example_utils.h" -#define TAG "BAP_BSRC" +#include "adv.h" ESP_BLE_AUDIO_BAP_LC3_BROADCAST_PRESET_16_2_1_DEFINE(preset_active, ESP_BLE_AUDIO_LOCATION_FRONT_LEFT | @@ -42,16 +35,6 @@ ESP_BLE_AUDIO_BAP_LC3_BROADCAST_PRESET_16_2_1_DEFINE(preset_active, #define LOCAL_BROADCAST_CODE "1234" /* Maximum length is 16 */ #define LOCAL_BROADCAST_ID 0x123456 -#define ADV_HANDLE 0x00 -#define ADV_SID 0 -#define ADV_TX_POWER 127 -#define ADV_ADDRESS BLE_OWN_ADDR_PUBLIC -#define ADV_PRIMARY_PHY BLE_HCI_LE_PHY_1M -#define ADV_SECONDARY_PHY BLE_HCI_LE_PHY_2M -#define ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(200) - -#define PER_ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(100) - #define STREAM_COUNT CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT #define SUBGROUP_COUNT CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT @@ -369,138 +352,55 @@ static uint8_t *per_adv_data_get(uint8_t *data_len) return data; } -static int ext_adv_start(void) +static int broadcast_start(void) { - struct ble_gap_periodic_adv_params per_params = {0}; - struct ble_gap_ext_adv_params ext_params = {0}; - struct os_mbuf *data = NULL; + esp_ble_audio_bap_broadcast_adv_info_t info = { + .adv_handle = ADV_HANDLE, + }; + /* BASE (per_adv_data_get) needs broadcast_source to exist — built earlier + * by broadcast_source_setup(), so safe to call here. */ uint8_t *ext_data = NULL; uint8_t *per_data = NULL; - uint8_t data_len = 0; - int err; + uint8_t ext_len = 0; + uint8_t per_len = 0; + int err = 0; - ext_params.connectable = 0; - ext_params.scannable = 0; - ext_params.legacy_pdu = 0; - ext_params.own_addr_type = ADV_ADDRESS; - ext_params.primary_phy = ADV_PRIMARY_PHY; - ext_params.secondary_phy = ADV_SECONDARY_PHY; - ext_params.tx_power = ADV_TX_POWER; - ext_params.sid = ADV_SID; - ext_params.itvl_min = ADV_INTERVAL; - ext_params.itvl_max = ADV_INTERVAL; - - err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL, - example_audio_gap_event_cb, NULL); - if (err) { - ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err); - goto end; - } - - ext_data = ext_adv_data_get(&data_len); + ext_data = ext_adv_data_get(&ext_len); if (ext_data == NULL) { err = -ENOMEM; goto end; } - data = os_msys_get_pkthdr(data_len, 0); - if (data == NULL) { - ESP_LOGE(TAG, "Failed to get ext adv mbuf"); - err = -ENOMEM; - goto end; - } - - err = os_mbuf_append(data, ext_data, data_len); - if (err) { - ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err); - os_mbuf_free_chain(data); - goto end; - } - - err = ble_gap_ext_adv_set_data(ADV_HANDLE, data); - if (err) { - ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err); - goto end; - } - - per_params.include_tx_power = 0; - per_params.itvl_min = PER_ADV_INTERVAL; - per_params.itvl_max = PER_ADV_INTERVAL; - - err = ble_gap_periodic_adv_configure(ADV_HANDLE, &per_params); - if (err) { - ESP_LOGE(TAG, "Failed to configure per adv params, err %d", err); - goto end; - } - - per_data = per_adv_data_get(&data_len); + per_data = per_adv_data_get(&per_len); if (per_data == NULL) { err = -ENOMEM; goto end; } - data = os_msys_get_pkthdr(data_len, 0); - if (data == NULL) { - ESP_LOGE(TAG, "Failed to get per adv mbuf"); - err = -ENOMEM; - goto end; - } - - err = os_mbuf_append(data, per_data, data_len); + err = ext_adv_start(ext_data, ext_len, per_data, per_len); if (err) { - ESP_LOGE(TAG, "Failed to append per adv data, err %d", err); - os_mbuf_free_chain(data); goto end; } - err = ble_gap_periodic_adv_set_data(ADV_HANDLE, data); - if (err) { - ESP_LOGE(TAG, "Failed to set per adv data, err %d", err); - goto end; - } - - err = ble_gap_periodic_adv_start(ADV_HANDLE); - if (err) { - ESP_LOGE(TAG, "Failed to start per advertising, err %d", err); - goto end; - } - - err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0); - if (err) { - ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err); - goto end; - } - - ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); - -end: - if (ext_data) { - free(ext_data); - } - if (per_data) { - free(per_data); - } - return err; -} - -static void broadcast_start(void) -{ - esp_ble_audio_bap_broadcast_adv_info_t info = { - .adv_handle = ADV_HANDLE, - }; - int err; - err = esp_ble_audio_bap_broadcast_adv_add(&info); if (err) { ESP_LOGE(TAG, "Failed to add adv for broadcast source, err %d", err); - return; + goto end; } err = esp_ble_audio_bap_broadcast_source_start(broadcast_source, ADV_HANDLE); if (err) { ESP_LOGE(TAG, "Failed to start broadcast source, err %d", err); - return; } + +end: + if (ext_data != NULL) { + free(ext_data); + } + if (per_data != NULL) { + free(per_data); + } + return err; } void app_main(void) @@ -521,6 +421,12 @@ void app_main(void) return; } + err = app_host_init(); + if (err) { + ESP_LOGE(TAG, "Failed to init host, err %d", err); + return; + } + err = esp_ble_audio_common_init(NULL); if (err) { ESP_LOGE(TAG, "Failed to initialize audio, err %d", err); @@ -548,10 +454,9 @@ void app_main(void) } } - err = ext_adv_start(); + err = broadcast_start(); if (err) { + ESP_LOGE(TAG, "Failed to start broadcast, err %d", err); return; } - - broadcast_start(); } diff --git a/examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/nimble/adv.c b/examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/nimble/adv.c new file mode 100644 index 00000000000..bbb1d3b7f08 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_source/main/nimble/adv.c @@ -0,0 +1,115 @@ +/* + * 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 "adv.h" + +/* Non-connectable broadcaster: no GAP events for the application. */ +static int gap_event_cb(struct ble_gap_event *event, void *arg) +{ + return 0; +} + +int app_host_init(void) +{ + return 0; +} + +int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len, + const uint8_t *per_data, uint8_t per_len) +{ + struct ble_gap_periodic_adv_params per_params = {0}; + struct ble_gap_ext_adv_params ext_params = {0}; + struct os_mbuf *data; + int err; + + ext_params.connectable = 0; + ext_params.scannable = 0; + ext_params.legacy_pdu = 0; + ext_params.own_addr_type = BLE_OWN_ADDR_PUBLIC; + ext_params.primary_phy = BLE_HCI_LE_PHY_1M; + ext_params.secondary_phy = BLE_HCI_LE_PHY_2M; + ext_params.tx_power = ADV_TX_POWER; + ext_params.sid = ADV_SID; + ext_params.itvl_min = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS); + ext_params.itvl_max = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS); + + err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL, + gap_event_cb, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err); + return err; + } + + data = os_msys_get_pkthdr(ext_len, 0); + if (data == NULL) { + ESP_LOGE(TAG, "Failed to get ext adv mbuf"); + return -1; + } + + err = os_mbuf_append(data, ext_data, ext_len); + if (err) { + ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err); + os_mbuf_free_chain(data); + return err; + } + + err = ble_gap_ext_adv_set_data(ADV_HANDLE, data); + if (err) { + ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err); + return err; + } + + per_params.include_tx_power = 0; + per_params.itvl_min = BLE_GAP_PERIODIC_ITVL_MS(PER_ADV_INTERVAL_MS); + per_params.itvl_max = BLE_GAP_PERIODIC_ITVL_MS(PER_ADV_INTERVAL_MS); + + err = ble_gap_periodic_adv_configure(ADV_HANDLE, &per_params); + if (err) { + ESP_LOGE(TAG, "Failed to configure per adv params, err %d", err); + return err; + } + + data = os_msys_get_pkthdr(per_len, 0); + if (data == NULL) { + ESP_LOGE(TAG, "Failed to get per adv mbuf"); + return -1; + } + + err = os_mbuf_append(data, per_data, per_len); + if (err) { + ESP_LOGE(TAG, "Failed to append per adv data, err %d", err); + os_mbuf_free_chain(data); + return err; + } + + err = ble_gap_periodic_adv_set_data(ADV_HANDLE, data); + if (err) { + ESP_LOGE(TAG, "Failed to set per adv data, err %d", err); + return err; + } + + err = ble_gap_periodic_adv_start(ADV_HANDLE); + if (err) { + ESP_LOGE(TAG, "Failed to start per advertising, err %d", err); + return err; + } + + err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0); + if (err) { + ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); + + return 0; +} 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 9ba26040ec5..cbc059c54a9 100644 --- a/examples/bluetooth/esp_ble_audio/bap/broadcast_source/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_source/sdkconfig.defaults @@ -3,14 +3,19 @@ # CONFIG_BT_ENABLED=y -CONFIG_BT_BLUEDROID_ENABLED=n -CONFIG_BT_NIMBLE_ENABLED=y -CONFIG_BT_NIMBLE_EXT_ADV=y -CONFIG_BT_NIMBLE_ISO=y -CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y +CONFIG_BT_NIMBLE_ENABLED=n +CONFIG_BT_BLUEDROID_ENABLED=y +CONFIG_BT_CLASSIC_ENABLED=n +CONFIG_BT_CONTROLLER_ENABLED=y +CONFIG_BT_BLE_ENABLED=y +CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y +CONFIG_BT_ACL_CONNECTIONS=1 +CONFIG_BT_BLE_FEAT_ISO_EN=y CONFIG_BT_ISO_MAX_CHAN=2 CONFIG_BT_BAP_BROADCAST_SOURCE=y +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y + CONFIG_FREERTOS_HZ=1000 diff --git a/examples/bluetooth/esp_ble_audio/bap/broadcast_source/sdkconfig.defaults.nimble b/examples/bluetooth/esp_ble_audio/bap/broadcast_source/sdkconfig.defaults.nimble new file mode 100644 index 00000000000..24402677507 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/bap/broadcast_source/sdkconfig.defaults.nimble @@ -0,0 +1,9 @@ +# 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_ISO=y +CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y 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 84b0d7922e2..e87b76fc90a 100644 --- a/examples/bluetooth/esp_ble_audio/bap/unicast_client/README.md +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_client/README.md @@ -7,7 +7,7 @@ ## Overview -This example implements a **BAP Unicast Client** on top of the NimBLE host stack. It scans for a peer advertising the ASCS service UUID with a complete local name of `BAP Unicast Server`, opens an ACL connection, initiates pairing, exchanges MTU, and starts GATT service discovery. +This example implements a **BAP Unicast Client** on top of the selected BLE host stack (Bluedroid by default; NimBLE via the `sdkconfig.defaults.nimble` overlay). It scans for a peer advertising the ASCS service UUID with a complete local name of `BAP Unicast Server`, opens an ACL connection, initiates pairing, exchanges MTU, and starts GATT service discovery. Once the peer is reachable, the client uses the BAP unicast client API (`esp_ble_audio_bap_unicast_client_discover`, `esp_ble_audio_bap_stream_config`, `esp_ble_audio_bap_unicast_group_create`, `esp_ble_audio_bap_stream_qos`, `esp_ble_audio_bap_stream_enable`, `esp_ble_audio_bap_stream_connect`, `esp_ble_audio_bap_stream_start`) to discover sink and source ASEs, configure each with the LC3 `16_2_1` unicast preset, build a unicast group, set QoS, enable, and ISO-connect the streams. Source (TX-from-server) streams are started by this client; sink (TX-to-server) streams are started by the server. @@ -34,19 +34,32 @@ This example inherits a Just-Works pairing model (LE Secure Connections, no MITM ## Build & Flash +The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`. + +### Bluedroid host (default) + ```bash idf.py set-target esp32h4 idf.py -p PORT flash monitor ``` +### NimBLE host + +```bash +idf.py set-target esp32h4 +idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor +``` + +For `esp32s31`, replace the chip overlay accordingly. + (Exit serial monitor with `Ctrl-]`.) ## Example Flow -1. `app_main` initializes NVS, NimBLE, and the LE Audio common layer, registers stream ops on every sink/source slot, registers the BAP unicast client callbacks, sets the device name `BAP Unicast Client`, and calls `ext_scan_start`. +1. `app_main` initializes NVS, the BLE host, runs `app_host_init` for host-specific setup (GAP callback, plus a Bluedroid-only GATTC app registration so we have a `gattc_if` for outgoing aux-open), brings up the LE Audio common layer, registers stream ops on every sink/source slot, registers the BAP unicast client callbacks, calls `set_device_name()` (uses `LOCAL_DEVICE_NAME` from `central.h`), then calls `ext_scan_start`. 2. Passive extended scanning runs forever; `scan_data_cb` matches the complete local name `BAP Unicast Server` and parses the ASCS service-data AD for announcement type plus a 32-bit available-contexts field. -3. On a connectable match `conn_create` cancels scanning and issues `ble_gap_connect`; the resulting `ESP_BLE_AUDIO_GAP_EVENT_ACL_CONNECT` saves the handle and calls `ble_gap_security_initiate`. -4. After the security-change event the client triggers `ble_gattc_exchange_mtu`; `gatt_mtu_change` then calls `esp_ble_audio_gattc_disc_start`, and once both MTU and discovery are reported `discover_sinks` runs. +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. @@ -116,8 +129,8 @@ I (xxx) BAP_UCL: Disconnected: handle ... reason 0x... Run [unicast_server](../unicast_server/) on a second board. 1. Server boots and starts connectable extended advertising with the ASCS UUID and `BAP Unicast Server` name. -2. Client boots and scans; on matching the name and ASCS service data it cancels scanning and issues `ble_gap_connect`. -3. After ACL is up the client initiates pairing/security; both sides log `Connected:` and the client logs `Security:`. +2. Client boots and scans; on matching the name and ASCS service data it cancels scanning and issues `conn_create` (NimBLE: `ble_gap_connect`; Bluedroid: `esp_ble_gattc_aux_open`). +3. After ACL is up the client initiates pairing/security; both sides log `Connected:` and `Security:`. 4. Client exchanges MTU and runs GATT discovery, then discovers sink ASEs followed by source ASEs on the server. 5. Client drives ASCS Config -> QoS -> Enable -> ISO Connect on every ASE; the server returns its QoS preference and accepts each operation. 6. Client starts source streams; server auto-starts each sink stream from `stream_enabled_cb`, after which the client's TX scheduler feeds SDUs and the server's `stream_recv_cb` accumulates RX metrics. diff --git a/examples/bluetooth/esp_ble_audio/bap/unicast_client/main/CMakeLists.txt b/examples/bluetooth/esp_ble_audio/bap/unicast_client/main/CMakeLists.txt index 3b4506801da..7666ff8aae9 100644 --- a/examples/bluetooth/esp_ble_audio/bap/unicast_client/main/CMakeLists.txt +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_client/main/CMakeLists.txt @@ -1,5 +1,12 @@ set(srcs "stream_tx.c" "main.c") -idf_component_register(SRCS "${srcs}" +if(CONFIG_BT_BLUEDROID_ENABLED) + list(APPEND srcs "bluedroid/central.c") +else() + list(APPEND srcs "nimble/central.c") +endif() + +idf_component_register(SRCS ${srcs} + INCLUDE_DIRS "." REQUIRES bt nvs_flash) diff --git a/examples/bluetooth/esp_ble_audio/bap/unicast_client/main/bluedroid/central.c b/examples/bluetooth/esp_ble_audio/bap/unicast_client/main/bluedroid/central.c new file mode 100644 index 00000000000..36a86e5139e --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_client/main/bluedroid/central.c @@ -0,0 +1,193 @@ +/* + * 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 "central.h" + +static SemaphoreHandle_t scan_sem; + +/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */ +static esp_bt_status_t scan_op_status; + +#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, scan_sem, portMAX_DELAY, scan_op_status) + +/* Cached peer address. Bluedroid's pairing and disconnect APIs key off + * bd_addr rather than conn_handle, so we stash the addr at conn_create + * time and reuse it in pairing_start / security_failed_recover. */ +static esp_bd_addr_t peer_bda; + +static esp_ble_ext_scan_params_t ext_scan_params = { + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE, + .cfg_mask = ESP_BLE_GAP_EXT_SCAN_CFG_UNCODE_MASK, + .uncoded_cfg = { + .scan_type = BLE_SCAN_TYPE_PASSIVE, + .scan_interval = SCAN_INTERVAL, + .scan_window = SCAN_WINDOW, + }, +}; + +static void gap_event_handler(esp_gap_ble_cb_event_t event, + esp_ble_gap_cb_param_t *param) +{ + switch (event) { + case ESP_GAP_BLE_SET_EXT_SCAN_PARAMS_COMPLETE_EVT: + scan_op_status = param->set_ext_scan_params.status; + xSemaphoreGive(scan_sem); + break; + case ESP_GAP_BLE_EXT_SCAN_START_COMPLETE_EVT: + scan_op_status = param->ext_scan_start.status; + xSemaphoreGive(scan_sem); + break; + case ESP_GAP_BLE_EXT_SCAN_STOP_COMPLETE_EVT: + scan_op_status = param->ext_scan_stop.status; + xSemaphoreGive(scan_sem); + break; + + /* SMP request handling for Just Works pairing (IO_CAP=NONE). NC_REQ + * still fires under LE Secure Connections — auto-accept. We never get + * SEC_REQ here because the central initiates via esp_ble_set_encryption. */ + case ESP_GAP_BLE_NC_REQ_EVT: + esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, true); + break; + + /* AUTH_CMPL has no BTA channel — app must forward it. EXT_ADV_REPORT + * is forwarded by adapter's BTA path, don't re-post here. */ + case ESP_GAP_BLE_AUTH_CMPL_EVT: + esp_ble_audio_gap_app_post_event(event, param); + break; + + default: + break; + } +} + +int app_host_init(void) +{ + esp_err_t err; + + scan_sem = xSemaphoreCreateBinary(); + if (scan_sem == NULL) { + ESP_LOGE(TAG, "Failed to create scan semaphore"); + return -1; + } + + err = esp_ble_gap_register_callback(gap_event_handler); + if (err) { + ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err); + vSemaphoreDelete(scan_sem); + return err; + } + + return 0; +} + +int set_device_name(void) +{ + return esp_ble_gap_set_device_name(LOCAL_DEVICE_NAME); +} + +int ext_scan_start(void) +{ + WAIT_API(esp_ble_gap_set_ext_scan_params(&ext_scan_params)); + WAIT_API(esp_ble_gap_start_ext_scan(0, 0)); + + ESP_LOGI(TAG, "Scanning for unicast server..."); + return ESP_OK; +} + +int ext_scan_stop(void) +{ + WAIT_API(esp_ble_gap_stop_ext_scan()); + return ESP_OK; +} + +int conn_create(uint8_t addr_type, const uint8_t addr[6]) +{ + /* Values shared with NimBLE side via central.h for behavioral parity. + * Without prefer_ext_connect_params_set, L2CAP falls back to defaults + * and logs "No extend connection parameters set". */ + const esp_ble_gap_conn_params_t conn_params = { + .scan_interval = INIT_SCAN_INTERVAL, + .scan_window = INIT_SCAN_WINDOW, + .interval_min = CONN_INTERVAL, + .interval_max = CONN_INTERVAL, + .latency = CONN_LATENCY, + .supervision_timeout = CONN_TIMEOUT, + .min_ce_len = CONN_MIN_CE_LEN, + .max_ce_len = CONN_MAX_CE_LEN, + }; + esp_gatt_if_t gattc_if; + esp_err_t err; + + memcpy(peer_bda, addr, sizeof(peer_bda)); + + err = esp_ble_gap_prefer_ext_connect_params_set( + peer_bda, ESP_BLE_GAP_PHY_1M_PREF_MASK, &conn_params, NULL, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to set ext conn params, err %d", err); + return err; + } + + /* Use the audio engine's GATTC if so OPEN/CONNECT events route back to + * the engine. aux_open initiates an ACL against an extended advertiser. + * + * engine returns ESP_GATT_IF_NONE (0xFF) when GATTC is not yet registered; + * feeding that into aux_open silently no-ops in BTC and no acl_connect + * event ever fires, leaving the caller in a no-scan / no-conn dead state. */ + gattc_if = esp_ble_audio_bluedroid_get_gattc_if(); + if (gattc_if == ESP_GATT_IF_NONE) { + ESP_LOGE(TAG, "GATTC not registered"); + return ESP_ERR_INVALID_STATE; + } + + return esp_ble_gattc_aux_open(gattc_if, peer_bda, + (esp_ble_addr_type_t)addr_type, true); +} + +int pairing_start(uint16_t conn_handle) +{ + (void)conn_handle; + return esp_ble_set_encryption(peer_bda, ESP_BLE_SEC_ENCRYPT_NO_MITM); +} + +int exchange_mtu(uint16_t conn_handle) +{ + (void)conn_handle; + /* The Bluedroid GATTC adapter exchanges MTU automatically when aux_open + * brings up the ACL — the MTU_UPDATED event arrives without an explicit + * kick-off. NimBLE has no such auto-exchange, so this wrapper is a no-op + * on bluedroid and a real ble_gattc_exchange_mtu on nimble. */ + return 0; +} + +void security_failed_recover(uint16_t conn_handle, uint8_t status) +{ + (void)conn_handle; + + /* Asymmetric bond state: we still hold an LTK for this peer but it + * cleared its side, so encrypt-with-cached-key times out. Drop the bond + * and tear down the link; the next reconnect runs fresh pairing. */ + ESP_LOGE(TAG, "Security change failed, status %u, clearing local bond and reconnecting", status); + + esp_ble_remove_bond_device(peer_bda); + esp_ble_gap_disconnect(peer_bda); +} diff --git a/examples/bluetooth/esp_ble_audio/bap/unicast_client/main/central.h b/examples/bluetooth/esp_ble_audio/bap/unicast_client/main/central.h new file mode 100644 index 00000000000..12692f847b8 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_client/main/central.h @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "ble_audio_example_utils.h" + +#define TAG "BAP_UCL" + +#define LOCAL_DEVICE_NAME "BAP Unicast Client" + +#define SCAN_INTERVAL 160 /* 100ms */ +#define SCAN_WINDOW 160 /* 100ms */ + +/* ACL init parameters shared between bluedroid and nimble host wrappers. + * Raw HCI units (scan: 0.625ms; conn interval: 1.25ms; timeout: 10ms). */ +#define INIT_SCAN_INTERVAL 16 /* 10ms */ +#define INIT_SCAN_WINDOW 16 /* 10ms */ +#define CONN_INTERVAL 24 /* 30ms */ +#define CONN_LATENCY 0 +#define CONN_TIMEOUT 500 /* 5s */ +#define CONN_MIN_CE_LEN 0xFFFF +#define CONN_MAX_CE_LEN 0xFFFF + +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); + +/* Recover from an SMP failure on the given connection — typically deletes the + * stale bond and terminates the link so the next reconnect runs fresh + * pairing. Host-specific implementation. */ +void security_failed_recover(uint16_t conn_handle, uint8_t status); 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 b1517eb001e..cb0cf4198f3 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 @@ -7,31 +7,16 @@ #include #include -#include #include #include "nvs_flash.h" -#include "esp_system.h" - -#include "host/ble_gap.h" -#include "services/gap/ble_svc_gap.h" +#include "central.h" #include "stream_tx.h" #define TARGET_DEVICE_NAME "BAP Unicast Server" #define TARGET_DEVICE_NAME_LEN (sizeof(TARGET_DEVICE_NAME) - 1) -#define SCAN_INTERVAL 160 /* 100ms */ -#define SCAN_WINDOW 160 /* 100ms */ - -#define INIT_SCAN_INTERVAL 16 /* 10ms */ -#define INIT_SCAN_WINDOW 16 /* 10ms */ -#define CONN_INTERVAL 64 /* 64 * 1.25 = 80ms */ -#define CONN_LATENCY 0 -#define CONN_TIMEOUT 500 /* 500 * 10ms = 5s */ -#define CONN_MAX_CE_LEN 0xFFFF -#define CONN_MIN_CE_LEN 0xFFFF -#define CONN_DURATION 10000 /* 10s */ #define CONN_HANDLE_INIT 0xFFFF #define ASCS_RSP_NONE (0b00) @@ -41,7 +26,6 @@ static uint16_t default_conn_handle = CONN_HANDLE_INIT; static bool disc_completed; -static bool disc_cancelled; static bool mtu_exchanged; ESP_BLE_AUDIO_BAP_LC3_UNICAST_PRESET_16_2_1_DEFINE(unicast_preset, @@ -81,8 +65,6 @@ static struct stream_pair_state { } stream_pairs[MAX(ARRAY_SIZE(sinks), ARRAY_SIZE(sources))]; static size_t stream_pair_count; -static int ext_scan_start(void); - static void reset_stream_pair_state(void) { stream_pair_count = 0; @@ -1049,49 +1031,6 @@ static esp_ble_audio_bap_unicast_client_cb_t unicast_client_cbs = { .discover = discover_cb, }; -static int conn_create(ble_addr_t *dst) -{ - struct ble_gap_conn_params params = {0}; - uint8_t own_addr_type = 0; - int err; - - err = ble_hs_id_infer_auto(0, &own_addr_type); - if (err) { - ESP_LOGE(TAG, "Failed to determine address type, err %d", err); - return err; - } - - err = ble_gap_disc_cancel(); - if (err) { - ESP_LOGE(TAG, "Failed to stop scanning, err %d", err); - return err; - } - - disc_cancelled = true; - - params.scan_itvl = INIT_SCAN_INTERVAL; - params.scan_window = INIT_SCAN_WINDOW; - params.itvl_min = CONN_INTERVAL; - params.itvl_max = CONN_INTERVAL; - params.latency = CONN_LATENCY; - params.supervision_timeout = CONN_TIMEOUT; - params.max_ce_len = CONN_MAX_CE_LEN; - params.min_ce_len = CONN_MIN_CE_LEN; - - return ble_gap_connect(own_addr_type, dst, CONN_DURATION, ¶ms, - example_audio_gap_event_cb, NULL); -} - -static int pairing_start(uint16_t conn_handle) -{ - return ble_gap_security_initiate(conn_handle); -} - -static int exchange_mtu(uint16_t conn_handle) -{ - return ble_gattc_exchange_mtu(conn_handle, NULL, NULL); -} - struct unicast_server_adv_data { bool target_matched; bool ascs_found; @@ -1157,7 +1096,6 @@ static bool scan_data_cb(uint8_t type, const uint8_t *data, static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event) { struct unicast_server_adv_data adv = {0}; - ble_addr_t dst = {0}; int err; if (default_conn_handle != CONN_HANDLE_INIT) { @@ -1178,17 +1116,21 @@ static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event) ESP_LOGI(TAG, "Unicast server found: type %u contexts 0x%08x", adv.announcement_type, adv.audio_contexts); - dst.type = event->ext_scan_recv.addr.type; - memcpy(dst.val, event->ext_scan_recv.addr.val, sizeof(dst.val)); + /* Stop scanning before connect — NimBLE rejects ble_gap_connect while + * a discovery procedure is running; Bluedroid prefers the same ordering + * even though it accepts both. On failure, restart scanning so we don't + * stall in a no-scan no-conn state. */ + err = ext_scan_stop(); + if (err) { + ESP_LOGE(TAG, "Failed to stop scanning, err %d", err); + return; + } - err = conn_create(&dst); + err = conn_create(event->ext_scan_recv.addr.type, + event->ext_scan_recv.addr.val); if (err) { ESP_LOGE(TAG, "Failed to create conn, err %d", err); - - if (disc_cancelled) { - disc_cancelled = false; - ext_scan_start(); - } + ext_scan_start(); } } @@ -1198,14 +1140,16 @@ static void acl_connect(esp_ble_audio_gap_app_event_t *event) if (event->acl_connect.status) { ESP_LOGE(TAG, "Connection failed, status %d", event->acl_connect.status); + /* ext_scan_recv stopped scanning before conn_create. acl_disconnect + * (which restarts scan) only fires on an established connection, so + * we must resume here to avoid a no-scan / no-conn dead state. */ + ext_scan_start(); return; } ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x", event->acl_connect.conn_handle, event->acl_connect.role, - event->acl_connect.dst.val[5], event->acl_connect.dst.val[4], - event->acl_connect.dst.val[3], event->acl_connect.dst.val[2], - event->acl_connect.dst.val[1], event->acl_connect.dst.val[0]); + EXAMPLE_BT_ADDR_PRINT_ARGS(event->acl_connect.dst.val)); default_conn_handle = event->acl_connect.conn_handle; @@ -1223,7 +1167,6 @@ static void acl_disconnect(esp_ble_audio_gap_app_event_t *event) default_conn_handle = CONN_HANDLE_INIT; disc_completed = false; - disc_cancelled = false; mtu_exchanged = false; /* Delete the group before reset_stream_state() memsets the streams: @@ -1240,13 +1183,13 @@ static void acl_disconnect(esp_ble_audio_gap_app_event_t *event) ext_scan_start(); } -static void security_change(esp_ble_iso_gap_app_event_t *event) +static void security_change(esp_ble_audio_gap_app_event_t *event) { int err; if (event->security_change.status) { - example_audio_security_failed_recover(TAG, event->security_change.conn_handle, - event->security_change.status); + security_failed_recover(event->security_change.conn_handle, + event->security_change.status); return; } @@ -1349,33 +1292,6 @@ static void iso_gatt_app_cb(esp_ble_audio_gatt_app_event_t *event) } } -static int ext_scan_start(void) -{ - struct ble_gap_disc_params params = {0}; - uint8_t own_addr_type; - int err; - - err = ble_hs_id_infer_auto(0, &own_addr_type); - if (err) { - ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err); - return err; - } - - params.passive = 1; - params.itvl = SCAN_INTERVAL; - params.window = SCAN_WINDOW; - - err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms, - example_audio_gap_event_cb, NULL); - if (err) { - ESP_LOGE(TAG, "Failed to start scanning, err %d", err); - return err; - } - - ESP_LOGI(TAG, "Scanning for unicast server..."); - return 0; -} - void app_main(void) { esp_ble_audio_init_info_t info = { @@ -1398,6 +1314,12 @@ void app_main(void) return; } + err = app_host_init(); + if (err) { + ESP_LOGE(TAG, "Failed to init host, err %d", err); + return; + } + err = esp_ble_audio_common_init(&info); if (err) { ESP_LOGE(TAG, "Failed to initialize audio, err %d", err); @@ -1428,7 +1350,7 @@ void app_main(void) return; } - err = ble_svc_gap_device_name_set("BAP Unicast Client"); + err = set_device_name(); if (err) { ESP_LOGE(TAG, "Failed to set device name, err %d", err); return; diff --git a/examples/bluetooth/esp_ble_audio/bap/unicast_client/main/nimble/central.c b/examples/bluetooth/esp_ble_audio/bap/unicast_client/main/nimble/central.c new file mode 100644 index 00000000000..2f8f1069a19 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_client/main/nimble/central.c @@ -0,0 +1,152 @@ +/* + * 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 "central.h" + +/* Init/conn parameters are shared with the bluedroid wrapper via central.h. + * CONN_DURATION is NimBLE-specific (ble_gap_connect's discovery timeout). */ +#define CONN_DURATION 10000 /* 10s */ + +static int gap_event_cb(struct ble_gap_event *event, void *arg) +{ + switch (event->type) { + case BLE_GAP_EVENT_EXT_DISC: + case BLE_GAP_EVENT_CONNECT: + case BLE_GAP_EVENT_DISCONNECT: + case BLE_GAP_EVENT_ENC_CHANGE: + esp_ble_audio_gap_app_post_event(event->type, event); + break; + case BLE_GAP_EVENT_MTU: + case BLE_GAP_EVENT_NOTIFY_RX: + case BLE_GAP_EVENT_NOTIFY_TX: + case BLE_GAP_EVENT_SUBSCRIBE: + esp_ble_audio_gatt_app_post_event(event->type, event); + break; + case BLE_GAP_EVENT_REPEAT_PAIRING: { + struct ble_gap_conn_desc desc = {0}; + int rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc); + if (rc == 0) { + ble_store_util_delete_peer(&desc.peer_id_addr); + } + return BLE_GAP_REPEAT_PAIRING_RETRY; + } + default: + break; + } + + return 0; +} + +int app_host_init(void) +{ + return 0; +} + +int set_device_name(void) +{ + return ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME); +} + +int ext_scan_start(void) +{ + struct ble_gap_disc_params params = {0}; + uint8_t own_addr_type; + int err; + + err = ble_hs_id_infer_auto(0, &own_addr_type); + if (err) { + ESP_LOGE(TAG, "Failed to determine address type, err %d", err); + return err; + } + + params.passive = 1; + params.itvl = SCAN_INTERVAL; + params.window = SCAN_WINDOW; + + err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms, + gap_event_cb, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to start scanning, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Scanning for unicast server..."); + return 0; +} + +int ext_scan_stop(void) +{ + return ble_gap_disc_cancel(); +} + +int conn_create(uint8_t addr_type, const uint8_t addr[6]) +{ + struct ble_gap_conn_params params = {0}; + uint8_t own_addr_type = 0; + ble_addr_t dst = {0}; + int err; + + err = ble_hs_id_infer_auto(0, &own_addr_type); + if (err) { + ESP_LOGE(TAG, "Failed to determine address type, err %d", err); + return err; + } + + params.scan_itvl = INIT_SCAN_INTERVAL; + params.scan_window = INIT_SCAN_WINDOW; + params.itvl_min = CONN_INTERVAL; + params.itvl_max = CONN_INTERVAL; + params.latency = CONN_LATENCY; + params.supervision_timeout = CONN_TIMEOUT; + params.max_ce_len = CONN_MAX_CE_LEN; + params.min_ce_len = CONN_MIN_CE_LEN; + + dst.type = addr_type; + memcpy(dst.val, addr, sizeof(dst.val)); + + return ble_gap_connect(own_addr_type, &dst, CONN_DURATION, + ¶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; + + /* status 13 = BLE_HS_ETIMEOUT: SMP exchange did not complete. Typical + * cause is asymmetric bond state — drop the stale entry and tear down + * the link so the next reconnect runs fresh pairing. */ + ESP_LOGE(TAG, "Security change failed, status %u, clearing local bond and reconnecting", status); + + rc = ble_gap_conn_find(conn_handle, &desc); + if (rc == 0) { + ble_store_util_delete_peer(&desc.peer_id_addr); + } + + ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM); +} 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 c1c26bbc584..56525a97b23 100644 --- a/examples/bluetooth/esp_ble_audio/bap/unicast_client/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_client/sdkconfig.defaults @@ -3,18 +3,21 @@ # CONFIG_BT_ENABLED=y -CONFIG_BT_BLUEDROID_ENABLED=n -CONFIG_BT_NIMBLE_ENABLED=y -CONFIG_BT_NIMBLE_EXT_ADV=y -CONFIG_BT_NIMBLE_NVS_PERSIST=y -CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 -CONFIG_BT_NIMBLE_MAX_CCCDS=20 -CONFIG_BT_NIMBLE_ISO=y -CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y +CONFIG_BT_NIMBLE_ENABLED=n +CONFIG_BT_BLUEDROID_ENABLED=y +CONFIG_BT_CLASSIC_ENABLED=n +CONFIG_BT_CONTROLLER_ENABLED=y +CONFIG_BT_BLE_ENABLED=y +CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y +CONFIG_BT_ACL_CONNECTIONS=1 +CONFIG_BT_GATTC_NOTIF_REG_MAX=20 +CONFIG_BT_BLE_FEAT_ISO_EN=y CONFIG_BT_ISO_MAX_CHAN=2 CONFIG_BT_BAP_UNICAST_CLIENT=y CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT=2 +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y + CONFIG_FREERTOS_HZ=1000 diff --git a/examples/bluetooth/esp_ble_audio/bap/unicast_client/sdkconfig.defaults.nimble b/examples/bluetooth/esp_ble_audio/bap/unicast_client/sdkconfig.defaults.nimble new file mode 100644 index 00000000000..ea3029ef03f --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_client/sdkconfig.defaults.nimble @@ -0,0 +1,12 @@ +# NimBLE host overlay for this example. +# Use with: +# idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.$IDF_TARGET;sdkconfig.defaults.nimble" build + +CONFIG_BT_BLUEDROID_ENABLED=n +CONFIG_BT_NIMBLE_ENABLED=y +CONFIG_BT_NIMBLE_EXT_ADV=y +CONFIG_BT_NIMBLE_NVS_PERSIST=y +CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 +CONFIG_BT_NIMBLE_MAX_CCCDS=20 +CONFIG_BT_NIMBLE_ISO=y +CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y diff --git a/examples/bluetooth/esp_ble_audio/bap/unicast_server/README.md b/examples/bluetooth/esp_ble_audio/bap/unicast_server/README.md index 3431689d4a3..e305cd99902 100644 --- a/examples/bluetooth/esp_ble_audio/bap/unicast_server/README.md +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_server/README.md @@ -7,7 +7,7 @@ ## Overview -This example implements a **BAP Unicast Server** on top of the NimBLE host stack. It registers PACS sink and source capabilities (LC3, any frequency, 10 ms frame duration, 2-channel support, 40-120 octets per frame, up to 2 frames per SDU) and a hard-coded location/context set (front-left + front-right; UNSPECIFIED | CONVERSATIONAL | MEDIA), then starts connectable extended advertising on 1M/2M PHY whose payload carries the Flags AD, the ASCS UUID, the targeted unicast announcement service data with sink/source contexts, and the device name `BAP Unicast Server`. +This example implements a **BAP Unicast Server** on top of the selected BLE host stack (Bluedroid by default; NimBLE via the `sdkconfig.defaults.nimble` overlay). It registers PACS sink and source capabilities (LC3, any frequency, 10 ms frame duration, 2-channel support, 40-120 octets per frame, up to 2 frames per SDU) and a hard-coded location/context set (front-left + front-right; UNSPECIFIED | CONVERSATIONAL | MEDIA), then starts connectable extended advertising on 1M/2M PHY whose payload carries the Flags AD, the ASCS UUID, the targeted unicast announcement service data with sink/source contexts, and the device name `BAP Unicast Server`. The unicast server callbacks (`config`, `reconfig`, `qos`, `enable`, `start`, `metadata`, `disable`, `stop`, `release`) react to ASCS PDUs from a peer client. `config_cb` allocates a free `sink_streams[]`/`source_streams[]` slot and returns a fixed `qos_pref` (unframed PDUs, 2M PHY preference, RTN 2, 10 ms max latency, 20-40 ms presentation delay). `enable_cb` decodes the codec config to sample rate, frame duration, and frame-blocks-per-SDU before accepting the request. Reconfig is rejected unconditionally. @@ -34,11 +34,24 @@ This example inherits a Just-Works pairing model (LE Secure Connections, no MITM ## Build & Flash +The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`. + +### Bluedroid host (default) + ```bash idf.py set-target esp32h4 idf.py -p PORT flash monitor ``` +### NimBLE host + +```bash +idf.py set-target esp32h4 +idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor +``` + +For `esp32s31`, replace the chip overlay accordingly. + (Exit serial monitor with `Ctrl-]`.) ## Example Flow @@ -46,7 +59,7 @@ idf.py -p PORT flash monitor 1. `app_main` initializes NVS, NimBLE, and the LE Audio common layer, then calls `esp_ble_audio_pacs_register` with `snk_pac/snk_loc/src_pac/src_loc` all true. 2. It registers the unicast server (`esp_ble_audio_bap_unicast_server_register` with `CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT` / `CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT`) and its callback table, registers sink and source PACS capabilities, and attaches `stream_ops` to every preallocated stream slot. 3. PACS location is set per direction (`SINK_LOCATION` / `SOURCE_LOCATION`) and supported plus available contexts are pushed for sink and source. -4. `esp_ble_audio_common_start` and `ble_svc_gap_device_name_set("BAP Unicast Server")` run, then `ext_adv_start` builds `ext_adv_data` with Flags, ASCS UUID, ASCS service data (targeted announcement + contexts), and the device name; it configures and starts the extended advertising set on `ADV_HANDLE` 0. +4. `esp_ble_audio_common_start` runs, then `adv_init("BAP Unicast Server")` performs host-specific setup (Bluedroid: create completion sem + register GAP cb + `esp_ble_gap_set_device_name`; NimBLE: `ble_svc_gap_device_name_set`). `ext_adv_start` then builds `ext_adv_data` with Flags, ASCS UUID, ASCS service data (targeted announcement + contexts), and the device name in host-agnostic bytes, and calls `adv_start(ext_adv_data, sizeof(ext_adv_data))`. The host-specific implementation in `main/bluedroid/adv.c` or `main/nimble/adv.c` configures and starts the connectable extended advertising set on `ADV_HANDLE` 0. 5. On `ESP_BLE_AUDIO_GAP_EVENT_ACL_CONNECT` the connection is logged; after the peer client triggers MTU exchange the server logs the new MTU and calls `esp_ble_audio_gattc_disc_start`. 6. ASCS callbacks fire as the client drives the state machine: `config_cb` allocates a stream and returns the QoS preference, `qos_cb` records `max_sdu`, `enable_cb` validates the codec, `metadata_cb` parses metadata, and `disable`/`stop`/`release` log their requests. 7. `stream_enabled_cb` auto-starts sink streams server-side; `stream_started_cb` resets TX or RX counters depending on direction; `stream_recv_cb` updates per-stream RX metrics on every ISO SDU. @@ -67,6 +80,7 @@ Client connects: ``` I (xxx) BAP_USR: Connected: handle ... role ... peer ... +I (xxx) BAP_USR: Security: handle ... level ... bonded ... I (xxx) BAP_USR: MTU updated: handle ... mtu ... I (xxx) BAP_USR: Service discovery started: handle ... I (xxx) BAP_USR: Service discovery complete: handle ... status ... @@ -97,13 +111,26 @@ I (xxx) BAP_USR: [SNK #...] Release request I (xxx) BAP_USR: Disconnected: handle ... reason 0x... ``` +**Note — disconnect race.** On peer-initiated disconnect (e.g. supervision timeout, peer drops the link) with active streams, an additional Bluedroid error may interleave: + +``` +W (xxx) BT_APPL: gattc_conn_cb: if=4 st=0 id=4 rsn=0x8 +W (xxx) BT_HCI: hcif disc complete: hdl 0x0, rsn 0x8 dev_find 1 +I (xxx) BAP_USR: [SNK #0] ISO disconnected, reason 0x08 +I (xxx) BAP_USR: [SNK #0] Stream disabled +E (xxx) BT_APPL: Unknown connection ID: 3 fail sending notification +I (xxx) BAP_USR: [SNK #0] Stream stopped, reason 0x08 +``` + +`Unknown connection ID` is harmless. GATT notifications for the ASE state changes are queued for Bluedroid to send. If Bluedroid clears the BTA connection on the ACL disconnect before those queued sends drain, they fail with this error. The peer has already disconnected, so the missed notifications have no effect on either side. + ## Peer Pairing Run [unicast_client](../unicast_client/) on a second board. 1. Server boots and starts connectable extended advertising with the ASCS UUID and `BAP Unicast Server` name. 2. Client boots and scans; on matching the name and ASCS service data it cancels scanning and issues `ble_gap_connect`. -3. After ACL is up the client initiates pairing/security; both sides log `Connected:` and the client logs `Security:`. +3. After ACL is up the client initiates pairing/security; both sides log `Connected:` and `Security:`. 4. Client exchanges MTU and runs GATT discovery, then discovers sink ASEs followed by source ASEs on the server. 5. Client drives ASCS Config -> QoS -> Enable -> ISO Connect on every ASE; the server returns its QoS preference and accepts each operation. 6. Client starts source streams; server auto-starts each sink stream from `stream_enabled_cb`, after which the client's TX scheduler feeds SDUs and the server's `stream_recv_cb` accumulates RX metrics. diff --git a/examples/bluetooth/esp_ble_audio/bap/unicast_server/main/CMakeLists.txt b/examples/bluetooth/esp_ble_audio/bap/unicast_server/main/CMakeLists.txt index 721967052c7..466446747c8 100644 --- a/examples/bluetooth/esp_ble_audio/bap/unicast_server/main/CMakeLists.txt +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_server/main/CMakeLists.txt @@ -1,4 +1,11 @@ set(srcs "main.c") -idf_component_register(SRCS "${srcs}" +if(CONFIG_BT_BLUEDROID_ENABLED) + list(APPEND srcs "bluedroid/peripheral.c") +else() + list(APPEND srcs "nimble/peripheral.c") +endif() + +idf_component_register(SRCS ${srcs} + INCLUDE_DIRS "." REQUIRES bt nvs_flash) diff --git a/examples/bluetooth/esp_ble_audio/bap/unicast_server/main/bluedroid/peripheral.c b/examples/bluetooth/esp_ble_audio/bap/unicast_server/main/bluedroid/peripheral.c new file mode 100644 index 00000000000..57c8b023964 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_server/main/bluedroid/peripheral.c @@ -0,0 +1,117 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#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_ble_audio_common_api.h" + +#include "peripheral.h" + +static SemaphoreHandle_t adv_sem; + +/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */ +static esp_bt_status_t adv_op_status; + +#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, adv_sem, portMAX_DELAY, adv_op_status) + +static esp_ble_gap_ext_adv_params_t ext_adv_params = { + .type = ESP_BLE_GAP_SET_EXT_ADV_PROP_CONNECTABLE, + .interval_min = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS), + .interval_max = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS), + .channel_map = ADV_CHNL_ALL, + .filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, + .primary_phy = ESP_BLE_GAP_PHY_1M, + .max_skip = 0, + .secondary_phy = ESP_BLE_GAP_PHY_2M, + .sid = ADV_SID, + .scan_req_notif = false, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .tx_power = ADV_TX_POWER, +}; + +static esp_ble_gap_ext_adv_t ext_adv_inst[1] = { + [0] = { ADV_HANDLE, 0, 0 }, +}; + +static void gap_event_handler(esp_gap_ble_cb_event_t event, + esp_ble_gap_cb_param_t *param) +{ + switch (event) { + 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; + + /* SMP request handling for Just Works pairing (IO_CAP=NONE). The peer + * (central) initiates; we accept the security request and confirm the + * numeric comparison. */ + case ESP_GAP_BLE_SEC_REQ_EVT: + esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true); + break; + case ESP_GAP_BLE_NC_REQ_EVT: + esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, true); + break; + + case ESP_GAP_BLE_AUTH_CMPL_EVT: + esp_ble_audio_gap_app_post_event(event, param); + break; + + default: + break; + } +} + +int app_host_init(void) +{ + esp_err_t err; + + adv_sem = xSemaphoreCreateBinary(); + if (adv_sem == NULL) { + ESP_LOGE(TAG, "Failed to create adv semaphore"); + return -1; + } + + err = esp_ble_gap_register_callback(gap_event_handler); + if (err) { + ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err); + vSemaphoreDelete(adv_sem); + return err; + } + + return 0; +} + +int set_device_name(void) +{ + return esp_ble_gap_set_device_name(LOCAL_DEVICE_NAME); +} + +int ext_adv_start(const uint8_t *ext_data, uint16_t ext_len) +{ + WAIT_API(esp_ble_gap_ext_adv_set_params(ADV_HANDLE, &ext_adv_params)); + WAIT_API(esp_ble_gap_config_ext_adv_data_raw(ADV_HANDLE, ext_len, ext_data)); + WAIT_API(esp_ble_gap_ext_adv_start(1, ext_adv_inst)); + + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); + return ESP_OK; +} diff --git a/examples/bluetooth/esp_ble_audio/bap/unicast_server/main/main.c b/examples/bluetooth/esp_ble_audio/bap/unicast_server/main/main.c index c922bb4f521..cc70105ec4f 100644 --- a/examples/bluetooth/esp_ble_audio/bap/unicast_server/main/main.c +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_server/main/main.c @@ -13,11 +13,6 @@ #include "esp_log.h" #include "nvs_flash.h" -#include "esp_system.h" -#include "esp_timer.h" - -#include "host/ble_hs.h" -#include "services/gap/ble_svc_gap.h" #include "esp_ble_audio_lc3_defs.h" #include "esp_ble_audio_bap_api.h" @@ -26,18 +21,7 @@ #include "ble_audio_example_init.h" #include "ble_audio_example_utils.h" -#define TAG "BAP_USR" - -#define LOCAL_DEVICE_NAME "BAP Unicast Server" -#define LOCAL_DEVICE_NAME_LEN (sizeof(LOCAL_DEVICE_NAME) - 1) - -#define ADV_HANDLE 0x00 -#define ADV_SID 0 -#define ADV_TX_POWER 127 -#define ADV_ADDRESS BLE_OWN_ADDR_PUBLIC -#define ADV_PRIMARY_PHY BLE_HCI_LE_PHY_1M -#define ADV_SECONDARY_PHY BLE_HCI_LE_PHY_2M -#define ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(200) +#include "peripheral.h" #define SINK_LOCATION (ESP_BLE_AUDIO_LOCATION_FRONT_LEFT | \ ESP_BLE_AUDIO_LOCATION_FRONT_RIGHT) @@ -536,7 +520,7 @@ static int set_pacs_available_contexts(void) return 0; } -static void build_adv_data(void) +static void build_ext_adv_data(void) { size_t idx = 0; @@ -569,60 +553,6 @@ static void build_adv_data(void) memcpy(&ext_adv_data[idx], LOCAL_DEVICE_NAME, LOCAL_DEVICE_NAME_LEN); } -static void ext_adv_start(void) -{ - struct ble_gap_ext_adv_params ext_params = {0}; - struct os_mbuf *data = NULL; - int err; - - build_adv_data(); - - ext_params.connectable = 1; - ext_params.scannable = 0; - ext_params.legacy_pdu = 0; - ext_params.own_addr_type = ADV_ADDRESS; - ext_params.primary_phy = ADV_PRIMARY_PHY; - ext_params.secondary_phy = ADV_SECONDARY_PHY; - ext_params.tx_power = ADV_TX_POWER; - ext_params.sid = ADV_SID; - ext_params.itvl_min = ADV_INTERVAL; - ext_params.itvl_max = ADV_INTERVAL; - - err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL, - example_audio_gap_event_cb, NULL); - if (err) { - ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err); - return; - } - - data = os_msys_get_pkthdr(sizeof(ext_adv_data), 0); - if (data == NULL) { - ESP_LOGE(TAG, "Failed to get ext adv mbuf"); - return; - } - - err = os_mbuf_append(data, ext_adv_data, sizeof(ext_adv_data)); - if (err) { - ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err); - os_mbuf_free_chain(data); - return; - } - - err = ble_gap_ext_adv_set_data(ADV_HANDLE, data); - if (err) { - ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err); - return; - } - - err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0); - if (err) { - ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err); - return; - } - - ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); -} - static void acl_connect(esp_ble_audio_gap_app_event_t *event) { if (event->acl_connect.status) { @@ -632,9 +562,7 @@ static void acl_connect(esp_ble_audio_gap_app_event_t *event) ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x", event->acl_connect.conn_handle, event->acl_connect.role, - event->acl_connect.dst.val[5], event->acl_connect.dst.val[4], - event->acl_connect.dst.val[3], event->acl_connect.dst.val[2], - event->acl_connect.dst.val[1], event->acl_connect.dst.val[0]); + EXAMPLE_BT_ADDR_PRINT_ARGS(event->acl_connect.dst.val)); } static void acl_disconnect(esp_ble_audio_gap_app_event_t *event) @@ -642,7 +570,22 @@ 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); - ext_adv_start(); + ext_adv_start(ext_adv_data, sizeof(ext_adv_data)); +} + +static void security_change(esp_ble_audio_gap_app_event_t *event) +{ + if (event->security_change.status) { + ESP_LOGE(TAG, "Security failed: handle %u status %d", + event->security_change.conn_handle, + event->security_change.status); + return; + } + + ESP_LOGI(TAG, "Security: handle %u level %u bonded %u", + event->security_change.conn_handle, + event->security_change.sec_level, + event->security_change.bonded); } static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event) @@ -654,6 +597,9 @@ static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event) case ESP_BLE_AUDIO_GAP_EVENT_ACL_DISCONNECT: acl_disconnect(event); break; + case ESP_BLE_AUDIO_GAP_EVENT_SECURITY_CHANGE: + security_change(event); + break; default: break; } @@ -735,6 +681,12 @@ void app_main(void) return; } + err = app_host_init(); + if (err) { + ESP_LOGE(TAG, "Failed to init host, err %d", err); + return; + } + err = esp_ble_audio_common_init(&info); if (err) { ESP_LOGE(TAG, "Failed to initialize audio, err %d", err); @@ -800,11 +752,13 @@ void app_main(void) return; } - err = ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME); + err = set_device_name(); if (err) { ESP_LOGE(TAG, "Failed to set device name, err %d", err); return; } - ext_adv_start(); + build_ext_adv_data(); + + ext_adv_start(ext_adv_data, sizeof(ext_adv_data)); } diff --git a/examples/bluetooth/esp_ble_audio/bap/unicast_server/main/nimble/peripheral.c b/examples/bluetooth/esp_ble_audio/bap/unicast_server/main/nimble/peripheral.c new file mode 100644 index 00000000000..3f4cd2fd593 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_server/main/nimble/peripheral.c @@ -0,0 +1,115 @@ +/* + * 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_store.h" +#include "services/gap/ble_svc_gap.h" + +#include "esp_ble_audio_common_api.h" + +#include "peripheral.h" + +static int gap_event_cb(struct ble_gap_event *event, void *arg) +{ + switch (event->type) { + case BLE_GAP_EVENT_CONNECT: + case BLE_GAP_EVENT_DISCONNECT: + case BLE_GAP_EVENT_ENC_CHANGE: + esp_ble_audio_gap_app_post_event(event->type, event); + break; + case BLE_GAP_EVENT_MTU: + case BLE_GAP_EVENT_NOTIFY_RX: + case BLE_GAP_EVENT_NOTIFY_TX: + case BLE_GAP_EVENT_SUBSCRIBE: + esp_ble_audio_gatt_app_post_event(event->type, event); + break; + case BLE_GAP_EVENT_REPEAT_PAIRING: { + /* Peer wants to re-pair on top of an existing bond — delete our stale + * bond entry and tell NimBLE to retry pairing. */ + struct ble_gap_conn_desc desc = {0}; + int rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc); + if (rc == 0) { + ble_store_util_delete_peer(&desc.peer_id_addr); + } + return BLE_GAP_REPEAT_PAIRING_RETRY; + } + default: + break; + } + + return 0; +} + +int app_host_init(void) +{ + return 0; +} + +int set_device_name(void) +{ + return ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME); +} + +int ext_adv_start(const uint8_t *ext_data, uint16_t ext_len) +{ + struct ble_gap_ext_adv_params ext_params = {0}; + struct os_mbuf *data; + int err; + + ext_params.connectable = 1; + ext_params.scannable = 0; + ext_params.legacy_pdu = 0; + ext_params.own_addr_type = BLE_OWN_ADDR_PUBLIC; + ext_params.primary_phy = BLE_HCI_LE_PHY_1M; + ext_params.secondary_phy = BLE_HCI_LE_PHY_2M; + ext_params.tx_power = ADV_TX_POWER; + ext_params.sid = ADV_SID; + ext_params.itvl_min = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS); + ext_params.itvl_max = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS); + + err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL, + gap_event_cb, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err); + return err; + } + + data = os_msys_get_pkthdr(ext_len, 0); + if (data == NULL) { + ESP_LOGE(TAG, "Failed to get ext adv mbuf"); + return -1; + } + + err = os_mbuf_append(data, ext_data, ext_len); + if (err) { + ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err); + os_mbuf_free_chain(data); + return err; + } + + err = ble_gap_ext_adv_set_data(ADV_HANDLE, data); + if (err) { + ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err); + /* ble_gap_ext_adv_set_data takes ownership of `data` and frees it via + * its 'done' label on both success and failure paths — do NOT free + * here (double-free). API header doesn't document this contract. */ + return err; + } + + err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0); + if (err) { + ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); + + return 0; +} diff --git a/examples/bluetooth/esp_ble_audio/bap/unicast_server/main/peripheral.h b/examples/bluetooth/esp_ble_audio/bap/unicast_server/main/peripheral.h new file mode 100644 index 00000000000..b2ee739dedd --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_server/main/peripheral.h @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "ble_audio_example_utils.h" + +#define TAG "BAP_USR" + +#define LOCAL_DEVICE_NAME "BAP Unicast Server" +#define LOCAL_DEVICE_NAME_LEN (sizeof(LOCAL_DEVICE_NAME) - 1) + +#define ADV_HANDLE 0 +#define ADV_SID 0 +#define ADV_TX_POWER 127 +#define ADV_INTERVAL_MS 200 + +int app_host_init(void); + +int set_device_name(void); + +int ext_adv_start(const uint8_t *ext_data, uint16_t ext_len); 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 5859e2c3258..0b97edc72f0 100644 --- a/examples/bluetooth/esp_ble_audio/bap/unicast_server/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_server/sdkconfig.defaults @@ -3,14 +3,15 @@ # CONFIG_BT_ENABLED=y -CONFIG_BT_BLUEDROID_ENABLED=n -CONFIG_BT_NIMBLE_ENABLED=y -CONFIG_BT_NIMBLE_EXT_ADV=y -CONFIG_BT_NIMBLE_NVS_PERSIST=y -CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 -CONFIG_BT_NIMBLE_MAX_CCCDS=20 -CONFIG_BT_NIMBLE_ISO=y -CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y +CONFIG_BT_NIMBLE_ENABLED=n +CONFIG_BT_BLUEDROID_ENABLED=y +CONFIG_BT_CLASSIC_ENABLED=n +CONFIG_BT_CONTROLLER_ENABLED=y +CONFIG_BT_BLE_ENABLED=y +CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y +CONFIG_BT_ACL_CONNECTIONS=1 +CONFIG_BT_GATTC_NOTIF_REG_MAX=20 +CONFIG_BT_BLE_FEAT_ISO_EN=y CONFIG_BT_ISO_MAX_CHAN=2 @@ -24,4 +25,6 @@ CONFIG_BT_PAC_SRC_LOC_WRITEABLE=y CONFIG_BT_PAC_SRC_LOC_NOTIFIABLE=y CONFIG_BT_PACS_SUPPORTED_CONTEXT_NOTIFIABLE=y +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y + CONFIG_FREERTOS_HZ=1000 diff --git a/examples/bluetooth/esp_ble_audio/bap/unicast_server/sdkconfig.defaults.nimble b/examples/bluetooth/esp_ble_audio/bap/unicast_server/sdkconfig.defaults.nimble new file mode 100644 index 00000000000..ea3029ef03f --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/bap/unicast_server/sdkconfig.defaults.nimble @@ -0,0 +1,12 @@ +# NimBLE host overlay for this example. +# Use with: +# idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.$IDF_TARGET;sdkconfig.defaults.nimble" build + +CONFIG_BT_BLUEDROID_ENABLED=n +CONFIG_BT_NIMBLE_ENABLED=y +CONFIG_BT_NIMBLE_EXT_ADV=y +CONFIG_BT_NIMBLE_NVS_PERSIST=y +CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 +CONFIG_BT_NIMBLE_MAX_CCCDS=20 +CONFIG_BT_NIMBLE_ISO=y +CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y diff --git a/examples/bluetooth/esp_ble_audio/cap/acceptor/README.md b/examples/bluetooth/esp_ble_audio/cap/acceptor/README.md index 2d936b9589b..ba4f1f56898 100644 --- a/examples/bluetooth/esp_ble_audio/cap/acceptor/README.md +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/README.md @@ -7,7 +7,7 @@ ## Overview -This example implements the **Common Audio Profile (CAP) Acceptor** role on top of the NimBLE host stack with ISO and LE Audio support. It can be built with one or both roles selected at build time: a **CAP Acceptor / BAP Unicast Server**, and / or a **CAP Acceptor / BAP Broadcast Sink** (with the BAP Scan Delegator role enabled). Dual-role builds are supported (BAP spec C.2). PACS is registered in all configurations with LC3 sink and source capabilities (any sampling frequency, 7.5 ms or 10 ms frame, up to 2 channels, 30..155 octets per frame, up to 2 frames per SDU). Sink and source PAC location are set to `FRONT_LEFT | FRONT_RIGHT` (note in `main.c`: with `MONO_AUDIO`, Samsung S24 declines unicast). +This example implements the **Common Audio Profile (CAP) Acceptor** role on top of the selected BLE host stack (Bluedroid by default; NimBLE via the `sdkconfig.defaults.nimble` overlay) with ISO and LE Audio support. It can be built with one or both roles selected at build time: a **CAP Acceptor / BAP Unicast Server**, and / or a **CAP Acceptor / BAP Broadcast Sink** (with the BAP Scan Delegator role enabled). Dual-role builds are supported (BAP spec C.2). PACS is registered in all configurations with LC3 sink and source capabilities (any sampling frequency, 7.5 ms or 10 ms frame, up to 2 channels, 30..155 octets per frame, up to 2 frames per SDU). Sink and source PAC location are set to `FRONT_LEFT | FRONT_RIGHT` (note in `main.c`: with `MONO_AUDIO`, Samsung S24 declines unicast). In **unicast** mode (`cap_acceptor_unicast.c`) the acceptor registers BAP unicast-server callbacks (config / reconfig / qos / enable / start / metadata / disable / stop / release) and CAP stream ops, and on enable of a sink ASE automatically issues `bap_stream_start` (Receiver Start Ready). When the source ASE starts, an internal TX scheduler begins sending dummy SDUs. In **broadcast** mode (`cap_acceptor_broadcast.c`) the acceptor registers BAP scan-delegator and broadcast-sink callbacks, drives PA sync (preferring PAST when the Assistant supports it) on request, receives BASE / BIGInfo / broadcast code, then calls `esp_ble_audio_bap_broadcast_sink_sync` on the BIS bitmap selected by the Assistant via BASS Modify Source — up to `CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT` BIS streams. The Broadcast Sink object is created on PA sync and deleted on PA loss; Assistant pause / resume keep the same sink. See **Broadcast Mode Internals** below for sequence, gate flags and sink lifecycle. Optional **self-scan** lets the acceptor scan for a broadcast source named `CAP Broadcast Source` and use a hardcoded broadcast code `1234` instead of waiting for a Broadcast Assistant. @@ -41,11 +41,24 @@ Just-Works pairing (LE Secure Connections, no MITM, `BLE_SM_IO_CAP_NO_IO`) with ## Build & Flash +The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`. + +### Bluedroid host (default) + ```bash idf.py set-target esp32h4 idf.py -p PORT flash monitor ``` +### NimBLE host + +```bash +idf.py set-target esp32h4 +idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor +``` + +For `esp32s31`, replace the chip overlay accordingly. + (Exit serial monitor with `Ctrl-]`.) ## Example Flow @@ -195,6 +208,7 @@ TAG: `CAP_ACC`. ``` I (xxx) CAP_ACC: Advertising started (handle 0) I (xxx) CAP_ACC: Connected: handle ... role ... peer ... +I (xxx) CAP_ACC: Security: handle ... level ... bonded ... I (xxx) CAP_ACC: MTU updated: handle ... mtu ... I (xxx) CAP_ACC: Service discovery started: handle ... I (xxx) CAP_ACC: Service discovery complete: handle ... @@ -227,11 +241,25 @@ I (xxx) CAP_ACC: Disconnected: handle ... reason 0x... I (xxx) CAP_ACC: Advertising started (handle 0) ``` +**Note — disconnect race.** On peer-initiated disconnect (e.g. supervision timeout, peer drops the link) with active streams, an additional Bluedroid error may interleave: + +``` +W (xxx) BT_APPL: gattc_conn_cb: if=4 st=0 id=4 rsn=0x8 +W (xxx) BT_HCI: hcif disc complete: hdl 0x0, rsn 0x8 dev_find 1 +I (xxx) CAP_ACC: [SNK #0] ISO disconnected, reason 0x08 +I (xxx) CAP_ACC: [SNK #0] Stream disabled +E (xxx) BT_APPL: Unknown connection ID: 3 fail sending notification +I (xxx) CAP_ACC: [SNK #0] Stream stopped, reason 0x08 +``` + +`Unknown connection ID` is harmless. GATT notifications for the ASE state changes are queued for Bluedroid to send. If Bluedroid clears the BTA connection on the ACL disconnect before those queued sends drain, they fail with this error. The peer has already disconnected, so the missed notifications have no effect on either side. + ### Broadcast mode (Broadcast Assistant) ``` I (xxx) CAP_ACC: Advertising started (handle 0) I (xxx) CAP_ACC: Connected: handle ... role ... peer ... +I (xxx) CAP_ACC: Security: handle ... level ... bonded ... I (xxx) CAP_ACC: Receive state updated, pa_sync 0x... encrypt 0x... I (xxx) CAP_ACC: Received request to sync to PA (PAST not available): ... I (xxx) CAP_ACC: Syncing without PAST diff --git a/examples/bluetooth/esp_ble_audio/cap/acceptor/main/CMakeLists.txt b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/CMakeLists.txt index fa2da099048..f7ff55b3080 100644 --- a/examples/bluetooth/esp_ble_audio/cap/acceptor/main/CMakeLists.txt +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/CMakeLists.txt @@ -1,12 +1,24 @@ set(srcs "main.c") +if(CONFIG_BT_BLUEDROID_ENABLED) + list(APPEND srcs "bluedroid/peripheral.c") +else() + list(APPEND srcs "nimble/peripheral.c") +endif() + if(CONFIG_EXAMPLE_UNICAST) list(APPEND srcs "cap_acceptor_unicast.c") endif() if(CONFIG_EXAMPLE_BROADCAST) list(APPEND srcs "cap_acceptor_broadcast.c") + if(CONFIG_BT_BLUEDROID_ENABLED) + list(APPEND srcs "bluedroid/scan.c") + else() + list(APPEND srcs "nimble/scan.c") + endif() endif() -idf_component_register(SRCS "${srcs}" +idf_component_register(SRCS ${srcs} + INCLUDE_DIRS "." REQUIRES bt nvs_flash) diff --git a/examples/bluetooth/esp_ble_audio/cap/acceptor/main/bluedroid/peripheral.c b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/bluedroid/peripheral.c new file mode 100644 index 00000000000..a08c0e34904 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/bluedroid/peripheral.c @@ -0,0 +1,148 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#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_ble_audio_common_api.h" + +#include "cap_acceptor.h" + +/* Shared with scan.c — single host_sem because init is serialized and only + * peripheral.c owns the GAP cb registration. */ +SemaphoreHandle_t cap_sem; + +/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */ +esp_bt_status_t cap_op_status; + +#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, cap_sem, portMAX_DELAY, cap_op_status) + +static esp_ble_gap_ext_adv_params_t ext_adv_params = { + .type = ESP_BLE_GAP_SET_EXT_ADV_PROP_CONNECTABLE, + .interval_min = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS), + .interval_max = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS), + .channel_map = ADV_CHNL_ALL, + .filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, + .primary_phy = ESP_BLE_GAP_PHY_1M, + .max_skip = 0, + .secondary_phy = ESP_BLE_GAP_PHY_2M, + .sid = ADV_SID, + .scan_req_notif = false, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .tx_power = ADV_TX_POWER, +}; + +static esp_ble_gap_ext_adv_t ext_adv_inst[1] = { + [0] = { ADV_HANDLE, 0, 0 }, +}; + +static void gap_event_handler(esp_gap_ble_cb_event_t event, + esp_ble_gap_cb_param_t *param) +{ + switch (event) { + /* Adv-side completion */ + case ESP_GAP_BLE_EXT_ADV_SET_PARAMS_COMPLETE_EVT: + cap_op_status = param->ext_adv_set_params.status; + xSemaphoreGive(cap_sem); + break; + case ESP_GAP_BLE_EXT_ADV_DATA_SET_COMPLETE_EVT: + cap_op_status = param->ext_adv_data_set.status; + xSemaphoreGive(cap_sem); + break; + case ESP_GAP_BLE_EXT_ADV_START_COMPLETE_EVT: + cap_op_status = param->ext_adv_start.status; + xSemaphoreGive(cap_sem); + break; + case ESP_GAP_BLE_EXT_ADV_STOP_COMPLETE_EVT: + cap_op_status = param->ext_adv_stop.status; + xSemaphoreGive(cap_sem); + break; + /* Scan-side completion */ + case ESP_GAP_BLE_SET_EXT_SCAN_PARAMS_COMPLETE_EVT: + cap_op_status = param->set_ext_scan_params.status; + xSemaphoreGive(cap_sem); + break; + case ESP_GAP_BLE_EXT_SCAN_START_COMPLETE_EVT: + cap_op_status = param->ext_scan_start.status; + xSemaphoreGive(cap_sem); + break; + case ESP_GAP_BLE_EXT_SCAN_STOP_COMPLETE_EVT: + cap_op_status = param->ext_scan_stop.status; + xSemaphoreGive(cap_sem); + break; + /* PAST params setup — pa_sync_with_past / pa_past_cancel wait via WAIT_API. + * CREATE_SYNC / SYNC_TERMINATE complete events are intentionally + * omitted — those calls are fire-and-forget (sync est/lost is air- + * dependent and surfaces via PA_SYNC_ESTAB / PA_SYNC_LOST). Giving + * the sem here would leave it stale-available for the next WAIT_API. */ + case ESP_GAP_BLE_SET_PAST_PARAMS_COMPLETE_EVT: + cap_op_status = param->set_past_params.status; + xSemaphoreGive(cap_sem); + break; + + /* SMP request handling for Just Works pairing (IO_CAP=NONE). The peer + * (central) initiates; we accept the security request and confirm the + * numeric comparison. */ + case ESP_GAP_BLE_SEC_REQ_EVT: + esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true); + break; + case ESP_GAP_BLE_NC_REQ_EVT: + esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, true); + break; + + /* AUTH_CMPL has no BTA channel — app must forward it. PA sync / ext adv + * events: already forwarded by adapter's BTA path, don't re-post here. */ + case ESP_GAP_BLE_AUTH_CMPL_EVT: + esp_ble_audio_gap_app_post_event(event, param); + break; + + default: + break; + } +} + +int app_host_init(void) +{ + esp_err_t err; + + cap_sem = xSemaphoreCreateBinary(); + if (cap_sem == NULL) { + ESP_LOGE(TAG, "Failed to create host semaphore"); + return -1; + } + + err = esp_ble_gap_register_callback(gap_event_handler); + if (err) { + ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err); + vSemaphoreDelete(cap_sem); + return err; + } + + return 0; +} + +int set_device_name(void) +{ + return esp_ble_gap_set_device_name(LOCAL_DEVICE_NAME); +} + +int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len) +{ + WAIT_API(esp_ble_gap_ext_adv_set_params(ADV_HANDLE, &ext_adv_params)); + WAIT_API(esp_ble_gap_config_ext_adv_data_raw(ADV_HANDLE, ext_len, ext_data)); + WAIT_API(esp_ble_gap_ext_adv_start(1, ext_adv_inst)); + + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); + return 0; +} diff --git a/examples/bluetooth/esp_ble_audio/cap/acceptor/main/bluedroid/scan.c b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/bluedroid/scan.c new file mode 100644 index 00000000000..43e80971e97 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/bluedroid/scan.c @@ -0,0 +1,130 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "esp_log.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +#include "esp_bt_defs.h" +#include "esp_gap_ble_api.h" + +#include "cap_acceptor.h" + +/* Owned by peripheral.c (single host_sem, serialized init). */ +extern SemaphoreHandle_t cap_sem; +extern esp_bt_status_t cap_op_status; + +#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, cap_sem, portMAX_DELAY, cap_op_status) + +#if CONFIG_EXAMPLE_SCAN_SELF +static esp_ble_ext_scan_params_t ext_scan_params = { + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE, + .cfg_mask = ESP_BLE_GAP_EXT_SCAN_CFG_UNCODE_MASK, + .uncoded_cfg = { + .scan_type = BLE_SCAN_TYPE_PASSIVE, + .scan_interval = SCAN_INTERVAL, + .scan_window = SCAN_WINDOW, + }, +}; +#endif /* CONFIG_EXAMPLE_SCAN_SELF */ + +#if CONFIG_EXAMPLE_SCAN_SELF +int ext_scan_start(void) +{ + WAIT_API(esp_ble_gap_set_ext_scan_params(&ext_scan_params)); + WAIT_API(esp_ble_gap_start_ext_scan(0, 0)); + + ESP_LOGI(TAG, "Scanning for broadcast source..."); + return 0; +} + +int ext_scan_stop(void) +{ + WAIT_API(esp_ble_gap_stop_ext_scan()); + return 0; +} +#endif /* CONFIG_EXAMPLE_SCAN_SELF */ + +int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid) +{ + esp_ble_gap_periodic_adv_sync_params_t params = { + .filter_policy = 0, + .sid = sid, + .addr_type = addr_type, + .skip = PA_SYNC_SKIP, + .sync_timeout = PA_SYNC_TIMEOUT, + }; + + memcpy(params.addr, addr, sizeof(params.addr)); + + /* Fire-and-forget: sync establishment (PERIODIC_ADV_SYNC_ESTAB_EVT) is + * air-dependent and surfaces asynchronously. */ + return esp_ble_gap_periodic_adv_create_sync(¶ms); +} + +int pa_sync_with_past(uint16_t conn_handle, uint8_t addr_type, const uint8_t addr[6]) +{ + /* PAST receive params. Mode 0x02 = sync + report events, no dedup. + * mode>=1 also implicitly enables the periodic adv report stream after the + * controller auto-syncs via PAST — no separate recv_enable needed. */ + esp_ble_gap_past_params_t params = { + .mode = ESP_BLE_GAP_PAST_MODE_DUP_FILTER_DISABLED, + .skip = PA_SYNC_SKIP, + .sync_timeout = PA_SYNC_TIMEOUT, + .cte_type = 0, + }; + /* Use Assistant's BD addr (peer.dst from ACL_CONNECT), not the broadcast + * source addr from `addr` — BTM dispatches the HCI cmd over the + * Assistant's ACL. */ + esp_bd_addr_t peer_addr; + + (void)conn_handle; + (void)addr_type; + (void)addr; + + memcpy(peer_addr, peer.dst, sizeof(peer_addr)); + + WAIT_API(esp_ble_gap_set_periodic_adv_sync_trans_params(peer_addr, ¶ms)); + + /* Actual sync handle is delivered later via PA_SYNC_PAST app event. */ + return 0; +} + +int pa_past_cancel(uint16_t conn_handle) +{ + /* mode=0 → controller no longer auto-syncs incoming PAST from Assistant. + * Like pa_sync_with_past, addressed by Assistant's BD addr (peer.dst). */ + esp_ble_gap_past_params_t params = { + .mode = ESP_BLE_GAP_PAST_MODE_NO_SYNC_EVT, + .skip = PA_SYNC_SKIP, + .sync_timeout = PA_SYNC_TIMEOUT, + .cte_type = 0, + }; + esp_bd_addr_t peer_addr; + + (void)conn_handle; + + memcpy(peer_addr, peer.dst, sizeof(peer_addr)); + + WAIT_API(esp_ble_gap_set_periodic_adv_sync_trans_params(peer_addr, ¶ms)); + + return 0; +} + +int pa_sync_terminate(uint16_t sync_handle) +{ + /* iso bluedroid/gap.c synthesizes a PA_SYNC_LOST event on + * BTM_BLE_5_GAP_PERIODIC_ADV_SYNC_TERMINATE_COMPLETE_EVT, matching + * NimBLE host behavior — the app's pa_sync_lost handler runs + * broadcast_pa_lost automatically. */ + return esp_ble_gap_periodic_adv_sync_terminate(sync_handle); +} 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 f2b63058b35..77cd669bf9f 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 @@ -5,13 +5,12 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include + #include "esp_log.h" #include "sdkconfig.h" -#include "host/ble_hs.h" -#include "services/gap/ble_svc_gap.h" - #include "esp_ble_audio_lc3_defs.h" #include "esp_ble_audio_cap_api.h" #include "esp_ble_audio_pacs_api.h" @@ -23,6 +22,19 @@ #define CONN_HANDLE_INIT 0xFFFF +#define LOCAL_DEVICE_NAME "CAP Acceptor" + +#define ADV_HANDLE 0 +#define ADV_SID 0 +#define ADV_TX_POWER 127 +#define ADV_INTERVAL_MS 200 + +#define SCAN_INTERVAL 160 /* 100ms */ +#define SCAN_WINDOW 160 /* 100ms */ + +#define PA_SYNC_SKIP 0 +#define PA_SYNC_TIMEOUT 1000 /* 1000 * 10ms = 10s */ + #define SINK_CONTEXT (ESP_BLE_AUDIO_CONTEXT_TYPE_UNSPECIFIED | \ ESP_BLE_AUDIO_CONTEXT_TYPE_CONVERSATIONAL | \ ESP_BLE_AUDIO_CONTEXT_TYPE_MEDIA | \ @@ -37,8 +49,32 @@ struct peer_config { esp_ble_audio_cap_stream_t source_stream; esp_ble_audio_cap_stream_t sink_stream; uint16_t conn_handle; + /* Assistant's BD addr — used by Bluedroid pa_sync_with_past to set per-peer + * PAST receive params. Populated at ACL_CONNECT, cleared at ACL_DISCONNECT. */ + uint8_t dst[6]; }; +extern struct peer_config peer; + +int app_host_init(void); + +int set_device_name(void); + +int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len); + +#if CONFIG_EXAMPLE_SCAN_SELF +int ext_scan_start(void); +int ext_scan_stop(void); +#endif /* CONFIG_EXAMPLE_SCAN_SELF */ + +int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid); + +int pa_sync_with_past(uint16_t conn_handle, uint8_t addr_type, const uint8_t addr[6]); + +int pa_past_cancel(uint16_t conn_handle); + +int pa_sync_terminate(uint16_t sync_handle); + int cap_acceptor_unicast_init(struct peer_config *peer); int cap_acceptor_broadcast_init(void); @@ -58,5 +94,5 @@ void broadcast_pa_synced(esp_ble_audio_gap_app_event_t *event); void broadcast_pa_sync_failed(esp_ble_audio_gap_app_event_t *event); -void broadcast_pa_lost(esp_ble_audio_gap_app_event_t *event); +void broadcast_pa_lost(uint16_t sync_handle); #endif /* CONFIG_EXAMPLE_BROADCAST */ 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 c6f5b7042a9..ed54dc2e58a 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 @@ -9,7 +9,6 @@ #include #include #include -#include #include #include "cap_acceptor.h" @@ -18,13 +17,8 @@ #define TARGET_DEVICE_NAME "CAP Broadcast Source" #define TARGET_DEVICE_NAME_LEN (sizeof(TARGET_DEVICE_NAME) - 1) #define TARGET_BROADCAST_CODE "1234" - -#define SCAN_INTERVAL 160 /* 100ms */ -#define SCAN_WINDOW 160 /* 100ms */ #endif /* CONFIG_EXAMPLE_SCAN_SELF */ -#define PA_SYNC_SKIP 0 -#define PA_SYNC_TIMEOUT 1000 /* 1000 * 10ms = 10s */ #define PA_SYNC_HANDLE_INIT UINT16_MAX enum broadcast_flag { @@ -78,101 +72,9 @@ static struct broadcast_sink { .sync_handle = PA_SYNC_HANDLE_INIT, }; -static example_audio_rx_metrics_t rx_metrics; - -static int pa_sync_without_past(const bt_addr_le_t *addr, uint8_t adv_sid) -{ - struct ble_gap_periodic_sync_params params = {0}; - ble_addr_t sync_addr = {0}; - int err; - - sync_addr.type = addr->type; - memcpy(sync_addr.val, addr->a.val, sizeof(sync_addr.val)); - params.skip = PA_SYNC_SKIP; - params.sync_timeout = PA_SYNC_TIMEOUT; - - err = ble_gap_periodic_adv_sync_create(&sync_addr, adv_sid, ¶ms, - example_audio_gap_event_cb, NULL); - if (err) { - ESP_LOGE(TAG, "Failed to create PA sync without past, err %d", err); - } - - return err; -} - -static int pa_sync_with_past(uint16_t conn_handle, - const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state) -{ - struct ble_gap_periodic_sync_params params = {0}; - int err; - - params.skip = PA_SYNC_SKIP; - params.sync_timeout = PA_SYNC_TIMEOUT; - - err = ble_gap_periodic_adv_sync_receive(conn_handle, ¶ms, - example_audio_gap_event_cb, NULL); - if (err) { - ESP_LOGE(TAG, "Failed to enable PAST receive, err %d", err); - return err; - } - - /* Tell the Assistant we are waiting for PAST so it sends - * HCI_LE_Periodic_Advertising_Sync_Transfer. - */ - err = esp_ble_audio_bap_scan_delegator_set_pa_state(recv_state->src_id, - ESP_BLE_AUDIO_BAP_PA_STATE_INFO_REQ); - if (err) { - ESP_LOGE(TAG, "Failed to set PA state to INFO_REQ, err %d", err); - } - - return err; -} - -static int pa_sync_terminate(void) -{ - int err; - - err = ble_gap_periodic_adv_sync_terminate(broadcast_sink.sync_handle); - if (err) { - ESP_LOGE(TAG, "Failed to terminate PA sync, err %d", err); - } - - return err; -} +static example_audio_rx_metrics_t rx_metrics[CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT]; #if CONFIG_EXAMPLE_SCAN_SELF -static int ext_scan_start(void) -{ - struct ble_gap_disc_params params = {0}; - uint8_t own_addr_type; - int err; - - err = ble_hs_id_infer_auto(0, &own_addr_type); - if (err) { - ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err); - return err; - } - - params.passive = 1; - params.itvl = SCAN_INTERVAL; - params.window = SCAN_WINDOW; - - err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms, - example_audio_gap_event_cb, NULL); - if (err) { - ESP_LOGE(TAG, "Failed to start scanning, err %d", err); - return err; - } - - ESP_LOGI(TAG, "Scanning for broadcast source..."); - return 0; -} - -static int ext_scan_stop(void) -{ - return ble_gap_disc_cancel(); -} - int check_start_scan(void) { int err; @@ -243,7 +145,6 @@ static bool data_cb(uint8_t type, const uint8_t *data, void broadcast_scan_recv(esp_ble_audio_gap_app_event_t *event) { struct scan_recv_data sr = {0}; - bt_addr_le_t addr; int err; /* Periodic advertising interval. 0 if no periodic advertising. */ @@ -267,10 +168,9 @@ void broadcast_scan_recv(esp_ble_audio_gap_app_event_t *event) broadcast_sink.requested_bis_sync = ESP_BLE_AUDIO_BAP_BIS_SYNC_NO_PREF; broadcast_sink.broadcast_id = sr.broadcast_id; - addr.type = event->ext_scan_recv.addr.type; - memcpy(addr.a.val, event->ext_scan_recv.addr.val, sizeof(addr.a.val)); - - err = pa_sync_without_past(&addr, event->ext_scan_recv.sid); + err = pa_sync_create(event->ext_scan_recv.addr.type, + event->ext_scan_recv.addr.val, + event->ext_scan_recv.sid); if (err) { return; } @@ -417,9 +317,11 @@ static uint8_t broadcast_stream_idx(const esp_ble_audio_bap_stream_t *stream) static void broadcast_stream_started_cb(esp_ble_audio_bap_stream_t *stream) { - ESP_LOGI(TAG, "[SNK #%u] Stream started", broadcast_stream_idx(stream)); + uint8_t idx = broadcast_stream_idx(stream); - example_audio_rx_metrics_reset(&rx_metrics); + ESP_LOGI(TAG, "[SNK #%u] Stream started", idx); + + example_audio_rx_metrics_reset(&rx_metrics[idx]); broadcast_sink.active_streams++; flag_clear(FLAG_BROADCAST_SYNCING); @@ -480,11 +382,12 @@ static void broadcast_stream_recv_cb(esp_ble_audio_bap_stream_t *stream, const esp_ble_iso_recv_info_t *info, const uint8_t *data, uint16_t len) { + uint8_t idx = broadcast_stream_idx(stream); char obj_name[10]; - rx_metrics.last_sdu_len = len; - snprintf(obj_name, sizeof(obj_name), "SNK #%u", broadcast_stream_idx(stream)); - example_audio_rx_metrics_on_recv(info, &rx_metrics, TAG, obj_name); + rx_metrics[idx].last_sdu_len = len; + snprintf(obj_name, sizeof(obj_name), "SNK #%u", idx); + example_audio_rx_metrics_on_recv(info, &rx_metrics[idx], TAG, obj_name); } static void base_recv_cb(esp_ble_audio_bap_broadcast_sink_t *sink, @@ -538,7 +441,7 @@ static void recv_state_updated_cb(esp_ble_conn_t *conn, recv_state->pa_sync_state, recv_state->encrypt_state); for (uint8_t i = 0; i < recv_state->num_subgroups; i++) { - ESP_LOGI(TAG, "subgroup %d bis_sync: 0x%08x", i, recv_state->subgroups[i].bis_sync); + ESP_LOGI(TAG, "subgroup %d bis_sync 0x%08x", i, recv_state->subgroups[i].bis_sync); } if (recv_state->pa_sync_state == ESP_BLE_AUDIO_BAP_PA_STATE_SYNCED) { @@ -566,14 +469,29 @@ static int pa_sync_req_cb(esp_ble_conn_t *conn, } if (past_available) { - err = pa_sync_with_past(conn->handle, recv_state); + err = pa_sync_with_past(conn->handle, + recv_state->addr.type, recv_state->addr.a.val); if (err) { return -EIO; } + /* Tell the Assistant we are waiting for PAST so it sends + * HCI_LE_Periodic_Advertising_Sync_Transfer. */ + err = esp_ble_audio_bap_scan_delegator_set_pa_state(recv_state->src_id, + ESP_BLE_AUDIO_BAP_PA_STATE_INFO_REQ); + if (err) { + ESP_LOGE(TAG, "Failed to set PA state to INFO_REQ, err %d", err); + /* Disarm the PAST receive we just enabled — Assistant was never + * notified, so any late-arriving PAST would land on stale state + * (broadcast_id=0, FLAG_PA_SYNCING unset). */ + pa_past_cancel(conn->handle); + return -EIO; + } + ESP_LOGI(TAG, "Waiting for PAST..."); } else { - err = pa_sync_without_past(&recv_state->addr, recv_state->adv_sid); + err = pa_sync_create(recv_state->addr.type, recv_state->addr.a.val, + recv_state->adv_sid); if (err) { return err; } @@ -596,8 +514,17 @@ static int pa_sync_term_req_cb(esp_ble_conn_t *conn, broadcast_sink.recv_state = recv_state; - err = pa_sync_terminate(); + /* Nothing to terminate if PAST/PA-sync never landed (e.g., PAST setup failed + * earlier). Issuing the HCI terminate with the sentinel handle gets 0x12 + * (Invalid HCI Command Parameters) back from the controller. */ + if (broadcast_sink.sync_handle == PA_SYNC_HANDLE_INIT) { + ESP_LOGI(TAG, "PA sync never established, skip terminate"); + return 0; + } + + err = pa_sync_terminate(broadcast_sink.sync_handle); if (err) { + ESP_LOGE(TAG, "Failed to terminate PA sync, err %d", err); return -EIO; } @@ -783,11 +710,11 @@ void broadcast_pa_sync_failed(esp_ble_audio_gap_app_event_t *event) } } -void broadcast_pa_lost(esp_ble_audio_gap_app_event_t *event) +void broadcast_pa_lost(uint16_t sync_handle) { esp_err_t err; - if (event->pa_sync_lost.sync_handle != broadcast_sink.sync_handle) { + if (sync_handle != broadcast_sink.sync_handle) { return; } @@ -836,6 +763,14 @@ void broadcast_pa_lost(esp_ble_audio_gap_app_event_t *event) flag_clear(FLAG_BASE_RECEIVED); flag_clear(FLAG_BROADCAST_SYNCABLE); flag_clear(FLAG_BROADCAST_CODE_REQUIRED); + +#if CONFIG_EXAMPLE_SCAN_SELF + /* Clear cached recv_state so broadcast_scan_recv's gate can drive + * a fresh PA sync. */ + broadcast_sink.recv_state = NULL; + + check_start_scan(); +#endif /* CONFIG_EXAMPLE_SCAN_SELF */ } int cap_acceptor_broadcast_init(void) 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 b611e2045e1..633f82d8ae5 100644 --- a/examples/bluetooth/esp_ble_audio/cap/acceptor/main/main.c +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/main.c @@ -6,25 +6,11 @@ */ #include -#include -#include -#include -#include #include "nvs_flash.h" -#include "esp_system.h" -#include "esp_timer.h" #include "cap_acceptor.h" -#define ADV_HANDLE 0x00 -#define ADV_SID 0 -#define ADV_TX_POWER 127 -#define ADV_ADDRESS BLE_OWN_ADDR_PUBLIC -#define ADV_PRIMARY_PHY BLE_HCI_LE_PHY_1M -#define ADV_SECONDARY_PHY BLE_HCI_LE_PHY_2M -#define ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(200) - static uint8_t codec_data[] = ESP_BLE_AUDIO_CODEC_CAP_LC3_DATA( ESP_BLE_AUDIO_CODEC_CAP_FREQ_ANY, /* Sampling frequency Any */ @@ -49,7 +35,7 @@ static esp_ble_audio_pacs_cap_t source_cap = { .codec_cap = &codec_cap, }; -static struct peer_config peer = { +struct peer_config peer = { .conn_handle = CONN_HANDLE_INIT, }; @@ -98,58 +84,6 @@ static uint8_t ext_adv_data[] = { 0x0d, EXAMPLE_AD_TYPE_NAME_COMPLETE, 'c', 'a', 'p', '_', 'a', 'c', 'c', 'e', 'p', 't', 'o', 'r', }; -static void ext_adv_start(void) -{ - struct ble_gap_ext_adv_params ext_params = {0}; - struct os_mbuf *data = NULL; - int err; - - ext_params.connectable = 1; - ext_params.scannable = 0; - ext_params.legacy_pdu = 0; - ext_params.own_addr_type = ADV_ADDRESS; - ext_params.primary_phy = ADV_PRIMARY_PHY; - ext_params.secondary_phy = ADV_SECONDARY_PHY; - ext_params.tx_power = ADV_TX_POWER; - ext_params.sid = ADV_SID; - ext_params.itvl_min = ADV_INTERVAL; - ext_params.itvl_max = ADV_INTERVAL; - - err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL, - example_audio_gap_event_cb, NULL); - if (err) { - ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err); - return; - } - - data = os_msys_get_pkthdr(sizeof(ext_adv_data), 0); - if (data == NULL) { - ESP_LOGE(TAG, "Failed to get ext adv mbuf"); - return; - } - - err = os_mbuf_append(data, ext_adv_data, sizeof(ext_adv_data)); - if (err) { - ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err); - os_mbuf_free_chain(data); - return; - } - - err = ble_gap_ext_adv_set_data(ADV_HANDLE, data); - if (err) { - ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err); - return; - } - - err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0); - if (err) { - ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err); - return; - } - - ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); -} - esp_ble_audio_cap_stream_t *stream_alloc(esp_ble_audio_dir_t dir) { if (dir == ESP_BLE_AUDIO_DIR_SINK) { @@ -265,7 +199,7 @@ static void pa_sync_lost(esp_ble_audio_gap_app_event_t *event) ESP_LOGI(TAG, "PA sync lost: sync_handle %u reason 0x%02x", event->pa_sync_lost.sync_handle, event->pa_sync_lost.reason); - broadcast_pa_lost(event); + broadcast_pa_lost(event->pa_sync_lost.sync_handle); } #endif /* CONFIG_EXAMPLE_BROADCAST */ @@ -278,11 +212,10 @@ static void acl_connect(esp_ble_audio_gap_app_event_t *event) ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x", event->acl_connect.conn_handle, event->acl_connect.role, - event->acl_connect.dst.val[5], event->acl_connect.dst.val[4], - event->acl_connect.dst.val[3], event->acl_connect.dst.val[2], - event->acl_connect.dst.val[1], event->acl_connect.dst.val[0]); + EXAMPLE_BT_ADDR_PRINT_ARGS(event->acl_connect.dst.val)); peer.conn_handle = event->acl_connect.conn_handle; + memcpy(peer.dst, event->acl_connect.dst.val, sizeof(peer.dst)); } static void acl_disconnect(esp_ble_audio_gap_app_event_t *event) @@ -291,8 +224,24 @@ static void acl_disconnect(esp_ble_audio_gap_app_event_t *event) event->acl_disconnect.conn_handle, event->acl_disconnect.reason); peer.conn_handle = CONN_HANDLE_INIT; + memset(peer.dst, 0, sizeof(peer.dst)); - ext_adv_start(); + ext_adv_start(ext_adv_data, sizeof(ext_adv_data)); +} + +static void security_change(esp_ble_audio_gap_app_event_t *event) +{ + if (event->security_change.status) { + ESP_LOGE(TAG, "Security failed: handle %u status %d", + event->security_change.conn_handle, + event->security_change.status); + return; + } + + ESP_LOGI(TAG, "Security: handle %u level %u bonded %u", + event->security_change.conn_handle, + event->security_change.sec_level, + event->security_change.bonded); } static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event) @@ -318,6 +267,9 @@ static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event) case ESP_BLE_AUDIO_GAP_EVENT_ACL_DISCONNECT: acl_disconnect(event); break; + case ESP_BLE_AUDIO_GAP_EVENT_SECURITY_CHANGE: + security_change(event); + break; default: break; } @@ -392,6 +344,12 @@ void app_main(void) return; } + err = app_host_init(); + if (err) { + ESP_LOGE(TAG, "Failed to init host, err %d", err); + return; + } + err = esp_ble_audio_common_init(&info); if (err) { ESP_LOGE(TAG, "Failed to initialize audio, err %d", err); @@ -423,7 +381,7 @@ void app_main(void) return; } - err = ble_svc_gap_device_name_set("CAP Acceptor"); + err = set_device_name(); if (err) { ESP_LOGE(TAG, "Failed to set device name, err %d", err); return; @@ -439,5 +397,5 @@ void app_main(void) /* Advertising will be used by Unicast Server and * Broadcast Sink when self-scanning is disabled. */ - ext_adv_start(); + ext_adv_start(ext_adv_data, sizeof(ext_adv_data)); } diff --git a/examples/bluetooth/esp_ble_audio/cap/acceptor/main/nimble/peripheral.c b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/nimble/peripheral.c new file mode 100644 index 00000000000..f971e83ed7c --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/nimble/peripheral.c @@ -0,0 +1,112 @@ +/* + * 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 "host/ble_store.h" +#include "services/gap/ble_svc_gap.h" + +#include "os/os_mbuf.h" + +#include "esp_ble_audio_common_api.h" + +#include "cap_acceptor.h" + +static int gap_event_cb(struct ble_gap_event *event, void *arg) +{ + switch (event->type) { + case BLE_GAP_EVENT_CONNECT: + case BLE_GAP_EVENT_DISCONNECT: + case BLE_GAP_EVENT_ENC_CHANGE: + esp_ble_audio_gap_app_post_event(event->type, event); + break; + case BLE_GAP_EVENT_MTU: + case BLE_GAP_EVENT_NOTIFY_RX: + case BLE_GAP_EVENT_NOTIFY_TX: + case BLE_GAP_EVENT_SUBSCRIBE: + esp_ble_audio_gatt_app_post_event(event->type, event); + break; + case BLE_GAP_EVENT_REPEAT_PAIRING: { + struct ble_gap_conn_desc desc = {0}; + int rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc); + if (rc == 0) { + ble_store_util_delete_peer(&desc.peer_id_addr); + } + return BLE_GAP_REPEAT_PAIRING_RETRY; + } + default: + break; + } + + return 0; +} + +int app_host_init(void) +{ + return 0; +} + +int set_device_name(void) +{ + return ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME); +} + +int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len) +{ + struct ble_gap_ext_adv_params ext_params = {0}; + struct os_mbuf *data = NULL; + int err; + + ext_params.connectable = 1; + ext_params.scannable = 0; + ext_params.legacy_pdu = 0; + ext_params.own_addr_type = BLE_OWN_ADDR_PUBLIC; + ext_params.primary_phy = BLE_HCI_LE_PHY_1M; + ext_params.secondary_phy = BLE_HCI_LE_PHY_2M; + ext_params.tx_power = ADV_TX_POWER; + ext_params.sid = ADV_SID; + ext_params.itvl_min = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS); + ext_params.itvl_max = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS); + + err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL, + gap_event_cb, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err); + return err; + } + + data = os_msys_get_pkthdr(ext_len, 0); + if (data == NULL) { + ESP_LOGE(TAG, "Failed to get ext adv mbuf"); + return -1; + } + + err = os_mbuf_append(data, ext_data, ext_len); + if (err) { + ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err); + os_mbuf_free_chain(data); + return err; + } + + err = ble_gap_ext_adv_set_data(ADV_HANDLE, data); + if (err) { + ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err); + return err; + } + + err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0); + if (err) { + ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); + return 0; +} diff --git a/examples/bluetooth/esp_ble_audio/cap/acceptor/main/nimble/scan.c b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/nimble/scan.c new file mode 100644 index 00000000000..cd4a8e9152f --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/main/nimble/scan.c @@ -0,0 +1,125 @@ +/* + * 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 "esp_ble_audio_common_api.h" + +#include "cap_acceptor.h" + +static int gap_event_cb(struct ble_gap_event *event, void *arg) +{ + switch (event->type) { + case BLE_GAP_EVENT_EXT_DISC: + case BLE_GAP_EVENT_PERIODIC_SYNC: + case BLE_GAP_EVENT_PERIODIC_REPORT: + case BLE_GAP_EVENT_PERIODIC_SYNC_LOST: + case BLE_GAP_EVENT_PERIODIC_TRANSFER: + case BLE_GAP_EVENT_PERIODIC_TRANSFER_V2: + esp_ble_audio_gap_app_post_event(event->type, event); + break; + default: + break; + } + + return 0; +} + +#if CONFIG_EXAMPLE_SCAN_SELF +int ext_scan_start(void) +{ + struct ble_gap_disc_params params = {0}; + uint8_t own_addr_type; + int err; + + err = ble_hs_id_infer_auto(0, &own_addr_type); + if (err) { + ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err); + return err; + } + + params.passive = 1; + params.itvl = SCAN_INTERVAL; + params.window = SCAN_WINDOW; + + err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms, + gap_event_cb, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to start scanning, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Scanning for broadcast source..."); + return 0; +} + +int ext_scan_stop(void) +{ + return ble_gap_disc_cancel(); +} +#endif /* CONFIG_EXAMPLE_SCAN_SELF */ + +int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid) +{ + struct ble_gap_periodic_sync_params params = {0}; + ble_addr_t sync_addr = {0}; + int err; + + sync_addr.type = addr_type; + memcpy(sync_addr.val, addr, sizeof(sync_addr.val)); + params.skip = PA_SYNC_SKIP; + params.sync_timeout = PA_SYNC_TIMEOUT; + + err = ble_gap_periodic_adv_sync_create(&sync_addr, sid, ¶ms, + gap_event_cb, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to create PA sync, err %d", err); + } + + return err; +} + +int pa_sync_with_past(uint16_t conn_handle, uint8_t addr_type, const uint8_t addr[6]) +{ + struct ble_gap_periodic_sync_params params = {0}; + int err; + + (void)addr_type; + (void)addr; + + params.skip = PA_SYNC_SKIP; + params.sync_timeout = PA_SYNC_TIMEOUT; + + err = ble_gap_periodic_adv_sync_receive(conn_handle, ¶ms, + gap_event_cb, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to enable PAST receive, err %d", err); + } + + return err; +} + +int pa_past_cancel(uint16_t conn_handle) +{ + /* params=NULL → mode=0: controller no longer auto-syncs incoming PAST. */ + int err = ble_gap_periodic_adv_sync_receive(conn_handle, NULL, NULL, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to cancel PAST receive, err %d", err); + } + + return err; +} + +int pa_sync_terminate(uint16_t sync_handle) +{ + return ble_gap_periodic_adv_sync_terminate(sync_handle); +} diff --git a/examples/bluetooth/esp_ble_audio/cap/acceptor/sdkconfig.defaults b/examples/bluetooth/esp_ble_audio/cap/acceptor/sdkconfig.defaults index f80986640a4..1da6c3dbbcf 100644 --- a/examples/bluetooth/esp_ble_audio/cap/acceptor/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/sdkconfig.defaults @@ -3,14 +3,16 @@ # CONFIG_BT_ENABLED=y -CONFIG_BT_BLUEDROID_ENABLED=n -CONFIG_BT_NIMBLE_ENABLED=y -CONFIG_BT_NIMBLE_EXT_ADV=y -CONFIG_BT_NIMBLE_NVS_PERSIST=y -CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 -CONFIG_BT_NIMBLE_MAX_CCCDS=20 -CONFIG_BT_NIMBLE_ISO=y -CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y +CONFIG_BT_NIMBLE_ENABLED=n +CONFIG_BT_BLUEDROID_ENABLED=y +CONFIG_BT_CLASSIC_ENABLED=n +CONFIG_BT_CONTROLLER_ENABLED=y +CONFIG_BT_BLE_ENABLED=y +CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y +CONFIG_BT_ACL_CONNECTIONS=1 +CONFIG_BT_GATTC_NOTIF_REG_MAX=20 +CONFIG_BT_BLE_FEAT_ISO_EN=y +CONFIG_BT_BLE_FEAT_PERIODIC_ADV_SYNC_TRANSFER=y CONFIG_BT_ISO_MAX_CHAN=2 @@ -26,7 +28,9 @@ CONFIG_BT_PAC_SRC_NOTIFIABLE=y CONFIG_BT_PAC_SRC_LOC_WRITEABLE=y CONFIG_BT_PAC_SRC_LOC_NOTIFIABLE=y CONFIG_BT_PACS_SUPPORTED_CONTEXT_NOTIFIABLE=y -CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=30 +CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=60 + +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y CONFIG_EXAMPLE_UNICAST=y diff --git a/examples/bluetooth/esp_ble_audio/cap/acceptor/sdkconfig.defaults.nimble b/examples/bluetooth/esp_ble_audio/cap/acceptor/sdkconfig.defaults.nimble new file mode 100644 index 00000000000..0c3c84403ac --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/acceptor/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/README.md b/examples/bluetooth/esp_ble_audio/cap/initiator/README.md index 2f708dfd627..3b35ab02317 100644 --- a/examples/bluetooth/esp_ble_audio/cap/initiator/README.md +++ b/examples/bluetooth/esp_ble_audio/cap/initiator/README.md @@ -7,7 +7,7 @@ ## Overview -This example implements the **Common Audio Profile (CAP) Initiator** role on top of the NimBLE host stack with ISO and LE Audio support. It is built in one of two mutually exclusive sub-modes selected at build time: a **CAP Unicast Initiator / BAP Unicast Client**, or a **CAP Broadcast Source**. A small TX module (`cap_initiator_tx.c`) drives audio packets onto every started stream using the LE Audio example TX scheduler. +This example implements the **Common Audio Profile (CAP) Initiator** role on top of the selected BLE host stack (Bluedroid by default; NimBLE via the `sdkconfig.defaults.nimble` overlay) with ISO and LE Audio support. It is built in one of two mutually exclusive sub-modes selected at build time: a **CAP Unicast Initiator / BAP Unicast Client**, or a **CAP Broadcast Source**. A small TX module (`cap_initiator_tx.c`) drives audio packets onto every started stream using the LE Audio example TX scheduler. In **unicast** mode, the initiator scans for connectable extended advertising that carries CAS service data, connects, initiates pairing, exchanges MTU, performs GATT service discovery, then discovers CAS, sink ASEs and source ASEs, configures the codec with the LC3 16_2_1 unicast preset, creates an ad-hoc unicast group, and starts the streams. In **broadcast** mode, the initiator creates a CAP broadcast source with the LC3 16_2_1 broadcast preset, starts non-connectable extended advertising plus periodic advertising carrying the Broadcast Audio Announcement UUID, a hardcoded broadcast ID (`0x123456`), the local device name (`CAP Broadcast Source`), and the BASE, then starts the broadcast stream. @@ -37,11 +37,24 @@ Just-Works pairing (LE Secure Connections, no MITM, `BLE_SM_IO_CAP_NO_IO`) with ## Build & Flash +The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`. + +### Bluedroid host (default) + ```bash idf.py set-target esp32h4 idf.py -p PORT flash monitor ``` +### NimBLE host + +```bash +idf.py set-target esp32h4 +idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor +``` + +For `esp32s31`, replace the chip overlay accordingly. + (Exit serial monitor with `Ctrl-]`.) ## Example Flow diff --git a/examples/bluetooth/esp_ble_audio/cap/initiator/main/CMakeLists.txt b/examples/bluetooth/esp_ble_audio/cap/initiator/main/CMakeLists.txt index 199c52d6467..654aeddad1e 100644 --- a/examples/bluetooth/esp_ble_audio/cap/initiator/main/CMakeLists.txt +++ b/examples/bluetooth/esp_ble_audio/cap/initiator/main/CMakeLists.txt @@ -3,11 +3,22 @@ set(srcs "cap_initiator_tx.c" if(CONFIG_EXAMPLE_UNICAST) list(APPEND srcs "cap_initiator_unicast.c") + if(CONFIG_BT_BLUEDROID_ENABLED) + list(APPEND srcs "bluedroid/central.c") + else() + list(APPEND srcs "nimble/central.c") + endif() endif() if(CONFIG_EXAMPLE_BROADCAST) list(APPEND srcs "cap_initiator_broadcast.c") + if(CONFIG_BT_BLUEDROID_ENABLED) + list(APPEND srcs "bluedroid/adv.c") + else() + list(APPEND srcs "nimble/adv.c") + endif() endif() -idf_component_register(SRCS "${srcs}" +idf_component_register(SRCS ${srcs} + INCLUDE_DIRS "." REQUIRES bt nvs_flash) diff --git a/examples/bluetooth/esp_ble_audio/cap/initiator/main/bluedroid/adv.c b/examples/bluetooth/esp_ble_audio/cap/initiator/main/bluedroid/adv.c new file mode 100644 index 00000000000..14bd9e835b2 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/initiator/main/bluedroid/adv.c @@ -0,0 +1,127 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#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 "cap_initiator.h" + +static SemaphoreHandle_t adv_sem; + +/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */ +static esp_bt_status_t adv_op_status; + +#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, adv_sem, portMAX_DELAY, adv_op_status) + +static esp_ble_gap_ext_adv_params_t ext_adv_params = { + .type = ESP_BLE_GAP_SET_EXT_ADV_PROP_NONCONN_NONSCANNABLE_UNDIRECTED, + .interval_min = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS), + .interval_max = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS), + .channel_map = ADV_CHNL_ALL, + .filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, + .primary_phy = ESP_BLE_GAP_PHY_1M, + .max_skip = 0, + .secondary_phy = ESP_BLE_GAP_PHY_2M, + .sid = ADV_SID, + .scan_req_notif = false, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .tx_power = ADV_TX_POWER, +}; + +static esp_ble_gap_periodic_adv_params_t periodic_adv_params = { + .interval_min = ESP_BLE_GAP_PERIODIC_ADV_ITVL_MS(PER_ADV_INTERVAL_MS), + .interval_max = ESP_BLE_GAP_PERIODIC_ADV_ITVL_MS(PER_ADV_INTERVAL_MS), + .properties = 0, +}; + +static esp_ble_gap_ext_adv_t ext_adv_inst[1] = { + [0] = { ADV_HANDLE, 0, 0 }, +}; + +static void gap_event_handler(esp_gap_ble_cb_event_t event, + esp_ble_gap_cb_param_t *param) +{ + switch (event) { + case ESP_GAP_BLE_EXT_ADV_SET_PARAMS_COMPLETE_EVT: + adv_op_status = param->ext_adv_set_params.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_EXT_ADV_DATA_SET_COMPLETE_EVT: + adv_op_status = param->ext_adv_data_set.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_EXT_ADV_START_COMPLETE_EVT: + adv_op_status = param->ext_adv_start.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_PERIODIC_ADV_SET_PARAMS_COMPLETE_EVT: + adv_op_status = param->peroid_adv_set_params.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_PERIODIC_ADV_DATA_SET_COMPLETE_EVT: + adv_op_status = param->period_adv_data_set.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_PERIODIC_ADV_START_COMPLETE_EVT: + adv_op_status = param->period_adv_start.status; + xSemaphoreGive(adv_sem); + break; + default: + break; + } +} + +int app_host_init(void) +{ + esp_err_t err; + + adv_sem = xSemaphoreCreateBinary(); + if (adv_sem == NULL) { + ESP_LOGE(TAG, "Failed to create adv semaphore"); + return -1; + } + + err = esp_ble_gap_register_callback(gap_event_handler); + if (err) { + ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err); + vSemaphoreDelete(adv_sem); + return err; + } + + return 0; +} + +int set_device_name(void) +{ + return esp_ble_gap_set_device_name(LOCAL_DEVICE_NAME); +} + +int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len, + const uint8_t *per_data, uint8_t per_len) +{ + WAIT_API(esp_ble_gap_ext_adv_set_params(ADV_HANDLE, &ext_adv_params)); + WAIT_API(esp_ble_gap_config_ext_adv_data_raw(ADV_HANDLE, ext_len, ext_data)); + WAIT_API(esp_ble_gap_periodic_adv_set_params(ADV_HANDLE, &periodic_adv_params)); +#if CONFIG_BT_BLE_FEAT_PERIODIC_ADV_ENH + WAIT_API(esp_ble_gap_config_periodic_adv_data_raw(ADV_HANDLE, per_len, per_data, false)); + WAIT_API(esp_ble_gap_periodic_adv_start(ADV_HANDLE, true)); +#else + WAIT_API(esp_ble_gap_config_periodic_adv_data_raw(ADV_HANDLE, per_len, per_data)); + WAIT_API(esp_ble_gap_periodic_adv_start(ADV_HANDLE)); +#endif + WAIT_API(esp_ble_gap_ext_adv_start(1, ext_adv_inst)); + + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); + return 0; +} diff --git a/examples/bluetooth/esp_ble_audio/cap/initiator/main/bluedroid/central.c b/examples/bluetooth/esp_ble_audio/cap/initiator/main/bluedroid/central.c new file mode 100644 index 00000000000..fbdfdd4dbb0 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/initiator/main/bluedroid/central.c @@ -0,0 +1,189 @@ +/* + * 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 "cap_initiator.h" + +static SemaphoreHandle_t scan_sem; + +/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */ +static esp_bt_status_t scan_op_status; + +#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, scan_sem, portMAX_DELAY, scan_op_status) + +/* Cached peer address. Bluedroid's pairing and disconnect APIs key off + * bd_addr rather than conn_handle, so we stash the addr at conn_create + * time and reuse it in pairing_start / security_failed_recover. */ +static esp_bd_addr_t peer_bda; + +static esp_ble_ext_scan_params_t ext_scan_params = { + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE, + .cfg_mask = ESP_BLE_GAP_EXT_SCAN_CFG_UNCODE_MASK, + .uncoded_cfg = { + .scan_type = BLE_SCAN_TYPE_PASSIVE, + .scan_interval = SCAN_INTERVAL, + .scan_window = SCAN_WINDOW, + }, +}; + +static void gap_event_handler(esp_gap_ble_cb_event_t event, + esp_ble_gap_cb_param_t *param) +{ + switch (event) { + case ESP_GAP_BLE_SET_EXT_SCAN_PARAMS_COMPLETE_EVT: + scan_op_status = param->set_ext_scan_params.status; + xSemaphoreGive(scan_sem); + break; + case ESP_GAP_BLE_EXT_SCAN_START_COMPLETE_EVT: + scan_op_status = param->ext_scan_start.status; + xSemaphoreGive(scan_sem); + break; + case ESP_GAP_BLE_EXT_SCAN_STOP_COMPLETE_EVT: + scan_op_status = param->ext_scan_stop.status; + xSemaphoreGive(scan_sem); + break; + + case ESP_GAP_BLE_NC_REQ_EVT: + esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, true); + break; + + /* AUTH_CMPL has no BTA channel — app must forward it. EXT_ADV_REPORT + * is forwarded by adapter's BTA path, don't re-post here. */ + case ESP_GAP_BLE_AUTH_CMPL_EVT: + esp_ble_audio_gap_app_post_event(event, param); + break; + + default: + break; + } +} + +int app_host_init(void) +{ + esp_err_t err; + + scan_sem = xSemaphoreCreateBinary(); + if (scan_sem == NULL) { + ESP_LOGE(TAG, "Failed to create scan semaphore"); + return -1; + } + + err = esp_ble_gap_register_callback(gap_event_handler); + if (err) { + ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err); + vSemaphoreDelete(scan_sem); + return err; + } + + return 0; +} + +int set_device_name(void) +{ + return esp_ble_gap_set_device_name(LOCAL_DEVICE_NAME); +} + +int ext_scan_start(void) +{ + WAIT_API(esp_ble_gap_set_ext_scan_params(&ext_scan_params)); + WAIT_API(esp_ble_gap_start_ext_scan(0, 0)); + + ESP_LOGI(TAG, "Scanning for CAP Acceptor..."); + return 0; +} + +int ext_scan_stop(void) +{ + WAIT_API(esp_ble_gap_stop_ext_scan()); + return 0; +} + +int conn_create(uint8_t addr_type, const uint8_t addr[6]) +{ + /* Values shared with NimBLE side via cap_initiator.h for behavioral parity. + * Without prefer_ext_connect_params_set, L2CAP falls back to defaults + * and logs "No extend connection parameters set". */ + const esp_ble_gap_conn_params_t conn_params = { + .scan_interval = INIT_SCAN_INTERVAL, + .scan_window = INIT_SCAN_WINDOW, + .interval_min = CONN_INTERVAL, + .interval_max = CONN_INTERVAL, + .latency = CONN_LATENCY, + .supervision_timeout = CONN_TIMEOUT, + .min_ce_len = CONN_MIN_CE_LEN, + .max_ce_len = CONN_MAX_CE_LEN, + }; + esp_gatt_if_t gattc_if; + esp_err_t err; + + memcpy(peer_bda, addr, sizeof(peer_bda)); + + err = esp_ble_gap_prefer_ext_connect_params_set( + peer_bda, ESP_BLE_GAP_PHY_1M_PREF_MASK, &conn_params, NULL, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to set ext conn params, err %d", err); + return err; + } + + /* Use the audio engine's GATTC if — events route back to the engine + * which forwards them to the lib. Registering a separate example GATTC + * app would steer events to a handler the engine never sees. + * + * engine returns ESP_GATT_IF_NONE (0xFF) when GATTC is not yet + * registered; feeding that into aux_open silently no-ops in BTC and + * no acl_connect event ever fires, leaving the caller in a no-scan + * / no-conn dead state. */ + gattc_if = esp_ble_audio_bluedroid_get_gattc_if(); + if (gattc_if == ESP_GATT_IF_NONE) { + ESP_LOGE(TAG, "GATTC not registered"); + return ESP_ERR_INVALID_STATE; + } + + return esp_ble_gattc_aux_open(gattc_if, peer_bda, + (esp_ble_addr_type_t)addr_type, true); +} + +int pairing_start(uint16_t conn_handle) +{ + (void)conn_handle; + return esp_ble_set_encryption(peer_bda, ESP_BLE_SEC_ENCRYPT_NO_MITM); +} + +int exchange_mtu(uint16_t conn_handle) +{ + (void)conn_handle; + /* The Bluedroid GATTC adapter exchanges MTU automatically when aux_open + * brings up the ACL — the MTU_UPDATED event arrives without an explicit + * kick-off. NimBLE has no such auto-exchange, so this wrapper is a no-op + * on bluedroid and a real ble_gattc_exchange_mtu on nimble. */ + return 0; +} + +void security_failed_recover(uint16_t conn_handle, uint8_t status) +{ + (void)conn_handle; + + ESP_LOGE(TAG, "Security change failed, status %u, clearing local bond and reconnecting", status); + + esp_ble_remove_bond_device(peer_bda); + esp_ble_gap_disconnect(peer_bda); +} 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 f8b2201290d..76fd734f957 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 @@ -5,13 +5,12 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include + #include "esp_log.h" #include "sdkconfig.h" -#include "host/ble_hs.h" -#include "services/gap/ble_svc_gap.h" - #include "esp_ble_audio_lc3_defs.h" #include "esp_ble_audio_bap_api.h" #include "esp_ble_audio_cap_api.h" @@ -23,7 +22,52 @@ #define TAG "CAP_INI" -#define CONN_HANDLE_INIT 0xFFFF +#define CONN_HANDLE_INIT 0xFFFF + +#if CONFIG_EXAMPLE_UNICAST +#define LOCAL_DEVICE_NAME "CAP Initiator" + +#define SCAN_INTERVAL 160 /* 100ms */ +#define SCAN_WINDOW 160 /* 100ms */ + +/* ACL init parameters shared between bluedroid and nimble host wrappers. + * Raw HCI units (scan: 0.625ms; conn interval: 1.25ms; timeout: 10ms). */ +#define INIT_SCAN_INTERVAL 16 /* 10ms */ +#define INIT_SCAN_WINDOW 16 /* 10ms */ +#define CONN_INTERVAL 24 /* 30ms */ +#define CONN_LATENCY 0 +#define CONN_TIMEOUT 500 /* 5s */ +#define CONN_MIN_CE_LEN 0xFFFF +#define CONN_MAX_CE_LEN 0xFFFF +#elif CONFIG_EXAMPLE_BROADCAST +#define LOCAL_DEVICE_NAME "CAP Broadcast Source" + +#define ADV_HANDLE 0 +#define ADV_SID 0 +#define ADV_TX_POWER 127 +#define ADV_INTERVAL_MS 200 +#define PER_ADV_INTERVAL_MS 100 +#endif + +int app_host_init(void); + +int set_device_name(void); + +#if CONFIG_EXAMPLE_UNICAST +int ext_scan_start(void); +int ext_scan_stop(void); + +int conn_create(uint8_t addr_type, const uint8_t addr[6]); + +int pairing_start(uint16_t conn_handle); + +int exchange_mtu(uint16_t conn_handle); + +void security_failed_recover(uint16_t conn_handle, uint8_t status); +#elif CONFIG_EXAMPLE_BROADCAST +int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len, + const uint8_t *per_data, uint8_t per_len); +#endif struct tx_stream { esp_ble_audio_cap_stream_t *stream; 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 4010d691377..dabd5e8e1e1 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 @@ -6,24 +6,11 @@ */ #include -#include #include -#include #include #include "cap_initiator.h" -#define ADV_HANDLE 0x00 -#define ADV_SID 0 -#define ADV_TX_POWER 127 -#define ADV_ADDRESS BLE_OWN_ADDR_PUBLIC -#define ADV_PRIMARY_PHY BLE_HCI_LE_PHY_1M -#define ADV_SECONDARY_PHY BLE_HCI_LE_PHY_2M -#define ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(200) - -#define PER_ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(100) - -#define LOCAL_DEVICE_NAME "CAP Broadcast Source" #define LOCAL_BROADCAST_ID 0x123456 ESP_BLE_AUDIO_BAP_LC3_BROADCAST_PRESET_16_2_1_DEFINE(broadcast_preset_16_2_1, @@ -146,120 +133,6 @@ static uint8_t *per_adv_data_get(uint8_t *data_len) return data; } -static int ext_adv_start(void) -{ - struct ble_gap_periodic_adv_params per_params = {0}; - struct ble_gap_ext_adv_params ext_params = {0}; - struct os_mbuf *data = NULL; - uint8_t *ext_data = NULL; - uint8_t *per_data = NULL; - uint8_t data_len = 0; - int err; - - ext_params.connectable = 0; - ext_params.scannable = 0; - ext_params.legacy_pdu = 0; - ext_params.own_addr_type = ADV_ADDRESS; - ext_params.primary_phy = ADV_PRIMARY_PHY; - ext_params.secondary_phy = ADV_SECONDARY_PHY; - ext_params.tx_power = ADV_TX_POWER; - ext_params.sid = ADV_SID; - ext_params.itvl_min = ADV_INTERVAL; - ext_params.itvl_max = ADV_INTERVAL; - - err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL, - example_audio_gap_event_cb, NULL); - if (err) { - ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err); - goto end; - } - - ext_data = ext_adv_data_get(&data_len); - if (ext_data == NULL) { - err = -ENOMEM; - goto end; - } - - data = os_msys_get_pkthdr(data_len, 0); - if (data == NULL) { - ESP_LOGE(TAG, "Failed to get ext adv mbuf"); - err = -ENOMEM; - goto end; - } - - err = os_mbuf_append(data, ext_data, data_len); - if (err) { - ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err); - os_mbuf_free_chain(data); - goto end; - } - - err = ble_gap_ext_adv_set_data(ADV_HANDLE, data); - if (err) { - ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err); - goto end; - } - - per_params.include_tx_power = 0; - per_params.itvl_min = PER_ADV_INTERVAL; - per_params.itvl_max = PER_ADV_INTERVAL; - - err = ble_gap_periodic_adv_configure(ADV_HANDLE, &per_params); - if (err) { - ESP_LOGE(TAG, "Failed to configure per adv params, err %d", err); - goto end; - } - - per_data = per_adv_data_get(&data_len); - if (per_data == NULL) { - err = -ENOMEM; - goto end; - } - - data = os_msys_get_pkthdr(data_len, 0); - if (data == NULL) { - ESP_LOGE(TAG, "Failed to get per adv mbuf"); - err = -ENOMEM; - goto end; - } - - err = os_mbuf_append(data, per_data, data_len); - if (err) { - ESP_LOGE(TAG, "Failed to append per adv data, err %d", err); - os_mbuf_free_chain(data); - goto end; - } - - err = ble_gap_periodic_adv_set_data(ADV_HANDLE, data); - if (err) { - ESP_LOGE(TAG, "Failed to set per adv data, err %d", err); - goto end; - } - - err = ble_gap_periodic_adv_start(ADV_HANDLE); - if (err) { - ESP_LOGE(TAG, "Failed to start per advertising, err %d", err); - goto end; - } - - err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0); - if (err) { - ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err); - goto end; - } - - ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); - -end: - if (ext_data) { - free(ext_data); - } - if (per_data) { - free(per_data); - } - return err; -} - int cap_initiator_broadcast_start(void) { esp_ble_audio_cap_initiator_broadcast_stream_param_t stream_params = { @@ -278,6 +151,11 @@ int cap_initiator_broadcast_start(void) esp_ble_audio_bap_broadcast_adv_info_t info = { .adv_handle = ADV_HANDLE, }; + /* BASE (per_adv_data_get) needs broadcast_source to exist — build after create. */ + uint8_t *ext_data = NULL; + uint8_t *per_data = NULL; + uint8_t ext_len = 0; + uint8_t per_len = 0; int err; ESP_LOGI(TAG, "Creating broadcast source"); @@ -288,7 +166,19 @@ int cap_initiator_broadcast_start(void) return err; } - err = ext_adv_start(); + ext_data = ext_adv_data_get(&ext_len); + if (ext_data == NULL) { + err = -ENOMEM; + goto end; + } + + per_data = per_adv_data_get(&per_len); + if (per_data == NULL) { + err = -ENOMEM; + goto end; + } + + err = ext_adv_start(ext_data, ext_len, per_data, per_len); if (err) { goto end; } @@ -302,15 +192,20 @@ int cap_initiator_broadcast_start(void) err = esp_ble_audio_cap_initiator_broadcast_audio_start(broadcast_source, ADV_HANDLE); if (err) { ESP_LOGE(TAG, "Failed to start broadcast source, err %d", err); - goto end; } - return 0; - end: - err = esp_ble_audio_cap_initiator_broadcast_audio_delete(broadcast_source); + if (ext_data != NULL) { + free(ext_data); + } + if (per_data != NULL) { + free(per_data); + } if (err) { - ESP_LOGE(TAG, "Failed to delete broadcast source, err %d", err); + esp_err_t e = esp_ble_audio_cap_initiator_broadcast_audio_delete(broadcast_source); + if (e) { + ESP_LOGE(TAG, "Failed to delete broadcast source, err %d", e); + } } return err; } 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 79e24f17b05..1966901d8e0 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 @@ -9,7 +9,6 @@ #include #include #include -#include #include #include "cap_initiator.h" 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 64afcd7fe69..127b8a875a3 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,24 +7,10 @@ #include #include -#include #include -#include #include "cap_initiator.h" -#define SCAN_INTERVAL 160 /* 100ms */ -#define SCAN_WINDOW 160 /* 100ms */ - -#define INIT_SCAN_INTERVAL 16 /* 10ms */ -#define INIT_SCAN_WINDOW 16 /* 10ms */ -#define CONN_INTERVAL 64 /* 64 * 1.25 = 80ms */ -#define CONN_LATENCY 0 -#define CONN_TIMEOUT 500 /* 500 * 10ms = 5s */ -#define CONN_MAX_CE_LEN 0xFFFF -#define CONN_MIN_CE_LEN 0xFFFF -#define CONN_DURATION 10000 /* 10s */ - ESP_BLE_AUDIO_BAP_LC3_UNICAST_PRESET_16_2_1_DEFINE(unicast_preset_16_2_1, ESP_BLE_AUDIO_LOCATION_MONO_AUDIO, ESP_BLE_AUDIO_CONTEXT_TYPE_UNSPECIFIED); @@ -39,7 +25,6 @@ static struct peer_config { esp_ble_conn_t *conn; uint16_t conn_handle; bool disc_completed; - bool disc_cancelled; bool mtu_exchanged; } peer = { .conn_handle = CONN_HANDLE_INIT, @@ -454,54 +439,10 @@ static esp_ble_audio_cap_initiator_cb_t cap_cb = { .unicast_start_complete = unicast_start_complete_cb, }; -static int conn_create(ble_addr_t *dst) -{ - struct ble_gap_conn_params params = {0}; - uint8_t own_addr_type = 0; - int err; - - err = ble_hs_id_infer_auto(0, &own_addr_type); - if (err) { - ESP_LOGE(TAG, "Failed to determine address type, err %d", err); - return err; - } - - err = ble_gap_disc_cancel(); - if (err) { - ESP_LOGE(TAG, "Failed to stop scanning, err %d", err); - return err; - } - - peer.disc_cancelled = true; - - params.scan_itvl = INIT_SCAN_INTERVAL; - params.scan_window = INIT_SCAN_WINDOW; - params.itvl_min = CONN_INTERVAL; - params.itvl_max = CONN_INTERVAL; - params.latency = CONN_LATENCY; - params.supervision_timeout = CONN_TIMEOUT; - params.max_ce_len = CONN_MAX_CE_LEN; - params.min_ce_len = CONN_MIN_CE_LEN; - - return ble_gap_connect(own_addr_type, dst, CONN_DURATION, ¶ms, - example_audio_gap_event_cb, NULL); -} - -static int pairing_start(uint16_t conn_handle) -{ - return ble_gap_security_initiate(conn_handle); -} - -static int exchange_mtu(uint16_t conn_handle) -{ - return ble_gattc_exchange_mtu(conn_handle, NULL, NULL); -} - static bool check_and_connect(uint8_t type, const uint8_t *data, uint8_t data_len, void *user_data) { esp_ble_audio_gap_app_event_t *event; - ble_addr_t dst = {0}; uint16_t uuid_val; int err; @@ -526,17 +467,20 @@ static bool check_and_connect(uint8_t type, const uint8_t *data, ESP_LOGI(TAG, "Found CAS in peer adv data!"); - dst.type = event->ext_scan_recv.addr.type; - memcpy(dst.val, event->ext_scan_recv.addr.val, sizeof(dst.val)); + /* Stop scanning before connect — NimBLE rejects ble_gap_connect while + * a discovery procedure is running. On failure restart scanning so we + * don't stall in no-scan no-conn state. */ + err = ext_scan_stop(); + if (err) { + ESP_LOGE(TAG, "Failed to stop scanning, err %d", err); + return false; + } - err = conn_create(&dst); + err = conn_create(event->ext_scan_recv.addr.type, + event->ext_scan_recv.addr.val); if (err) { ESP_LOGE(TAG, "Failed to create conn, err %d", err); - - if (peer.disc_cancelled) { - peer.disc_cancelled = false; - cap_initiator_unicast_start(); - } + cap_initiator_unicast_start(); } return false; /* Stop parsing */ @@ -562,14 +506,16 @@ static void acl_connect(esp_ble_audio_gap_app_event_t *event) if (event->acl_connect.status) { ESP_LOGE(TAG, "Connection failed, status %d", event->acl_connect.status); + /* check_and_connect stopped scanning before conn_create. acl_disconnect + * (which restarts scan) only fires on an established connection, so + * we must resume here to avoid a no-scan / no-conn dead state. */ + cap_initiator_unicast_start(); return; } ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x", event->acl_connect.conn_handle, event->acl_connect.role, - event->acl_connect.dst.val[5], event->acl_connect.dst.val[4], - event->acl_connect.dst.val[3], event->acl_connect.dst.val[2], - event->acl_connect.dst.val[1], event->acl_connect.dst.val[0]); + EXAMPLE_BT_ADDR_PRINT_ARGS(event->acl_connect.dst.val)); peer.conn_handle = event->acl_connect.conn_handle; @@ -590,7 +536,6 @@ static void acl_disconnect(esp_ble_audio_gap_app_event_t *event) peer.source_ep = NULL; peer.sink_ep = NULL; peer.disc_completed = false; - peer.disc_cancelled = false; peer.mtu_exchanged = false; unicast_group_delete(); @@ -603,8 +548,8 @@ static void security_change(esp_ble_iso_gap_app_event_t *event) int err; if (event->security_change.status) { - example_audio_security_failed_recover(TAG, event->security_change.conn_handle, - event->security_change.status); + security_failed_recover(event->security_change.conn_handle, + event->security_change.status); return; } @@ -707,29 +652,7 @@ void cap_initiator_unicast_gatt_cb(esp_ble_audio_gatt_app_event_t *event) int cap_initiator_unicast_start(void) { - struct ble_gap_disc_params params = {0}; - uint8_t own_addr_type; - int err; - - err = ble_hs_id_infer_auto(0, &own_addr_type); - if (err) { - ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err); - return err; - } - - params.passive = 1; - params.itvl = SCAN_INTERVAL; - params.window = SCAN_WINDOW; - - err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms, - example_audio_gap_event_cb, NULL); - if (err) { - ESP_LOGE(TAG, "Failed to start scanning, err %d", err); - return err; - } - - ESP_LOGI(TAG, "Scanning for CAP Acceptor..."); - return 0; + return ext_scan_start(); } int cap_initiator_unicast_init(void) diff --git a/examples/bluetooth/esp_ble_audio/cap/initiator/main/main.c b/examples/bluetooth/esp_ble_audio/cap/initiator/main/main.c index a5cf2e9ad54..80994b8b4c9 100644 --- a/examples/bluetooth/esp_ble_audio/cap/initiator/main/main.c +++ b/examples/bluetooth/esp_ble_audio/cap/initiator/main/main.c @@ -6,13 +6,8 @@ */ #include -#include -#include -#include -#include #include "nvs_flash.h" -#include "esp_system.h" #include "cap_initiator.h" @@ -40,6 +35,12 @@ void app_main(void) return; } + err = app_host_init(); + if (err) { + ESP_LOGE(TAG, "Failed to init host, err %d", err); + return; + } + err = esp_ble_audio_common_init(&info); if (err) { ESP_LOGE(TAG, "Failed to initialize audio, err %d", err); @@ -68,16 +69,7 @@ void app_main(void) return; } - /* Match the GAP device name to the role this build was compiled for, so - * the name advertised in GAP reads matches the local-name field embedded - * in extended advertising (broadcast mode uses "CAP Broadcast Source", - * which is what the acceptor's self-scan path matches against). - */ -#if CONFIG_EXAMPLE_BROADCAST - err = ble_svc_gap_device_name_set("CAP Broadcast Source"); -#else - err = ble_svc_gap_device_name_set("CAP Initiator"); -#endif + err = set_device_name(); if (err) { ESP_LOGE(TAG, "Failed to set device name, err %d", err); return; diff --git a/examples/bluetooth/esp_ble_audio/cap/initiator/main/nimble/adv.c b/examples/bluetooth/esp_ble_audio/cap/initiator/main/nimble/adv.c new file mode 100644 index 00000000000..1457e44a389 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/initiator/main/nimble/adv.c @@ -0,0 +1,123 @@ +/* + * 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 "services/gap/ble_svc_gap.h" + +#include "os/os_mbuf.h" + +#include "cap_initiator.h" + +/* Non-connectable broadcaster: no GAP events for the application. */ +static int gap_event_cb(struct ble_gap_event *event, void *arg) +{ + return 0; +} + +int app_host_init(void) +{ + return 0; +} + +int set_device_name(void) +{ + return ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME); +} + +int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len, + const uint8_t *per_data, uint8_t per_len) +{ + struct ble_gap_periodic_adv_params per_params = {0}; + struct ble_gap_ext_adv_params ext_params = {0}; + struct os_mbuf *data = NULL; + int err; + + ext_params.connectable = 0; + ext_params.scannable = 0; + ext_params.legacy_pdu = 0; + ext_params.own_addr_type = BLE_OWN_ADDR_PUBLIC; + ext_params.primary_phy = BLE_HCI_LE_PHY_1M; + ext_params.secondary_phy = BLE_HCI_LE_PHY_2M; + ext_params.tx_power = ADV_TX_POWER; + ext_params.sid = ADV_SID; + ext_params.itvl_min = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS); + ext_params.itvl_max = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS); + + err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL, + gap_event_cb, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err); + return err; + } + + data = os_msys_get_pkthdr(ext_len, 0); + if (data == NULL) { + ESP_LOGE(TAG, "Failed to get ext adv mbuf"); + return -1; + } + + err = os_mbuf_append(data, ext_data, ext_len); + if (err) { + ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err); + os_mbuf_free_chain(data); + return err; + } + + err = ble_gap_ext_adv_set_data(ADV_HANDLE, data); + if (err) { + ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err); + return err; + } + + per_params.include_tx_power = 0; + per_params.itvl_min = BLE_GAP_PERIODIC_ITVL_MS(PER_ADV_INTERVAL_MS); + per_params.itvl_max = BLE_GAP_PERIODIC_ITVL_MS(PER_ADV_INTERVAL_MS); + + err = ble_gap_periodic_adv_configure(ADV_HANDLE, &per_params); + if (err) { + ESP_LOGE(TAG, "Failed to configure per adv params, err %d", err); + return err; + } + + data = os_msys_get_pkthdr(per_len, 0); + if (data == NULL) { + ESP_LOGE(TAG, "Failed to get per adv mbuf"); + return -1; + } + + err = os_mbuf_append(data, per_data, per_len); + if (err) { + ESP_LOGE(TAG, "Failed to append per adv data, err %d", err); + os_mbuf_free_chain(data); + return err; + } + + err = ble_gap_periodic_adv_set_data(ADV_HANDLE, data); + if (err) { + ESP_LOGE(TAG, "Failed to set per adv data, err %d", err); + return err; + } + + err = ble_gap_periodic_adv_start(ADV_HANDLE); + if (err) { + ESP_LOGE(TAG, "Failed to start per advertising, err %d", err); + return err; + } + + err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0); + if (err) { + ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); + return 0; +} diff --git a/examples/bluetooth/esp_ble_audio/cap/initiator/main/nimble/central.c b/examples/bluetooth/esp_ble_audio/cap/initiator/main/nimble/central.c new file mode 100644 index 00000000000..608c432ce32 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/initiator/main/nimble/central.c @@ -0,0 +1,149 @@ +/* + * 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_initiator.h" + +/* Init/conn parameters are shared with the bluedroid wrapper via cap_initiator.h. + * CONN_DURATION is NimBLE-specific (ble_gap_connect's discovery timeout). */ +#define CONN_DURATION 10000 /* 10s */ + +static int gap_event_cb(struct ble_gap_event *event, void *arg) +{ + switch (event->type) { + case BLE_GAP_EVENT_EXT_DISC: + case BLE_GAP_EVENT_CONNECT: + case BLE_GAP_EVENT_DISCONNECT: + case BLE_GAP_EVENT_ENC_CHANGE: + esp_ble_audio_gap_app_post_event(event->type, event); + break; + case BLE_GAP_EVENT_MTU: + case BLE_GAP_EVENT_NOTIFY_RX: + case BLE_GAP_EVENT_NOTIFY_TX: + case BLE_GAP_EVENT_SUBSCRIBE: + esp_ble_audio_gatt_app_post_event(event->type, event); + break; + case BLE_GAP_EVENT_REPEAT_PAIRING: { + struct ble_gap_conn_desc desc = {0}; + int rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc); + if (rc == 0) { + ble_store_util_delete_peer(&desc.peer_id_addr); + } + return BLE_GAP_REPEAT_PAIRING_RETRY; + } + default: + break; + } + + return 0; +} + +int app_host_init(void) +{ + return 0; +} + +int set_device_name(void) +{ + return ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME); +} + +int ext_scan_start(void) +{ + struct ble_gap_disc_params params = {0}; + uint8_t own_addr_type; + int err; + + err = ble_hs_id_infer_auto(0, &own_addr_type); + if (err) { + ESP_LOGE(TAG, "Failed to determine address type, err %d", err); + return err; + } + + params.passive = 1; + params.itvl = SCAN_INTERVAL; + params.window = SCAN_WINDOW; + + err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶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}; + uint8_t own_addr_type = 0; + ble_addr_t dst = {0}; + int err; + + err = ble_hs_id_infer_auto(0, &own_addr_type); + if (err) { + ESP_LOGE(TAG, "Failed to determine address type, err %d", err); + return err; + } + + params.scan_itvl = INIT_SCAN_INTERVAL; + params.scan_window = INIT_SCAN_WINDOW; + params.itvl_min = CONN_INTERVAL; + params.itvl_max = CONN_INTERVAL; + params.latency = CONN_LATENCY; + params.supervision_timeout = CONN_TIMEOUT; + params.max_ce_len = CONN_MAX_CE_LEN; + params.min_ce_len = CONN_MIN_CE_LEN; + + dst.type = addr_type; + memcpy(dst.val, addr, sizeof(dst.val)); + + return ble_gap_connect(own_addr_type, &dst, CONN_DURATION, + ¶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/initiator/sdkconfig.defaults b/examples/bluetooth/esp_ble_audio/cap/initiator/sdkconfig.defaults index 478a4cc3975..63c1276ecd2 100644 --- a/examples/bluetooth/esp_ble_audio/cap/initiator/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/cap/initiator/sdkconfig.defaults @@ -3,14 +3,15 @@ # CONFIG_BT_ENABLED=y -CONFIG_BT_BLUEDROID_ENABLED=n -CONFIG_BT_NIMBLE_ENABLED=y -CONFIG_BT_NIMBLE_EXT_ADV=y -CONFIG_BT_NIMBLE_NVS_PERSIST=y -CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 -CONFIG_BT_NIMBLE_MAX_CCCDS=20 -CONFIG_BT_NIMBLE_ISO=y -CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y +CONFIG_BT_NIMBLE_ENABLED=n +CONFIG_BT_BLUEDROID_ENABLED=y +CONFIG_BT_CLASSIC_ENABLED=n +CONFIG_BT_CONTROLLER_ENABLED=y +CONFIG_BT_BLE_ENABLED=y +CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y +CONFIG_BT_ACL_CONNECTIONS=1 +CONFIG_BT_GATTC_NOTIF_REG_MAX=20 +CONFIG_BT_BLE_FEAT_ISO_EN=y CONFIG_BT_ISO_MAX_CHAN=2 @@ -20,6 +21,8 @@ CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT=2 CONFIG_BT_BAP_BROADCAST_SOURCE=y CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT=2 +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y + CONFIG_EXAMPLE_UNICAST=y CONFIG_FREERTOS_HZ=1000 diff --git a/examples/bluetooth/esp_ble_audio/cap/initiator/sdkconfig.defaults.nimble b/examples/bluetooth/esp_ble_audio/cap/initiator/sdkconfig.defaults.nimble new file mode 100644 index 00000000000..ea3029ef03f --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/cap/initiator/sdkconfig.defaults.nimble @@ -0,0 +1,12 @@ +# NimBLE host overlay for this example. +# Use with: +# idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.$IDF_TARGET;sdkconfig.defaults.nimble" build + +CONFIG_BT_BLUEDROID_ENABLED=n +CONFIG_BT_NIMBLE_ENABLED=y +CONFIG_BT_NIMBLE_EXT_ADV=y +CONFIG_BT_NIMBLE_NVS_PERSIST=y +CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 +CONFIG_BT_NIMBLE_MAX_CCCDS=20 +CONFIG_BT_NIMBLE_ISO=y +CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y diff --git a/examples/bluetooth/esp_ble_audio/common_components/example_init/ble_audio_example_init.c b/examples/bluetooth/esp_ble_audio/common_components/example_init/ble_audio_example_init.c index 876ca84fa1a..ac3bea395be 100644 --- a/examples/bluetooth/esp_ble_audio/common_components/example_init/ble_audio_example_init.c +++ b/examples/bluetooth/esp_ble_audio/common_components/example_init/ble_audio_example_init.c @@ -4,16 +4,21 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include -#include +#include "sdkconfig.h" #include "esp_log.h" -#include "sdkconfig.h" #if CONFIG_BLE_LOG_ENABLED #include "ble_log.h" #endif +#ifdef CONFIG_BT_BLUEDROID_ENABLED +#include "esp_bt.h" +#include "esp_bt_main.h" +#include "esp_gap_ble_api.h" +#endif /* CONFIG_BT_BLUEDROID_ENABLED */ + +#ifdef CONFIG_BT_NIMBLE_ENABLED #include "nimble/nimble_port.h" #include "nimble/nimble_port_freertos.h" @@ -24,9 +29,109 @@ #include "services/gap/ble_svc_gap.h" #include "services/gatt/ble_svc_gatt.h" +#endif /* CONFIG_BT_NIMBLE_ENABLED */ + +#include "ble_audio_example_init.h" #define TAG "EXAMPLE_INIT" +#ifdef CONFIG_BT_BLUEDROID_ENABLED +esp_err_t bluetooth_init(void) +{ + esp_err_t ret; + + /* When controller log is disabled (or running in v1 mode), the + * automatic ble_log_init() inside esp_bt_controller_init() is skipped. + * Init manually here so HOST/ISO/AUDIO compressed logs aren't dropped + * during host bring-up. The function is idempotent. + * + * TODO: Remove this block (and the ble_log_flush() below) once + * ble_log_init() is decoupled from the controller log path and + * owns its own initialization independently. */ +#if CONFIG_BLE_LOG_ENABLED && \ + !(CONFIG_BT_LE_CONTROLLER_LOG_ENABLED && CONFIG_BT_LE_CONTROLLER_LOG_MODE_BLE_LOG_V2) + if (!ble_log_init()) { + ESP_LOGE(TAG, "Failed to init ble_log"); + return ESP_FAIL; + } +#endif + + ret = esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); + if (ret && ret != ESP_ERR_INVALID_STATE) { + ESP_LOGE(TAG, "Failed to release classic BT memory, err %d", ret); + goto err_log; + } + + esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); + ret = esp_bt_controller_init(&bt_cfg); + if (ret) { + ESP_LOGE(TAG, "Failed to init controller, err %d", ret); + goto err_log; + } + + ret = esp_bt_controller_enable(ESP_BT_MODE_BLE); + if (ret) { + ESP_LOGE(TAG, "Failed to enable controller, err %d", ret); + goto err_controller_init; + } + + esp_bluedroid_config_t cfg = BT_BLUEDROID_INIT_CONFIG_DEFAULT(); + ret = esp_bluedroid_init_with_cfg(&cfg); + if (ret) { + ESP_LOGE(TAG, "Failed to init bluedroid, err %d", ret); + goto err_controller_enable; + } + + ret = esp_bluedroid_enable(); + if (ret) { + ESP_LOGE(TAG, "Failed to enable bluedroid, err %d", ret); + goto err_bluedroid_init; + } + + /* Just Works pairing (no MITM, SC + bond, IO_CAP=NONE). + * Matches the NimBLE ble_hs_cfg defaults below. */ + esp_ble_auth_req_t auth_req = ESP_LE_AUTH_REQ_SC_BOND; + esp_ble_io_cap_t iocap = ESP_IO_CAP_NONE; + uint8_t key_size = 16; + uint8_t init_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK; + uint8_t rsp_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK; + + esp_ble_gap_set_security_param(ESP_BLE_SM_AUTHEN_REQ_MODE, &auth_req, sizeof(auth_req)); + esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(iocap)); + esp_ble_gap_set_security_param(ESP_BLE_SM_MAX_KEY_SIZE, &key_size, sizeof(key_size)); + esp_ble_gap_set_security_param(ESP_BLE_SM_SET_INIT_KEY, &init_key, sizeof(init_key)); + esp_ble_gap_set_security_param(ESP_BLE_SM_SET_RSP_KEY, &rsp_key, sizeof(rsp_key)); + + /* Drain startup-phase log buffers and emit a FLUSH boundary so the + * parser resets stats here — the example's runtime logs start clean. + * + * TODO: Remove together with the manual ble_log_init() above once + * ble_log_init() is decoupled from the controller log path. */ +#if CONFIG_BLE_LOG_ENABLED + ble_log_flush(); +#endif + + return ESP_OK; + +err_bluedroid_init: + esp_bluedroid_deinit(); +err_controller_enable: + esp_bt_controller_disable(); +err_controller_init: + esp_bt_controller_deinit(); +err_log: + /* Mirror the manual ble_log_init() block above — same Kconfig gate so + * we only deinit what we initialized; the controller-owned path is + * left alone. */ +#if CONFIG_BLE_LOG_ENABLED && \ + !(CONFIG_BT_LE_CONTROLLER_LOG_ENABLED && CONFIG_BT_LE_CONTROLLER_LOG_MODE_BLE_LOG_V2) + ble_log_deinit(); +#endif + return ret; +} +#endif /* CONFIG_BT_BLUEDROID_ENABLED */ + +#ifdef CONFIG_BT_NIMBLE_ENABLED static SemaphoreHandle_t example_audio_sem; void ble_store_config_init(void); @@ -228,7 +333,7 @@ esp_err_t bluetooth_init(void) /* When controller log is disabled (or running in v1 mode), the * automatic ble_log_init() inside esp_bt_controller_init() is skipped. * Init manually here so HOST/ISO/AUDIO compressed logs aren't dropped - * during NimBLE bring-up. The function is idempotent. + * during host bring-up. The function is idempotent. * * TODO: Remove this block (and the ble_log_flush() below) once * ble_log_init() is decoupled from the controller log path and @@ -297,3 +402,4 @@ err_log: #endif return ret; } +#endif /* CONFIG_BT_NIMBLE_ENABLED */ diff --git a/examples/bluetooth/esp_ble_audio/common_components/example_utils/ble_audio_example_utils.c b/examples/bluetooth/esp_ble_audio/common_components/example_utils/ble_audio_example_utils.c index 8a01ac4f0c3..7a6fca06de7 100644 --- a/examples/bluetooth/esp_ble_audio/common_components/example_utils/ble_audio_example_utils.c +++ b/examples/bluetooth/esp_ble_audio/common_components/example_utils/ble_audio_example_utils.c @@ -26,66 +26,6 @@ */ #define LOG_INTERVAL_PACKETS 6000 -int example_audio_gap_event_cb(struct ble_gap_event *event, void *arg) -{ - if (event->type == BLE_GAP_EVENT_EXT_DISC || - event->type == BLE_GAP_EVENT_PERIODIC_SYNC || - event->type == BLE_GAP_EVENT_PERIODIC_REPORT || - event->type == BLE_GAP_EVENT_PERIODIC_SYNC_LOST || - event->type == BLE_GAP_EVENT_PERIODIC_TRANSFER || - event->type == BLE_GAP_EVENT_PERIODIC_TRANSFER_V2 || - event->type == BLE_GAP_EVENT_CONNECT || - event->type == BLE_GAP_EVENT_DISCONNECT || - event->type == BLE_GAP_EVENT_ENC_CHANGE) { - esp_ble_audio_gap_app_post_event(event->type, event); - } else if (event->type == BLE_GAP_EVENT_MTU || - event->type == BLE_GAP_EVENT_NOTIFY_RX || - event->type == BLE_GAP_EVENT_NOTIFY_TX || - event->type == BLE_GAP_EVENT_SUBSCRIBE) { - esp_ble_audio_gatt_app_post_event(event->type, event); - } else if (event->type == BLE_GAP_EVENT_REPEAT_PAIRING) { - /* Peer wants to re-pair on top of an existing bond (e.g. phone - * cleared its LTK via "Forget device" but our NVS still has it). - * Delete our stale bond and tell NimBLE to retry pairing. - */ - struct ble_gap_conn_desc desc = {0}; - int rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc); - if (rc == 0) { - ble_store_util_delete_peer(&desc.peer_id_addr); - } - return BLE_GAP_REPEAT_PAIRING_RETRY; - } - - return 0; -} - -void example_audio_security_failed_recover(const char *tag, uint16_t conn_handle, uint8_t status) -{ - struct ble_gap_conn_desc desc = {0}; - int rc; - - /* status 13 = BLE_HS_ETIMEOUT: SMP exchange did not complete. Typical - * cause is asymmetric bond state — the two sides bonded previously, - * then one side erased its NVS while the other kept the LTK. The side - * that still has the bond tries encryption with the cached key; the - * other side has nothing to match it against, so SMP times out. Drop - * our entry for this peer and disconnect; the next connection runs - * fresh pairing (peer side recovers via REPEAT_PAIRING). - */ - ESP_LOGE(tag, "Security change failed, status %u, clearing local bond and reconnecting", status); - - rc = ble_gap_conn_find(conn_handle, &desc); - if (rc == 0) { - ble_store_util_delete_peer(&desc.peer_id_addr); - } - - /* Tear down the link: SMP cannot be retried on the same connection - * after a failed/timed-out exchange. The next reconnect starts a - * fresh SMP procedure with the now-cleared bond state. - */ - ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM); -} - static void print_hex(const uint8_t *ptr, size_t len) { while (len--) { diff --git a/examples/bluetooth/esp_ble_audio/common_components/example_utils/ble_audio_example_utils.h b/examples/bluetooth/esp_ble_audio/common_components/example_utils/ble_audio_example_utils.h index c23d32fd7d0..cb59426b62a 100644 --- a/examples/bluetooth/esp_ble_audio/common_components/example_utils/ble_audio_example_utils.h +++ b/examples/bluetooth/esp_ble_audio/common_components/example_utils/ble_audio_example_utils.h @@ -16,12 +16,10 @@ #include "sdkconfig.h" #include "esp_err.h" +#include "esp_log.h" -#include "zephyr/kernel.h" - -#include "host/ble_gap.h" -#include "host/ble_hs_adv.h" -#include "host/ble_store.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" #include "esp_ble_audio_bap_api.h" @@ -52,9 +50,60 @@ #define EXAMPLE_BYTES_LIST_LE48 BT_BYTES_LIST_LE48 #define EXAMPLE_BYTES_LIST_LE64 BT_BYTES_LIST_LE64 -int example_audio_gap_event_cb(struct ble_gap_event *event, void *arg); +/* bt_le_addr.val carries the host stack's native byte order: NimBLE is + * LSB-first (BT spec wire order), Bluedroid is MSB-first (BD_ADDR). Use + * this with "%02x:%02x:%02x:%02x:%02x:%02x" to print MSB-first regardless + * of host. Examples can otherwise pass val[] straight to their host APIs + * without conversion. */ +#if CONFIG_BT_BLUEDROID_ENABLED +#define EXAMPLE_BT_ADDR_PRINT_ARGS(_v) \ + (_v)[0], (_v)[1], (_v)[2], (_v)[3], (_v)[4], (_v)[5] -void example_audio_security_failed_recover(const char *tag, uint16_t conn_handle, uint8_t status); +/* Fire-and-wait helper for Bluedroid GAP APIs that pair an async call with + * a matching ESP_GAP_BLE_*_COMPLETE_EVT. The example's GAP event handler + * is expected to xSemaphoreGive(_sem) on that COMPLETE_EVT. `TAG` and the + * enclosing function's `esp_err_t` return are taken from the call site. + * + * !!! Demo-only — DO NOT copy into production code. !!! + * Risks (acceptable in linear init sequences, not in customer apps): + * - Same-task deadlock: must NOT be called from the task that dispatches + * the COMPLETE_EVT (BTC task by default; lib forwarding can route some + * events through other tasks — verify the path for your event). + * - portMAX_DELAY = no timeout: hangs forever on controller/firmware + * misbehavior. Production must use a bounded timeout + recovery. + * - Shared binary sem: concurrent callers race; first take consumes + * whichever give arrived. Examples avoid this by serializing init. + * - No abort hook: nothing can break a portMAX_DELAY wait at shutdown. + * - Status not checked: see EXAMPLE_WAIT_API_CHECK below. + * + * Production should drive BLE init/teardown via an event-driven state + * machine (one task owns the BLE flow, callbacks advance state, no + * blocking from inside callbacks). NimBLE host returns ctrl errors + * synchronously and sidesteps most of this complexity. */ +#define EXAMPLE_WAIT_API(_call, _sem, _delay) do { \ + esp_err_t _err = (_call); \ + if (_err != ESP_OK) { \ + ESP_LOGE(TAG, #_call " failed, err %d", _err); \ + return _err; \ + } \ + xSemaphoreTake(_sem, _delay); \ + } while (0) + +/* As EXAMPLE_WAIT_API but also checks the controller status latched by the + * gap_event_handler into `_op_status` after the sem is taken. Returns + * ESP_FAIL on async failure. Caller must provide a static esp_bt_status_t + * and write it from each COMPLETE_EVT param before xSemaphoreGive. */ +#define EXAMPLE_WAIT_API_CHECK(_call, _sem, _delay, _op_status) do { \ + EXAMPLE_WAIT_API(_call, _sem, _delay); \ + if ((_op_status) != ESP_BT_STATUS_SUCCESS) { \ + ESP_LOGE(TAG, #_call " ctrl status %d", (_op_status)); \ + return ESP_FAIL; \ + } \ + } while (0) +#else +#define EXAMPLE_BT_ADDR_PRINT_ARGS(_v) \ + (_v)[5], (_v)[4], (_v)[3], (_v)[2], (_v)[1], (_v)[0] +#endif void example_print_codec_cfg(const char *tag, const esp_ble_audio_codec_cfg_t *codec_cfg); diff --git a/examples/bluetooth/esp_ble_audio/tmap/bmr/README.md b/examples/bluetooth/esp_ble_audio/tmap/bmr/README.md index 3078f91144e..641d8e7887f 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/bmr/README.md +++ b/examples/bluetooth/esp_ble_audio/tmap/bmr/README.md @@ -9,7 +9,7 @@ This example implements the **Telephony and Media Audio Profile (TMAP) Broadcast Media Receiver (BMR)** role. The device scans for non-connectable extended advertisers that carry both the Broadcast Audio Announcement service (with a 24-bit broadcast ID) and the TMAS service with the **BMS** role bit set, then synchronizes to periodic advertising and the BIG to receive broadcast audio. -The implementation runs on the **NimBLE** host stack and uses ESP-BLE-AUDIO for TMAP role registration, the BAP broadcast sink, the BAP scan delegator, the PACS sink (LC3 capability: 48 kHz, 10 ms, 1 channel, 100 octets/frame, media context — sized for the 48_2_1 preset that the paired BMS sends), and a VCP **Volume Renderer** (initial volume 10, unmuted, step 1). Up to `CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT` streams are pre-allocated and reused for every BIG sync; received SDUs are accounted for via `example_audio_rx_metrics_*`. +The implementation uses ESP-BLE-AUDIO for TMAP role registration, the BAP broadcast sink, the BAP scan delegator, the PACS sink (LC3 capability: 48 kHz, 10 ms, 1 channel, 100 octets/frame, media context — sized for the 48_2_1 preset that the paired BMS sends), and a VCP **Volume Renderer** (initial volume 10, unmuted, step 1). Up to `CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT` streams are pre-allocated and reused for every BIG sync; received SDUs are accounted for via `example_audio_rx_metrics_*`. ## Requirements @@ -26,15 +26,28 @@ No build-time options — runtime defaults are baked into source. ### Security & Pairing -NimBLE host security is inherited from `../../common_components/example_init/ble_audio_example_init.c`: LE Secure Connections with bonding enabled, no MITM, **Just-Works** pairing (`BLE_SM_IO_CAP_NO_IO`). The BMR receives broadcast (unencrypted) audio, so these settings only apply if a peer (e.g. a Broadcast Assistant) opens an ATT connection to the scan delegator / VCP renderer. +Security is inherited from `../../common_components/example_init/ble_audio_example_init.c`: LE Secure Connections with bonding enabled, no MITM, **Just-Works** pairing (`BLE_SM_IO_CAP_NO_IO`). The BMR receives broadcast (unencrypted) audio, so these settings only apply if a peer (e.g. a Broadcast Assistant) opens an ATT connection to the scan delegator / VCP renderer. ## Build & Flash +The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`. + +### Bluedroid host (default) + ```bash idf.py set-target esp32h4 idf.py -p PORT flash monitor ``` +### NimBLE host + +```bash +idf.py set-target esp32h4 +idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor +``` + +For `esp32s31`, replace the chip overlay accordingly. + (Exit serial monitor with `Ctrl-]`.) ## Example Flow diff --git a/examples/bluetooth/esp_ble_audio/tmap/bmr/main/CMakeLists.txt b/examples/bluetooth/esp_ble_audio/tmap/bmr/main/CMakeLists.txt index befc7314e98..624a874cfa2 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/bmr/main/CMakeLists.txt +++ b/examples/bluetooth/esp_ble_audio/tmap/bmr/main/CMakeLists.txt @@ -2,5 +2,12 @@ set(srcs "bap_broadcast_sink.c" "vcp_vol_renderer.c" "main.c") +if(CONFIG_BT_BLUEDROID_ENABLED) + list(APPEND srcs "bluedroid/scan.c") +else() + list(APPEND srcs "nimble/scan.c") +endif() + idf_component_register(SRCS "${srcs}" + INCLUDE_DIRS "." REQUIRES bt nvs_flash) 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 532ab6c8ec9..4471171ae1b 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 @@ -9,15 +9,9 @@ #include #include #include -#include #include "tmap_bmr.h" -#define SCAN_INTERVAL 160 /* 100ms */ -#define SCAN_WINDOW 160 /* 100ms */ - -#define PA_SYNC_SKIP 0 -#define PA_SYNC_TIMEOUT 1000 /* 1000 * 10ms = 10s */ #define PA_SYNC_HANDLE_INIT UINT16_MAX static uint16_t sync_handle = PA_SYNC_HANDLE_INIT; @@ -142,30 +136,6 @@ static esp_ble_audio_bap_broadcast_sink_cb_t broadcast_sink_cbs = { static esp_ble_audio_bap_scan_delegator_cb_t scan_delegator_cbs; -static void sync_broadcast_pa(const bt_addr_le_t *addr, - uint8_t adv_sid, - uint32_t broadcast_id) -{ - struct ble_gap_periodic_sync_params params = {0}; - ble_addr_t sync_addr = {0}; - int err; - - sync_addr.type = addr->type; - memcpy(sync_addr.val, addr->a.val, sizeof(sync_addr.val)); - params.skip = PA_SYNC_SKIP; - params.sync_timeout = PA_SYNC_TIMEOUT; - - err = ble_gap_periodic_adv_sync_create(&sync_addr, adv_sid, ¶ms, - example_audio_gap_event_cb, NULL); - if (err) { - ESP_LOGE(TAG, "Failed to create PA sync, err %d", err); - return; - } - - bcast_id = broadcast_id; - pa_syncing = true; /* Mark PA sync as in progress */ -} - static bool scan_check_and_sync_broadcast(uint8_t type, const uint8_t *data, uint8_t data_len, void *user_data) { @@ -216,7 +186,7 @@ static bool scan_check_and_sync_broadcast(uint8_t type, const uint8_t *data, void bap_broadcast_scan_recv(esp_ble_audio_gap_app_event_t *event) { uint32_t broadcast_id; - bt_addr_le_t addr; + int err; if ((event->ext_scan_recv.event_type & EXAMPLE_ADV_PROP_CONNECTABLE) || event->ext_scan_recv.per_adv_itvl == 0) { @@ -238,10 +208,16 @@ void bap_broadcast_scan_recv(esp_ble_audio_gap_app_event_t *event) ESP_LOGI(TAG, "Found TMAP BMS, starting PA sync (broadcast ID 0x%06lx)", broadcast_id); - addr.type = event->ext_scan_recv.addr.type; - memcpy(addr.a.val, event->ext_scan_recv.addr.val, sizeof(addr.a.val)); + err = pa_sync_create(event->ext_scan_recv.addr.type, + event->ext_scan_recv.addr.val, + event->ext_scan_recv.sid); + if (err) { + ESP_LOGE(TAG, "Failed to create PA sync, err %d", err); + return; + } - sync_broadcast_pa(&addr, event->ext_scan_recv.sid, broadcast_id); + bcast_id = broadcast_id; + pa_syncing = true; } } @@ -259,7 +235,7 @@ void bap_broadcast_pa_sync(esp_ble_audio_gap_app_event_t *event) ESP_LOGI(TAG, "PA sync %u synced with broadcast ID 0x%06x", event->pa_sync.sync_handle, bcast_id); - err = ble_gap_disc_cancel(); + err = ext_scan_stop(); if (err) { ESP_LOGE(TAG, "Failed to stop scanning, err %d", err); return; @@ -296,29 +272,7 @@ void bap_broadcast_pa_lost(esp_ble_audio_gap_app_event_t *event) int bap_broadcast_sink_scan(void) { - struct ble_gap_disc_params params = {0}; - uint8_t own_addr_type; - int err; - - err = ble_hs_id_infer_auto(0, &own_addr_type); - if (err) { - ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err); - return err; - } - - params.passive = 1; - params.itvl = SCAN_INTERVAL; - params.window = SCAN_WINDOW; - - err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms, - example_audio_gap_event_cb, NULL); - if (err) { - ESP_LOGE(TAG, "Failed to start scanning, err %d", err); - return err; - } - - ESP_LOGI(TAG, "Scanning for broadcaster..."); - return 0; + return ext_scan_start(); } int bap_broadcast_sink_init(void) diff --git a/examples/bluetooth/esp_ble_audio/tmap/bmr/main/bluedroid/scan.c b/examples/bluetooth/esp_ble_audio/tmap/bmr/main/bluedroid/scan.c new file mode 100644 index 00000000000..e279d68137a --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/tmap/bmr/main/bluedroid/scan.c @@ -0,0 +1,117 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "esp_log.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +#include "esp_bt_defs.h" +#include "esp_gap_ble_api.h" + +#include "tmap_bmr.h" + +static SemaphoreHandle_t scan_sem; + +/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */ +static esp_bt_status_t scan_op_status; + +#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, scan_sem, portMAX_DELAY, scan_op_status) + +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; + + /* PA sync / ext adv events: forwarded by adapter's BTA path, not here. */ + default: + break; + } +} + +int app_host_init(void) +{ + esp_err_t err; + + scan_sem = xSemaphoreCreateBinary(); + if (scan_sem == NULL) { + ESP_LOGE(TAG, "Failed to create scan semaphore"); + return -1; + } + + err = esp_ble_gap_register_callback(gap_event_handler); + if (err) { + ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err); + vSemaphoreDelete(scan_sem); + return err; + } + + return 0; +} + +int 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 broadcaster..."); + return 0; +} + +int ext_scan_stop(void) +{ + WAIT_API(esp_ble_gap_stop_ext_scan()); + return 0; +} + +int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid) +{ + esp_ble_gap_periodic_adv_sync_params_t params = { + .filter_policy = 0, + .sid = sid, + .addr_type = addr_type, + .skip = PA_SYNC_SKIP, + .sync_timeout = PA_SYNC_TIMEOUT, + }; + + memcpy(params.addr, addr, sizeof(params.addr)); + + /* Fire-and-forget: sync establishment (PERIODIC_ADV_SYNC_ESTAB_EVT) is + * air-dependent and surfaces asynchronously. */ + return esp_ble_gap_periodic_adv_create_sync(¶ms); +} + +int pa_sync_terminate(uint16_t sync_handle) +{ + return esp_ble_gap_periodic_adv_sync_terminate(sync_handle); +} diff --git a/examples/bluetooth/esp_ble_audio/tmap/bmr/main/main.c b/examples/bluetooth/esp_ble_audio/tmap/bmr/main/main.c index 7d14d92e8fd..e809ec6034c 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/bmr/main/main.c +++ b/examples/bluetooth/esp_ble_audio/tmap/bmr/main/main.c @@ -6,13 +6,8 @@ */ #include -#include -#include -#include -#include #include "nvs_flash.h" -#include "esp_system.h" #include "tmap_bmr.h" @@ -26,9 +21,7 @@ static void pa_sync(esp_ble_audio_gap_app_event_t *event) ESP_LOGI(TAG, "PA synced: handle %u sid %u phy %u peer %02x:%02x:%02x:%02x:%02x:%02x", event->pa_sync.sync_handle, event->pa_sync.sid, event->pa_sync.adv_phy, - event->pa_sync.addr.val[5], event->pa_sync.addr.val[4], - event->pa_sync.addr.val[3], event->pa_sync.addr.val[2], - event->pa_sync.addr.val[1], event->pa_sync.addr.val[0]); + EXAMPLE_BT_ADDR_PRINT_ARGS(event->pa_sync.addr.val)); bap_broadcast_pa_sync(event); } @@ -80,6 +73,12 @@ void app_main(void) return; } + err = app_host_init(); + if (err) { + ESP_LOGE(TAG, "Failed to init host, err %d", err); + return; + } + err = esp_ble_audio_common_init(&init_info); if (err) { ESP_LOGE(TAG, "Failed to initialize audio, err %d", err); diff --git a/examples/bluetooth/esp_ble_audio/tmap/bmr/main/nimble/scan.c b/examples/bluetooth/esp_ble_audio/tmap/bmr/main/nimble/scan.c new file mode 100644 index 00000000000..c52e60c092b --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/tmap/bmr/main/nimble/scan.c @@ -0,0 +1,90 @@ +/* + * 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 "esp_ble_audio_common_api.h" + +#include "tmap_bmr.h" + +/* Forward only the GAP events the application consumes. */ +static int gap_event_cb(struct ble_gap_event *event, void *arg) +{ + switch (event->type) { + case BLE_GAP_EVENT_EXT_DISC: + case BLE_GAP_EVENT_PERIODIC_SYNC: + case BLE_GAP_EVENT_PERIODIC_REPORT: + case BLE_GAP_EVENT_PERIODIC_SYNC_LOST: + esp_ble_audio_gap_app_post_event(event->type, event); + break; + default: + break; + } + + return 0; +} + +int app_host_init(void) +{ + return 0; +} + +int ext_scan_start(void) +{ + struct ble_gap_disc_params params = {0}; + uint8_t own_addr_type; + int err; + + err = ble_hs_id_infer_auto(0, &own_addr_type); + if (err) { + ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err); + return err; + } + + params.passive = 1; + params.itvl = SCAN_INTERVAL; + params.window = SCAN_WINDOW; + + err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms, + gap_event_cb, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to start scanning, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Scanning for broadcaster..."); + return 0; +} + +int ext_scan_stop(void) +{ + return ble_gap_disc_cancel(); +} + +int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid) +{ + struct ble_gap_periodic_sync_params params = {0}; + ble_addr_t sync_addr = {0}; + + sync_addr.type = addr_type; + memcpy(sync_addr.val, addr, sizeof(sync_addr.val)); + params.skip = PA_SYNC_SKIP; + params.sync_timeout = PA_SYNC_TIMEOUT; + + return ble_gap_periodic_adv_sync_create(&sync_addr, sid, ¶ms, + gap_event_cb, NULL); +} + +int pa_sync_terminate(uint16_t sync_handle) +{ + return ble_gap_periodic_adv_sync_terminate(sync_handle); +} diff --git a/examples/bluetooth/esp_ble_audio/tmap/bmr/main/tmap_bmr.h b/examples/bluetooth/esp_ble_audio/tmap/bmr/main/tmap_bmr.h index 24f0de93020..8df89fb53f5 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/bmr/main/tmap_bmr.h +++ b/examples/bluetooth/esp_ble_audio/tmap/bmr/main/tmap_bmr.h @@ -5,12 +5,12 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include + #include "esp_log.h" #include "sdkconfig.h" -#include "host/ble_hs.h" - #include "esp_ble_audio_bap_api.h" #include "esp_ble_audio_cap_api.h" #include "esp_ble_audio_pacs_api.h" @@ -22,6 +22,20 @@ #define TAG "TMAP_BMR" +#define SCAN_INTERVAL 160 /* 100ms */ +#define SCAN_WINDOW 160 /* 100ms */ + +#define PA_SYNC_SKIP 0 +#define PA_SYNC_TIMEOUT 1000 /* 1000 * 10ms = 10s */ + +int app_host_init(void); + +int ext_scan_start(void); +int ext_scan_stop(void); + +int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid); +int pa_sync_terminate(uint16_t sync_handle); + int vcp_vol_renderer_init(void); void bap_broadcast_scan_recv(esp_ble_audio_gap_app_event_t *event); diff --git a/examples/bluetooth/esp_ble_audio/tmap/bmr/main/vcp_vol_renderer.c b/examples/bluetooth/esp_ble_audio/tmap/bmr/main/vcp_vol_renderer.c index 12cf614bf85..55b088a5f74 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/bmr/main/vcp_vol_renderer.c +++ b/examples/bluetooth/esp_ble_audio/tmap/bmr/main/vcp_vol_renderer.c @@ -9,13 +9,6 @@ */ #include -#include -#include -#include -#include - -#include "nvs_flash.h" -#include "esp_system.h" #include "tmap_bmr.h" diff --git a/examples/bluetooth/esp_ble_audio/tmap/bmr/sdkconfig.defaults b/examples/bluetooth/esp_ble_audio/tmap/bmr/sdkconfig.defaults index 0d9fb343304..5febc040e4e 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/bmr/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/tmap/bmr/sdkconfig.defaults @@ -3,14 +3,16 @@ # CONFIG_BT_ENABLED=y -CONFIG_BT_BLUEDROID_ENABLED=n -CONFIG_BT_NIMBLE_ENABLED=y -CONFIG_BT_NIMBLE_EXT_ADV=y -CONFIG_BT_NIMBLE_NVS_PERSIST=y -CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 -CONFIG_BT_NIMBLE_MAX_CCCDS=30 -CONFIG_BT_NIMBLE_ISO=y -CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y +CONFIG_BT_NIMBLE_ENABLED=n +CONFIG_BT_BLUEDROID_ENABLED=y +CONFIG_BT_CLASSIC_ENABLED=n +CONFIG_BT_CONTROLLER_ENABLED=y +CONFIG_BT_BLE_ENABLED=y +CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y +CONFIG_BT_ACL_CONNECTIONS=1 +CONFIG_BT_GATT_MAX_SR_PROFILES=12 +CONFIG_BT_GATTC_NOTIF_REG_MAX=30 +CONFIG_BT_BLE_FEAT_ISO_EN=y CONFIG_BT_ISO_MAX_CHAN=2 @@ -19,6 +21,8 @@ CONFIG_BT_CAP_ACCEPTOR=y CONFIG_BT_BAP_SCAN_DELEGATOR=y CONFIG_BT_BAP_BROADCAST_SINK=y CONFIG_BT_VCP_VOL_REND=y -CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=30 +CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=60 + +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y CONFIG_FREERTOS_HZ=1000 diff --git a/examples/bluetooth/esp_ble_audio/tmap/bmr/sdkconfig.defaults.nimble b/examples/bluetooth/esp_ble_audio/tmap/bmr/sdkconfig.defaults.nimble new file mode 100644 index 00000000000..ea3029ef03f --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/tmap/bmr/sdkconfig.defaults.nimble @@ -0,0 +1,12 @@ +# NimBLE host overlay for this example. +# Use with: +# idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.$IDF_TARGET;sdkconfig.defaults.nimble" build + +CONFIG_BT_BLUEDROID_ENABLED=n +CONFIG_BT_NIMBLE_ENABLED=y +CONFIG_BT_NIMBLE_EXT_ADV=y +CONFIG_BT_NIMBLE_NVS_PERSIST=y +CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 +CONFIG_BT_NIMBLE_MAX_CCCDS=20 +CONFIG_BT_NIMBLE_ISO=y +CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y diff --git a/examples/bluetooth/esp_ble_audio/tmap/bms/README.md b/examples/bluetooth/esp_ble_audio/tmap/bms/README.md index 1efa6a495ef..c2e9bc1675f 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/bms/README.md +++ b/examples/bluetooth/esp_ble_audio/tmap/bms/README.md @@ -9,7 +9,7 @@ This example implements the **Telephony and Media Audio Profile (TMAP) Broadcast Media Sender (BMS)** role. The device acts as a non-connectable BAP Broadcast Source: extended advertising carries the TMAS service UUID with the BMS role flag, the Broadcast Audio Announcement service with a 24-bit broadcast ID (`0x123456`), and the complete local name (`"TMAP Broadcast Source"`); periodic advertising carries the encoded BASE. -The implementation runs on the **NimBLE** host stack and uses the ESP-BLE-AUDIO library for the TMAP role registration and the CAP initiator broadcast pieces. A single subgroup with one stream is configured from the LC3 `48_2_1` broadcast preset (front-left location, media context). After the broadcast source is created and the BIG is started, a periodic TX scheduler (`example_audio_tx_scheduler_*`) drives ISO SDU transmission at the stream's QoS interval, and a sent callback tracks packet counts and drift. +The implementation uses the ESP-BLE-AUDIO library for the TMAP role registration and the CAP initiator broadcast pieces. A single subgroup with one stream is configured from the LC3 `48_2_1` broadcast preset (front-left location, media context). After the broadcast source is created and the BIG is started, a periodic TX scheduler (`example_audio_tx_scheduler_*`) drives ISO SDU transmission at the stream's QoS interval, and a sent callback tracks packet counts and drift. ## Requirements @@ -26,15 +26,28 @@ No build-time options — runtime defaults are baked into source. ### Security & Pairing -NimBLE host security is inherited from `../../common_components/example_init/ble_audio_example_init.c`: LE Secure Connections with bonding enabled, no MITM, **Just-Works** pairing (`BLE_SM_IO_CAP_NO_IO`). The BMS broadcast itself is non-connectable and unencrypted, so these settings only apply if a peer ever opens an ATT connection. +Security is inherited from `../../common_components/example_init/ble_audio_example_init.c`: LE Secure Connections with bonding enabled, no MITM, **Just-Works** pairing (`BLE_SM_IO_CAP_NO_IO`). The BMS broadcast itself is non-connectable and unencrypted, so these settings only apply if a peer ever opens an ATT connection. ## Build & Flash +The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`. + +### Bluedroid host (default) + ```bash idf.py set-target esp32h4 idf.py -p PORT flash monitor ``` +### NimBLE host + +```bash +idf.py set-target esp32h4 +idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor +``` + +For `esp32s31`, replace the chip overlay accordingly. + (Exit serial monitor with `Ctrl-]`.) ## Example Flow diff --git a/examples/bluetooth/esp_ble_audio/tmap/bms/main/CMakeLists.txt b/examples/bluetooth/esp_ble_audio/tmap/bms/main/CMakeLists.txt index 176dda4b21f..bce5fcc9ab2 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/bms/main/CMakeLists.txt +++ b/examples/bluetooth/esp_ble_audio/tmap/bms/main/CMakeLists.txt @@ -1,5 +1,12 @@ set(srcs "cap_initiator.c" "main.c") +if(CONFIG_BT_BLUEDROID_ENABLED) + list(APPEND srcs "bluedroid/adv.c") +else() + list(APPEND srcs "nimble/adv.c") +endif() + idf_component_register(SRCS "${srcs}" + INCLUDE_DIRS "." REQUIRES bt nvs_flash) diff --git a/examples/bluetooth/esp_ble_audio/tmap/bms/main/bluedroid/adv.c b/examples/bluetooth/esp_ble_audio/tmap/bms/main/bluedroid/adv.c new file mode 100644 index 00000000000..533eb467c1a --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/tmap/bms/main/bluedroid/adv.c @@ -0,0 +1,146 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#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 "tmap_bms.h" + +static SemaphoreHandle_t adv_sem; + +/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */ +static esp_bt_status_t adv_op_status; + +#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, adv_sem, portMAX_DELAY, adv_op_status) + +static esp_ble_gap_ext_adv_params_t ext_adv_params = { + .type = ESP_BLE_GAP_SET_EXT_ADV_PROP_NONCONN_NONSCANNABLE_UNDIRECTED, + .interval_min = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS), + .interval_max = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS), + .channel_map = ADV_CHNL_ALL, + .filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, + .primary_phy = ESP_BLE_GAP_PHY_1M, + .max_skip = 0, + .secondary_phy = ESP_BLE_GAP_PHY_2M, + .sid = ADV_SID, + .scan_req_notif = false, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .tx_power = ADV_TX_POWER, +}; + +static esp_ble_gap_periodic_adv_params_t periodic_adv_params = { + .interval_min = ESP_BLE_GAP_PERIODIC_ADV_ITVL_MS(PER_ADV_INTERVAL_MS), + .interval_max = ESP_BLE_GAP_PERIODIC_ADV_ITVL_MS(PER_ADV_INTERVAL_MS), + .properties = 0, +}; + +static esp_ble_gap_ext_adv_t ext_adv_inst[1] = { + [0] = { ADV_HANDLE, 0, 0 }, +}; + +static void gap_event_handler(esp_gap_ble_cb_event_t event, + esp_ble_gap_cb_param_t *param) +{ + switch (event) { + case ESP_GAP_BLE_EXT_ADV_SET_PARAMS_COMPLETE_EVT: + adv_op_status = param->ext_adv_set_params.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_EXT_ADV_DATA_SET_COMPLETE_EVT: + adv_op_status = param->ext_adv_data_set.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_EXT_ADV_START_COMPLETE_EVT: + adv_op_status = param->ext_adv_start.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_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; + default: + break; + } +} + +int app_host_init(void) +{ + esp_err_t err; + + adv_sem = xSemaphoreCreateBinary(); + if (adv_sem == NULL) { + ESP_LOGE(TAG, "Failed to create adv semaphore"); + return -1; + } + + err = esp_ble_gap_register_callback(gap_event_handler); + if (err) { + ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err); + vSemaphoreDelete(adv_sem); + return err; + } + + return 0; +} + +int set_device_name(void) +{ + return esp_ble_gap_set_device_name(LOCAL_DEVICE_NAME); +} + +int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len, + const uint8_t *per_data, uint8_t per_len) +{ + WAIT_API(esp_ble_gap_ext_adv_set_params(ADV_HANDLE, &ext_adv_params)); + WAIT_API(esp_ble_gap_config_ext_adv_data_raw(ADV_HANDLE, ext_len, ext_data)); + WAIT_API(esp_ble_gap_periodic_adv_set_params(ADV_HANDLE, &periodic_adv_params)); +#if CONFIG_BT_BLE_FEAT_PERIODIC_ADV_ENH + WAIT_API(esp_ble_gap_config_periodic_adv_data_raw(ADV_HANDLE, per_len, per_data, false)); + WAIT_API(esp_ble_gap_periodic_adv_start(ADV_HANDLE, true)); +#else + WAIT_API(esp_ble_gap_config_periodic_adv_data_raw(ADV_HANDLE, per_len, per_data)); + WAIT_API(esp_ble_gap_periodic_adv_start(ADV_HANDLE)); +#endif + WAIT_API(esp_ble_gap_ext_adv_start(1, ext_adv_inst)); + + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); + return 0; +} + +int ext_adv_stop(void) +{ + static const uint8_t ext_adv_stop_inst[1] = { ADV_HANDLE }; + + WAIT_API(esp_ble_gap_periodic_adv_stop(ADV_HANDLE)); + WAIT_API(esp_ble_gap_ext_adv_stop(1, ext_adv_stop_inst)); + + ESP_LOGI(TAG, "Advertising stopped (handle %u)", ADV_HANDLE); + return 0; +} diff --git a/examples/bluetooth/esp_ble_audio/tmap/bms/main/cap_initiator.c b/examples/bluetooth/esp_ble_audio/tmap/bms/main/cap_initiator.c index d39c47626ef..15efc516350 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/bms/main/cap_initiator.c +++ b/examples/bluetooth/esp_ble_audio/tmap/bms/main/cap_initiator.c @@ -6,27 +6,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include #include -#include #include - -#include "esp_timer.h" -#include "esp_random.h" +#include +#include #include "tmap_bms.h" -#define ADV_HANDLE 0x00 -#define ADV_SID 0 -#define ADV_TX_POWER 127 -#define ADV_ADDRESS BLE_OWN_ADDR_PUBLIC -#define ADV_PRIMARY_PHY BLE_HCI_LE_PHY_1M -#define ADV_SECONDARY_PHY BLE_HCI_LE_PHY_2M -#define ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(200) - -#define PER_ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(100) - -#define LOCAL_DEVICE_NAME "TMAP Broadcast Source" #define LOCAL_BROADCAST_ID 0x123456 static esp_ble_audio_cap_initiator_broadcast_subgroup_param_t subgroup_param; @@ -233,144 +219,6 @@ static uint8_t *per_adv_data_get(uint8_t *data_len) return data; } -static int ext_adv_start(void) -{ - struct ble_gap_periodic_adv_params per_params = {0}; - struct ble_gap_ext_adv_params ext_params = {0}; - struct os_mbuf *data = NULL; - uint8_t *ext_data = NULL; - uint8_t *per_data = NULL; - uint8_t data_len = 0; - int err; - - ext_params.connectable = 0; - ext_params.scannable = 0; - ext_params.legacy_pdu = 0; - ext_params.own_addr_type = ADV_ADDRESS; - ext_params.primary_phy = ADV_PRIMARY_PHY; - ext_params.secondary_phy = ADV_SECONDARY_PHY; - ext_params.tx_power = ADV_TX_POWER; - ext_params.sid = ADV_SID; - ext_params.itvl_min = ADV_INTERVAL; - ext_params.itvl_max = ADV_INTERVAL; - - err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL, - example_audio_gap_event_cb, NULL); - if (err) { - ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err); - goto end; - } - - ext_data = ext_adv_data_get(&data_len); - if (ext_data == NULL) { - err = -ENOMEM; - goto end; - } - - data = os_msys_get_pkthdr(data_len, 0); - if (data == NULL) { - ESP_LOGE(TAG, "Failed to get ext adv mbuf"); - err = -ENOMEM; - goto end; - } - - err = os_mbuf_append(data, ext_data, data_len); - if (err) { - ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err); - os_mbuf_free_chain(data); - goto end; - } - - err = ble_gap_ext_adv_set_data(ADV_HANDLE, data); - if (err) { - ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err); - goto end; - } - - per_params.include_tx_power = 0; - per_params.itvl_min = PER_ADV_INTERVAL; - per_params.itvl_max = PER_ADV_INTERVAL; - - err = ble_gap_periodic_adv_configure(ADV_HANDLE, &per_params); - if (err) { - ESP_LOGE(TAG, "Failed to configure per adv params, err %d", err); - goto end; - } - - per_data = per_adv_data_get(&data_len); - if (per_data == NULL) { - err = -ENOMEM; - goto end; - } - - data = os_msys_get_pkthdr(data_len, 0); - if (data == NULL) { - ESP_LOGE(TAG, "Failed to get per adv mbuf"); - err = -ENOMEM; - goto end; - } - - err = os_mbuf_append(data, per_data, data_len); - if (err) { - ESP_LOGE(TAG, "Failed to append per adv data, err %d", err); - os_mbuf_free_chain(data); - goto end; - } - - err = ble_gap_periodic_adv_set_data(ADV_HANDLE, data); - if (err) { - ESP_LOGE(TAG, "Failed to set per adv data, err %d", err); - goto end; - } - - err = ble_gap_periodic_adv_start(ADV_HANDLE); - if (err) { - ESP_LOGE(TAG, "Failed to start per advertising, err %d", err); - goto end; - } - - err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0); - if (err) { - ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err); - goto end; - } - - ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); - -end: - if (ext_data) { - free(ext_data); - } - if (per_data) { - free(per_data); - } - return err; -} - -static int ext_adv_stop(void) -{ - int ret = 0; - int err; - - err = ble_gap_periodic_adv_stop(ADV_HANDLE); - if (err) { - ESP_LOGE(TAG, "Failed to stop per advertising, err %d", err); - ret = err; - } - - err = ble_gap_ext_adv_stop(ADV_HANDLE); - if (err) { - ESP_LOGE(TAG, "Failed to stop ext advertising, err %d", err); - ret = err; - } - - if (ret == 0) { - ESP_LOGI(TAG, "Advertising stopped (handle %u)", ADV_HANDLE); - } - - return ret; -} - int cap_initiator_setup(void) { esp_ble_audio_bap_broadcast_adv_info_t info = { @@ -378,6 +226,11 @@ int cap_initiator_setup(void) }; bool adv_started = false; bool adv_added = false; + /* BASE (per_adv_data_get) needs broadcast_source to exist — build after create. */ + uint8_t *ext_data = NULL; + uint8_t *per_data = NULL; + uint8_t ext_len = 0; + uint8_t per_len = 0; int err; stream_params.stream = &broadcast_stream; @@ -402,7 +255,19 @@ int cap_initiator_setup(void) return err; } - err = ext_adv_start(); + ext_data = ext_adv_data_get(&ext_len); + if (ext_data == NULL) { + err = -ENOMEM; + goto end; + } + + per_data = per_adv_data_get(&per_len); + if (per_data == NULL) { + err = -ENOMEM; + goto end; + } + + err = ext_adv_start(ext_data, ext_len, per_data, per_len); if (err) { goto end; } @@ -420,18 +285,25 @@ int cap_initiator_setup(void) err = esp_ble_audio_cap_initiator_broadcast_audio_start(broadcast_source, ADV_HANDLE); if (err) { ESP_LOGE(TAG, "Failed to start broadcast source, err %d", err); - goto end; } - ESP_LOGI(TAG, "CAP initiator setup"); - - return 0; - end: + if (ext_data != NULL) { + free(ext_data); + } + if (per_data != NULL) { + free(per_data); + } + + if (err == 0) { + ESP_LOGI(TAG, "CAP initiator setup"); + return 0; + } + if (adv_added) { - err = esp_ble_audio_bap_broadcast_adv_delete(&info); - if (err) { - ESP_LOGE(TAG, "Failed to delete adv for broadcast source, err %d", err); + esp_err_t e = esp_ble_audio_bap_broadcast_adv_delete(&info); + if (e) { + ESP_LOGE(TAG, "Failed to delete adv for broadcast source, err %d", e); } } diff --git a/examples/bluetooth/esp_ble_audio/tmap/bms/main/main.c b/examples/bluetooth/esp_ble_audio/tmap/bms/main/main.c index 96253717642..8a899d349b1 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/bms/main/main.c +++ b/examples/bluetooth/esp_ble_audio/tmap/bms/main/main.c @@ -6,13 +6,8 @@ */ #include -#include -#include -#include -#include #include "nvs_flash.h" -#include "esp_system.h" #include "tmap_bms.h" @@ -38,6 +33,12 @@ void app_main(void) return; } + err = app_host_init(); + if (err) { + ESP_LOGE(TAG, "Failed to init host, err %d", err); + return; + } + err = esp_ble_audio_common_init(&init_info); if (err) { ESP_LOGE(TAG, "Failed to initialize audio, err %d", err); @@ -61,5 +62,11 @@ void app_main(void) return; } + err = set_device_name(); + if (err) { + ESP_LOGE(TAG, "Failed to set device name, err %d", err); + return; + } + cap_initiator_setup(); } diff --git a/examples/bluetooth/esp_ble_audio/tmap/bms/main/nimble/adv.c b/examples/bluetooth/esp_ble_audio/tmap/bms/main/nimble/adv.c new file mode 100644 index 00000000000..c481601ca0d --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/tmap/bms/main/nimble/adv.c @@ -0,0 +1,147 @@ +/* + * 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 "services/gap/ble_svc_gap.h" + +#include "os/os_mbuf.h" + +#include "tmap_bms.h" + +/* Non-connectable broadcaster: no GAP events for the application. */ +static int gap_event_cb(struct ble_gap_event *event, void *arg) +{ + return 0; +} + +int app_host_init(void) +{ + return 0; +} + +int set_device_name(void) +{ + return ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME); +} + +int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len, + const uint8_t *per_data, uint8_t per_len) +{ + struct ble_gap_periodic_adv_params per_params = {0}; + struct ble_gap_ext_adv_params ext_params = {0}; + struct os_mbuf *data = NULL; + int err; + + ext_params.connectable = 0; + ext_params.scannable = 0; + ext_params.legacy_pdu = 0; + ext_params.own_addr_type = BLE_OWN_ADDR_PUBLIC; + ext_params.primary_phy = BLE_HCI_LE_PHY_1M; + ext_params.secondary_phy = BLE_HCI_LE_PHY_2M; + ext_params.tx_power = ADV_TX_POWER; + ext_params.sid = ADV_SID; + ext_params.itvl_min = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS); + ext_params.itvl_max = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS); + + err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL, + gap_event_cb, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err); + return err; + } + + data = os_msys_get_pkthdr(ext_len, 0); + if (data == NULL) { + ESP_LOGE(TAG, "Failed to get ext adv mbuf"); + return -1; + } + + err = os_mbuf_append(data, ext_data, ext_len); + if (err) { + ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err); + os_mbuf_free_chain(data); + return err; + } + + err = ble_gap_ext_adv_set_data(ADV_HANDLE, data); + if (err) { + ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err); + return err; + } + + per_params.include_tx_power = 0; + per_params.itvl_min = BLE_GAP_PERIODIC_ITVL_MS(PER_ADV_INTERVAL_MS); + per_params.itvl_max = BLE_GAP_PERIODIC_ITVL_MS(PER_ADV_INTERVAL_MS); + + err = ble_gap_periodic_adv_configure(ADV_HANDLE, &per_params); + if (err) { + ESP_LOGE(TAG, "Failed to configure per adv params, err %d", err); + return err; + } + + data = os_msys_get_pkthdr(per_len, 0); + if (data == NULL) { + ESP_LOGE(TAG, "Failed to get per adv mbuf"); + return -1; + } + + err = os_mbuf_append(data, per_data, per_len); + if (err) { + ESP_LOGE(TAG, "Failed to append per adv data, err %d", err); + os_mbuf_free_chain(data); + return err; + } + + err = ble_gap_periodic_adv_set_data(ADV_HANDLE, data); + if (err) { + ESP_LOGE(TAG, "Failed to set per adv data, err %d", err); + return err; + } + + err = ble_gap_periodic_adv_start(ADV_HANDLE); + if (err) { + ESP_LOGE(TAG, "Failed to start per advertising, err %d", err); + return err; + } + + err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0); + if (err) { + ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); + return 0; +} + +int ext_adv_stop(void) +{ + int ret = 0; + int err; + + err = ble_gap_periodic_adv_stop(ADV_HANDLE); + if (err) { + ESP_LOGE(TAG, "Failed to stop per advertising, err %d", err); + ret = err; + } + + err = ble_gap_ext_adv_stop(ADV_HANDLE); + if (err) { + ESP_LOGE(TAG, "Failed to stop ext advertising, err %d", err); + ret = err; + } + + if (ret == 0) { + ESP_LOGI(TAG, "Advertising stopped (handle %u)", ADV_HANDLE); + } + + return ret; +} diff --git a/examples/bluetooth/esp_ble_audio/tmap/bms/main/tmap_bms.h b/examples/bluetooth/esp_ble_audio/tmap/bms/main/tmap_bms.h index 127ecf61c23..f21e2a5141a 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/bms/main/tmap_bms.h +++ b/examples/bluetooth/esp_ble_audio/tmap/bms/main/tmap_bms.h @@ -5,12 +5,14 @@ * SPDX-License-Identifier: Apache-2.0 */ +#pragma once + +#include + #include "esp_log.h" #include "sdkconfig.h" -#include "host/ble_hs.h" - #include "esp_ble_audio_cap_api.h" #include "esp_ble_audio_tmap_api.h" #include "esp_ble_audio_bap_lc3_preset_defs.h" @@ -20,6 +22,23 @@ #define TAG "TMAP_BMS" +#define LOCAL_DEVICE_NAME "TMAP Broadcast Source" + +#define ADV_HANDLE 0 +#define ADV_SID 0 +#define ADV_TX_POWER 127 +#define ADV_INTERVAL_MS 200 +#define PER_ADV_INTERVAL_MS 100 + +int app_host_init(void); + +int set_device_name(void); + +int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len, + const uint8_t *per_data, uint8_t per_len); + +int ext_adv_stop(void); + int cap_initiator_setup(void); int cap_initiator_update(void); diff --git a/examples/bluetooth/esp_ble_audio/tmap/bms/sdkconfig.defaults b/examples/bluetooth/esp_ble_audio/tmap/bms/sdkconfig.defaults index 30d10006978..31c481d6a80 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/bms/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/tmap/bms/sdkconfig.defaults @@ -3,13 +3,14 @@ # CONFIG_BT_ENABLED=y -CONFIG_BT_BLUEDROID_ENABLED=n -CONFIG_BT_NIMBLE_ENABLED=y -CONFIG_BT_NIMBLE_EXT_ADV=y -CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 -CONFIG_BT_NIMBLE_MAX_CCCDS=30 -CONFIG_BT_NIMBLE_ISO=y -CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y +CONFIG_BT_NIMBLE_ENABLED=n +CONFIG_BT_BLUEDROID_ENABLED=y +CONFIG_BT_CLASSIC_ENABLED=n +CONFIG_BT_CONTROLLER_ENABLED=y +CONFIG_BT_BLE_ENABLED=y +CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y +CONFIG_BT_ACL_CONNECTIONS=1 +CONFIG_BT_BLE_FEAT_ISO_EN=y CONFIG_BT_ISO_MAX_CHAN=2 @@ -17,4 +18,6 @@ CONFIG_BT_TMAP=y CONFIG_BT_CAP_INITIATOR=y CONFIG_BT_BAP_BROADCAST_SOURCE=y +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y + CONFIG_FREERTOS_HZ=1000 diff --git a/examples/bluetooth/esp_ble_audio/tmap/bms/sdkconfig.defaults.nimble b/examples/bluetooth/esp_ble_audio/tmap/bms/sdkconfig.defaults.nimble new file mode 100644 index 00000000000..b06a72b686f --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/tmap/bms/sdkconfig.defaults.nimble @@ -0,0 +1,10 @@ +# 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_MAX_CONNECTIONS=1 +CONFIG_BT_NIMBLE_ISO=y +CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y diff --git a/examples/bluetooth/esp_ble_audio/tmap/central/README.md b/examples/bluetooth/esp_ble_audio/tmap/central/README.md index d57df2ecf95..975acfcd944 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/central/README.md +++ b/examples/bluetooth/esp_ble_audio/tmap/central/README.md @@ -9,7 +9,7 @@ This example takes the **TMAP Call Gateway (CG)** and **Unicast Media Sender (UMS)** roles, registered together via `esp_ble_audio_tmap_register(ESP_BLE_AUDIO_TMAP_ROLE_CG | ESP_BLE_AUDIO_TMAP_ROLE_UMS)`. It scans for connectable extended advertising that carries TMAS service data with the **UMR** role bit set, connects to the first match, pairs, exchanges MTU, and then drives TMAP and VCP discovery before bringing up unicast audio. -The host stack is NimBLE. The example uses the ESP-BLE-AUDIO library pieces for: CAP initiator with the BAP Unicast Client (LC3 preset 48_2_1, sink direction, FRONT_LEFT, MEDIA context), VCP Volume Controller, MCP server backed by the media proxy player, and CCP server registering a single GTBS bearer (`Generic TBS`, UCI `un000`, `tel,wechat` URI schemes, 5G technology). A periodic TX scheduler in the ISO task feeds dummy ISO SDUs filled with the sequence number. Device name is set to `TMAP Central`. +The example uses the ESP-BLE-AUDIO library pieces for: CAP initiator with the BAP Unicast Client (LC3 preset 48_2_1, sink direction, FRONT_LEFT, MEDIA context), VCP Volume Controller, MCP server backed by the media proxy player, and CCP server registering a single GTBS bearer (`Generic TBS`, UCI `un000`, `tel,wechat` URI schemes, 5G technology). A periodic TX scheduler in the ISO task feeds dummy ISO SDUs filled with the sequence number. Device name is set to `TMAP Central`. ## Requirements @@ -30,11 +30,24 @@ Just-Works pairing (LE Secure Connections, no MITM, no I/O capability) with bond ## Build & Flash +The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`. + +### Bluedroid host (default) + ```bash idf.py set-target esp32h4 idf.py -p PORT flash monitor ``` +### NimBLE host + +```bash +idf.py set-target esp32h4 +idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor +``` + +For `esp32s31`, replace the chip overlay accordingly. + (Exit serial monitor with `Ctrl-]`.) ## Example Flow 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 f8c704047c9..e851db1810b 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/central/main/CMakeLists.txt +++ b/examples/bluetooth/esp_ble_audio/tmap/central/main/CMakeLists.txt @@ -4,5 +4,12 @@ set(srcs "cap_initiator.c" "vcp_vol_ctlr.c" "main.c") +if(CONFIG_BT_BLUEDROID_ENABLED) + list(APPEND srcs "bluedroid/central.c") +else() + list(APPEND srcs "nimble/central.c") +endif() + idf_component_register(SRCS "${srcs}" + INCLUDE_DIRS "." REQUIRES bt nvs_flash) 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 new file mode 100644 index 00000000000..2abf8deba6a --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/tmap/central/main/bluedroid/central.c @@ -0,0 +1,193 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "esp_log.h" +#include "esp_err.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +#include "esp_gap_ble_api.h" +#include "esp_gattc_api.h" + +#include "esp_ble_audio_common_api.h" + +#include "tmap_central.h" + +static SemaphoreHandle_t scan_sem; + +/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */ +static esp_bt_status_t scan_op_status; + +#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, scan_sem, portMAX_DELAY, scan_op_status) + +/* Cached peer address. Bluedroid's pairing and disconnect APIs key off + * bd_addr rather than conn_handle, so we stash the addr at conn_create + * time and reuse it in pairing_start / security_failed_recover. */ +static esp_bd_addr_t peer_bda; + +static esp_ble_ext_scan_params_t ext_scan_params = { + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE, + .cfg_mask = ESP_BLE_GAP_EXT_SCAN_CFG_UNCODE_MASK, + .uncoded_cfg = { + .scan_type = BLE_SCAN_TYPE_PASSIVE, + .scan_interval = SCAN_INTERVAL, + .scan_window = SCAN_WINDOW, + }, +}; + +static void gap_event_handler(esp_gap_ble_cb_event_t event, + esp_ble_gap_cb_param_t *param) +{ + switch (event) { + case ESP_GAP_BLE_SET_EXT_SCAN_PARAMS_COMPLETE_EVT: + scan_op_status = param->set_ext_scan_params.status; + xSemaphoreGive(scan_sem); + break; + case ESP_GAP_BLE_EXT_SCAN_START_COMPLETE_EVT: + scan_op_status = param->ext_scan_start.status; + xSemaphoreGive(scan_sem); + break; + case ESP_GAP_BLE_EXT_SCAN_STOP_COMPLETE_EVT: + scan_op_status = param->ext_scan_stop.status; + xSemaphoreGive(scan_sem); + break; + + /* SMP request handling for Just Works pairing (IO_CAP=NONE). NC_REQ + * still fires under LE Secure Connections — auto-accept. We never get + * SEC_REQ here because the central initiates via esp_ble_set_encryption. */ + case ESP_GAP_BLE_NC_REQ_EVT: + esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, true); + break; + + /* AUTH_CMPL has no BTA channel — app must forward it. EXT_ADV_REPORT + * is forwarded by adapter's BTA path, don't re-post here. */ + case ESP_GAP_BLE_AUTH_CMPL_EVT: + esp_ble_audio_gap_app_post_event(event, param); + break; + + default: + break; + } +} + +int app_host_init(void) +{ + esp_err_t err; + + scan_sem = xSemaphoreCreateBinary(); + if (scan_sem == NULL) { + ESP_LOGE(TAG, "Failed to create scan semaphore"); + return -1; + } + + err = esp_ble_gap_register_callback(gap_event_handler); + if (err) { + ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err); + vSemaphoreDelete(scan_sem); + return err; + } + + return 0; +} + +int set_device_name(void) +{ + return esp_ble_gap_set_device_name(LOCAL_DEVICE_NAME); +} + +int ext_scan_start(void) +{ + WAIT_API(esp_ble_gap_set_ext_scan_params(&ext_scan_params)); + WAIT_API(esp_ble_gap_start_ext_scan(0, 0)); + + ESP_LOGI(TAG, "Scanning for peripheral..."); + return 0; +} + +int ext_scan_stop(void) +{ + WAIT_API(esp_ble_gap_stop_ext_scan()); + return 0; +} + +int conn_create(uint8_t addr_type, const uint8_t addr[6]) +{ + /* Values shared with NimBLE side via tmap_central.h for behavioral parity. + * Without prefer_ext_connect_params_set, L2CAP falls back to defaults + * and logs "No extend connection parameters set". */ + const esp_ble_gap_conn_params_t conn_params = { + .scan_interval = INIT_SCAN_INTERVAL, + .scan_window = INIT_SCAN_WINDOW, + .interval_min = CONN_INTERVAL, + .interval_max = CONN_INTERVAL, + .latency = CONN_LATENCY, + .supervision_timeout = CONN_TIMEOUT, + .min_ce_len = CONN_MIN_CE_LEN, + .max_ce_len = CONN_MAX_CE_LEN, + }; + esp_gatt_if_t gattc_if; + esp_err_t err; + + memcpy(peer_bda, addr, sizeof(peer_bda)); + + err = esp_ble_gap_prefer_ext_connect_params_set( + peer_bda, ESP_BLE_GAP_PHY_1M_PREF_MASK, &conn_params, NULL, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to set ext conn params, err %d", err); + return err; + } + + /* Use the audio engine's GATTC if so OPEN/CONNECT events route back to + * the engine. aux_open initiates an ACL against an extended advertiser. + * + * engine returns ESP_GATT_IF_NONE (0xFF) when GATTC is not yet registered; + * feeding that into aux_open silently no-ops in BTC and no acl_connect + * event ever fires, leaving the caller in a no-scan / no-conn dead state. */ + gattc_if = esp_ble_audio_bluedroid_get_gattc_if(); + if (gattc_if == ESP_GATT_IF_NONE) { + ESP_LOGE(TAG, "GATTC not registered"); + return ESP_ERR_INVALID_STATE; + } + + return esp_ble_gattc_aux_open(gattc_if, peer_bda, + (esp_ble_addr_type_t)addr_type, true); +} + +int pairing_start(uint16_t conn_handle) +{ + (void)conn_handle; + return esp_ble_set_encryption(peer_bda, ESP_BLE_SEC_ENCRYPT_NO_MITM); +} + +int exchange_mtu(uint16_t conn_handle) +{ + (void)conn_handle; + /* The Bluedroid GATTC adapter exchanges MTU automatically when aux_open + * brings up the ACL — the MTU_UPDATED event arrives without an explicit + * kick-off. NimBLE has no such auto-exchange, so this wrapper is a no-op + * on bluedroid and a real ble_gattc_exchange_mtu on nimble. */ + return 0; +} + +void security_failed_recover(uint16_t conn_handle, uint8_t status) +{ + (void)conn_handle; + + /* Asymmetric bond state: we still hold an LTK for this peer but it + * cleared its side, so encrypt-with-cached-key times out. Drop the bond + * and tear down the link; the next reconnect runs fresh pairing. */ + ESP_LOGE(TAG, "Security change failed, status %u, clearing local bond and reconnecting", status); + + esp_ble_remove_bond_device(peer_bda); + esp_ble_gap_disconnect(peer_bda); +} 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 5330b91a3f1..3bb47dc307d 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 @@ -11,8 +11,6 @@ #include #include -#include "esp_timer.h" - #include "esp_ble_audio_bap_lc3_preset_defs.h" #include "tmap_central.h" diff --git a/examples/bluetooth/esp_ble_audio/tmap/central/main/ccp_server.c b/examples/bluetooth/esp_ble_audio/tmap/central/main/ccp_server.c index d396b5bc7d9..c3174caa1f9 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/central/main/ccp_server.c +++ b/examples/bluetooth/esp_ble_audio/tmap/central/main/ccp_server.c @@ -7,8 +7,6 @@ */ #include -#include -#include #include "tmap_central.h" 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 4d1233b5d57..40820630e56 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/central/main/main.c +++ b/examples/bluetooth/esp_ble_audio/tmap/central/main/main.c @@ -7,36 +7,14 @@ */ #include -#include #include #include "nvs_flash.h" -#include "esp_system.h" -#include "esp_timer.h" - -#include "host/ble_hs.h" -#include "services/gap/ble_svc_gap.h" #include "tmap_central.h" -#include "ble_audio_example_init.h" -#include "ble_audio_example_utils.h" - -#define SCAN_INTERVAL 160 /* 100ms */ -#define SCAN_WINDOW 160 /* 100ms */ - -#define INIT_SCAN_INTERVAL 16 /* 10ms */ -#define INIT_SCAN_WINDOW 16 /* 10ms */ -#define CONN_INTERVAL 64 /* 64 * 1.25 = 80ms */ -#define CONN_LATENCY 0 -#define CONN_TIMEOUT 500 /* 500 * 10ms = 5s */ -#define CONN_MAX_CE_LEN 0xFFFF -#define CONN_MIN_CE_LEN 0xFFFF -#define CONN_DURATION 10000 /* 10s */ - static uint16_t default_conn_handle = CONN_HANDLE_INIT; static bool disc_completed; -static bool disc_cancelled; static bool mtu_exchanged; uint16_t default_conn_handle_get(void) @@ -68,80 +46,10 @@ static esp_ble_audio_tmap_cb_t tmap_callbacks = { .discovery_complete = tmap_discovery_complete, }; -static void ext_scan_start(void) -{ - struct ble_gap_disc_params params = {0}; - uint8_t own_addr_type; - int err; - - err = ble_hs_id_infer_auto(0, &own_addr_type); - if (err) { - ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err); - return; - } - - params.passive = 1; - params.itvl = SCAN_INTERVAL; - params.window = SCAN_WINDOW; - - err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms, - example_audio_gap_event_cb, NULL); - if (err) { - ESP_LOGE(TAG, "Failed to start scanning, err %d", err); - return; - } - - ESP_LOGI(TAG, "Scanning for peripheral..."); -} - -static int conn_create(ble_addr_t *dst) -{ - struct ble_gap_conn_params params = {0}; - uint8_t own_addr_type = 0; - int err; - - err = ble_hs_id_infer_auto(0, &own_addr_type); - if (err) { - ESP_LOGE(TAG, "Failed to determine address type, err %d", err); - return err; - } - - err = ble_gap_disc_cancel(); - if (err) { - ESP_LOGE(TAG, "Failed to stop scanning, err %d", err); - return err; - } - - disc_cancelled = true; - - params.scan_itvl = INIT_SCAN_INTERVAL; - params.scan_window = INIT_SCAN_WINDOW; - params.itvl_min = CONN_INTERVAL; - params.itvl_max = CONN_INTERVAL; - params.latency = CONN_LATENCY; - params.supervision_timeout = CONN_TIMEOUT; - params.max_ce_len = CONN_MAX_CE_LEN; - params.min_ce_len = CONN_MIN_CE_LEN; - - return ble_gap_connect(own_addr_type, dst, CONN_DURATION, ¶ms, - example_audio_gap_event_cb, NULL); -} - -static int pairing_start(uint16_t conn_handle) -{ - return ble_gap_security_initiate(conn_handle); -} - -static int exchange_mtu(uint16_t conn_handle) -{ - return ble_gattc_exchange_mtu(conn_handle, NULL, NULL); -} - static bool check_and_connect(uint8_t type, const uint8_t *data, uint8_t data_len, void *user_data) { esp_ble_audio_gap_app_event_t *event; - ble_addr_t dst = {0}; uint16_t tmap_role; uint16_t uuid_val; int err; @@ -179,17 +87,17 @@ static bool check_and_connect(uint8_t type, const uint8_t *data, return false; /* Stop parsing */ } - dst.type = event->ext_scan_recv.addr.type; - memcpy(dst.val, event->ext_scan_recv.addr.val, sizeof(dst.val)); + err = ext_scan_stop(); + if (err) { + ESP_LOGE(TAG, "Failed to stop scanning, err %d", err); + return false; + } - err = conn_create(&dst); + err = conn_create(event->ext_scan_recv.addr.type, + event->ext_scan_recv.addr.val); if (err) { ESP_LOGE(TAG, "Failed to create conn, err %d", err); - - if (disc_cancelled) { - disc_cancelled = false; - ext_scan_start(); - } + ext_scan_start(); } return false; /* Stop parsing */ @@ -220,9 +128,7 @@ static void acl_connect(esp_ble_audio_gap_app_event_t *event) ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x", event->acl_connect.conn_handle, event->acl_connect.role, - event->acl_connect.dst.val[5], event->acl_connect.dst.val[4], - event->acl_connect.dst.val[3], event->acl_connect.dst.val[2], - event->acl_connect.dst.val[1], event->acl_connect.dst.val[0]); + EXAMPLE_BT_ADDR_PRINT_ARGS(event->acl_connect.dst.val)); default_conn_handle = event->acl_connect.conn_handle; @@ -240,7 +146,6 @@ static void acl_disconnect(esp_ble_audio_gap_app_event_t *event) default_conn_handle = CONN_HANDLE_INIT; disc_completed = false; - disc_cancelled = false; mtu_exchanged = false; unicast_group_delete(); @@ -253,8 +158,8 @@ static void security_change(esp_ble_iso_gap_app_event_t *event) int err; if (event->security_change.status) { - example_audio_security_failed_recover(TAG, event->security_change.conn_handle, - event->security_change.status); + security_failed_recover(event->security_change.conn_handle, + event->security_change.status); return; } @@ -393,6 +298,12 @@ void app_main(void) return; } + err = app_host_init(); + if (err) { + ESP_LOGE(TAG, "Failed to init host, err %d", err); + return; + } + err = esp_ble_audio_common_init(&init_info); if (err) { ESP_LOGE(TAG, "Failed to initialize audio, err %d", err); @@ -431,7 +342,7 @@ void app_main(void) return; } - err = ble_svc_gap_device_name_set("TMAP Central"); + err = set_device_name(); if (err) { ESP_LOGE(TAG, "Failed to set device name, err %d", err); return; diff --git a/examples/bluetooth/esp_ble_audio/tmap/central/main/mcp_server.c b/examples/bluetooth/esp_ble_audio/tmap/central/main/mcp_server.c index 1cbc88de6eb..9125280a6b4 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/central/main/mcp_server.c +++ b/examples/bluetooth/esp_ble_audio/tmap/central/main/mcp_server.c @@ -5,9 +5,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include -#include - #include "tmap_central.h" int mcp_server_init(void) diff --git a/examples/bluetooth/esp_ble_audio/tmap/central/main/nimble/central.c b/examples/bluetooth/esp_ble_audio/tmap/central/main/nimble/central.c new file mode 100644 index 00000000000..157c0e04d67 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/tmap/central/main/nimble/central.c @@ -0,0 +1,149 @@ +/* + * 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 "tmap_central.h" + +/* Init/conn parameters are shared with the bluedroid wrapper via tmap_central.h. + * CONN_DURATION is NimBLE-specific (ble_gap_connect's discovery timeout). */ +#define CONN_DURATION 10000 /* 10s */ + +static int gap_event_cb(struct ble_gap_event *event, void *arg) +{ + switch (event->type) { + case BLE_GAP_EVENT_EXT_DISC: + case BLE_GAP_EVENT_CONNECT: + case BLE_GAP_EVENT_DISCONNECT: + case BLE_GAP_EVENT_ENC_CHANGE: + esp_ble_audio_gap_app_post_event(event->type, event); + break; + case BLE_GAP_EVENT_MTU: + case BLE_GAP_EVENT_NOTIFY_RX: + case BLE_GAP_EVENT_NOTIFY_TX: + case BLE_GAP_EVENT_SUBSCRIBE: + esp_ble_audio_gatt_app_post_event(event->type, event); + break; + case BLE_GAP_EVENT_REPEAT_PAIRING: { + struct ble_gap_conn_desc desc = {0}; + int rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc); + if (rc == 0) { + ble_store_util_delete_peer(&desc.peer_id_addr); + } + return BLE_GAP_REPEAT_PAIRING_RETRY; + } + default: + break; + } + + return 0; +} + +int app_host_init(void) +{ + return 0; +} + +int set_device_name(void) +{ + return ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME); +} + +int ext_scan_start(void) +{ + struct ble_gap_disc_params params = {0}; + uint8_t own_addr_type; + int err; + + err = ble_hs_id_infer_auto(0, &own_addr_type); + if (err) { + ESP_LOGE(TAG, "Failed to determine address type, err %d", err); + return err; + } + + params.passive = 1; + params.itvl = SCAN_INTERVAL; + params.window = SCAN_WINDOW; + + err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms, + gap_event_cb, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to start scanning, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Scanning for peripheral..."); + return 0; +} + +int ext_scan_stop(void) +{ + return ble_gap_disc_cancel(); +} + +int conn_create(uint8_t addr_type, const uint8_t addr[6]) +{ + struct ble_gap_conn_params params = {0}; + uint8_t own_addr_type = 0; + ble_addr_t dst = {0}; + int err; + + err = ble_hs_id_infer_auto(0, &own_addr_type); + if (err) { + ESP_LOGE(TAG, "Failed to determine address type, err %d", err); + return err; + } + + params.scan_itvl = INIT_SCAN_INTERVAL; + params.scan_window = INIT_SCAN_WINDOW; + params.itvl_min = CONN_INTERVAL; + params.itvl_max = CONN_INTERVAL; + params.latency = CONN_LATENCY; + params.supervision_timeout = CONN_TIMEOUT; + params.max_ce_len = CONN_MAX_CE_LEN; + params.min_ce_len = CONN_MIN_CE_LEN; + + dst.type = addr_type; + memcpy(dst.val, addr, sizeof(dst.val)); + + return ble_gap_connect(own_addr_type, &dst, CONN_DURATION, + ¶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/tmap/central/main/tmap_central.h b/examples/bluetooth/esp_ble_audio/tmap/central/main/tmap_central.h index 7da6ee3ccea..a8b44f463af 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,6 +6,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include #include #include @@ -27,11 +28,42 @@ #include "esp_ble_audio_vocs_api.h" #include "esp_ble_audio_tbs_api.h" +#include "ble_audio_example_init.h" #include "ble_audio_example_utils.h" #define TAG "TMAP_CEN" -#define CONN_HANDLE_INIT 0xFFFF +#define CONN_HANDLE_INIT 0xFFFF + +#define LOCAL_DEVICE_NAME "TMAP Central" + +#define SCAN_INTERVAL 160 /* 100ms */ +#define SCAN_WINDOW 160 /* 100ms */ + +/* ACL init parameters shared between bluedroid and nimble host wrappers. + * Raw HCI units (scan: 0.625ms; conn interval: 1.25ms; timeout: 10ms). */ +#define INIT_SCAN_INTERVAL 16 /* 10ms */ +#define INIT_SCAN_WINDOW 16 /* 10ms */ +#define CONN_INTERVAL 24 /* 30ms */ +#define CONN_LATENCY 0 +#define CONN_TIMEOUT 500 /* 5s */ +#define CONN_MIN_CE_LEN 0xFFFF +#define CONN_MAX_CE_LEN 0xFFFF + +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); uint16_t default_conn_handle_get(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 01116ba398e..8ef8aaccff3 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,7 +7,6 @@ */ #include -#include #include #include "tmap_central.h" diff --git a/examples/bluetooth/esp_ble_audio/tmap/central/sdkconfig.defaults b/examples/bluetooth/esp_ble_audio/tmap/central/sdkconfig.defaults index 6658248e246..c0d383c7c12 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/central/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/tmap/central/sdkconfig.defaults @@ -2,18 +2,17 @@ # Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration # -CONFIG_PARTITION_TABLE_CUSTOM=y -CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" - CONFIG_BT_ENABLED=y -CONFIG_BT_BLUEDROID_ENABLED=n -CONFIG_BT_NIMBLE_ENABLED=y -CONFIG_BT_NIMBLE_EXT_ADV=y -CONFIG_BT_NIMBLE_NVS_PERSIST=y -CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 -CONFIG_BT_NIMBLE_MAX_CCCDS=40 -CONFIG_BT_NIMBLE_ISO=y -CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y +CONFIG_BT_NIMBLE_ENABLED=n +CONFIG_BT_BLUEDROID_ENABLED=y +CONFIG_BT_CLASSIC_ENABLED=n +CONFIG_BT_CONTROLLER_ENABLED=y +CONFIG_BT_BLE_ENABLED=y +CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y +CONFIG_BT_ACL_CONNECTIONS=1 +CONFIG_BT_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 @@ -32,4 +31,6 @@ CONFIG_BT_MCTL_LOCAL_PLAYER_REMOTE_CONTROL=y CONFIG_BT_TBS=y CONFIG_BT_TBS_SUPPORTED_FEATURES=3 +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y + CONFIG_FREERTOS_HZ=1000 diff --git a/examples/bluetooth/esp_ble_audio/tmap/central/sdkconfig.defaults.nimble b/examples/bluetooth/esp_ble_audio/tmap/central/sdkconfig.defaults.nimble new file mode 100644 index 00000000000..615c7722d8d --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/tmap/central/sdkconfig.defaults.nimble @@ -0,0 +1,12 @@ +# NimBLE host overlay for this example. +# Use with: +# idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.$IDF_TARGET;sdkconfig.defaults.nimble" build + +CONFIG_BT_BLUEDROID_ENABLED=n +CONFIG_BT_NIMBLE_ENABLED=y +CONFIG_BT_NIMBLE_EXT_ADV=y +CONFIG_BT_NIMBLE_NVS_PERSIST=y +CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 +CONFIG_BT_NIMBLE_MAX_CCCDS=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 f402af71182..f5fb6895384 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/peripheral/README.md +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/README.md @@ -9,7 +9,7 @@ This example takes the **TMAP Call Terminal (CT)** and **Unicast Media Receiver (UMR)** roles, registered together via `esp_ble_audio_tmap_register(ESP_BLE_AUDIO_TMAP_ROLE_CT | ESP_BLE_AUDIO_TMAP_ROLE_UMR)`. It runs connectable extended advertising at a 200 ms interval that includes the GAP Earbud appearance, the ASCS/CAS/TMAS UUIDs, an ASCS targeted unicast announcement carrying sink and source contexts (UNSPECIFIED | CONVERSATIONAL | MEDIA | GAME | INSTRUCTIONAL), a CAS targeted announcement, the TMAS UMR|CT role payload, and the device name `tmap_peripheral`. When the Duo earbuds option is selected, a CSIS RSI is added to the advertisement. -The host stack is NimBLE. The example uses the ESP-BLE-AUDIO library pieces for: BAP Unicast Server with PACS (LC3 cap with 16/32/48 kHz, 7.5/10 ms duration, 30–155 octets), VCP Volume Renderer (initial volume 10, unmuted, with VOCS+AICS instances built from Kconfig counts), TBS client (CCP Call Terminal — discovers GTBS, reads URI list, originates/terminates calls), MCC controller (reads player name, track title/duration/position, playback/seeking speed, playing order, media state, opcodes, CCID, plus a `mcp_send_cmd` for PLAY/PAUSE), and optionally CSIP Set Member with SIRK and rank from Kconfig. Sink streams are auto-started from the `enabled` callback. Device name is set to `TMAP Peripheral`. +The example uses the ESP-BLE-AUDIO library pieces for: BAP Unicast Server with PACS (LC3 cap with 16/32/48 kHz, 7.5/10 ms duration, 30–155 octets), VCP Volume Renderer (initial volume 10, unmuted, with VOCS+AICS instances built from Kconfig counts), TBS client (CCP Call Terminal — discovers GTBS, reads URI list, originates/terminates calls), MCC controller (reads player name, track title/duration/position, playback/seeking speed, playing order, media state, opcodes, CCID, plus a `mcp_send_cmd` for PLAY/PAUSE), and optionally CSIP Set Member with SIRK and rank from Kconfig. Sink streams are auto-started from the `enabled` callback. Device name is set to `TMAP Peripheral`. ## Requirements @@ -34,11 +34,24 @@ Just-Works pairing (LE Secure Connections, no MITM, no I/O capability) with bond ## Build & Flash +The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`. + +### Bluedroid host (default) + ```bash idf.py set-target esp32h4 idf.py -p PORT flash monitor ``` +### NimBLE host + +```bash +idf.py set-target esp32h4 +idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor +``` + +For `esp32s31`, replace the chip overlay accordingly. + (Exit serial monitor with `Ctrl-]`.) ## Example Flow @@ -75,6 +88,7 @@ Connection / discovery: ``` TMAP_PER: Connected: handle role peer +TMAP_PER: Security: handle level bonded TMAP_PER: MTU updated: handle mtu TMAP_PER: Service discovery started: handle TMAP_PER: Service discovery complete: handle status 0 @@ -113,6 +127,19 @@ TMAP_PER: Call terminated TMAP_PER: Disconnected: handle reason 0x ``` +**Note — disconnect race.** On peer-initiated disconnect (e.g. supervision timeout, peer drops the link) with active streams, an additional Bluedroid error may interleave: + +``` +W BT_APPL: gattc_conn_cb: if=4 st=0 id=4 rsn=0x8 +W BT_HCI: hcif disc complete: hdl 0x0, rsn 0x8 dev_find 1 +TMAP_PER: [SNK #0] ISO disconnected, reason 0x08 +TMAP_PER: [SNK #0] Stream disabled +E BT_APPL: Unknown connection ID: 3 fail sending notification +TMAP_PER: [SNK #0] Stream stopped, reason 0x08 +``` + +`Unknown connection ID` is harmless. GATT notifications for the ASE state changes are queued for Bluedroid to send. If Bluedroid clears the BTA connection on the ACL disconnect before those queued sends drain, they fail with this error. The peer has already disconnected, so the missed notifications have no effect on either side. + Tag is `TMAP_PER`. ## Peer Pairing diff --git a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/CMakeLists.txt b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/CMakeLists.txt index 4403c811e8d..1128e4e2547 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/CMakeLists.txt +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/CMakeLists.txt @@ -10,5 +10,12 @@ if(CONFIG_EXAMPLE_TMAP_PER_DUO) list(APPEND srcs "csip_set_member.c") endif() +if(CONFIG_BT_BLUEDROID_ENABLED) + list(APPEND srcs "bluedroid/peripheral.c") +else() + list(APPEND srcs "nimble/peripheral.c") +endif() + idf_component_register(SRCS "${srcs}" + INCLUDE_DIRS "." REQUIRES bt nvs_flash) 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 new file mode 100644 index 00000000000..6f28f783052 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/bluedroid/peripheral.c @@ -0,0 +1,116 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "esp_log.h" +#include "esp_err.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +#include "esp_gap_ble_api.h" + +#include "esp_ble_audio_common_api.h" + +#include "tmap_peripheral.h" + +static SemaphoreHandle_t adv_sem; + +/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */ +static esp_bt_status_t adv_op_status; + +#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, adv_sem, portMAX_DELAY, adv_op_status) + +static esp_ble_gap_ext_adv_params_t ext_adv_params = { + .type = ESP_BLE_GAP_SET_EXT_ADV_PROP_CONNECTABLE, + .interval_min = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS), + .interval_max = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS), + .channel_map = ADV_CHNL_ALL, + .filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, + .primary_phy = ESP_BLE_GAP_PHY_1M, + .max_skip = 0, + .secondary_phy = ESP_BLE_GAP_PHY_2M, + .sid = ADV_SID, + .scan_req_notif = false, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .tx_power = ADV_TX_POWER, +}; + +static esp_ble_gap_ext_adv_t ext_adv_inst[1] = { + [0] = { ADV_HANDLE, 0, 0 }, +}; + +static void gap_event_handler(esp_gap_ble_cb_event_t event, + esp_ble_gap_cb_param_t *param) +{ + switch (event) { + 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; + + /* SMP request handling for Just Works pairing (IO_CAP=NONE). The peer + * (central) initiates; we accept the security request and confirm the + * numeric comparison. */ + case ESP_GAP_BLE_SEC_REQ_EVT: + esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true); + break; + case ESP_GAP_BLE_NC_REQ_EVT: + esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, true); + break; + + case ESP_GAP_BLE_AUTH_CMPL_EVT: + esp_ble_audio_gap_app_post_event(event, param); + break; + + default: + break; + } +} + +int app_host_init(void) +{ + esp_err_t err; + + adv_sem = xSemaphoreCreateBinary(); + if (adv_sem == NULL) { + ESP_LOGE(TAG, "Failed to create adv semaphore"); + return -1; + } + + err = esp_ble_gap_register_callback(gap_event_handler); + if (err) { + ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err); + vSemaphoreDelete(adv_sem); + return err; + } + + return 0; +} + +int set_device_name(void) +{ + return esp_ble_gap_set_device_name(LOCAL_DEVICE_NAME); +} + +int ext_adv_start(const uint8_t *ext_data, uint16_t ext_len) +{ + WAIT_API(esp_ble_gap_ext_adv_set_params(ADV_HANDLE, &ext_adv_params)); + WAIT_API(esp_ble_gap_config_ext_adv_data_raw(ADV_HANDLE, ext_len, ext_data)); + WAIT_API(esp_ble_gap_ext_adv_start(1, ext_adv_inst)); + + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); + return 0; +} diff --git a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/ccp_call_ctrl.c b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/ccp_call_ctrl.c index cf7f6194e7e..c38ba9560a3 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/ccp_call_ctrl.c +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/ccp_call_ctrl.c @@ -10,7 +10,6 @@ #include #include #include -#include #include #include "tmap_peripheral.h" 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 bd8b20009c7..cc42a9d6819 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 @@ -9,11 +9,6 @@ #include #include -#include -#include -#include - -#include "host/ble_hs.h" #include "tmap_peripheral.h" diff --git a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/main.c b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/main.c index 0a04c073e88..1323b22dcbf 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/main.c +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/main.c @@ -7,31 +7,12 @@ */ #include -#include #include -#include -#include #include "nvs_flash.h" -#include "esp_system.h" -#include "esp_timer.h" - -#include "host/ble_hs.h" -#include "services/gap/ble_svc_gap.h" #include "tmap_peripheral.h" -#include "ble_audio_example_init.h" -#include "ble_audio_example_utils.h" - -#define ADV_HANDLE 0x00 -#define ADV_SID 0 -#define ADV_TX_POWER 127 -#define ADV_ADDRESS BLE_OWN_ADDR_PUBLIC -#define ADV_PRIMARY_PHY BLE_HCI_LE_PHY_1M -#define ADV_SECONDARY_PHY BLE_HCI_LE_PHY_2M -#define ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(200) - static uint8_t general_adv_data[] = { /* Flags */ 0x02, EXAMPLE_AD_TYPE_FLAGS, (EXAMPLE_AD_FLAGS_GENERAL | EXAMPLE_AD_FLAGS_NO_BREDR), @@ -77,9 +58,9 @@ static uint8_t tmap_adv_data[] = { }; static uint8_t dev_name_adv_data[] = { - /* Complete Device Name */ - 0x10, EXAMPLE_AD_TYPE_NAME_COMPLETE, 't', 'm', 'a', 'p', '_', - 'p', 'e', 'r', 'i', 'p', 'h', 'e', 'r', 'a', 'l', + /* Complete Device Name — must match LOCAL_DEVICE_NAME set via set_device_name(). */ + 0x10, EXAMPLE_AD_TYPE_NAME_COMPLETE, 'T', 'M', 'A', 'P', ' ', + 'P', 'e', 'r', 'i', 'p', 'h', 'e', 'r', 'a', 'l', }; #if CONFIG_BT_CSIP_SET_MEMBER @@ -108,7 +89,7 @@ uint16_t default_conn_handle_get(void) return default_conn_handle; } -static int set_ext_adv_data(void) +static int build_ext_adv_data(void) { uint16_t data_len = 0; @@ -143,63 +124,6 @@ static int set_ext_adv_data(void) return 0; } -static void ext_adv_start(void) -{ - struct ble_gap_ext_adv_params ext_params = {0}; - struct os_mbuf *data = NULL; - int err; - - ext_params.connectable = 1; - ext_params.scannable = 0; - ext_params.legacy_pdu = 0; - ext_params.own_addr_type = ADV_ADDRESS; - ext_params.primary_phy = ADV_PRIMARY_PHY; - ext_params.secondary_phy = ADV_SECONDARY_PHY; - ext_params.tx_power = ADV_TX_POWER; - ext_params.sid = ADV_SID; - ext_params.itvl_min = ADV_INTERVAL; - ext_params.itvl_max = ADV_INTERVAL; - - err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL, - example_audio_gap_event_cb, NULL); - if (err) { - ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err); - return; - } - - err = set_ext_adv_data(); - if (err) { - return; - } - - data = os_msys_get_pkthdr(sizeof(ext_adv_data), 0); - if (data == NULL) { - ESP_LOGE(TAG, "Failed to get ext adv mbuf"); - return; - } - - err = os_mbuf_append(data, ext_adv_data, sizeof(ext_adv_data)); - if (err) { - ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err); - os_mbuf_free_chain(data); - return; - } - - err = ble_gap_ext_adv_set_data(ADV_HANDLE, data); - if (err) { - ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err); - return; - } - - err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0); - if (err) { - ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err); - return; - } - - ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); -} - static void acl_connect(esp_ble_audio_gap_app_event_t *event) { if (event->acl_connect.status) { @@ -209,9 +133,7 @@ static void acl_connect(esp_ble_audio_gap_app_event_t *event) ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x", event->acl_connect.conn_handle, event->acl_connect.role, - event->acl_connect.dst.val[5], event->acl_connect.dst.val[4], - event->acl_connect.dst.val[3], event->acl_connect.dst.val[2], - event->acl_connect.dst.val[1], event->acl_connect.dst.val[0]); + EXAMPLE_BT_ADDR_PRINT_ARGS(event->acl_connect.dst.val)); default_conn_handle = event->acl_connect.conn_handle; } @@ -223,7 +145,22 @@ static void acl_disconnect(esp_ble_audio_gap_app_event_t *event) default_conn_handle = CONN_HANDLE_INIT; - ext_adv_start(); + ext_adv_start(ext_adv_data, sizeof(ext_adv_data)); +} + +static void security_change(esp_ble_audio_gap_app_event_t *event) +{ + if (event->security_change.status) { + ESP_LOGE(TAG, "Security failed: handle %u status %d", + event->security_change.conn_handle, + event->security_change.status); + return; + } + + ESP_LOGI(TAG, "Security: handle %u level %u bonded %u", + event->security_change.conn_handle, + event->security_change.sec_level, + event->security_change.bonded); } static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event) @@ -235,6 +172,9 @@ static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event) case ESP_BLE_AUDIO_GAP_EVENT_ACL_DISCONNECT: acl_disconnect(event); break; + case ESP_BLE_AUDIO_GAP_EVENT_SECURITY_CHANGE: + security_change(event); + break; default: break; } @@ -317,6 +257,12 @@ void app_main(void) return; } + err = app_host_init(); + if (err) { + ESP_LOGE(TAG, "Failed to init host, err %d", err); + return; + } + err = esp_ble_audio_common_init(&init_info); if (err) { ESP_LOGE(TAG, "Failed to initialize audio, err %d", err); @@ -369,11 +315,16 @@ void app_main(void) return; } - err = ble_svc_gap_device_name_set("TMAP Peripheral"); + err = set_device_name(); if (err) { ESP_LOGE(TAG, "Failed to set device name, err %d", err); return; } - ext_adv_start(); + err = build_ext_adv_data(); + if (err) { + return; + } + + ext_adv_start(ext_adv_data, sizeof(ext_adv_data)); } diff --git a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/mcp_ctlr.c b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/mcp_ctlr.c index 1a3eb55af78..f3f0386417d 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/mcp_ctlr.c +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/mcp_ctlr.c @@ -7,9 +7,6 @@ */ #include -#include -#include -#include #include #include "tmap_peripheral.h" @@ -322,6 +319,9 @@ int mcp_discover_mcs(void) return -ENOTCONN; } + /* New session — re-arm the initial read cascade. */ + is_remote_read = false; + err = esp_ble_audio_mcc_discover_mcs(conn_handle, true); if (err) { ESP_LOGE(TAG, "Failed to discover MCS, err %d", err); 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 new file mode 100644 index 00000000000..b7c305bec42 --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/nimble/peripheral.c @@ -0,0 +1,112 @@ +/* + * 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 "host/ble_store.h" +#include "services/gap/ble_svc_gap.h" + +#include "os/os_mbuf.h" + +#include "esp_ble_audio_common_api.h" + +#include "tmap_peripheral.h" + +static int gap_event_cb(struct ble_gap_event *event, void *arg) +{ + switch (event->type) { + case BLE_GAP_EVENT_CONNECT: + case BLE_GAP_EVENT_DISCONNECT: + case BLE_GAP_EVENT_ENC_CHANGE: + esp_ble_audio_gap_app_post_event(event->type, event); + break; + case BLE_GAP_EVENT_MTU: + case BLE_GAP_EVENT_NOTIFY_RX: + case BLE_GAP_EVENT_NOTIFY_TX: + case BLE_GAP_EVENT_SUBSCRIBE: + esp_ble_audio_gatt_app_post_event(event->type, event); + break; + case BLE_GAP_EVENT_REPEAT_PAIRING: { + struct ble_gap_conn_desc desc = {0}; + int rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc); + if (rc == 0) { + ble_store_util_delete_peer(&desc.peer_id_addr); + } + return BLE_GAP_REPEAT_PAIRING_RETRY; + } + default: + break; + } + + return 0; +} + +int app_host_init(void) +{ + return 0; +} + +int set_device_name(void) +{ + return ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME); +} + +int ext_adv_start(const uint8_t *ext_data, uint16_t ext_len) +{ + struct ble_gap_ext_adv_params ext_params = {0}; + struct os_mbuf *data = NULL; + int err; + + ext_params.connectable = 1; + ext_params.scannable = 0; + ext_params.legacy_pdu = 0; + ext_params.own_addr_type = BLE_OWN_ADDR_PUBLIC; + ext_params.primary_phy = BLE_HCI_LE_PHY_1M; + ext_params.secondary_phy = BLE_HCI_LE_PHY_2M; + ext_params.tx_power = ADV_TX_POWER; + ext_params.sid = ADV_SID; + ext_params.itvl_min = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS); + ext_params.itvl_max = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS); + + err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL, + gap_event_cb, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err); + return err; + } + + data = os_msys_get_pkthdr(ext_len, 0); + if (data == NULL) { + ESP_LOGE(TAG, "Failed to get ext adv mbuf"); + return -1; + } + + err = os_mbuf_append(data, ext_data, ext_len); + if (err) { + ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err); + os_mbuf_free_chain(data); + return err; + } + + err = ble_gap_ext_adv_set_data(ADV_HANDLE, data); + if (err) { + ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err); + return err; + } + + err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0); + if (err) { + ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); + return 0; +} diff --git a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/tmap_ct_umr.c b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/tmap_ct_umr.c index dbe64b9d8f3..f7a7833982e 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/tmap_ct_umr.c +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/tmap_ct_umr.c @@ -7,9 +7,6 @@ */ #include -#include -#include -#include #include #include "tmap_peripheral.h" 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 e159262e565..6baa3058354 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 @@ -5,6 +5,10 @@ * SPDX-License-Identifier: Apache-2.0 */ +#pragma once + +#include + #include "esp_log.h" #include "sdkconfig.h" @@ -22,11 +26,19 @@ #include "esp_ble_audio_media_proxy_api.h" #include "esp_ble_audio_vocs_api.h" +#include "ble_audio_example_init.h" #include "ble_audio_example_utils.h" #define TAG "TMAP_PER" -#define CONN_HANDLE_INIT 0xFFFF +#define CONN_HANDLE_INIT 0xFFFF + +#define LOCAL_DEVICE_NAME "TMAP Peripheral" + +#define ADV_HANDLE 0 +#define ADV_SID 0 +#define ADV_TX_POWER 127 +#define ADV_INTERVAL_MS 200 #define SINK_CONTEXT (ESP_BLE_AUDIO_CONTEXT_TYPE_UNSPECIFIED | \ ESP_BLE_AUDIO_CONTEXT_TYPE_CONVERSATIONAL | \ @@ -40,6 +52,12 @@ ESP_BLE_AUDIO_CONTEXT_TYPE_GAME | \ ESP_BLE_AUDIO_CONTEXT_TYPE_INSTRUCTIONAL) +int app_host_init(void); + +int set_device_name(void); + +int ext_adv_start(const uint8_t *ext_data, uint16_t ext_len); + uint16_t default_conn_handle_get(void); int vcp_vol_renderer_init(void); diff --git a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/vcp_vol_renderer.c b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/vcp_vol_renderer.c index ed1dc20238f..6d13d8f5ebc 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/vcp_vol_renderer.c +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/main/vcp_vol_renderer.c @@ -10,9 +10,6 @@ #include #include -#include -#include -#include #include "tmap_peripheral.h" diff --git a/examples/bluetooth/esp_ble_audio/tmap/peripheral/sdkconfig.defaults b/examples/bluetooth/esp_ble_audio/tmap/peripheral/sdkconfig.defaults index 7307864a1ba..bfd6b33c04b 100644 --- a/examples/bluetooth/esp_ble_audio/tmap/peripheral/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/sdkconfig.defaults @@ -2,18 +2,17 @@ # Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration # -CONFIG_PARTITION_TABLE_CUSTOM=y -CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" - CONFIG_BT_ENABLED=y -CONFIG_BT_BLUEDROID_ENABLED=n -CONFIG_BT_NIMBLE_ENABLED=y -CONFIG_BT_NIMBLE_EXT_ADV=y -CONFIG_BT_NIMBLE_NVS_PERSIST=y -CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 -CONFIG_BT_NIMBLE_MAX_CCCDS=30 -CONFIG_BT_NIMBLE_ISO=y -CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y +CONFIG_BT_NIMBLE_ENABLED=n +CONFIG_BT_BLUEDROID_ENABLED=y +CONFIG_BT_CLASSIC_ENABLED=n +CONFIG_BT_CONTROLLER_ENABLED=y +CONFIG_BT_BLE_ENABLED=y +CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y +CONFIG_BT_ACL_CONNECTIONS=1 +CONFIG_BT_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 @@ -39,7 +38,10 @@ CONFIG_BT_TBS_CLIENT_TBS=y CONFIG_BT_TBS_CLIENT_ORIGINATE_CALL=y 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 diff --git a/examples/bluetooth/esp_ble_audio/tmap/peripheral/sdkconfig.defaults.nimble b/examples/bluetooth/esp_ble_audio/tmap/peripheral/sdkconfig.defaults.nimble new file mode 100644 index 00000000000..615c7722d8d --- /dev/null +++ b/examples/bluetooth/esp_ble_audio/tmap/peripheral/sdkconfig.defaults.nimble @@ -0,0 +1,12 @@ +# NimBLE host overlay for this example. +# Use with: +# idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.$IDF_TARGET;sdkconfig.defaults.nimble" build + +CONFIG_BT_BLUEDROID_ENABLED=n +CONFIG_BT_NIMBLE_ENABLED=y +CONFIG_BT_NIMBLE_EXT_ADV=y +CONFIG_BT_NIMBLE_NVS_PERSIST=y +CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 +CONFIG_BT_NIMBLE_MAX_CCCDS=64 +CONFIG_BT_NIMBLE_ISO=y +CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y diff --git a/examples/bluetooth/esp_ble_iso/big_broadcaster/README.md b/examples/bluetooth/esp_ble_iso/big_broadcaster/README.md index 9d4591517ab..f777fe7e033 100644 --- a/examples/bluetooth/esp_ble_iso/big_broadcaster/README.md +++ b/examples/bluetooth/esp_ble_iso/big_broadcaster/README.md @@ -7,7 +7,7 @@ ## Overview -This example demonstrates a raw BLE Isochronous Broadcaster — it creates a Broadcast Isochronous Group (BIG) directly at the ISO transport layer over the NimBLE host, without any BLE Audio profile (BAP/CAP) on top. +This example demonstrates a raw BLE Isochronous Broadcaster — it creates a Broadcast Isochronous Group (BIG) directly at the ISO transport layer over either the NimBLE or Bluedroid host (selected at build time via Kconfig), without any BLE Audio profile (BAP/CAP) on top. The device acts as the **broadcaster**: it starts extended + periodic advertising, creates a BIG carrying two BIS streams, sets up an HCI input data path on each BIS, and then transmits SDUs on a fixed 10 ms cadence using a software TX scheduler. The peer (`big_receiver`) discovers the broadcaster by name, syncs to the periodic advertising train, decodes BIGInfo, and joins the BIS sub-events. @@ -39,20 +39,33 @@ Notable hard-coded parameters in `main/main.c`: ### Security & Pairing -The shared init at `../common_components/example_init/ble_iso_example_init.c` configures Just-Works pairing (LE Secure Connections, no MITM, `BLE_SM_IO_CAP_NO_IO`) with bonding enabled, and leaves `gatts_register_cb = NULL` (no GATT services). These settings are not exercised by this example — BIG broadcast traffic is non-connectable and BIS payload encryption is driven by the broadcast code, independent of host SMP. +On the NimBLE path, the shared init at `../common_components/example_init/ble_iso_example_init.c` configures Just-Works pairing (LE Secure Connections, no MITM, `BLE_SM_IO_CAP_NO_IO`) with bonding enabled, and leaves `gatts_register_cb = NULL` (no GATT services). On the Bluedroid path the same init just brings up the controller and host with no SMP configuration. Either way these settings are not exercised by this example — BIG broadcast traffic is non-connectable and BIS payload encryption is driven by the broadcast code, independent of host SMP. ## Build & Flash +The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`. + +### Bluedroid host (default) + ```bash idf.py set-target esp32h4 idf.py -p PORT flash monitor ``` +### NimBLE host + +```bash +idf.py set-target esp32h4 +idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor +``` + +For `esp32s31`, replace the chip overlay accordingly. + (Exit serial monitor with `Ctrl-]`.) ## Example Flow -1. NVS, NimBLE host, and the ISO common layer are initialised. +1. NVS, the selected BLE host (NimBLE or Bluedroid), and the ISO common layer are initialised. 2. A TX scheduler is initialised for each of the two BIS channels. 3. Extended advertising parameters / data and periodic advertising parameters / data are configured for handle 0 (non-connectable, non-scannable, primary 1M / secondary 2M). 4. Periodic advertising and extended advertising are started. diff --git a/examples/bluetooth/esp_ble_iso/big_broadcaster/main/CMakeLists.txt b/examples/bluetooth/esp_ble_iso/big_broadcaster/main/CMakeLists.txt index 721967052c7..fb840295253 100644 --- a/examples/bluetooth/esp_ble_iso/big_broadcaster/main/CMakeLists.txt +++ b/examples/bluetooth/esp_ble_iso/big_broadcaster/main/CMakeLists.txt @@ -1,4 +1,11 @@ set(srcs "main.c") -idf_component_register(SRCS "${srcs}" +if(CONFIG_BT_BLUEDROID_ENABLED) + list(APPEND srcs "bluedroid/adv.c") +else() + list(APPEND srcs "nimble/adv.c") +endif() + +idf_component_register(SRCS ${srcs} + INCLUDE_DIRS "." REQUIRES bt nvs_flash) diff --git a/examples/bluetooth/esp_ble_iso/big_broadcaster/main/adv.h b/examples/bluetooth/esp_ble_iso/big_broadcaster/main/adv.h new file mode 100644 index 00000000000..8ccf1e20e17 --- /dev/null +++ b/examples/bluetooth/esp_ble_iso/big_broadcaster/main/adv.h @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "ble_iso_example_utils.h" + +#define TAG "BIG_BRD" + +#define ADV_HANDLE 0 +#define ADV_SID 0 +#define ADV_TX_POWER 127 +#define ADV_INTERVAL_MS 200 +#define PER_ADV_INTERVAL_MS 100 + +int app_host_init(void); + +int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len, + const uint8_t *per_data, uint8_t per_len); diff --git a/examples/bluetooth/esp_ble_iso/big_broadcaster/main/bluedroid/adv.c b/examples/bluetooth/esp_ble_iso/big_broadcaster/main/bluedroid/adv.c new file mode 100644 index 00000000000..4b1623882c8 --- /dev/null +++ b/examples/bluetooth/esp_ble_iso/big_broadcaster/main/bluedroid/adv.c @@ -0,0 +1,121 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "esp_log.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +#include "esp_bt_defs.h" +#include "esp_gap_ble_api.h" + +#include "adv.h" + +static SemaphoreHandle_t adv_sem; + +/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */ +static esp_bt_status_t adv_op_status; + +#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, adv_sem, portMAX_DELAY, adv_op_status) + +static esp_ble_gap_ext_adv_params_t ext_adv_params = { + .type = ESP_BLE_GAP_SET_EXT_ADV_PROP_NONCONN_NONSCANNABLE_UNDIRECTED, + .interval_min = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS), + .interval_max = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS), + .channel_map = ADV_CHNL_ALL, + .filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, + .primary_phy = ESP_BLE_GAP_PHY_1M, + .max_skip = 0, + .secondary_phy = ESP_BLE_GAP_PHY_2M, + .sid = ADV_SID, + .scan_req_notif = false, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .tx_power = ADV_TX_POWER, +}; + +static esp_ble_gap_periodic_adv_params_t periodic_adv_params = { + .interval_min = ESP_BLE_GAP_PERIODIC_ADV_ITVL_MS(PER_ADV_INTERVAL_MS), + .interval_max = ESP_BLE_GAP_PERIODIC_ADV_ITVL_MS(PER_ADV_INTERVAL_MS), + .properties = 0, +}; + +static esp_ble_gap_ext_adv_t ext_adv_inst[1] = { + [0] = { ADV_HANDLE, 0, 0 }, +}; + +static void gap_event_handler(esp_gap_ble_cb_event_t event, + esp_ble_gap_cb_param_t *param) +{ + switch (event) { + case ESP_GAP_BLE_EXT_ADV_SET_PARAMS_COMPLETE_EVT: + adv_op_status = param->ext_adv_set_params.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_EXT_ADV_DATA_SET_COMPLETE_EVT: + adv_op_status = param->ext_adv_data_set.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_EXT_ADV_START_COMPLETE_EVT: + adv_op_status = param->ext_adv_start.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_PERIODIC_ADV_SET_PARAMS_COMPLETE_EVT: + adv_op_status = param->peroid_adv_set_params.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_PERIODIC_ADV_DATA_SET_COMPLETE_EVT: + adv_op_status = param->period_adv_data_set.status; + xSemaphoreGive(adv_sem); + break; + case ESP_GAP_BLE_PERIODIC_ADV_START_COMPLETE_EVT: + adv_op_status = param->period_adv_start.status; + xSemaphoreGive(adv_sem); + break; + default: + break; + } +} + +int app_host_init(void) +{ + esp_err_t err; + + adv_sem = xSemaphoreCreateBinary(); + if (adv_sem == NULL) { + ESP_LOGE(TAG, "Failed to create adv semaphore"); + return -1; + } + + err = esp_ble_gap_register_callback(gap_event_handler); + if (err) { + ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err); + vSemaphoreDelete(adv_sem); + return err; + } + + return 0; +} + +int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len, + const uint8_t *per_data, uint8_t per_len) +{ + WAIT_API(esp_ble_gap_ext_adv_set_params(ADV_HANDLE, &ext_adv_params)); + WAIT_API(esp_ble_gap_config_ext_adv_data_raw(ADV_HANDLE, ext_len, ext_data)); + WAIT_API(esp_ble_gap_periodic_adv_set_params(ADV_HANDLE, &periodic_adv_params)); +#if CONFIG_BT_BLE_FEAT_PERIODIC_ADV_ENH + WAIT_API(esp_ble_gap_config_periodic_adv_data_raw(ADV_HANDLE, per_len, per_data, false)); + WAIT_API(esp_ble_gap_periodic_adv_start(ADV_HANDLE, true)); +#else + WAIT_API(esp_ble_gap_config_periodic_adv_data_raw(ADV_HANDLE, per_len, per_data)); + WAIT_API(esp_ble_gap_periodic_adv_start(ADV_HANDLE)); +#endif + WAIT_API(esp_ble_gap_ext_adv_start(1, ext_adv_inst)); + + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); + return 0; +} diff --git a/examples/bluetooth/esp_ble_iso/big_broadcaster/main/main.c b/examples/bluetooth/esp_ble_iso/big_broadcaster/main/main.c index f4f6bb40e05..12a1a11d1b6 100644 --- a/examples/bluetooth/esp_ble_iso/big_broadcaster/main/main.c +++ b/examples/bluetooth/esp_ble_iso/big_broadcaster/main/main.c @@ -7,36 +7,21 @@ #include #include -#include #include "esp_log.h" #include "nvs_flash.h" -#include "esp_timer.h" - -#include "host/ble_gap.h" #include "esp_ble_iso_common_api.h" #include "ble_iso_example_init.h" #include "ble_iso_example_utils.h" -#define TAG "BIG_BRD" +#include "adv.h" #define LOCAL_DEVICE_NAME "BIG Broadcaster" #define LOCAL_DEVICE_NAME_LEN (sizeof(LOCAL_DEVICE_NAME) - 1) - #define LOCAL_BROADCAST_CODE "1234" /* Maximum length is 16 */ -#define ADV_HANDLE 0 -#define ADV_SID 0 -#define ADV_TX_POWER 127 -#define ADV_ADDRESS BLE_OWN_ADDR_PUBLIC -#define ADV_PRIMARY_PHY BLE_HCI_LE_PHY_1M -#define ADV_SECONDARY_PHY BLE_HCI_LE_PHY_2M -#define ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(200) - -#define PER_ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(100) - #define BIG_SDU_INTERVAL_US 10000 /* 10ms */ #define BIG_LATENCY_MS 10 /* 10ms */ #define BIG_PHY ESP_BLE_ISO_PHY_2M @@ -47,7 +32,6 @@ #define BIS_SDU_SIZE 120 #define BIG_ENCRYPTION true -/* CONFIG_BT_ISO_MAX_CHAN must be >= 2 */ _Static_assert(CONFIG_BT_ISO_MAX_CHAN >= BIS_ISO_CHAN_COUNT, "CONFIG_BT_ISO_MAX_CHAN must be >= BIS_ISO_CHAN_COUNT"); @@ -187,110 +171,6 @@ static int bis_chan_index_get(const esp_ble_iso_chan_t *chan) return -1; } -static void build_adv_data(void) -{ - ext_adv_data[0] = 0x02; - ext_adv_data[1] = EXAMPLE_AD_TYPE_FLAGS; - ext_adv_data[2] = EXAMPLE_AD_FLAGS_GENERAL | EXAMPLE_AD_FLAGS_NO_BREDR; - ext_adv_data[3] = (uint8_t)(LOCAL_DEVICE_NAME_LEN + 1); /* AD type + name */ - ext_adv_data[4] = EXAMPLE_AD_TYPE_NAME_COMPLETE; - memcpy(&ext_adv_data[5], LOCAL_DEVICE_NAME, LOCAL_DEVICE_NAME_LEN); - - memcpy(per_adv_data, LOCAL_DEVICE_NAME, LOCAL_DEVICE_NAME_LEN); -} - -static int ext_adv_start(void) -{ - struct ble_gap_periodic_adv_params per_params = {0}; - struct ble_gap_ext_adv_params ext_params = {0}; - struct os_mbuf *data = NULL; - int err; - - build_adv_data(); - - ext_params.connectable = 0; - ext_params.scannable = 0; - ext_params.legacy_pdu = 0; - ext_params.own_addr_type = ADV_ADDRESS; - ext_params.primary_phy = ADV_PRIMARY_PHY; - ext_params.secondary_phy = ADV_SECONDARY_PHY; - ext_params.tx_power = ADV_TX_POWER; - ext_params.sid = ADV_SID; - ext_params.itvl_min = ADV_INTERVAL; - ext_params.itvl_max = ADV_INTERVAL; - - err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL, - example_iso_gap_event_cb, NULL); - if (err) { - ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err); - return err; - } - - data = os_msys_get_pkthdr(sizeof(ext_adv_data), 0); - if (data == NULL) { - ESP_LOGE(TAG, "Failed to get ext adv mbuf"); - return -1; - } - - err = os_mbuf_append(data, ext_adv_data, sizeof(ext_adv_data)); - if (err) { - ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err); - os_mbuf_free_chain(data); - return err; - } - - err = ble_gap_ext_adv_set_data(ADV_HANDLE, data); - if (err) { - ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err); - return err; - } - - per_params.include_tx_power = 0; - per_params.itvl_min = PER_ADV_INTERVAL; - per_params.itvl_max = PER_ADV_INTERVAL; - - err = ble_gap_periodic_adv_configure(ADV_HANDLE, &per_params); - if (err) { - ESP_LOGE(TAG, "Failed to configure per adv params, err %d", err); - return err; - } - - data = os_msys_get_pkthdr(sizeof(per_adv_data), 0); - if (data == NULL) { - ESP_LOGE(TAG, "Failed to get per adv mbuf"); - return -1; - } - - err = os_mbuf_append(data, per_adv_data, sizeof(per_adv_data)); - if (err) { - ESP_LOGE(TAG, "Failed to append per adv data, err %d", err); - os_mbuf_free_chain(data); - return err; - } - - err = ble_gap_periodic_adv_set_data(ADV_HANDLE, data); - if (err) { - ESP_LOGE(TAG, "Failed to set per adv data, err %d", err); - return err; - } - - err = ble_gap_periodic_adv_start(ADV_HANDLE); - if (err) { - ESP_LOGE(TAG, "Failed to start per adv, err %d", err); - return err; - } - - err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0); - if (err) { - ESP_LOGE(TAG, "Failed to start ext adv, err %d", err); - return err; - } - - ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); - - return 0; -} - static void big_create(void) { esp_ble_iso_big_create_param_t param = {0}; @@ -370,6 +250,18 @@ static void tx_scheduler_cb(void *arg) iso_chan_send(tx->chan_idx); } +static void build_ext_adv_data(void) +{ + ext_adv_data[0] = 0x02; + ext_adv_data[1] = EXAMPLE_AD_TYPE_FLAGS; + ext_adv_data[2] = EXAMPLE_AD_FLAGS_GENERAL | EXAMPLE_AD_FLAGS_NO_BREDR; + ext_adv_data[3] = (uint8_t)(LOCAL_DEVICE_NAME_LEN + 1); /* AD type + name */ + ext_adv_data[4] = EXAMPLE_AD_TYPE_NAME_COMPLETE; + memcpy(&ext_adv_data[5], LOCAL_DEVICE_NAME, LOCAL_DEVICE_NAME_LEN); + + memcpy(per_adv_data, LOCAL_DEVICE_NAME, LOCAL_DEVICE_NAME_LEN); +} + void app_main(void) { esp_ble_iso_init_info_t info = {0}; @@ -389,6 +281,12 @@ void app_main(void) return; } + err = app_host_init(); + if (err) { + ESP_LOGE(TAG, "Failed to init host, err %d", err); + return; + } + err = esp_ble_iso_common_init(&info); if (err) { ESP_LOGE(TAG, "Failed to initialize ISO, err %d", err); @@ -406,7 +304,10 @@ void app_main(void) } } - err = ext_adv_start(); + build_ext_adv_data(); + + err = ext_adv_start(ext_adv_data, sizeof(ext_adv_data), + per_adv_data, sizeof(per_adv_data)); if (err) { return; } diff --git a/examples/bluetooth/esp_ble_iso/big_broadcaster/main/nimble/adv.c b/examples/bluetooth/esp_ble_iso/big_broadcaster/main/nimble/adv.c new file mode 100644 index 00000000000..bd3a398860c --- /dev/null +++ b/examples/bluetooth/esp_ble_iso/big_broadcaster/main/nimble/adv.c @@ -0,0 +1,118 @@ +/* + * 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 "adv.h" + +/* Non-connectable broadcaster: no GAP events for the application. */ +static int gap_event_cb(struct ble_gap_event *event, void *arg) +{ + return 0; +} + +int app_host_init(void) +{ + return 0; +} + +int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len, + const uint8_t *per_data, uint8_t per_len) +{ + struct ble_gap_periodic_adv_params per_params = {0}; + struct ble_gap_ext_adv_params ext_params = {0}; + struct os_mbuf *data = NULL; + int err; + + ext_params.connectable = 0; + ext_params.scannable = 0; + ext_params.legacy_pdu = 0; + ext_params.own_addr_type = BLE_OWN_ADDR_PUBLIC; + ext_params.primary_phy = BLE_HCI_LE_PHY_1M; + ext_params.secondary_phy = BLE_HCI_LE_PHY_2M; + ext_params.tx_power = ADV_TX_POWER; + ext_params.sid = ADV_SID; + ext_params.itvl_min = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS); + ext_params.itvl_max = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS); + + err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL, + gap_event_cb, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err); + return err; + } + + data = os_msys_get_pkthdr(ext_len, 0); + if (data == NULL) { + ESP_LOGE(TAG, "Failed to get ext adv mbuf"); + return -1; + } + + err = os_mbuf_append(data, ext_data, ext_len); + if (err) { + ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err); + os_mbuf_free_chain(data); + return err; + } + + err = ble_gap_ext_adv_set_data(ADV_HANDLE, data); + if (err) { + ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err); + /* ble_gap_ext_adv_set_data takes ownership of `data` and frees it via + * its 'done' label on both success and failure paths — do NOT free + * here (double-free). API header doesn't document this contract. */ + return err; + } + + per_params.include_tx_power = 0; + per_params.itvl_min = BLE_GAP_PERIODIC_ITVL_MS(PER_ADV_INTERVAL_MS); + per_params.itvl_max = BLE_GAP_PERIODIC_ITVL_MS(PER_ADV_INTERVAL_MS); + + err = ble_gap_periodic_adv_configure(ADV_HANDLE, &per_params); + if (err) { + ESP_LOGE(TAG, "Failed to configure per adv params, err %d", err); + return err; + } + + data = os_msys_get_pkthdr(per_len, 0); + if (data == NULL) { + ESP_LOGE(TAG, "Failed to get per adv mbuf"); + return -1; + } + + err = os_mbuf_append(data, per_data, per_len); + if (err) { + ESP_LOGE(TAG, "Failed to append per adv data, err %d", err); + os_mbuf_free_chain(data); + return err; + } + + err = ble_gap_periodic_adv_set_data(ADV_HANDLE, data); + if (err) { + ESP_LOGE(TAG, "Failed to set per adv data, err %d", err); + return err; + } + + err = ble_gap_periodic_adv_start(ADV_HANDLE); + if (err) { + ESP_LOGE(TAG, "Failed to start per adv, err %d", err); + return err; + } + + err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0); + if (err) { + ESP_LOGE(TAG, "Failed to start ext adv, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); + + return 0; +} diff --git a/examples/bluetooth/esp_ble_iso/big_broadcaster/sdkconfig.defaults b/examples/bluetooth/esp_ble_iso/big_broadcaster/sdkconfig.defaults index d5aeca2558c..8cad6879b3c 100644 --- a/examples/bluetooth/esp_ble_iso/big_broadcaster/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_iso/big_broadcaster/sdkconfig.defaults @@ -3,13 +3,17 @@ # CONFIG_BT_ENABLED=y -CONFIG_BT_BLUEDROID_ENABLED=n -CONFIG_BT_NIMBLE_ENABLED=y -CONFIG_BT_NIMBLE_EXT_ADV=y -CONFIG_BT_NIMBLE_ISO=y -CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y +CONFIG_BT_NIMBLE_ENABLED=n +CONFIG_BT_BLUEDROID_ENABLED=y +CONFIG_BT_CLASSIC_ENABLED=n +CONFIG_BT_CONTROLLER_ENABLED=y +CONFIG_BT_BLE_ENABLED=y +CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y +CONFIG_BT_BLE_FEAT_ISO_EN=y CONFIG_BT_ISO_BROADCASTER=y CONFIG_BT_ISO_MAX_CHAN=2 +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y + CONFIG_FREERTOS_HZ=1000 diff --git a/examples/bluetooth/esp_ble_iso/big_broadcaster/sdkconfig.defaults.nimble b/examples/bluetooth/esp_ble_iso/big_broadcaster/sdkconfig.defaults.nimble new file mode 100644 index 00000000000..24402677507 --- /dev/null +++ b/examples/bluetooth/esp_ble_iso/big_broadcaster/sdkconfig.defaults.nimble @@ -0,0 +1,9 @@ +# 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_ISO=y +CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y diff --git a/examples/bluetooth/esp_ble_iso/big_receiver/README.md b/examples/bluetooth/esp_ble_iso/big_receiver/README.md index 43d5b6a4edf..96aeed47148 100644 --- a/examples/bluetooth/esp_ble_iso/big_receiver/README.md +++ b/examples/bluetooth/esp_ble_iso/big_receiver/README.md @@ -7,7 +7,7 @@ ## Overview -This example demonstrates a raw BLE Isochronous Broadcast Receiver — it joins a Broadcast Isochronous Group (BIG) directly at the ISO transport layer over the NimBLE host, without any BLE Audio profile (BAP/CAP) on top. +This example demonstrates a raw BLE Isochronous Broadcast Receiver — it joins a Broadcast Isochronous Group (BIG) directly at the ISO transport layer over either the NimBLE or Bluedroid host (selected at build time via Kconfig), without any BLE Audio profile (BAP/CAP) on top. The device acts as the **receiver (BIG sync sink)**: it scans for an extended advertiser by device name, synchronises to the peer's periodic advertising train, parses the BIGInfo report, and then issues `LE BIG Create Sync` to join two BIS sub-events. An HCI output data path is installed on each BIS and incoming SDUs are accounted for by a small RX-metrics helper. @@ -26,7 +26,7 @@ idf.py menuconfig No build-time options — runtime defaults are baked into source. -Notable hard-coded parameters in `main/main.c`: +Notable hard-coded parameters in `main/main.c` and `main//scan.c`: * `TARGET_DEVICE_NAME` — `"BIG Broadcaster"` (matched against the AD complete-name field) * `TARGET_BROADCAST_CODE` — `"1234"` (must match the broadcaster) @@ -37,22 +37,35 @@ Notable hard-coded parameters in `main/main.c`: ### Security & Pairing -The shared init at `../common_components/example_init/ble_iso_example_init.c` configures Just-Works pairing (LE Secure Connections, no MITM, `BLE_SM_IO_CAP_NO_IO`) with bonding enabled, and leaves `gatts_register_cb = NULL` (no GATT services). These settings are not exercised by this example — the receiver passively syncs to PA + BIS without ATT or pairing. +On the NimBLE path, the shared init at `../common_components/example_init/ble_iso_example_init.c` configures Just-Works pairing (LE Secure Connections, no MITM, `BLE_SM_IO_CAP_NO_IO`) with bonding enabled, and leaves `gatts_register_cb = NULL` (no GATT services). On the Bluedroid path the same init just brings up the controller and host with no SMP configuration. Either way these settings are not exercised by this example — the receiver passively syncs to PA + BIS without ATT or pairing. ## Build & Flash +The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`. + +### Bluedroid host (default) + ```bash idf.py set-target esp32h4 idf.py -p PORT flash monitor ``` +### NimBLE host + +```bash +idf.py set-target esp32h4 +idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor +``` + +For `esp32s31`, replace the chip overlay accordingly. + (Exit serial monitor with `Ctrl-]`.) ## Example Flow -1. NVS, NimBLE host, and the ISO common layer are initialised; a GAP application callback is registered for ISO-related GAP events. -2. Passive extended scanning is started via `ble_gap_disc()`. -3. On each `EXT_SCAN_RECV` the AD payload is parsed; if the complete-name field equals `"BIG Broadcaster"` and the report carries a non-zero periodic-advertising interval, `ble_gap_periodic_adv_sync_create()` is called. +1. NVS, the selected BLE host (NimBLE or Bluedroid), and the ISO common layer are initialised; a GAP application callback is registered for ISO-related GAP events. +2. Passive extended scanning is started via the host-specific `scan_start()` wrapper. +3. On each `EXT_SCAN_RECV` the AD payload is parsed; if the complete-name field equals `"BIG Broadcaster"` and the report carries a non-zero periodic-advertising interval, `pa_sync_create()` is called. 4. On `PA_SYNC` success the extended scan is cancelled (BIGInfo arrives over the PA channel anyway). 5. On the first `BIGINFO_RECV` event the receiver fills `esp_ble_iso_big_sync_param_t` (both BIS in `bis_bitfield`, broadcast code `"1234"`, MSE = `nse` from BIGInfo) and calls `esp_ble_iso_big_sync()` — the HCI `LE BIG Create Sync` command. 6. When each BIS becomes ready, the connected callback resets that BIS's RX metrics and installs an HCI output data path (`ESP_BLE_ISO_DATA_PATH_DIR_OUTPUT`, transparent coding). diff --git a/examples/bluetooth/esp_ble_iso/big_receiver/main/CMakeLists.txt b/examples/bluetooth/esp_ble_iso/big_receiver/main/CMakeLists.txt index 721967052c7..8b18ad60330 100644 --- a/examples/bluetooth/esp_ble_iso/big_receiver/main/CMakeLists.txt +++ b/examples/bluetooth/esp_ble_iso/big_receiver/main/CMakeLists.txt @@ -1,4 +1,11 @@ set(srcs "main.c") -idf_component_register(SRCS "${srcs}" +if(CONFIG_BT_BLUEDROID_ENABLED) + list(APPEND srcs "bluedroid/scan.c") +else() + list(APPEND srcs "nimble/scan.c") +endif() + +idf_component_register(SRCS ${srcs} + INCLUDE_DIRS "." REQUIRES bt nvs_flash) diff --git a/examples/bluetooth/esp_ble_iso/big_receiver/main/bluedroid/scan.c b/examples/bluetooth/esp_ble_iso/big_receiver/main/bluedroid/scan.c new file mode 100644 index 00000000000..dbb1a94eca2 --- /dev/null +++ b/examples/bluetooth/esp_ble_iso/big_receiver/main/bluedroid/scan.c @@ -0,0 +1,116 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "esp_log.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +#include "esp_bt_defs.h" +#include "esp_gap_ble_api.h" + +#include "esp_ble_iso_common_api.h" + +#include "scan.h" + +static SemaphoreHandle_t scan_sem; + +/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */ +static esp_bt_status_t scan_op_status; + +#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, scan_sem, portMAX_DELAY, scan_op_status) + +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) +{ + /* Data events (EXT_ADV_REPORT / PA_SYNC_ESTAB / PA_SYNC_LOST) are + * posted to the iso task by the lib's bluedroid gap.c directly from + * BTU context, so we only handle BTC-side completion events here. */ + 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; + + default: + break; + } +} + +int app_host_init(void) +{ + esp_err_t err; + + scan_sem = xSemaphoreCreateBinary(); + if (scan_sem == NULL) { + ESP_LOGE(TAG, "Failed to create scan semaphore"); + return -1; + } + + err = esp_ble_gap_register_callback(gap_event_handler); + if (err) { + ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err); + vSemaphoreDelete(scan_sem); + return err; + } + + return 0; +} + +int 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 broadcaster..."); + return 0; +} + +int ext_scan_stop(void) +{ + WAIT_API(esp_ble_gap_stop_ext_scan()); + return 0; +} + +int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid) +{ + esp_ble_gap_periodic_adv_sync_params_t params = { + .filter_policy = 0, + .sid = sid, + .addr_type = addr_type, + .skip = PA_SYNC_SKIP, + .sync_timeout = PA_SYNC_TIMEOUT, + }; + + memcpy(params.addr, addr, sizeof(params.addr)); + + /* Fire-and-forget: sync establishment (PERIODIC_ADV_SYNC_ESTAB_EVT) is + * air-dependent and surfaces asynchronously. */ + return esp_ble_gap_periodic_adv_create_sync(¶ms); +} diff --git a/examples/bluetooth/esp_ble_iso/big_receiver/main/main.c b/examples/bluetooth/esp_ble_iso/big_receiver/main/main.c index 07a57949930..80f2d29e4ce 100644 --- a/examples/bluetooth/esp_ble_iso/big_receiver/main/main.c +++ b/examples/bluetooth/esp_ble_iso/big_receiver/main/main.c @@ -11,28 +11,19 @@ #include "esp_log.h" #include "nvs_flash.h" -#include "esp_system.h" - -#include "host/ble_gap.h" #include "esp_ble_iso_common_api.h" #include "ble_iso_example_init.h" #include "ble_iso_example_utils.h" -#define TAG "BIG_SNC" +#include "scan.h" #define TARGET_DEVICE_NAME "BIG Broadcaster" #define TARGET_DEVICE_NAME_LEN (sizeof(TARGET_DEVICE_NAME) - 1) #define TARGET_BROADCAST_CODE "1234" -#define SCAN_INTERVAL 160 /* 100ms */ -#define SCAN_WINDOW 160 /* 100ms */ - -#define PA_SYNC_SKIP 0 -#define PA_SYNC_TIMEOUT 1000 /* 1000 * 10ms = 10s */ - #define BIS_ISO_CHAN_COUNT 2 /* Use exactly 2 BIS channels */ #define BIG_SYNC_TIMEOUT 100 /* 100 * 10ms = 1s */ @@ -157,46 +148,6 @@ static int bis_chan_index_get(const esp_ble_iso_chan_t *chan) return -1; } -static void ext_scan_start(void) -{ - struct ble_gap_disc_params params = {0}; - uint8_t own_addr_type; - int err; - - err = ble_hs_id_infer_auto(0, &own_addr_type); - if (err) { - ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err); - return; - } - - params.passive = 1; - params.itvl = SCAN_INTERVAL; - params.window = SCAN_WINDOW; - - err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms, - example_iso_gap_event_cb, NULL); - if (err) { - ESP_LOGE(TAG, "Failed to start scanning, err %d", err); - return; - } - - ESP_LOGI(TAG, "Scanning for broadcaster..."); -} - -static int pa_sync_create(uint8_t addr_type, uint8_t addr[6], uint8_t sid) -{ - struct ble_gap_periodic_sync_params params = {0}; - ble_addr_t sync_addr = {0}; - - sync_addr.type = addr_type; - memcpy(sync_addr.val, addr, sizeof(sync_addr.val)); - params.skip = PA_SYNC_SKIP; - params.sync_timeout = PA_SYNC_TIMEOUT; - - return ble_gap_periodic_adv_sync_create(&sync_addr, sid, ¶ms, - example_iso_gap_event_cb, NULL); -} - static bool data_cb(uint8_t type, const uint8_t *data, uint8_t data_len, void *user_data) { @@ -253,15 +204,13 @@ static void pa_sync(esp_ble_iso_gap_app_event_t *event) ESP_LOGI(TAG, "PA synced: handle %u sid %u phy %u peer %02x:%02x:%02x:%02x:%02x:%02x", event->pa_sync.sync_handle, event->pa_sync.sid, event->pa_sync.adv_phy, - event->pa_sync.addr.val[5], event->pa_sync.addr.val[4], - event->pa_sync.addr.val[3], event->pa_sync.addr.val[2], - event->pa_sync.addr.val[1], event->pa_sync.addr.val[0]); + EXAMPLE_BT_ADDR_PRINT_ARGS(event->pa_sync.addr.val)); /* PA sync is established; the BIGInfo report will arrive via the * PA sync channel, so the extended scanner is no longer needed. * Stop it now — pa_sync_lost() will restart it on loss. */ - err = ble_gap_disc_cancel(); + err = ext_scan_stop(); if (err) { ESP_LOGW(TAG, "Failed to stop scanning, err %d", err); } @@ -347,11 +296,21 @@ void app_main(void) return; } + err = app_host_init(); + if (err) { + ESP_LOGE(TAG, "Failed to init host, err %d", err); + return; + } + err = esp_ble_iso_common_init(&info); if (err) { ESP_LOGE(TAG, "Failed to initialize ISO, err %d", err); return; } - ext_scan_start(); + err = ext_scan_start(); + if (err) { + ESP_LOGE(TAG, "Failed to start scan, err %d", err); + return; + } } diff --git a/examples/bluetooth/esp_ble_iso/big_receiver/main/nimble/scan.c b/examples/bluetooth/esp_ble_iso/big_receiver/main/nimble/scan.c new file mode 100644 index 00000000000..7a974ded8b8 --- /dev/null +++ b/examples/bluetooth/esp_ble_iso/big_receiver/main/nimble/scan.c @@ -0,0 +1,84 @@ +/* + * 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 "esp_ble_iso_common_api.h" + +#include "scan.h" + +/* Forward only the GAP events the application consumes. */ +static int gap_event_cb(struct ble_gap_event *event, void *arg) +{ + switch (event->type) { + case BLE_GAP_EVENT_EXT_DISC: + case BLE_GAP_EVENT_PERIODIC_SYNC: + case BLE_GAP_EVENT_PERIODIC_SYNC_LOST: + esp_ble_iso_gap_app_post_event(event->type, event); + break; + default: + break; + } + + return 0; +} + +int app_host_init(void) +{ + return 0; +} + +int ext_scan_start(void) +{ + struct ble_gap_disc_params params = {0}; + uint8_t own_addr_type; + int err; + + err = ble_hs_id_infer_auto(0, &own_addr_type); + if (err) { + ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err); + return err; + } + + params.passive = 1; + params.itvl = SCAN_INTERVAL; + params.window = SCAN_WINDOW; + + err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms, + gap_event_cb, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to start scanning, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Scanning for broadcaster..."); + return 0; +} + +int ext_scan_stop(void) +{ + return ble_gap_disc_cancel(); +} + +int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid) +{ + struct ble_gap_periodic_sync_params params = {0}; + ble_addr_t sync_addr = {0}; + + sync_addr.type = addr_type; + memcpy(sync_addr.val, addr, sizeof(sync_addr.val)); + params.skip = PA_SYNC_SKIP; + params.sync_timeout = PA_SYNC_TIMEOUT; + + return ble_gap_periodic_adv_sync_create(&sync_addr, sid, ¶ms, + gap_event_cb, NULL); +} diff --git a/examples/bluetooth/esp_ble_iso/big_receiver/main/scan.h b/examples/bluetooth/esp_ble_iso/big_receiver/main/scan.h new file mode 100644 index 00000000000..43953368919 --- /dev/null +++ b/examples/bluetooth/esp_ble_iso/big_receiver/main/scan.h @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "ble_iso_example_utils.h" + +#define TAG "BIG_SNC" + +#define SCAN_INTERVAL 160 /* 100ms */ +#define SCAN_WINDOW 160 /* 100ms */ + +#define PA_SYNC_SKIP 0 +#define PA_SYNC_TIMEOUT 1000 /* 1000 * 10ms = 10s */ + +int app_host_init(void); + +int ext_scan_start(void); +int ext_scan_stop(void); + +int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid); diff --git a/examples/bluetooth/esp_ble_iso/big_receiver/sdkconfig.defaults b/examples/bluetooth/esp_ble_iso/big_receiver/sdkconfig.defaults index 022dd160c7a..bb0d86ba91f 100644 --- a/examples/bluetooth/esp_ble_iso/big_receiver/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_iso/big_receiver/sdkconfig.defaults @@ -3,13 +3,17 @@ # CONFIG_BT_ENABLED=y -CONFIG_BT_BLUEDROID_ENABLED=n -CONFIG_BT_NIMBLE_ENABLED=y -CONFIG_BT_NIMBLE_EXT_ADV=y -CONFIG_BT_NIMBLE_ISO=y -CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y +CONFIG_BT_NIMBLE_ENABLED=n +CONFIG_BT_BLUEDROID_ENABLED=y +CONFIG_BT_CLASSIC_ENABLED=n +CONFIG_BT_CONTROLLER_ENABLED=y +CONFIG_BT_BLE_ENABLED=y +CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y +CONFIG_BT_BLE_FEAT_ISO_EN=y CONFIG_BT_ISO_SYNC_RECEIVER=y CONFIG_BT_ISO_MAX_CHAN=2 +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y + CONFIG_FREERTOS_HZ=1000 diff --git a/examples/bluetooth/esp_ble_iso/big_receiver/sdkconfig.defaults.nimble b/examples/bluetooth/esp_ble_iso/big_receiver/sdkconfig.defaults.nimble new file mode 100644 index 00000000000..24402677507 --- /dev/null +++ b/examples/bluetooth/esp_ble_iso/big_receiver/sdkconfig.defaults.nimble @@ -0,0 +1,9 @@ +# 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_ISO=y +CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y diff --git a/examples/bluetooth/esp_ble_iso/cis_central/README.md b/examples/bluetooth/esp_ble_iso/cis_central/README.md index 782a7a06770..47dc7eddd0a 100644 --- a/examples/bluetooth/esp_ble_iso/cis_central/README.md +++ b/examples/bluetooth/esp_ble_iso/cis_central/README.md @@ -7,11 +7,13 @@ ## Overview -This is a raw BLE Connected Isochronous Stream (CIS) example operating directly at the ISO transport layer. It is **not** a BAP/CAP (BLE Audio profile) example — it does not implement Unicast Server/Client, ASCS, PACS, or any LC3 codec; it only exercises the underlying CIG/CIS plumbing. +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 transmitted payload is a dummy buffer filled with the current sequence number byte — there is no real audio data, the example just demonstrates the ISO transport mechanics on top of the NimBLE host with ISO support and the `esp_ble_iso_*` APIs. +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. + +> **Bluedroid host status (phase 1):** scanning and AUTH_CMPL forwarding work; ACL connection initiation and pairing kick-off (`conn_create` / `pairing_start`) currently return `ESP_ERR_NOT_SUPPORTED` and will be wired up against the public Bluedroid APIs in a follow-up. Use the NimBLE overlay for end-to-end runtime testing. ## Requirements @@ -28,20 +30,33 @@ No build-time options — runtime defaults are baked into source. ### Security & Pairing -Just-Works pairing (LE Secure Connections, no MITM, `BLE_SM_IO_CAP_NO_IO`) with bonding enabled, inherited from the shared host init in `../common_components/example_init/ble_iso_example_init.c`. ISO examples do not register any GATT services (`gatts_register_cb = NULL`). +Just-Works pairing (LE Secure Connections, no MITM, IO capability = None) with bonding enabled. On NimBLE the configuration is inherited from the shared host init in `../common_components/example_init/ble_iso_example_init.c`; on Bluedroid the equivalent SMP setup will live in `main/bluedroid/scan.c` once `pairing_start` is implemented. ISO examples do not register any custom GATT services. ## Build & Flash +The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`. + +### Bluedroid host (default) + ```bash idf.py set-target esp32h4 idf.py -p PORT flash monitor ``` +### NimBLE host + +```bash +idf.py set-target esp32h4 +idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor +``` + +For `esp32s31`, replace the chip overlay accordingly. + (Exit serial monitor with `Ctrl-]`.) ## Example Flow -1. Initialize NVS, NimBLE host, and the ISO common layer with a GAP callback. +1. Initialize NVS, the selected BLE host, and the ISO common layer with a GAP callback. 2. Start passive extended scanning for a device whose Complete Local Name is `CIS Peripheral`. 3. Cancel scan and create an ACL connection (interval 80 ms, supervision 5 s) once the target is matched. 4. On ACL connect, initiate pairing because the configured security level is `ESP_BLE_ISO_SECURITY_NO_MITM`. diff --git a/examples/bluetooth/esp_ble_iso/cis_central/main/CMakeLists.txt b/examples/bluetooth/esp_ble_iso/cis_central/main/CMakeLists.txt index 721967052c7..82ba5af67b3 100644 --- a/examples/bluetooth/esp_ble_iso/cis_central/main/CMakeLists.txt +++ b/examples/bluetooth/esp_ble_iso/cis_central/main/CMakeLists.txt @@ -1,4 +1,11 @@ set(srcs "main.c") -idf_component_register(SRCS "${srcs}" +if(CONFIG_BT_BLUEDROID_ENABLED) + list(APPEND srcs "bluedroid/central.c") +else() + list(APPEND srcs "nimble/central.c") +endif() + +idf_component_register(SRCS ${srcs} + INCLUDE_DIRS "." REQUIRES bt nvs_flash) diff --git a/examples/bluetooth/esp_ble_iso/cis_central/main/bluedroid/central.c b/examples/bluetooth/esp_ble_iso/cis_central/main/bluedroid/central.c new file mode 100644 index 00000000000..606e48ba50b --- /dev/null +++ b/examples/bluetooth/esp_ble_iso/cis_central/main/bluedroid/central.c @@ -0,0 +1,184 @@ +/* + * 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_iso_common_api.h" + +#include "central.h" + +static SemaphoreHandle_t scan_sem; + +/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */ +static esp_bt_status_t scan_op_status; + +#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, scan_sem, portMAX_DELAY, scan_op_status) + +/* Cached peer address. Bluedroid's pairing and disconnect APIs key off + * bd_addr rather than conn_handle, so we stash the addr at conn_create + * time and reuse it in pairing_start / security_failed_recover. */ +static esp_bd_addr_t peer_bda; + +static esp_ble_ext_scan_params_t ext_scan_params = { + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE, + .cfg_mask = ESP_BLE_GAP_EXT_SCAN_CFG_UNCODE_MASK, + .uncoded_cfg = { + .scan_type = BLE_SCAN_TYPE_PASSIVE, + .scan_interval = SCAN_INTERVAL, + .scan_window = SCAN_WINDOW, + }, +}; + +static void gap_event_handler(esp_gap_ble_cb_event_t event, + esp_ble_gap_cb_param_t *param) +{ + switch (event) { + case ESP_GAP_BLE_SET_EXT_SCAN_PARAMS_COMPLETE_EVT: + scan_op_status = param->set_ext_scan_params.status; + xSemaphoreGive(scan_sem); + break; + case ESP_GAP_BLE_EXT_SCAN_START_COMPLETE_EVT: + scan_op_status = param->ext_scan_start.status; + xSemaphoreGive(scan_sem); + break; + case ESP_GAP_BLE_EXT_SCAN_STOP_COMPLETE_EVT: + scan_op_status = param->ext_scan_stop.status; + xSemaphoreGive(scan_sem); + break; + + /* SMP request handling for Just Works pairing (IO_CAP=NONE). NC_REQ + * still fires under LE Secure Connections — auto-accept. We never get + * SEC_REQ here because the central initiates via esp_ble_set_encryption. */ + case ESP_GAP_BLE_NC_REQ_EVT: + esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, true); + break; + + case ESP_GAP_BLE_AUTH_CMPL_EVT: + esp_ble_iso_gap_app_post_event(event, param); + break; + + default: + break; + } +} + +int app_host_init(void) +{ + esp_err_t err; + + scan_sem = xSemaphoreCreateBinary(); + if (scan_sem == NULL) { + ESP_LOGE(TAG, "Failed to create scan semaphore"); + return -1; + } + + err = esp_ble_gap_register_callback(gap_event_handler); + if (err) { + ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err); + vSemaphoreDelete(scan_sem); + return err; + } + + return 0; +} + +int set_device_name(void) +{ + return esp_ble_gap_set_device_name(LOCAL_DEVICE_NAME); +} + +int ext_scan_start(void) +{ + WAIT_API(esp_ble_gap_set_ext_scan_params(&ext_scan_params)); + WAIT_API(esp_ble_gap_start_ext_scan(0, 0)); + + ESP_LOGI(TAG, "Scanning for peripheral..."); + return 0; +} + +int ext_scan_stop(void) +{ + WAIT_API(esp_ble_gap_stop_ext_scan()); + return 0; +} + +int conn_create(uint8_t addr_type, const uint8_t addr[6]) +{ + /* Values shared with NimBLE side via central.h for behavioral parity. + * Without prefer_ext_connect_params_set, L2CAP falls back to defaults + * and logs "No extend connection parameters set". */ + const esp_ble_gap_conn_params_t conn_params = { + .scan_interval = INIT_SCAN_INTERVAL, + .scan_window = INIT_SCAN_WINDOW, + .interval_min = CONN_INTERVAL, + .interval_max = CONN_INTERVAL, + .latency = CONN_LATENCY, + .supervision_timeout = CONN_TIMEOUT, + .min_ce_len = CONN_MIN_CE_LEN, + .max_ce_len = CONN_MAX_CE_LEN, + }; + esp_gatt_if_t gattc_if; + esp_err_t err; + + /* `addr` is in BD_ADDR (Bluedroid native, MSB-first) — direct copy. */ + 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; + } + + /* aux_open initiates an ACL against an extended advertiser. Use the + * engine's gattc_if so the resulting CONNECT/OPEN events route back to + * the engine — no separate BTA GATTC app needed. + * + * 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_iso_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); +} + +void security_failed_recover(uint16_t conn_handle, uint8_t status) +{ + (void)conn_handle; + + /* Asymmetric bond state: we still hold an LTK for this peer but it + * cleared its side, so encrypt-with-cached-key times out. Drop the bond + * and tear down the link; the next reconnect runs fresh pairing. Same + * reasoning as the NimBLE side. */ + 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_iso/cis_central/main/central.h b/examples/bluetooth/esp_ble_iso/cis_central/main/central.h new file mode 100644 index 00000000000..720c9ab95b7 --- /dev/null +++ b/examples/bluetooth/esp_ble_iso/cis_central/main/central.h @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "ble_iso_example_utils.h" + +#define TAG "CIS_CEN" + +#define LOCAL_DEVICE_NAME "CIS Central" + +#define SCAN_INTERVAL 160 /* 100ms */ +#define SCAN_WINDOW 160 /* 100ms */ + +/* ACL init parameters shared between bluedroid and nimble host wrappers. + * Raw HCI units (scan: 0.625ms; conn interval: 1.25ms; timeout: 10ms). */ +#define INIT_SCAN_INTERVAL 16 /* 10ms */ +#define INIT_SCAN_WINDOW 16 /* 10ms */ +#define CONN_INTERVAL 24 /* 30ms */ +#define CONN_LATENCY 0 +#define CONN_TIMEOUT 500 /* 5s */ +#define CONN_MIN_CE_LEN 0xFFFF +#define CONN_MAX_CE_LEN 0xFFFF + +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); + +/* Recover from an SMP failure on the given connection — typically deletes the + * stale bond and terminates the link so the next reconnect runs fresh + * pairing. Host-specific implementation. */ +void security_failed_recover(uint16_t conn_handle, uint8_t status); 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 23a0f2e422e..423faf1eca6 100644 --- a/examples/bluetooth/esp_ble_iso/cis_central/main/main.c +++ b/examples/bluetooth/esp_ble_iso/cis_central/main/main.c @@ -10,36 +10,17 @@ #include "esp_log.h" #include "nvs_flash.h" -#include "esp_system.h" -#include "esp_timer.h" - -#include "host/ble_gap.h" -#include "services/gap/ble_svc_gap.h" #include "esp_ble_iso_common_api.h" #include "ble_iso_example_init.h" #include "ble_iso_example_utils.h" -#define TAG "CIS_CEN" - -#define LOCAL_DEVICE_NAME "CIS Central" +#include "central.h" #define TARGET_DEVICE_NAME "CIS Peripheral" #define TARGET_DEVICE_NAME_LEN (sizeof(TARGET_DEVICE_NAME) - 1) -#define SCAN_INTERVAL 160 /* 100ms */ -#define SCAN_WINDOW 160 /* 100ms */ - -#define INIT_SCAN_INTERVAL 16 /* 10ms */ -#define INIT_SCAN_WINDOW 16 /* 10ms */ -#define CONN_INTERVAL 64 /* 64 * 1.25 = 80ms */ -#define CONN_LATENCY 0 -#define CONN_TIMEOUT 500 /* 500 * 10ms = 5s */ -#define CONN_MAX_CE_LEN 0xFFFF -#define CONN_MIN_CE_LEN 0xFFFF -#define CONN_DURATION 10000 /* 10s */ - #define SECURITY_LEVEL ESP_BLE_ISO_SECURITY_NO_MITM #define CIG_LATENCY_MS 10 /* 10ms */ @@ -205,72 +186,6 @@ static void tx_scheduler_cb(void *arg) iso_chan_send(); } -static void ext_scan_start(void) -{ - struct ble_gap_disc_params params = {0}; - uint8_t own_addr_type; - int err; - - err = ble_hs_id_infer_auto(0, &own_addr_type); - if (err) { - ESP_LOGE(TAG, "Failed to determine address type, err %d", err); - return; - } - - params.passive = 1; - params.itvl = SCAN_INTERVAL; - params.window = SCAN_WINDOW; - - err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms, - example_iso_gap_event_cb, NULL); - if (err) { - ESP_LOGE(TAG, "Failed to start scanning, err %d", err); - return; - } - - ESP_LOGI(TAG, "Scanning for peripheral..."); -} - -static int conn_create(uint8_t addr_type, uint8_t addr[6]) -{ - struct ble_gap_conn_params params = {0}; - uint8_t own_addr_type = 0; - ble_addr_t dst = {0}; - int err; - - err = ble_gap_disc_cancel(); - if (err) { - ESP_LOGE(TAG, "Failed to stop scanning, err %d", err); - return err; - } - - err = ble_hs_id_infer_auto(0, &own_addr_type); - if (err) { - ESP_LOGE(TAG, "Failed to determine address type, err %d", err); - return err; - } - - params.scan_itvl = INIT_SCAN_INTERVAL; - params.scan_window = INIT_SCAN_WINDOW; - params.itvl_min = CONN_INTERVAL; - params.itvl_max = CONN_INTERVAL; - params.latency = CONN_LATENCY; - params.supervision_timeout = CONN_TIMEOUT; - params.max_ce_len = CONN_MAX_CE_LEN; - params.min_ce_len = CONN_MIN_CE_LEN; - - dst.type = addr_type; - memcpy(dst.val, addr, sizeof(dst.val)); - - return ble_gap_connect(own_addr_type, &dst, CONN_DURATION, ¶ms, - example_iso_gap_event_cb, NULL); -} - -static int pairing_start(uint16_t conn_handle) -{ - return ble_gap_security_initiate(conn_handle); -} - static bool data_cb(uint8_t type, const uint8_t *data, uint8_t data_len, void *user_data) { @@ -302,9 +217,19 @@ static void ext_scan_recv(esp_ble_iso_gap_app_event_t *event) return; } + err = ext_scan_stop(); + if (err) { + ESP_LOGE(TAG, "Failed to stop scanning, err %d", err); + return; + } + err = conn_create(event->ext_scan_recv.addr.type, event->ext_scan_recv.addr.val); if (err) { ESP_LOGE(TAG, "Failed to create connection, err %d", err); + /* Scan was stopped above. acl_disconnect (which restarts scan) only + * fires on an established connection — resume here to avoid a + * no-scan / no-conn dead state. */ + ext_scan_start(); return; } @@ -318,14 +243,15 @@ static void acl_connect(esp_ble_iso_gap_app_event_t *event) if (event->acl_connect.status) { ESP_LOGE(TAG, "Connection failed, status %d", event->acl_connect.status); acl_connected = false; + /* Async failure: acl_disconnect won't fire (never established), + * so restart scanning here to avoid a no-scan / no-conn dead state. */ + ext_scan_start(); return; } ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x", event->acl_connect.conn_handle, event->acl_connect.role, - event->acl_connect.dst.val[5], event->acl_connect.dst.val[4], - event->acl_connect.dst.val[3], event->acl_connect.dst.val[2], - event->acl_connect.dst.val[1], event->acl_connect.dst.val[0]); + EXAMPLE_BT_ADDR_PRINT_ARGS(event->acl_connect.dst.val)); if (iso_chan.required_sec_level == ESP_BLE_ISO_SECURITY_NO_MITM || iso_chan.required_sec_level == ESP_BLE_ISO_SECURITY_MITM) { @@ -352,8 +278,8 @@ static void acl_disconnect(esp_ble_iso_gap_app_event_t *event) static void security_change(esp_ble_iso_gap_app_event_t *event) { if (event->security_change.status) { - example_iso_security_failed_recover(TAG, event->security_change.conn_handle, - event->security_change.status); + security_failed_recover(event->security_change.conn_handle, + event->security_change.status); return; } @@ -406,15 +332,15 @@ void app_main(void) return; } - err = esp_ble_iso_common_init(&info); + err = app_host_init(); if (err) { - ESP_LOGE(TAG, "Failed to initialize ISO, err %d", err); + ESP_LOGE(TAG, "Failed to init host, err %d", err); return; } - err = ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME); + err = esp_ble_iso_common_init(&info); if (err) { - ESP_LOGE(TAG, "Failed to set device name, err %d", err); + ESP_LOGE(TAG, "Failed to initialize ISO, err %d", err); return; } @@ -426,5 +352,15 @@ void app_main(void) return; } - ext_scan_start(); + err = set_device_name(); + if (err) { + ESP_LOGE(TAG, "Failed to set device name, err %d", err); + return; + } + + err = ext_scan_start(); + if (err) { + ESP_LOGE(TAG, "Failed to start scan, err %d", err); + return; + } } diff --git a/examples/bluetooth/esp_ble_iso/cis_central/main/nimble/central.c b/examples/bluetooth/esp_ble_iso/cis_central/main/nimble/central.c new file mode 100644 index 00000000000..083f1a1e11b --- /dev/null +++ b/examples/bluetooth/esp_ble_iso/cis_central/main/nimble/central.c @@ -0,0 +1,150 @@ +/* + * 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_iso_common_api.h" + +#include "central.h" + +/* Init/conn parameters are shared with the bluedroid wrapper via central.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_iso_gap_app_post_event(event->type, event); + break; + case BLE_GAP_EVENT_REPEAT_PAIRING: { + /* Peer wants to re-pair on top of an existing bond — delete our stale + * bond entry and tell NimBLE to retry pairing. */ + struct ble_gap_conn_desc desc = {0}; + int rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc); + if (rc == 0) { + ble_store_util_delete_peer(&desc.peer_id_addr); + } + return BLE_GAP_REPEAT_PAIRING_RETRY; + } + default: + break; + } + + return 0; +} + +int app_host_init(void) +{ + return 0; +} + +int set_device_name(void) +{ + return ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME); +} + +int ext_scan_start(void) +{ + struct ble_gap_disc_params params = {0}; + uint8_t own_addr_type; + int err; + + err = ble_hs_id_infer_auto(0, &own_addr_type); + if (err) { + ESP_LOGE(TAG, "Failed to determine address type, err %d", err); + return err; + } + + params.passive = 1; + params.itvl = SCAN_INTERVAL; + params.window = SCAN_WINDOW; + + err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms, + gap_event_cb, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to start scanning, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Scanning for peripheral..."); + return 0; +} + +int ext_scan_stop(void) +{ + return ble_gap_disc_cancel(); +} + +int conn_create(uint8_t addr_type, const uint8_t addr[6]) +{ + struct ble_gap_conn_params params = {0}; + uint8_t own_addr_type = 0; + ble_addr_t dst = {0}; + int err; + + err = ble_hs_id_infer_auto(0, &own_addr_type); + if (err) { + ESP_LOGE(TAG, "Failed to determine address type, err %d", err); + return err; + } + + params.scan_itvl = INIT_SCAN_INTERVAL; + params.scan_window = INIT_SCAN_WINDOW; + params.itvl_min = CONN_INTERVAL; + params.itvl_max = CONN_INTERVAL; + params.latency = CONN_LATENCY; + params.supervision_timeout = CONN_TIMEOUT; + params.max_ce_len = CONN_MAX_CE_LEN; + params.min_ce_len = CONN_MIN_CE_LEN; + + dst.type = addr_type; + memcpy(dst.val, addr, sizeof(dst.val)); + + return ble_gap_connect(own_addr_type, &dst, CONN_DURATION, + ¶ms, gap_event_cb, NULL); +} + +int pairing_start(uint16_t conn_handle) +{ + return ble_gap_security_initiate(conn_handle); +} + +void security_failed_recover(uint16_t conn_handle, uint8_t status) +{ + struct ble_gap_conn_desc desc = {0}; + int rc; + + /* status 13 = BLE_HS_ETIMEOUT: SMP exchange did not complete. Typical + * cause is asymmetric bond state — the two sides bonded previously, + * then one side erased its NVS while the other kept the LTK. The side + * that still has the bond tries encryption with the cached key; the + * other side has nothing to match it against, so SMP times out. Drop + * our entry for this peer and disconnect; the next connection runs + * fresh pairing (peer side recovers via REPEAT_PAIRING). */ + ESP_LOGE(TAG, "Security change failed, status %u, clearing local bond and reconnecting", status); + + rc = ble_gap_conn_find(conn_handle, &desc); + if (rc == 0) { + ble_store_util_delete_peer(&desc.peer_id_addr); + } + + /* Tear down the link: SMP cannot be retried on the same connection + * after a failed/timed-out exchange. The next reconnect starts a + * fresh SMP procedure with the now-cleared bond state. */ + ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM); +} diff --git a/examples/bluetooth/esp_ble_iso/cis_central/sdkconfig.defaults b/examples/bluetooth/esp_ble_iso/cis_central/sdkconfig.defaults index 4a31aac41b7..6da20d7e9c1 100644 --- a/examples/bluetooth/esp_ble_iso/cis_central/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_iso/cis_central/sdkconfig.defaults @@ -3,14 +3,16 @@ # CONFIG_BT_ENABLED=y -CONFIG_BT_BLUEDROID_ENABLED=n -CONFIG_BT_NIMBLE_ENABLED=y -CONFIG_BT_NIMBLE_EXT_ADV=y -CONFIG_BT_NIMBLE_NVS_PERSIST=y -CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 -CONFIG_BT_NIMBLE_ISO=y -CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y +CONFIG_BT_NIMBLE_ENABLED=n +CONFIG_BT_BLUEDROID_ENABLED=y +CONFIG_BT_CLASSIC_ENABLED=n +CONFIG_BT_CONTROLLER_ENABLED=y +CONFIG_BT_BLE_ENABLED=y +CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y +CONFIG_BT_BLE_FEAT_ISO_EN=y CONFIG_BT_ISO_CENTRAL=y +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y + CONFIG_FREERTOS_HZ=1000 diff --git a/examples/bluetooth/esp_ble_iso/cis_central/sdkconfig.defaults.nimble b/examples/bluetooth/esp_ble_iso/cis_central/sdkconfig.defaults.nimble new file mode 100644 index 00000000000..c74ae1a0d95 --- /dev/null +++ b/examples/bluetooth/esp_ble_iso/cis_central/sdkconfig.defaults.nimble @@ -0,0 +1,11 @@ +# 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_ISO=y +CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y diff --git a/examples/bluetooth/esp_ble_iso/cis_peripheral/README.md b/examples/bluetooth/esp_ble_iso/cis_peripheral/README.md index a3816d29c08..d1da73f0924 100644 --- a/examples/bluetooth/esp_ble_iso/cis_peripheral/README.md +++ b/examples/bluetooth/esp_ble_iso/cis_peripheral/README.md @@ -7,11 +7,11 @@ ## Overview -This is a raw BLE Connected Isochronous Stream (CIS) example operating directly at the ISO transport layer. It is **not** a BAP/CAP (BLE Audio profile) example — it does not implement Unicast Server, ASCS, PACS, or any LC3 codec; it only exercises the underlying CIS accept/receive plumbing. +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. -There is no audio decoding — incoming SDUs are simply counted (valid / error / lost / null), so any 120-byte dummy payload from the central is consumed as opaque data by the NimBLE host with ISO support and the `esp_ble_iso_*` APIs. +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. ## Requirements @@ -28,20 +28,33 @@ No build-time options — runtime defaults are baked into source. ### Security & Pairing -Just-Works pairing (LE Secure Connections, no MITM, `BLE_SM_IO_CAP_NO_IO`) with bonding enabled, inherited from the shared host init in `../common_components/example_init/ble_iso_example_init.c`. ISO examples do not register any GATT services (`gatts_register_cb = NULL`). +Just-Works pairing (LE Secure Connections, no MITM, IO capability = None) with bonding enabled. The configuration is set up in the shared host init `../common_components/example_init/ble_iso_example_init.c` for both NimBLE (`ble_hs_cfg.sm_*`) and Bluedroid (`esp_ble_gap_set_security_param()`). ISO examples do not register any custom GATT services. ## Build & Flash +The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`. + +### Bluedroid host (default) + ```bash idf.py set-target esp32h4 idf.py -p PORT flash monitor ``` +### NimBLE host + +```bash +idf.py set-target esp32h4 +idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor +``` + +For `esp32s31`, replace the chip overlay accordingly. + (Exit serial monitor with `Ctrl-]`.) ## Example Flow -1. Initialize NVS, NimBLE host, and the ISO common layer with a GAP callback. +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. 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. diff --git a/examples/bluetooth/esp_ble_iso/cis_peripheral/main/CMakeLists.txt b/examples/bluetooth/esp_ble_iso/cis_peripheral/main/CMakeLists.txt index 721967052c7..466446747c8 100644 --- a/examples/bluetooth/esp_ble_iso/cis_peripheral/main/CMakeLists.txt +++ b/examples/bluetooth/esp_ble_iso/cis_peripheral/main/CMakeLists.txt @@ -1,4 +1,11 @@ set(srcs "main.c") -idf_component_register(SRCS "${srcs}" +if(CONFIG_BT_BLUEDROID_ENABLED) + list(APPEND srcs "bluedroid/peripheral.c") +else() + list(APPEND srcs "nimble/peripheral.c") +endif() + +idf_component_register(SRCS ${srcs} + INCLUDE_DIRS "." REQUIRES bt nvs_flash) diff --git a/examples/bluetooth/esp_ble_iso/cis_peripheral/main/bluedroid/peripheral.c b/examples/bluetooth/esp_ble_iso/cis_peripheral/main/bluedroid/peripheral.c new file mode 100644 index 00000000000..4aeed15e85f --- /dev/null +++ b/examples/bluetooth/esp_ble_iso/cis_peripheral/main/bluedroid/peripheral.c @@ -0,0 +1,118 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "esp_log.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +#include "esp_bt_defs.h" +#include "esp_gap_ble_api.h" + +#include "esp_ble_iso_common_api.h" + +#include "peripheral.h" + +static SemaphoreHandle_t adv_sem; + +/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */ +static esp_bt_status_t adv_op_status; + +#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, adv_sem, portMAX_DELAY, adv_op_status) + +static esp_ble_gap_ext_adv_params_t ext_adv_params = { + .type = ESP_BLE_GAP_SET_EXT_ADV_PROP_CONNECTABLE, + .interval_min = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS), + .interval_max = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS), + .channel_map = ADV_CHNL_ALL, + .filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, + .primary_phy = ESP_BLE_GAP_PHY_1M, + .max_skip = 0, + .secondary_phy = ESP_BLE_GAP_PHY_2M, + .sid = ADV_SID, + .scan_req_notif = false, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .tx_power = ADV_TX_POWER, +}; + +static esp_ble_gap_ext_adv_t ext_adv_inst[1] = { + [0] = { ADV_HANDLE, 0, 0 }, +}; + +static void gap_event_handler(esp_gap_ble_cb_event_t event, + esp_ble_gap_cb_param_t *param) +{ + switch (event) { + 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; + + /* SMP request handling for Just Works pairing (IO_CAP=NONE). The peer + * (central) initiates; we accept the security request and confirm the + * numeric comparison. */ + case ESP_GAP_BLE_SEC_REQ_EVT: + esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true); + break; + case ESP_GAP_BLE_NC_REQ_EVT: + esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, true); + break; + + /* Forward to iso engine — translates to BT_LE_GAP_APP_EVENT_SECURITY_CHANGE + * which the application's iso_gap_app_cb consumes. */ + case ESP_GAP_BLE_AUTH_CMPL_EVT: + esp_ble_iso_gap_app_post_event(event, param); + break; + + default: + break; + } +} + +int app_host_init(void) +{ + esp_err_t err; + + adv_sem = xSemaphoreCreateBinary(); + if (adv_sem == NULL) { + ESP_LOGE(TAG, "Failed to create adv semaphore"); + return -1; + } + + err = esp_ble_gap_register_callback(gap_event_handler); + if (err) { + ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err); + vSemaphoreDelete(adv_sem); + return err; + } + + return 0; +} + +int set_device_name(void) +{ + return esp_ble_gap_set_device_name(LOCAL_DEVICE_NAME); +} + +int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len) +{ + WAIT_API(esp_ble_gap_ext_adv_set_params(ADV_HANDLE, &ext_adv_params)); + WAIT_API(esp_ble_gap_config_ext_adv_data_raw(ADV_HANDLE, ext_len, ext_data)); + WAIT_API(esp_ble_gap_ext_adv_start(1, ext_adv_inst)); + + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); + return 0; +} 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 2e704aa9484..eeb0f80b95c 100644 --- a/examples/bluetooth/esp_ble_iso/cis_peripheral/main/main.c +++ b/examples/bluetooth/esp_ble_iso/cis_peripheral/main/main.c @@ -7,33 +7,17 @@ #include #include -#include #include #include "esp_log.h" #include "nvs_flash.h" -#include "esp_system.h" - -#include "host/ble_gap.h" -#include "services/gap/ble_svc_gap.h" #include "esp_ble_iso_common_api.h" #include "ble_iso_example_init.h" #include "ble_iso_example_utils.h" -#define TAG "CIS_PER" - -#define LOCAL_DEVICE_NAME "CIS Peripheral" -#define LOCAL_DEVICE_NAME_LEN (sizeof(LOCAL_DEVICE_NAME) - 1) - -#define ADV_HANDLE 0 -#define ADV_SID 0 -#define ADV_TX_POWER 127 -#define ADV_ADDRESS BLE_OWN_ADDR_PUBLIC -#define ADV_PRIMARY_PHY BLE_HCI_LE_PHY_1M -#define ADV_SECONDARY_PHY BLE_HCI_LE_PHY_2M -#define ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(200) +#include "peripheral.h" #define SECURITY_LEVEL ESP_BLE_ISO_SECURITY_NO_MITM @@ -115,70 +99,6 @@ static esp_ble_iso_server_t iso_server = { .accept = iso_accept, }; -static void build_adv_data(void) -{ - ext_adv_data[0] = 0x02; - ext_adv_data[1] = EXAMPLE_AD_TYPE_FLAGS; - ext_adv_data[2] = EXAMPLE_AD_FLAGS_GENERAL | EXAMPLE_AD_FLAGS_NO_BREDR; - ext_adv_data[3] = (uint8_t)(LOCAL_DEVICE_NAME_LEN + 1); - ext_adv_data[4] = EXAMPLE_AD_TYPE_NAME_COMPLETE; - memcpy(&ext_adv_data[5], LOCAL_DEVICE_NAME, LOCAL_DEVICE_NAME_LEN); -} - -static void ext_adv_start(void) -{ - struct ble_gap_ext_adv_params ext_params = {0}; - struct os_mbuf *data = NULL; - int err; - - build_adv_data(); - - ext_params.connectable = 1; - ext_params.scannable = 0; - ext_params.legacy_pdu = 0; - ext_params.own_addr_type = ADV_ADDRESS; - ext_params.primary_phy = ADV_PRIMARY_PHY; - ext_params.secondary_phy = ADV_SECONDARY_PHY; - ext_params.tx_power = ADV_TX_POWER; - ext_params.sid = ADV_SID; - ext_params.itvl_min = ADV_INTERVAL; - ext_params.itvl_max = ADV_INTERVAL; - - err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL, - example_iso_gap_event_cb, NULL); - if (err) { - ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err); - return; - } - - data = os_msys_get_pkthdr(sizeof(ext_adv_data), 0); - if (data == NULL) { - ESP_LOGE(TAG, "Failed to get ext adv mbuf"); - return; - } - - err = os_mbuf_append(data, ext_adv_data, sizeof(ext_adv_data)); - if (err) { - ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err); - os_mbuf_free_chain(data); - return; - } - - err = ble_gap_ext_adv_set_data(ADV_HANDLE, data); - if (err) { - ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err); - return; - } - - err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0); - if (err) { - ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err); - return; - } - - ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); -} - static void acl_connect(esp_ble_iso_gap_app_event_t *event) { if (event->acl_connect.status) { @@ -188,9 +108,7 @@ static void acl_connect(esp_ble_iso_gap_app_event_t *event) ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x", event->acl_connect.conn_handle, event->acl_connect.role, - event->acl_connect.dst.val[5], event->acl_connect.dst.val[4], - event->acl_connect.dst.val[3], event->acl_connect.dst.val[2], - event->acl_connect.dst.val[1], event->acl_connect.dst.val[0]); + EXAMPLE_BT_ADDR_PRINT_ARGS(event->acl_connect.dst.val)); } static void acl_disconnect(esp_ble_iso_gap_app_event_t *event) @@ -198,7 +116,7 @@ static void acl_disconnect(esp_ble_iso_gap_app_event_t *event) ESP_LOGI(TAG, "Disconnected: handle %u reason 0x%02x", event->acl_disconnect.conn_handle, event->acl_disconnect.reason); - ext_adv_start(); + ext_adv_start(ext_adv_data, sizeof(ext_adv_data)); } static void security_change(esp_ble_iso_gap_app_event_t *event) @@ -231,6 +149,16 @@ static void iso_gap_app_cb(esp_ble_iso_gap_app_event_t *event) } } +static void build_ext_adv_data(void) +{ + ext_adv_data[0] = 0x02; + ext_adv_data[1] = EXAMPLE_AD_TYPE_FLAGS; + ext_adv_data[2] = EXAMPLE_AD_FLAGS_GENERAL | EXAMPLE_AD_FLAGS_NO_BREDR; + ext_adv_data[3] = (uint8_t)(LOCAL_DEVICE_NAME_LEN + 1); /* AD type + name */ + ext_adv_data[4] = EXAMPLE_AD_TYPE_NAME_COMPLETE; + memcpy(&ext_adv_data[5], LOCAL_DEVICE_NAME, LOCAL_DEVICE_NAME_LEN); +} + void app_main(void) { esp_ble_iso_init_info_t info = { @@ -252,6 +180,12 @@ void app_main(void) return; } + err = app_host_init(); + if (err) { + ESP_LOGE(TAG, "Failed to init host, err %d", err); + return; + } + err = esp_ble_iso_common_init(&info); if (err) { ESP_LOGE(TAG, "Failed to initialize ISO, err %d", err); @@ -264,11 +198,13 @@ void app_main(void) return; } - err = ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME); + err = set_device_name(); if (err) { ESP_LOGE(TAG, "Failed to set device name, err %d", err); return; } - ext_adv_start(); + build_ext_adv_data(); + + ext_adv_start(ext_adv_data, sizeof(ext_adv_data)); } diff --git a/examples/bluetooth/esp_ble_iso/cis_peripheral/main/nimble/peripheral.c b/examples/bluetooth/esp_ble_iso/cis_peripheral/main/nimble/peripheral.c new file mode 100644 index 00000000000..d1b6d728253 --- /dev/null +++ b/examples/bluetooth/esp_ble_iso/cis_peripheral/main/nimble/peripheral.c @@ -0,0 +1,108 @@ +/* + * 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_store.h" +#include "services/gap/ble_svc_gap.h" + +#include "esp_ble_iso_common_api.h" + +#include "peripheral.h" + +static int gap_event_cb(struct ble_gap_event *event, void *arg) +{ + switch (event->type) { + case BLE_GAP_EVENT_CONNECT: + case BLE_GAP_EVENT_DISCONNECT: + case BLE_GAP_EVENT_ENC_CHANGE: + esp_ble_iso_gap_app_post_event(event->type, event); + break; + case BLE_GAP_EVENT_REPEAT_PAIRING: { + /* Peer wants to re-pair on top of an existing bond — delete our stale + * bond entry and tell NimBLE to retry pairing. */ + struct ble_gap_conn_desc desc = {0}; + int rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc); + if (rc == 0) { + ble_store_util_delete_peer(&desc.peer_id_addr); + } + return BLE_GAP_REPEAT_PAIRING_RETRY; + } + default: + break; + } + + return 0; +} + +int app_host_init(void) +{ + return 0; +} + +int set_device_name(void) +{ + return ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME); +} + +int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len) +{ + struct ble_gap_ext_adv_params ext_params = {0}; + struct os_mbuf *data = NULL; + int err; + + ext_params.connectable = 1; + ext_params.scannable = 0; + ext_params.legacy_pdu = 0; + ext_params.own_addr_type = BLE_OWN_ADDR_PUBLIC; + ext_params.primary_phy = BLE_HCI_LE_PHY_1M; + ext_params.secondary_phy = BLE_HCI_LE_PHY_2M; + ext_params.tx_power = ADV_TX_POWER; + ext_params.sid = ADV_SID; + ext_params.itvl_min = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS); + ext_params.itvl_max = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS); + + err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL, + gap_event_cb, NULL); + if (err) { + ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err); + return err; + } + + data = os_msys_get_pkthdr(ext_len, 0); + if (data == NULL) { + ESP_LOGE(TAG, "Failed to get ext adv mbuf"); + return -1; + } + + err = os_mbuf_append(data, ext_data, ext_len); + if (err) { + ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err); + os_mbuf_free_chain(data); + return err; + } + + err = ble_gap_ext_adv_set_data(ADV_HANDLE, data); + if (err) { + ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err); + /* ble_gap_ext_adv_set_data takes ownership of `data` and frees it via + * its 'done' label on both success and failure paths — do NOT free + * here (double-free). API header doesn't document this contract. */ + return err; + } + + err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0); + if (err) { + ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err); + return err; + } + + ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE); + return 0; +} diff --git a/examples/bluetooth/esp_ble_iso/cis_peripheral/main/peripheral.h b/examples/bluetooth/esp_ble_iso/cis_peripheral/main/peripheral.h new file mode 100644 index 00000000000..90cba83a52b --- /dev/null +++ b/examples/bluetooth/esp_ble_iso/cis_peripheral/main/peripheral.h @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "ble_iso_example_utils.h" + +#define TAG "CIS_PER" + +#define LOCAL_DEVICE_NAME "CIS Peripheral" +#define LOCAL_DEVICE_NAME_LEN (sizeof(LOCAL_DEVICE_NAME) - 1) + +#define ADV_HANDLE 0 +#define ADV_SID 0 +#define ADV_TX_POWER 127 +#define ADV_INTERVAL_MS 200 + +int app_host_init(void); + +int set_device_name(void); + +int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len); diff --git a/examples/bluetooth/esp_ble_iso/cis_peripheral/sdkconfig.defaults b/examples/bluetooth/esp_ble_iso/cis_peripheral/sdkconfig.defaults index ada2e5c53eb..b412ca65563 100644 --- a/examples/bluetooth/esp_ble_iso/cis_peripheral/sdkconfig.defaults +++ b/examples/bluetooth/esp_ble_iso/cis_peripheral/sdkconfig.defaults @@ -3,14 +3,16 @@ # CONFIG_BT_ENABLED=y -CONFIG_BT_BLUEDROID_ENABLED=n -CONFIG_BT_NIMBLE_ENABLED=y -CONFIG_BT_NIMBLE_EXT_ADV=y -CONFIG_BT_NIMBLE_NVS_PERSIST=y -CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 -CONFIG_BT_NIMBLE_ISO=y -CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y +CONFIG_BT_NIMBLE_ENABLED=n +CONFIG_BT_BLUEDROID_ENABLED=y +CONFIG_BT_CLASSIC_ENABLED=n +CONFIG_BT_CONTROLLER_ENABLED=y +CONFIG_BT_BLE_ENABLED=y +CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y +CONFIG_BT_BLE_FEAT_ISO_EN=y CONFIG_BT_ISO_PERIPHERAL=y +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y + CONFIG_FREERTOS_HZ=1000 diff --git a/examples/bluetooth/esp_ble_iso/cis_peripheral/sdkconfig.defaults.nimble b/examples/bluetooth/esp_ble_iso/cis_peripheral/sdkconfig.defaults.nimble new file mode 100644 index 00000000000..c74ae1a0d95 --- /dev/null +++ b/examples/bluetooth/esp_ble_iso/cis_peripheral/sdkconfig.defaults.nimble @@ -0,0 +1,11 @@ +# 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_ISO=y +CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y diff --git a/examples/bluetooth/esp_ble_iso/common_components/example_init/ble_iso_example_init.c b/examples/bluetooth/esp_ble_iso/common_components/example_init/ble_iso_example_init.c index f63b3fc0558..15b88908984 100644 --- a/examples/bluetooth/esp_ble_iso/common_components/example_init/ble_iso_example_init.c +++ b/examples/bluetooth/esp_ble_iso/common_components/example_init/ble_iso_example_init.c @@ -4,16 +4,21 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include -#include +#include "sdkconfig.h" #include "esp_log.h" -#include "sdkconfig.h" #if CONFIG_BLE_LOG_ENABLED #include "ble_log.h" #endif +#ifdef CONFIG_BT_BLUEDROID_ENABLED +#include "esp_bt.h" +#include "esp_bt_main.h" +#include "esp_gap_ble_api.h" +#endif /* CONFIG_BT_BLUEDROID_ENABLED */ + +#ifdef CONFIG_BT_NIMBLE_ENABLED #include "nimble/nimble_port.h" #include "nimble/nimble_port_freertos.h" @@ -24,9 +29,109 @@ #include "services/gap/ble_svc_gap.h" #include "services/gatt/ble_svc_gatt.h" +#endif /* CONFIG_BT_NIMBLE_ENABLED */ + +#include "ble_iso_example_init.h" #define TAG "EXAMPLE_INIT" +#ifdef CONFIG_BT_BLUEDROID_ENABLED +esp_err_t bluetooth_init(void) +{ + esp_err_t ret; + + /* When controller log is disabled (or running in v1 mode), the + * automatic ble_log_init() inside esp_bt_controller_init() is skipped. + * Init manually here so HOST/ISO/AUDIO compressed logs aren't dropped + * during host bring-up. The function is idempotent. + * + * TODO: Remove this block (and the ble_log_flush() below) once + * ble_log_init() is decoupled from the controller log path and + * owns its own initialization independently. */ +#if CONFIG_BLE_LOG_ENABLED && \ + !(CONFIG_BT_LE_CONTROLLER_LOG_ENABLED && CONFIG_BT_LE_CONTROLLER_LOG_MODE_BLE_LOG_V2) + if (!ble_log_init()) { + ESP_LOGE(TAG, "Failed to init ble_log"); + return ESP_FAIL; + } +#endif + + ret = esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); + if (ret && ret != ESP_ERR_INVALID_STATE) { + ESP_LOGE(TAG, "Failed to release classic BT memory, err %d", ret); + goto err_log; + } + + esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); + ret = esp_bt_controller_init(&bt_cfg); + if (ret) { + ESP_LOGE(TAG, "Failed to init controller, err %d", ret); + goto err_log; + } + + ret = esp_bt_controller_enable(ESP_BT_MODE_BLE); + if (ret) { + ESP_LOGE(TAG, "Failed to enable controller, err %d", ret); + goto err_controller_init; + } + + esp_bluedroid_config_t cfg = BT_BLUEDROID_INIT_CONFIG_DEFAULT(); + ret = esp_bluedroid_init_with_cfg(&cfg); + if (ret) { + ESP_LOGE(TAG, "Failed to init bluedroid, err %d", ret); + goto err_controller_enable; + } + + ret = esp_bluedroid_enable(); + if (ret) { + ESP_LOGE(TAG, "Failed to enable bluedroid, err %d", ret); + goto err_bluedroid_init; + } + + /* Just Works pairing (no MITM, SC + bond, IO_CAP=NONE). + * Matches the NimBLE ble_hs_cfg defaults below. */ + esp_ble_auth_req_t auth_req = ESP_LE_AUTH_REQ_SC_BOND; + esp_ble_io_cap_t iocap = ESP_IO_CAP_NONE; + uint8_t key_size = 16; + uint8_t init_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK; + uint8_t rsp_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK; + + esp_ble_gap_set_security_param(ESP_BLE_SM_AUTHEN_REQ_MODE, &auth_req, sizeof(auth_req)); + esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(iocap)); + esp_ble_gap_set_security_param(ESP_BLE_SM_MAX_KEY_SIZE, &key_size, sizeof(key_size)); + esp_ble_gap_set_security_param(ESP_BLE_SM_SET_INIT_KEY, &init_key, sizeof(init_key)); + esp_ble_gap_set_security_param(ESP_BLE_SM_SET_RSP_KEY, &rsp_key, sizeof(rsp_key)); + + /* Drain startup-phase log buffers and emit a FLUSH boundary so the + * parser resets stats here — the example's runtime logs start clean. + * + * TODO: Remove together with the manual ble_log_init() above once + * ble_log_init() is decoupled from the controller log path. */ +#if CONFIG_BLE_LOG_ENABLED + ble_log_flush(); +#endif + + return ESP_OK; + +err_bluedroid_init: + esp_bluedroid_deinit(); +err_controller_enable: + esp_bt_controller_disable(); +err_controller_init: + esp_bt_controller_deinit(); +err_log: + /* Mirror the manual ble_log_init() block above — same Kconfig gate so + * we only deinit what we initialized; the controller-owned path is + * left alone. */ +#if CONFIG_BLE_LOG_ENABLED && \ + !(CONFIG_BT_LE_CONTROLLER_LOG_ENABLED && CONFIG_BT_LE_CONTROLLER_LOG_MODE_BLE_LOG_V2) + ble_log_deinit(); +#endif + return ret; +} +#endif /* CONFIG_BT_BLUEDROID_ENABLED */ + +#ifdef CONFIG_BT_NIMBLE_ENABLED static SemaphoreHandle_t example_iso_sem; void ble_store_config_init(void); @@ -63,7 +168,7 @@ esp_err_t bluetooth_init(void) /* When controller log is disabled (or running in v1 mode), the * automatic ble_log_init() inside esp_bt_controller_init() is skipped. * Init manually here so HOST/ISO/AUDIO compressed logs aren't dropped - * during NimBLE bring-up. The function is idempotent. + * during host bring-up. The function is idempotent. * * TODO: Remove this block (and the ble_log_flush() below) once * ble_log_init() is decoupled from the controller log path and @@ -132,3 +237,4 @@ err_log: #endif return ret; } +#endif /* CONFIG_BT_NIMBLE_ENABLED */ diff --git a/examples/bluetooth/esp_ble_iso/common_components/example_utils/ble_iso_example_utils.c b/examples/bluetooth/esp_ble_iso/common_components/example_utils/ble_iso_example_utils.c index 5807a0a01f8..8c244d907a0 100644 --- a/examples/bluetooth/esp_ble_iso/common_components/example_utils/ble_iso_example_utils.c +++ b/examples/bluetooth/esp_ble_iso/common_components/example_utils/ble_iso_example_utils.c @@ -7,9 +7,7 @@ */ #include -#include #include -#include #include "esp_log.h" @@ -22,61 +20,6 @@ */ #define LOG_INTERVAL_PACKETS 6000 -int example_iso_gap_event_cb(struct ble_gap_event *event, void *arg) -{ - if (event->type == BLE_GAP_EVENT_EXT_DISC || - event->type == BLE_GAP_EVENT_PERIODIC_SYNC || - event->type == BLE_GAP_EVENT_PERIODIC_REPORT || - event->type == BLE_GAP_EVENT_PERIODIC_SYNC_LOST || - event->type == BLE_GAP_EVENT_PERIODIC_TRANSFER || - event->type == BLE_GAP_EVENT_PERIODIC_TRANSFER_V2 || - event->type == BLE_GAP_EVENT_CONNECT || - event->type == BLE_GAP_EVENT_DISCONNECT || - event->type == BLE_GAP_EVENT_ENC_CHANGE) { - esp_ble_iso_gap_app_post_event(event->type, event); - } else if (event->type == BLE_GAP_EVENT_REPEAT_PAIRING) { - /* Peer wants to re-pair on top of an existing bond (e.g. phone - * cleared its LTK via "Forget device" but our NVS still has it). - * Delete our stale bond and tell NimBLE to retry pairing. - */ - struct ble_gap_conn_desc desc = {0}; - int rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc); - if (rc == 0) { - ble_store_util_delete_peer(&desc.peer_id_addr); - } - return BLE_GAP_REPEAT_PAIRING_RETRY; - } - - return 0; -} - -void example_iso_security_failed_recover(const char *tag, uint16_t conn_handle, uint8_t status) -{ - struct ble_gap_conn_desc desc = {0}; - int rc; - - /* status 13 = BLE_HS_ETIMEOUT: SMP exchange did not complete. Typical - * cause is asymmetric bond state — the two sides bonded previously, - * then one side erased its NVS while the other kept the LTK. The side - * that still has the bond tries encryption with the cached key; the - * other side has nothing to match it against, so SMP times out. Drop - * our entry for this peer and disconnect; the next connection runs - * fresh pairing (peer side recovers via REPEAT_PAIRING). - */ - ESP_LOGE(tag, "Security change failed, status %u, clearing local bond and reconnecting", status); - - rc = ble_gap_conn_find(conn_handle, &desc); - if (rc == 0) { - ble_store_util_delete_peer(&desc.peer_id_addr); - } - - /* Tear down the link: SMP cannot be retried on the same connection - * after a failed/timed-out exchange. The next reconnect starts a - * fresh SMP procedure with the now-cleared bond state. - */ - ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM); -} - static void example_iso_tx_work_handler(struct k_work *work) { example_iso_tx_scheduler_t *scheduler = work->user_data; diff --git a/examples/bluetooth/esp_ble_iso/common_components/example_utils/ble_iso_example_utils.h b/examples/bluetooth/esp_ble_iso/common_components/example_utils/ble_iso_example_utils.h index dc7fc7169f8..80cf705b7e9 100644 --- a/examples/bluetooth/esp_ble_iso/common_components/example_utils/ble_iso_example_utils.h +++ b/examples/bluetooth/esp_ble_iso/common_components/example_utils/ble_iso_example_utils.h @@ -15,12 +15,10 @@ #include "sdkconfig.h" #include "esp_err.h" +#include "esp_log.h" -#include "zephyr/kernel.h" - -#include "host/ble_gap.h" -#include "host/ble_hs_adv.h" -#include "host/ble_store.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" #include "esp_ble_iso_common_api.h" @@ -51,9 +49,60 @@ #define EXAMPLE_BYTES_LIST_LE48 BT_BYTES_LIST_LE48 #define EXAMPLE_BYTES_LIST_LE64 BT_BYTES_LIST_LE64 -int example_iso_gap_event_cb(struct ble_gap_event *event, void *arg); +/* bt_le_addr.val carries the host stack's native byte order: NimBLE is + * LSB-first (BT spec wire order), Bluedroid is MSB-first (BD_ADDR). Use + * this with "%02x:%02x:%02x:%02x:%02x:%02x" to print MSB-first regardless + * of host. Examples can otherwise pass val[] straight to their host APIs + * without conversion. */ +#if CONFIG_BT_BLUEDROID_ENABLED +#define EXAMPLE_BT_ADDR_PRINT_ARGS(_v) \ + (_v)[0], (_v)[1], (_v)[2], (_v)[3], (_v)[4], (_v)[5] -void example_iso_security_failed_recover(const char *tag, uint16_t conn_handle, uint8_t status); +/* Fire-and-wait helper for Bluedroid GAP APIs that pair an async call with + * a matching ESP_GAP_BLE_*_COMPLETE_EVT. The example's GAP event handler + * is expected to xSemaphoreGive(_sem) on that COMPLETE_EVT. `TAG` and the + * enclosing function's `esp_err_t` return are taken from the call site. + * + * !!! Demo-only — DO NOT copy into production code. !!! + * Risks (acceptable in linear init sequences, not in customer apps): + * - Same-task deadlock: must NOT be called from the task that dispatches + * the COMPLETE_EVT (BTC task by default; lib forwarding can route some + * events through other tasks — verify the path for your event). + * - portMAX_DELAY = no timeout: hangs forever on controller/firmware + * misbehavior. Production must use a bounded timeout + recovery. + * - Shared binary sem: concurrent callers race; first take consumes + * whichever give arrived. Examples avoid this by serializing init. + * - No abort hook: nothing can break a portMAX_DELAY wait at shutdown. + * - Status not checked: see EXAMPLE_WAIT_API_CHECK below. + * + * Production should drive BLE init/teardown via an event-driven state + * machine (one task owns the BLE flow, callbacks advance state, no + * blocking from inside callbacks). NimBLE host returns ctrl errors + * synchronously and sidesteps most of this complexity. */ +#define EXAMPLE_WAIT_API(_call, _sem, _delay) do { \ + esp_err_t _err = (_call); \ + if (_err != ESP_OK) { \ + ESP_LOGE(TAG, #_call " failed, err %d", _err); \ + return _err; \ + } \ + xSemaphoreTake(_sem, _delay); \ + } while (0) + +/* As EXAMPLE_WAIT_API but also checks the controller status latched by the + * gap_event_handler into `_op_status` after the sem is taken. Returns + * ESP_FAIL on async failure. Caller must provide a static esp_bt_status_t + * and write it from each COMPLETE_EVT param before xSemaphoreGive. */ +#define EXAMPLE_WAIT_API_CHECK(_call, _sem, _delay, _op_status) do { \ + EXAMPLE_WAIT_API(_call, _sem, _delay); \ + if ((_op_status) != ESP_BT_STATUS_SUCCESS) { \ + ESP_LOGE(TAG, #_call " ctrl status %d", (_op_status)); \ + return ESP_FAIL; \ + } \ + } while (0) +#else +#define EXAMPLE_BT_ADDR_PRINT_ARGS(_v) \ + (_v)[5], (_v)[4], (_v)[3], (_v)[2], (_v)[1], (_v)[0] +#endif /** * @brief TX scheduler for periodic ISO data transmission.