diff --git a/samples/bluetooth/cap_acceptor/Kconfig b/samples/bluetooth/cap_acceptor/Kconfig index 8bf991475740b..05608d7333ecf 100644 --- a/samples/bluetooth/cap_acceptor/Kconfig +++ b/samples/bluetooth/cap_acceptor/Kconfig @@ -20,7 +20,7 @@ config SAMPLE_UNICAST config SAMPLE_BROADCAST bool "Whether or not to search for CAP acceptors for unicast audio" - default y if !SAMPLE_UNICAST + default y select BT_BAP_SCAN_DELEGATOR select BT_OBSERVER select BT_ISO_SYNC_RECEIVER diff --git a/samples/bluetooth/cap_acceptor/prj.conf b/samples/bluetooth/cap_acceptor/prj.conf index 4f02a26f34a5f..472f50887a365 100644 --- a/samples/bluetooth/cap_acceptor/prj.conf +++ b/samples/bluetooth/cap_acceptor/prj.conf @@ -33,3 +33,5 @@ CONFIG_BT_ISO_MAX_CHAN=2 # Support long Metadata size for audio configuration CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=24 + +CONFIG_BT_BAP_SCAN_DELEGATOR_LOG_LEVEL_DBG=y diff --git a/samples/bluetooth/cap_acceptor/src/cap_acceptor.h b/samples/bluetooth/cap_acceptor/src/cap_acceptor.h index 42c181f34dd2c..4f9ec41fc2a35 100644 --- a/samples/bluetooth/cap_acceptor/src/cap_acceptor.h +++ b/samples/bluetooth/cap_acceptor/src/cap_acceptor.h @@ -18,8 +18,8 @@ struct peer_config { /** Stream for the source endpoint */ struct bt_cap_stream source_stream; - /** Stream for the sink endpoint */ - struct bt_cap_stream sink_stream; + /** Streams for the sink endpoint */ + struct bt_cap_stream sink_streams[CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT]; /** Semaphore to help wait for a release operation if the source stream is not idle */ struct k_sem source_stream_sem; /** Semaphore to help wait for a release operation if the sink stream is not idle */ diff --git a/samples/bluetooth/cap_acceptor/src/cap_acceptor_broadcast.c b/samples/bluetooth/cap_acceptor/src/cap_acceptor_broadcast.c index d8e653c7846e8..9ef9e3f6ecc1a 100644 --- a/samples/bluetooth/cap_acceptor/src/cap_acceptor_broadcast.c +++ b/samples/bluetooth/cap_acceptor/src/cap_acceptor_broadcast.c @@ -100,7 +100,6 @@ static int check_start_scan(void) static void broadcast_stream_started_cb(struct bt_bap_stream *bap_stream) { LOG_INF("Started bap_stream %p", bap_stream); - total_broadcast_rx_iso_packet_count = 0U; atomic_clear_bit(flags, FLAG_BROADCAST_SYNCING); atomic_set_bit(flags, FLAG_BROADCAST_SYNCED); @@ -482,6 +481,8 @@ static int bis_sync_req_cb(struct bt_conn *conn, } if (broadcast_sink.requested_bis_sync == new_bis_sync_req) { + LOG_INF("New request (0x%08x) is the same as last request; ignoring", + bis_sync_req[0]); return 0; /* no op */ } @@ -491,6 +492,8 @@ static int bis_sync_req_cb(struct bt_conn *conn, */ int err; + LOG_INF("Already synced. Stopping current sync and attempt resyncing"); + /* The stream stopped callback will be called as part of this, * and we do not need to wait for any events from the * controller. Thus, when this returns, the broadcast sink is stopped diff --git a/samples/bluetooth/cap_acceptor/src/cap_acceptor_unicast.c b/samples/bluetooth/cap_acceptor/src/cap_acceptor_unicast.c index 55a9f91099e5b..bfaa2846e3b5d 100644 --- a/samples/bluetooth/cap_acceptor/src/cap_acceptor_unicast.c +++ b/samples/bluetooth/cap_acceptor/src/cap_acceptor_unicast.c @@ -296,7 +296,6 @@ static void unicast_stream_enabled_cb(struct bt_bap_stream *bap_stream) static void unicast_stream_started_cb(struct bt_bap_stream *bap_stream) { LOG_INF("Started bap_stream %p", bap_stream); - total_unicast_rx_iso_packet_count = 0U; } static void unicast_stream_metadata_updated_cb(struct bt_bap_stream *bap_stream) @@ -438,7 +437,10 @@ int init_cap_acceptor_unicast(struct peer_config *peer) } bt_cap_stream_ops_register(&peer->source_stream, &unicast_stream_ops); - bt_cap_stream_ops_register(&peer->sink_stream, &unicast_stream_ops); + + ARRAY_FOR_EACH_PTR(peer->sink_streams, stream) { + bt_cap_stream_ops_register(stream, &unicast_stream_ops); + } if (IS_ENABLED(CONFIG_BT_ASCS_ASE_SRC)) { static bool thread_started; @@ -459,5 +461,8 @@ int init_cap_acceptor_unicast(struct peer_config *peer) k_sem_init(&peer->source_stream_sem, 0, 1); k_sem_init(&peer->sink_stream_sem, 0, 1); + total_unicast_rx_iso_packet_count = 0U; + total_unicast_tx_iso_packet_count = 0U; + return 0; } diff --git a/samples/bluetooth/cap_acceptor/src/main.c b/samples/bluetooth/cap_acceptor/src/main.c index fc51a8a0ade0d..e6c4130da597c 100644 --- a/samples/bluetooth/cap_acceptor/src/main.c +++ b/samples/bluetooth/cap_acceptor/src/main.c @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -140,8 +141,12 @@ static int advertise(void) struct bt_cap_stream *stream_alloc(enum bt_audio_dir dir) { - if (dir == BT_AUDIO_DIR_SINK && peer.sink_stream.bap_stream.ep == NULL) { - return &peer.sink_stream; + if (dir == BT_AUDIO_DIR_SINK) { + ARRAY_FOR_EACH_PTR(peer.sink_streams, stream) { + if (stream->bap_stream.ep == NULL) { + return stream; + } + } } else if (dir == BT_AUDIO_DIR_SOURCE && peer.source_stream.bap_stream.ep == NULL) { return &peer.source_stream; } @@ -153,8 +158,10 @@ void stream_released(const struct bt_cap_stream *cap_stream) { if (cap_stream == &peer.source_stream) { k_sem_give(&peer.source_stream_sem); - } else if (cap_stream == &peer.sink_stream) { + } else if (IS_ARRAY_ELEMENT(peer.sink_streams, cap_stream)) { k_sem_give(&peer.sink_stream_sem); + } else { + __ASSERT(false, "Invalid stream: %p", cap_stream); } } @@ -201,11 +208,13 @@ static int reset_cap_acceptor(void) } } - if (peer.sink_stream.bap_stream.ep != NULL) { - err = k_sem_take(&peer.sink_stream_sem, SEM_TIMEOUT); - if (err != 0) { - LOG_ERR("Timeout on sink_stream_sem: %d", err); - return err; + ARRAY_FOR_EACH_PTR(peer.sink_streams, stream) { + if (stream->bap_stream.ep != NULL) { + err = k_sem_take(&peer.sink_stream_sem, SEM_TIMEOUT); + if (err != 0) { + LOG_ERR("Timeout on sink_stream_sem: %d", err); + return err; + } } } diff --git a/samples/bluetooth/cap_handover/CMakeLists.txt b/samples/bluetooth/cap_handover/CMakeLists.txt new file mode 100644 index 0000000000000..acf98fba22bf3 --- /dev/null +++ b/samples/bluetooth/cap_handover/CMakeLists.txt @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(cap_handover) + +target_sources(app PRIVATE + src/main.c + src/cap_stream_tx.c +) + +zephyr_library_include_directories(${ZEPHYR_BASE}/samples/bluetooth) diff --git a/samples/bluetooth/cap_handover/Kconfig b/samples/bluetooth/cap_handover/Kconfig new file mode 100644 index 0000000000000..ed34ae5761395 --- /dev/null +++ b/samples/bluetooth/cap_handover/Kconfig @@ -0,0 +1,21 @@ +# Copyright (c) 2024 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +mainmenu "Bluetooth: Common Audio Profile initiator sample" + +config SAMPLE_STATIC_BROADCAST_ID + bool "Use static broadcast ID" + default y + help + Enabling this option will make the application use static broadcast ID, as opposed to a + randomly generated one. + +config SAMPLE_BROADCAST_ID + hex "The static broadcast ID to use" + range 0x000000 0xFFFFFF + depends on STATIC_BROADCAST_ID + default 0x123456 + help + This is the 3-octet broadcast ID advertised if static broadcast IDs are enabled. + +source "Kconfig.zephyr" diff --git a/samples/bluetooth/cap_handover/Kconfig.sysbuild b/samples/bluetooth/cap_handover/Kconfig.sysbuild new file mode 100644 index 0000000000000..f37b265ecbc27 --- /dev/null +++ b/samples/bluetooth/cap_handover/Kconfig.sysbuild @@ -0,0 +1,15 @@ +# Copyright 2023 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +source "share/sysbuild/Kconfig" + +config NET_CORE_BOARD + string + default "nrf5340dk/nrf5340/cpunet" if "$(BOARD)" = "nrf5340dk" + default "nrf5340_audio_dk/nrf5340/cpunet" if "$(BOARD)" = "nrf5340_audio_dk" + default "nrf5340bsim/nrf5340/cpunet" if $(BOARD_TARGET_STRING) = "NRF5340BSIM_NRF5340_CPUAPP" + +config NET_CORE_IMAGE_HCI_IPC + bool "HCI IPC image on network core" + default y + depends on NET_CORE_BOARD != "" diff --git a/samples/bluetooth/cap_handover/README.rst b/samples/bluetooth/cap_handover/README.rst new file mode 100644 index 0000000000000..cd22d012b4044 --- /dev/null +++ b/samples/bluetooth/cap_handover/README.rst @@ -0,0 +1,75 @@ +.. zephyr:code-sample:: bluetooth_cap_handover + :name: Common Audio Profile (CAP) Handover + :relevant-api: bluetooth bt_bap bt_cap bt_conn + + Connects to a CAP acceptor and performs CAP handover procedures + +Overview +******** + +Application demonstrating the CAP handover functionality. +Starts by scanning for a CAP Acceptor then sets up unicast audio, and then switches between unicast +and broadcast using the CAP handover procedures. + +This sample can be found under :zephyr_file:`samples/bluetooth/cap_handover` in the Zephyr tree. + +Check the :zephyr:code-sample-category:`bluetooth` samples for general information. + +Requirements +************ + +* BlueZ running on the host, or +* A board with Bluetooth Low Energy 5.2 support + +Building and Running +******************** + +When building targeting an nrf52 series board with the Zephyr Bluetooth Controller, +use ``-DEXTRA_CONF_FILE=overlay-bt_ll_sw_split.conf`` to enable the required ISO +feature support. + +Building for an nrf5340dk +------------------------- + +You can build both the application core image and an appropriate controller image for the network +core with: + +.. zephyr-app-commands:: + :zephyr-app: samples/bluetooth/cap_handover/ + :board: nrf5340dk/nrf5340/cpuapp + :goals: build + :west-args: --sysbuild + +If you prefer to only build the application core image, you can do so by doing instead: + +.. zephyr-app-commands:: + :zephyr-app: samples/bluetooth/cap_handover/ + :board: nrf5340dk/nrf5340/cpuapp + :goals: build + +In that case you can pair this application core image with the +:zephyr:code-sample:`bluetooth_hci_ipc` sample +:zephyr_file:`samples/bluetooth/hci_ipc/nrf5340_cpunet_iso-bt_ll_sw_split.conf` configuration. + +Building for a simulated nrf5340bsim +------------------------------------ + +Similarly to how you would for real HW, you can do: + +.. zephyr-app-commands:: + :zephyr-app: samples/bluetooth/cap_handover/ + :board: nrf5340bsim/nrf5340/cpuapp + :goals: build + :west-args: --sysbuild + +Note this will produce a Linux executable in :file:`./build/zephyr/zephyr.exe`. +For more information, check :ref:`this board documentation `. + +Building for a simulated nrf52_bsim +----------------------------------- + +.. zephyr-app-commands:: + :zephyr-app: samples/bluetooth/cap_handover/ + :board: nrf52_bsim + :goals: build + :gen-args: -DEXTRA_CONF_FILE=overlay-bt_ll_sw_split.conf diff --git a/samples/bluetooth/cap_handover/boards/nrf5340_audio_dk_nrf5340_cpuapp.conf b/samples/bluetooth/cap_handover/boards/nrf5340_audio_dk_nrf5340_cpuapp.conf new file mode 100644 index 0000000000000..09fa41842ebd9 --- /dev/null +++ b/samples/bluetooth/cap_handover/boards/nrf5340_audio_dk_nrf5340_cpuapp.conf @@ -0,0 +1,4 @@ +CONFIG_BT_BUF_EVT_RX_SIZE=255 +CONFIG_BT_BUF_ACL_RX_SIZE=255 +CONFIG_BT_BUF_ACL_TX_SIZE=251 +CONFIG_BT_BUF_CMD_TX_SIZE=255 diff --git a/samples/bluetooth/cap_handover/boards/nrf5340dk_nrf5340_cpuapp.conf b/samples/bluetooth/cap_handover/boards/nrf5340dk_nrf5340_cpuapp.conf new file mode 100644 index 0000000000000..09fa41842ebd9 --- /dev/null +++ b/samples/bluetooth/cap_handover/boards/nrf5340dk_nrf5340_cpuapp.conf @@ -0,0 +1,4 @@ +CONFIG_BT_BUF_EVT_RX_SIZE=255 +CONFIG_BT_BUF_ACL_RX_SIZE=255 +CONFIG_BT_BUF_ACL_TX_SIZE=251 +CONFIG_BT_BUF_CMD_TX_SIZE=255 diff --git a/samples/bluetooth/cap_handover/overlay-bt_ll_sw_split.conf b/samples/bluetooth/cap_handover/overlay-bt_ll_sw_split.conf new file mode 100644 index 0000000000000..ff31c997b19f4 --- /dev/null +++ b/samples/bluetooth/cap_handover/overlay-bt_ll_sw_split.conf @@ -0,0 +1,42 @@ +# Zephyr Bluetooth Controller +CONFIG_BT_LL_SW_SPLIT=y +CONFIG_BT_CTLR_ASSERT_HANDLER=y +CONFIG_BT_CTLR_DTM_HCI=y + +CONFIG_BT_BROADCASTER=y +CONFIG_BT_OBSERVER=y +CONFIG_BT_EXT_ADV=y +CONFIG_BT_PER_ADV=y +CONFIG_BT_PER_ADV_SYNC=y +CONFIG_BT_CENTRAL=y +CONFIG_BT_MAX_CONN=1 +CONFIG_BT_CTLR_PHY_CODED=y + +# Zephyr Controller tested maximum advertising data that can be set in a single HCI command +CONFIG_BT_CTLR_SCAN_DATA_LEN_MAX=191 +CONFIG_BT_CTLR_ADV_DATA_LEN_MAX=191 + +CONFIG_BT_ISO_BROADCASTER=y +CONFIG_BT_ISO_SYNC_RECEIVER=y +CONFIG_BT_ISO_CENTRAL=y +CONFIG_BT_ISO_TX_MTU=310 +CONFIG_BT_ISO_RX_MTU=310 +CONFIG_BT_ISO_MAX_CHAN=2 + +CONFIG_BT_CTLR_ADV_ISO_STREAM_COUNT=2 +CONFIG_BT_CTLR_CONN_ISO_GROUPS=1 +CONFIG_BT_CTLR_CONN_ISO_STREAMS=2 +CONFIG_BT_CTLR_CONN_ISO_STREAMS_PER_GROUP=2 + +# In theory, CONFIG_BT_ISO_TX_BUF_COUNT=1, should be sufficient but this count +# is used in the context of IPC which falls into a "Newton's Cradle" effect +# where probably (CONFIG_BT_CTLR_ISO_TX_BUFFERS - CONFIG_BT_ISO_TX_BUF_COUNT) +# buffers get throttled. Hence, always have the value equal or greater. +CONFIG_BT_ISO_TX_BUF_COUNT=12 +CONFIG_BT_ISO_RX_BUF_COUNT=1 + +# Support the highest SDU size required by any BAP LC3 presets (310) + 8 bytes of HCI ISO Data +# packet overhead (the Packet_Sequence_Number, ISO_SDU_Length, Packet_Status_Flag fields; and +# the optional Time_Stamp field, if supplied) +CONFIG_BT_CTLR_ISO_TX_BUFFER_SIZE=318 +CONFIG_BT_CTLR_ISO_TX_SDU_LEN_MAX=310 diff --git a/samples/bluetooth/cap_handover/prj.conf b/samples/bluetooth/cap_handover/prj.conf new file mode 100644 index 0000000000000..ce6e4c92ce11b --- /dev/null +++ b/samples/bluetooth/cap_handover/prj.conf @@ -0,0 +1,52 @@ +CONFIG_LOG=y +CONFIG_ASSERT=y + +CONFIG_BT=y +CONFIG_BT_BROADCASTER=y +CONFIG_BT_CENTRAL=y +CONFIG_BT_GATT_CLIENT=y +CONFIG_BT_GATT_AUTO_DISCOVER_CCC=y +CONFIG_BT_GATT_AUTO_UPDATE_MTU=y +CONFIG_BT_GATT_DYNAMIC_DB=y +CONFIG_BT_KEYS_OVERWRITE_OLDEST=y +CONFIG_BT_SMP=y +CONFIG_BT_EXT_ADV=y +CONFIG_BT_PER_ADV=y +CONFIG_BT_PER_ADV_SYNC=y +CONFIG_BT_DEVICE_NAME="CAP handover central" +CONFIG_BT_AUDIO=y + +# ISO configs +CONFIG_BT_ISO_MAX_CHAN=2 +CONFIG_BT_ISO_CENTRAL=y +CONFIG_BT_ISO_BROADCASTER=y +CONFIG_BT_ISO_SYNC_RECEIVER=y + +# BAP configs +CONFIG_BT_BAP_BROADCAST_ASSISTANT=y + +CONFIG_BT_BAP_UNICAST_CLIENT=y +CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT=2 +CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT=2 + +CONFIG_BT_BAP_SCAN_DELEGATOR=y + +CONFIG_BT_BAP_BROADCAST_SOURCE=y +CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT=1 +CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT=1 + +CONFIG_BT_BAP_BROADCAST_ASSISTANT=y + +# CAP configs +CONFIG_BT_CAP_INITIATOR=y +CONFIG_BT_CAP_COMMANDER=y +CONFIG_BT_CAP_HANDOVER=y + +# CSIP configs +CONFIG_BT_CSIP_SET_COORDINATOR=y + +CONFIG_BT_CAP_ACCEPTOR_LOG_LEVEL_DBG=y +CONFIG_BT_CAP_COMMANDER_LOG_LEVEL_DBG=y +CONFIG_BT_CAP_COMMON_LOG_LEVEL_DBG=y +CONFIG_BT_CAP_HANDOVER_LOG_LEVEL_DBG=y +CONFIG_BT_BAP_BROADCAST_ASSISTANT_LOG_LEVEL_DBG=y diff --git a/samples/bluetooth/cap_handover/sample.yaml b/samples/bluetooth/cap_handover/sample.yaml new file mode 100644 index 0000000000000..9f0afae4a99b7 --- /dev/null +++ b/samples/bluetooth/cap_handover/sample.yaml @@ -0,0 +1,30 @@ +sample: + description: Bluetooth Low Energy Common Audio Profile Handover sample + name: Bluetooth Low Energy Common Audio Profile Handover sample +tests: + sample.bluetooth.cap_handover: + harness: bluetooth + platform_allow: + - qemu_cortex_m3 + - qemu_x86 + - nrf5340dk/nrf5340/cpuapp + - nrf5340bsim/nrf5340/cpuapp + integration_platforms: + - qemu_x86 + - nrf5340dk/nrf5340/cpuapp + tags: bluetooth + sysbuild: true + sample.bluetooth.cap_handover.bt_ll_sw_split: + harness: bluetooth + platform_allow: + - nrf52_bsim + - nrf52833dk/nrf52833 + - nrf52840dk/nrf52840 + - nrf52840dongle/nrf52840 + integration_platforms: + - nrf52_bsim + - nrf52833dk/nrf52833 + - nrf52840dk/nrf52840 + - nrf52840dongle/nrf52840 + extra_args: EXTRA_CONF_FILE=overlay-bt_ll_sw_split.conf + tags: bluetooth diff --git a/samples/bluetooth/cap_handover/src/cap_stream_tx.c b/samples/bluetooth/cap_handover/src/cap_stream_tx.c new file mode 100644 index 0000000000000..f618bcbf626c1 --- /dev/null +++ b/samples/bluetooth/cap_handover/src/cap_stream_tx.c @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cap_stream_tx.h" + +LOG_MODULE_REGISTER(cap_stream_tx, LOG_LEVEL_INF); + +/** Mutex to prevent race conditions as the values are accessed by multiple threads */ +#define TX_MUTEX_TIMEOUT K_MSEC(1000) +static K_MUTEX_DEFINE(tx_mutex); + +struct tx_stream tx_streams[CAP_STREAM_TX_MAX]; + +static void tx_thread_func(void *arg1, void *arg2, void *arg3) +{ + NET_BUF_POOL_FIXED_DEFINE(tx_pool, CONFIG_BT_ISO_TX_BUF_COUNT, + BT_ISO_SDU_BUF_SIZE(CONFIG_BT_ISO_TX_MTU), + CONFIG_BT_CONN_TX_USER_DATA_SIZE, NULL); + static uint8_t data[CONFIG_BT_ISO_TX_MTU]; + + for (size_t i = 0U; i < ARRAY_SIZE(data); i++) { + data[i] = (uint8_t)i; + } + + while (true) { + bool delay_and_retry = true; + int err; + + err = k_mutex_lock(&tx_mutex, TX_MUTEX_TIMEOUT); + if (err != 0) { + goto delay_and_retry; + } + + for (size_t i = 0U; i < ARRAY_SIZE(tx_streams); i++) { + const struct bt_bap_stream *bap_stream; + struct bt_cap_stream *cap_stream; + struct bt_bap_ep_info ep_info; + + cap_stream = tx_streams[i].stream; + if (cap_stream == NULL) { + continue; + } + + bap_stream = &cap_stream->bap_stream; + + /* No-op if stream is not configured */ + if (bap_stream->ep == NULL) { + continue; + } + + err = bt_bap_ep_get_info(bap_stream->ep, &ep_info); + __ASSERT(err == 0, "Failed to get ep_info: %d", err); + + if (ep_info.state == BT_BAP_EP_STATE_STREAMING) { + struct net_buf *buf; + + buf = net_buf_alloc(&tx_pool, K_FOREVER); + net_buf_reserve(buf, BT_ISO_CHAN_SEND_RESERVE); + + net_buf_add_mem(buf, data, bap_stream->qos->sdu); + + err = bt_cap_stream_send(cap_stream, buf, tx_streams[i].seq_num); + if (err == 0) { + tx_streams[i].seq_num++; + delay_and_retry = false; + } else { + LOG_ERR("Unable to send: %d", err); + net_buf_unref(buf); + } + } + } + + err = k_mutex_unlock(&tx_mutex); + __ASSERT(err == 0, "Failed to unlock mutex: %d", err); + + if (delay_and_retry) { +delay_and_retry: + /* In case of any errors or nothing sent, retry with a delay */ + k_sleep(K_MSEC(100)); + } + } +} + +int cap_stream_tx_register_stream(struct bt_cap_stream *cap_stream) +{ + int ret; + int err; + + if (cap_stream == NULL) { + return -EINVAL; + } + + err = k_mutex_lock(&tx_mutex, TX_MUTEX_TIMEOUT); + if (err != 0) { + LOG_ERR("Failed to take mutex: %d", err); + return err; + } + + ret = -ENOMEM; + for (size_t i = 0U; i < ARRAY_SIZE(tx_streams); i++) { + if (tx_streams[i].stream == NULL) { + tx_streams[i].stream = cap_stream; + tx_streams[i].seq_num = 0U; + ret = 0; + + break; + } + } + + err = k_mutex_unlock(&tx_mutex); + __ASSERT(err == 0, "Failed to unlock mutex: %d", err); + + return ret; +} + +int cap_stream_tx_unregister_stream(struct bt_cap_stream *cap_stream) +{ + int ret; + int err; + + if (cap_stream == NULL) { + return -EINVAL; + } + + err = k_mutex_lock(&tx_mutex, TX_MUTEX_TIMEOUT); + if (err != 0) { + LOG_ERR("Failed to take mutex: %d", err); + return err; + } + + ret = -ENODATA; + for (size_t i = 0U; i < ARRAY_SIZE(tx_streams); i++) { + if (tx_streams[i].stream == cap_stream) { + tx_streams[i].stream = NULL; + + ret = 0; + break; + } + } + + err = k_mutex_unlock(&tx_mutex); + __ASSERT(err == 0, "Failed to unlock mutex: %d", err); + + return ret; +} + +void cap_stream_tx_init(void) +{ + static bool thread_started; + + if (!thread_started) { + static K_KERNEL_STACK_DEFINE(tx_thread_stack, 1024); + const int tx_thread_prio = K_PRIO_PREEMPT(5); + static struct k_thread tx_thread; + + k_thread_create(&tx_thread, tx_thread_stack, K_KERNEL_STACK_SIZEOF(tx_thread_stack), + tx_thread_func, NULL, NULL, NULL, tx_thread_prio, 0, K_NO_WAIT); + k_thread_name_set(&tx_thread, "TX thread"); + thread_started = true; + } +} diff --git a/samples/bluetooth/cap_handover/src/cap_stream_tx.h b/samples/bluetooth/cap_handover/src/cap_stream_tx.h new file mode 100644 index 0000000000000..1c60653c849f2 --- /dev/null +++ b/samples/bluetooth/cap_handover/src/cap_stream_tx.h @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include + +#include +#include +#include +#include +#include +#include + +/* Number of streams we support + * If this value is > 1, then we may not always be able to perform the handover. + * For example if the remote device supports 2 unicast sink streams, but only 1 broadcast sink + * stream, then the handover procedure cannot be performed. + */ +#define CAP_STREAM_TX_MAX \ + (MIN(CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT, CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT)) + +struct tx_stream { + /** Reference to a stream */ + struct bt_cap_stream *stream; + + /** The last sent sequence number */ + uint16_t seq_num; +}; + +/** + * @brief Initialize TX + * + * This will initialize TX if not already initialized. This creates and starts a thread that + * will attempt to send data on all streams registered with cap_stream_tx_register_stream(). + */ +void cap_stream_tx_init(void); + +/** + * @brief Register a stream for TX + * + * This will add it to the list of streams the TX thread will attempt to send on. + * + * @retval 0 on success + * @retval -EINVAL if @p cap_stream is NULL + * @retval -ENOMEM if not more streams can be registered + */ +int cap_stream_tx_register_stream(struct bt_cap_stream *cap_stream); + +/** + * @brief Unregister a stream for TX + * + * This will remove it to the list of streams the TX thread will attempt to send on. + * + * @retval 0 on success + * @retval -EINVAL if @p cap_stream is NULL + * @retval -EALREADY if the stream is currently not registered + */ +int cap_stream_tx_unregister_stream(struct bt_cap_stream *cap_stream); diff --git a/samples/bluetooth/cap_handover/src/main.c b/samples/bluetooth/cap_handover/src/main.c new file mode 100644 index 0000000000000..3afc555d0c67e --- /dev/null +++ b/samples/bluetooth/cap_handover/src/main.c @@ -0,0 +1,1340 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cap_stream_tx.h" + +LOG_MODULE_REGISTER(cap_handover, LOG_LEVEL_INF); + +#define SEM_TIMEOUT K_SECONDS(5) + +/* For simplicity we use the mandatory configuration 16_2_1 */ +static struct bt_bap_lc3_preset unicast_preset_16_2_1 = BT_BAP_LC3_UNICAST_PRESET_16_2_1( + BT_AUDIO_LOCATION_MONO_AUDIO, BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED); +static struct bt_bap_lc3_preset broadcast_preset_16_2_1 = BT_BAP_LC3_BROADCAST_PRESET_16_2_1( + BT_AUDIO_LOCATION_FRONT_LEFT, BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED); + +static struct bt_cap_broadcast_source *broadcast_source; +static struct bt_cap_unicast_group *unicast_group; +static struct bt_le_ext_adv *ext_adv; +static uint32_t broadcast_id; + +uint64_t total_tx_iso_packet_count; /* This value is exposed to test code */ + +/* Store the parameters globally so we can reuse the parameters when switching between unicast and + * broadcast + */ +static struct bt_cap_initiator_broadcast_create_param broadcast_create_param; +static struct bt_cap_unicast_audio_start_param unicast_audio_start_param; +static struct bt_cap_unicast_group_param unicast_group_param; + +/** Struct to contain information for a specific peer (CAP) device */ +struct peer_config { + /** The receive state on the peer that represent our broadcast source */ + const struct bt_bap_scan_delegator_recv_state *recv_state; + + /** Streams - Can either be unicast or broadcast */ + struct bt_cap_stream streams[CAP_STREAM_TX_MAX]; + + /** Reference to the unicast endpoints */ + struct bt_bap_ep *unicast_eps[CAP_STREAM_TX_MAX]; + + /** ACL connection object for the peer device */ + struct bt_conn *conn; + + /** src_id that represents the recv_state - Needed as recv_state is a pointer that may be + * updated by the stack + */ + uint8_t src_id; +} peer; + +static K_SEM_DEFINE(sem_proc, 0, 1); +static K_SEM_DEFINE(sem_state_change, 0, 1); +static K_SEM_DEFINE(sem_mtu_exchanged, 0, 1); +static K_SEM_DEFINE(sem_security_changed, 0, 1); +static K_SEM_DEFINE(sem_broadcast_stopped, 1, 1); +static K_SEM_DEFINE(sem_receive_state_updated, 0, 1); +static K_SEM_DEFINE(sem_streams, 0, CAP_STREAM_TX_MAX); + +static void stream_configured_cb(struct bt_bap_stream *stream, + const struct bt_bap_qos_cfg_pref *pref) +{ + LOG_INF("Configured stream %p", stream); + + LOG_INF("Remote preferences: unframed %s, phy %u, rtn %u, latency %u, pd_min %u, pd_max " + "%u, pref_pd_min %u, pref_pd_max %u", + pref->unframed_supported ? "supported" : "not supported", pref->phy, pref->rtn, + pref->latency, pref->pd_min, pref->pd_max, pref->pref_pd_min, pref->pref_pd_max); +} + +static void stream_qos_set_cb(struct bt_bap_stream *stream) +{ + LOG_INF("QoS set stream %p", stream); +} + +static void stream_enabled_cb(struct bt_bap_stream *stream) +{ + LOG_INF("Enabled stream %p", stream); +} + +static void stream_started_cb(struct bt_bap_stream *stream) +{ + struct bt_cap_stream *cap_stream = CONTAINER_OF(stream, struct bt_cap_stream, bap_stream); + int err; + + LOG_INF("Started stream %p", stream); + + err = cap_stream_tx_register_stream(cap_stream); + if (err != 0) { + LOG_ERR("Failed to register %p for TX: %d", stream, err); + } +} + +static void stream_metadata_updated_cb(struct bt_bap_stream *stream) +{ + LOG_INF("Metadata updated stream %p", stream); +} + +static void stream_disabled_cb(struct bt_bap_stream *stream) +{ + LOG_INF("Disabled stream %p", stream); +} + +static void stream_stopped_cb(struct bt_bap_stream *stream, uint8_t reason) +{ + struct bt_cap_stream *cap_stream = CONTAINER_OF(stream, struct bt_cap_stream, bap_stream); + int err; + + LOG_INF("Stopped stream %p with reason 0x%02X", stream, reason); + + err = cap_stream_tx_unregister_stream(cap_stream); + if (err != 0) { + LOG_ERR("Failed to unregister %p for TX: %d", stream, err); + } +} + +static void stream_released_cb(struct bt_bap_stream *stream) +{ + LOG_INF("Released stream %p", stream); + + k_sem_give(&sem_streams); +} + +static void stream_sent_cb(struct bt_bap_stream *stream) +{ + /* Triggered every time we have sent an HCI data packet to the controller */ + + if ((total_tx_iso_packet_count % 100U) == 0U) { + LOG_INF("Sent %llu HCI ISO data packets", total_tx_iso_packet_count); + } + + total_tx_iso_packet_count++; +} + +static void +bap_broadcast_assistant_recv_state_cb(struct bt_conn *conn, int err, + const struct bt_bap_scan_delegator_recv_state *state) +{ + struct bt_le_ext_adv_info adv_info; + + if (err != 0) { + LOG_ERR("Unexpected error: %d", err); + return; + } + + if (state == NULL) { + /* Ignore */ + return; + } + + err = bt_le_ext_adv_get_info(ext_adv, &adv_info); + __ASSERT(err == 0, "Failed to get adv info: %d\n", err); + + LOG_ERR("broadcast_id %u %u", state->broadcast_id, broadcast_id); + LOG_ERR("adv_sid %u %u", state->adv_sid, adv_info.sid); + LOG_ERR("state->addr.type %u %u", state->addr.type, adv_info.addr->type); + + /* The broadcast_id, adv_sid and address type is the combination that makes a receive state + * unique as defined by the BAP spec + */ + if (state->broadcast_id == broadcast_id && state->adv_sid == adv_info.sid && + state->addr.type == adv_info.addr->type) { + LOG_DBG("Received receive state notification from %p with pa_state %d and " + "src_id 0x%02X", + (void *)conn, state->pa_sync_state, state->src_id); + peer.recv_state = state; + peer.src_id = state->src_id; + + k_sem_give(&sem_receive_state_updated); + } + + /* TODO: We need to copy the receive state */ +} + +static void bap_broadcast_assistant_recv_state_removed_cb(struct bt_conn *conn, uint8_t src_id) +{ + if (src_id == peer.src_id) { + LOG_DBG("Receive state removed"); + peer.recv_state = NULL; + } +} + +static void bap_broadcast_assistant_discover_cb(struct bt_conn *conn, int err, + uint8_t recv_state_count) +{ + if (err == 0) { + LOG_DBG("BASS discover done with %u recv states", recv_state_count); + } else { + LOG_DBG("BASS discover failed (%d)", err); + } + + k_sem_give(&sem_proc); +} + +static bool log_codec_cb(struct bt_data *data, void *user_data) +{ + const char *str = (const char *)user_data; + + LOG_DBG("\t%s: type 0x%02x value_len %u", str, data->type, data->data_len); + LOG_HEXDUMP_DBG(data->data, data->data_len, "\t\tdata"); + + return true; +} + +static void log_codec(const struct bt_audio_codec_cap *codec_cap, enum bt_audio_dir dir) +{ + LOG_INF("codec id 0x%02x cid 0x%04x vid 0x%04x count %u", codec_cap->id, codec_cap->cid, + codec_cap->vid, codec_cap->data_len); + + if (codec_cap->id == BT_HCI_CODING_FORMAT_LC3) { + bt_audio_data_parse(codec_cap->data, codec_cap->data_len, log_codec_cb, "data"); + } else { /* If not LC3, we cannot assume it's LTV */ + LOG_HEXDUMP_DBG(codec_cap->data, codec_cap->data_len, "data"); + } + + bt_audio_data_parse(codec_cap->meta, codec_cap->meta_len, log_codec_cb, "meta"); +} + +static void add_remote_sink(struct bt_bap_ep *ep) +{ + ARRAY_FOR_EACH(peer.unicast_eps, i) { + if (peer.unicast_eps[i] == NULL) { + LOG_INF("Sink ep[%zu]: %p", i, (void *)ep); + peer.unicast_eps[i] = ep; + return; + } + } +} + +static void discover_cb(struct bt_conn *conn, int err, enum bt_audio_dir dir) +{ + if (err != 0) { + LOG_ERR("Discovery sinks failed: %d", err); + } else { + LOG_INF("Discover sinks complete"); + } + + k_sem_give(&sem_proc); +} + +static void pac_record_cb(struct bt_conn *conn, enum bt_audio_dir dir, + const struct bt_audio_codec_cap *codec_cap) +{ + log_codec(codec_cap, dir); +} + +static void endpoint_cb(struct bt_conn *conn, enum bt_audio_dir dir, struct bt_bap_ep *ep) +{ + add_remote_sink(ep); +} + +static int discover_sinks(void) +{ + int err; + + LOG_INF("Discovering sink ASEs"); + k_sem_reset(&sem_proc); + + err = bt_bap_unicast_client_discover(peer.conn, BT_AUDIO_DIR_SINK); + if (err != 0) { + LOG_ERR("Failed to discover sink: %d", err); + return err; + } + + err = k_sem_take(&sem_proc, SEM_TIMEOUT); + if (err != 0) { + LOG_ERR("Timeout on sinks discover: %d", err); + return err; + } + + return 0; +} + +static int discover_bass(void) +{ + int err; + + k_sem_reset(&sem_proc); + + err = bt_bap_broadcast_assistant_discover(peer.conn); + if (err != 0) { + LOG_ERR("Failed to discover BASS on the sink: %d\n", err); + return err; + } + + err = k_sem_take(&sem_proc, SEM_TIMEOUT); + if (err != 0) { + LOG_ERR("Timeout on sinks discover: %d", err); + return err; + } + + return 0; +} + +static int unicast_group_create(void) +{ + /* Keep the parameters static as that is a requirement when doing broadcast to unicast */ + static struct bt_cap_unicast_group_stream_param sink_stream_params[CAP_STREAM_TX_MAX] = {0}; + static struct bt_cap_unicast_group_stream_pair_param pair_params[CAP_STREAM_TX_MAX] = {0}; + int err; + + (void)memset(sink_stream_params, 0, sizeof(sink_stream_params)); + (void)memset(pair_params, 0, sizeof(pair_params)); + (void)memset(&unicast_group_param, 0, sizeof(unicast_group_param)); + + ARRAY_FOR_EACH(peer.streams, i) { + if (peer.unicast_eps[i] == NULL) { + break; + } + + sink_stream_params[i].stream = &peer.streams[i]; + sink_stream_params[i].qos_cfg = &unicast_preset_16_2_1.qos; + pair_params[i].tx_param = &sink_stream_params[i]; + + unicast_group_param.params_count++; + } + + unicast_group_param.params = pair_params; + unicast_group_param.packing = BT_ISO_PACKING_SEQUENTIAL; + + err = bt_cap_unicast_group_create(&unicast_group_param, &unicast_group); + if (err != 0) { + LOG_ERR("Failed to create group: %d", err); + return err; + } + + LOG_INF("Created group"); + + return 0; +} + +static int unicast_group_delete(void) +{ + int err; + + err = bt_cap_unicast_group_delete(unicast_group); + if (err != 0) { + LOG_ERR("Failed to delete group: %d", err); + return err; + } + unicast_group = NULL; + + LOG_INF("Deleted group"); + + return 0; +} + +static void cap_discovery_complete_cb(struct bt_conn *conn, int err, + const struct bt_csip_set_coordinator_set_member *member, + const struct bt_csip_set_coordinator_csis_inst *csis_inst) +{ + if (err != 0) { + LOG_ERR("CAS discovery completed with error: %d", err); + + return; + } + + if (IS_ENABLED(CONFIG_BT_CAP_ACCEPTOR_SET_MEMBER) && csis_inst != NULL) { + LOG_INF("Found CAS with CSIS %p", csis_inst); + /* TODO: Do set member discovery */ + } else { + LOG_INF("Found CAS"); + } + + k_sem_give(&sem_proc); +} + +static void unicast_start_complete_cb(int err, struct bt_conn *conn) +{ + if (err != 0) { + LOG_ERR("Failed to start (failing conn %p): %d", (void *)conn, err); + return; + } + + k_sem_give(&sem_proc); +} + +static int discover_cas(void) +{ + int err; + + LOG_INF("Discovering CAS"); + k_sem_reset(&sem_proc); + + err = bt_cap_initiator_unicast_discover(peer.conn); + if (err != 0) { + LOG_ERR("Failed to discover CAS: %d", err); + return err; + } + + err = k_sem_take(&sem_proc, SEM_TIMEOUT); + if (err != 0) { + LOG_ERR("Timeout on CAS discover: %d", err); + return err; + } + + return 0; +} + +static void unicast_to_broadcast_complete_cb(int err, struct bt_conn *conn, + struct bt_cap_unicast_group *group, + struct bt_cap_broadcast_source *source) +{ + if (err != 0) { + LOG_ERR("Failed to handover unicast to broadcast (failing conn %p): %d", conn, err); + + err = bt_conn_disconnect(peer.conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); + if (err != 0) { + LOG_ERR("Failed to disconnect connection: %d", err); + } + } else { + LOG_DBG("Unicast to broadcast handover completed with new source %p", source); + err = k_sem_take(&sem_broadcast_stopped, K_NO_WAIT); + __ASSERT(err == 0, "Failed to take sem_broadcast_stopped"); + } + + /* Update the unicast group and broadcast source as they may have been created or deleted */ + unicast_group = group; + broadcast_source = source; + + k_sem_give(&sem_proc); +} + +static void broadcast_to_unicast_complete_cb(int err, struct bt_conn *conn, + struct bt_cap_broadcast_source *source, + struct bt_cap_unicast_group *group) +{ + if (err != 0) { + LOG_ERR("Failed to handover broadcast to unicast (failing conn %p): %d", conn, err); + + err = bt_conn_disconnect(conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); + if (err != 0) { + LOG_ERR("Failed to disconnect connection: %d", err); + } + } else { + LOG_DBG("Broadcast to unicast handover completed with new unicast group %p", group); + k_sem_give(&sem_broadcast_stopped); + } + + /* Update the unicast group and broadcast source as they may have been created or deleted */ + unicast_group = group; + broadcast_source = source; + + k_sem_give(&sem_proc); +} + +static int unicast_audio_start(void) +{ + static struct bt_cap_unicast_audio_start_stream_param stream_param[CAP_STREAM_TX_MAX] = {0}; + int err; + + LOG_INF("Starting streams"); + (void)memset(stream_param, 0, sizeof(stream_param)); + (void)memset(&unicast_audio_start_param, 0, sizeof(unicast_audio_start_param)); + + ARRAY_FOR_EACH(peer.streams, i) { + if (peer.unicast_eps[i] == NULL) { + break; + } + + stream_param[i].member.member = peer.conn; + stream_param[i].stream = &peer.streams[i]; + stream_param[i].ep = peer.unicast_eps[i]; + stream_param[i].codec_cfg = &unicast_preset_16_2_1.codec_cfg; + unicast_audio_start_param.count++; + } + + unicast_audio_start_param.type = BT_CAP_SET_TYPE_AD_HOC; + unicast_audio_start_param.stream_params = stream_param; + + k_sem_reset(&sem_proc); + + err = bt_cap_initiator_unicast_audio_start(&unicast_audio_start_param); + if (err != 0) { + LOG_ERR("Failed to start unicast audio: %d", err); + return err; + } + + err = k_sem_take(&sem_proc, SEM_TIMEOUT); + if (err != 0) { + LOG_ERR("Timeout on sinks discover: %d", err); + return err; + } + + return 0; +} + +static void broadcast_stopped_cb(struct bt_cap_broadcast_source *source, uint8_t reason) +{ + k_sem_give(&sem_broadcast_stopped); +} + +static void att_mtu_updated_cb(struct bt_conn *conn, uint16_t tx, uint16_t rx) +{ + LOG_INF("MTU exchanged: %u/%u", tx, rx); + k_sem_give(&sem_mtu_exchanged); +} + +static void start_scan(void) +{ + int err; + + err = bt_le_scan_start(BT_LE_SCAN_PASSIVE, NULL); + if (err != 0) { + LOG_ERR("Scanning failed to start: %d", err); + return; + } + + LOG_INF("Scanning successfully started"); +} + +static void connected_cb(struct bt_conn *conn, uint8_t err) +{ + char addr[BT_ADDR_LE_STR_LEN]; + + (void)bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + if (err != 0) { + LOG_ERR("Failed to connect to %s: %u", addr, err); + + bt_conn_unref(peer.conn); + peer.conn = NULL; + peer.recv_state = NULL; + (void)memset(peer.unicast_eps, 0, sizeof(peer.unicast_eps)); + + start_scan(); + return; + } + + if (conn != peer.conn) { + return; + } + + LOG_INF("Connected: %s", addr); + k_sem_give(&sem_state_change); +} + +static void disconnected_cb(struct bt_conn *conn, uint8_t reason) +{ + char addr[BT_ADDR_LE_STR_LEN]; + + if (conn != peer.conn) { + return; + } + + (void)bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + LOG_INF("Disconnected: %s, reason 0x%02x %s", addr, reason, bt_hci_err_to_str(reason)); + + bt_conn_unref(peer.conn); + peer.conn = NULL; + + k_sem_give(&sem_state_change); +} + +static void security_changed_cb(struct bt_conn *conn, bt_security_t level, + enum bt_security_err sec_err) +{ + if (sec_err == 0) { + LOG_INF("Security changed: %u", level); + k_sem_give(&sem_security_changed); + } else { + LOG_ERR("Failed to set security level: %s(%d)", bt_security_err_to_str(sec_err), + sec_err); + + if (sec_err == BT_SECURITY_ERR_PIN_OR_KEY_MISSING) { + int err; + + LOG_INF("Removing old key"); + err = bt_unpair(BT_ID_DEFAULT, bt_conn_get_dst(conn)); + if (err != 0) { + LOG_ERR("Failed to remove old key: %d", err); + } + } + } +} + +BT_CONN_CB_DEFINE(conn_callbacks) = { + .connected = connected_cb, + .disconnected = disconnected_cb, + .security_changed = security_changed_cb, +}; + +static bool check_audio_support_and_connect_cb(struct bt_data *data, void *user_data) +{ + char addr_str[BT_ADDR_LE_STR_LEN]; + bt_addr_le_t *addr = user_data; + const struct bt_uuid *uuid; + uint16_t uuid_val; + int err; + + LOG_DBG("[AD]: %u data_len %u", data->type, data->data_len); + + if (data->type != BT_DATA_SVC_DATA16) { + return true; /* Continue parsing to next AD data type */ + } + + if (data->data_len < sizeof(uuid_val)) { + LOG_DBG("AD invalid size %u", data->data_len); + return true; /* Continue parsing to next AD data type */ + } + + /* We are looking for the CAS service data */ + uuid_val = sys_get_le16(data->data); + uuid = BT_UUID_DECLARE_16(uuid_val); + if (bt_uuid_cmp(uuid, BT_UUID_CAS) != 0) { + return true; /* Continue parsing to next AD data type */ + } + + bt_addr_le_to_str(addr, addr_str, sizeof(addr_str)); + LOG_INF("Attempt to connect to %s", addr_str); + + err = bt_le_scan_stop(); + if (err != 0) { + LOG_ERR("Failed to stop scan: %d", err); + return false; + } + + err = bt_conn_le_create(addr, BT_CONN_LE_CREATE_CONN, BT_BAP_CONN_PARAM_RELAXED, + &peer.conn); + if (err != 0) { + LOG_WRN("Create conn to failed: %d, restarting scan", err); + start_scan(); + } + + return false; /* Stop parsing */ +} + +static void scan_recv_cb(const struct bt_le_scan_recv_info *info, struct net_buf_simple *buf) +{ + if (peer.conn != NULL) { + /* Already connected */ + return; + } + + /* Check for connectable, extended advertising */ + if (((info->adv_props & BT_GAP_ADV_PROP_EXT_ADV) != 0) && + ((info->adv_props & BT_GAP_ADV_PROP_CONNECTABLE)) != 0) { + /* Check for TMAS support in advertising data */ + bt_data_parse(buf, check_audio_support_and_connect_cb, (void *)info->addr); + } +} + +static int scan_and_connect(void) +{ + int err; + + start_scan(); + + err = k_sem_take(&sem_state_change, K_FOREVER); + if (err != 0) { + LOG_ERR("Failed to take sem_state_change: %d", err); + return err; + } + + return 0; +} + +static void exchange_cb(struct bt_conn *conn, uint8_t err, struct bt_gatt_exchange_params *params) +{ + if (err == BT_ATT_ERR_SUCCESS) { + LOG_INF("MTU exchange done"); + k_sem_give(&sem_proc); + } else { + LOG_ERR("MTU exchange failed: err %u", err); + } +} + +static int exchange_mtu(void) +{ + int err; + + if (!IS_ENABLED(CONFIG_BT_GATT_AUTO_UPDATE_MTU)) { + static struct bt_gatt_exchange_params exchange_params = { + .func = exchange_cb, + }; + + LOG_INF("Exchanging MTU"); + + k_sem_reset(&sem_proc); + + err = bt_gatt_exchange_mtu(peer.conn, &exchange_params); + if (err != 0) { + LOG_ERR("Failed to exchange MTU: %d", err); + } + + err = k_sem_take(&sem_proc, SEM_TIMEOUT); + if (err != 0) { + LOG_ERR("Timeout on MTU exchange request: %d", err); + return err; + } + } + + LOG_INF("Waiting for MTU exchange"); + err = k_sem_take(&sem_mtu_exchanged, SEM_TIMEOUT); + if (err != 0) { + LOG_ERR("Timeout on MTU exchange: %d", err); + return err; + } + + return 0; +} + +static int update_security(void) +{ + int err; + + err = bt_conn_set_security(peer.conn, BT_SECURITY_L2); + if (err != 0) { + LOG_ERR("Failed to set security: %d", err); + return err; + } + + LOG_INF("Waiting for security change"); + err = k_sem_take(&sem_security_changed, SEM_TIMEOUT); + if (err != 0) { + LOG_ERR("Timeout on security: %d", err); + return err; + } + + return 0; +} + +static int setup_broadcast_adv(void) +{ + struct bt_le_adv_param ext_adv_param = *BT_BAP_ADV_PARAM_BROADCAST_FAST; + int err; + + /* Zephyr Controller works best while Extended Advertising interval is a multiple + * of the ISO Interval minus 10 ms (max. advertising random delay). This is + * required to place the AUX_ADV_IND PDUs in a non-overlapping interval with the + * Broadcast ISO radio events. + */ + + ext_adv_param.interval_min -= BT_GAP_MS_TO_ADV_INTERVAL(10U); + ext_adv_param.interval_max -= BT_GAP_MS_TO_ADV_INTERVAL(10U); + + /* Create a non-connectable advertising set */ + err = bt_le_ext_adv_create(&ext_adv_param, NULL, &ext_adv); + if (err != 0) { + LOG_ERR("Unable to create extended advertising set: %d\n", err); + return err; + } + + /* Set periodic advertising parameters */ + err = bt_le_per_adv_set_param(ext_adv, BT_BAP_PER_ADV_PARAM_BROADCAST_FAST); + if (err != 0) { + LOG_ERR("Failed to set periodic advertising parameters: %d\n", err); + + __maybe_unused const int del_err = bt_le_ext_adv_delete(ext_adv); + __ASSERT(del_err == 0, "Failed to delete ext_adv: %d", del_err); + + return err; + } + + return 0; +} + +static int set_base_data(void) +{ + struct bt_data per_ad; + int err; + + NET_BUF_SIMPLE_DEFINE(base_buf, BT_BASE_MAX_SIZE); + + err = bt_cap_initiator_broadcast_get_base(broadcast_source, &base_buf); + if (err != 0) { + LOG_ERR("Failed to get encoded BASE: %d\n", err); + return err; + } + + per_ad.type = BT_DATA_SVC_DATA16; + per_ad.data_len = base_buf.len; + per_ad.data = base_buf.data; + err = bt_le_per_adv_set_data(ext_adv, &per_ad, 1); + if (err != 0) { + LOG_ERR("Failed to set periodic advertising data: %d\n", err); + return err; + } + + return 0; +} + +static int handover_unicast_to_broadcast(void) +{ + static struct bt_cap_initiator_broadcast_stream_param stream_params[CAP_STREAM_TX_MAX]; + static struct bt_cap_initiator_broadcast_subgroup_param subgroup_param = {0}; + /* Struct containing the converted unicast group configuration */ + struct bt_cap_handover_unicast_to_broadcast_param param = {0}; + size_t stream_cnt = 0U; + bool all_synced; + int err; + + ARRAY_FOR_EACH(stream_params, i) { + struct bt_cap_stream *cap_stream = &peer.streams[i]; + const struct bt_bap_ep *ep = cap_stream->bap_stream.ep; + struct bt_bap_ep_info ep_info; + + if (ep == NULL) { + /* Not configured */ + continue; + } + + err = bt_bap_ep_get_info(ep, &ep_info); + __ASSERT(err == 0, "Failed to get endpoint info: %d", err); + + if (ep_info.state != BT_BAP_EP_STATE_STREAMING) { + /* Not streaming - Handover is only applied to streaming streams */ + continue; + } + + stream_params[stream_cnt].stream = cap_stream; + stream_params[stream_cnt].data_len = 0U; + stream_params[stream_cnt].data = NULL; + + stream_cnt++; + } + + if (stream_cnt == 0U) { + LOG_ERR("No streams can be handed over"); + return -ENODEV; + } + + subgroup_param.stream_count = stream_cnt; + subgroup_param.stream_params = stream_params; + subgroup_param.codec_cfg = &broadcast_preset_16_2_1.codec_cfg; + + broadcast_create_param.subgroup_count = 1U; + broadcast_create_param.subgroup_params = &subgroup_param; + broadcast_create_param.qos = &broadcast_preset_16_2_1.qos; + broadcast_create_param.packing = BT_ISO_PACKING_SEQUENTIAL; + broadcast_create_param.encryption = false; + + param.type = BT_CAP_SET_TYPE_AD_HOC; + param.unicast_group = unicast_group; + param.broadcast_create_param = &broadcast_create_param; + param.ext_adv = ext_adv; + param.pa_interval = BT_BAP_PA_INTERVAL_UNKNOWN; + param.broadcast_id = broadcast_id; + + k_sem_reset(&sem_proc); + + LOG_INF("Handing over %u streams from unicast to broadcast", stream_cnt); + + err = bt_cap_handover_unicast_to_broadcast(¶m); + if (err != 0) { + LOG_ERR("Failed to handover unicast audio to broadcast: %d\n", err); + return err; + } + + err = k_sem_take(&sem_proc, SEM_TIMEOUT); + if (err != 0) { + LOG_ERR("Timeout on unicast to broadcast: %d", err); + return err; + } + + if (broadcast_source == NULL) { + /* Handover failed */ + return -EBADMSG; + } + + /* Enable Periodic Advertising - + * Extended advertising is started by bt_cap_handover_unicast_to_broadcast + */ + err = bt_le_per_adv_start(ext_adv); + if (err != 0) { + LOG_ERR("Failed to enable periodic advertising: %d\n", err); + return err; + } + + /* Update the Periodic Advertising data with the BASE from the new broadcast source */ + err = set_base_data(); + if (err != 0) { + LOG_ERR("Failed to set BASE data: %d\n", err); + return err; + } + + all_synced = false; + while (!all_synced) { + /* Use a high timeout here as syncing to the Periodic Advertising, receiving the + * BASE and the BIGInfo and finally syncing to the BIG may take a while + */ + err = k_sem_take(&sem_receive_state_updated, K_SECONDS(60)); + if (err != 0) { + LOG_ERR("Timeout on receive state update: %d", err); + return err; + } + + /* Once the receive state indicates a non-empty BIS*/ + if (peer.recv_state->num_subgroups == broadcast_create_param.subgroup_count) { + for (uint8_t i = 0U; i < peer.recv_state->num_subgroups; i++) { + + LOG_ERR("peer.recv_state->subgroups[%u].bis_sync 0x%08X", i, + peer.recv_state->subgroups[i].bis_sync); + + const size_t bis_synced = sys_count_bits( + &peer.recv_state->subgroups[i].bis_sync, + sizeof(peer.recv_state->subgroups[i].bis_sync)); + const uint8_t expected_bis_synced = + broadcast_create_param.subgroup_params[i].stream_count; + + if (bis_synced == 0) { + all_synced = false; + break; + } else if (bis_synced > 0 && bis_synced != expected_bis_synced) { + LOG_ERR("Remote synced to subgroup[%u]: 0x%08X, but we " + "expected %u BIS syncs", + i, peer.recv_state->subgroups[i].bis_sync, + broadcast_create_param.subgroup_params[i] + .stream_count); + + return -EBADMSG; + } else { + all_synced = true; + } + } + } + } + + return 0; +} + +static int handover_broadcast_to_unicast(void) +{ + struct bt_cap_commander_broadcast_reception_stop_param reception_stop_param = {0}; + struct bt_cap_commander_broadcast_reception_stop_member_param member_param = {0}; + struct bt_cap_handover_broadcast_to_unicast_param param = {0}; + struct bt_le_ext_adv_info adv_info; + int err; + + err = bt_le_ext_adv_get_info(ext_adv, &adv_info); + __ASSERT(err == 0, "Failed to get adv info: %d\n", err); + + if (peer.recv_state == NULL) { + LOG_ERR("Remote device does not have receive state for our broadcast, cannot " + "handover"); + return -ENODEV; + } + + reception_stop_param.type = BT_CAP_SET_TYPE_AD_HOC; + reception_stop_param.param = &member_param; + reception_stop_param.count = 1; + + member_param.member.member = peer.conn; + member_param.src_id = peer.src_id; + member_param.num_subgroups = broadcast_create_param.subgroup_count; + + param.adv_type = adv_info.addr->type; + param.adv_sid = adv_info.sid; + param.broadcast_id = broadcast_id; + param.broadcast_source = broadcast_source; + param.unicast_group_param = &unicast_group_param; + /* unicast_audio_start_param will still be valid here, so we can just reuse them */ + param.unicast_start_param = &unicast_audio_start_param; + param.reception_stop_param = &reception_stop_param; + + k_sem_reset(&sem_proc); + + LOG_INF("Handing over %u streams from broadcast to unicast", + unicast_audio_start_param.count); + + err = bt_cap_handover_broadcast_to_unicast(¶m); + if (err != 0) { + LOG_ERR("Failed to handover unicast audio to broadcast: %d\n", err); + return err; + } + + err = k_sem_take(&sem_proc, SEM_TIMEOUT); + if (err != 0) { + LOG_ERR("Timeout on broadcast to unicast: %d", err); + return err; + } + + /* Disable Periodic Advertising as broadcast has now stopped */ + err = bt_le_per_adv_stop(ext_adv); + if (err != 0) { + LOG_ERR("Failed to enable periodic advertising: %d\n", err); + return err; + } + + /* Disable Extended Advertising as broadcast has now stopped */ + err = bt_le_ext_adv_stop(ext_adv); + if (err != 0) { + LOG_ERR("Failed to enable extended advertising: %d\n", err); + return err; + } + + return 0; +} + +static int init_cap_handover(void) +{ + static struct bt_bap_broadcast_assistant_cb broadcast_assistant_cbs = { + .discover = bap_broadcast_assistant_discover_cb, + .recv_state = bap_broadcast_assistant_recv_state_cb, + .recv_state_removed = bap_broadcast_assistant_recv_state_removed_cb, + }; + static struct bt_bap_unicast_client_cb unicast_client_cbs = { + .discover = discover_cb, + .pac_record = pac_record_cb, + .endpoint = endpoint_cb, + }; + static struct bt_cap_initiator_cb cap_initiator_cb = { + .unicast_discovery_complete = cap_discovery_complete_cb, + .unicast_start_complete = unicast_start_complete_cb, + .broadcast_stopped = broadcast_stopped_cb, + }; + static struct bt_cap_handover_cb cap_handover_cb = { + .unicast_to_broadcast_complete = unicast_to_broadcast_complete_cb, + .broadcast_to_unicast_complete = broadcast_to_unicast_complete_cb, + }; + static struct bt_gatt_cb gatt_callbacks = { + .att_mtu_updated = att_mtu_updated_cb, + }; + static struct bt_le_scan_cb scan_callbacks = { + .recv = scan_recv_cb, + }; + static struct bt_bap_stream_ops stream_ops = { + .configured = stream_configured_cb, + .qos_set = stream_qos_set_cb, + .enabled = stream_enabled_cb, + .started = stream_started_cb, + .metadata_updated = stream_metadata_updated_cb, + .disabled = stream_disabled_cb, + .stopped = stream_stopped_cb, + .released = stream_released_cb, + .sent = stream_sent_cb, + }; + int err; + + err = bt_enable(NULL); + if (err != 0) { + LOG_ERR("Bluetooth enable failed: %d", err); + + return err; + } + + err = bt_cap_initiator_register_cb(&cap_initiator_cb); + if (err != 0) { + LOG_ERR("Failed to register CAP initiator callbacks: %d", err); + + return err; + } + + err = bt_cap_handover_register_cb(&cap_handover_cb); + if (err != 0) { + LOG_ERR("Failed to register CAP handover callbacks: %d", err); + + return err; + } + + err = bt_bap_unicast_client_register_cb(&unicast_client_cbs); + if (err != 0) { + LOG_ERR("Failed to register BAP unicast client callbacks: %d", err); + + return err; + } + + err = bt_bap_broadcast_assistant_register_cb(&broadcast_assistant_cbs); + if (err != 0) { + LOG_ERR("Failed to register broadcast assistant callbacks: %d", err); + return err; + } + + ARRAY_FOR_EACH_PTR(peer.streams, stream) { + bt_cap_stream_ops_register(stream, &stream_ops); + } + + bt_gatt_cb_register(&gatt_callbacks); + bt_le_scan_cb_register(&scan_callbacks); + + if (IS_ENABLED(CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK)) { + cap_stream_tx_init(); + } + + return 0; +} + +static int reset_cap_handover(void) +{ + int err; + + LOG_INF("Resetting"); + + if (peer.conn != NULL) { + err = bt_conn_disconnect(peer.conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); + if (err != 0) { + return err; + } + + err = k_sem_take(&sem_state_change, K_FOREVER); + if (err != 0) { + LOG_ERR("Timeout on disconnect: %d", err); + return err; + } + } + + /* If there are streams with active endpoints, we wait until they have been released */ + ARRAY_FOR_EACH_PTR(peer.streams, stream) { + if (stream->bap_stream.ep != NULL) { + err = k_sem_take(&sem_streams, SEM_TIMEOUT); + if (err != 0) { + LOG_ERR("Timeout on sem_streams: %d", err); + return err; + } + } + } + + if (unicast_group != NULL) { + err = unicast_group_delete(); + if (err != 0) { + return err; + } + + return err; + } + + if (broadcast_source != NULL) { + /* Stop broadcast if active - Since this is an asynchronous operation we have to + * wait for the */ + if (k_sem_count_get(&sem_broadcast_stopped) == 0U) { + err = bt_cap_initiator_broadcast_audio_stop(broadcast_source); + if (err != 0) { + /* -EBADMSG is returned if the broadcast source is already stopped + */ + LOG_ERR("Could not stop broadcast source: %d", err); + return err; + } + + err = k_sem_take(&sem_broadcast_stopped, SEM_TIMEOUT); + if (err != 0) { + LOG_ERR("Timeout on broadcast stop: %d", err); + return err; + } + } + + err = bt_cap_initiator_broadcast_audio_delete(broadcast_source); + if (err != 0) { + /* -EBADMSG is returned if the broadcast source is already stopped */ + LOG_ERR("Could not delete broadcast source: %d", err); + return err; + } + broadcast_source = NULL; + } + + k_sem_reset(&sem_proc); + k_sem_reset(&sem_state_change); + k_sem_reset(&sem_mtu_exchanged); + k_sem_reset(&sem_security_changed); + k_sem_reset(&sem_receive_state_updated); + k_sem_reset(&sem_streams); + + /* Since sem_broadcast_stopped needs to be reset to 1, we use k_sem_give instead of + * k_sem_reset + */ + k_sem_give(&sem_broadcast_stopped); + + total_tx_iso_packet_count = 0U; + + return 0; +} + +int main(void) +{ + int err; + + err = init_cap_handover(); + if (err != 0) { + LOG_ERR("Initialization failed: %d", err); + + return 0; + } + + LOG_INF("Bluetooth initialized"); + +#if defined(CONFIG_STATIC_BROADCAST_ID) + broadcast_id = CONFIG_BROADCAST_ID; +#else + broadcast_id = 0U; + err = bt_rand(&broadcast_id, BT_AUDIO_BROADCAST_ID_SIZE); + if (err != 0) { + LOG_ERR("Failed to generate broadcast ID: %d\n", err); + return err; + } + LOG_INF("Generated broadcast_id: %06X", broadcast_id); +#endif /* CONFIG_STATIC_BROADCAST_ID */ + + err = setup_broadcast_adv(); + if (err != 0) { + LOG_ERR("Failed to setup broadcast adv: %d\n", err); + return err; + } + + while (true) { + err = reset_cap_handover(); + if (err != 0) { + LOG_ERR("Failed to reset"); + + return err; + } + + /* Start scanning for and connecting to CAP Acceptors. CAP Acceptors are identified + * by their advertising data + */ + err = scan_and_connect(); + if (err != 0) { + continue; + } + + /* BAP mandates support for an MTU of at least 65 octets. Because of that, we + * should always exchange the MTU before accessing BAP related services to ensure + * correctness + */ + err = exchange_mtu(); + if (err != 0) { + continue; + } + + /* LE Audio services require encryption with LE Secure Connections, so we increase + * security before attempting to do any LE Audio operations + */ + err = update_security(); + if (err != 0) { + continue; + } + + /* All remote CAP Acceptors shall have the Common Audio Service (CAS) so we start by + * discovering that on the remote device to determine if they are really CAP + * Acceptors. If they are only a BAP Unicast Server we ignore them. + */ + err = discover_cas(); + if (err != 0) { + continue; + } + + /* Discover sink ASEs and capabilities. This may not result in any endpoints if they + * remote device is only a source (e.g. a microphone) + */ + err = discover_sinks(); + if (err != 0) { + continue; + } + + /* Discover the BASS so that we can add our broadcast source to the acceptor */ + err = discover_bass(); + if (err != 0) { + continue; + } + + /* Create a unicast group (Connected Isochronous Group (CIG)) based on what we have + * found on the remote device + */ + err = unicast_group_create(); + if (err != 0) { + continue; + } + + /* Execute the start operation which will take one or more streams into the + * streaming state + */ + err = unicast_audio_start(); + if (err != 0) { + continue; + } + + while (true) { + /* Switch between unicast and broadcast */ + + /* Wait between switches */ + k_sleep(K_SECONDS(10)); + + err = handover_unicast_to_broadcast(); + if (err != 0) { + break; + } + + /* Wait between switches */ + k_sleep(K_SECONDS(10)); + + err = handover_broadcast_to_unicast(); + if (err != 0) { + break; + } + } + + /* If handover failed, we attempt to disconnect if not already */ + if (peer.conn != NULL) { + err = bt_conn_disconnect(peer.conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); + if (err != 0) { + LOG_ERR("Failed to disconnect: %d", err); + continue; + } + } + + /* Reset if disconnected */ + err = k_sem_take(&sem_state_change, K_FOREVER); + if (err != 0) { + LOG_ERR("Failed to take sem_state_change: err %d", err); + + return err; + } + } + + return 0; +} diff --git a/samples/bluetooth/cap_handover/sysbuild.cmake b/samples/bluetooth/cap_handover/sysbuild.cmake new file mode 100644 index 0000000000000..b3ff4fc7d146a --- /dev/null +++ b/samples/bluetooth/cap_handover/sysbuild.cmake @@ -0,0 +1,29 @@ +# Copyright (c) 2023 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +if(SB_CONFIG_NET_CORE_IMAGE_HCI_IPC) + # For builds in the nrf5340, we build the netcore image with the controller + + set(NET_APP hci_ipc) + set(NET_APP_SRC_DIR ${ZEPHYR_BASE}/samples/bluetooth/${NET_APP}) + + ExternalZephyrProject_Add( + APPLICATION ${NET_APP} + SOURCE_DIR ${NET_APP_SRC_DIR} + BOARD ${SB_CONFIG_NET_CORE_BOARD} + ) + + # set(${NET_APP}_EXTRA_CONF_FILE + # ${ZEPHYR_BASE}/samples/bluetooth/cap_handover/overlay-bt_ll_sw_split.conf + # CACHE INTERNAL "" + # ) + + set(${NET_APP}_CONF_FILE + ${NET_APP_SRC_DIR}/nrf5340_cpunet_iso-bt_ll_sw_split.conf + CACHE INTERNAL "" + ) + + native_simulator_set_child_images(${DEFAULT_IMAGE} ${NET_APP}) +endif() + +native_simulator_set_final_executable(${DEFAULT_IMAGE}) diff --git a/samples/bluetooth/cap_initiator/src/cap_initiator_unicast.c b/samples/bluetooth/cap_initiator/src/cap_initiator_unicast.c index 3fd08baaed7e4..1ce2f13a51fd1 100644 --- a/samples/bluetooth/cap_initiator/src/cap_initiator_unicast.c +++ b/samples/bluetooth/cap_initiator/src/cap_initiator_unicast.c @@ -45,7 +45,7 @@ LOG_MODULE_REGISTER(cap_initiator_unicast, LOG_LEVEL_INF); static struct bt_bap_lc3_preset unicast_preset_16_2_1 = BT_BAP_LC3_UNICAST_PRESET_16_2_1( BT_AUDIO_LOCATION_MONO_AUDIO, BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED); static struct bt_cap_unicast_group *unicast_group; -uint64_t total_rx_iso_packet_count; /* This value is exposed to test code */ +uint64_t total_unicast_rx_iso_packet_count; /* This value is exposed to test code */ uint64_t total_unicast_tx_iso_packet_count; /* This value is exposed to test code */ /** Struct to contain information for a specific peer (CAP) device */ @@ -119,8 +119,6 @@ static void unicast_stream_enabled_cb(struct bt_bap_stream *stream) static void unicast_stream_started_cb(struct bt_bap_stream *stream) { LOG_INF("Started stream %p", stream); - total_rx_iso_packet_count = 0U; - total_unicast_tx_iso_packet_count = 0U; if (is_tx_stream(stream)) { struct bt_cap_stream *cap_stream = @@ -179,11 +177,11 @@ static void unicast_stream_recv_cb(struct bt_bap_stream *stream, * (see the `info->flags` for which flags to check), */ - if ((total_rx_iso_packet_count % 100U) == 0U) { - LOG_INF("Received %llu HCI ISO data packets", total_rx_iso_packet_count); + if ((total_unicast_rx_iso_packet_count % 100U) == 0U) { + LOG_INF("Received %llu HCI ISO data packets", total_unicast_rx_iso_packet_count); } - total_rx_iso_packet_count++; + total_unicast_rx_iso_packet_count++; } static void unicast_stream_sent_cb(struct bt_bap_stream *stream) @@ -801,6 +799,9 @@ static int reset_cap_initiator(void) k_sem_reset(&sem_state_change); k_sem_reset(&sem_mtu_exchanged); + total_unicast_rx_iso_packet_count = 0U; + total_unicast_tx_iso_packet_count = 0U; + return 0; } diff --git a/scripts/native_simulator/common/src/nsi_tasks.h b/scripts/native_simulator/common/src/include/nsi_tasks.h similarity index 100% rename from scripts/native_simulator/common/src/nsi_tasks.h rename to scripts/native_simulator/common/src/include/nsi_tasks.h diff --git a/scripts/native_simulator/common/src/nce.c b/scripts/native_simulator/common/src/nce.c index 86b4abf59cd73..7380088a3c6a9 100644 --- a/scripts/native_simulator/common/src/nce.c +++ b/scripts/native_simulator/common/src/nce.c @@ -52,7 +52,7 @@ NSI_INLINE int nce_sem_rewait(sem_t *semaphore) { int ret; - while ((ret = sem_wait(semaphore)) == EINTR) { + while (((ret = sem_wait(semaphore)) == -1) && (errno == EINTR)) { /* Restart wait if we were interrupted */ } return ret; diff --git a/scripts/native_simulator/common/src/nct.c b/scripts/native_simulator/common/src/nct.c index a2ef8a1f6282c..3e5c944925113 100644 --- a/scripts/native_simulator/common/src/nct.c +++ b/scripts/native_simulator/common/src/nct.c @@ -146,7 +146,7 @@ NSI_INLINE int nct_sem_rewait(sem_t *semaphore) { int ret; - while ((ret = sem_wait(semaphore)) == EINTR) { + while (((ret = sem_wait(semaphore)) == -1) && (errno == EINTR)) { /* Restart wait if we were interrupted */ } return ret; diff --git a/scripts/native_simulator/common/src/nsi_internal.h b/scripts/native_simulator/common/src/nsi_internal.h index ebe8289b17831..d054064bc91b3 100644 --- a/scripts/native_simulator/common/src/nsi_internal.h +++ b/scripts/native_simulator/common/src/nsi_internal.h @@ -8,6 +8,7 @@ #define NSI_COMMON_SRC_NSI_INTERNAL_H #include +#include "nsi_utils.h" #ifdef __cplusplus extern "C" { @@ -23,8 +24,7 @@ extern "C" { * * @return least significant bit set, 0 if @a op is 0 */ - -static inline unsigned int nsi_find_lsb_set(uint32_t op) +NSI_INLINE unsigned int nsi_find_lsb_set(uint32_t op) { return __builtin_ffs(op); } @@ -39,8 +39,7 @@ static inline unsigned int nsi_find_lsb_set(uint32_t op) * * @return least significant bit set, 0 if @a op is 0 */ - -static inline unsigned int nsi_find_lsb_set64(uint64_t op) +NSI_INLINE unsigned int nsi_find_lsb_set64(uint64_t op) { return __builtin_ffsll(op); } diff --git a/subsys/bluetooth/audio/bap_broadcast_assistant.c b/subsys/bluetooth/audio/bap_broadcast_assistant.c index 77ead34fef688..fca4bf39d7013 100644 --- a/subsys/bluetooth/audio/bap_broadcast_assistant.c +++ b/subsys/bluetooth/audio/bap_broadcast_assistant.c @@ -581,7 +581,7 @@ static uint8_t read_recv_state_cb(struct bt_conn *conn, uint8_t err, (void)memset(params, 0, sizeof(*params)); - LOG_DBG("%s receive state", active_recv_state ? "Active " : "Inactive"); + LOG_DBG("%s receive state", active_recv_state ? "Active" : "Inactive"); if (cb_err == 0 && active_recv_state) { int16_t index; @@ -1407,7 +1407,9 @@ int bt_bap_broadcast_assistant_add_src(struct bt_conn *conn, /* Check if this operation would result in a duplicate before proceeding */ if (broadcast_src_is_duplicate(inst, param->broadcast_id, param->adv_sid, param->addr.type)) { - LOG_DBG("Broadcast source already exists"); + LOG_DBG("Broadcast source already exists for broadcast_id 0x%06X, sid 0x%02X and " + "type 0x%02X", + param->broadcast_id, param->adv_sid, param->addr.type); return -EINVAL; } diff --git a/subsys/bluetooth/audio/cap_handover.c b/subsys/bluetooth/audio/cap_handover.c index 3ebc1b2e4b861..6267e149a2bdc 100644 --- a/subsys/bluetooth/audio/cap_handover.c +++ b/subsys/bluetooth/audio/cap_handover.c @@ -23,6 +23,7 @@ #include #include #include +#include #include "bap_endpoint.h" #include "cap_internal.h" @@ -146,7 +147,7 @@ void bt_cap_handover_broadcast_source_stopped(uint8_t reason) if (proc_param->is_unicast_to_broadcast) { LOG_DBG("Unexpected broadcast source stop with reason 0x%02x", reason); - const int err = bt_cap_initiator_broadcast_audio_delete( + __maybe_unused const int err = bt_cap_initiator_broadcast_audio_delete( proc_param->unicast_to_broadcast.broadcast_source); __ASSERT_NO_MSG(err == 0); @@ -259,9 +260,14 @@ void bt_cap_handover_unicast_to_broadcast_reception_start(void) err = cap_commander_broadcast_reception_start(¶m); if (err != 0) { + struct bt_cap_commander_broadcast_reception_start_member_param *member_param = + &proc_param->unicast_to_broadcast.reception_start_member_params[0]; + struct bt_conn *member_conn = bt_cap_common_get_member_conn( + proc_param->unicast_to_broadcast.type, &member_param->member); + LOG_DBG("Failed to start reception start: %d", err); active_proc->err = err; - active_proc->failed_conn = NULL; + active_proc->failed_conn = member_conn; bt_cap_handover_complete(); } diff --git a/tests/bsim/bluetooth/audio_samples/cap/acceptor/src/cap_acceptor_sample_test.c b/tests/bsim/bluetooth/audio_samples/cap/acceptor/src/cap_acceptor_sample_test.c index cc15a7807696d..a08ebe0d77715 100644 --- a/tests/bsim/bluetooth/audio_samples/cap/acceptor/src/cap_acceptor_sample_test.c +++ b/tests/bsim/bluetooth/audio_samples/cap/acceptor/src/cap_acceptor_sample_test.c @@ -14,7 +14,7 @@ #include "bs_utils.h" #include "bstests.h" -#define WAIT_TIME 15 /* Seconds */ +#define WAIT_TIME 60 /* Seconds */ #define PASS_THRESHOLD 100 /* Audio packets */ @@ -51,13 +51,14 @@ static void test_cap_acceptor_sample_tick(bs_time_t HW_device_time) extern uint64_t total_unicast_tx_iso_packet_count; bs_trace_info_time(2, "%" PRIu64 " unicast packets received, expected >= %i\n", - total_unicast_tx_iso_packet_count, PASS_THRESHOLD); + total_unicast_rx_iso_packet_count, PASS_THRESHOLD); bs_trace_info_time(2, "%" PRIu64 " unicast packets sent, expected >= %i\n", total_unicast_tx_iso_packet_count, PASS_THRESHOLD); - if (total_unicast_rx_iso_packet_count < PASS_THRESHOLD || + /* Fail if we neither sent or receive any ISO packets */ + if (total_unicast_rx_iso_packet_count < PASS_THRESHOLD && total_unicast_tx_iso_packet_count < PASS_THRESHOLD) { - FAIL("cap_acceptor FAILED with(Did not pass after %d seconds)\n ", + FAIL("cap_acceptor unicast FAILED (Did not pass after %d seconds)\n ", WAIT_TIME); return; } @@ -70,7 +71,7 @@ static void test_cap_acceptor_sample_tick(bs_time_t HW_device_time) total_broadcast_rx_iso_packet_count, PASS_THRESHOLD); if (total_broadcast_rx_iso_packet_count < PASS_THRESHOLD) { - FAIL("cap_acceptor FAILED with (Did not pass after %d seconds)\n ", + FAIL("cap_acceptor broadcast FAILED (Did not pass after %d seconds)\n ", WAIT_TIME); return; } diff --git a/tests/bsim/bluetooth/audio_samples/cap/compile.sh b/tests/bsim/bluetooth/audio_samples/cap/compile.sh index 73457639187a0..fca3b08c8c075 100755 --- a/tests/bsim/bluetooth/audio_samples/cap/compile.sh +++ b/tests/bsim/bluetooth/audio_samples/cap/compile.sh @@ -29,9 +29,20 @@ if [ "${BOARD_TS}" == "nrf5340bsim_nrf5340_cpuapp" ]; then exe_name=bs_${BOARD_TS}_${app}_unicast_prj_conf sysbuild=1 compile app=tests/bsim/bluetooth/audio_samples/cap/acceptor \ sample=${ZEPHYR_BASE}/samples/bluetooth/cap_acceptor \ + cmake_extra_args="-DCONFIG_SAMPLE_BROADCAST=n" \ conf_file=${sample}/prj.conf \ conf_overlay=${sample}/boards/nrf5340_audio_dk_nrf5340_cpuapp.conf \ exe_name=bs_${BOARD_TS}_${app}_unicast_prj_conf sysbuild=1 compile + app=tests/bsim/bluetooth/audio_samples/cap/handover \ + sample=${ZEPHYR_BASE}/samples/bluetooth/cap_handover \ + conf_file=${sample}/prj.conf \ + conf_overlay=${sample}/boards/nrf5340_audio_dk_nrf5340_cpuapp.conf \ + exe_name=bs_${BOARD_TS}_${app}_prj_conf sysbuild=1 compile + app=tests/bsim/bluetooth/audio_samples/cap/acceptor \ + sample=${ZEPHYR_BASE}/samples/bluetooth/cap_acceptor \ + conf_file=${sample}/prj.conf \ + conf_overlay=${sample}/boards/nrf5340_audio_dk_nrf5340_cpuapp.conf \ + exe_name=bs_${BOARD_TS}_${app}_handover_prj_conf sysbuild=1 compile else app=tests/bsim/bluetooth/audio_samples/cap/initiator \ sample=${ZEPHYR_BASE}/samples/bluetooth/cap_initiator \ @@ -52,9 +63,20 @@ else exe_name=bs_${BOARD_TS}_${app}_unicast_prj_conf sysbuild=1 compile app=tests/bsim/bluetooth/audio_samples/cap/acceptor \ sample=${ZEPHYR_BASE}/samples/bluetooth/cap_acceptor \ + cmake_extra_args="-DCONFIG_SAMPLE_BROADCAST=n" \ conf_file=${sample}/prj.conf \ conf_overlay=${sample}/overlay-bt_ll_sw_split.conf \ exe_name=bs_${BOARD_TS}_${app}_unicast_prj_conf sysbuild=1 compile + app=tests/bsim/bluetooth/audio_samples/cap/handover \ + sample=${ZEPHYR_BASE}/samples/bluetooth/cap_handover \ + conf_file=${sample}/prj.conf \ + conf_overlay=${sample}/overlay-bt_ll_sw_split.conf \ + exe_name=bs_${BOARD_TS}_${app}_prj_conf sysbuild=1 compile + app=tests/bsim/bluetooth/audio_samples/cap/acceptor \ + sample=${ZEPHYR_BASE}/samples/bluetooth/cap_acceptor \ + conf_file=${sample}/prj.conf \ + conf_overlay=${sample}/overlay-bt_ll_sw_split.conf \ + exe_name=bs_${BOARD_TS}_${app}_handover_prj_conf sysbuild=1 compile fi wait_for_background_jobs diff --git a/tests/bsim/bluetooth/audio_samples/cap/handover/CMakeLists.txt b/tests/bsim/bluetooth/audio_samples/cap/handover/CMakeLists.txt new file mode 100644 index 0000000000000..9737598c821b8 --- /dev/null +++ b/tests/bsim/bluetooth/audio_samples/cap/handover/CMakeLists.txt @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(cap_handover_self_tets) + +set(cap_handover_path ${ZEPHYR_BASE}/samples/bluetooth/cap_handover) + +target_sources(app PRIVATE + src/cap_handover_sample_test.c + src/test_main.c + + ${cap_handover_path}/src/main.c + ${cap_handover_path}/src/cap_stream_tx.c +) + +zephyr_library_include_directories(${ZEPHYR_BASE}/samples/bluetooth) + +zephyr_include_directories( + ${BSIM_COMPONENTS_PATH}/libUtilv1/src/ + ${BSIM_COMPONENTS_PATH}/libPhyComv1/src/ + ) diff --git a/tests/bsim/bluetooth/audio_samples/cap/handover/Kconfig b/tests/bsim/bluetooth/audio_samples/cap/handover/Kconfig new file mode 100644 index 0000000000000..1f8ed7ad6d7f4 --- /dev/null +++ b/tests/bsim/bluetooth/audio_samples/cap/handover/Kconfig @@ -0,0 +1,4 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +source "$(ZEPHYR_BASE)/samples/bluetooth/cap_handover/Kconfig" diff --git a/tests/bsim/bluetooth/audio_samples/cap/handover/Kconfig.sysbuild b/tests/bsim/bluetooth/audio_samples/cap/handover/Kconfig.sysbuild new file mode 100644 index 0000000000000..19a7c557b1cd2 --- /dev/null +++ b/tests/bsim/bluetooth/audio_samples/cap/handover/Kconfig.sysbuild @@ -0,0 +1,10 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +source "$(ZEPHYR_BASE)/samples/bluetooth/cap_handover/Kconfig.sysbuild" + +config NATIVE_SIMULATOR_PRIMARY_MCU_INDEX + int + # Let's pass the test arguments to the application MCU test + # otherwise by default they would have gone to the net core. + default 0 if $(BOARD_TARGET_STRING) = "NRF5340BSIM_NRF5340_CPUAPP" diff --git a/tests/bsim/bluetooth/audio_samples/cap/handover/prj.conf b/tests/bsim/bluetooth/audio_samples/cap/handover/prj.conf new file mode 100644 index 0000000000000..49bfee2a1fab9 --- /dev/null +++ b/tests/bsim/bluetooth/audio_samples/cap/handover/prj.conf @@ -0,0 +1,2 @@ +# Please build using the sample configuration file: +# ${ZEPHYR_BASE}/samples/bluetooth/cap_handover/prj.conf diff --git a/tests/bsim/bluetooth/audio_samples/cap/handover/src/cap_handover_sample_test.c b/tests/bsim/bluetooth/audio_samples/cap/handover/src/cap_handover_sample_test.c new file mode 100644 index 0000000000000..693b7d41df1b8 --- /dev/null +++ b/tests/bsim/bluetooth/audio_samples/cap/handover/src/cap_handover_sample_test.c @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include + +#include + +#include "bs_types.h" +#include "bs_tracing.h" +#include "bs_utils.h" +#include "bstests.h" + +#define WAIT_TIME 60 /* Seconds */ + +#define PASS_THRESHOLD 100 /* Audio packets */ + +extern enum bst_result_t bst_result; + +#define FAIL(...) \ + do { \ + bst_result = Failed; \ + bs_trace_error_time_line(__VA_ARGS__); \ + } while (0) + +#define PASS(...) \ + do { \ + bst_result = Passed; \ + bs_trace_info_time(1, __VA_ARGS__); \ + } while (0) + +static void test_cap_handover_sample_init(void) +{ + bst_ticker_set_next_tick_absolute(WAIT_TIME * 1e6); + bst_result = In_progress; +} + +static void test_cap_handover_sample_tick(bs_time_t HW_device_time) +{ + /* + * If in WAIT_TIME seconds we did not get enough packets through + * we consider the test failed + */ + + extern uint64_t total_tx_iso_packet_count; + + bs_trace_info_time(2, "%" PRIu64 " unicast packets sent, expected >= %i\n", + total_tx_iso_packet_count, PASS_THRESHOLD); + + if (total_tx_iso_packet_count < PASS_THRESHOLD) { + FAIL("cap_handover FAILED with(Did not pass after %d seconds)\n", WAIT_TIME); + return; + } + + PASS("cap_handover PASSED\n"); +} + +static const struct bst_test_instance test_sample[] = { + { + .test_id = "cap_handover", + .test_descr = "Test based on the unicast client sample. " + "It expects to be connected to a compatible unicast server, " + "waits for " STR(WAIT_TIME) " seconds, and checks how " + "many audio packets have been received correctly", + .test_post_init_f = test_cap_handover_sample_init, + .test_tick_f = test_cap_handover_sample_tick, + }, + BSTEST_END_MARKER}; + +struct bst_test_list *test_cap_handover_sample_install(struct bst_test_list *tests) +{ + tests = bst_add_tests(tests, test_sample); + return tests; +} diff --git a/tests/bsim/bluetooth/audio_samples/cap/handover/src/test_main.c b/tests/bsim/bluetooth/audio_samples/cap/handover/src/test_main.c new file mode 100644 index 0000000000000..d455567d2e847 --- /dev/null +++ b/tests/bsim/bluetooth/audio_samples/cap/handover/src/test_main.c @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include + +#include "bstests.h" + +extern struct bst_test_list *test_cap_handover_sample_install(struct bst_test_list *tests); + +bst_test_install_t test_installers[] = { + test_cap_handover_sample_install, + NULL, +}; diff --git a/tests/bsim/bluetooth/audio_samples/cap/handover/sysbuild.cmake b/tests/bsim/bluetooth/audio_samples/cap/handover/sysbuild.cmake new file mode 100644 index 0000000000000..4fd7fec10c9d8 --- /dev/null +++ b/tests/bsim/bluetooth/audio_samples/cap/handover/sysbuild.cmake @@ -0,0 +1,6 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +include(${ZEPHYR_BASE}/samples/bluetooth/cap_handover/sysbuild.cmake) + +native_simulator_set_primary_mcu_index(${DEFAULT_IMAGE} ${NET_APP}) diff --git a/tests/bsim/bluetooth/audio_samples/cap/initiator/src/cap_initiator_sample_test.c b/tests/bsim/bluetooth/audio_samples/cap/initiator/src/cap_initiator_sample_test.c index 4c87c43979652..bddda6885b8fa 100644 --- a/tests/bsim/bluetooth/audio_samples/cap/initiator/src/cap_initiator_sample_test.c +++ b/tests/bsim/bluetooth/audio_samples/cap/initiator/src/cap_initiator_sample_test.c @@ -14,7 +14,7 @@ #include "bs_utils.h" #include "bstests.h" -#define WAIT_TIME 15 /* Seconds */ +#define WAIT_TIME 60 /* Seconds */ #define PASS_THRESHOLD 100 /* Audio packets */ @@ -46,17 +46,17 @@ static void test_cap_initiator_sample_tick(bs_time_t HW_device_time) */ if (IS_ENABLED(CONFIG_SAMPLE_UNICAST)) { - extern uint64_t total_rx_iso_packet_count; + extern uint64_t total_unicast_rx_iso_packet_count; extern uint64_t total_unicast_tx_iso_packet_count; bs_trace_info_time(2, "%" PRIu64 " unicast packets received, expected >= %i\n", - total_rx_iso_packet_count, PASS_THRESHOLD); + total_unicast_rx_iso_packet_count, PASS_THRESHOLD); bs_trace_info_time(2, "%" PRIu64 " unicast packets sent, expected >= %i\n", total_unicast_tx_iso_packet_count, PASS_THRESHOLD); - if (total_rx_iso_packet_count < PASS_THRESHOLD || + if (total_unicast_rx_iso_packet_count < PASS_THRESHOLD || total_unicast_tx_iso_packet_count < PASS_THRESHOLD) { - FAIL("cap_initiator FAILED with(Did not pass after %d seconds)\n ", + FAIL("cap_initiator unicast FAILED (Did not pass after %d seconds)\n ", WAIT_TIME); return; } @@ -69,7 +69,7 @@ static void test_cap_initiator_sample_tick(bs_time_t HW_device_time) total_broadcast_tx_iso_packet_count, PASS_THRESHOLD); if (total_broadcast_tx_iso_packet_count < PASS_THRESHOLD) { - FAIL("cap_initiator FAILED with (Did not pass after %d seconds)\n ", + FAIL("cap_initiator broadcast FAILED (Did not pass after %d seconds)\n ", WAIT_TIME); return; } diff --git a/tests/bsim/bluetooth/audio_samples/cap/tests_scripts/cap_broadcast.sh b/tests/bsim/bluetooth/audio_samples/cap/tests_scripts/cap_broadcast.sh index 1791b48ed67f5..a0cc29d42c368 100755 --- a/tests/bsim/bluetooth/audio_samples/cap/tests_scripts/cap_broadcast.sh +++ b/tests/bsim/bluetooth/audio_samples/cap/tests_scripts/cap_broadcast.sh @@ -22,6 +22,6 @@ Execute ./bs_${BOARD_TS}_tests_bsim_bluetooth_audio_samples_cap_acceptor_broadca -v=${verbosity_level} -s=${simulation_id} -d=1 -RealEncryption=1 -testid=cap_acceptor Execute ./bs_2G4_phy_v1 -v=${verbosity_level} -s=${simulation_id} \ - -D=2 -sim_length=20e6 $@ -argschannel -at=40 + -D=2 -sim_length=120e6 $@ -argschannel -at=40 wait_for_background_jobs #Wait for all programs in background and return != 0 if any fails diff --git a/tests/bsim/bluetooth/audio_samples/cap/tests_scripts/cap_handover.sh b/tests/bsim/bluetooth/audio_samples/cap/tests_scripts/cap_handover.sh new file mode 100755 index 0000000000000..08c193908e06f --- /dev/null +++ b/tests/bsim/bluetooth/audio_samples/cap/tests_scripts/cap_handover.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Copyright 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +# Simple selfchecking test for the CAP samples for handover. +# It relies on the bs_tests hooks to register a test timer callback, which after a deadline +# will check how many audio packets the acceptor has received + +simulation_id="cap_handover_test" +verbosity_level=2 +EXECUTE_TIMEOUT=120 + +source ${ZEPHYR_BASE}/tests/bsim/sh_common.source + +cd ${BSIM_OUT_PATH}/bin + +Execute ./bs_${BOARD_TS}_tests_bsim_bluetooth_audio_samples_cap_handover_prj_conf \ + -v=${verbosity_level} -s=${simulation_id} -d=0 -RealEncryption=1 -testid=cap_handover + +Execute ./bs_${BOARD_TS}_tests_bsim_bluetooth_audio_samples_cap_acceptor_handover_prj_conf \ + -v=${verbosity_level} -s=${simulation_id} -d=1 -RealEncryption=1 -testid=cap_acceptor + +Execute ./bs_2G4_phy_v1 -v=${verbosity_level} -s=${simulation_id} \ + -D=2 -sim_length=120e6 $@ -argschannel -at=40 + +wait_for_background_jobs #Wait for all programs in background and return != 0 if any fails diff --git a/tests/bsim/bluetooth/audio_samples/cap/tests_scripts/cap_unicast.sh b/tests/bsim/bluetooth/audio_samples/cap/tests_scripts/cap_unicast.sh index cc9d9fc9c83e5..f1f9d9f9549e8 100755 --- a/tests/bsim/bluetooth/audio_samples/cap/tests_scripts/cap_unicast.sh +++ b/tests/bsim/bluetooth/audio_samples/cap/tests_scripts/cap_unicast.sh @@ -22,6 +22,6 @@ Execute ./bs_${BOARD_TS}_tests_bsim_bluetooth_audio_samples_cap_acceptor_unicast -v=${verbosity_level} -s=${simulation_id} -d=1 -RealEncryption=1 -testid=cap_acceptor Execute ./bs_2G4_phy_v1 -v=${verbosity_level} -s=${simulation_id} \ - -D=2 -sim_length=20e6 $@ -argschannel -at=40 + -D=2 -sim_length=120e6 $@ -argschannel -at=40 wait_for_background_jobs #Wait for all programs in background and return != 0 if any fails