diff --git a/subsys/bluetooth/audio/bap_broadcast_assistant.c b/subsys/bluetooth/audio/bap_broadcast_assistant.c index 77ead34fef688..7427e03b4ec1c 100644 --- a/subsys/bluetooth/audio/bap_broadcast_assistant.c +++ b/subsys/bluetooth/audio/bap_broadcast_assistant.c @@ -28,7 +28,6 @@ #include #include #include -#include #include #include #include diff --git a/tests/bluetooth/audio/cap_handover/CMakeLists.txt b/tests/bluetooth/audio/cap_handover/CMakeLists.txt new file mode 100644 index 0000000000000..4f97856b0b86e --- /dev/null +++ b/tests/bluetooth/audio/cap_handover/CMakeLists.txt @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(bluetooth_cap_handover) + +target_include_directories(app PRIVATE + ${ZEPHYR_BASE}/subsys/bluetooth/ + + ${ZEPHYR_BASE}/tests/bluetooth/audio/cap_handover/include + ${ZEPHYR_BASE}/tests/bluetooth/audio/mocks/include +) + +target_sources(app PRIVATE + # Test source files + src/unicast_to_broadcast.c + src/test_common.c + + # UUT files + uut/bap_broadcast_assistant.c + uut/bap_unicast_client.c + uut/cap_handover.c + uut/cap_initiator.c + uut/csip.c + + # Stack source file + ${ZEPHYR_BASE}/subsys/bluetooth/audio/audio.c + ${ZEPHYR_BASE}/subsys/bluetooth/audio/codec.c + ${ZEPHYR_BASE}/subsys/bluetooth/audio/bap_broadcast_source.c + ${ZEPHYR_BASE}/subsys/bluetooth/audio/bap_iso.c + ${ZEPHYR_BASE}/subsys/bluetooth/audio/bap_stream.c + ${ZEPHYR_BASE}/subsys/bluetooth/audio/cap_commander.c + ${ZEPHYR_BASE}/subsys/bluetooth/audio/cap_handover.c + ${ZEPHYR_BASE}/subsys/bluetooth/audio/cap_initiator.c + ${ZEPHYR_BASE}/subsys/bluetooth/audio/cap_common.c + ${ZEPHYR_BASE}/subsys/bluetooth/audio/cap_stream.c + ${ZEPHYR_BASE}/subsys/bluetooth/audio/ccid.c + ${ZEPHYR_BASE}/subsys/bluetooth/common/addr.c + ${ZEPHYR_BASE}/subsys/bluetooth/common/bt_str.c + ${ZEPHYR_BASE}/subsys/bluetooth/host/uuid.c + + # Mock files + ${ZEPHYR_BASE}/tests/bluetooth/audio/mocks/src/adv.c + ${ZEPHYR_BASE}/tests/bluetooth/audio/mocks/src/conn.c + ${ZEPHYR_BASE}/tests/bluetooth/audio/mocks/src/gatt.c + ${ZEPHYR_BASE}/tests/bluetooth/audio/mocks/src/iso.c +) diff --git a/tests/bluetooth/audio/cap_handover/Kconfig b/tests/bluetooth/audio/cap_handover/Kconfig new file mode 100644 index 0000000000000..44112aa19cf63 --- /dev/null +++ b/tests/bluetooth/audio/cap_handover/Kconfig @@ -0,0 +1,111 @@ +# Copyright (c) 2025 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +config BT_ATT_PREPARE_COUNT + int + default 1 + +config BT_AUDIO + bool + default y + +config BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE + int + default 20 + +config BT_BAP_BROADCAST_ASSISTANT + bool + default y + +config BT_BAP_BROADCAST_SOURCE + bool + default y + +config BT_BAP_BROADCAST_SRC_STREAM_COUNT + int + default 2 + +config BT_BAP_SCAN_DELEGATOR + bool + default y + +config BT_BAP_STREAM + bool + default y + +config BT_BAP_UNICAST_CLIENT + bool + default y + +config BT_BAP_UNICAST_CLIENT_GROUP_COUNT + int + default 1 + +config BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT + int + default 4 + +config BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT + int + default 2 + +config BT_BONDABLE + bool + default y + +config BT_BUF_ACL_RX_SIZE + int + default 69 + +config BT_BUF_ACL_TX_COUNT + int + default 5 + +config BT_BUF_EVT_RX_COUNT + int + default 10 + +config BT_CONN + bool + default y + +config BT_CSIP_SET_COORDINATOR + bool + default y + +config BT_ISO_MAX_BIG + int + default 1 + +config BT_ISO_MAX_CIG + int + default 1 + +config BT_ISO_MAX_CHAN + int + default 4 + +config BT_MAX_PAIRED + int + default 1 + +config BT_L2CAP_TX_MTU + int + default 65 + +config BT_LOG + bool + default y + +config BT_MAX_CONN + int + default 2 + +config BT_SMP + bool + default y + +# Include Zephyr's Kconfig. +source "Kconfig" +source "subsys/bluetooth/audio/Kconfig" +source "subsys/bluetooth/Kconfig.logging" diff --git a/tests/bluetooth/audio/cap_handover/include/cap_handover.h b/tests/bluetooth/audio/cap_handover/include/cap_handover.h new file mode 100644 index 0000000000000..cf2c41eea5e39 --- /dev/null +++ b/tests/bluetooth/audio/cap_handover/include/cap_handover.h @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef MOCKS_CAP_HANDOVER_H_ +#define MOCKS_CAP_HANDOVER_H_ + +#include +#include +#include +#include + +extern const struct bt_cap_handover_cb mock_cap_handover_cb; + +void mock_cap_handover_init(void); + +DECLARE_FAKE_VOID_FUNC(mock_unicast_to_broadcast_complete_cb, int, struct bt_conn *, + struct bt_cap_unicast_group *, struct bt_cap_broadcast_source *); + +#endif /* MOCKS_CAP_HANDOVER_H_ */ diff --git a/tests/bluetooth/audio/cap_handover/include/cap_initiator.h b/tests/bluetooth/audio/cap_handover/include/cap_initiator.h new file mode 100644 index 0000000000000..cc2b4a97ad8c2 --- /dev/null +++ b/tests/bluetooth/audio/cap_handover/include/cap_initiator.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef MOCKS_CAP_INITIATOR_H_ +#define MOCKS_CAP_INITIATOR_H_ + +#include +#include +#include +#include + +extern const struct bt_cap_initiator_cb mock_cap_initiator_cb; + +void mock_cap_initiator_init(void); + +DECLARE_FAKE_VOID_FUNC(mock_unicast_start_complete_cb, int, struct bt_conn *); + +#endif /* MOCKS_CAP_INITIATOR_H_ */ diff --git a/tests/bluetooth/audio/cap_handover/include/test_common.h b/tests/bluetooth/audio/cap_handover/include/test_common.h new file mode 100644 index 0000000000000..169068d392aa8 --- /dev/null +++ b/tests/bluetooth/audio/cap_handover/include/test_common.h @@ -0,0 +1,22 @@ +/* test_common.h */ + +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +void test_mocks_init(void); +void test_mocks_cleanup(void); +void mock_bt_csip_cleanup(void); + +void test_conn_init(struct bt_conn *conn); + +void test_unicast_set_state(struct bt_cap_stream *cap_stream, struct bt_conn *conn, + struct bt_bap_ep *ep, struct bt_bap_lc3_preset *preset, + enum bt_bap_ep_state state); diff --git a/tests/bluetooth/audio/cap_handover/prj.conf b/tests/bluetooth/audio/cap_handover/prj.conf new file mode 100644 index 0000000000000..f8875d96d7f75 --- /dev/null +++ b/tests/bluetooth/audio/cap_handover/prj.conf @@ -0,0 +1,18 @@ +CONFIG_ZTEST=y + +CONFIG_NET_BUF=y + +CONFIG_BT_CAP_INITIATOR=y +CONFIG_BT_CAP_COMMANDER=y +CONFIG_BT_CAP_HANDOVER=y + +CONFIG_ASSERT=y +CONFIG_ASSERT_LEVEL=2 +CONFIG_ASSERT_VERBOSE=y + +CONFIG_LOG=y +CONFIG_BT_BAP_STREAM_LOG_LEVEL_DBG=y +CONFIG_BT_CAP_COMMON_LOG_LEVEL_DBG=y +CONFIG_BT_CAP_INITIATOR_LOG_LEVEL_DBG=y +CONFIG_BT_CAP_COMMANDER_LOG_LEVEL_DBG=y +CONFIG_BT_BAP_BROADCAST_SOURCE_LOG_LEVEL_DBG=y diff --git a/tests/bluetooth/audio/cap_handover/src/test_common.c b/tests/bluetooth/audio/cap_handover/src/test_common.c new file mode 100644 index 0000000000000..69271c3671854 --- /dev/null +++ b/tests/bluetooth/audio/cap_handover/src/test_common.c @@ -0,0 +1,70 @@ +/* test_common.c - common procedures for unit test of CAP handover */ + +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "audio/bap_endpoint.h" +#include "cap_initiator.h" +#include "cap_handover.h" +#include "conn.h" +#include "test_common.h" + +DEFINE_FFF_GLOBALS; + +void test_mocks_init(void) +{ + mock_cap_initiator_init(); + mock_cap_handover_init(); +} + +void test_mocks_cleanup(void) +{ + mock_bt_csip_cleanup(); +} + +void test_conn_init(struct bt_conn *conn) +{ + conn->index = 0; + conn->info.type = BT_CONN_TYPE_LE; + conn->info.role = BT_CONN_ROLE_CENTRAL; + conn->info.state = BT_CONN_STATE_CONNECTED; + conn->info.security.level = BT_SECURITY_L2; + conn->info.security.enc_key_size = BT_ENC_KEY_SIZE_MAX; + conn->info.security.flags = BT_SECURITY_FLAG_OOB | BT_SECURITY_FLAG_SC; +} + +void test_unicast_set_state(struct bt_cap_stream *cap_stream, struct bt_conn *conn, + struct bt_bap_ep *ep, struct bt_bap_lc3_preset *preset, + enum bt_bap_ep_state state) +{ + struct bt_bap_stream *bap_stream = &cap_stream->bap_stream; + + printk("Setting stream %p to state %d\n", bap_stream, state); + + if (state == BT_BAP_EP_STATE_IDLE) { + return; + } + + zassert_not_null(cap_stream); + zassert_not_null(conn); + zassert_not_null(ep); + zassert_not_null(preset); + + bap_stream->conn = conn; + bap_stream->ep = ep; + bap_stream->qos = &preset->qos; + bap_stream->codec_cfg = &preset->codec_cfg; + bap_stream->ep->state = state; +} diff --git a/tests/bluetooth/audio/cap_handover/src/unicast_to_broadcast.c b/tests/bluetooth/audio/cap_handover/src/unicast_to_broadcast.c new file mode 100644 index 0000000000000..936321598c2c6 --- /dev/null +++ b/tests/bluetooth/audio/cap_handover/src/unicast_to_broadcast.c @@ -0,0 +1,451 @@ +/* main.c - Application main entry point */ + +/* + * 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 "audio/bap_endpoint.h" +#include "audio/bap_iso.h" +#include "bluetooth.h" +#include "cap_initiator.h" +#include "cap_handover.h" +#include "conn.h" +#include "expects_util.h" +#include "test_common.h" + +static void mock_init_rule_before(const struct ztest_unit_test *test, void *fixture) +{ + test_mocks_init(); +} + +static void mock_destroy_rule_after(const struct ztest_unit_test *test, void *fixture) +{ + test_mocks_cleanup(); +} + +ZTEST_RULE(mock_rule, mock_init_rule_before, mock_destroy_rule_after); + +#define STREAMS_PER_DIRECTION 2 +#define MAX_STREAMS 4 +BUILD_ASSERT(CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT >= MAX_STREAMS); +BUILD_ASSERT(CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT >= STREAMS_PER_DIRECTION); + +struct cap_handover_unicast_to_broadcast_test_suite_fixture { + struct bt_cap_unicast_group_stream_pair_param + unicast_group_stream_pair_params[DIV_ROUND_UP(MAX_STREAMS, STREAMS_PER_DIRECTION)]; + struct bt_cap_unicast_audio_start_stream_param + unicast_audio_start_stream_params[MAX_STREAMS]; + struct bt_cap_initiator_broadcast_stream_param broadcast_stream_params[MAX_STREAMS]; + struct bt_cap_unicast_group_stream_param unicast_group_stream_params[MAX_STREAMS]; + struct bt_cap_handover_unicast_to_broadcast_param unicast_to_broadcast_param; + struct bt_cap_initiator_broadcast_create_param broadcast_create_param; + struct bt_cap_unicast_audio_start_param unicast_audio_start_param; + struct bt_cap_initiator_broadcast_subgroup_param subgroup_params; + struct bt_bap_lc3_preset unicast_presets[MAX_STREAMS]; + struct bt_cap_unicast_group_param group_param; + struct bt_cap_stream cap_streams[MAX_STREAMS]; + struct bt_bap_lc3_preset broadcast_preset; + struct bt_cap_unicast_group *unicast_group; + struct bt_conn conns[CONFIG_BT_MAX_CONN]; + struct bt_bap_ep eps[MAX_STREAMS]; + struct bt_le_ext_adv ext_adv; +}; + +static void *cap_handover_unicast_to_broadcast_test_suite_setup(void) +{ + struct cap_handover_unicast_to_broadcast_test_suite_fixture *fixture; + + fixture = malloc(sizeof(*fixture)); + zassert_not_null(fixture); + + return fixture; +} + +static void cap_handover_unicast_to_broadcast_test_suite_before(void *f) +{ + struct cap_handover_unicast_to_broadcast_test_suite_fixture *fixture = f; + size_t stream_cnt = 0U; + size_t pair_cnt = 0U; + int err; + + memset(fixture, 0, sizeof(*fixture)); + + err = bt_cap_initiator_register_cb(&mock_cap_initiator_cb); + zassert_equal(0, err, "Unexpected return value %d", err); + + err = bt_cap_handover_register_cb(&mock_cap_handover_cb); + zassert_equal(0, err, "Unexpected return value %d", err); + + /* Create unicast group */ + ARRAY_FOR_EACH(fixture->unicast_presets, i) { + fixture->unicast_presets[i] = + (struct bt_bap_lc3_preset)BT_BAP_LC3_UNICAST_PRESET_16_2_1( + BT_AUDIO_LOCATION_MONO_AUDIO, BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED); + } + + for (size_t i = 0U; i < ARRAY_SIZE(fixture->conns); i++) { + test_conn_init(&fixture->conns[i]); + } + + for (size_t i = 0U; i < ARRAY_SIZE(fixture->eps); i++) { + if ((i & 1) == 0) { + fixture->eps[i].dir = BT_AUDIO_DIR_SINK; + } else { + fixture->eps[i].dir = BT_AUDIO_DIR_SOURCE; + } + } + + while (stream_cnt < ARRAY_SIZE(fixture->unicast_group_stream_params)) { + fixture->unicast_group_stream_params[stream_cnt].stream = + &fixture->cap_streams[stream_cnt]; + fixture->unicast_group_stream_params[stream_cnt].qos_cfg = + &fixture->unicast_presets[stream_cnt].qos; + + /* Switch between sink and source depending on index*/ + if ((stream_cnt & 1) == 0) { + fixture->unicast_group_stream_pair_params[pair_cnt].tx_param = + &fixture->unicast_group_stream_params[stream_cnt]; + } else { + fixture->unicast_group_stream_pair_params[pair_cnt].rx_param = + &fixture->unicast_group_stream_params[stream_cnt]; + } + + pair_cnt = DIV_ROUND_UP(stream_cnt, 2U); + stream_cnt++; + } + + fixture->group_param.packing = BT_ISO_PACKING_SEQUENTIAL; + fixture->group_param.params_count = pair_cnt; + fixture->group_param.params = fixture->unicast_group_stream_pair_params; + + err = bt_cap_unicast_group_create(&fixture->group_param, &fixture->unicast_group); + zassert_equal(err, 0, "Unexpected return value %d", err); + + /* Start unicast group */ + fixture->unicast_audio_start_param.type = BT_CAP_SET_TYPE_AD_HOC; + fixture->unicast_audio_start_param.count = + ARRAY_SIZE(fixture->unicast_audio_start_stream_params); + fixture->unicast_audio_start_param.stream_params = + fixture->unicast_audio_start_stream_params; + + for (size_t i = 0U; i < ARRAY_SIZE(fixture->unicast_audio_start_stream_params); i++) { + fixture->unicast_audio_start_stream_params[i].stream = &fixture->cap_streams[i]; + fixture->unicast_audio_start_stream_params[i].codec_cfg = + &fixture->unicast_presets[i].codec_cfg; + /* Distribute the streams equally among the connections */ + fixture->unicast_audio_start_stream_params[i].member.member = + &fixture->conns[i % ARRAY_SIZE(fixture->conns)]; + fixture->unicast_audio_start_stream_params[i].ep = &fixture->eps[i]; + } + + err = bt_cap_initiator_unicast_audio_start(&fixture->unicast_audio_start_param); + zassert_equal(err, 0, "Unexpected return value %d", err); + + zexpect_call_count("bt_cap_initiator_cb.unicast_start_complete_cb", 1, + mock_unicast_start_complete_cb_fake.call_count); + zassert_equal(0, mock_unicast_start_complete_cb_fake.arg0_history[0]); + zassert_equal_ptr(NULL, mock_unicast_start_complete_cb_fake.arg1_history[0]); + + /* Prepare default handover parameters including broadcast source create parameters */ + fixture->ext_adv.ext_adv_state = BT_LE_EXT_ADV_STATE_ENABLED; + fixture->ext_adv.per_adv_state = BT_LE_PER_ADV_STATE_ENABLED; + + fixture->broadcast_preset = (struct bt_bap_lc3_preset)BT_BAP_LC3_BROADCAST_PRESET_16_2_1( + BT_AUDIO_LOCATION_MONO_AUDIO, BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED); + fixture->unicast_to_broadcast_param.type = BT_CAP_SET_TYPE_AD_HOC; + fixture->unicast_to_broadcast_param.ext_adv = &fixture->ext_adv; + fixture->unicast_to_broadcast_param.unicast_group = fixture->unicast_group; + fixture->unicast_to_broadcast_param.pa_interval = 0x1234U; + fixture->unicast_to_broadcast_param.broadcast_id = 0x123456U; + fixture->unicast_to_broadcast_param.broadcast_create_param = + &fixture->broadcast_create_param; + + fixture->broadcast_create_param.subgroup_count = 1U; + fixture->broadcast_create_param.subgroup_params = &fixture->subgroup_params; + fixture->broadcast_create_param.qos = &fixture->broadcast_preset.qos; + fixture->broadcast_create_param.packing = BT_ISO_PACKING_SEQUENTIAL; + fixture->broadcast_create_param.encryption = false; + + /* We use pair_cnt as stream_count as it is equal to the number of sink streams */ + fixture->subgroup_params.stream_count = pair_cnt; + fixture->subgroup_params.stream_params = fixture->broadcast_stream_params; + fixture->subgroup_params.codec_cfg = &fixture->broadcast_preset.codec_cfg; + + for (size_t i = 0U; i < pair_cnt; i++) { + fixture->broadcast_stream_params[i].stream = + fixture->unicast_group_stream_pair_params[i].tx_param->stream; + } +} + +static void cap_handover_unicast_to_broadcast_test_suite_after(void *f) +{ + struct cap_handover_unicast_to_broadcast_test_suite_fixture *fixture = f; + + (void)bt_cap_initiator_unregister_cb(&mock_cap_initiator_cb); + (void)bt_cap_handover_unregister_cb(&mock_cap_handover_cb); + + for (size_t i = 0; i < ARRAY_SIZE(fixture->conns); i++) { + mock_bt_conn_disconnected(&fixture->conns[i], BT_HCI_ERR_REMOTE_USER_TERM_CONN); + } + + /* In the case of a test failing, we cancel the procedure so that subsequent won't fail */ + (void)bt_cap_initiator_unicast_audio_cancel(); + + /* In the case of a test failing, we delete the group so that subsequent tests won't fail */ + if (fixture->unicast_group != NULL) { + struct bt_cap_stream *cap_stream_ptrs[MAX_STREAMS]; + + const struct bt_cap_unicast_audio_stop_param param = { + .type = BT_CAP_SET_TYPE_AD_HOC, + .count = ARRAY_SIZE(fixture->cap_streams), + .streams = cap_stream_ptrs, + .release = true, + }; + + ARRAY_FOR_EACH(cap_stream_ptrs, idx) { + cap_stream_ptrs[idx] = &fixture->cap_streams[idx]; + } + + (void)bt_cap_initiator_unicast_audio_stop(¶m); + (void)bt_cap_unicast_group_delete(fixture->unicast_group); + } + + /* If a broadcast source was create it exists as the 4th parameter in the callback */ + if (mock_unicast_to_broadcast_complete_cb_fake.arg3_history[0] != NULL) { + struct bt_cap_broadcast_source *broadcast_source = + mock_unicast_to_broadcast_complete_cb_fake.arg3_history[0]; + + (void)bt_cap_initiator_broadcast_audio_stop(broadcast_source); + (void)bt_cap_initiator_broadcast_audio_delete(broadcast_source); + } +} + +static void cap_handover_unicast_to_broadcast_test_suite_teardown(void *f) +{ + free(f); +} + +ZTEST_SUITE(cap_handover_unicast_to_broadcast_test_suite, NULL, + cap_handover_unicast_to_broadcast_test_suite_setup, + cap_handover_unicast_to_broadcast_test_suite_before, + cap_handover_unicast_to_broadcast_test_suite_after, + cap_handover_unicast_to_broadcast_test_suite_teardown); + +static void validate_handover_callback(void) +{ + zexpect_call_count("bt_cap_initiator_cb.unicast_to_broadcast_complete_cb", 1, + mock_unicast_to_broadcast_complete_cb_fake.call_count); + zassert_equal(0, mock_unicast_to_broadcast_complete_cb_fake.arg0_history[0]); + zassert_equal_ptr(NULL, mock_unicast_to_broadcast_complete_cb_fake.arg1_history[0]); + zassert_equal_ptr(NULL, mock_unicast_to_broadcast_complete_cb_fake.arg2_history[0]); + zassert_not_equal(NULL, mock_unicast_to_broadcast_complete_cb_fake.arg3_history[0]); +} + +static ZTEST_F(cap_handover_unicast_to_broadcast_test_suite, test_handover_unicast_to_broadcast) +{ + int err = 0; + + err = bt_cap_handover_unicast_to_broadcast(&fixture->unicast_to_broadcast_param); + zassert_equal(err, 0, "Unexpected return value %d", err); + validate_handover_callback(); + fixture->unicast_group = NULL; +} + +static ZTEST_F(cap_handover_unicast_to_broadcast_test_suite, + test_handover_unicast_to_broadcast_inactive_adv) +{ + int err = 0; + + fixture->unicast_to_broadcast_param.ext_adv->ext_adv_state = BT_LE_EXT_ADV_STATE_DISABLED; + + err = bt_cap_handover_unicast_to_broadcast(&fixture->unicast_to_broadcast_param); + zassert_equal(err, 0, "Unexpected return value %d", err); + fixture->unicast_group = NULL; +} + +static ZTEST_F(cap_handover_unicast_to_broadcast_test_suite, + test_handover_unicast_to_broadcast_inval_null_param) +{ + int err = 0; + + err = bt_cap_handover_unicast_to_broadcast(NULL); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_handover_unicast_to_broadcast_test_suite, + test_handover_unicast_to_broadcast_inval_null_unicast_group) +{ + int err = 0; + + fixture->unicast_to_broadcast_param.unicast_group = NULL; + + err = bt_cap_handover_unicast_to_broadcast(&fixture->unicast_to_broadcast_param); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_handover_unicast_to_broadcast_test_suite, + test_handover_unicast_to_broadcast_inval_null_ext_adv) +{ + int err = 0; + + fixture->unicast_to_broadcast_param.ext_adv = NULL; + + err = bt_cap_handover_unicast_to_broadcast(&fixture->unicast_to_broadcast_param); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_handover_unicast_to_broadcast_test_suite, + test_handover_unicast_to_broadcast_inval_ext_adv_no_pa) +{ + int err = 0; + + fixture->unicast_to_broadcast_param.ext_adv->per_adv_state = BT_LE_PER_ADV_STATE_NONE; + + err = bt_cap_handover_unicast_to_broadcast(&fixture->unicast_to_broadcast_param); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_handover_unicast_to_broadcast_test_suite, + test_handover_unicast_to_broadcast_inval_pa_interval) +{ + int err = 0; + + fixture->unicast_to_broadcast_param.pa_interval = 0U; + + err = bt_cap_handover_unicast_to_broadcast(&fixture->unicast_to_broadcast_param); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_handover_unicast_to_broadcast_test_suite, + test_handover_unicast_to_broadcast_inval_broadcast_id) +{ + int err = 0; + + fixture->unicast_to_broadcast_param.broadcast_id = 0xFFFFFFFFU; + + err = bt_cap_handover_unicast_to_broadcast(&fixture->unicast_to_broadcast_param); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_handover_unicast_to_broadcast_test_suite, + test_handover_unicast_to_broadcast_inval_null_broadcast_create_param) +{ + int err = 0; + + fixture->unicast_to_broadcast_param.broadcast_create_param = NULL; + + err = bt_cap_handover_unicast_to_broadcast(&fixture->unicast_to_broadcast_param); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_handover_unicast_to_broadcast_test_suite, + test_handover_unicast_to_broadcast_inval_broadcast_stream) +{ + struct bt_cap_stream cap_stream = {0}; + int err = 0; + + /* Attempt to use a stream not in the unicast group */ + fixture->broadcast_stream_params[0].stream = &cap_stream; + + err = bt_cap_handover_unicast_to_broadcast(&fixture->unicast_to_broadcast_param); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_handover_unicast_to_broadcast_test_suite, + test_handover_unicast_to_broadcast_inval_broadcast_stream_cnt) +{ + int err = 0; + + if (fixture->subgroup_params.stream_count == 1U) { + ztest_test_skip(); + } + + /* Attempt to not convert all sink streams */ + fixture->subgroup_params.stream_count--; + + err = bt_cap_handover_unicast_to_broadcast(&fixture->unicast_to_broadcast_param); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_handover_unicast_to_broadcast_test_suite, + test_handover_unicast_to_broadcast_inval_no_active_sink_streams) +{ + struct bt_cap_stream *cap_stream_ptrs[MAX_STREAMS]; + const struct bt_cap_unicast_audio_stop_param param = { + .type = BT_CAP_SET_TYPE_AD_HOC, + .count = fixture->subgroup_params.stream_count, + .streams = cap_stream_ptrs, + .release = false, + }; + int err = 0; + + for (size_t i = 0; i < param.count; i++) { + param.streams[i] = fixture->broadcast_stream_params[i].stream; + } + + /* Test that it will fail if there are no active sink streams */ + err = bt_cap_initiator_unicast_audio_stop(¶m); + zassert_equal(err, 0, "Unexpected return value %d", err); + + err = bt_cap_handover_unicast_to_broadcast(&fixture->unicast_to_broadcast_param); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_handover_unicast_to_broadcast_test_suite, + test_handover_unicast_to_broadcast_inval_unique_metadata) +{ + int err = 0; + + if (STREAMS_PER_DIRECTION <= 1) { + ztest_test_skip(); + } + + /* Make metadate unique per stream to require additional subgroups */ + ARRAY_FOR_EACH(fixture->cap_streams, i) { + fixture->cap_streams[i].bap_stream.codec_cfg->meta[0] = 3; /* length */ + fixture->cap_streams[i].bap_stream.codec_cfg->meta[1] = + BT_AUDIO_METADATA_TYPE_STREAM_CONTEXT; /* type */ + sys_put_le16(i + 1, + &fixture->cap_streams[i].bap_stream.codec_cfg->meta[2]); /* value */ + fixture->cap_streams[i].bap_stream.codec_cfg->meta_len = 4; + } + + err = bt_cap_handover_unicast_to_broadcast(&fixture->unicast_to_broadcast_param); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} + +static ZTEST_F(cap_handover_unicast_to_broadcast_test_suite, + test_handover_unicast_to_broadcast_inval_unicast_group) +{ + int err = 0; + + /* Attempt to use a stream with invalid unicast group */ + fixture->broadcast_stream_params[0].stream->bap_stream.group = UINT_TO_POINTER(0x12345678); + + err = bt_cap_handover_unicast_to_broadcast(&fixture->unicast_to_broadcast_param); + zassert_equal(err, -EINVAL, "Unexpected return value %d", err); +} diff --git a/tests/bluetooth/audio/cap_handover/testcase.yaml b/tests/bluetooth/audio/cap_handover/testcase.yaml new file mode 100644 index 0000000000000..1b9fafd0bedcc --- /dev/null +++ b/tests/bluetooth/audio/cap_handover/testcase.yaml @@ -0,0 +1,10 @@ +common: + tags: + - bluetooth + - bluetooth_audio +tests: + bluetooth.audio.cap_handover.test_default: + platform_allow: + - native_sim + integration_platforms: + - native_sim diff --git a/tests/bluetooth/audio/cap_handover/uut/bap_broadcast_assistant.c b/tests/bluetooth/audio/cap_handover/uut/bap_broadcast_assistant.c new file mode 100644 index 0000000000000..c4add24a42729 --- /dev/null +++ b/tests/bluetooth/audio/cap_handover/uut/bap_broadcast_assistant.c @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +static struct bt_bap_broadcast_assistant_cb *broadcast_assistant_cb; + +struct bap_broadcast_assistant_recv_state_info { + uint8_t src_id; + /** Cached PAST available */ + bool past_avail; + uint8_t adv_sid; + uint32_t broadcast_id; + bt_addr_le_t addr; +}; + +struct bap_broadcast_assistant_instance { + struct bt_conn *conn; + struct bap_broadcast_assistant_recv_state_info recv_state; + /* + * the following are not part of the broadcast_assistant instance, but adding them allow us + * to easily check pa_sync and bis_sync states + */ + enum bt_bap_pa_state pa_sync_state; + uint8_t num_subgroups; + struct bt_bap_bass_subgroup subgroups[CONFIG_BT_BAP_BASS_MAX_SUBGROUPS]; +}; + +static struct bap_broadcast_assistant_instance broadcast_assistants[CONFIG_BT_MAX_CONN]; + +int bt_bap_broadcast_assistant_register_cb(struct bt_bap_broadcast_assistant_cb *cb) +{ + broadcast_assistant_cb = cb; + + return 0; +} + +static struct bap_broadcast_assistant_instance *inst_by_conn(struct bt_conn *conn) +{ + struct bap_broadcast_assistant_instance *inst; + + __ASSERT(conn != NULL, "conn is NULL"); + + inst = &broadcast_assistants[bt_conn_index(conn)]; + + return inst; +} + +int bt_bap_broadcast_assistant_add_src(struct bt_conn *conn, + const struct bt_bap_broadcast_assistant_add_src_param *param) +{ + struct bap_broadcast_assistant_instance *inst; + struct bt_bap_scan_delegator_recv_state state; + + /* Note that proper parameter checking is done in the caller */ + __ASSERT(conn != NULL, "conn is NULL"); + __ASSERT(param != NULL, "param is NULL"); + + inst = inst_by_conn(conn); + __ASSERT(inst != NULL, "inst is NULL"); + + inst->recv_state.src_id = 1U; + inst->recv_state.past_avail = false; + inst->recv_state.adv_sid = param->adv_sid; + inst->recv_state.broadcast_id = param->broadcast_id; + inst->pa_sync_state = param->pa_sync; + inst->num_subgroups = param->num_subgroups; + state.pa_sync_state = param->pa_sync; + state.src_id = inst->recv_state.src_id; + state.num_subgroups = param->num_subgroups; + for (size_t i = 0; i < param->num_subgroups; i++) { + state.subgroups[i].bis_sync = param->subgroups[i].bis_sync; + inst->subgroups[i].bis_sync = param->subgroups[i].bis_sync; + } + + bt_addr_le_copy(&inst->recv_state.addr, ¶m->addr); + + if (broadcast_assistant_cb != NULL) { + if (broadcast_assistant_cb->add_src != NULL) { + broadcast_assistant_cb->add_src(conn, 0); + } + if (broadcast_assistant_cb->recv_state != NULL) { + broadcast_assistant_cb->recv_state(conn, 0, &state); + } + } + + return 0; +} + +int bt_bap_broadcast_assistant_mod_src(struct bt_conn *conn, + const struct bt_bap_broadcast_assistant_mod_src_param *param) +{ + return 0; +} + +int bt_bap_broadcast_assistant_set_broadcast_code( + struct bt_conn *conn, uint8_t src_id, + const uint8_t broadcast_code[BT_ISO_BROADCAST_CODE_SIZE]) +{ + return 0; +} + +int bt_bap_broadcast_assistant_rem_src(struct bt_conn *conn, uint8_t src_id) +{ + return 0; +} diff --git a/tests/bluetooth/audio/cap_handover/uut/bap_unicast_client.c b/tests/bluetooth/audio/cap_handover/uut/bap_unicast_client.c new file mode 100644 index 0000000000000..7a57dcf81103b --- /dev/null +++ b/tests/bluetooth/audio/cap_handover/uut/bap_unicast_client.c @@ -0,0 +1,600 @@ +/* + * 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 "audio/bap_endpoint.h" +#include "audio/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_unicast_client_has_ep(const struct bt_bap_ep *ep) +{ + return true; +} + +int bt_bap_unicast_client_config(struct bt_bap_stream *stream, + const struct bt_audio_codec_cfg *codec_cfg) +{ + if (stream == NULL || stream->ep == NULL || codec_cfg == NULL) { + return -EINVAL; + } + + switch (stream->ep->state) { + case BT_BAP_EP_STATE_IDLE: + case BT_BAP_EP_STATE_CODEC_CONFIGURED: + break; + default: + return -EINVAL; + } + + if (unicast_client_cb != NULL && unicast_client_cb->config != NULL) { + unicast_client_cb->config(stream, BT_BAP_ASCS_RSP_CODE_SUCCESS, + BT_BAP_ASCS_REASON_NONE); + } + + stream->ep->state = BT_BAP_EP_STATE_CODEC_CONFIGURED; + + if (stream->ops != NULL && stream->ops->configured != NULL) { + const struct bt_bap_qos_cfg_pref pref = {0}; + + stream->ops->configured(stream, &pref); + } + + return 0; +} + +int bt_bap_unicast_client_qos(struct bt_conn *conn, struct bt_bap_unicast_group *group) +{ + struct bt_bap_stream *stream; + + if (conn == NULL || group == NULL) { + return -EINVAL; + } + + SYS_SLIST_FOR_EACH_CONTAINER(&group->streams, stream, _node) { + + if (stream->conn == conn) { + struct bt_bap_ep *ep; + + ep = stream->ep; + if (ep == NULL) { + return -EINVAL; + } + + switch (ep->state) { + case BT_BAP_EP_STATE_CODEC_CONFIGURED: + case BT_BAP_EP_STATE_QOS_CONFIGURED: + break; + default: + return -EINVAL; + } + } + } + + SYS_SLIST_FOR_EACH_CONTAINER(&group->streams, stream, _node) { + if (stream->conn == conn) { + if (unicast_client_cb != NULL && unicast_client_cb->qos != NULL) { + unicast_client_cb->qos(stream, BT_BAP_ASCS_RSP_CODE_SUCCESS, + BT_BAP_ASCS_REASON_NONE); + } + + stream->ep->state = BT_BAP_EP_STATE_QOS_CONFIGURED; + if (stream->ep->iso == NULL) { + struct bt_bap_iso *bap_iso = + CONTAINER_OF(stream->iso, struct bt_bap_iso, chan); + + /* Not yet bound with the bap_iso */ + bt_bap_iso_bind_ep(bap_iso, stream->ep); + } + + if (stream->ops != NULL && stream->ops->qos_set != NULL) { + stream->ops->qos_set(stream); + } + } + } + + return 0; +} + +int bt_bap_unicast_client_enable(struct bt_bap_stream *stream, const uint8_t meta[], + size_t meta_len) +{ + if (stream == NULL || stream->ep == NULL) { + return -EINVAL; + } + + if (stream->ep->state != BT_BAP_EP_STATE_QOS_CONFIGURED) { + return -EINVAL; + } + + if (unicast_client_cb != NULL && unicast_client_cb->enable != NULL) { + unicast_client_cb->enable(stream, BT_BAP_ASCS_RSP_CODE_SUCCESS, + BT_BAP_ASCS_REASON_NONE); + } + + stream->ep->state = BT_BAP_EP_STATE_ENABLING; + + if (stream->ops != NULL && stream->ops->enabled != NULL) { + stream->ops->enabled(stream); + } + + return 0; +} + +int bt_bap_unicast_client_metadata(struct bt_bap_stream *stream, const uint8_t meta[], + size_t meta_len) +{ + if (stream == NULL || stream->ep == NULL) { + return -EINVAL; + } + + switch (stream->ep->state) { + case BT_BAP_EP_STATE_ENABLING: + case BT_BAP_EP_STATE_STREAMING: + break; + default: + return -EINVAL; + } + + if (unicast_client_cb != NULL && unicast_client_cb->metadata != NULL) { + unicast_client_cb->metadata(stream, BT_BAP_ASCS_RSP_CODE_SUCCESS, + BT_BAP_ASCS_REASON_NONE); + } + + if (stream->ops != NULL && stream->ops->metadata_updated != NULL) { + stream->ops->metadata_updated(stream); + } + + return 0; +} + +int bt_bap_unicast_client_connect(struct bt_bap_stream *stream) +{ + struct bt_bap_ep *ep; + + if (stream == NULL) { + return -EINVAL; + } + + ep = stream->ep; + __ASSERT_NO_MSG(ep != NULL && ep->iso != NULL); + + switch (ep->state) { + case BT_BAP_EP_STATE_QOS_CONFIGURED: + case BT_BAP_EP_STATE_ENABLING: + break; + default: + return -EINVAL; + } + + ep->iso->chan.state = BT_ISO_STATE_CONNECTED; + if (stream->ops != NULL && stream->ops->connected != NULL) { + stream->ops->connected(stream); + } + + if (ep->dir == BT_AUDIO_DIR_SINK) { + /* Mocking that the unicast server automatically starts the stream */ + ep->state = BT_BAP_EP_STATE_STREAMING; + + if (stream->ops != NULL && stream->ops->started != NULL) { + stream->ops->started(stream); + } + } + + return 0; +} + +int bt_bap_unicast_client_start(struct bt_bap_stream *stream) +{ + /* As per the ASCS spec, only source streams can be started by the client */ + if (stream == NULL || stream->ep == NULL || stream->ep->dir == BT_AUDIO_DIR_SINK) { + return -EINVAL; + } + + if (stream->ep->state != BT_BAP_EP_STATE_ENABLING) { + return -EINVAL; + } + + if (unicast_client_cb != NULL && unicast_client_cb->start != NULL) { + unicast_client_cb->start(stream, BT_BAP_ASCS_RSP_CODE_SUCCESS, + BT_BAP_ASCS_REASON_NONE); + } + + stream->ep->state = BT_BAP_EP_STATE_STREAMING; + + if (stream->ops != NULL && stream->ops->started != NULL) { + stream->ops->started(stream); + } + + return 0; +} + +int bt_bap_unicast_client_disable(struct bt_bap_stream *stream) +{ + if (stream == NULL || stream->ep == NULL) { + return -EINVAL; + } + + switch (stream->ep->state) { + case BT_BAP_EP_STATE_ENABLING: + case BT_BAP_EP_STATE_STREAMING: + break; + default: + return -EINVAL; + } + + /* Even though the ASCS spec does not have the disabling state for sink ASEs, the unicast + * client implementation fakes the behavior of it and always calls the disabled callback + * when leaving the streaming state in a non-release manner + */ + + if (unicast_client_cb != NULL && unicast_client_cb->disable != NULL) { + unicast_client_cb->disable(stream, BT_BAP_ASCS_RSP_CODE_SUCCESS, + BT_BAP_ASCS_REASON_NONE); + } + + /* Disabled sink ASEs go directly to the QoS configured state */ + if (stream->ep->dir == BT_AUDIO_DIR_SINK) { + stream->ep->state = BT_BAP_EP_STATE_QOS_CONFIGURED; + + if (stream->ops != NULL && stream->ops->disabled != NULL) { + stream->ops->disabled(stream); + } + + if (stream->ops != NULL && stream->ops->stopped != NULL) { + stream->ops->stopped(stream, BT_HCI_ERR_LOCALHOST_TERM_CONN); + } + + if (stream->ops != NULL && stream->ops->qos_set != NULL) { + stream->ops->qos_set(stream); + } + } else if (stream->ep->dir == BT_AUDIO_DIR_SOURCE) { + stream->ep->state = BT_BAP_EP_STATE_DISABLING; + + if (stream->ops != NULL && stream->ops->disabled != NULL) { + stream->ops->disabled(stream); + } + } else { + __ASSERT(false, "Invalid stream->ep->dir %d", stream->ep->dir); + } + + return 0; +} + +int bt_bap_unicast_client_stop(struct bt_bap_stream *stream) +{ + /* As per the ASCS spec, only source streams can be stopped by the client */ + if (stream == NULL || stream->ep == NULL || stream->ep->dir == BT_AUDIO_DIR_SINK) { + return -EINVAL; + } + + if (stream->ep->state != BT_BAP_EP_STATE_DISABLING) { + return -EINVAL; + } + + if (unicast_client_cb != NULL && unicast_client_cb->stop != NULL) { + unicast_client_cb->stop(stream, BT_BAP_ASCS_RSP_CODE_SUCCESS, + BT_BAP_ASCS_REASON_NONE); + } + + stream->ep->state = BT_BAP_EP_STATE_QOS_CONFIGURED; + + if (stream->ops != NULL && stream->ops->stopped != NULL) { + stream->ops->stopped(stream, BT_HCI_ERR_LOCALHOST_TERM_CONN); + } + + if (stream->ops != NULL && stream->ops->qos_set != NULL) { + stream->ops->qos_set(stream); + } + + /* If the stream can be disconnected, BAP will disconnect the stream once it reaches the + * QoS Configured state. We simulate that behavior here, and if the stream is disconnected, + * then the Unicast Server will set any paired stream to the QoS Configured state + * autonomously as well. + */ + if (bt_bap_stream_can_disconnect(stream)) { + struct bt_bap_ep *pair_ep = bt_bap_iso_get_paired_ep(stream->ep); + + if (pair_ep != NULL && pair_ep->stream != NULL) { + struct bt_bap_stream *pair_stream = pair_ep->stream; + + pair_stream->ep->state = BT_BAP_EP_STATE_QOS_CONFIGURED; + + if (pair_stream->ops != NULL && pair_stream->ops->stopped != NULL) { + pair_stream->ops->stopped(pair_stream, + BT_HCI_ERR_LOCALHOST_TERM_CONN); + } + + if (pair_stream->ops != NULL && pair_stream->ops->qos_set != NULL) { + pair_stream->ops->qos_set(pair_stream); + } + } + } + + return 0; +} + +int bt_bap_unicast_client_release(struct bt_bap_stream *stream) +{ + if (stream == NULL || stream->ep == NULL) { + return -EINVAL; + } + + switch (stream->ep->state) { + case BT_BAP_EP_STATE_CODEC_CONFIGURED: + case BT_BAP_EP_STATE_QOS_CONFIGURED: + case BT_BAP_EP_STATE_ENABLING: + case BT_BAP_EP_STATE_STREAMING: + case BT_BAP_EP_STATE_DISABLING: + break; + default: + return -EINVAL; + } + + if (unicast_client_cb != NULL && unicast_client_cb->release != NULL) { + unicast_client_cb->release(stream, BT_BAP_ASCS_RSP_CODE_SUCCESS, + BT_BAP_ASCS_REASON_NONE); + } + + stream->ep->state = BT_BAP_EP_STATE_IDLE; + bt_bap_stream_reset(stream); + + if (stream->ops != NULL && stream->ops->released != NULL) { + stream->ops->released(stream); + } + + return 0; +} + +int bt_bap_unicast_client_register_cb(struct bt_bap_unicast_client_cb *cb) +{ + unicast_client_cb = cb; + + return 0; +} + +struct bt_bap_iso *bt_bap_unicast_client_new_audio_iso(void) +{ + static struct bt_iso_chan_ops unicast_client_iso_ops; + struct bt_bap_iso *bap_iso; + + bap_iso = bt_bap_iso_new(); + if (bap_iso == NULL) { + return NULL; + } + + bt_bap_iso_init(bap_iso, &unicast_client_iso_ops); + + return bap_iso; +} + +static int unicast_group_add_iso(struct bt_bap_unicast_group *group, struct bt_bap_iso *iso) +{ + struct bt_iso_chan **chan_slot = NULL; + + __ASSERT_NO_MSG(group != NULL); + __ASSERT_NO_MSG(iso != NULL); + + /* Append iso channel to the group->cis array */ + for (size_t i = 0U; i < ARRAY_SIZE(group->cis); i++) { + /* Return if already there */ + if (group->cis[i] == &iso->chan) { + return 0; + } + + if (chan_slot == NULL && group->cis[i] == NULL) { + chan_slot = &group->cis[i]; + } + } + + if (chan_slot == NULL) { + return -ENOMEM; + } + + *chan_slot = &iso->chan; + + return 0; +} + +static void unicast_group_add_stream(struct bt_bap_unicast_group *group, + struct bt_bap_unicast_group_stream_param *param, + struct bt_bap_iso *iso, enum bt_audio_dir dir) +{ + struct bt_bap_stream *stream = param->stream; + struct bt_bap_qos_cfg *qos = param->qos; + + __ASSERT_NO_MSG(stream->ep == NULL || (stream->ep != NULL && stream->ep->iso == NULL)); + + stream->qos = qos; + stream->group = group; + printk("stream %p group %p\n", stream, group); + + /* iso initialized already */ + bt_bap_iso_bind_stream(iso, stream, dir); + if (stream->ep != NULL) { + bt_bap_iso_bind_ep(iso, stream->ep); + } + + sys_slist_append(&group->streams, &stream->_node); +} + +static int unicast_group_add_stream_pair(struct bt_bap_unicast_group *group, + struct bt_bap_unicast_group_stream_pair_param *param) +{ + struct bt_bap_iso *iso; + int err; + + __ASSERT_NO_MSG(group != NULL); + __ASSERT_NO_MSG(param != NULL); + __ASSERT_NO_MSG(param->rx_param != NULL || param->tx_param != NULL); + + iso = bt_bap_unicast_client_new_audio_iso(); + if (iso == NULL) { + printk("A\n"); + return -ENOMEM; + } + + err = unicast_group_add_iso(group, iso); + if (err < 0) { + printk("B\n"); + bt_bap_iso_unref(iso); + return err; + } + + if (param->rx_param != NULL) { + unicast_group_add_stream(group, param->rx_param, iso, BT_AUDIO_DIR_SOURCE); + } + + if (param->tx_param != NULL) { + unicast_group_add_stream(group, param->tx_param, iso, BT_AUDIO_DIR_SINK); + } + + bt_bap_iso_unref(iso); + + 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++) { + struct bt_bap_unicast_group_stream_pair_param *stream_param; + int err; + + stream_param = ¶m->params[i]; + + err = unicast_group_add_stream_pair(*unicast_group, stream_param); + __ASSERT_NO_MSG(err == 0); + } + + 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; +} + +static void unicast_group_free(struct bt_bap_unicast_group *group) +{ + struct bt_bap_stream *stream, *next; + + __ASSERT_NO_MSG(group != NULL); + + SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&group->streams, stream, next, _node) { + struct bt_bap_iso *bap_iso = CONTAINER_OF(stream->iso, struct bt_bap_iso, chan); + struct bt_bap_ep *ep = stream->ep; + + stream->group = NULL; + if (bap_iso != NULL) { + if (bap_iso->rx.stream == stream) { + bt_bap_iso_unbind_stream(stream, BT_AUDIO_DIR_SOURCE); + } else if (bap_iso->tx.stream == stream) { + bt_bap_iso_unbind_stream(stream, BT_AUDIO_DIR_SINK); + } else { + __ASSERT_PRINT("stream %p has invalid bap_iso %p", stream, bap_iso); + } + } + + if (ep != NULL && ep->iso != NULL) { + bt_bap_iso_unbind_ep(ep->iso, ep); + } + + sys_slist_remove(&group->streams, NULL, &stream->_node); + } + + group->allocated = false; +} + +int bt_bap_unicast_group_delete(struct bt_bap_unicast_group *unicast_group) +{ + if (unicast_group == NULL) { + return -EINVAL; + } + + unicast_group->allocated = false; + + unicast_group_free(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) { + 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/cap_handover/uut/cap_handover.c b/tests/bluetooth/audio/cap_handover/uut/cap_handover.c new file mode 100644 index 0000000000000..2ea442430ba57 --- /dev/null +++ b/tests/bluetooth/audio/cap_handover/uut/cap_handover.c @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#include "cap_handover.h" + +/* List of fakes used by this unit tester */ +#define FFF_FAKES_LIST(FAKE) FAKE(mock_unicast_to_broadcast_complete_cb) + +DEFINE_FAKE_VOID_FUNC(mock_unicast_to_broadcast_complete_cb, int, struct bt_conn *, + struct bt_cap_unicast_group *, struct bt_cap_broadcast_source *); + +const struct bt_cap_handover_cb mock_cap_handover_cb = { + .unicast_to_broadcast_complete = mock_unicast_to_broadcast_complete_cb, +}; + +void mock_cap_handover_init(void) +{ + FFF_FAKES_LIST(RESET_FAKE); +} diff --git a/tests/bluetooth/audio/cap_handover/uut/cap_initiator.c b/tests/bluetooth/audio/cap_handover/uut/cap_initiator.c new file mode 100644 index 0000000000000..ce0a6449424ad --- /dev/null +++ b/tests/bluetooth/audio/cap_handover/uut/cap_initiator.c @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#include "cap_initiator.h" + +/* List of fakes used by this unit tester */ +#define FFF_FAKES_LIST(FAKE) FAKE(mock_unicast_start_complete_cb) + +DEFINE_FAKE_VOID_FUNC(mock_unicast_start_complete_cb, int, struct bt_conn *); + +const struct bt_cap_initiator_cb mock_cap_initiator_cb = { + .unicast_start_complete = mock_unicast_start_complete_cb, +}; + +void mock_cap_initiator_init(void) +{ + FFF_FAKES_LIST(RESET_FAKE); +} diff --git a/tests/bluetooth/audio/cap_handover/uut/csip.c b/tests/bluetooth/audio/cap_handover/uut/csip.c new file mode 100644 index 0000000000000..be113fac6b1d5 --- /dev/null +++ b/tests/bluetooth/audio/cap_handover/uut/csip.c @@ -0,0 +1,72 @@ +/* csip.c - CAP initiator specific CSIP mocks */ + +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include + +#include +#include + +static struct bt_csip_set_coordinator_cb *csip_cb; + +struct bt_csip_set_coordinator_svc_inst { + struct bt_conn *conn; + struct bt_csip_set_coordinator_set_info *set_info; +} svc_inst; + +static struct bt_csip_set_coordinator_set_member member = { + .insts = { + { + .info = { + .set_size = 2U, + .rank = 1U, + .lockable = false, + }, + .svc_inst = &svc_inst, + }, + }, +}; + +struct bt_csip_set_coordinator_csis_inst * +bt_csip_set_coordinator_csis_inst_by_handle(struct bt_conn *conn, uint16_t start_handle) +{ + return &member.insts[0]; +} + +int bt_csip_set_coordinator_register_cb(struct bt_csip_set_coordinator_cb *cb) +{ + csip_cb = cb; + + return 0; +} + +int bt_csip_set_coordinator_discover(struct bt_conn *conn) +{ + if (csip_cb != NULL) { + svc_inst.conn = conn; + svc_inst.set_info = &member.insts[0].info; + csip_cb->discover(conn, &member, 0, 1); + } + + return 0; +} + +struct bt_csip_set_coordinator_set_member * +bt_csip_set_coordinator_set_member_by_conn(const struct bt_conn *conn) +{ + if (conn == NULL) { + return NULL; + } + + return &member; +} + +void mock_bt_csip_cleanup(void) +{ + csip_cb = NULL; +} diff --git a/tests/bluetooth/audio/mocks/CMakeLists.txt b/tests/bluetooth/audio/mocks/CMakeLists.txt index f5f8c0933b69b..a587755d8244b 100644 --- a/tests/bluetooth/audio/mocks/CMakeLists.txt +++ b/tests/bluetooth/audio/mocks/CMakeLists.txt @@ -9,6 +9,7 @@ # add_library(mocks STATIC + src/adv.c src/assert.c src/bap_stream.c src/conn.c diff --git a/tests/bluetooth/audio/mocks/include/bluetooth.h b/tests/bluetooth/audio/mocks/include/bluetooth.h index b95199581fdbf..97cdbe2d67a77 100644 --- a/tests/bluetooth/audio/mocks/include/bluetooth.h +++ b/tests/bluetooth/audio/mocks/include/bluetooth.h @@ -8,12 +8,20 @@ #define MOCKS_BLUETOOTH_H_ #include +#include + struct bt_le_ext_adv { /* ID Address used for advertising */ uint8_t id; /* Advertising handle */ uint8_t handle; + + /** Extended advertising state */ + enum bt_le_ext_adv_state ext_adv_state; + + /** Periodic advertising state */ + enum bt_le_per_adv_state per_adv_state; }; #endif /* MOCKS_BLUETOOTH_H_ */ diff --git a/tests/bluetooth/audio/mocks/src/adv.c b/tests/bluetooth/audio/mocks/src/adv.c new file mode 100644 index 0000000000000..d75b3583f61a6 --- /dev/null +++ b/tests/bluetooth/audio/mocks/src/adv.c @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include + +#include "bluetooth.h" + +int bt_le_ext_adv_get_info(const struct bt_le_ext_adv *adv, struct bt_le_ext_adv_info *info) +{ + static bt_addr_le_t adv_addr = { + .type = BT_ADDR_LE_RANDOM, + .a.val = {1, 2, 3, 4, 5, 6}, + }; + + if (adv == NULL) { + return -EINVAL; + } + + if (info == NULL) { + return -EINVAL; + } + + (void)memset(info, 0, sizeof(*info)); + + info->id = 0; + info->tx_power = 0; + info->addr = &adv_addr; + info->ext_adv_state = adv->ext_adv_state; + info->per_adv_state = adv->per_adv_state; + + return 0; +} + +int bt_le_ext_adv_start(struct bt_le_ext_adv *adv, const struct bt_le_ext_adv_start_param *param) +{ + if (adv == NULL) { + return -EINVAL; + } + + adv->ext_adv_state = BT_LE_EXT_ADV_STATE_ENABLED; + + return 0; +}