diff --git a/doc/releases/release-notes-4.2.rst b/doc/releases/release-notes-4.2.rst index 124bf7543c23..67ea83f2aa72 100644 --- a/doc/releases/release-notes-4.2.rst +++ b/doc/releases/release-notes-4.2.rst @@ -146,6 +146,12 @@ New APIs and options * :c:macro:`BT_BAP_PER_ADV_PARAM_BROADCAST_SLOW` * :c:func:`bt_csip_set_member_set_size_and_rank` * :c:func:`bt_csip_set_member_get_info` + * :c:func:`bt_bap_unicast_group_foreach_stream` + * :c:func:`bt_cap_unicast_group_create` + * :c:func:`bt_cap_unicast_group_reconfig` + * :c:func:`bt_cap_unicast_group_add_streams` + * :c:func:`bt_cap_unicast_group_delete` + * :c:func:`bt_cap_unicast_group_foreach_stream` * Host diff --git a/include/zephyr/bluetooth/audio/bap.h b/include/zephyr/bluetooth/audio/bap.h index 7c7ccdc39f19..74b7fc7d7daa 100644 --- a/include/zephyr/bluetooth/audio/bap.h +++ b/include/zephyr/bluetooth/audio/bap.h @@ -3,7 +3,7 @@ * @brief Header for Bluetooth BAP. * * Copyright (c) 2020 Bose Corporation - * Copyright (c) 2021-2024 Nordic Semiconductor ASA + * Copyright (c) 2021-2025 Nordic Semiconductor ASA * * SPDX-License-Identifier: Apache-2.0 */ @@ -1720,6 +1720,32 @@ int bt_bap_unicast_group_add_streams(struct bt_bap_unicast_group *unicast_group, */ int bt_bap_unicast_group_delete(struct bt_bap_unicast_group *unicast_group); +/** Callback function for bt_bap_unicast_group_foreach_stream() + * + * @param stream The audio stream + * @param user_data User data + * + * @retval true Stop iterating. + * @retval false Continue iterating. + */ +typedef bool (*bt_bap_unicast_group_foreach_stream_func_t)(struct bt_bap_stream *stream, + void *user_data); + +/** + * @brief Iterate through all streams in a unicast group + * + * @param unicast_group The unicast group + * @param func The callback function + * @param user_data User specified data that sent to the callback function + * + * @retval 0 Success (even if no streams exists in the group). + * @retval -ECANCELED Iteration was stopped by the callback function before complete. + * @retval -EINVAL @p unicast_group or @p func were NULL. + */ +int bt_bap_unicast_group_foreach_stream(struct bt_bap_unicast_group *unicast_group, + bt_bap_unicast_group_foreach_stream_func_t func, + void *user_data); + /** Unicast Client callback structure */ struct bt_bap_unicast_client_cb { /** diff --git a/include/zephyr/bluetooth/audio/cap.h b/include/zephyr/bluetooth/audio/cap.h index 046002318dbf..861f4f43ef7e 100644 --- a/include/zephyr/bluetooth/audio/cap.h +++ b/include/zephyr/bluetooth/audio/cap.h @@ -4,7 +4,7 @@ */ /* - * Copyright (c) 2022-2024 Nordic Semiconductor ASA + * Copyright (c) 2022-2025 Nordic Semiconductor ASA * * SPDX-License-Identifier: Apache-2.0 */ @@ -50,6 +50,9 @@ extern "C" { /** @brief Abstract Audio Broadcast Source structure. */ struct bt_cap_broadcast_source; +/** @brief Abstract CAP Unicast Group structure. */ +struct bt_cap_unicast_group; + /** * @brief Register the Common Audio Service. * @@ -144,6 +147,22 @@ struct bt_cap_initiator_cb { */ void (*broadcast_stopped)(struct bt_cap_broadcast_source *source, uint8_t reason); #endif /* CONFIG_BT_BAP_BROADCAST_SOURCE */ +#if defined(CONFIG_BT_CAP_HANDOVER_SUPPORTED) + /** + * @brief The unicast to broadcast handover procedure has finished + * + * @param err 0 if success else a negative errno value. + * @param conn Pointer to the connection where the error + * occurred or NULL local failure. + * @param unicast_group NULL if the unicast group was deleted during the procedure, else + * pointer to the unicast group provided in the parameters. + * @param broadcast_source Pointer to newly created broadcast source, or NULL in case of an + * error happening before it was created. + */ + void (*unicast_to_broadcast_complete)(int err, struct bt_conn *conn, + struct bt_cap_unicast_group *unicast_group, + struct bt_cap_broadcast_source *broadcast_source); +#endif /* CONFIG_BT_CAP_HANDOVER_SUPPORTED */ }; /** @@ -254,6 +273,173 @@ int bt_cap_stream_send_ts(struct bt_cap_stream *stream, struct net_buf *buf, uin */ int bt_cap_stream_get_tx_sync(struct bt_cap_stream *stream, struct bt_iso_tx_info *info); +/** Parameter struct for each stream in the unicast group */ +struct bt_cap_unicast_group_stream_param { + /** Pointer to a stream object. */ + struct bt_cap_stream *stream; + + /** The QoS settings for the stream object. */ + struct bt_bap_qos_cfg *qos_cfg; +}; + +/** + * @brief Parameter struct for the unicast group functions + * + * Parameter struct for the bt_cap_unicast_group_create() and + * bt_cap_unicast_group_add_streams() functions. + */ +struct bt_cap_unicast_group_stream_pair_param { + /** Pointer to a receiving stream parameters. */ + struct bt_cap_unicast_group_stream_param *rx_param; + + /** Pointer to a transmitting stream parameters. */ + struct bt_cap_unicast_group_stream_param *tx_param; +}; + +/** Parameters for the creating unicast groups with bt_cap_unicast_group_create() */ +struct bt_cap_unicast_group_param { + /** The number of parameters in @p params */ + size_t params_count; + + /** Array of stream parameters */ + struct bt_cap_unicast_group_stream_pair_param *params; + + /** + * @brief Unicast Group packing mode. + * + * @ref BT_ISO_PACKING_SEQUENTIAL or @ref BT_ISO_PACKING_INTERLEAVED. + * + * @note This is a recommendation to the controller, which the controller may ignore. + */ + uint8_t packing; + +#if defined(CONFIG_BT_ISO_TEST_PARAMS) || defined(__DOXYGEN__) + /** + * @brief Central to Peripheral flush timeout + * + * The flush timeout in multiples of ISO_Interval for each payload sent + * from the Central to Peripheral. + * + * Value range from @ref BT_ISO_FT_MIN to @ref BT_ISO_FT_MAX + */ + uint8_t c_to_p_ft; + + /** + * @brief Peripheral to Central flush timeout + * + * The flush timeout in multiples of ISO_Interval for each payload sent + * from the Peripheral to Central. + * + * Value range from @ref BT_ISO_FT_MIN to @ref BT_ISO_FT_MAX. + */ + uint8_t p_to_c_ft; + + /** + * @brief ISO interval + * + * Time between consecutive CIS anchor points. + * + * Value range from @ref BT_ISO_ISO_INTERVAL_MIN to @ref BT_ISO_ISO_INTERVAL_MAX. + */ + uint16_t iso_interval; +#endif /* CONFIG_BT_ISO_TEST_PARAMS */ +}; + +/** + * @brief Create unicast group. + * + * Create a new audio unicast group with one or more audio streams as a unicast client. + * All streams shall share the same framing. + * All streams in the same direction shall share the same interval and latency (see + * @ref bt_bap_qos_cfg). + * + * @param[in] param The unicast group create parameters. + * @param[out] unicast_group Pointer to the unicast group created. + * + * @return Zero on success or (negative) error code otherwise. + */ +int bt_cap_unicast_group_create(const struct bt_cap_unicast_group_param *param, + struct bt_cap_unicast_group **unicast_group); + +/** + * @brief Reconfigure unicast group. + * + * Reconfigure a unicast group with one or more audio streams as a unicast client. + * All streams shall share the same framing. + * All streams in the same direction shall share the same interval and latency (see + * @ref bt_bap_qos_cfg). + * All streams in @p param shall already belong to @p unicast_group. + * Use bt_cap_unicast_group_add_streams() to add additional streams. + * + * @param unicast_group Pointer to the unicast group created. + * @param param The unicast group reconfigure parameters. + * + * @return Zero on success or (negative) error code otherwise. + */ +int bt_cap_unicast_group_reconfig(struct bt_cap_unicast_group *unicast_group, + const struct bt_cap_unicast_group_param *param); + +/** + * @brief Add streams to a unicast group as a unicast client + * + * This function can be used to add additional streams to a bt_cap_unicast_group. + * + * This can be called at any time before any of the streams in the group has been started + * (see bt_bap_stream_ops.started()). + * This can also be called after the streams have been stopped (see bt_bap_stream_ops.stopped()). + * + * Once a stream has been added to a unicast group, it cannot be removed. To remove a stream from a + * group, the group must be deleted with bt_cap_unicast_group_delete(), but this will require all + * streams in the group to be released first. + * + * @param unicast_group Pointer to the unicast group + * @param params Array of stream parameters with streams being added to the group. + * @param num_param Number of parameters in @p params. + * + * @return 0 in case of success or negative value in case of error. + */ +int bt_cap_unicast_group_add_streams(struct bt_cap_unicast_group *unicast_group, + const struct bt_cap_unicast_group_stream_pair_param params[], + size_t num_param); + +/** + * @brief Delete audio unicast group. + * + * Delete a audio unicast group as a client. All streams in the group shall + * be in the idle or configured state. + * + * @param unicast_group Pointer to the unicast group to delete + * + * @return Zero on success or (negative) error code otherwise. + */ +int bt_cap_unicast_group_delete(struct bt_cap_unicast_group *unicast_group); + +/** Callback function for bt_bap_unicast_group_foreach_stream() + * + * @param stream The audio stream + * @param user_data User data + * + * @retval true Stop iterating. + * @retval false Continue iterating. + */ +typedef bool (*bt_cap_unicast_group_foreach_stream_func_t)(struct bt_cap_stream *stream, + void *user_data); + +/** + * @brief Iterate through all streams in a unicast group + * + * @param unicast_group The unicast group + * @param func The callback function + * @param user_data User specified data that sent to the callback function + * + * @retval 0 Success (even if no streams exists in the group). + * @retval -ECANCELED Iteration was stopped by the callback function before complete. + * @retval -EINVAL @p unicast_group or @p func were NULL. + */ +int bt_cap_unicast_group_foreach_stream(struct bt_cap_unicast_group *unicast_group, + bt_cap_unicast_group_foreach_stream_func_t func, + void *user_data); + /** Stream specific parameters for the bt_cap_initiator_unicast_audio_start() function */ struct bt_cap_unicast_audio_start_stream_param { /** Coordinated or ad-hoc set member. */ @@ -644,50 +830,54 @@ int bt_cap_initiator_broadcast_get_base(struct bt_cap_broadcast_source *broadcas /** Parameters for bt_cap_initiator_unicast_to_broadcast() */ struct bt_cap_unicast_to_broadcast_param { + /** The type of the set. */ + enum bt_cap_set_type type; + /** The source unicast group with the streams. */ - struct bt_bap_unicast_group *unicast_group; + struct bt_cap_unicast_group *unicast_group; /** - * @brief Whether or not to encrypt the streams. + * @brief The advertising set to use for the broadcast source * - * If set to true, then the broadcast code in @p broadcast_code - * will be used to encrypt the streams. + * This shall remain valid until the procedure has completed. + * If the advertising set is not started at the time of calling + * bt_cap_initiator_unicast_to_broadcast(), + * the procedure will start the advertising set with @ref BT_LE_EXT_ADV_START_DEFAULT. */ - bool encrypt; + struct bt_le_ext_adv *ext_adv; + + /** The SID of the advertising set. */ + uint8_t sid; + + /** The periodic advertising interval configured for the advertising set. */ + uint16_t pa_interval; + + /** The broadcast ID the advertising set is, or will be, using. */ + uint32_t broadcast_id; /** - * @brief 16-octet broadcast code. + * @brief Broadcast source parameters. * - * Only valid if @p encrypt is true. - * - * If the value is a string or a the value is less than 16 octets, - * the remaining octets shall be 0. - * - * Example: - * The string "Broadcast Code" shall be - * [42 72 6F 61 64 63 61 73 74 20 43 6F 64 65 00 00] + * These parameters shall remain valid until the procedure has completed. */ - uint8_t broadcast_code[BT_ISO_BROADCAST_CODE_SIZE]; + struct bt_cap_initiator_broadcast_create_param *broadcast_create_param; }; /** - * @brief Hands over the data streams in a unicast group to a broadcast source. + * @brief Hands over the sink streams in a unicast group to a broadcast source. * - * The streams in the unicast group will be stopped and the unicast group - * will be deleted. This can only be done for source streams. + * All streams in the provided unicast group will be stopped and released. The sink streams will be + * tranferred to a broadcast source, and the broadcast source information will be shared with + * all accepters that are currently receiving audio. Any stream that is not in the streaming state + * will only be released. * - * @note @kconfig{CONFIG_BT_CAP_INITIATOR}, - * @kconfig{CONFIG_BT_BAP_UNICAST_CLIENT} and - * @kconfig{CONFIG_BT_BAP_BROADCAST_SOURCE} must be enabled for this function - * to be enabled. + * @kconfig_dep{CONFIG_BT_CAP_HANDOVER_SUPPORTED} * - * @param param The parameters for the handover. - * @param source The resulting broadcast source. + * @param param The parameters for the handover. * * @return 0 on success or negative error value on failure. */ -int bt_cap_initiator_unicast_to_broadcast(const struct bt_cap_unicast_to_broadcast_param *param, - struct bt_cap_broadcast_source **source); +int bt_cap_initiator_unicast_to_broadcast(const struct bt_cap_unicast_to_broadcast_param *param); /** Parameters for bt_cap_initiator_broadcast_to_unicast() */ struct bt_cap_broadcast_to_unicast_param { @@ -720,10 +910,7 @@ struct bt_cap_broadcast_to_unicast_param { * The streams in the broadcast source will be stopped and the broadcast source * will be deleted. * - * @note @kconfig{CONFIG_BT_CAP_INITIATOR}, - * @kconfig{CONFIG_BT_BAP_UNICAST_CLIENT} and - * @kconfig{CONFIG_BT_BAP_BROADCAST_SOURCE} must be enabled for this function - * to be enabled. + * @kconfig_dep{CONFIG_BT_CAP_HANDOVER_SUPPORTED} * * @param[in] param The parameters for the handover. * @param[out] unicast_group The resulting broadcast source. diff --git a/include/zephyr/bluetooth/bluetooth.h b/include/zephyr/bluetooth/bluetooth.h index c68327647599..c9c7f8e13bf6 100644 --- a/include/zephyr/bluetooth/bluetooth.h +++ b/include/zephyr/bluetooth/bluetooth.h @@ -1714,6 +1714,31 @@ int bt_le_ext_adv_delete(struct bt_le_ext_adv *adv); */ uint8_t bt_le_ext_adv_get_index(struct bt_le_ext_adv *adv); +enum bt_le_ext_adv_state { + /** No state */ + BT_LE_EXT_ADV_STATE_NONE, + + /** The advertising set has been created but not started */ + BT_LE_EXT_ADV_STATE_CREATED, + + /** The advertising set is started */ + BT_LE_EXT_ADV_STATE_STARTED, + + /** Ther advertising set is temporarily paused */ + BT_LE_EXT_ADV_STATE_PAUSED, +}; + +enum bt_le_per_adv_state { + /** No state */ + BT_LE_PER_ADV_STATE_NONE, + + /** The advertising set has been configured for periodic advertising */ + BT_LE_PER_ADV_STATE_CONFIGURED, + + /** Periodic advertising is started */ + BT_LE_PER_ADV_STATE_STARTED, +}; + /** @brief Advertising set info structure. */ struct bt_le_ext_adv_info { /** Local identity handle. */ @@ -1722,8 +1747,19 @@ struct bt_le_ext_adv_info { /** Currently selected Transmit Power (dBM). */ int8_t tx_power; - /** Current local advertising address used. */ + /** + * @brief Current local advertising address used. + * + * If @ref bt_le_ext_adv_info.ext_adv_state is not BT_LE_EXT_ADV_STATE_STARTED the value + * of this address may change when bt_le_ext_adv_start() is called. + */ const bt_addr_le_t *addr; + + /** Extended advertising state */ + enum bt_le_ext_adv_state ext_adv_state; + + /** Periodic advertising state */ + enum bt_le_per_adv_state per_adv_state; }; /** @@ -1732,7 +1768,9 @@ struct bt_le_ext_adv_info { * @param adv Advertising set object * @param info Advertising set info object * - * @return Zero on success or (negative) error code on failure. + * @retval 0 Success + * @retval -EINVAL @p adv or @p info is NULL + * @retval -ENXIO @p adv is not a created advertising set */ int bt_le_ext_adv_get_info(const struct bt_le_ext_adv *adv, struct bt_le_ext_adv_info *info); diff --git a/samples/bluetooth/cap_initiator/src/cap_initiator_unicast.c b/samples/bluetooth/cap_initiator/src/cap_initiator_unicast.c index df45dfcadaa4..1d17a0cd5022 100644 --- a/samples/bluetooth/cap_initiator/src/cap_initiator_unicast.c +++ b/samples/bluetooth/cap_initiator/src/cap_initiator_unicast.c @@ -44,7 +44,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_bap_unicast_group *unicast_group; +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_tx_iso_packet_count; /* This value is exposed to test code */ @@ -336,16 +336,16 @@ static int discover_sources(void) static int unicast_group_create(void) { - struct bt_bap_unicast_group_stream_param source_stream_param = { - .qos = &unicast_preset_16_2_1.qos, - .stream = &peer.source_stream.bap_stream, + struct bt_cap_unicast_group_stream_param source_stream_param = { + .qos_cfg = &unicast_preset_16_2_1.qos, + .stream = &peer.source_stream, }; - struct bt_bap_unicast_group_stream_param sink_stream_param = { - .qos = &unicast_preset_16_2_1.qos, - .stream = &peer.sink_stream.bap_stream, + struct bt_cap_unicast_group_stream_param sink_stream_param = { + .qos_cfg = &unicast_preset_16_2_1.qos, + .stream = &peer.sink_stream, }; - struct bt_bap_unicast_group_stream_pair_param pair_params = {0}; - struct bt_bap_unicast_group_param group_param = {0}; + struct bt_cap_unicast_group_stream_pair_param pair_params = {0}; + struct bt_cap_unicast_group_param group_param = {0}; int err; if (peer.source_ep != NULL) { @@ -359,7 +359,7 @@ static int unicast_group_create(void) group_param.params_count = 1U; group_param.params = &pair_params; - err = bt_bap_unicast_group_create(&group_param, &unicast_group); + err = bt_cap_unicast_group_create(&group_param, &unicast_group); if (err != 0) { LOG_ERR("Failed to create group: %d", err); return err; @@ -374,7 +374,7 @@ static int unicast_group_delete(void) { int err; - err = bt_bap_unicast_group_delete(unicast_group); + err = bt_cap_unicast_group_delete(unicast_group); if (err != 0) { LOG_ERR("Failed to delete group: %d", err); return err; diff --git a/samples/bluetooth/tmap_central/src/cap_initiator.c b/samples/bluetooth/tmap_central/src/cap_initiator.c index e2722db64f5e..ee129bf1f132 100644 --- a/samples/bluetooth/tmap_central/src/cap_initiator.c +++ b/samples/bluetooth/tmap_central/src/cap_initiator.c @@ -313,15 +313,15 @@ static struct bt_bap_unicast_client_cb unicast_client_cbs = { .endpoint = endpoint_cb, }; -static int unicast_group_create(struct bt_bap_unicast_group **out_unicast_group) +static int unicast_group_create(struct bt_cap_unicast_group **out_unicast_group) { int err = 0; - struct bt_bap_unicast_group_stream_param group_stream_params; - struct bt_bap_unicast_group_stream_pair_param pair_params; - struct bt_bap_unicast_group_param group_param; + struct bt_cap_unicast_group_stream_param group_stream_params; + struct bt_cap_unicast_group_stream_pair_param pair_params; + struct bt_cap_unicast_group_param group_param; - group_stream_params.qos = &unicast_preset_48_2_1.qos; - group_stream_params.stream = &unicast_streams[0].bap_stream; + group_stream_params.qos_cfg = &unicast_preset_48_2_1.qos; + group_stream_params.stream = &unicast_streams[0]; pair_params.tx_param = &group_stream_params; pair_params.rx_param = NULL; @@ -329,7 +329,7 @@ static int unicast_group_create(struct bt_bap_unicast_group **out_unicast_group) group_param.params_count = 1; group_param.params = &pair_params; - err = bt_bap_unicast_group_create(&group_param, out_unicast_group); + err = bt_cap_unicast_group_create(&group_param, out_unicast_group); if (err != 0) { printk("Failed to create group: %d\n", err); return err; @@ -444,7 +444,7 @@ int cap_initiator_init(void) int cap_initiator_setup(struct bt_conn *conn) { int err = 0; - struct bt_bap_unicast_group *unicast_group; + struct bt_cap_unicast_group *unicast_group; k_sem_reset(&sem_cas_discovery); k_sem_reset(&sem_discover_sink); diff --git a/subsys/bluetooth/audio/Kconfig.cap b/subsys/bluetooth/audio/Kconfig.cap index d87f109f2b22..f0005642fbc6 100644 --- a/subsys/bluetooth/audio/Kconfig.cap +++ b/subsys/bluetooth/audio/Kconfig.cap @@ -48,3 +48,7 @@ config BT_CAP_COMMANDER BT_MCC help Enabling this will enable the CAP Initiator role. + +config BT_CAP_HANDOVER_SUPPORTED + def_bool BT_CAP_COMMANDER && BT_CAP_INITIATOR && BT_BAP_BROADCAST_ASSISTANT && \ + BT_BAP_BROADCAST_SOURCE && BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT > 0 diff --git a/subsys/bluetooth/audio/bap_unicast_client.c b/subsys/bluetooth/audio/bap_unicast_client.c index 1f081ee805b1..2e2b40957def 100644 --- a/subsys/bluetooth/audio/bap_unicast_client.c +++ b/subsys/bluetooth/audio/bap_unicast_client.c @@ -2733,30 +2733,35 @@ static bool valid_unicast_group_stream_param(const struct bt_bap_unicast_group * { const struct bt_bap_qos_cfg *qos; - CHECKIF(param->stream == NULL) { + if (param->stream == NULL) { LOG_DBG("param->stream is NULL"); - return -EINVAL; + return false; } CHECKIF(param->qos == NULL) { LOG_DBG("param->qos is NULL"); - return -EINVAL; + return false; } - if (param->stream != NULL && param->stream->group != NULL) { - if (unicast_group != NULL && param->stream->group != unicast_group) { + /* If unicast_group is non-NULL then we are doing a reconfigure */ + if (unicast_group != NULL) { + if (param->stream->group != unicast_group) { LOG_DBG("stream %p not part of group %p (%p)", param->stream, unicast_group, param->stream->group); - } else { + return false; + } + } else { + if (param->stream->group != NULL) { LOG_DBG("stream %p already part of group %p", param->stream, param->stream->group); + + return false; } - return -EALREADY; } CHECKIF(bt_audio_verify_qos(param->qos) != BT_BAP_ASCS_REASON_NONE) { LOG_DBG("Invalid QoS"); - return -EINVAL; + return false; } qos = param->qos; @@ -3141,6 +3146,33 @@ int bt_bap_unicast_group_delete(struct bt_bap_unicast_group *unicast_group) return 0; } +int bt_bap_unicast_group_foreach_stream(struct bt_bap_unicast_group *unicast_group, + bt_bap_unicast_group_foreach_stream_func_t func, + void *user_data) +{ + struct bt_bap_stream *stream, *next; + + if (unicast_group == NULL) { + LOG_DBG("unicast_group is NULL"); + return -EINVAL; + } + + if (func == NULL) { + LOG_DBG("func is NULL"); + return -EINVAL; + } + + SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&unicast_group->streams, stream, next, _node) { + const bool stop = func(stream, user_data); + + if (stop) { + return -ECANCELED; + } + } + + return 0; +} + int bt_bap_unicast_client_config(struct bt_bap_stream *stream, const struct bt_audio_codec_cfg *codec_cfg) { diff --git a/subsys/bluetooth/audio/cap_commander.c b/subsys/bluetooth/audio/cap_commander.c index 83ba8941a0a1..ca36092570c1 100644 --- a/subsys/bluetooth/audio/cap_commander.c +++ b/subsys/bluetooth/audio/cap_commander.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023 Nordic Semiconductor ASA + * Copyright (c) 2022-2025 Nordic Semiconductor ASA * * SPDX-License-Identifier: Apache-2.0 */ @@ -28,6 +28,7 @@ #include #include #include +#include #include "audio_internal.h" #include "bap_endpoint.h" @@ -325,7 +326,7 @@ static bool valid_broadcast_reception_start_param( return true; } -int bt_cap_commander_broadcast_reception_start( +int cap_commander_broadcast_reception_start( const struct bt_cap_commander_broadcast_reception_start_param *param) { struct bt_bap_broadcast_assistant_add_src_param add_src_param = {0}; @@ -334,16 +335,6 @@ int bt_cap_commander_broadcast_reception_start( struct bt_conn *conn; int err; - if (!valid_broadcast_reception_start_param(param)) { - return -EINVAL; - } - - if (bt_cap_common_test_and_set_proc_active()) { - LOG_DBG("A CAP procedure is already in progress"); - - return -EBUSY; - } - broadcast_assistant_cb.add_src = cap_commander_broadcast_assistant_add_src_cb; if (!broadcast_assistant_cb_registered) { err = cap_commander_register_broadcast_assistant_cb(); @@ -406,6 +397,22 @@ int bt_cap_commander_broadcast_reception_start( return 0; } +int bt_cap_commander_broadcast_reception_start( + const struct bt_cap_commander_broadcast_reception_start_param *param) +{ + if (!valid_broadcast_reception_start_param(param)) { + return -EINVAL; + } + + if (bt_cap_common_test_and_set_proc_active()) { + LOG_DBG("A CAP procedure is already in progress"); + + return -EBUSY; + } + + return cap_commander_broadcast_reception_start(param); +} + static void copy_broadcast_reception_stop_param(struct bt_bap_broadcast_assistant_mod_src_param *mod_src_param, struct cap_broadcast_reception_stop *stop_param) @@ -875,6 +882,18 @@ static void cap_commander_proc_complete(void) failed_conn = active_proc->failed_conn; err = active_proc->err; proc_type = active_proc->proc_type; + + if (IS_ENABLED(CONFIG_BT_CAP_HANDOVER_SUPPORTED) && bt_cap_common_handover_is_active()) { + /* Complete handover procedure. At this point we do not know if the remote + * device will attempt to use PAST or scan for itself, so it's best to leave + * this up to the application layer + */ + __ASSERT_NO_MSG(proc_type == BT_CAP_COMMON_PROC_TYPE_BROADCAST_RECEPTION_START); + + bt_cap_handover_proc_complete(); + return; + } + bt_cap_common_clear_active_proc(); if (cap_cb == NULL) { diff --git a/subsys/bluetooth/audio/cap_common.c b/subsys/bluetooth/audio/cap_common.c index 59845acb6eb4..68e168ccb992 100644 --- a/subsys/bluetooth/audio/cap_common.c +++ b/subsys/bluetooth/audio/cap_common.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023-2024 Nordic Semiconductor ASA + * Copyright (c) 2023-2025 Nordic Semiconductor ASA * * SPDX-License-Identifier: Apache-2.0 */ @@ -21,6 +21,7 @@ #include #include #include +#include #include "cap_internal.h" #include "csip_internal.h" @@ -74,6 +75,18 @@ bool bt_cap_common_subproc_is_type(enum bt_cap_common_subproc_type subproc_type) } #endif /* CONFIG_BT_CAP_INITIATOR_UNICAST */ +#if defined(CONFIG_BT_CAP_HANDOVER_SUPPORTED) +void bt_cap_common_set_handover_active(void) +{ + atomic_set_bit(active_proc.proc_state_flags, BT_CAP_COMMON_PROC_STATE_HANDOVER); +} + +bool bt_cap_common_handover_is_active(void) +{ + return atomic_test_bit(active_proc.proc_state_flags, BT_CAP_COMMON_PROC_STATE_HANDOVER); +} +#endif /* CONFIG_BT_CAP_HANDOVER_SUPPORTED */ + struct bt_conn *bt_cap_common_get_member_conn(enum bt_cap_set_type type, const union bt_cap_set_member *member) { diff --git a/subsys/bluetooth/audio/cap_initiator.c b/subsys/bluetooth/audio/cap_initiator.c index 4d7c3f06f27b..7d24303c3e07 100644 --- a/subsys/bluetooth/audio/cap_initiator.c +++ b/subsys/bluetooth/audio/cap_initiator.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024 Nordic Semiconductor ASA + * Copyright (c) 2022-2025 Nordic Semiconductor ASA * * SPDX-License-Identifier: Apache-2.0 */ @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -19,11 +20,15 @@ #include #include #include +#include #include +#include #include +#include #include #include #include +#include #include #include #include @@ -37,6 +42,11 @@ LOG_MODULE_REGISTER(bt_cap_initiator, CONFIG_BT_CAP_INITIATOR_LOG_LEVEL); #include "common/bt_str.h" +#if defined(CONFIG_BT_CAP_HANDOVER_SUPPORTED) +static void handover_unicast_to_broadcast_reception_start(void); +static void handover_unicast_to_broadcast_setup_broadcast(void); +#endif /* CONFIG_BT_CAP_HANDOVER_SUPPORTED */ + static const struct bt_cap_initiator_cb *cap_cb; int bt_cap_initiator_register_cb(const struct bt_cap_initiator_cb *cb) @@ -141,6 +151,20 @@ static struct bt_cap_broadcast_source { static bool cap_initiator_broadcast_audio_start_valid_param( const struct bt_cap_initiator_broadcast_create_param *param) { + if (param == NULL) { + LOG_DBG("param is NULL"); + return false; + } + + if (param->subgroup_params == NULL) { + LOG_DBG("subgroup_params is NULL"); + return false; + } + + if (param->subgroup_count == 0U) { + LOG_DBG("subgroup_count is 0"); + return false; + } for (size_t i = 0U; i < param->subgroup_count; i++) { const struct bt_cap_initiator_broadcast_subgroup_param *subgroup_param; @@ -157,8 +181,7 @@ static bool cap_initiator_broadcast_audio_start_valid_param( return false; } - valid_metadata = - cap_initiator_valid_metadata(codec_cfg->meta, codec_cfg->meta_len); + valid_metadata = cap_initiator_valid_metadata(codec_cfg->meta, codec_cfg->meta_len); CHECKIF(!valid_metadata) { LOG_DBG("Invalid metadata supplied for subgroup[%zu]", i); @@ -235,11 +258,6 @@ int bt_cap_initiator_broadcast_audio_create( struct bt_bap_broadcast_source_param bap_create_param = {0}; int err; - CHECKIF(param == NULL) { - LOG_DBG("param is NULL"); - return -EINVAL; - } - CHECKIF(broadcast_source == NULL) { LOG_DBG("source is NULL"); return -EINVAL; @@ -293,17 +311,34 @@ static struct bt_cap_broadcast_source *get_cap_broadcast_source_by_bap_broadcast return NULL; } +#if defined(CONFIG_BT_CAP_HANDOVER_SUPPORTED) +static bool is_handover_broadcast_source(const struct bt_cap_broadcast_source *cap_broadcast_source) +{ + const struct bt_cap_common_proc *active_proc = bt_cap_common_get_active_proc(); + const struct bt_cap_handover_proc_param *proc_param = &active_proc->proc_param.handover; + + return cap_broadcast_source == proc_param->unicast_to_broadcast.broadcast_source; +} +#endif /* CONFIG_BT_CAP_HANDOVER_SUPPORTED */ + static void broadcast_source_started_cb(struct bt_bap_broadcast_source *bap_broadcast_source) { - if (cap_cb && cap_cb->broadcast_started) { - struct bt_cap_broadcast_source *source = - get_cap_broadcast_source_by_bap_broadcast_source(bap_broadcast_source); + struct bt_cap_broadcast_source *source = + get_cap_broadcast_source_by_bap_broadcast_source(bap_broadcast_source); - if (source == NULL) { - /* Not one of ours */ - return; - } + if (source == NULL) { + /* Not one of ours */ + return; + } + +#if defined(CONFIG_BT_CAP_HANDOVER_SUPPORTED) + if (bt_cap_common_handover_is_active() && is_handover_broadcast_source(source)) { + handover_unicast_to_broadcast_reception_start(); + return; + } +#endif /* CONFIG_BT_CAP_HANDOVER_SUPPORTED */ + if (cap_cb != NULL && cap_cb->broadcast_started != NULL) { cap_cb->broadcast_started(source); } } @@ -311,15 +346,40 @@ static void broadcast_source_started_cb(struct bt_bap_broadcast_source *bap_broa static void broadcast_source_stopped_cb(struct bt_bap_broadcast_source *bap_broadcast_source, uint8_t reason) { - if (cap_cb && cap_cb->broadcast_stopped) { - struct bt_cap_broadcast_source *source = - get_cap_broadcast_source_by_bap_broadcast_source(bap_broadcast_source); + struct bt_cap_broadcast_source *source = + get_cap_broadcast_source_by_bap_broadcast_source(bap_broadcast_source); - if (source == NULL) { - /* Not one of ours */ - return; + if (source == NULL) { + /* Not one of ours */ + return; + } + +#if defined(CONFIG_BT_CAP_HANDOVER_SUPPORTED) + if (bt_cap_common_handover_is_active() && is_handover_broadcast_source(source)) { + if (reason == BT_HCI_ERR_LOCALHOST_TERM_CONN) { + /* TODO: This will be the case when we do broadcast to unicast handover and + * it successfully stops the broadcast + */ + } else { + struct bt_cap_common_proc *active_proc = bt_cap_common_get_active_proc(); + struct bt_cap_handover_proc_param *proc_param = + &active_proc->proc_param.handover; + int err; + + err = bt_cap_initiator_broadcast_audio_delete( + proc_param->unicast_to_broadcast.broadcast_source); + __ASSERT_NO_MSG(err == 0); + proc_param->unicast_to_broadcast.broadcast_source = NULL; + + active_proc->err = reason; + active_proc->failed_conn = NULL; + bt_cap_handover_proc_complete(); } + return; + } +#endif /* CONFIG_BT_CAP_HANDOVER_SUPPORTED */ + if (cap_cb && cap_cb->broadcast_stopped) { cap_cb->broadcast_stopped(source, reason); } } @@ -420,6 +480,9 @@ int bt_cap_initiator_broadcast_get_base(struct bt_cap_broadcast_source *broadcas #endif /* CONFIG_BT_BAP_BROADCAST_SOURCE */ #if defined(CONFIG_BT_BAP_UNICAST_CLIENT) +static struct bt_cap_unicast_group { + struct bt_bap_unicast_group *bap_unicast_group; +} unicast_groups[CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_COUNT]; static enum bt_bap_ep_state stream_get_state(const struct bt_bap_stream *bap_stream) { @@ -777,6 +840,274 @@ int bt_cap_initiator_unicast_discover(struct bt_conn *conn) return bt_cap_common_discover(conn, bt_cap_initiator_discover_complete); } +/** Validates that the stream parameter does not contain invalid values */ +static bool valid_unicast_group_stream_param(const struct bt_cap_unicast_group_stream_param *param) +{ + if (param->stream == NULL) { + LOG_DBG("param->stream is NULL"); + return false; + } + + /* Remaining values will be validated by BAP */ + + return true; +} + +static bool +valid_group_stream_pair_param(const struct bt_cap_unicast_group_stream_pair_param *pair_param) +{ + if (pair_param->rx_param != NULL && + !valid_unicast_group_stream_param(pair_param->rx_param)) { + return false; + } + + if (pair_param->tx_param != NULL && + !valid_unicast_group_stream_param(pair_param->tx_param)) { + return false; + } + + return true; +} + +static bool valid_unicast_group_param(const struct bt_cap_unicast_group_param *param) +{ + if (param == NULL) { + LOG_DBG("param is NULL"); + return false; + } + + if (param->params_count > CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT) { + LOG_DBG("Too many streams provided: %u/%u", param->params_count, + UNICAST_GROUP_STREAM_CNT); + return false; + } + + for (size_t i = 0U; i < param->params_count; i++) { + if (!valid_group_stream_pair_param(¶m->params[i])) { + return false; + } + } + + return true; +} + +struct cap_to_bap_unicast_params { + struct bt_bap_unicast_group_stream_param + stream_params[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT + + CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT]; + struct bt_bap_unicast_group_stream_pair_param + pair_params[CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT]; + struct bt_bap_unicast_group_param group_param; +}; + +static void cap_to_bap_unicast_group_pair_param( + const struct bt_cap_unicast_group_stream_pair_param pair_params[], + struct cap_to_bap_unicast_params *bap_params) +{ + size_t stream_param_idx = 0U; + + for (size_t i = 0U; i < bap_params->group_param.params_count; i++) { + const struct bt_cap_unicast_group_stream_pair_param *pair_param = &pair_params[i]; + struct bt_bap_unicast_group_stream_pair_param *bap_pair_param = + &bap_params->pair_params[i]; + struct bt_bap_unicast_group_stream_param *bap_stream_param; + + if (pair_param->rx_param != NULL) { + bap_stream_param = &bap_params->stream_params[stream_param_idx++]; + + bap_stream_param->stream = &pair_param->rx_param->stream->bap_stream; + bap_stream_param->qos = pair_param->rx_param->qos_cfg; + + bap_pair_param->rx_param = bap_stream_param; + } else { + bap_pair_param->rx_param = NULL; + } + + if (pair_param->tx_param != NULL) { + bap_stream_param = &bap_params->stream_params[stream_param_idx++]; + + bap_stream_param->stream = &pair_param->tx_param->stream->bap_stream; + bap_stream_param->qos = pair_param->tx_param->qos_cfg; + + bap_pair_param->tx_param = bap_stream_param; + } else { + bap_pair_param->tx_param = NULL; + } + } +} + +static void cap_to_bap_unicast_group_param(const struct bt_cap_unicast_group_param *param, + struct cap_to_bap_unicast_params *bap_params) +{ + bap_params->group_param.params_count = param->params_count; + bap_params->group_param.packing = param->packing; +#if defined(CONFIG_BT_ISO_TEST_PARAMS) + bap_params->group_param.c_to_p_ft = param->c_to_p_ft; + bap_params->group_param.p_to_c_ft = param->p_to_c_ft; + bap_params->group_param.iso_interval = param->iso_interval; +#endif /* CONFIG_BT_ISO_TEST_PARAMS */ + bap_params->group_param.params = bap_params->pair_params; + + cap_to_bap_unicast_group_pair_param(param->params, bap_params); +} + +int bt_cap_unicast_group_create(const struct bt_cap_unicast_group_param *param, + struct bt_cap_unicast_group **unicast_group) +{ + struct cap_to_bap_unicast_params bap_param = {0}; + int err; + + static K_MUTEX_DEFINE(list_mutex); + + if (unicast_group == NULL) { + LOG_DBG("unicast_group is NULL"); + return -EINVAL; + } + + if (!valid_unicast_group_param(param)) { + return -EINVAL; + } + + *unicast_group = NULL; + + (void)k_mutex_lock(&list_mutex, K_FOREVER); + for (size_t i = 0; i < ARRAY_SIZE(unicast_groups); i++) { + if (unicast_groups[i].bap_unicast_group == NULL) { + *unicast_group = &unicast_groups[i]; + break; + } + } + + if (*unicast_group == NULL) { + LOG_DBG("Could not allocate more unicast groups"); + (void)k_mutex_unlock(&list_mutex); + return -ENOMEM; + } + + /* Populate the BAP group params based on the CAP params (mostly just a copy) */ + cap_to_bap_unicast_group_param(param, &bap_param); + + err = bt_bap_unicast_group_create(&bap_param.group_param, + &(*unicast_group)->bap_unicast_group); + if (err != 0) { + LOG_DBG("Failed to create unicast group: %d", err); + } + (void)k_mutex_unlock(&list_mutex); + + return err; +} + +int bt_cap_unicast_group_reconfig(struct bt_cap_unicast_group *unicast_group, + const struct bt_cap_unicast_group_param *param) +{ + struct cap_to_bap_unicast_params bap_param = {0}; + + if (unicast_group == NULL) { + LOG_DBG("unicast_group is NULL"); + return -EINVAL; + } + + if (!valid_unicast_group_param(param)) { + return -EINVAL; + } + + /* Populate the BAP group params based on the CAP params (mostly just a copy) */ + cap_to_bap_unicast_group_param(param, &bap_param); + + return bt_bap_unicast_group_reconfig(unicast_group->bap_unicast_group, + &bap_param.group_param); +} + +int bt_cap_unicast_group_add_streams(struct bt_cap_unicast_group *unicast_group, + const struct bt_cap_unicast_group_stream_pair_param params[], + size_t num_param) +{ + struct cap_to_bap_unicast_params bap_param = {.group_param.params_count = num_param}; + + if (unicast_group == NULL) { + LOG_DBG("unicast_group is NULL"); + return -EINVAL; + } + + if (params == NULL) { + LOG_DBG("params is NULL"); + return -EINVAL; + } + + if (num_param == 0U) { + LOG_DBG("num_param is 0"); + return -EINVAL; + } + + for (size_t i = 0U; i < num_param; i++) { + if (!valid_group_stream_pair_param(¶ms[i])) { + return -EINVAL; + } + } + + cap_to_bap_unicast_group_pair_param(params, &bap_param); + + return bt_bap_unicast_group_add_streams(unicast_group->bap_unicast_group, + bap_param.pair_params, num_param); +} + +int bt_cap_unicast_group_delete(struct bt_cap_unicast_group *unicast_group) +{ + int err; + + if (unicast_group == NULL) { + LOG_DBG("unicast_group is NULL"); + return -EINVAL; + } + + err = bt_bap_unicast_group_delete(unicast_group->bap_unicast_group); + if (err == 0) { + unicast_group->bap_unicast_group = NULL; + } + + return err; +} + +struct cap_unicast_group_foreach_stream_data { + bt_cap_unicast_group_foreach_stream_func_t func; + void *user_data; +}; + +static bool bap_unicast_group_foreach_stream_cb(struct bt_bap_stream *bap_stream, void *user_data) +{ + struct cap_unicast_group_foreach_stream_data *data = user_data; + + /* Since we are iterating on a CAP unicast group, we can assume that all streams are CAP + * streams + */ + data->func(CONTAINER_OF(bap_stream, struct bt_cap_stream, bap_stream), data->user_data); + + return false; +} + +int bt_cap_unicast_group_foreach_stream(struct bt_cap_unicast_group *unicast_group, + bt_cap_unicast_group_foreach_stream_func_t func, + void *user_data) +{ + struct cap_unicast_group_foreach_stream_data data = { + .func = func, + .user_data = user_data, + }; + + if (unicast_group == NULL) { + LOG_DBG("unicast_group is NULL"); + return -EINVAL; + } + + if (func == NULL) { + LOG_DBG("func is NULL"); + return -EINVAL; + } + + return bt_bap_unicast_group_foreach_stream(unicast_group->bap_unicast_group, + bap_unicast_group_foreach_stream_cb, &data); +} + static bool valid_unicast_audio_start_param(const struct bt_cap_unicast_audio_start_param *param) { struct bt_bap_unicast_group *unicast_group = NULL; @@ -799,14 +1130,13 @@ static bool valid_unicast_audio_start_param(const struct bt_cap_unicast_audio_st CHECKIF(param->count > CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT) { LOG_DBG("param->count (%zu) is larger than " "CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT (%d)", - param->count, - CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT); + param->count, CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT); return false; } for (size_t i = 0U; i < param->count; i++) { const struct bt_cap_unicast_audio_start_stream_param *stream_param = - ¶m->stream_params[i]; + ¶m->stream_params[i]; const union bt_cap_set_member *member = &stream_param->member; const struct bt_cap_stream *cap_stream = stream_param->stream; const struct bt_audio_codec_cfg *codec_cfg = stream_param->codec_cfg; @@ -872,8 +1202,7 @@ static bool valid_unicast_audio_start_param(const struct bt_cap_unicast_audio_st LOG_DBG("param->stream_params[%zu] (%p) is " "duplicated by " "param->stream_params[%zu] (%p)", - j, param->stream_params[j].stream, - i, cap_stream); + j, param->stream_params[j].stream, i, cap_stream); return false; } } @@ -892,6 +1221,23 @@ static void cap_initiator_unicast_audio_proc_complete(void) failed_conn = active_proc->failed_conn; err = active_proc->err; proc_type = active_proc->proc_type; + + LOG_DBG("conn %p err %d proc_type %d", (void *)failed_conn, err, proc_type); + +#if defined(CONFIG_BT_CAP_HANDOVER_SUPPORTED) + if (bt_cap_common_handover_is_active()) { + __ASSERT_NO_MSG(proc_type == BT_CAP_COMMON_PROC_TYPE_STOP); + + if (err != 0) { + bt_cap_handover_proc_complete(); + } else { + /* continue */ + handover_unicast_to_broadcast_setup_broadcast(); + } + return; + } +#endif /* CONFIG_BT_CAP_HANDOVER_SUPPORTED */ + bt_cap_common_clear_active_proc(); if (cap_cb == NULL) { @@ -957,8 +1303,8 @@ void bt_cap_initiator_cp_cb(struct bt_cap_stream *cap_stream, enum bt_bap_ascs_r } } -static int cap_initiator_unicast_audio_configure( - const struct bt_cap_unicast_audio_start_param *param) +static int +cap_initiator_unicast_audio_configure(const struct bt_cap_unicast_audio_start_param *param) { struct bt_cap_common_proc *active_proc = bt_cap_common_get_active_proc(); struct bt_cap_initiator_proc_param *proc_param; @@ -974,7 +1320,7 @@ static int cap_initiator_unicast_audio_configure( for (size_t i = 0U; i < param->count; i++) { struct bt_cap_unicast_audio_start_stream_param *stream_param = - ¶m->stream_params[i]; + ¶m->stream_params[i]; union bt_cap_set_member *member = &stream_param->member; struct bt_cap_stream *cap_stream = stream_param->stream; @@ -1927,7 +2273,7 @@ static bool valid_unicast_audio_stop_param(const struct bt_cap_unicast_audio_sto return true; } -int bt_cap_initiator_unicast_audio_stop(const struct bt_cap_unicast_audio_stop_param *param) +static int unicast_audio_stop(const struct bt_cap_unicast_audio_stop_param *param) { struct bt_cap_common_proc *active_proc = bt_cap_common_get_active_proc(); bool can_release = false; @@ -1935,16 +2281,6 @@ int bt_cap_initiator_unicast_audio_stop(const struct bt_cap_unicast_audio_stop_p bool can_stop = false; int err; - if (!valid_unicast_audio_stop_param(param)) { - return -EINVAL; - } - - if (bt_cap_common_test_and_set_proc_active()) { - LOG_DBG("A CAP procedure is already in progress"); - - return -EBUSY; - } - for (size_t i = 0U; i < param->count; i++) { struct bt_cap_stream *cap_stream = param->streams[i]; struct bt_bap_stream *bap_stream = &cap_stream->bap_stream; @@ -2046,6 +2382,21 @@ int bt_cap_initiator_unicast_audio_stop(const struct bt_cap_unicast_audio_stop_p return err; } +int bt_cap_initiator_unicast_audio_stop(const struct bt_cap_unicast_audio_stop_param *param) +{ + if (!valid_unicast_audio_stop_param(param)) { + return -EINVAL; + } + + if (bt_cap_common_test_and_set_proc_active()) { + LOG_DBG("A CAP procedure is already in progress"); + + return -EBUSY; + } + + return unicast_audio_stop(param); +} + void bt_cap_initiator_disabled(struct bt_cap_stream *cap_stream) { struct bt_cap_common_proc *active_proc = bt_cap_common_get_active_proc(); @@ -2268,13 +2619,555 @@ void bt_cap_initiator_released(struct bt_cap_stream *cap_stream) #endif /* CONFIG_BT_BAP_UNICAST_CLIENT */ -#if defined(CONFIG_BT_BAP_BROADCAST_SOURCE) && defined(CONFIG_BT_BAP_UNICAST_CLIENT) +#if defined(CONFIG_BT_CAP_HANDOVER_SUPPORTED) +struct cap_unicast_group_stream_lookup { + struct bt_cap_stream *active_sink_streams[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT]; + struct bt_cap_stream *streams[CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT]; + size_t active_sink_streams_cnt; + size_t cnt; +}; -int bt_cap_initiator_unicast_to_broadcast( - const struct bt_cap_unicast_to_broadcast_param *param, - struct bt_cap_broadcast_source **source) +static bool unicast_group_foreach_stream_cb(struct bt_cap_stream *cap_stream, void *user_data) { - return -ENOSYS; + struct cap_unicast_group_stream_lookup *data = user_data; + const struct bt_bap_stream *bap_stream = &cap_stream->bap_stream; + + __ASSERT_NO_MSG(data->cnt < ARRAY_SIZE(data->streams)); + __ASSERT_NO_MSG(data->active_sink_streams_cnt < ARRAY_SIZE(data->active_sink_streams)); + + if (bap_stream->ep != NULL) { + struct bt_bap_ep_info ep_info; + int err; + + err = bt_bap_ep_get_info(bap_stream->ep, &ep_info); + __ASSERT_NO_MSG(err == 0); + + /* Only consider sink streams for handover to broadcast */ + if (ep_info.state == BT_BAP_EP_STATE_STREAMING && + ep_info.dir == BT_AUDIO_DIR_SINK) { + data->active_sink_streams[data->active_sink_streams_cnt++] = cap_stream; + } + } + + data->streams[data->cnt++] = cap_stream; + + return false; +} + +void bt_cap_handover_proc_complete(void) +{ + struct bt_cap_common_proc *active_proc = bt_cap_common_get_active_proc(); + struct bt_cap_handover_proc_param *proc_param = &active_proc->proc_param.handover; + struct bt_cap_broadcast_source *broadcast_source = + proc_param->unicast_to_broadcast.broadcast_source; + struct bt_cap_unicast_group *unicast_group = proc_param->unicast_to_broadcast.unicast_group; + struct bt_conn *failed_conn = active_proc->failed_conn; + const int err = active_proc->err; + + bt_cap_common_clear_active_proc(); + + if (cap_cb != NULL && cap_cb->unicast_to_broadcast_complete != NULL) { + cap_cb->unicast_to_broadcast_complete(err, failed_conn, unicast_group, + broadcast_source); + } +} + +static void handover_unicast_to_broadcast_reception_start(void) +{ + struct bt_cap_commander_broadcast_reception_start_param param = {0}; + struct bt_cap_initiator_broadcast_create_param *create_param; + struct bt_cap_handover_proc_param *proc_param; + struct bt_cap_common_proc *active_proc; + struct bt_le_ext_adv_info adv_info; + int err; + + active_proc = bt_cap_common_get_active_proc(); + proc_param = &active_proc->proc_param.handover; + create_param = proc_param->unicast_to_broadcast.broadcast_create_param; + param.type = proc_param->unicast_to_broadcast.type; + param.param = proc_param->unicast_to_broadcast.reception_start_member_params; + + err = bt_le_ext_adv_get_info(proc_param->unicast_to_broadcast.ext_adv, &adv_info); + __ASSERT_NO_MSG(err != -EINVAL); + if (err != 0) { + /* May happen if the advertising set was deleted while in this procedure */ + LOG_DBG("Failed to get adv info: %d", err); + active_proc->err = err; + active_proc->failed_conn = NULL; + + bt_cap_handover_proc_complete(); + return; + } + + if (adv_info.ext_adv_state == BT_LE_EXT_ADV_STATE_CREATED) { + /* Start advertising to get the actual adv addr */ + err = bt_le_ext_adv_start(proc_param->unicast_to_broadcast.ext_adv, + BT_LE_EXT_ADV_START_DEFAULT); + if (err != 0) { + LOG_DBG("Failed to start advertising set (err %d)\n", err); + + active_proc->err = err; + active_proc->failed_conn = NULL; + + bt_cap_handover_proc_complete(); + + return; + } + + /* Since bt_le_ext_adv_get_info returns the pointer to actaul advertising address we + * do not need to call it again to get the address + */ + } + /* else if we are in the paused of active state, we get the current active adv address */ + + ARRAY_FOR_EACH(proc_param->unicast_to_broadcast.reception_start_member_params, i) { + struct bt_cap_commander_broadcast_reception_start_member_param *member_param = + &proc_param->unicast_to_broadcast.reception_start_member_params[i]; + + /* The member_param->member.member is populated from index and upwards, thus once we + * reach a NULL pointer in the array, there are no more acceptors to send the + * reception start request to. + */ + if (member_param->member.member == NULL) { + break; + } + + member_param->addr = bt_addr_le_any; + member_param->adv_sid = proc_param->unicast_to_broadcast.sid; + member_param->pa_interval = proc_param->unicast_to_broadcast.pa_interval; + member_param->broadcast_id = proc_param->unicast_to_broadcast.broadcast_id; + bt_addr_le_copy(&member_param->addr, adv_info.addr); + + member_param->num_subgroups = create_param->subgroup_count; + for (size_t j = 0U; j < member_param->num_subgroups; j++) { + const struct bt_cap_initiator_broadcast_subgroup_param *subgroup_param = + &create_param->subgroup_params[j]; + struct bt_bap_bass_subgroup *subgroup = &member_param->subgroups[j]; + + subgroup->metadata_len = subgroup_param->codec_cfg->meta_len; + (void)memcpy(subgroup->metadata, subgroup_param->codec_cfg->meta, + subgroup->metadata_len); + + /* The bis_sync value has been set up earlier in + * bt_cap_initiator_unicast_to_broadcast while we still had the ACL + * references + */ + __ASSERT(subgroup->bis_sync != 0U, "BIS sync was not properly setup"); + } + + param.count++; + } + + err = cap_commander_broadcast_reception_start(¶m); + if (err != 0) { + LOG_DBG("Failed to start reception start: %d", err); + active_proc->err = err; + active_proc->failed_conn = NULL; + + bt_cap_handover_proc_complete(); + } +} + +static void handover_unicast_to_broadcast_setup_broadcast(void) +{ + struct bt_cap_initiator_broadcast_create_param *broadcast_create_param; + struct bt_cap_broadcast_source **broadcast_source; + struct bt_cap_handover_proc_param *proc_param; + struct bt_cap_common_proc *active_proc; + struct bt_le_ext_adv *ext_adv; + int err; + + active_proc = bt_cap_common_get_active_proc(); + proc_param = &active_proc->proc_param.handover; + broadcast_create_param = proc_param->unicast_to_broadcast.broadcast_create_param; + + broadcast_source = &proc_param->unicast_to_broadcast.broadcast_source; + + err = bt_cap_unicast_group_delete(proc_param->unicast_to_broadcast.unicast_group); + __ASSERT_NO_MSG(err == 0); + proc_param->unicast_to_broadcast.unicast_group = NULL; + + err = bt_cap_initiator_broadcast_audio_create(broadcast_create_param, broadcast_source); + if (err != 0) { + LOG_DBG("Failed to create broadcast source: %d", err); + active_proc->err = err; + active_proc->failed_conn = NULL; + + bt_cap_handover_proc_complete(); + + return; + } + + ext_adv = active_proc->proc_param.handover.unicast_to_broadcast.ext_adv; + err = bt_cap_initiator_broadcast_audio_start(*broadcast_source, ext_adv); + if (err != 0) { + LOG_DBG("Failed to start broadcast source: %d", err); + active_proc->err = err; + active_proc->failed_conn = NULL; + + err = bt_cap_initiator_broadcast_audio_delete(*broadcast_source); + __ASSERT_NO_MSG(err == 0); + + bt_cap_handover_proc_complete(); + + return; + } +} + +static bool valid_unicast_to_broadcast_stream_metadata_param( + const struct bt_cap_initiator_broadcast_subgroup_param *subgroup_param, + const struct bt_cap_stream *stream, + const struct cap_unicast_group_stream_lookup *lookup_data) +{ + const uint8_t *broadcast_ccid_list; + const uint8_t *unicast_ccid_list; + int broadcast_ret; + int unicast_ret; + + /* Compare existing unicast metadata with the subgroup param meteadata. It + * is mandatory that the CCID list and the context type remain the same + */ + if (stream->bap_stream.codec_cfg == subgroup_param->codec_cfg) { + return true; + } + + /* Verify CCID lists */ + unicast_ret = bt_audio_codec_cfg_meta_get_ccid_list(stream->bap_stream.codec_cfg, + &unicast_ccid_list); + + /* CCID list is not mandatory, so it is OK if it is missing, as long + * as it is missing for both unicast and broadcast + */ + + if (unicast_ret < 0 && unicast_ret != -ENODATA) { + return false; + } + + broadcast_ret = bt_audio_codec_cfg_meta_get_ccid_list(subgroup_param->codec_cfg, + &broadcast_ccid_list); + + if (unicast_ret != broadcast_ret) { + return false; + } + + /* we only need to compare if the list exists and is non-empty */ + if (unicast_ret > 0 && !util_memeq(unicast_ccid_list, broadcast_ccid_list, unicast_ret)) { + return false; + } + + /* Verify streaming contexts (mandatory to be in the metadata )*/ + unicast_ret = bt_audio_codec_cfg_meta_get_stream_context(stream->bap_stream.codec_cfg); + if (unicast_ret <= 0) { /* mandatory to have a streaming context */ + return false; + } + + broadcast_ret = bt_audio_codec_cfg_meta_get_stream_context(subgroup_param->codec_cfg); + if (unicast_ret != broadcast_ret) { + return false; + } + + return true; +} + +static bool +valid_unicast_to_broadcast_metadata(const struct bt_cap_unicast_to_broadcast_param *param, + const struct cap_unicast_group_stream_lookup *lookup_data) +{ + size_t unique_metadata_cnt = 0U; + + /* Count unique metadata from the unicast streams to verify the correct number of + * subgroups exist. Each unique set of metadata needs to be in its own group + */ + for (size_t i = 0U; i < lookup_data->active_sink_streams_cnt; i++) { + const struct bt_bap_stream *bap_stream_i = + &lookup_data->active_sink_streams[i]->bap_stream; + const struct bt_audio_codec_cfg *codec_cfg_i = bap_stream_i->codec_cfg; + bool unique_metadata = true; + + for (size_t j = 0U; j < i; j++) { + const struct bt_bap_stream *bap_stream_j = + &lookup_data->active_sink_streams[j]->bap_stream; + const struct bt_audio_codec_cfg *codec_cfg_j = bap_stream_j->codec_cfg; + + if (codec_cfg_i == codec_cfg_j || + util_eq(codec_cfg_i->meta, codec_cfg_i->meta_len, codec_cfg_j->meta, + codec_cfg_j->meta_len)) { + unique_metadata = false; + break; + } + } + + if (unique_metadata) { + unique_metadata_cnt++; + } + } + + if (unique_metadata_cnt > CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT) { + LOG_DBG("Cannot create broadcast source with %zu subgroups (max %d)", + unique_metadata_cnt, CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT); + + return false; + } + + if (unique_metadata_cnt > param->broadcast_create_param->subgroup_count) { + LOG_DBG("Mismatch between unique metadata from unicast (%zu) and number of " + "subgroups (%zu)", + unique_metadata_cnt, param->broadcast_create_param->subgroup_count); + + return false; + } + + return true; +} + +static bool +valid_unicast_to_broadcast_create_param(const struct bt_cap_unicast_to_broadcast_param *param, + const struct cap_unicast_group_stream_lookup *lookup_data) +{ + size_t total_broadcast_streams = 0U; + + for (size_t i = 0U; i < param->broadcast_create_param->subgroup_count; i++) { + const struct bt_cap_initiator_broadcast_subgroup_param *subgroup_param = + ¶m->broadcast_create_param->subgroup_params[i]; + + for (size_t j = 0U; j < subgroup_param->stream_count; j++) { + const struct bt_cap_stream *stream = + subgroup_param->stream_params[j].stream; + bool stream_is_handed_over = false; + + if (stream->bap_stream.group != param->unicast_group->bap_unicast_group) { + LOG_DBG("Stream %p is not part of the unicast group %p", stream, + param->unicast_group->bap_unicast_group); + return false; + } + + for (size_t k = 0U; k < lookup_data->active_sink_streams_cnt; k++) { + if (stream == lookup_data->active_sink_streams[k]) { + stream_is_handed_over = true; + break; + } + } + + if (!stream_is_handed_over) { + LOG_DBG("Stream %p was not in unicast group %p", stream, + param->unicast_group); + return false; + } + + if (!valid_unicast_to_broadcast_stream_metadata_param( + subgroup_param, stream, lookup_data)) { + return false; + } + + total_broadcast_streams++; + } + } + + return true; +} + +static bool valid_unicast_to_broadcast_param(const struct bt_cap_unicast_to_broadcast_param *param, + struct cap_unicast_group_stream_lookup *lookup_data) +{ + struct bt_le_ext_adv_info adv_info; + int err; + + if (param == NULL) { + LOG_DBG("param is NULL"); + return false; + } + + if (param->unicast_group == NULL) { + LOG_DBG("param->unicast_group is NULL"); + return false; + } + + if (!cap_initiator_broadcast_audio_start_valid_param(param->broadcast_create_param)) { + LOG_DBG("param->broadcast_create_param is invalid"); + return false; + } + + if (param->ext_adv == NULL) { + LOG_DBG("param->ext_adv is NULL"); + return false; + } + + err = bt_le_ext_adv_get_info(param->ext_adv, &adv_info); + __ASSERT_NO_MSG(err == 0); + + if (adv_info.per_adv_state == BT_LE_PER_ADV_STATE_NONE) { + LOG_DBG("Advertising set %p not configured for periodic advertising", + param->ext_adv); + return false; + } + + if (param->broadcast_id > BT_AUDIO_BROADCAST_ID_MAX) { + LOG_DBG("param->broadcast_id is invalid: 0x%08X", param->broadcast_id); + return false; + } + + if (!IN_RANGE(param->pa_interval, BT_GAP_PER_ADV_MIN_INTERVAL, + BT_GAP_PER_ADV_MAX_INTERVAL)) { + LOG_DBG("param->pa_interval is invalid: %u", param->pa_interval); + return false; + } + + err = bt_cap_unicast_group_foreach_stream(param->unicast_group, + unicast_group_foreach_stream_cb, lookup_data); + __ASSERT_NO_MSG(err == 0); + if (lookup_data->active_sink_streams_cnt == 0U) { + LOG_DBG("param->unicast_group does not contain any active streams"); + + return false; + } + + if (!valid_unicast_to_broadcast_create_param(param, lookup_data)) { + return false; + } + + if (!valid_unicast_to_broadcast_metadata(param, lookup_data)) { + return false; + } + + return true; +} + +int bt_cap_initiator_unicast_to_broadcast(const struct bt_cap_unicast_to_broadcast_param *param) +{ + struct cap_unicast_group_stream_lookup lookup_data = {0}; + struct bt_cap_unicast_audio_stop_param stop_param = {0}; + struct bt_cap_handover_proc_param *proc_param; + struct bt_cap_common_proc *active_proc; + uint8_t bis_index; + int err; + + if (!valid_unicast_to_broadcast_param(param, &lookup_data)) { + return -EINVAL; + } + + if (bt_cap_common_test_and_set_proc_active()) { + LOG_DBG("A CAP procedure is already in progress"); + + return -EBUSY; + } + + /* TBD: How do we prevent the unicast group from being modified or deleted while we are + * doing this procedure? + * TBD: How do we check for BASS? + */ + + if (lookup_data.cnt == 0U) { + LOG_DBG("param->unicast_group does not contain any streams"); + + bt_cap_common_clear_active_proc(); + + return -EINVAL; + } + + active_proc = bt_cap_common_get_active_proc(); + proc_param = &active_proc->proc_param.handover; + + /* Populate an array of unique connection pointers to determine which acceptors to add the + * broadcast source to + */ + for (size_t i = 0U; i < lookup_data.active_sink_streams_cnt; i++) { + const struct bt_cap_stream *stream = lookup_data.active_sink_streams[i]; + bool conn_added; + + /* Add stream's conn to conns if not already there */ + LOG_ERR("proc_param->unicast_to_broadcast.reception_start_member_params %zu", + ARRAY_SIZE(proc_param->unicast_to_broadcast.reception_start_member_params)); + conn_added = false; + ARRAY_FOR_EACH(proc_param->unicast_to_broadcast.reception_start_member_params, j) { + struct bt_cap_commander_broadcast_reception_start_member_param + *member_param = &proc_param->unicast_to_broadcast + .reception_start_member_params[j]; + + if (member_param->member.member == stream->bap_stream.conn) { + conn_added = true; + break; + } else if (member_param->member.member == NULL) { + member_param->member.member = stream->bap_stream.conn; + conn_added = true; + break; + } + } + + if (!conn_added) { + LOG_DBG("Stream %p connection could be added. " + "Some streams contains an invalid conn pointer", + stream); + + bt_cap_common_clear_active_proc(); + + return -EINVAL; + } + } + + /* We need to set up the expected BIS index fields for the reception start now, as once the + * unicast group has been stopped, the reference to the ACL connection from the stream will + * be lost + */ + + bis_index = 1U; /* BIS indexes start from 1 */ + for (size_t i = 0U; i < param->broadcast_create_param->subgroup_count; i++) { + const struct bt_cap_initiator_broadcast_subgroup_param *subgroup_param = + ¶m->broadcast_create_param->subgroup_params[i]; + + for (size_t j = 0U; j < subgroup_param->stream_count; j++) { + const struct bt_cap_stream *stream = + subgroup_param->stream_params[j].stream; + + ARRAY_FOR_EACH_PTR( + proc_param->unicast_to_broadcast.reception_start_member_params, + member_param) { + /* Once we reach a NULL connection pointer, we've handled all + * accepters from the unicast group + */ + if (member_param->member.member == NULL) { + break; + } + + if (stream->bap_stream.conn == member_param->member.member) { + member_param->subgroups[i].bis_sync |= + BT_ISO_BIS_INDEX_BIT(bis_index); + } + } + + bis_index++; + } + } + + /* Store the broadcast parameters for later */ + proc_param->unicast_to_broadcast.broadcast_create_param = param->broadcast_create_param; + proc_param->unicast_to_broadcast.unicast_group = param->unicast_group; + proc_param->unicast_to_broadcast.ext_adv = param->ext_adv; + proc_param->unicast_to_broadcast.type = param->type; + proc_param->unicast_to_broadcast.sid = param->sid; + proc_param->unicast_to_broadcast.pa_interval = param->pa_interval; + proc_param->unicast_to_broadcast.broadcast_id = param->broadcast_id; + + stop_param.type = param->type; + stop_param.release = true; + stop_param.streams = lookup_data.streams; + stop_param.count = lookup_data.cnt; + + bt_cap_common_set_handover_active(); + + __ASSERT_NO_MSG(valid_unicast_audio_stop_param(&stop_param)); + + err = unicast_audio_stop(&stop_param); + if (err != 0) { + LOG_DBG("Failed to stop unicast audio: %d", err); + + /* TODO return expected return values */ + + bt_cap_common_clear_active_proc(); + + return -ENOEXEC; + } + + return 0; } int bt_cap_initiator_broadcast_to_unicast(const struct bt_cap_broadcast_to_unicast_param *param, @@ -2283,4 +3176,4 @@ int bt_cap_initiator_broadcast_to_unicast(const struct bt_cap_broadcast_to_unica return -ENOSYS; } -#endif /* CONFIG_BT_BAP_BROADCAST_SOURCE && CONFIG_BT_BAP_UNICAST_CLIENT */ +#endif /* CONFIG_BT_CAP_HANDOVER_SUPPORTED */ diff --git a/subsys/bluetooth/audio/cap_internal.h b/subsys/bluetooth/audio/cap_internal.h index f6bdb40d6e3d..ae0de7d83ed3 100644 --- a/subsys/bluetooth/audio/cap_internal.h +++ b/subsys/bluetooth/audio/cap_internal.h @@ -1,7 +1,7 @@ /* Bluetooth Audio Common Audio Profile internal header */ /* - * Copyright (c) 2022-2024 Nordic Semiconductor ASA + * Copyright (c) 2022-2025 Nordic Semiconductor ASA * * SPDX-License-Identifier: Apache-2.0 */ @@ -17,10 +17,13 @@ #include #include #include +#include #include #include #include #include +#include +#include #include bool bt_cap_acceptor_ccid_exist(const struct bt_conn *conn, uint8_t ccid); @@ -41,6 +44,11 @@ void bt_cap_initiator_cp_cb(struct bt_cap_stream *cap_stream, enum bt_bap_ascs_r enum bt_cap_common_proc_state { BT_CAP_COMMON_PROC_STATE_ACTIVE, BT_CAP_COMMON_PROC_STATE_ABORTED, + /* While the handover is technically a procedure, and arguably should use + * `bt_cap_common_set_proc`, we set the it as a flag instead so that we can rely on the API + * functions to perform their procedures + */ + BT_CAP_COMMON_PROC_STATE_HANDOVER, BT_CAP_COMMON_PROC_STATE_FLAG_NUM, }; @@ -161,6 +169,36 @@ struct bt_cap_commander_proc_param { }; }; +#if defined(CONFIG_BT_CAP_HANDOVER_SUPPORTED) +struct bt_cap_handover_proc_param { + union { + struct { + /* Struct containing the converted unicast group configuration */ + struct bt_cap_initiator_broadcast_create_param *broadcast_create_param; + /* The resulting broadcast source */ + struct bt_cap_broadcast_source *broadcast_source; + /* The advertising set to start the source on */ + struct bt_le_ext_adv *ext_adv; + /* The source unicast group with the streams. */ + struct bt_cap_unicast_group *unicast_group; + /* Set type */ + enum bt_cap_set_type type; + + /* The SID of the ext_adv */ + uint8_t sid; + /* The PA interval of the ext_adv */ + uint16_t pa_interval; + /* The broadcast ID the broadcast source will use */ + uint32_t broadcast_id; + + struct bt_cap_commander_broadcast_reception_start_member_param + reception_start_member_params[CONFIG_BT_MAX_CONN]; + } unicast_to_broadcast; + /* TODO: Add unicast broadcast_to_unicast params */ + }; +}; +#endif /* CONFIG_BT_CAP_HANDOVER_SUPPORTED */ + typedef void (*bt_cap_common_discover_func_t)( 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); @@ -175,6 +213,12 @@ struct bt_cap_common_proc_param { struct bt_cap_commander_proc_param commander[CONFIG_BT_MAX_CONN]; #endif /* CONFIG_BT_CAP_COMMANDER */ }; +#if defined(CONFIG_BT_CAP_HANDOVER_SUPPORTED) + /* The handover parameters cannot be part of the union as they need to exist while we are + * performing CAP initiator and CAP Commander procedures + */ + struct bt_cap_handover_proc_param handover; +#endif /* CONFIG_BT_CAP_HANDOVER_SUPPORTED */ }; struct bt_cap_common_proc { @@ -206,6 +250,8 @@ struct bt_cap_common_proc *bt_cap_common_get_active_proc(void); void bt_cap_common_clear_active_proc(void); void bt_cap_common_set_proc(enum bt_cap_common_proc_type proc_type, size_t proc_cnt); void bt_cap_common_set_subproc(enum bt_cap_common_subproc_type subproc_type); +void bt_cap_common_set_handover_active(void); +bool bt_cap_common_handover_is_active(void); bool bt_cap_common_proc_is_type(enum bt_cap_common_proc_type proc_type); bool bt_cap_common_subproc_is_type(enum bt_cap_common_subproc_type subproc_type); struct bt_conn *bt_cap_common_get_member_conn(enum bt_cap_set_type type, @@ -223,3 +269,7 @@ struct bt_cap_common_client *bt_cap_common_get_client_by_acl(const struct bt_con struct bt_cap_common_client * bt_cap_common_get_client_by_csis(const struct bt_csip_set_coordinator_csis_inst *csis_inst); int bt_cap_common_discover(struct bt_conn *conn, bt_cap_common_discover_func_t func); + +void bt_cap_handover_proc_complete(void); +int cap_commander_broadcast_reception_start( + const struct bt_cap_commander_broadcast_reception_start_param *param); diff --git a/subsys/bluetooth/audio/shell/cap_initiator.c b/subsys/bluetooth/audio/shell/cap_initiator.c index bfcba65f70e0..d9a076984966 100644 --- a/subsys/bluetooth/audio/shell/cap_initiator.c +++ b/subsys/bluetooth/audio/shell/cap_initiator.c @@ -42,6 +42,8 @@ #define UNICAST_SINK_SUPPORTED (CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT > 0) #define UNICAST_SRC_SUPPORTED (CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT > 0) +struct bt_cap_unicast_group *cap_unicast_group; + #define CAP_UNICAST_CLIENT_STREAM_COUNT ARRAY_SIZE(unicast_streams) static void cap_discover_cb(struct bt_conn *conn, int err, @@ -88,13 +90,13 @@ static void unicast_stop_complete_cb(int err, struct bt_conn *conn) } else { bt_shell_print("Unicast stop completed"); - if (default_unicast_group != NULL) { - err = bt_bap_unicast_group_delete(default_unicast_group); + if (cap_unicast_group != NULL) { + err = bt_cap_unicast_group_delete(cap_unicast_group); if (err != 0) { bt_shell_error("Failed to delete unicast group %p: %d", - default_unicast_group, err); + cap_unicast_group, err); } else { - default_unicast_group = NULL; + cap_unicast_group = NULL; } } } @@ -146,15 +148,15 @@ static void populate_connected_conns(struct bt_conn *conn, void *data) static int cmd_cap_initiator_unicast_start(const struct shell *sh, size_t argc, char *argv[]) { - struct bt_bap_unicast_group_stream_param + struct bt_cap_unicast_group_stream_param group_stream_params[CAP_UNICAST_CLIENT_STREAM_COUNT] = {0}; - struct bt_bap_unicast_group_stream_pair_param + struct bt_cap_unicast_group_stream_pair_param pair_params[CAP_UNICAST_CLIENT_STREAM_COUNT] = {0}; struct bt_cap_unicast_audio_start_stream_param stream_param[CAP_UNICAST_CLIENT_STREAM_COUNT] = {0}; struct bt_conn *connected_conns[CONFIG_BT_MAX_CONN] = {0}; struct bt_cap_unicast_audio_start_param start_param = {0}; - struct bt_bap_unicast_group_param group_param = {0}; + struct bt_cap_unicast_group_param group_param = {0}; size_t source_cnt = 1U; ssize_t conn_cnt = 1U; size_t sink_cnt = 1U; @@ -262,8 +264,8 @@ static int cmd_cap_initiator_unicast_start(const struct shell *sh, size_t argc, stream_param[start_param.count].codec_cfg = &uni_stream->codec_cfg; group_stream_params[start_param.count].stream = - &stream_param[start_param.count].stream->bap_stream; - group_stream_params[start_param.count].qos = &uni_stream->qos; + stream_param[start_param.count].stream; + group_stream_params[start_param.count].qos_cfg = &uni_stream->qos; pair_params[pair_cnt + j].tx_param = &group_stream_params[start_param.count]; @@ -293,8 +295,8 @@ static int cmd_cap_initiator_unicast_start(const struct shell *sh, size_t argc, copy_unicast_stream_preset(uni_stream, &default_source_preset); stream_param[start_param.count].codec_cfg = &uni_stream->codec_cfg; group_stream_params[start_param.count].stream = - &stream_param[start_param.count].stream->bap_stream; - group_stream_params[start_param.count].qos = &uni_stream->qos; + stream_param[start_param.count].stream; + group_stream_params[start_param.count].qos_cfg = &uni_stream->qos; pair_params[pair_cnt + j].rx_param = &group_stream_params[start_param.count]; @@ -318,8 +320,8 @@ static int cmd_cap_initiator_unicast_start(const struct shell *sh, size_t argc, group_param.params_count = pair_cnt; group_param.params = pair_params; - if (default_unicast_group == NULL) { - err = bt_bap_unicast_group_create(&group_param, &default_unicast_group); + if (cap_unicast_group == NULL) { + err = bt_cap_unicast_group_create(&group_param, &cap_unicast_group); if (err != 0) { shell_print(sh, "Failed to create group: %d", err); @@ -748,7 +750,7 @@ int cap_ac_unicast(const struct shell *sh, const struct bap_unicast_ac_param *pa size_t src_cnt = 0; int err; - if (default_unicast_group != NULL) { + if (cap_unicast_group != NULL) { shell_error(sh, "Unicast Group already exist, please delete first"); return -ENOEXEC; } @@ -840,11 +842,11 @@ int cap_ac_unicast(const struct shell *sh, const struct bap_unicast_ac_param *pa if (err != 0) { shell_error(sh, "Failed to start unicast audio: %d", err); - err = bt_bap_unicast_group_delete(default_unicast_group); + err = bt_cap_unicast_group_delete(cap_unicast_group); if (err != 0) { shell_error(sh, "Failed to delete group: %d", err); } else { - default_unicast_group = NULL; + cap_unicast_group = NULL; } return -ENOEXEC; diff --git a/subsys/bluetooth/host/adv.c b/subsys/bluetooth/host/adv.c index 0694c2ce54b3..7fdc3ec1a58e 100644 --- a/subsys/bluetooth/host/adv.c +++ b/subsys/bluetooth/host/adv.c @@ -1592,10 +1592,45 @@ void bt_le_adv_resume(void) int bt_le_ext_adv_get_info(const struct bt_le_ext_adv *adv, struct bt_le_ext_adv_info *info) { + if (adv == NULL) { + LOG_DBG("adv is NULL"); + return -EINVAL; + } + + if (info == NULL) { + LOG_DBG("info is NULL"); + return -EINVAL; + } + + if (!atomic_test_bit(adv->flags, BT_ADV_CREATED)) { + LOG_DBG("Advertising set %p is not created", adv); + return -ENXIO; + } + + (void)memset(info, 0, sizeof(*info)); + info->id = adv->id; info->tx_power = adv->tx_power; info->addr = &adv->random_addr; + if (atomic_test_bit(adv->flags, BT_ADV_ENABLED)) { + info->ext_adv_state = BT_LE_EXT_ADV_STATE_STARTED; + } else if (atomic_test_bit(adv->flags, BT_ADV_PAUSED)) { + info->ext_adv_state = BT_LE_EXT_ADV_STATE_PAUSED; + } else { + info->ext_adv_state = BT_LE_EXT_ADV_STATE_CREATED; + } + + if (IS_ENABLED(CONFIG_BT_PER_ADV)) { + if (atomic_test_bit(adv->flags, BT_PER_ADV_PARAMS_SET)) { + info->per_adv_state = BT_LE_PER_ADV_STATE_CONFIGURED; + } else if (atomic_test_bit(adv->flags, BT_PER_ADV_ENABLED)) { + info->per_adv_state = BT_LE_PER_ADV_STATE_STARTED; + } else { + info->per_adv_state = BT_LE_PER_ADV_STATE_NONE; + } + } + return 0; } diff --git a/subsys/bluetooth/host/shell/bt.c b/subsys/bluetooth/host/shell/bt.c index a2ee453b2ea7..3d49d60bf398 100644 --- a/subsys/bluetooth/host/shell/bt.c +++ b/subsys/bluetooth/host/shell/bt.c @@ -2566,7 +2566,11 @@ static int cmd_adv_info(const struct shell *sh, size_t argc, char *argv[]) shell_print(sh, "Advertiser[%d] %p", selected_adv, adv); shell_print(sh, "Id: %d, TX power: %d dBm", info.id, info.tx_power); - print_le_addr("Address", info.addr); + shell_print(sh, "Adv state: %d", info.ext_adv_state); + if (info.ext_adv_state == BT_LE_EXT_ADV_STATE_STARTED) { + print_le_addr("Address", info.addr); + } + shell_print(sh, "Per Adv state: %d", info.per_adv_state); return 0; } diff --git a/tests/bluetooth/audio/cap_initiator/CMakeLists.txt b/tests/bluetooth/audio/cap_initiator/CMakeLists.txt index 2e4b1d275f0b..29d56f22a283 100644 --- a/tests/bluetooth/audio/cap_initiator/CMakeLists.txt +++ b/tests/bluetooth/audio/cap_initiator/CMakeLists.txt @@ -16,6 +16,7 @@ target_sources(testbinary PRIVATE src/main.c src/test_common.c + src/test_unicast_group.c src/test_unicast_start.c src/test_unicast_stop.c ) diff --git a/tests/bluetooth/audio/cap_initiator/src/test_unicast_group.c b/tests/bluetooth/audio/cap_initiator/src/test_unicast_group.c new file mode 100644 index 000000000000..ee6da9e53401 --- /dev/null +++ b/tests/bluetooth/audio/cap_initiator/src/test_unicast_group.c @@ -0,0 +1,390 @@ +/* test_unicast_group.c - unit test for unicast group functions */ + +/* + * 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 "bap_endpoint.h" +#include "test_common.h" +#include "ztest_assert.h" +#include "ztest_test.h" + +struct cap_initiator_test_unicast_group_fixture { + struct bt_cap_unicast_group_param *group_param; + struct bt_cap_unicast_group *unicast_group; + struct bt_bap_qos_cfg *qos_cfg; +}; + +static void *cap_initiator_test_unicast_group_setup(void) +{ + struct cap_initiator_test_unicast_group_fixture *fixture; + + fixture = malloc(sizeof(*fixture)); + zassert_not_null(fixture); + + return fixture; +} + +static void cap_initiator_test_unicast_group_before(void *f) +{ + + struct cap_initiator_test_unicast_group_fixture *fixture = f; + struct bt_cap_unicast_group_stream_pair_param *pair_params; + struct bt_cap_unicast_group_stream_param *stream_params; + struct bt_cap_stream *cap_streams; + size_t pair_cnt = 0U; + size_t str_cnt = 0U; + + memset(f, 0, sizeof(struct cap_initiator_test_unicast_group_fixture)); + + fixture->group_param = calloc(sizeof(struct bt_cap_unicast_group_param), 1); + zassert_not_null(fixture->group_param); + pair_params = calloc(sizeof(struct bt_cap_unicast_group_stream_pair_param), + DIV_ROUND_UP(CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT, 2U)); + zassert_not_null(pair_params); + stream_params = calloc(sizeof(struct bt_cap_unicast_group_stream_param), + CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT); + zassert_not_null(stream_params); + cap_streams = calloc(sizeof(struct bt_cap_stream), + CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT); + zassert_not_null(cap_streams); + fixture->qos_cfg = calloc(sizeof(struct bt_bap_qos_cfg), 1); + zassert_not_null(fixture->qos_cfg); + + *fixture->qos_cfg = BT_BAP_QOS_CFG_UNFRAMED(10000u, 40u, 2u, 10u, 40000u); /* 16_2_1 */ + + while (str_cnt < CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT) { + stream_params[str_cnt].stream = &cap_streams[str_cnt]; + stream_params[str_cnt].qos_cfg = fixture->qos_cfg; + + if (str_cnt & 1) { + pair_params[pair_cnt].tx_param = &stream_params[str_cnt]; + } else { + pair_params[pair_cnt].rx_param = &stream_params[str_cnt]; + } + + str_cnt++; + pair_cnt = str_cnt / 2U; + } + + fixture->group_param->packing = BT_ISO_PACKING_SEQUENTIAL; + fixture->group_param->params_count = pair_cnt; + fixture->group_param->params = pair_params; +} + +static void cap_initiator_test_unicast_group_after(void *f) +{ + struct cap_initiator_test_unicast_group_fixture *fixture = f; + struct bt_cap_unicast_group_param *group_param; + + /* In the case of a test failing, we delete the group so that subsequent tests won't fail */ + if (fixture->unicast_group != NULL) { + bt_cap_unicast_group_delete(fixture->unicast_group); + } + + group_param = fixture->group_param; + + free(group_param->params[0].rx_param->stream); + free(group_param->params[0].rx_param); + free(group_param->params); + free(group_param); + free(fixture->qos_cfg); +} + +static void cap_initiator_test_unicast_group_teardown(void *f) +{ + free(f); +} + +ZTEST_SUITE(cap_initiator_test_unicast_group, NULL, cap_initiator_test_unicast_group_setup, + cap_initiator_test_unicast_group_before, cap_initiator_test_unicast_group_after, + cap_initiator_test_unicast_group_teardown); + +static ZTEST_F(cap_initiator_test_unicast_group, test_initiator_unicast_group_create) +{ + int err = 0; + + err = bt_cap_unicast_group_create(fixture->group_param, &fixture->unicast_group); + zassert_equal(err, 0, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_initiator_test_unicast_group, + test_initiator_unicast_group_create_inval_null_param) +{ + int err = 0; + + err = bt_cap_unicast_group_create(NULL, &fixture->unicast_group); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_initiator_test_unicast_group, + test_initiator_unicast_group_create_inval_null_rx_stream) +{ + int err = 0; + + if (fixture->group_param->params[0].rx_param->stream == NULL) { + ztest_test_skip(); + } + fixture->group_param->params[0].rx_param->stream = NULL; + + err = bt_cap_unicast_group_create(fixture->group_param, &fixture->unicast_group); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_initiator_test_unicast_group, + test_initiator_unicast_group_create_inval_null_tx_stream) +{ + int err = 0; + + if (fixture->group_param->params[0].tx_param->stream == NULL) { + ztest_test_skip(); + } + fixture->group_param->params[0].tx_param->stream = NULL; + + err = bt_cap_unicast_group_create(fixture->group_param, &fixture->unicast_group); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_initiator_test_unicast_group, + test_initiator_unicast_group_create_inval_too_many_streams) +{ + int err = 0; + + fixture->group_param->params_count = CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT + 1; + + err = bt_cap_unicast_group_create(fixture->group_param, &fixture->unicast_group); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_initiator_test_unicast_group, test_initiator_unicast_group_reconfig) +{ + int err = 0; + + err = bt_cap_unicast_group_create(fixture->group_param, &fixture->unicast_group); + zassert_equal(err, 0, "Unexpected return value %d", err); + + err = bt_cap_unicast_group_reconfig(fixture->unicast_group, fixture->group_param); + zassert_equal(err, 0, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_initiator_test_unicast_group, + test_initiator_unicast_group_reconfig_inval_null_group) +{ + int err = 0; + + err = bt_cap_unicast_group_create(fixture->group_param, &fixture->unicast_group); + zassert_equal(err, 0, "Unexpected return value %d", err); + + err = bt_cap_unicast_group_reconfig(NULL, fixture->group_param); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_initiator_test_unicast_group, + test_initiator_unicast_group_reconfig_inval_null_param) +{ + int err = 0; + + err = bt_cap_unicast_group_create(fixture->group_param, &fixture->unicast_group); + zassert_equal(err, 0, "Unexpected return value %d", err); + + err = bt_cap_unicast_group_reconfig(fixture->unicast_group, NULL); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_initiator_test_unicast_group, test_initiator_unicast_group_add_streams) +{ + struct bt_cap_stream stream = {}; + struct bt_cap_unicast_group_stream_param stream_param = { + .stream = &stream, + .qos_cfg = fixture->qos_cfg, + }; + const struct bt_cap_unicast_group_stream_pair_param pair_param = { + .rx_param = &stream_param, + }; + int err = 0; + + err = bt_cap_unicast_group_create(fixture->group_param, &fixture->unicast_group); + zassert_equal(err, 0, "Unexpected return value %d", err); + + err = bt_cap_unicast_group_add_streams(fixture->unicast_group, &pair_param, 1); + zassert_equal(err, 0, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_initiator_test_unicast_group, + test_initiator_unicast_group_add_streams_inval_null_group) +{ + struct bt_cap_stream stream = {}; + struct bt_cap_unicast_group_stream_param stream_param = { + .stream = &stream, + .qos_cfg = fixture->qos_cfg, + }; + const struct bt_cap_unicast_group_stream_pair_param pair_param = { + .rx_param = &stream_param, + }; + int err = 0; + + err = bt_cap_unicast_group_create(fixture->group_param, &fixture->unicast_group); + zassert_equal(err, 0, "Unexpected return value %d", err); + + err = bt_cap_unicast_group_add_streams(NULL, &pair_param, 1); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_initiator_test_unicast_group, + test_initiator_unicast_group_add_streams_inval_null_param) +{ + int err = 0; + + err = bt_cap_unicast_group_create(fixture->group_param, &fixture->unicast_group); + zassert_equal(err, 0, "Unexpected return value %d", err); + + err = bt_cap_unicast_group_add_streams(fixture->unicast_group, NULL, 1); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_initiator_test_unicast_group, + test_initiator_unicast_group_add_streams_inval_0_param) +{ + struct bt_cap_stream stream = {}; + struct bt_cap_unicast_group_stream_param stream_param = { + .stream = &stream, + .qos_cfg = fixture->qos_cfg, + }; + const struct bt_cap_unicast_group_stream_pair_param pair_param = { + .rx_param = &stream_param, + }; + int err = 0; + + err = bt_cap_unicast_group_create(fixture->group_param, &fixture->unicast_group); + zassert_equal(err, 0, "Unexpected return value %d", err); + + err = bt_cap_unicast_group_add_streams(fixture->unicast_group, &pair_param, 0); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_initiator_test_unicast_group, test_initiator_unicast_group_delete) +{ + int err = 0; + + err = bt_cap_unicast_group_create(fixture->group_param, &fixture->unicast_group); + zassert_equal(err, 0, "Unexpected return value %d", err); + + err = bt_cap_unicast_group_delete(fixture->unicast_group); + zassert_equal(err, 0, "Unexpected return value %d", err); + fixture->unicast_group = NULL; +} + +static ZTEST_F(cap_initiator_test_unicast_group, + test_initiator_unicast_group_delete_inval_null_group) +{ + int err = 0; + + err = bt_cap_unicast_group_create(fixture->group_param, &fixture->unicast_group); + zassert_equal(err, 0, "Unexpected return value %d", err); + + err = bt_cap_unicast_group_delete(NULL); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_initiator_test_unicast_group, + test_initiator_unicast_group_delete_inval_double_delete) +{ + int err = 0; + + err = bt_cap_unicast_group_create(fixture->group_param, &fixture->unicast_group); + zassert_equal(err, 0, "Unexpected return value %d", err); + + err = bt_cap_unicast_group_delete(fixture->unicast_group); + zassert_equal(err, 0, "Unexpected return value %d", err); + + err = bt_cap_unicast_group_delete(fixture->unicast_group); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); + fixture->unicast_group = NULL; +} + +static bool unicast_group_foreach_stream_cb(struct bt_cap_stream *cap_stream, void *user_data) +{ + size_t *cnt = user_data; + + (*cnt)++; + + return false; +} + +static ZTEST_F(cap_initiator_test_unicast_group, test_initiator_unicast_group_foreach_stream) +{ + size_t expect_cnt = 0U; + size_t cnt = 0U; + int err; + + err = bt_cap_unicast_group_create(fixture->group_param, &fixture->unicast_group); + zassert_equal(err, 0, "Unexpected return value %d", err); + + err = bt_cap_unicast_group_foreach_stream(fixture->unicast_group, + unicast_group_foreach_stream_cb, &cnt); + zassert_equal(err, 0, "Unexpected return value %d", err); + + for (size_t i = 0; i < fixture->group_param->params_count; i++) { + if (fixture->group_param->params[i].rx_param != NULL) { + expect_cnt++; + } + + if (fixture->group_param->params[i].tx_param != NULL) { + expect_cnt++; + } + } + + zassert_equal(cnt, expect_cnt, "Unexpected cnt (%zu != %zu)", cnt, expect_cnt); +} + +static ZTEST_F(cap_initiator_test_unicast_group, + test_initiator_unicast_group_foreach_stream_inval_null_group) +{ + size_t expect_cnt = 0U; + size_t cnt = 0U; + int err; + + err = bt_cap_unicast_group_create(fixture->group_param, &fixture->unicast_group); + zassert_equal(err, 0, "Unexpected return value %d", err); + + err = bt_cap_unicast_group_foreach_stream(NULL, unicast_group_foreach_stream_cb, &cnt); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); + + zassert_equal(cnt, expect_cnt, "Unexpected cnt (%zu != %zu)", cnt, expect_cnt); +} + +static ZTEST_F(cap_initiator_test_unicast_group, + test_initiator_unicast_group_foreach_stream_inval_null_func) +{ + size_t expect_cnt = 0U; + size_t cnt = 0U; + int err; + + err = bt_cap_unicast_group_create(fixture->group_param, &fixture->unicast_group); + zassert_equal(err, 0, "Unexpected return value %d", err); + + err = bt_cap_unicast_group_foreach_stream(fixture->unicast_group, NULL, &cnt); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); + + zassert_equal(cnt, expect_cnt, "Unexpected cnt (%zu != %zu)", cnt, expect_cnt); +} diff --git a/tests/bluetooth/audio/cap_initiator/uut/bap_unicast_client.c b/tests/bluetooth/audio/cap_initiator/uut/bap_unicast_client.c index 5a8994f36cdf..b054ea26e460 100644 --- a/tests/bluetooth/audio/cap_initiator/uut/bap_unicast_client.c +++ b/tests/bluetooth/audio/cap_initiator/uut/bap_unicast_client.c @@ -1,6 +1,6 @@ /* * Copyright (c) 2023 Codecoup - * Copyright (c) 2024 Nordic Semiconductor ASA + * Copyright (c) 2024-2025 Nordic Semiconductor ASA * * SPDX-License-Identifier: Apache-2.0 */ @@ -23,6 +23,7 @@ #include "bap_iso.h" static struct bt_bap_unicast_client_cb *unicast_client_cb; +static struct bt_bap_unicast_group bap_unicast_group; bool bt_bap_ep_is_unicast_client(const struct bt_bap_ep *ep) { @@ -361,3 +362,104 @@ int bt_bap_unicast_client_register_cb(struct bt_bap_unicast_client_cb *cb) return 0; } + +int bt_bap_unicast_group_create(struct bt_bap_unicast_group_param *param, + struct bt_bap_unicast_group **unicast_group) +{ + if (bap_unicast_group.allocated) { + return -ENOMEM; + } + + bap_unicast_group.allocated = true; + *unicast_group = &bap_unicast_group; + + sys_slist_init(&bap_unicast_group.streams); + for (size_t i = 0U; i < param->params_count; i++) { + if (param->params[i].rx_param != NULL) { + sys_slist_append(&bap_unicast_group.streams, + ¶m->params[i].rx_param->stream->_node); + } + + if (param->params[i].tx_param != NULL) { + sys_slist_append(&bap_unicast_group.streams, + ¶m->params[i].tx_param->stream->_node); + } + } + + return 0; +} + +int bt_bap_unicast_group_reconfig(struct bt_bap_unicast_group *unicast_group, + const struct bt_bap_unicast_group_param *param) +{ + if (unicast_group == NULL || param == NULL) { + return -EINVAL; + } + + return 0; +} + +int bt_bap_unicast_group_add_streams(struct bt_bap_unicast_group *unicast_group, + struct bt_bap_unicast_group_stream_pair_param params[], + size_t num_param) +{ + if (unicast_group == NULL || params == NULL) { + return -EINVAL; + } + + for (size_t i = 0U; i < num_param; i++) { + if (params[i].rx_param != NULL) { + sys_slist_append(&unicast_group->streams, + ¶ms[i].rx_param->stream->_node); + } + + if (params[i].tx_param != NULL) { + sys_slist_append(&unicast_group->streams, + ¶ms[i].tx_param->stream->_node); + } + } + + return 0; +} + +int bt_bap_unicast_group_delete(struct bt_bap_unicast_group *unicast_group) +{ + struct bt_bap_stream *stream, *next; + + if (unicast_group == NULL) { + return -EINVAL; + } + + unicast_group->allocated = false; + + SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&unicast_group->streams, stream, next, _node) { + sys_slist_remove(&unicast_group->streams, NULL, &stream->_node); + } + + return 0; +} + +int bt_bap_unicast_group_foreach_stream(struct bt_bap_unicast_group *unicast_group, + bt_bap_unicast_group_foreach_stream_func_t func, + void *user_data) +{ + struct bt_bap_stream *stream, *next; + + if (unicast_group == NULL) { + return -EINVAL; + } + + if (func == NULL) { + return -EINVAL; + } + + SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&unicast_group->streams, stream, next, _node) { + const bool stop = func(stream, user_data); + + if (stop) { + return -ECANCELED; + } + } + + return 0; +} diff --git a/tests/bluetooth/audio/mocks/src/kernel.c b/tests/bluetooth/audio/mocks/src/kernel.c index f150d5cb5cff..4b84b9a94ad4 100644 --- a/tests/bluetooth/audio/mocks/src/kernel.c +++ b/tests/bluetooth/audio/mocks/src/kernel.c @@ -21,6 +21,8 @@ #define FFF_FAKES_LIST(FAKE) \ FAKE(z_timeout_remaining) \ FAKE(k_work_cancel_delayable_sync) \ + FAKE(k_sem_take) \ + FAKE(k_sem_give) /* List of k_work items to be worked. */ static sys_slist_t work_pending; diff --git a/tests/bsim/bluetooth/audio/src/bap_bass_broadcaster_test.c b/tests/bsim/bluetooth/audio/src/bap_bass_broadcaster_test.c index fb1da4e69049..09fad3478d45 100644 --- a/tests/bsim/bluetooth/audio/src/bap_bass_broadcaster_test.c +++ b/tests/bsim/bluetooth/audio/src/bap_bass_broadcaster_test.c @@ -51,19 +51,7 @@ static void test_main(void) return; } - /* Enable Periodic Advertising */ - err = bt_le_per_adv_start(adv); - if (err) { - FAIL("Failed to enable periodic advertising (err %d)\n", err); - return; - } - - /* Start extended advertising */ - err = bt_le_ext_adv_start(adv, BT_LE_EXT_ADV_START_DEFAULT); - if (err) { - FAIL("Failed to start extended advertising (err %d)\n", err); - return; - } + start_broadcast_adv(adv); printk("Advertising successfully started\n"); diff --git a/tests/bsim/bluetooth/audio/src/bap_broadcast_source_test.c b/tests/bsim/bluetooth/audio/src/bap_broadcast_source_test.c index 792161bdadb7..9f6417eee290 100644 --- a/tests/bsim/bluetooth/audio/src/bap_broadcast_source_test.c +++ b/tests/bsim/bluetooth/audio/src/bap_broadcast_source_test.c @@ -375,19 +375,7 @@ static int setup_extended_adv(struct bt_bap_broadcast_source *source, struct bt_ return err; } - /* Start extended advertising */ - err = bt_le_ext_adv_start(*adv, BT_LE_EXT_ADV_START_DEFAULT); - if (err) { - printk("Failed to start extended advertising: %d\n", err); - return err; - } - - /* Enable Periodic Advertising */ - err = bt_le_per_adv_start(*adv); - if (err) { - printk("Failed to enable periodic advertising: %d\n", err); - return err; - } + start_broadcast_adv(*adv); return 0; } diff --git a/tests/bsim/bluetooth/audio/src/cap_acceptor_test.c b/tests/bsim/bluetooth/audio/src/cap_acceptor_test.c index d21b5c018432..af3bb51429d6 100644 --- a/tests/bsim/bluetooth/audio/src/cap_acceptor_test.c +++ b/tests/bsim/bluetooth/audio/src/cap_acceptor_test.c @@ -647,7 +647,7 @@ static int set_supported_contexts(void) return 0; } -void test_start_adv(void) +static void test_start_adv(void) { struct bt_le_ext_adv *ext_adv; @@ -680,8 +680,7 @@ static void init(void) .rank = 1, .lockable = true, /* Using the CSIP_SET_MEMBER test sample SIRK */ - .sirk = { 0xcd, 0xcc, 0x72, 0xdd, 0x86, 0x8c, 0xcd, 0xce, - 0x22, 0xfd, 0xa1, 0x21, 0x09, 0x7d, 0x7d, 0x45 }, + .sirk = TEST_SAMPLE_SIRK, }; static const struct bt_audio_codec_cap codec_cap = BT_AUDIO_CODEC_CAP_LC3( BT_AUDIO_CODEC_CAP_FREQ_ANY, BT_AUDIO_CODEC_CAP_DURATION_ANY, diff --git a/tests/bsim/bluetooth/audio/src/cap_handover_central_test.c b/tests/bsim/bluetooth/audio/src/cap_handover_central_test.c new file mode 100644 index 000000000000..3d1a175dea1a --- /dev/null +++ b/tests/bsim/bluetooth/audio/src/cap_handover_central_test.c @@ -0,0 +1,784 @@ +/* + * 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 "bap_common.h" +#include "bap_stream_tx.h" +#include "bap_stream_rx.h" +#include "bstests.h" +#include "common.h" + +LOG_MODULE_REGISTER(cap_handover_central, LOG_LEVEL_DBG); + +#if defined(CONFIG_BT_CAP_HANDOVER_SUPPORTED) + +#define CONTEXT (BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED) +#define LOCATION (BT_AUDIO_LOCATION_FRONT_LEFT | BT_AUDIO_LOCATION_FRONT_RIGHT) + +extern enum bst_result_t bst_result; + +static struct bt_bap_lc3_preset unicast_preset_16_2_1 = BT_BAP_LC3_UNICAST_PRESET_16_2_1( + BT_AUDIO_LOCATION_FRONT_LEFT, 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 cap_acceptor { + struct audio_test_stream sink_stream; + struct audio_test_stream source_stream; + struct bt_bap_ep *unicast_sink_ep; + struct bt_bap_ep *unicast_source_ep; + struct bt_conn *conn; +} cap_acceptors[CONFIG_BT_MAX_CONN]; + +static size_t connected_conn_cnt; +static struct bt_cap_broadcast_source *broadcast_source; +static bt_addr_le_t remote_dev_addr; + +CREATE_FLAG(flag_dev_found); +CREATE_FLAG(flag_discovered); +CREATE_FLAG(flag_codec_found); +CREATE_FLAG(flag_endpoint_found); +CREATE_FLAG(flag_started); +CREATE_FLAG(flag_stopped); +CREATE_FLAG(flag_handover_unicast_to_broadcast); +CREATE_FLAG(flag_mtu_exchanged); +CREATE_FLAG(flag_sink_discovered); +CREATE_FLAG(flag_source_discovered); +CREATE_FLAG(flag_broadcast_started); +CREATE_FLAG(flag_broadcast_stopped); + +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) { + FAIL("Failed to discover CAS: %d", err); + + return; + } + + if (IS_ENABLED(CONFIG_BT_CAP_ACCEPTOR_SET_MEMBER)) { + if (csis_inst == NULL) { + FAIL("Failed to discover CAS CSIS"); + + return; + } + + LOG_DBG("Found CAS with CSIS %p", csis_inst); + } else { + LOG_DBG("Found CAS"); + } + + SET_FLAG(flag_discovered); +} + +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); + } + + SET_FLAG(flag_discovered); +} + +static void unicast_start_complete_cb(int err, struct bt_conn *conn) +{ + if (err != 0) { + LOG_DBG("Failed to start (failing conn %p): %d", conn, err); + } else { + SET_FLAG(flag_started); + } +} + +static void unicast_stop_complete_cb(int err, struct bt_conn *conn) +{ + if (err != 0) { + FAIL("Failed to stop (failing conn %p): %d", conn, err); + + return; + } + + SET_FLAG(flag_stopped); +} + +static void unicast_to_broadcast_complete_cb(int err, struct bt_conn *conn, + struct bt_cap_unicast_group *unicast_group, + struct bt_cap_broadcast_source *source) +{ + if (err != 0) { + FAIL("Failed to handover unicast to broadcast (failing conn %p): %d", conn, err); + + return; + } + + broadcast_source = source; + SET_FLAG(flag_handover_unicast_to_broadcast); +} + +static void add_remote_sink(const struct bt_conn *conn, struct bt_bap_ep *ep) +{ + const uint8_t conn_index = bt_conn_index(conn); + + if (cap_acceptors[conn_index].unicast_sink_ep == NULL) { + LOG_DBG("Acceptor[%u] %p: Sink ep %p", conn_index, conn, ep); + cap_acceptors[conn_index].unicast_sink_ep = ep; + } else { + LOG_DBG("Could not add sink ep %p", ep); + } +} + +static void add_remote_source(const struct bt_conn *conn, struct bt_bap_ep *ep) +{ + const uint8_t conn_index = bt_conn_index(conn); + + if (cap_acceptors[conn_index].unicast_source_ep == NULL) { + LOG_DBG("Acceptor[%u] %p: Source ep %p", conn_index, conn, ep); + cap_acceptors[conn_index].unicast_source_ep = ep; + } else { + LOG_DBG("Could not add Source ep %p", ep); + } +} + +static void print_remote_codec(const struct bt_audio_codec_cap *codec_cap, enum bt_audio_dir dir) +{ + LOG_DBG("codec_cap %p dir 0x%02x", codec_cap, dir); + + print_codec_cap(codec_cap); +} + +static void pac_record_cb(struct bt_conn *conn, enum bt_audio_dir dir, + const struct bt_audio_codec_cap *codec_cap) +{ + print_remote_codec(codec_cap, dir); + SET_FLAG(flag_codec_found); +} + +static void discover_cb(struct bt_conn *conn, int err, enum bt_audio_dir dir) +{ + if (err != 0) { + FAIL("Discovery failed: %d\n", err); + return; + } + + if (dir == BT_AUDIO_DIR_SINK) { + LOG_DBG("Sink discover complete"); + + SET_FLAG(flag_sink_discovered); + } else if (dir == BT_AUDIO_DIR_SOURCE) { + LOG_DBG("Source discover complete"); + + SET_FLAG(flag_source_discovered); + } else { + FAIL("Invalid dir: %u\n", dir); + } +} + +static void endpoint_cb(struct bt_conn *conn, enum bt_audio_dir dir, struct bt_bap_ep *ep) +{ + if (dir == BT_AUDIO_DIR_SINK) { + add_remote_sink(conn, ep); + SET_FLAG(flag_endpoint_found); + } else if (dir == BT_AUDIO_DIR_SOURCE) { + add_remote_source(conn, ep); + SET_FLAG(flag_endpoint_found); + } else { + FAIL("Invalid param dir: %u\n", dir); + } +} + +static void att_mtu_updated(struct bt_conn *conn, uint16_t tx, uint16_t rx) +{ + LOG_DBG("MTU exchanged"); + SET_FLAG(flag_mtu_exchanged); +} + +static struct bt_gatt_cb gatt_callbacks = { + .att_mtu_updated = att_mtu_updated, +}; + +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; + + LOG_DBG("data->type %u", data->type); + + if (data->type != BT_DATA_SVC_DATA16) { + return true; /* Continue parsing to next AD data type */ + } + + if (data->data_len < sizeof(uuid_val)) { + 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_DBG("Device found: %s", addr_str); + + bt_addr_le_copy(&remote_dev_addr, addr); + SET_FLAG(flag_dev_found); + + return false; /* Stop parsing */ +} + +static void scan_recv_cb(const struct bt_le_scan_recv_info *info, struct net_buf_simple *buf) +{ + struct bt_conn *conn; + + conn = bt_conn_lookup_addr_le(BT_ID_DEFAULT, info->addr); + if (conn != NULL) { + /* Already connected to this device */ + bt_conn_unref(conn); + 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) { + bt_addr_le_t addr; + + bt_addr_le_copy(&addr, info->addr); + + /* Check for CAS support in advertising data */ + bt_data_parse(buf, check_audio_support_and_connect_cb, (void *)&addr); + } +} + +static void stream_started_cb(struct bt_bap_stream *stream) +{ + struct audio_test_stream *test_stream = audio_test_stream_from_bap_stream(stream); + + memset(&test_stream->last_info, 0, sizeof(test_stream->last_info)); + test_stream->rx_cnt = 0U; + test_stream->valid_rx_cnt = 0U; + test_stream->seq_num = 0U; + test_stream->tx_cnt = 0U; + + LOG_DBG("Started stream %p", stream); + + if (bap_stream_tx_can_send(stream)) { + int err; + + err = bap_stream_tx_register(stream); + if (err != 0) { + FAIL("Failed to register stream %p for TX: %d\n", stream, err); + return; + } + } +} + +static void stream_stopped_cb(struct bt_bap_stream *stream, uint8_t reason) +{ + LOG_DBG("Stopped stream %p with reason 0x%02X", stream, reason); + + if (bap_stream_tx_can_send(stream)) { + int err; + + err = bap_stream_tx_unregister(stream); + if (err != 0) { + FAIL("Failed to unregister stream %p for TX: %d\n", stream, err); + return; + } + } +} + +static void broadcast_source_started_cb(struct bt_bap_broadcast_source *source) +{ + SET_FLAG(flag_broadcast_started); +} + +static void broadcast_source_stopped_cb(struct bt_bap_broadcast_source *source, uint8_t reason) +{ + SET_FLAG(flag_broadcast_stopped); +} + +static void init(void) +{ + static struct bt_le_scan_cb scan_callbacks = { + .recv = scan_recv_cb, + }; + static struct bt_bap_broadcast_assistant_cb ba_cbs = { + .discover = bap_broadcast_assistant_discover_cb, + }; + static struct bt_cap_initiator_cb cap_cb = { + .unicast_discovery_complete = cap_discovery_complete_cb, + .unicast_start_complete = unicast_start_complete_cb, + .unicast_stop_complete = unicast_stop_complete_cb, + .unicast_to_broadcast_complete = unicast_to_broadcast_complete_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_bap_stream_ops stream_ops = { + .started = stream_started_cb, + .stopped = stream_stopped_cb, + .sent = bap_stream_tx_sent_cb, + }; + static struct bt_bap_broadcast_source_cb broadcast_source_cbs = { + .started = broadcast_source_started_cb, + .stopped = broadcast_source_stopped_cb, + }; + + int err; + + err = bt_enable(NULL); + if (err != 0) { + FAIL("Bluetooth enable failed (err %d)\n", err); + return; + } + + LOG_DBG("Bluetooth initialized"); + bap_stream_tx_init(); + + bt_gatt_cb_register(&gatt_callbacks); + err = bt_le_scan_cb_register(&scan_callbacks); + if (err != 0) { + FAIL("Failed to register scan callbacks (err %d)\n", err); + return; + } + + err = bt_bap_unicast_client_register_cb(&unicast_client_cbs); + if (err != 0) { + FAIL("Failed to register BAP unicast client callbacks (err %d)\n", err); + return; + } + + err = bt_cap_initiator_register_cb(&cap_cb); + if (err != 0) { + FAIL("Failed to register CAP callbacks (err %d)\n", err); + return; + } + + err = bt_bap_broadcast_assistant_register_cb(&ba_cbs); + if (err != 0) { + FAIL("Failed to register broadcast assistant callbacks (err %d)\n"); + return; + } + + err = bt_bap_broadcast_source_register_cb(&broadcast_source_cbs); + if (err != 0) { + FAIL("Failed to register broadcast assistant callbacks (err %d)\n"); + return; + } + + ARRAY_FOR_EACH_PTR(cap_acceptors, acceptor) { + bt_cap_stream_ops_register( + cap_stream_from_audio_test_stream(&acceptor->sink_stream), &stream_ops); + bt_cap_stream_ops_register( + cap_stream_from_audio_test_stream(&acceptor->source_stream), &stream_ops); + } +} + +static void scan_and_connect(struct bt_conn **conn) +{ + int err; + + UNSET_FLAG(flag_dev_found); + UNSET_FLAG(flag_connected); + + err = bt_le_scan_start(BT_LE_SCAN_PASSIVE, NULL); + if (err != 0) { + FAIL("Scanning failed to start (err %d)\n", err); + return; + } + + WAIT_FOR_FLAG(flag_dev_found); + LOG_DBG("Stopping scan"); + if (bt_le_scan_stop() != 0) { + FAIL("Could not stop scan"); + return; + } + + err = bt_conn_le_create(&remote_dev_addr, BT_CONN_LE_CREATE_CONN, BT_BAP_CONN_PARAM_RELAXED, + conn); + if (err != 0) { + FAIL("Could not connect to peer: %d", err); + return; + } + + LOG_DBG("Scanning successfully started"); + WAIT_FOR_FLAG(flag_connected); + connected_conn_cnt++; +} + +static void discover_sink(struct bt_conn *conn) +{ + const uint8_t conn_index = bt_conn_index(conn); + int err; + + UNSET_FLAG(flag_sink_discovered); + UNSET_FLAG(flag_codec_found); + UNSET_FLAG(flag_endpoint_found); + cap_acceptors[conn_index].unicast_sink_ep = NULL; + + err = bt_bap_unicast_client_discover(conn, BT_AUDIO_DIR_SINK); + if (err != 0) { + LOG_DBG("Failed to discover sink: %d", err); + return; + } + + WAIT_FOR_FLAG(flag_sink_discovered); + WAIT_FOR_FLAG(flag_endpoint_found); + WAIT_FOR_FLAG(flag_codec_found); +} + +static void discover_source(struct bt_conn *conn) +{ + const uint8_t conn_index = bt_conn_index(conn); + int err; + + UNSET_FLAG(flag_source_discovered); + UNSET_FLAG(flag_codec_found); + UNSET_FLAG(flag_endpoint_found); + cap_acceptors[conn_index].unicast_source_ep = NULL; + + err = bt_bap_unicast_client_discover(conn, BT_AUDIO_DIR_SOURCE); + if (err != 0) { + LOG_DBG("Failed to discover sink: %d", err); + return; + } + + WAIT_FOR_FLAG(flag_source_discovered); + WAIT_FOR_FLAG(flag_endpoint_found); + WAIT_FOR_FLAG(flag_codec_found); +} + +static void discover_cas(struct bt_conn *conn) +{ + int err; + + UNSET_FLAG(flag_discovered); + + err = bt_cap_initiator_unicast_discover(conn); + if (err != 0) { + LOG_DBG("Failed to discover CAS: %d", err); + return; + } + + WAIT_FOR_FLAG(flag_discovered); +} + +static void discover_bass(struct bt_conn *conn) +{ + int err; + + UNSET_FLAG(flag_discovered); + + err = bt_bap_broadcast_assistant_discover(conn); + if (err != 0) { + FAIL("Failed to discover BASS on the sink (err %d)\n", err); + return; + } + + WAIT_FOR_FLAG(flag_discovered); +} + +static void unicast_group_create(struct bt_cap_unicast_group **out_unicast_group) +{ + struct bt_cap_unicast_group_stream_param group_source_stream_params[CONFIG_BT_MAX_CONN]; + struct bt_cap_unicast_group_stream_param group_sink_stream_params[CONFIG_BT_MAX_CONN]; + struct bt_cap_unicast_group_stream_pair_param pair_params[CONFIG_BT_MAX_CONN]; + struct bt_cap_unicast_group_param group_param; + int err; + + for (size_t i = 0U; i < connected_conn_cnt; i++) { + group_sink_stream_params[i].qos_cfg = &unicast_preset_16_2_1.qos; + group_sink_stream_params[i].stream = + cap_stream_from_audio_test_stream(&cap_acceptors[i].sink_stream); + group_source_stream_params[i].qos_cfg = &unicast_preset_16_2_1.qos; + group_source_stream_params[i].stream = + cap_stream_from_audio_test_stream(&cap_acceptors[i].source_stream); + pair_params[i].tx_param = &group_sink_stream_params[i]; + pair_params[i].rx_param = &group_source_stream_params[i]; + } + + group_param.packing = BT_ISO_PACKING_SEQUENTIAL; + group_param.params_count = connected_conn_cnt; + group_param.params = pair_params; + + err = bt_cap_unicast_group_create(&group_param, out_unicast_group); + if (err != 0) { + FAIL("Failed to create group: %d\n", err); + return; + } +} + +static void unicast_audio_start(struct bt_cap_unicast_group *unicast_group) +{ + struct bt_cap_unicast_audio_start_stream_param stream_param[2 * ARRAY_SIZE(cap_acceptors)]; + struct bt_cap_unicast_audio_start_param param; + size_t stream_param_cnt = 0U; + int err; + + for (size_t i = 0U; i < connected_conn_cnt; i++) { + /* Sink param */ + stream_param[stream_param_cnt].member.member = cap_acceptors[i].conn; + stream_param[stream_param_cnt].stream = + cap_stream_from_audio_test_stream(&cap_acceptors[i].sink_stream); + stream_param[stream_param_cnt].ep = cap_acceptors[i].unicast_sink_ep; + stream_param[stream_param_cnt].codec_cfg = &unicast_preset_16_2_1.codec_cfg; + stream_param_cnt++; + + /* source param */ + stream_param[stream_param_cnt].member.member = cap_acceptors[i].conn; + stream_param[stream_param_cnt].stream = + cap_stream_from_audio_test_stream(&cap_acceptors[i].source_stream); + stream_param[stream_param_cnt].ep = cap_acceptors[i].unicast_source_ep; + stream_param[stream_param_cnt].codec_cfg = &unicast_preset_16_2_1.codec_cfg; + stream_param_cnt++; + } + + param.type = BT_CAP_SET_TYPE_AD_HOC; + param.count = stream_param_cnt; + param.stream_params = stream_param; + + UNSET_FLAG(flag_started); + + err = bt_cap_initiator_unicast_audio_start(¶m); + if (err != 0) { + FAIL("Failed to start unicast audio: %d\n", err); + return; + } + + WAIT_FOR_FLAG(flag_started); + /* let other devices know we have started what we wanted */ + backchannel_sync_send_all(); +} + +static void handover_unicast_to_broadcast(struct bt_cap_unicast_group *unicast_group, + struct bt_le_ext_adv *ext_adv) +{ + static struct bt_cap_initiator_broadcast_stream_param + stream_params[ARRAY_SIZE(cap_acceptors)]; + static struct bt_cap_initiator_broadcast_subgroup_param subgroup_param = {0}; + /* Struct containing the converted unicast group configuration */ + static struct bt_cap_initiator_broadcast_create_param create_param = {0}; + struct bt_cap_unicast_to_broadcast_param param = {0}; + size_t stream_cnt = 0U; + int err; + + ARRAY_FOR_EACH(stream_params, i) { + const struct bt_bap_ep *ep = + bap_stream_from_audio_test_stream(&cap_acceptors[i].sink_stream)->ep; + struct bt_bap_ep_info ep_info; + + if (ep == NULL) { + /* Not configured */ + continue; + } + + err = bt_bap_ep_get_info(ep, &ep_info); + + if (err != 0) { + FAIL("Failed to get endpoint info: %d", err); + return; + } + + 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_from_audio_test_stream(&cap_acceptors[i].sink_stream); + stream_params[stream_cnt].data_len = 0U; + stream_params[stream_cnt].data = NULL; + + stream_cnt++; + } + + if (stream_cnt == 0U) { + FAIL("No streams can be handed over"); + return; + } + + subgroup_param.stream_count = stream_cnt; + subgroup_param.stream_params = stream_params; + subgroup_param.codec_cfg = &broadcast_preset_16_2_1.codec_cfg; + + create_param.subgroup_count = 1U; + create_param.subgroup_params = &subgroup_param; + create_param.qos = &broadcast_preset_16_2_1.qos; + create_param.packing = BT_ISO_PACKING_SEQUENTIAL; + create_param.encryption = false; + + param.type = BT_CAP_SET_TYPE_AD_HOC; + param.unicast_group = unicast_group; + param.broadcast_create_param = &create_param; + param.ext_adv = ext_adv; + param.sid = 0x00; + param.pa_interval = BT_BAP_PA_INTERVAL_UNKNOWN; + param.broadcast_id = 0x123456; + + UNSET_FLAG(flag_handover_unicast_to_broadcast); + + err = bt_cap_initiator_unicast_to_broadcast(¶m); + if (err != 0) { + FAIL("Failed to handover unicast audio to broadcast: %d\n", err); + return; + } + + WAIT_FOR_FLAG(flag_handover_unicast_to_broadcast); + LOG_DBG("Handover procedure completed"); +} + +static void set_base_data(struct bt_le_ext_adv *ext_adv) +{ + struct bt_data per_ad; + int err; + + NET_BUF_SIMPLE_DEFINE(base_buf, 128); + + err = bt_cap_initiator_broadcast_get_base(broadcast_source, &base_buf); + if (err != 0) { + FAIL("Failed to get encoded BASE: %d\n", err); + return; + } + + 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) { + FAIL("Failed to set periodic advertising data: %d\n", err); + return; + } +} + +static void stop_broadcast(void) +{ + int err; + + /* Verify that it cannot be stopped twice */ + err = bt_cap_initiator_broadcast_audio_stop(broadcast_source); + if (err != 0) { + FAIL("Failed to stop broadcast source: %d\n", err); + return; + } + + WAIT_FOR_FLAG(flag_broadcast_stopped); + + err = bt_cap_initiator_broadcast_audio_delete(broadcast_source); + if (err != 0) { + FAIL("Failed to delete broadcast source: %d\n", err); + return; + } + + broadcast_source = NULL; +} + +static void test_main_cap_handover_unicast_to_broadcast(void) +{ + const size_t acceptor_cnt = get_dev_cnt() - 1; /* Assume all other devices are acceptors */ + struct bt_cap_unicast_group *unicast_group; + struct bt_le_ext_adv *ext_adv; + + if (acceptor_cnt > CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT) { + FAIL("Cannot run test with %zu acceptors and maximum %d broadcast streams", + CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT); + } + + init(); + + /* Connect to and do discovery on all CAP acceptors */ + for (size_t i = 0U; i < acceptor_cnt; i++) { + scan_and_connect(&cap_acceptors[i].conn); + + WAIT_FOR_FLAG(flag_mtu_exchanged); + + discover_cas(cap_acceptors[i].conn); + discover_bass(cap_acceptors[i].conn); + + discover_sink(cap_acceptors[i].conn); + discover_source(cap_acceptors[i].conn); + } + + unicast_group_create(&unicast_group); + + unicast_audio_start(unicast_group); + + /* Wait for acceptors to receive some data */ + backchannel_sync_wait_all(); + + setup_broadcast_adv(&ext_adv); + + handover_unicast_to_broadcast(unicast_group, ext_adv); + set_base_data(ext_adv); + start_broadcast_adv(ext_adv); + + /* Wait for acceptors to receive some data */ + backchannel_sync_wait_all(); + + stop_broadcast(); + + PASS("CAP initiator handover unicast to broadcast passed\n"); +} + +static const struct bst_test_instance test_cap_handover[] = { + { + .test_id = "cap_handover_unicast_to_broadcast", + .test_pre_init_f = test_init, + .test_tick_f = test_tick, + .test_main_f = test_main_cap_handover_unicast_to_broadcast, + }, + BSTEST_END_MARKER, +}; + +struct bst_test_list *test_cap_handover_central_install(struct bst_test_list *tests) +{ + return bst_add_tests(tests, test_cap_handover); +} + +#else /* !CONFIG_BT_CAP_HANDOVER_SUPPORTED */ + +struct bst_test_list *test_cap_handover_central_install(struct bst_test_list *tests) +{ + return tests; +} + +#endif /* CONFIG_BT_CAP_HANDOVER_SUPPORTED */ diff --git a/tests/bsim/bluetooth/audio/src/cap_handover_peripheral_test.c b/tests/bsim/bluetooth/audio/src/cap_handover_peripheral_test.c new file mode 100644 index 000000000000..1f0ce09dcb66 --- /dev/null +++ b/tests/bsim/bluetooth/audio/src/cap_handover_peripheral_test.c @@ -0,0 +1,820 @@ +/* + * Copyright (c) 2022-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 "bap_stream_rx.h" +#include "bstests.h" +#include "common.h" +#include "bap_common.h" + +LOG_MODULE_REGISTER(cap_handover_peripheral, LOG_LEVEL_DBG); + +#if defined(CONFIG_BT_CAP_ACCEPTOR) +extern enum bst_result_t bst_result; + +#define CAP_INITIATOR_DEV_ID 0 /* CAP initiator shall be ID 0 for these tests */ + +CREATE_FLAG(flag_broadcast_code); +CREATE_FLAG(flag_base_received); +CREATE_FLAG(flag_pa_synced); +CREATE_FLAG(flag_syncable); +CREATE_FLAG(flag_pa_sync_lost); +CREATE_FLAG(flag_pa_request); +CREATE_FLAG(flag_bis_sync_requested); +CREATE_FLAG(flag_base_metadata_updated); +CREATE_FLAG(flag_stream_configured); +CREATE_FLAG(flag_stream_started); +CREATE_FLAG(flag_stream_stopped); +CREATE_FLAG(flag_broadcast_started); +CREATE_FLAG(flag_broadcast_stopped); + +static struct bt_bap_broadcast_sink *broadcast_sink; +static struct bt_le_per_adv_sync *pa_sync; +static const struct bt_bap_scan_delegator_recv_state *cached_recv_state; +static uint32_t cached_bis_sync_req; +static uint16_t cached_pa_interval; +static struct audio_test_stream + streams[MIN(CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT, + CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT + CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT)]; +static uint8_t received_base[UINT8_MAX]; +static size_t received_base_size; + +static const struct bt_bap_qos_cfg_pref unicast_qos_pref = + BT_BAP_QOS_CFG_PREF(true, BT_GAP_LE_PHY_2M, 0u, 60u, 20000u, 40000u, 20000u, 40000u); + +static bool subgroup_data_func_cb(struct bt_data *data, void *user_data) +{ + bool *stream_context_found = (bool *)user_data; + + LOG_DBG("type %u len %u", data->type, data->data_len); + + if (!valid_metadata_type(data->type, data->data_len)) { + return false; + } + + if (data->type == BT_AUDIO_METADATA_TYPE_STREAM_CONTEXT) { + if (data->data_len != 2) { /* Stream context size */ + return false; + } + + *stream_context_found = true; + return false; + } + + return true; +} + +static bool valid_subgroup_metadata_cb(const struct bt_bap_base_subgroup *subgroup, void *user_data) +{ + static uint8_t metadata[CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE]; + static size_t metadata_size; + bool stream_context_found = false; + uint8_t *meta; + int ret; + + ret = bt_bap_base_get_subgroup_codec_meta(subgroup, &meta); + if (ret < 0) { + FAIL("Could not get subgroup meta: %d\n", ret); + return false; + } + + if (TEST_FLAG(flag_base_received) && (!util_eq(meta, ret, metadata, metadata_size))) { + LOG_DBG("Metadata updated"); + SET_FLAG(flag_base_metadata_updated); + } + + metadata_size = (size_t)ret; + + ret = bt_audio_data_parse(meta, (size_t)ret, subgroup_data_func_cb, &stream_context_found); + if (ret != 0 && ret != -ECANCELED) { + return false; + } + + if (!stream_context_found) { + LOG_DBG("Subgroup did not have streaming context"); + } + + /* if this is false, the iterator will return early with an error */ + return stream_context_found; +} + +static void base_recv_cb(struct bt_bap_broadcast_sink *sink, const struct bt_bap_base *base, + size_t base_size) +{ + int ret; + + if (TEST_FLAG(flag_base_received)) { + /* Don't expect any BASE updates */ + return; + } + + ret = bt_bap_base_get_subgroup_count(base); + if (ret < 0) { + FAIL("Failed to get subgroup count: %d\n", ret); + return; + } else if (ret == 0) { + FAIL("subgroup_count was 0\n"); + return; + } + + LOG_DBG("Received BASE with %d subgroups from broadcast sink %p", ret, sink); + + ret = bt_bap_base_foreach_subgroup(base, valid_subgroup_metadata_cb, NULL); + if (ret != 0) { + FAIL("Failed to parse subgroups: %d\n", ret); + return; + } + + (void)memcpy(received_base, base, base_size); + received_base_size = base_size; + + SET_FLAG(flag_base_received); +} + +static void syncable_cb(struct bt_bap_broadcast_sink *sink, const struct bt_iso_biginfo *biginfo) +{ + LOG_DBG("Broadcast sink %p syncable with%s encryption", sink, + biginfo->encryption ? "" : "out"); + SET_FLAG(flag_syncable); +} + +static void broadcast_sink_started_cb(struct bt_bap_broadcast_sink *sink) +{ + SET_FLAG(flag_broadcast_started); +} + +static void broadcast_sink_stopped_cb(struct bt_bap_broadcast_sink *sink, uint8_t reason) +{ + SET_FLAG(flag_broadcast_stopped); +} + +static void bap_pa_sync_synced_cb(struct bt_le_per_adv_sync *sync, + struct bt_le_per_adv_sync_synced_info *info) +{ + if (sync == pa_sync) { + LOG_DBG("PA sync %p synced for broadcast sink with broadcast ID 0x%06X", sync, + cached_recv_state->broadcast_id); + + SET_FLAG(flag_pa_synced); + } else { + FAIL("Unexpected PA sync: %p\n"); + } +} + +static void bap_pa_sync_terminated_cb(struct bt_le_per_adv_sync *sync, + const struct bt_le_per_adv_sync_term_info *info) +{ + if (sync == pa_sync) { + LOG_DBG("PA sync %p lost with reason %u", sync, info->reason); + pa_sync = NULL; + + SET_FLAG(flag_pa_sync_lost); + } +} + +static void stream_enabled_cb(struct bt_bap_stream *stream) +{ + struct bt_bap_ep_info ep_info; + int err; + + LOG_DBG("Enabled: stream %p ", stream); + + err = bt_bap_ep_get_info(stream->ep, &ep_info); + if (err != 0) { + FAIL("Failed to get ep info: %d\n", err); + return; + } + + if (ep_info.dir == BT_AUDIO_DIR_SINK) { + /* Automatically do the receiver start ready operation */ + err = bt_bap_stream_start(stream); + + if (err != 0) { + FAIL("Failed to start stream: %d\n", err); + return; + } + } +} + +static void stream_started_cb(struct bt_bap_stream *stream) +{ + struct audio_test_stream *test_stream = audio_test_stream_from_bap_stream(stream); + + memset(&test_stream->last_info, 0, sizeof(test_stream->last_info)); + test_stream->rx_cnt = 0U; + test_stream->valid_rx_cnt = 0U; + test_stream->seq_num = 0U; + test_stream->tx_cnt = 0U; + + LOG_DBG("Started stream %p", stream); + + SET_FLAG(flag_stream_started); +} + +static void stream_stopped_cb(struct bt_bap_stream *stream, uint8_t reason) +{ + LOG_DBG("Stopped stream %p with reason 0x%02X", stream, reason); + + SET_FLAG(flag_stream_stopped); +} + +static int pa_sync_req_cb(struct bt_conn *conn, + const struct bt_bap_scan_delegator_recv_state *recv_state, + bool past_avail, uint16_t pa_interval) +{ + if (recv_state->pa_sync_state == BT_BAP_PA_STATE_SYNCED || + recv_state->pa_sync_state == BT_BAP_PA_STATE_INFO_REQ) { + /* Already syncing */ + /* TODO: Terminate existing sync and then sync to new?*/ + return -EALREADY; + } + + LOG_DBG("Sync request"); + + cached_pa_interval = pa_interval; + cached_recv_state = recv_state; + + SET_FLAG(flag_pa_request); + + return 0; +} + +static int pa_sync_term_req_cb(struct bt_conn *conn, + const struct bt_bap_scan_delegator_recv_state *recv_state) +{ + if (pa_sync == NULL || recv_state->pa_sync_state == BT_BAP_PA_STATE_NOT_SYNCED) { + return -EALREADY; + } + + UNSET_FLAG(flag_pa_request); + + return 0; +} + +static int bis_sync_req_cb(struct bt_conn *conn, + const struct bt_bap_scan_delegator_recv_state *recv_state, + const uint32_t bis_sync_req[CONFIG_BT_BAP_BASS_MAX_SUBGROUPS]) +{ + cached_bis_sync_req = 0U; + + for (uint8_t i = 0U; i < recv_state->num_subgroups; i++) { + cached_bis_sync_req |= bis_sync_req[i]; + } + + if (cached_bis_sync_req != 0U) { + SET_FLAG(flag_bis_sync_requested); + } else { + UNSET_FLAG(flag_bis_sync_requested); + } + + LOG_DBG("bis_sync_req 0x%08X", cached_bis_sync_req); + + cached_recv_state = recv_state; + + return 0; +} + +static void broadcast_code_cb(struct bt_conn *conn, + const struct bt_bap_scan_delegator_recv_state *recv_state, + const uint8_t broadcast_code[BT_ISO_BROADCAST_CODE_SIZE]) +{ + LOG_DBG("Broadcast code received for %p", recv_state); + + if (memcmp(broadcast_code, BROADCAST_CODE, sizeof(BROADCAST_CODE)) != 0) { + FAIL("Failed to receive correct broadcast code\n"); + return; + } + + SET_FLAG(flag_broadcast_code); +} + +static struct bt_bap_stream *stream_alloc(void) +{ + for (size_t i = 0; i < ARRAY_SIZE(streams); i++) { + struct bt_bap_stream *stream = bap_stream_from_audio_test_stream(&streams[i]); + + if (!stream->conn) { + return stream; + } + } + + return NULL; +} + +static int unicast_server_config(struct bt_conn *conn, const struct bt_bap_ep *ep, + enum bt_audio_dir dir, const struct bt_audio_codec_cfg *codec_cfg, + struct bt_bap_stream **stream, + struct bt_bap_qos_cfg_pref *const pref, + struct bt_bap_ascs_rsp *rsp) +{ + LOG_DBG("ASE Codec Config: conn %p ep %p dir %u", conn, ep, dir); + + print_codec_cfg(codec_cfg); + + *stream = stream_alloc(); + if (*stream == NULL) { + LOG_DBG("No streams available"); + *rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_NO_MEM, BT_BAP_ASCS_REASON_NONE); + + return -ENOMEM; + } + + LOG_DBG("ASE Codec Config stream %p", *stream); + + SET_FLAG(flag_stream_configured); + + *pref = unicast_qos_pref; + + return 0; +} + +static int unicast_server_reconfig(struct bt_bap_stream *stream, enum bt_audio_dir dir, + const struct bt_audio_codec_cfg *codec_cfg, + struct bt_bap_qos_cfg_pref *const pref, + struct bt_bap_ascs_rsp *rsp) +{ + LOG_DBG("ASE Codec Reconfig: stream %p", stream); + + print_codec_cfg(codec_cfg); + + *pref = unicast_qos_pref; + + *rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_CONF_UNSUPPORTED, BT_BAP_ASCS_REASON_NONE); + + /* We only support one QoS at the moment, reject changes */ + return -ENOEXEC; +} + +static int unicast_server_qos(struct bt_bap_stream *stream, const struct bt_bap_qos_cfg *qos, + struct bt_bap_ascs_rsp *rsp) +{ + LOG_DBG("QoS: stream %p qos %p", stream, qos); + + print_qos(qos); + + return 0; +} + +static bool ascs_data_func_cb(struct bt_data *data, void *user_data) +{ + struct bt_bap_ascs_rsp *rsp = (struct bt_bap_ascs_rsp *)user_data; + + if (!BT_AUDIO_METADATA_TYPE_IS_KNOWN(data->type)) { + LOG_DBG("Invalid metadata type %u or length %u", data->type, data->data_len); + *rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_METADATA_REJECTED, data->type); + return false; + } + + return true; +} + +static int unicast_server_enable(struct bt_bap_stream *stream, const uint8_t meta[], + size_t meta_len, struct bt_bap_ascs_rsp *rsp) +{ + LOG_DBG("Enable: stream %p meta_len %zu", stream, meta_len); + + return bt_audio_data_parse(meta, meta_len, ascs_data_func_cb, rsp); +} + +static int unicast_server_start(struct bt_bap_stream *stream, struct bt_bap_ascs_rsp *rsp) +{ + LOG_DBG("Start: stream %p", stream); + + return 0; +} + +static int unicast_server_metadata(struct bt_bap_stream *stream, const uint8_t meta[], + size_t meta_len, struct bt_bap_ascs_rsp *rsp) +{ + LOG_DBG("Metadata: stream %p meta_len %zu", stream, meta_len); + + return bt_audio_data_parse(meta, meta_len, ascs_data_func_cb, rsp); +} + +static int unicast_server_disable(struct bt_bap_stream *stream, struct bt_bap_ascs_rsp *rsp) +{ + LOG_DBG("Disable: stream %p", stream); + + return 0; +} + +static int unicast_server_stop(struct bt_bap_stream *stream, struct bt_bap_ascs_rsp *rsp) +{ + LOG_DBG("Stop: stream %p", stream); + + return 0; +} + +static int unicast_server_release(struct bt_bap_stream *stream, struct bt_bap_ascs_rsp *rsp) +{ + LOG_DBG("Release: stream %p", stream); + + return 0; +} + +static void set_location(void) +{ + int err; + + if (IS_ENABLED(CONFIG_BT_PAC_SNK_LOC)) { + err = bt_pacs_set_location(BT_AUDIO_DIR_SINK, BT_AUDIO_LOCATION_FRONT_CENTER); + if (err != 0) { + FAIL("Failed to set sink location (err %d)\n", err); + return; + } + } + + if (IS_ENABLED(CONFIG_BT_PAC_SRC_LOC)) { + err = bt_pacs_set_location(BT_AUDIO_DIR_SOURCE, + BT_AUDIO_LOCATION_FRONT_LEFT | + BT_AUDIO_LOCATION_FRONT_RIGHT); + if (err != 0) { + FAIL("Failed to set source location (err %d)\n", err); + return; + } + } + + LOG_DBG("Location successfully set"); +} + +static void set_supported_contexts(void) +{ + int err; + + if (IS_ENABLED(CONFIG_BT_PAC_SNK)) { + err = bt_pacs_set_supported_contexts(BT_AUDIO_DIR_SINK, SINK_CONTEXT); + if (err != 0) { + FAIL("Failed to set sink supported contexts (err %d)\n", err); + return; + } + + LOG_DBG("Supported sink contexts successfully set to 0x%04X", SINK_CONTEXT); + } + + if (IS_ENABLED(CONFIG_BT_PAC_SRC)) { + err = bt_pacs_set_supported_contexts(BT_AUDIO_DIR_SOURCE, SOURCE_CONTEXT); + if (err != 0) { + FAIL("Failed to set source supported contexts (err %d)\n", err); + return; + } + + LOG_DBG("Supported source contexts successfully set to 0x%04X", SOURCE_CONTEXT); + } +} + +static void set_available_contexts(void) +{ + if (IS_ENABLED(CONFIG_BT_PAC_SNK)) { + const int err = bt_pacs_set_available_contexts(BT_AUDIO_DIR_SINK, SINK_CONTEXT); + + if (err != 0) { + FAIL("Failed to set sink available contexts (err %d)\n", err); + return; + } + + LOG_DBG("Available sink contexts successfully set to 0x%04X", SINK_CONTEXT); + } + + if (IS_ENABLED(CONFIG_BT_PAC_SRC)) { + const int err = bt_pacs_set_available_contexts(BT_AUDIO_DIR_SOURCE, SOURCE_CONTEXT); + + if (err != 0) { + FAIL("Failed to set source available contexts (err %d)\n", err); + return; + } + + LOG_DBG("Available source contexts successfully set to 0x%04X", SOURCE_CONTEXT); + } +} + +static void test_start_adv(void) +{ + struct bt_le_ext_adv *ext_adv; + + setup_connectable_adv(&ext_adv); +} + +static void register_callbacks(void) +{ + static struct bt_bap_broadcast_sink_cb broadcast_sink_cbs = { + .base_recv = base_recv_cb, + .syncable = syncable_cb, + .started = broadcast_sink_started_cb, + .stopped = broadcast_sink_stopped_cb, + }; + static struct bt_bap_scan_delegator_cb scan_delegator_cbs = { + .pa_sync_req = pa_sync_req_cb, + .pa_sync_term_req = pa_sync_term_req_cb, + .bis_sync_req = bis_sync_req_cb, + .broadcast_code = broadcast_code_cb, + }; + static struct bt_bap_unicast_server_cb unicast_server_cbs = { + .config = unicast_server_config, + .reconfig = unicast_server_reconfig, + .qos = unicast_server_qos, + .enable = unicast_server_enable, + .start = unicast_server_start, + .metadata = unicast_server_metadata, + .disable = unicast_server_disable, + .stop = unicast_server_stop, + .release = unicast_server_release, + }; + static struct bt_le_per_adv_sync_cb bap_pa_sync_cb = { + .synced = bap_pa_sync_synced_cb, + .term = bap_pa_sync_terminated_cb, + }; + static struct bt_bap_stream_ops stream_ops = { + .enabled = stream_enabled_cb, + .started = stream_started_cb, + .stopped = stream_stopped_cb, + .recv = bap_stream_rx_recv_cb, + }; + int err; + + err = bt_bap_unicast_server_register_cb(&unicast_server_cbs); + if (err != 0) { + FAIL("Failed to register unicast server callbacks (err %d)\n", err); + + return; + } + + err = bt_bap_scan_delegator_register(&scan_delegator_cbs); + if (err != 0) { + FAIL("Scan deligator register failed (err %d)\n", err); + + return; + } + + err = bt_bap_broadcast_sink_register_cb(&broadcast_sink_cbs); + if (err != 0) { + FAIL("Scan deligator register failed (err %d)\n", err); + + return; + } + + err = bt_le_per_adv_sync_cb_register(&bap_pa_sync_cb); + if (err != 0) { + FAIL("Scan deligator register failed (err %d)\n", err); + + return; + } + + ARRAY_FOR_EACH_PTR(streams, stream) { + bt_cap_stream_ops_register(cap_stream_from_audio_test_stream(stream), &stream_ops); + } +} + +static void init(void) +{ + static const struct bt_audio_codec_cap codec_cap = BT_AUDIO_CODEC_CAP_LC3( + BT_AUDIO_CODEC_CAP_FREQ_ANY, BT_AUDIO_CODEC_CAP_DURATION_ANY, + BT_AUDIO_CODEC_CAP_CHAN_COUNT_SUPPORT(1, 2), 30, 240, 2, + (BT_AUDIO_CONTEXT_TYPE_CONVERSATIONAL | BT_AUDIO_CONTEXT_TYPE_MEDIA)); + const struct bt_pacs_register_param pacs_param = { +#if defined(CONFIG_BT_PAC_SNK) + .snk_pac = true, +#endif /* CONFIG_BT_PAC_SNK */ +#if defined(CONFIG_BT_PAC_SNK_LOC) + .snk_loc = true, +#endif /* CONFIG_BT_PAC_SNK_LOC */ +#if defined(CONFIG_BT_PAC_SRC) + .src_pac = true, +#endif /* CONFIG_BT_PAC_SRC */ +#if defined(CONFIG_BT_PAC_SRC_LOC) + .src_loc = true, +#endif /* CONFIG_BT_PAC_SRC_LOC */ + }; + const struct bt_csip_set_member_register_param csip_set_member_param = { + .set_size = 3, + .rank = 1, + .lockable = true, + .sirk = TEST_SAMPLE_SIRK, + }; + static struct bt_bap_unicast_server_register_param param = { + .snk_cnt = CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT, + .src_cnt = CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT, + }; + static struct bt_csip_set_member_svc_inst *csip_set_member; + static struct bt_pacs_cap pacs_cap = { + .codec_cap = &codec_cap, + }; + + int err; + + err = bt_enable(NULL); + if (err != 0) { + FAIL("Bluetooth enable failed (err %d)\n", err); + return; + } + + LOG_DBG("Bluetooth initialized"); + + err = bt_pacs_register(&pacs_param); + if (err) { + FAIL("Could not register PACS (err %d)\n", err); + return; + } + + err = bt_cap_acceptor_register(&csip_set_member_param, &csip_set_member); + if (err != 0) { + FAIL("CAP acceptor failed to register (err %d)\n", err); + return; + } + + err = bt_pacs_cap_register(BT_AUDIO_DIR_SINK, &pacs_cap); + if (err != 0) { + FAIL("Broadcast capability register failed (err %d)\n", err); + + return; + } + + err = bt_pacs_cap_register(BT_AUDIO_DIR_SOURCE, &pacs_cap); + if (err != 0) { + FAIL("Broadcast capability register failed (err %d)\n", err); + + return; + } + + err = bt_bap_unicast_server_register(¶m); + if (err != 0) { + FAIL("Failed to register unicast server (err %d)\n", err); + + return; + } + + set_supported_contexts(); + set_available_contexts(); + set_location(); + + register_callbacks(); +} + +static void pa_sync_create(void) +{ + struct bt_le_per_adv_sync_param create_params = {0}; + int err; + + bt_addr_le_copy(&create_params.addr, &cached_recv_state->addr); + create_params.options = BT_LE_PER_ADV_SYNC_OPT_NONE; + create_params.sid = cached_recv_state->adv_sid; + create_params.skip = PA_SYNC_SKIP; + create_params.timeout = interval_to_sync_timeout(cached_pa_interval); + + err = bt_le_per_adv_sync_create(&create_params, &pa_sync); + if (err != 0) { + FAIL("Could not create Broadcast PA sync: %d\n", err); + return; + } + + LOG_DBG("Waiting for PA sync"); + WAIT_FOR_FLAG(flag_pa_synced); +} + +static void create_and_sync_sink(void) +{ + struct bt_bap_stream *broadcast_streams[ARRAY_SIZE(streams)]; + int err; + + LOG_DBG("Creating the broadcast sink"); + err = bt_bap_broadcast_sink_create(pa_sync, cached_recv_state->broadcast_id, + &broadcast_sink); + if (err != 0) { + FAIL("Unable to create the sink: %d\n", err); + return; + } + + LOG_DBG("Broadcast source PA synced, waiting for BASE"); + WAIT_FOR_FLAG(flag_base_received); + LOG_DBG("BASE received"); + + LOG_DBG("Waiting for BIG syncable"); + WAIT_FOR_FLAG(flag_syncable); + + for (size_t i = 0U; i < ARRAY_SIZE(streams); i++) { + broadcast_streams[i] = bap_stream_from_audio_test_stream(&streams[i]); + } + + if (cached_bis_sync_req == 0U) { + FAIL("Invalid cached_bis_sync_req: %u", cached_bis_sync_req); + return; + } + + LOG_DBG("Syncing the sink to 0x%08x", cached_bis_sync_req); + + err = bt_bap_broadcast_sink_sync(broadcast_sink, cached_bis_sync_req, broadcast_streams, + NULL); + if (err != 0) { + FAIL("Unable to sync the sink: %d\n", err); + return; + } + + /* Wait for all to be started */ + LOG_DBG("Waiting for broadcast streams to be started"); + WAIT_FOR_FLAG(flag_broadcast_started); +} + +static void wait_for_data(void) +{ + UNSET_FLAG(flag_audio_received); + + LOG_DBG("Waiting for data"); + WAIT_FOR_FLAG(flag_audio_received); + LOG_DBG("Data received"); +} + +static void test_cap_handover_peripheral_unicast_to_broadcast(void) +{ + init(); + + test_start_adv(); + + WAIT_FOR_FLAG(flag_connected); + + /* Wait until initiator is done starting streams */ + WAIT_FOR_FLAG(flag_stream_started); + backchannel_sync_wait(CAP_INITIATOR_DEV_ID); + + wait_for_data(); + + /* let initiator know we have received what we wanted */ + backchannel_sync_send(CAP_INITIATOR_DEV_ID); + + /* Wait for unicast to be stopped */ + WAIT_FOR_FLAG(flag_stream_stopped); + + /* Wait for a PA sync request to switch from unicast to broadcast */ + LOG_DBG("Waiting for PA sync request"); + WAIT_FOR_FLAG(flag_pa_request); + pa_sync_create(); + + /* Wait for a BIG sync request to sync to broadcast */ + WAIT_FOR_FLAG(flag_bis_sync_requested); + create_and_sync_sink(); + + wait_for_data(); + + /* let initiator know we have received what we wanted */ + backchannel_sync_send(CAP_INITIATOR_DEV_ID); + + /* Wait for broadcast to be stopped */ + WAIT_FOR_FLAG(flag_broadcast_stopped); + + PASS("CAP acceptor unicast passed\n"); +} + +static const struct bst_test_instance test_cap_handover_peripheral[] = { + { + .test_id = "cap_handover_peripheral_unicast_to_broadcast", + .test_pre_init_f = test_init, + .test_tick_f = test_tick, + .test_main_f = test_cap_handover_peripheral_unicast_to_broadcast, + }, + BSTEST_END_MARKER, +}; + +struct bst_test_list *test_cap_handover_peripheral_install(struct bst_test_list *tests) +{ + return bst_add_tests(tests, test_cap_handover_peripheral); +} + +#else /* !(CONFIG_BT_CAP_ACCEPTOR) */ + +struct bst_test_list *test_cap_handover_peripheral_install(struct bst_test_list *tests) +{ + return tests; +} + +#endif /* CONFIG_BT_CAP_ACCEPTOR */ diff --git a/tests/bsim/bluetooth/audio/src/cap_initiator_broadcast_test.c b/tests/bsim/bluetooth/audio/src/cap_initiator_broadcast_test.c index 7b149f9dae50..ef7663f7fbd7 100644 --- a/tests/bsim/bluetooth/audio/src/cap_initiator_broadcast_test.c +++ b/tests/bsim/bluetooth/audio/src/cap_initiator_broadcast_test.c @@ -248,25 +248,6 @@ static void setup_extended_adv_data(struct bt_cap_broadcast_source *source, } } -static void start_extended_adv(struct bt_le_ext_adv *adv) -{ - int err; - - /* Start extended advertising */ - err = bt_le_ext_adv_start(adv, BT_LE_EXT_ADV_START_DEFAULT); - if (err) { - FAIL("Failed to start extended advertising: %d\n", err); - return; - } - - /* Enable Periodic Advertising */ - err = bt_le_per_adv_start(adv); - if (err) { - FAIL("Failed to enable periodic advertising: %d\n", err); - return; - } -} - static void stop_and_delete_extended_adv(struct bt_le_ext_adv *adv) { int err; @@ -662,7 +643,7 @@ static void test_main_cap_initiator_broadcast(void) setup_extended_adv_data(broadcast_source, adv); - start_extended_adv(adv); + start_broadcast_adv(adv); /* Wait for all to be started */ printk("Waiting for broadcast_streams to be started\n"); @@ -708,7 +689,7 @@ static void test_main_cap_initiator_broadcast_inval(void) setup_extended_adv_data(broadcast_source, adv); - start_extended_adv(adv); + start_broadcast_adv(adv); /* Wait for all to be started */ printk("Waiting for broadcast_streams to be started\n"); @@ -754,7 +735,7 @@ static void test_main_cap_initiator_broadcast_update(void) setup_extended_adv_data(broadcast_source, adv); - start_extended_adv(adv); + start_broadcast_adv(adv); /* Wait for all to be started */ printk("Waiting for broadcast_streams to be started\n"); @@ -848,7 +829,7 @@ static int test_cap_initiator_ac(const struct cap_initiator_ac_param *param) test_broadcast_audio_start(broadcast_source, adv); setup_extended_adv_data(broadcast_source, adv); - start_extended_adv(adv); + start_broadcast_adv(adv); /* Wait for all to be started */ printk("Waiting for broadcast_streams to be started\n"); diff --git a/tests/bsim/bluetooth/audio/src/cap_initiator_unicast_test.c b/tests/bsim/bluetooth/audio/src/cap_initiator_unicast_test.c index d3e415ac3967..5a073f2b8257 100644 --- a/tests/bsim/bluetooth/audio/src/cap_initiator_unicast_test.c +++ b/tests/bsim/bluetooth/audio/src/cap_initiator_unicast_test.c @@ -616,20 +616,20 @@ static void discover_cas(struct bt_conn *conn) WAIT_FOR_FLAG(flag_discovered); } -static void unicast_group_create(struct bt_bap_unicast_group **out_unicast_group) +static void unicast_group_create(struct bt_cap_unicast_group **out_unicast_group) { - struct bt_bap_unicast_group_stream_param group_source_stream_params; - struct bt_bap_unicast_group_stream_param group_sink_stream_params; - struct bt_bap_unicast_group_stream_pair_param pair_params; - struct bt_bap_unicast_group_param group_param; + struct bt_cap_unicast_group_stream_param group_source_stream_params; + struct bt_cap_unicast_group_stream_param group_sink_stream_params; + struct bt_cap_unicast_group_stream_pair_param pair_params; + struct bt_cap_unicast_group_param group_param; int err; - group_sink_stream_params.qos = &unicast_preset_16_2_1.qos; + group_sink_stream_params.qos_cfg = &unicast_preset_16_2_1.qos; group_sink_stream_params.stream = - bap_stream_from_audio_test_stream(&unicast_client_sink_streams[0]); - group_source_stream_params.qos = &unicast_preset_16_2_1.qos; + cap_stream_from_audio_test_stream(&unicast_client_sink_streams[0]); + group_source_stream_params.qos_cfg = &unicast_preset_16_2_1.qos; group_source_stream_params.stream = - bap_stream_from_audio_test_stream(&unicast_client_source_streams[0]); + cap_stream_from_audio_test_stream(&unicast_client_source_streams[0]); pair_params.tx_param = &group_sink_stream_params; pair_params.rx_param = &group_source_stream_params; @@ -637,14 +637,14 @@ static void unicast_group_create(struct bt_bap_unicast_group **out_unicast_group group_param.params_count = 1; group_param.params = &pair_params; - err = bt_bap_unicast_group_create(&group_param, out_unicast_group); + err = bt_cap_unicast_group_create(&group_param, out_unicast_group); if (err != 0) { FAIL("Failed to create group: %d\n", err); return; } } -static void unicast_audio_start(struct bt_bap_unicast_group *unicast_group, bool wait) +static void unicast_audio_start(struct bt_cap_unicast_group *unicast_group, bool wait) { struct bt_cap_unicast_audio_start_stream_param stream_param[2]; struct bt_cap_unicast_audio_start_param param; @@ -761,7 +761,7 @@ static void unicast_audio_update(void) WAIT_FOR_FLAG(flag_updated); } -static void unicast_audio_stop(struct bt_bap_unicast_group *unicast_group) +static void unicast_audio_stop(struct bt_cap_unicast_group *unicast_group) { struct bt_cap_unicast_audio_stop_param param; int err; @@ -828,27 +828,27 @@ static void unicast_group_delete_inval(void) { int err; - err = bt_bap_unicast_group_delete(NULL); + err = bt_cap_unicast_group_delete(NULL); if (err == 0) { - FAIL("bt_bap_unicast_group_delete with NULL group did not fail\n"); + FAIL("bt_cap_unicast_group_delete with NULL group did not fail\n"); return; } } -static void unicast_group_delete(struct bt_bap_unicast_group *unicast_group) +static void unicast_group_delete(struct bt_cap_unicast_group *unicast_group) { int err; - err = bt_bap_unicast_group_delete(unicast_group); + err = bt_cap_unicast_group_delete(unicast_group); if (err != 0) { FAIL("Failed to create group: %d\n", err); return; } /* Verify that it cannot be deleted twice */ - err = bt_bap_unicast_group_delete(unicast_group); + err = bt_cap_unicast_group_delete(unicast_group); if (err == 0) { - FAIL("bt_bap_unicast_group_delete with already-deleted unicast group did not " + FAIL("bt_cap_unicast_group_delete with already-deleted unicast group did not " "fail\n"); return; } @@ -856,7 +856,7 @@ static void unicast_group_delete(struct bt_bap_unicast_group *unicast_group) static void test_main_cap_initiator_unicast(void) { - struct bt_bap_unicast_group *unicast_group; + struct bt_cap_unicast_group *unicast_group; const size_t iterations = 2; init(); @@ -909,7 +909,7 @@ static void test_main_cap_initiator_unicast(void) static void test_main_cap_initiator_unicast_inval(void) { - struct bt_bap_unicast_group *unicast_group; + struct bt_cap_unicast_group *unicast_group; init(); @@ -948,7 +948,7 @@ static void test_main_cap_initiator_unicast_inval(void) static void test_cap_initiator_unicast_timeout(void) { - struct bt_bap_unicast_group *unicast_group; + struct bt_cap_unicast_group *unicast_group; const k_timeout_t timeout = K_SECONDS(10); const size_t iterations = 2; @@ -1013,7 +1013,7 @@ static void unset_invalid_metadata_type(uint8_t type) static void test_cap_initiator_unicast_ase_error(void) { - struct bt_bap_unicast_group *unicast_group; + struct bt_cap_unicast_group *unicast_group; const uint8_t inval_type = 0xFD; init(); @@ -1071,12 +1071,12 @@ static int cap_initiator_ac_create_unicast_group(const struct cap_initiator_ac_p size_t snk_cnt, struct unicast_stream *src_uni_streams[], size_t src_cnt, - struct bt_bap_unicast_group **unicast_group) + struct bt_cap_unicast_group **unicast_group) { - struct bt_bap_unicast_group_stream_param snk_group_stream_params[CAP_AC_MAX_SNK] = {0}; - struct bt_bap_unicast_group_stream_param src_group_stream_params[CAP_AC_MAX_SRC] = {0}; - struct bt_bap_unicast_group_stream_pair_param pair_params[CAP_AC_MAX_PAIR] = {0}; - struct bt_bap_unicast_group_param group_param = {0}; + struct bt_cap_unicast_group_stream_param snk_group_stream_params[CAP_AC_MAX_SNK] = {0}; + struct bt_cap_unicast_group_stream_param src_group_stream_params[CAP_AC_MAX_SRC] = {0}; + struct bt_cap_unicast_group_stream_pair_param pair_params[CAP_AC_MAX_PAIR] = {0}; + struct bt_cap_unicast_group_param group_param = {0}; struct bt_bap_qos_cfg *snk_qos[CAP_AC_MAX_SNK]; struct bt_bap_qos_cfg *src_qos[CAP_AC_MAX_SRC]; size_t snk_stream_cnt = 0U; @@ -1097,14 +1097,14 @@ static int cap_initiator_ac_create_unicast_group(const struct cap_initiator_ac_p * and direction */ for (size_t i = 0U; i < snk_cnt; i++) { - snk_group_stream_params[i].qos = snk_qos[i]; + snk_group_stream_params[i].qos_cfg = snk_qos[i]; snk_group_stream_params[i].stream = - bap_stream_from_audio_test_stream(&snk_uni_streams[i]->stream); + cap_stream_from_audio_test_stream(&snk_uni_streams[i]->stream); } for (size_t i = 0U; i < src_cnt; i++) { - src_group_stream_params[i].qos = src_qos[i]; + src_group_stream_params[i].qos_cfg = src_qos[i]; src_group_stream_params[i].stream = - bap_stream_from_audio_test_stream(&src_uni_streams[i]->stream); + cap_stream_from_audio_test_stream(&src_uni_streams[i]->stream); } for (size_t i = 0U; i < param->conn_cnt; i++) { @@ -1131,7 +1131,7 @@ static int cap_initiator_ac_create_unicast_group(const struct cap_initiator_ac_p group_param.params = pair_params; group_param.params_count = pair_cnt; - return bt_bap_unicast_group_create(&group_param, unicast_group); + return bt_cap_unicast_group_create(&group_param, unicast_group); } static int cap_initiator_ac_cap_unicast_start(const struct cap_initiator_ac_param *param, @@ -1139,7 +1139,7 @@ static int cap_initiator_ac_cap_unicast_start(const struct cap_initiator_ac_para size_t snk_cnt, struct unicast_stream *src_uni_streams[], size_t src_cnt, - struct bt_bap_unicast_group *unicast_group) + struct bt_cap_unicast_group *unicast_group) { struct bt_cap_unicast_audio_start_stream_param stream_params[CAP_AC_MAX_STREAM] = {0}; struct bt_audio_codec_cfg *snk_codec_cfgs[CAP_AC_MAX_SNK] = {0}; @@ -1271,7 +1271,7 @@ static int cap_initiator_ac_cap_unicast_start(const struct cap_initiator_ac_para } static int cap_initiator_ac_unicast(const struct cap_initiator_ac_param *param, - struct bt_bap_unicast_group **unicast_group) + struct bt_cap_unicast_group **unicast_group) { /* Allocate params large enough for any params, but only use what is required */ struct unicast_stream *snk_uni_streams[CAP_AC_MAX_SNK]; @@ -1376,7 +1376,7 @@ static int cap_initiator_ac_unicast(const struct cap_initiator_ac_param *param, static void test_cap_initiator_ac(const struct cap_initiator_ac_param *param) { - struct bt_bap_unicast_group *unicast_group; + struct bt_cap_unicast_group *unicast_group; bool expect_tx = false; bool expect_rx = false; diff --git a/tests/bsim/bluetooth/audio/src/common.c b/tests/bsim/bluetooth/audio/src/common.c index 2c23a0836241..6896d879b866 100644 --- a/tests/bsim/bluetooth/audio/src/common.c +++ b/tests/bsim/bluetooth/audio/src/common.c @@ -247,6 +247,45 @@ void setup_broadcast_adv(struct bt_le_ext_adv **adv) } } +void start_broadcast_adv(struct bt_le_ext_adv *adv) +{ + char addr_str[BT_ADDR_LE_STR_LEN]; + struct bt_le_ext_adv_info info; + int err; + + err = bt_le_ext_adv_get_info(adv, &info); + if (err != 0) { + FAIL("Failed to get adv info: %d\n", err); + return; + } + + if (info.per_adv_state == BT_LE_PER_ADV_STATE_NONE) { + FAIL("Cannot start periodic advertising for non-periodic advertising set"); + return; + } + + if (info.ext_adv_state == BT_LE_EXT_ADV_STATE_CREATED) { + /* Start extended advertising */ + err = bt_le_ext_adv_start(adv, BT_LE_EXT_ADV_START_DEFAULT); + if (err != 0) { + FAIL("Failed to start extended advertising: %d\n", err); + return; + } + } + + if (info.per_adv_state == BT_LE_PER_ADV_STATE_CONFIGURED) { + /* Enable Periodic Advertising */ + err = bt_le_per_adv_start(adv); + if (err != 0) { + FAIL("Failed to enable periodic advertising: %d\n", err); + return; + } + } + + bt_addr_le_to_str(info.addr, addr_str, sizeof(addr_str)); + printk("Started advertising with addr %s\n", addr_str); +} + void test_tick(bs_time_t HW_device_time) { if (bst_result != Passed) { diff --git a/tests/bsim/bluetooth/audio/src/common.h b/tests/bsim/bluetooth/audio/src/common.h index 6ba2106a6c9a..a17b3466072d 100644 --- a/tests/bsim/bluetooth/audio/src/common.h +++ b/tests/bsim/bluetooth/audio/src/common.h @@ -72,16 +72,26 @@ static const uint8_t mock_iso_data[] = { 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff, }; +/* The sample SIRK as defined by the CSIS spec Appendix A.1. + * Sample data is Big Endian, so we reverse it for little-endian + */ +#define TEST_SAMPLE_SIRK \ + {REVERSE_ARGS(0x45, 0x7d, 0x7d, 0x09, 0x21, 0xa1, 0xfd, 0x22, 0xce, 0xcd, 0x8c, 0x86, \ + 0xdd, 0x72, 0xcc, 0xcd)} + #define MIN_SEND_COUNT 100 #define WAIT_SECONDS 100 /* seconds */ -#define WAIT_TIME (WAIT_SECONDS * USEC_PER_SEC) /* microseconds*/ +#define WAIT_TIME (WAIT_SECONDS * USEC_PER_SEC) /* microseconds*/ -#define WAIT_FOR_COND(cond) while (!(cond)) { k_sleep(K_MSEC(1)); } +#define WAIT_FOR_COND(cond) \ + while (!(cond)) { \ + k_sleep(K_MSEC(1)); \ + } -#define CREATE_FLAG(flag) static atomic_t flag = (atomic_t)false -#define SET_FLAG(flag) (void)atomic_set(&flag, (atomic_t)true) -#define UNSET_FLAG(flag) (void)atomic_clear(&flag) -#define TEST_FLAG(flag) (atomic_get(&flag) == (atomic_t)true) +#define CREATE_FLAG(flag) static atomic_t flag = (atomic_t) false +#define SET_FLAG(flag) (void)atomic_set(&flag, (atomic_t) true) +#define UNSET_FLAG(flag) (void)atomic_clear(&flag) +#define TEST_FLAG(flag) (atomic_get(&flag) == (atomic_t) true) #define WAIT_FOR_FLAG(flag) \ while (!(bool)atomic_get(&flag)) { \ (void)k_sleep(K_MSEC(1)); \ @@ -90,26 +100,26 @@ static const uint8_t mock_iso_data[] = { while (!(bool)atomic_clear(&flag)) { \ (void)k_sleep(K_MSEC(1)); \ } -#define WAIT_FOR_UNSET_FLAG(flag) \ - while (atomic_get(&flag) != (atomic_t)false) { \ - (void)k_sleep(K_MSEC(1)); \ +#define WAIT_FOR_UNSET_FLAG(flag) \ + while (atomic_get(&flag) != (atomic_t) false) { \ + (void)k_sleep(K_MSEC(1)); \ } extern enum bst_result_t bst_result; -#define FAIL(...) \ - do { \ - bst_result = Failed; \ - bs_trace_error_time_line(__VA_ARGS__); \ +#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, "PASSED: " __VA_ARGS__); \ +#define PASS(...) \ + do { \ + bst_result = Passed; \ + bs_trace_info_time(1, "PASSED: " __VA_ARGS__); \ } while (0) #define PA_SYNC_INTERVAL_TO_TIMEOUT_RATIO 20 /* Set the timeout relative to interval */ -#define PA_SYNC_SKIP 5 +#define PA_SYNC_SKIP 5 #define PBP_STREAMS_TO_SEND 2 @@ -140,6 +150,7 @@ extern uint8_t csip_rsi[BT_CSIP_RSI_SIZE]; void disconnected(struct bt_conn *conn, uint8_t reason); void setup_connectable_adv(struct bt_le_ext_adv **ext_adv); void setup_broadcast_adv(struct bt_le_ext_adv **adv); +void start_broadcast_adv(struct bt_le_ext_adv *adv); void test_tick(bs_time_t HW_device_time); void test_init(void); uint16_t get_dev_cnt(void); diff --git a/tests/bsim/bluetooth/audio/src/csip_set_member_test.c b/tests/bsim/bluetooth/audio/src/csip_set_member_test.c index dd4d4f2cd661..bd878f7c9897 100644 --- a/tests/bsim/bluetooth/audio/src/csip_set_member_test.c +++ b/tests/bsim/bluetooth/audio/src/csip_set_member_test.c @@ -26,8 +26,7 @@ static uint8_t sirk_read_req_rsp = BT_CSIP_READ_SIRK_REQ_RSP_ACCEPT; struct bt_csip_set_member_register_param param = { .lockable = true, /* Using the CSIS test sample SIRK */ - .sirk = {0xcd, 0xcc, 0x72, 0xdd, 0x86, 0x8c, 0xcd, 0xce, 0x22, 0xfd, 0xa1, 0x21, 0x09, 0x7d, - 0x7d, 0x45}, + .sirk = TEST_SAMPLE_SIRK, }; static void csip_lock_changed_cb(struct bt_conn *conn, diff --git a/tests/bsim/bluetooth/audio/src/gmap_ugg_test.c b/tests/bsim/bluetooth/audio/src/gmap_ugg_test.c index c1d608058712..77c85bf01257 100644 --- a/tests/bsim/bluetooth/audio/src/gmap_ugg_test.c +++ b/tests/bsim/bluetooth/audio/src/gmap_ugg_test.c @@ -613,14 +613,14 @@ static int gmap_unicast_ac_create_unicast_group(const struct gmap_unicast_ac_par size_t snk_cnt, struct unicast_stream *src_uni_streams[], size_t src_cnt, - struct bt_bap_unicast_group **unicast_group) + struct bt_cap_unicast_group **unicast_group) { - struct bt_bap_unicast_group_stream_param + struct bt_cap_unicast_group_stream_param snk_group_stream_params[GMAP_UNICAST_AC_MAX_SNK] = {0}; - struct bt_bap_unicast_group_stream_param + struct bt_cap_unicast_group_stream_param src_group_stream_params[GMAP_UNICAST_AC_MAX_SRC] = {0}; - struct bt_bap_unicast_group_stream_pair_param pair_params[GMAP_UNICAST_AC_MAX_PAIR] = {0}; - struct bt_bap_unicast_group_param group_param = {0}; + struct bt_cap_unicast_group_stream_pair_param pair_params[GMAP_UNICAST_AC_MAX_PAIR] = {0}; + struct bt_cap_unicast_group_param group_param = {0}; size_t snk_stream_cnt = 0U; size_t src_stream_cnt = 0U; size_t pair_cnt = 0U; @@ -631,14 +631,14 @@ static int gmap_unicast_ac_create_unicast_group(const struct gmap_unicast_ac_par * and direction */ for (size_t i = 0U; i < snk_cnt; i++) { - snk_group_stream_params[i].qos = &snk_uni_streams[i]->qos; + snk_group_stream_params[i].qos_cfg = &snk_uni_streams[i]->qos; snk_group_stream_params[i].stream = - bap_stream_from_audio_test_stream(&snk_uni_streams[i]->stream); + cap_stream_from_audio_test_stream(&snk_uni_streams[i]->stream); } for (size_t i = 0U; i < src_cnt; i++) { - src_group_stream_params[i].qos = &src_uni_streams[i]->qos; + src_group_stream_params[i].qos_cfg = &src_uni_streams[i]->qos; src_group_stream_params[i].stream = - bap_stream_from_audio_test_stream(&src_uni_streams[i]->stream); + cap_stream_from_audio_test_stream(&src_uni_streams[i]->stream); } for (size_t i = 0U; i < param->conn_cnt; i++) { @@ -665,13 +665,13 @@ static int gmap_unicast_ac_create_unicast_group(const struct gmap_unicast_ac_par group_param.params = pair_params; group_param.params_count = pair_cnt; - return bt_bap_unicast_group_create(&group_param, unicast_group); + return bt_cap_unicast_group_create(&group_param, unicast_group); } static int gmap_ac_cap_unicast_start(const struct gmap_unicast_ac_param *param, struct unicast_stream *snk_uni_streams[], size_t snk_cnt, struct unicast_stream *src_uni_streams[], size_t src_cnt, - struct bt_bap_unicast_group *unicast_group) + struct bt_cap_unicast_group *unicast_group) { struct bt_cap_unicast_audio_start_stream_param stream_params[GMAP_UNICAST_AC_MAX_STREAM] = { 0}; @@ -813,7 +813,7 @@ static int gmap_ac_cap_unicast_start(const struct gmap_unicast_ac_param *param, } static int gmap_ac_unicast(const struct gmap_unicast_ac_param *param, - struct bt_bap_unicast_group **unicast_group) + struct bt_cap_unicast_group **unicast_group) { /* Allocate params large enough for any params, but only use what is required */ struct unicast_stream *snk_uni_streams[GMAP_UNICAST_AC_MAX_SNK]; @@ -900,7 +900,7 @@ static int gmap_ac_unicast(const struct gmap_unicast_ac_param *param, return 0; } -static void unicast_audio_stop(struct bt_bap_unicast_group *unicast_group) +static void unicast_audio_stop(struct bt_cap_unicast_group *unicast_group) { struct bt_cap_unicast_audio_stop_param param; int err; @@ -924,11 +924,11 @@ static void unicast_audio_stop(struct bt_bap_unicast_group *unicast_group) memset(started_unicast_streams, 0, sizeof(started_unicast_streams)); } -static void unicast_group_delete(struct bt_bap_unicast_group *unicast_group) +static void unicast_group_delete(struct bt_cap_unicast_group *unicast_group) { int err; - err = bt_bap_unicast_group_delete(unicast_group); + err = bt_cap_unicast_group_delete(unicast_group); if (err != 0) { FAIL("Failed to create group: %d\n", err); return; @@ -937,7 +937,7 @@ static void unicast_group_delete(struct bt_bap_unicast_group *unicast_group) static void test_gmap_ugg_unicast_ac(const struct gmap_unicast_ac_param *param) { - struct bt_bap_unicast_group *unicast_group; + struct bt_cap_unicast_group *unicast_group; bool expect_tx = false; bool expect_rx = false; @@ -1066,25 +1066,6 @@ static void setup_extended_adv_data(struct bt_cap_broadcast_source *source, } } -static void start_extended_adv(struct bt_le_ext_adv *adv) -{ - int err; - - /* Start extended advertising */ - err = bt_le_ext_adv_start(adv, BT_LE_EXT_ADV_START_DEFAULT); - if (err) { - FAIL("Failed to start extended advertising: %d\n", err); - return; - } - - /* Enable Periodic Advertising */ - err = bt_le_per_adv_start(adv); - if (err) { - FAIL("Failed to enable periodic advertising: %d\n", err); - return; - } -} - static void stop_and_delete_extended_adv(struct bt_le_ext_adv *adv) { int err; @@ -1165,10 +1146,10 @@ static int test_gmap_ugg_broadcast_ac(const struct gmap_broadcast_ac_param *para uint8_t stereo_data[] = { BT_AUDIO_CODEC_DATA(BT_AUDIO_CODEC_CFG_CHAN_ALLOC, BT_AUDIO_LOCATION_FRONT_RIGHT | BT_AUDIO_LOCATION_FRONT_LEFT)}; - uint8_t right_data[] = {BT_AUDIO_CODEC_DATA(BT_AUDIO_CODEC_CFG_CHAN_ALLOC, - BT_AUDIO_LOCATION_FRONT_RIGHT)}; - uint8_t left_data[] = {BT_AUDIO_CODEC_DATA(BT_AUDIO_CODEC_CFG_CHAN_ALLOC, - BT_AUDIO_LOCATION_FRONT_LEFT)}; + uint8_t right_data[] = { + BT_AUDIO_CODEC_DATA(BT_AUDIO_CODEC_CFG_CHAN_ALLOC, BT_AUDIO_LOCATION_FRONT_RIGHT)}; + uint8_t left_data[] = { + BT_AUDIO_CODEC_DATA(BT_AUDIO_CODEC_CFG_CHAN_ALLOC, BT_AUDIO_LOCATION_FRONT_LEFT)}; struct bt_cap_initiator_broadcast_subgroup_param subgroup_param = {0}; struct bt_cap_initiator_broadcast_create_param create_param = {0}; struct bt_cap_initiator_broadcast_stream_param @@ -1222,7 +1203,7 @@ static int test_gmap_ugg_broadcast_ac(const struct gmap_broadcast_ac_param *para broadcast_audio_start(broadcast_source, adv); setup_extended_adv_data(broadcast_source, adv); - start_extended_adv(adv); + start_broadcast_adv(adv); /* Wait for all to be started */ printk("Waiting for broadcast_streams to be started\n"); diff --git a/tests/bsim/bluetooth/audio/src/gmap_ugt_test.c b/tests/bsim/bluetooth/audio/src/gmap_ugt_test.c index 5507639cf3f4..f2a606a2227c 100644 --- a/tests/bsim/bluetooth/audio/src/gmap_ugt_test.c +++ b/tests/bsim/bluetooth/audio/src/gmap_ugt_test.c @@ -455,8 +455,7 @@ static void test_main(void) .rank = csis_rank, .lockable = true, /* Using the CSIP_SET_MEMBER test sample SIRK */ - .sirk = { 0xcd, 0xcc, 0x72, 0xdd, 0x86, 0x8c, 0xcd, 0xce, - 0x22, 0xfd, 0xa1, 0x21, 0x09, 0x7d, 0x7d, 0x45 }, + .sirk = TEST_SAMPLE_SIRK, }; err = bt_cap_acceptor_register(&csip_set_member_param, &csip_set_member); diff --git a/tests/bsim/bluetooth/audio/src/main.c b/tests/bsim/bluetooth/audio/src/main.c index 3e8161fe7de4..6d58ceb2a22c 100644 --- a/tests/bsim/bluetooth/audio/src/main.c +++ b/tests/bsim/bluetooth/audio/src/main.c @@ -30,6 +30,8 @@ extern struct bst_test_list *test_cap_acceptor_install(struct bst_test_list *tes extern struct bst_test_list *test_cap_commander_install(struct bst_test_list *tests); extern struct bst_test_list *test_cap_initiator_broadcast_install(struct bst_test_list *tests); extern struct bst_test_list *test_cap_initiator_unicast_install(struct bst_test_list *tests); +extern struct bst_test_list *test_cap_handover_central_install(struct bst_test_list *tests); +extern struct bst_test_list *test_cap_handover_peripheral_install(struct bst_test_list *tests); extern struct bst_test_list *test_has_install(struct bst_test_list *tests); extern struct bst_test_list *test_has_client_install(struct bst_test_list *tests); extern struct bst_test_list *test_ias_install(struct bst_test_list *tests); @@ -70,6 +72,8 @@ bst_test_install_t test_installers[] = { test_cap_acceptor_install, test_cap_initiator_broadcast_install, test_cap_initiator_unicast_install, + test_cap_handover_central_install, + test_cap_handover_peripheral_install, test_has_install, test_has_client_install, test_ias_install, diff --git a/tests/bsim/bluetooth/audio/src/pbp_public_broadcast_source_test.c b/tests/bsim/bluetooth/audio/src/pbp_public_broadcast_source_test.c index eb2e789f24a3..5b7763df87cf 100644 --- a/tests/bsim/bluetooth/audio/src/pbp_public_broadcast_source_test.c +++ b/tests/bsim/bluetooth/audio/src/pbp_public_broadcast_source_test.c @@ -33,7 +33,7 @@ #if defined(CONFIG_BT_PBP) /* PBS ASCII text */ -#define PBS_DEMO 'P', 'B', 'P' +#define PBS_DEMO 'P', 'B', 'P' #define SEM_TIMEOUT K_SECONDS(2) extern enum bst_result_t bst_result; @@ -41,9 +41,8 @@ extern enum bst_result_t bst_result; static const uint8_t pba_metadata[] = { BT_AUDIO_CODEC_DATA(BT_AUDIO_METADATA_TYPE_PROGRAM_INFO, PBS_DEMO)}; -static uint8_t bis_codec_data[] = { - BT_AUDIO_CODEC_DATA(BT_AUDIO_CODEC_CFG_FREQ, - BT_BYTES_LIST_LE16(BT_AUDIO_CODEC_CFG_FREQ_48KHZ))}; +static uint8_t bis_codec_data[] = {BT_AUDIO_CODEC_DATA( + BT_AUDIO_CODEC_CFG_FREQ, BT_BYTES_LIST_LE16(BT_AUDIO_CODEC_CFG_FREQ_48KHZ))}; static struct audio_test_stream broadcast_source_stream; static struct bt_cap_stream *broadcast_stream; @@ -54,8 +53,7 @@ static struct bt_cap_initiator_broadcast_create_param create_param; static struct bt_cap_broadcast_source *broadcast_source; static struct bt_bap_lc3_preset broadcast_preset_48_2_1 = - BT_BAP_LC3_UNICAST_PRESET_48_2_1(BT_AUDIO_LOCATION_FRONT_LEFT, - BT_AUDIO_CONTEXT_TYPE_MEDIA); + BT_BAP_LC3_UNICAST_PRESET_48_2_1(BT_AUDIO_LOCATION_FRONT_LEFT, BT_AUDIO_CONTEXT_TYPE_MEDIA); static K_SEM_DEFINE(sem_started, 0U, 1); static K_SEM_DEFINE(sem_stopped, 0U, 1); @@ -175,29 +173,6 @@ static int setup_extended_adv_data(struct bt_cap_broadcast_source *source, return 0; } -static int start_extended_adv(struct bt_le_ext_adv *adv) -{ - int err; - - /* Start extended advertising */ - err = bt_le_ext_adv_start(adv, BT_LE_EXT_ADV_START_DEFAULT); - if (err) { - printk("Failed to start extended advertising: %d\n", err); - - return err; - } - - /* Enable Periodic Advertising */ - err = bt_le_per_adv_start(adv); - if (err) { - printk("Failed to enable periodic advertising: %d\n", err); - - return err; - } - - return 0; -} - static int stop_extended_adv(struct bt_le_ext_adv *adv) { int err; @@ -289,12 +264,7 @@ static void test_main(void) FAIL("Public Broadcast source failed\n"); } - err = start_extended_adv(adv); - if (err != 0) { - printk("Unable to start extended advertiser: %d\n", err); - FAIL("Public Broadcast source failed\n"); - } - + start_broadcast_adv(adv); k_sem_take(&sem_started, SEM_TIMEOUT); /* Wait for other devices to let us know when we can stop the source */ @@ -330,14 +300,11 @@ static void test_main(void) } static const struct bst_test_instance test_pbp_broadcaster[] = { - { - .test_id = "public_broadcast_source", - .test_pre_init_f = test_init, - .test_tick_f = test_tick, - .test_main_f = test_main - }, - BSTEST_END_MARKER -}; + {.test_id = "public_broadcast_source", + .test_pre_init_f = test_init, + .test_tick_f = test_tick, + .test_main_f = test_main}, + BSTEST_END_MARKER}; struct bst_test_list *test_public_broadcast_source_install(struct bst_test_list *tests) { diff --git a/tests/bsim/bluetooth/audio/test_scripts/cap_handover_unicast_to_broadcast.sh b/tests/bsim/bluetooth/audio/test_scripts/cap_handover_unicast_to_broadcast.sh new file mode 100755 index 000000000000..28dbdd78c460 --- /dev/null +++ b/tests/bsim/bluetooth/audio/test_scripts/cap_handover_unicast_to_broadcast.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 + +SIMULATION_ID="cap_handover_unicast_to_broadcast" +VERBOSITY_LEVEL=2 +NR_OF_DEVICES=3 +EXECUTE_TIMEOUT=240 + +source ${ZEPHYR_BASE}/tests/bsim/sh_common.source + +cd ${BSIM_OUT_PATH}/bin + +printf "\n\n======== Running CAP handover unicast to broadcast =========\n\n" + +Execute ./bs_${BOARD_TS}_tests_bsim_bluetooth_audio_prj_conf \ + -v=${VERBOSITY_LEVEL} -s=${SIMULATION_ID} -d=0 \ + -testid=cap_handover_unicast_to_broadcast \ + -RealEncryption=1 -rs=23 -D=${NR_OF_DEVICES} + +Execute ./bs_${BOARD_TS}_tests_bsim_bluetooth_audio_prj_conf \ + -v=${VERBOSITY_LEVEL} -s=${SIMULATION_ID} -d=1 \ + -testid=cap_handover_peripheral_unicast_to_broadcast \ + -RealEncryption=1 -rs=46 -D=${NR_OF_DEVICES} + +Execute ./bs_${BOARD_TS}_tests_bsim_bluetooth_audio_prj_conf \ + -v=${VERBOSITY_LEVEL} -s=${SIMULATION_ID} -d=2 \ + -testid=cap_handover_peripheral_unicast_to_broadcast \ + -RealEncryption=1 -rs=69 -D=${NR_OF_DEVICES} + +Execute ./bs_2G4_phy_v1 -v=${VERBOSITY_LEVEL} -s=${SIMULATION_ID} \ + -D=${NR_OF_DEVICES} -sim_length=120e6 $@ + +wait_for_background_jobs