diff --git a/tests/bluetooth/tester/overlay-le-audio.conf b/tests/bluetooth/tester/overlay-le-audio.conf index e6ada545978..e22151c9c57 100644 --- a/tests/bluetooth/tester/overlay-le-audio.conf +++ b/tests/bluetooth/tester/overlay-le-audio.conf @@ -166,3 +166,6 @@ CONFIG_BT_TBS_SUPPORTED_FEATURES=3 # TMAP CONFIG_BT_TMAP=y + +# PBP +CONFIG_BT_PBP=y diff --git a/tests/bluetooth/tester/src/audio/CMakeLists.txt b/tests/bluetooth/tester/src/audio/CMakeLists.txt index 072819eeca4..d6f2833d3da 100644 --- a/tests/bluetooth/tester/src/audio/CMakeLists.txt +++ b/tests/bluetooth/tester/src/audio/CMakeLists.txt @@ -67,3 +67,7 @@ endif() if(CONFIG_BT_TMAP) target_sources(app PRIVATE btp_tmap.c) endif() + +if(CONFIG_BT_PBP) + target_sources(app PRIVATE btp_pbp.c) +endif() diff --git a/tests/bluetooth/tester/src/audio/btp/btp_pbp.h b/tests/bluetooth/tester/src/audio/btp/btp_pbp.h new file mode 100644 index 00000000000..614d25e8296 --- /dev/null +++ b/tests/bluetooth/tester/src/audio/btp/btp_pbp.h @@ -0,0 +1,46 @@ +/* btp_pbp.c - Bluetooth PBP Tester */ + +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* This is main.h */ + +#define BTP_PBP_READ_SUPPORTED_COMMANDS 0x01 +struct btp_pbp_read_supported_commands_rp { + uint8_t data[0]; +} __packed; + +#define BTP_PBP_SET_PUBLIC_BROADCAST_ANNOUNCEMENT 0x02 +struct btp_pbp_set_public_broadcast_announcement_cmd { + uint8_t features; + uint8_t metadata_len; + uint8_t metadata[]; +} __packed; + +#define BTP_PBP_SET_BROADCAST_NAME 0x03 +struct btp_pbp_set_broadcast_name_cmd { + uint8_t name_len; + uint8_t name[]; +} __packed; + +#define BTP_PBP_BROADCAST_SCAN_START 0x04 +struct btp_pbp_broadcast_scan_start_cmd { +} __packed; + +#define BTP_PBP_BROADCAST_SCAN_STOP 0x05 +struct btp_pbp_broadcast_scan_stop_cmd { +} __packed; + +#define BTP_PBP_EV_PUBLIC_BROADCAST_ANOUNCEMENT_FOUND 0x80 +struct btp_pbp_ev_public_broadcast_anouncement_found_rp { + bt_addr_le_t address; + uint8_t broadcast_id[BT_AUDIO_BROADCAST_ID_SIZE]; + uint8_t advertiser_sid; + uint16_t padv_interval; + uint8_t pba_features; + uint8_t broadcast_name_len; + uint8_t broadcast_name[]; +} __packed; diff --git a/tests/bluetooth/tester/src/audio/btp_cap.c b/tests/bluetooth/tester/src/audio/btp_cap.c index 6563a761e7e..e0356aae097 100644 --- a/tests/bluetooth/tester/src/audio/btp_cap.c +++ b/tests/bluetooth/tester/src/audio/btp_cap.c @@ -533,40 +533,37 @@ static int cap_broadcast_source_adv_setup(struct btp_bap_broadcast_local_source uint32_t *gap_settings) { int err; - struct bt_le_adv_param param = *BT_LE_EXT_ADV_NCONN; - uint32_t broadcast_id; - NET_BUF_SIMPLE_DEFINE(ad_buf, BT_UUID_SIZE_16 + BT_AUDIO_BROADCAST_ID_SIZE); NET_BUF_SIMPLE_DEFINE(base_buf, 128); /* Broadcast Audio Streaming Endpoint advertising data */ - struct bt_data base_ad[2]; struct bt_data per_ad; - err = bt_rand(&broadcast_id, BT_AUDIO_BROADCAST_ID_SIZE); - if (err) { - printk("Unable to generate broadcast ID: %d\n", err); - - return -EINVAL; - } - - *gap_settings = BIT(BTP_GAP_SETTINGS_DISCOVERABLE) | - BIT(BTP_GAP_SETTINGS_EXTENDED_ADVERTISING); - /* Setup extended advertising data */ - net_buf_simple_add_le16(&ad_buf, BT_UUID_BROADCAST_AUDIO_VAL); - net_buf_simple_add_le24(&ad_buf, source->broadcast_id); - base_ad[0].type = BT_DATA_SVC_DATA16; - base_ad[0].data_len = ad_buf.len; - base_ad[0].data = ad_buf.data; - base_ad[1].type = BT_DATA_NAME_COMPLETE; - base_ad[1].data_len = sizeof(CONFIG_BT_DEVICE_NAME) - 1; - base_ad[1].data = CONFIG_BT_DEVICE_NAME; - err = tester_gap_create_adv_instance(¶m, BTP_GAP_ADDR_TYPE_IDENTITY, base_ad, 2, NULL, - 0, gap_settings); - if (err != 0) { - LOG_DBG("Failed to create extended advertising instance: %d", err); - - return -EINVAL; + /* A more specialized adv instance may already have been created by another btp module */ + if (tester_gap_ext_adv_get() == NULL) { + struct bt_le_adv_param param = *BT_LE_EXT_ADV_NCONN; + struct bt_data base_ad[2]; + + NET_BUF_SIMPLE_DEFINE(ad_buf, BT_UUID_SIZE_16 + BT_AUDIO_BROADCAST_ID_SIZE); + *gap_settings = BIT(BTP_GAP_SETTINGS_DISCOVERABLE) | + BIT(BTP_GAP_SETTINGS_EXTENDED_ADVERTISING); + /* Setup extended advertising data */ + net_buf_simple_add_le16(&ad_buf, BT_UUID_BROADCAST_AUDIO_VAL); + net_buf_simple_add_le24(&ad_buf, source->broadcast_id); + base_ad[0].type = BT_DATA_SVC_DATA16; + base_ad[0].data_len = ad_buf.len; + base_ad[0].data = ad_buf.data; + base_ad[1].type = BT_DATA_NAME_COMPLETE; + base_ad[1].data_len = sizeof(CONFIG_BT_DEVICE_NAME) - 1; + base_ad[1].data = CONFIG_BT_DEVICE_NAME; + + err = tester_gap_create_adv_instance(¶m, BTP_GAP_ADDR_TYPE_IDENTITY, base_ad, + 2, NULL, 0, gap_settings); + if (err != 0) { + LOG_DBG("Failed to create extended advertising instance: %d", err); + + return -EINVAL; + } } err = tester_gap_padv_configure(BT_LE_PER_ADV_PARAM(BT_GAP_PER_ADV_FAST_INT_MIN_2, diff --git a/tests/bluetooth/tester/src/audio/btp_pbp.c b/tests/bluetooth/tester/src/audio/btp_pbp.c new file mode 100644 index 00000000000..d3acfcbd807 --- /dev/null +++ b/tests/bluetooth/tester/src/audio/btp_pbp.c @@ -0,0 +1,287 @@ +/* btp_pbp.c - Bluetooth PBP Tester */ + +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "btp/btp.h" + +#include +#include +#include +#include +#define LOG_MODULE_NAME bttester_pbp +LOG_MODULE_REGISTER(LOG_MODULE_NAME, CONFIG_BTTESTER_LOG_LEVEL); + +#define PBP_EXT_ADV_METADATA_LEN_MAX 128 + +static uint8_t pbp_features_cached; +static uint8_t pbp_metadata_cached[PBP_EXT_ADV_METADATA_LEN_MAX]; +static uint8_t pbp_metadata_cached_len; +static uint8_t pbp_broadcast_name_cached[BT_AUDIO_BROADCAST_NAME_LEN_MAX]; +static uint8_t pbp_name_cached_len; + +static bool scan_get_broadcast_name_len(struct bt_data *data, void *user_data) +{ + uint8_t *broadcast_name_len = user_data; + + switch (data->type) { + case BT_DATA_BROADCAST_NAME: + *broadcast_name_len = data->data_len; + return false; + default: + return true; + } +} + +static bool scan_get_data(struct bt_data *data, void *user_data) +{ + enum bt_pbp_announcement_feature source_features; + uint32_t broadcast_id; + uint8_t *metadata; + struct bt_uuid_16 adv_uuid; + struct btp_pbp_ev_public_broadcast_anouncement_found_rp *ev = user_data; + + switch (data->type) { + case BT_DATA_BROADCAST_NAME: + ev->broadcast_name_len = data->data_len; + memcpy(ev->broadcast_name, data->data, data->data_len); + return true; + case BT_DATA_SVC_DATA16: + if (!bt_uuid_create(&adv_uuid.uuid, data->data, BT_UUID_SIZE_16)) { + return true; + } + if (!bt_uuid_cmp(&adv_uuid.uuid, BT_UUID_BROADCAST_AUDIO)) { + broadcast_id = sys_get_le24(data->data + BT_UUID_SIZE_16); + sys_put_le24(broadcast_id, ev->broadcast_id); + return true; + } + + int ret = bt_pbp_parse_announcement(data, &source_features, &metadata); + + if (ret >= 0) { + ev->pba_features = source_features; + return true; + } + + return true; + default: + return true; + } +} + +static void pbp_scan_recv(const struct bt_le_scan_recv_info *info, struct net_buf_simple *ad) +{ + if ((info->adv_props & BT_GAP_ADV_PROP_CONNECTABLE) || + !(info->adv_props & BT_GAP_ADV_PROP_EXT_ADV) || info->interval == 0) { + return; + } + + uint8_t broadcast_name_len = 0; + struct net_buf_simple ad_copy; + + /* Initial parse to determine broadcast_name_len before allocating memory */ + net_buf_simple_clone(ad, &ad_copy); + bt_data_parse(&ad_copy, scan_get_broadcast_name_len, &broadcast_name_len); + + struct btp_pbp_ev_public_broadcast_anouncement_found_rp *ev_ptr; + + tester_rsp_buffer_lock(); + tester_rsp_buffer_allocate(sizeof(*ev_ptr) + broadcast_name_len, (uint8_t **)&ev_ptr); + + sys_put_le24(BT_BAP_INVALID_BROADCAST_ID, ev_ptr->broadcast_id); + ev_ptr->pba_features = 0U; + ev_ptr->broadcast_name_len = 0U; + + bt_addr_le_copy(&ev_ptr->address, info->addr); + ev_ptr->advertiser_sid = info->sid; + ev_ptr->padv_interval = info->interval; + bt_data_parse(ad, scan_get_data, ev_ptr); + + if (sys_get_le24(ev_ptr->broadcast_id) != BT_BAP_INVALID_BROADCAST_ID && + ev_ptr->pba_features != 0U && ev_ptr->broadcast_name_len > 0) { + tester_event(BTP_SERVICE_ID_PBP, BTP_PBP_EV_PUBLIC_BROADCAST_ANOUNCEMENT_FOUND, + ev_ptr, sizeof(*ev_ptr) + broadcast_name_len); + } + + tester_rsp_buffer_free(); + tester_rsp_buffer_unlock(); +} + +static struct bt_le_scan_cb pbp_scan_cb = { + .recv = pbp_scan_recv, +}; + +static uint8_t pbp_read_supported_commands(const void *cmd, uint16_t cmd_len, void *rsp, + uint16_t *rsp_len) +{ + struct btp_pbp_read_supported_commands_rp *rp = rsp; + + tester_set_bit(rp->data, BTP_PBP_READ_SUPPORTED_COMMANDS); + tester_set_bit(rp->data, BTP_PBP_SET_PUBLIC_BROADCAST_ANNOUNCEMENT); + tester_set_bit(rp->data, BTP_PBP_SET_BROADCAST_NAME); + tester_set_bit(rp->data, BTP_PBP_BROADCAST_SCAN_START); + tester_set_bit(rp->data, BTP_PBP_BROADCAST_SCAN_STOP); + + *rsp_len = sizeof(*rp) + 1; + + return BTP_STATUS_SUCCESS; +} + +static int pbp_broadcast_source_adv_setup(void) +{ + struct bt_le_adv_param param = + BT_LE_ADV_PARAM_INIT(0, BT_GAP_ADV_FAST_INT_MIN_2, BT_GAP_ADV_FAST_INT_MAX_2, NULL); + uint32_t gap_settings = BIT(BTP_GAP_SETTINGS_DISCOVERABLE) | + BIT(BTP_GAP_SETTINGS_EXTENDED_ADVERTISING); + uint32_t broadcast_id; + + NET_BUF_SIMPLE_DEFINE(ad_buf, BT_UUID_SIZE_16 + BT_AUDIO_BROADCAST_ID_SIZE); + NET_BUF_SIMPLE_DEFINE(pba_buf, BT_PBP_MIN_PBA_SIZE + pbp_metadata_cached_len); + + int err = bt_rand(&broadcast_id, BT_AUDIO_BROADCAST_ID_SIZE); + struct bt_data ext_ad[3]; + + if (err) { + LOG_ERR("Unable to generate broadcast ID: %d\n", err); + return -EINVAL; + } + + ext_ad[0].type = BT_DATA_BROADCAST_NAME; + ext_ad[0].data_len = pbp_name_cached_len; + ext_ad[0].data = pbp_broadcast_name_cached; + net_buf_simple_add_le16(&ad_buf, BT_UUID_BROADCAST_AUDIO_VAL); + net_buf_simple_add_le24(&ad_buf, broadcast_id); + ext_ad[1].type = BT_DATA_SVC_DATA16; + ext_ad[1].data_len = ad_buf.len; + ext_ad[1].data = ad_buf.data; + net_buf_simple_add_le16(&pba_buf, BT_UUID_PBA_VAL); + net_buf_simple_add_u8(&pba_buf, pbp_features_cached); + net_buf_simple_add_u8(&pba_buf, pbp_metadata_cached_len); + net_buf_simple_add_mem(&pba_buf, pbp_metadata_cached, pbp_metadata_cached_len); + ext_ad[2].type = BT_DATA_SVC_DATA16; + ext_ad[2].data_len = pba_buf.len; + ext_ad[2].data = pba_buf.data; + + err = tester_gap_create_adv_instance(¶m, BTP_GAP_ADDR_TYPE_IDENTITY, ext_ad, + ARRAY_SIZE(ext_ad), NULL, 0, &gap_settings); + if (err) { + LOG_ERR("Could not set up extended advertisement: %d", err); + return -EINVAL; + } + + return 0; +} + +static uint8_t pbp_set_public_broadcast_announcement(const void *cmd, uint16_t cmd_len, + void *rsp, uint16_t *rsp_len) +{ + const struct btp_pbp_set_public_broadcast_announcement_cmd *cp = cmd; + int err = -EINVAL; + + if (cp->metadata_len <= PBP_EXT_ADV_METADATA_LEN_MAX) { + pbp_features_cached = cp->features; + pbp_metadata_cached_len = cp->metadata_len; + memcpy(pbp_metadata_cached, cp->metadata, cp->metadata_len); + err = pbp_broadcast_source_adv_setup(); + } else { + LOG_ERR("Metadata too long: %d > %d", cp->metadata_len, + PBP_EXT_ADV_METADATA_LEN_MAX); + } + + return BTP_STATUS_VAL(err); +} + +static uint8_t pbp_set_broadcast_name(const void *cmd, uint16_t cmd_len, void *rsp, + uint16_t *rsp_len) +{ + const struct btp_pbp_set_broadcast_name_cmd *cp = cmd; + int err = -EINVAL; + + if (cp->name_len <= BT_AUDIO_BROADCAST_NAME_LEN_MAX) { + pbp_name_cached_len = cp->name_len; + memcpy(pbp_broadcast_name_cached, cp->name, cp->name_len); + err = pbp_broadcast_source_adv_setup(); + } else { + LOG_ERR("Broadcast name too long: %d > %d", cp->name_len, + BT_AUDIO_BROADCAST_NAME_LEN_MAX); + } + + return BTP_STATUS_VAL(err); +} + +static uint8_t pbp_broadcast_scan_start(const void *cmd, uint16_t cmd_len, void *rsp, + uint16_t *rsp_len) +{ + int err; + + err = bt_le_scan_start(BT_LE_SCAN_ACTIVE, NULL); + if (err != 0 && err != -EALREADY) { + LOG_DBG("Unable to start scan for broadcast sources: %d", err); + + return BTP_STATUS_FAILED; + } + + return BTP_STATUS_SUCCESS; +} + +static uint8_t pbp_broadcast_scan_stop(const void *cmd, uint16_t cmd_len, void *rsp, + uint16_t *rsp_len) +{ + int err; + + err = bt_le_scan_stop(); + if (err != 0) { + LOG_DBG("Failed to stop scan, %d", err); + + return BTP_STATUS_FAILED; + } + + return BTP_STATUS_SUCCESS; +} + +static const struct btp_handler pbp_handlers[] = { + { + .opcode = BTP_PBP_READ_SUPPORTED_COMMANDS, + .index = BTP_INDEX_NONE, + .expect_len = 0, + .func = pbp_read_supported_commands + }, + { + .opcode = BTP_PBP_SET_PUBLIC_BROADCAST_ANNOUNCEMENT, + .expect_len = BTP_HANDLER_LENGTH_VARIABLE, + .func = pbp_set_public_broadcast_announcement + }, + { + .opcode = BTP_PBP_SET_BROADCAST_NAME, + .expect_len = BTP_HANDLER_LENGTH_VARIABLE, + .func = pbp_set_broadcast_name + }, + { + .opcode = BTP_PBP_BROADCAST_SCAN_START, + .expect_len = sizeof(struct btp_pbp_broadcast_scan_start_cmd), + .func = pbp_broadcast_scan_start + }, + { + .opcode = BTP_PBP_BROADCAST_SCAN_STOP, + .expect_len = sizeof(struct btp_pbp_broadcast_scan_stop_cmd), + .func = pbp_broadcast_scan_stop + } +}; + +uint8_t tester_init_pbp(void) +{ + tester_register_command_handlers(BTP_SERVICE_ID_PBP, pbp_handlers, + ARRAY_SIZE(pbp_handlers)); + + bt_le_scan_cb_register(&pbp_scan_cb); + + return BTP_STATUS_SUCCESS; +} + +uint8_t tester_unregister_pbp(void) +{ + return BTP_STATUS_SUCCESS; +} diff --git a/tests/bluetooth/tester/src/btp/btp.h b/tests/bluetooth/tester/src/btp/btp.h index c74d7bec452..06dc0788398 100644 --- a/tests/bluetooth/tester/src/btp/btp.h +++ b/tests/bluetooth/tester/src/btp/btp.h @@ -38,6 +38,7 @@ #include "btp_tbs.h" #include "btp_tmap.h" #include "btp_ots.h" +#include "btp_pbp.h" #define BTP_MTU 1024 #define BTP_DATA_MAX_SIZE (BTP_MTU - sizeof(struct btp_hdr)) @@ -75,8 +76,9 @@ #define BTP_SERVICE_ID_TBS 0x1b #define BTP_SERVICE_ID_TMAP 0x1c #define BTP_SERVICE_ID_OTS 0x1d +#define BTP_SERVICE_ID_PBP 0x1e -#define BTP_SERVICE_ID_MAX BTP_SERVICE_ID_OTS +#define BTP_SERVICE_ID_MAX BTP_SERVICE_ID_PBP #define BTP_STATUS_SUCCESS 0x00 #define BTP_STATUS_FAILED 0x01 diff --git a/tests/bluetooth/tester/src/btp/bttester.h b/tests/bluetooth/tester/src/btp/bttester.h index d7ce2699310..c43725a0aaf 100644 --- a/tests/bluetooth/tester/src/btp/bttester.h +++ b/tests/bluetooth/tester/src/btp/bttester.h @@ -138,3 +138,6 @@ uint8_t tester_unregister_tmap(void); uint8_t tester_init_ots(void); uint8_t tester_unregister_ots(void); + +uint8_t tester_init_pbp(void); +uint8_t tester_unregister_pbp(void); diff --git a/tests/bluetooth/tester/src/btp_core.c b/tests/bluetooth/tester/src/btp_core.c index 8b1b571792d..2da735f44ec 100644 --- a/tests/bluetooth/tester/src/btp_core.c +++ b/tests/bluetooth/tester/src/btp_core.c @@ -107,6 +107,9 @@ static uint8_t supported_services(const void *cmd, uint16_t cmd_len, #if defined(CONFIG_BT_TMAP) tester_set_bit(rp->data, BTP_SERVICE_ID_TMAP); #endif /* CONFIG_BT_TMAP */ +#if defined(CONFIG_BT_PBP) + tester_set_bit(rp->data, BTP_SERVICE_ID_PBP); +#endif /* CONFIG_BT_PBP */ *rsp_len = sizeof(*rp) + 2; @@ -250,6 +253,11 @@ static uint8_t register_service(const void *cmd, uint16_t cmd_len, status = tester_init_ots(); break; #endif /* CONFIG_BT_OTS */ +#if defined(CONFIG_BT_PBP) + case BTP_SERVICE_ID_PBP: + status = tester_init_pbp(); + break; +#endif /* CONFIG_BT_PBP */ default: LOG_WRN("unknown id: 0x%02x", cp->id); status = BTP_STATUS_FAILED; @@ -397,6 +405,11 @@ static uint8_t unregister_service(const void *cmd, uint16_t cmd_len, status = tester_unregister_ots(); break; #endif /* CONFIG_BT_OTS */ +#if defined(CONFIG_BT_PBP) + case BTP_SERVICE_ID_PBP: + status = tester_unregister_pbp(); + break; +#endif /* CONFIG_BT_PBP */ default: LOG_WRN("unknown id: 0x%x", cp->id); status = BTP_STATUS_FAILED;