| /* |
| * Copyright (c) 2024 Demant A/S |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #include <zephyr/types.h> |
| #include <stddef.h> |
| #include <strings.h> |
| #include <errno.h> |
| #include <zephyr/kernel.h> |
| #include <zephyr/sys/printk.h> |
| |
| #include <zephyr/bluetooth/bluetooth.h> |
| #include <zephyr/bluetooth/audio/audio.h> |
| #include <zephyr/bluetooth/audio/bap.h> |
| #include <zephyr/sys/byteorder.h> |
| |
| #define NAME_LEN 30 |
| |
| /* Broadcast IDs are 24bit, so this is out of valid range */ |
| #define INVALID_BROADCAST_ID 0xFFFFFFFFU |
| |
| static void scan_for_broadcast_sink(void); |
| |
| /* Struct to collect information from scanning |
| * for Broadcast Source or Sink |
| */ |
| struct scan_recv_info { |
| char bt_name[NAME_LEN]; |
| char broadcast_name[NAME_LEN]; |
| uint32_t broadcast_id; |
| bool has_bass; |
| bool has_pacs; |
| }; |
| |
| static struct bt_conn *broadcast_sink_conn; |
| static uint32_t selected_broadcast_id; |
| static uint8_t selected_sid; |
| static uint16_t selected_pa_interval; |
| static bt_addr_le_t selected_addr; |
| |
| static bool scanning_for_broadcast_source; |
| |
| static K_SEM_DEFINE(sem_source_discovered, 0, 1); |
| static K_SEM_DEFINE(sem_sink_discovered, 0, 1); |
| static K_SEM_DEFINE(sem_sink_connected, 0, 1); |
| static K_SEM_DEFINE(sem_sink_disconnected, 0, 1); |
| static K_SEM_DEFINE(sem_security_updated, 0, 1); |
| static K_SEM_DEFINE(sem_bass_discovered, 0, 1); |
| |
| static bool device_found(struct bt_data *data, void *user_data) |
| { |
| struct scan_recv_info *sr_info = (struct scan_recv_info *)user_data; |
| struct bt_uuid_16 adv_uuid; |
| |
| switch (data->type) { |
| case BT_DATA_NAME_SHORTENED: |
| case BT_DATA_NAME_COMPLETE: |
| memcpy(sr_info->bt_name, data->data, MIN(data->data_len, NAME_LEN - 1)); |
| return true; |
| case BT_DATA_BROADCAST_NAME: |
| memcpy(sr_info->broadcast_name, data->data, MIN(data->data_len, NAME_LEN - 1)); |
| return true; |
| case BT_DATA_SVC_DATA16: |
| /* Check for Broadcast ID */ |
| if (data->data_len < BT_UUID_SIZE_16 + BT_AUDIO_BROADCAST_ID_SIZE) { |
| return true; |
| } |
| |
| 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) != 0) { |
| return true; |
| } |
| |
| sr_info->broadcast_id = sys_get_le24(data->data + BT_UUID_SIZE_16); |
| return true; |
| case BT_DATA_UUID16_SOME: |
| case BT_DATA_UUID16_ALL: |
| /* NOTE: According to the BAP 1.0.1 Spec, |
| * Section 3.9.2. Additional Broadcast Audio Scan Service requirements, |
| * If the Scan Delegator implements a Broadcast Sink, it should also |
| * advertise a Service Data field containing the Broadcast Audio |
| * Scan Service (BASS) UUID. |
| * |
| * However, it seems that this is not the case with the sinks available |
| * while developing this sample application. Therefore, we instead, |
| * search for the existence of BASS and PACS in the list of service UUIDs, |
| * which does seem to exist in the sinks available. |
| */ |
| |
| /* Check for BASS and PACS */ |
| if (data->data_len % sizeof(uint16_t) != 0U) { |
| printk("UUID16 AD malformed\n"); |
| return true; |
| } |
| |
| for (size_t i = 0; i < data->data_len; i += sizeof(uint16_t)) { |
| const struct bt_uuid *uuid; |
| uint16_t u16; |
| |
| memcpy(&u16, &data->data[i], sizeof(u16)); |
| uuid = BT_UUID_DECLARE_16(sys_le16_to_cpu(u16)); |
| |
| if (bt_uuid_cmp(uuid, BT_UUID_BASS)) { |
| sr_info->has_bass = true; |
| continue; |
| } |
| |
| if (bt_uuid_cmp(uuid, BT_UUID_PACS)) { |
| sr_info->has_pacs = true; |
| continue; |
| } |
| } |
| return true; |
| default: |
| return true; |
| } |
| } |
| |
| static bool is_substring(const char *substr, const char *str) |
| { |
| const size_t str_len = strlen(str); |
| const size_t sub_str_len = strlen(substr); |
| |
| if (sub_str_len > str_len) { |
| return false; |
| } |
| |
| for (size_t pos = 0; pos < str_len; pos++) { |
| if (pos + sub_str_len > str_len) { |
| return false; |
| } |
| |
| if (strncasecmp(substr, &str[pos], sub_str_len) == 0) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| static void scan_recv_cb(const struct bt_le_scan_recv_info *info, |
| struct net_buf_simple *ad) |
| { |
| int err; |
| struct scan_recv_info sr_info = {0}; |
| |
| if (scanning_for_broadcast_source) { |
| /* Scan for and select Broadcast Source */ |
| |
| sr_info.broadcast_id = INVALID_BROADCAST_ID; |
| |
| /* We are only interested in non-connectable periodic advertisers */ |
| if ((info->adv_props & BT_GAP_ADV_PROP_CONNECTABLE) != 0 || |
| info->interval == 0) { |
| return; |
| } |
| |
| bt_data_parse(ad, device_found, (void *)&sr_info); |
| |
| if (sr_info.broadcast_id != INVALID_BROADCAST_ID) { |
| printk("Broadcast Source Found:\n"); |
| printk(" BT Name: %s\n", sr_info.bt_name); |
| printk(" Broadcast Name: %s\n", sr_info.broadcast_name); |
| printk(" Broadcast ID: 0x%06x\n\n", sr_info.broadcast_id); |
| |
| if (strlen(CONFIG_SELECT_SOURCE_NAME) > 0U) { |
| /* Compare names with CONFIG_SELECT_SOURCE_NAME */ |
| if (is_substring(CONFIG_SELECT_SOURCE_NAME, sr_info.bt_name) || |
| is_substring(CONFIG_SELECT_SOURCE_NAME, |
| sr_info.broadcast_name)) { |
| printk("Match found for '%s'\n", CONFIG_SELECT_SOURCE_NAME); |
| } else { |
| printk("'%s' not found in names\n\n", |
| CONFIG_SELECT_SOURCE_NAME); |
| return; |
| } |
| } |
| |
| err = bt_le_scan_stop(); |
| if (err != 0) { |
| printk("bt_le_scan_stop failed with %d\n", err); |
| } |
| |
| /* TODO: Add support for syncing to the PA and parsing the BASE |
| * in order to obtain the right subgroup information to send to |
| * the sink when adding a broadcast source (see in main function below). |
| */ |
| |
| printk("Selecting Broadcast ID: 0x%06x\n", sr_info.broadcast_id); |
| |
| selected_broadcast_id = sr_info.broadcast_id; |
| selected_sid = info->sid; |
| selected_pa_interval = info->interval; |
| bt_addr_le_copy(&selected_addr, info->addr); |
| |
| k_sem_give(&sem_source_discovered); |
| } |
| } else { |
| /* Scan for and connect to Broadcast Sink */ |
| |
| /* We are only interested in connectable advertisers */ |
| if ((info->adv_props & BT_GAP_ADV_PROP_CONNECTABLE) == 0) { |
| return; |
| } |
| |
| bt_data_parse(ad, device_found, (void *)&sr_info); |
| |
| if (sr_info.has_bass && sr_info.has_pacs) { |
| printk("Broadcast Sink Found:\n"); |
| printk(" BT Name: %s\n", sr_info.bt_name); |
| |
| if (strlen(CONFIG_SELECT_SINK_NAME) > 0U) { |
| /* Compare names with CONFIG_SELECT_SINK_NAME */ |
| if (is_substring(CONFIG_SELECT_SINK_NAME, sr_info.bt_name)) { |
| printk("Match found for '%s'\n", CONFIG_SELECT_SINK_NAME); |
| } else { |
| printk("'%s' not found in names\n\n", |
| CONFIG_SELECT_SINK_NAME); |
| return; |
| } |
| } |
| |
| err = bt_le_scan_stop(); |
| if (err != 0) { |
| printk("bt_le_scan_stop failed with %d\n", err); |
| } |
| |
| printk("Connecting to Broadcast Sink: %s\n", sr_info.bt_name); |
| |
| err = bt_conn_le_create(info->addr, BT_CONN_LE_CREATE_CONN, |
| BT_LE_CONN_PARAM_DEFAULT, |
| &broadcast_sink_conn); |
| if (err != 0) { |
| printk("Failed creating connection (err=%u)\n", err); |
| scan_for_broadcast_sink(); |
| } |
| |
| k_sem_give(&sem_sink_discovered); |
| } |
| } |
| } |
| |
| static void scan_timeout_cb(void) |
| { |
| printk("Scan timeout\n"); |
| } |
| |
| static struct bt_le_scan_cb scan_callbacks = { |
| .recv = scan_recv_cb, |
| .timeout = scan_timeout_cb, |
| }; |
| |
| static void scan_for_broadcast_source(void) |
| { |
| int err; |
| |
| scanning_for_broadcast_source = true; |
| |
| err = bt_le_scan_start(BT_LE_SCAN_PASSIVE, NULL); |
| if (err) { |
| printk("Scanning failed to start (err %d)\n", err); |
| return; |
| } |
| |
| printk("Scanning for Broadcast Source successfully started\n"); |
| |
| err = k_sem_take(&sem_source_discovered, K_FOREVER); |
| if (err != 0) { |
| printk("Failed to take sem_source_discovered (err %d)\n", err); |
| } |
| } |
| |
| static void scan_for_broadcast_sink(void) |
| { |
| int err; |
| |
| scanning_for_broadcast_source = false; |
| |
| err = bt_le_scan_start(BT_LE_SCAN_PASSIVE, NULL); |
| if (err) { |
| printk("Scanning failed to start (err %d)\n", err); |
| return; |
| } |
| |
| printk("Scanning for Broadcast Sink successfully started\n"); |
| |
| err = k_sem_take(&sem_sink_discovered, K_FOREVER); |
| if (err != 0) { |
| printk("Failed to take sem_sink_discovered (err %d)\n", err); |
| } |
| } |
| |
| static void connected(struct bt_conn *conn, uint8_t err) |
| { |
| char addr[BT_ADDR_LE_STR_LEN]; |
| |
| (void)bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); |
| |
| if (err != 0) { |
| printk("Failed to connect to %s (%u)\n", addr, err); |
| |
| bt_conn_unref(broadcast_sink_conn); |
| broadcast_sink_conn = NULL; |
| |
| scan_for_broadcast_sink(); |
| return; |
| } |
| |
| if (conn != broadcast_sink_conn) { |
| return; |
| } |
| |
| printk("Connected: %s\n", addr); |
| k_sem_give(&sem_sink_connected); |
| } |
| |
| static void disconnected(struct bt_conn *conn, uint8_t reason) |
| { |
| char addr[BT_ADDR_LE_STR_LEN]; |
| |
| if (conn != broadcast_sink_conn) { |
| return; |
| } |
| |
| (void)bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); |
| |
| printk("Disconnected: %s (reason 0x%02x)\n", addr, reason); |
| |
| bt_conn_unref(broadcast_sink_conn); |
| broadcast_sink_conn = NULL; |
| |
| k_sem_give(&sem_sink_disconnected); |
| } |
| |
| static void security_changed_cb(struct bt_conn *conn, bt_security_t level, |
| enum bt_security_err err) |
| { |
| if (err == 0) { |
| printk("Security level changed: %u\n", level); |
| k_sem_give(&sem_security_updated); |
| } else { |
| printk("Failed to set security level: %u\n", err); |
| } |
| } |
| |
| static void bap_broadcast_assistant_discover_cb(struct bt_conn *conn, int err, |
| uint8_t recv_state_count) |
| { |
| if (err == 0) { |
| printk("BASS discover done with %u recv states\n", |
| recv_state_count); |
| k_sem_give(&sem_bass_discovered); |
| } else { |
| printk("BASS discover failed (%d)\n", err); |
| } |
| } |
| |
| static void bap_broadcast_assistant_add_src_cb(struct bt_conn *conn, int err) |
| { |
| if (err == 0) { |
| printk("BASS add source successful\n"); |
| } else { |
| printk("BASS add source failed (%d)\n", err); |
| } |
| } |
| |
| static struct bt_bap_broadcast_assistant_cb ba_cbs = { |
| .discover = bap_broadcast_assistant_discover_cb, |
| .add_src = bap_broadcast_assistant_add_src_cb, |
| }; |
| |
| static void reset(void) |
| { |
| printk("\n\nReset...\n\n"); |
| |
| broadcast_sink_conn = NULL; |
| selected_broadcast_id = INVALID_BROADCAST_ID; |
| selected_sid = 0; |
| selected_pa_interval = 0; |
| (void)memset(&selected_addr, 0, sizeof(selected_addr)); |
| |
| k_sem_reset(&sem_source_discovered); |
| k_sem_reset(&sem_sink_discovered); |
| k_sem_reset(&sem_sink_connected); |
| k_sem_reset(&sem_sink_disconnected); |
| k_sem_reset(&sem_security_updated); |
| k_sem_reset(&sem_bass_discovered); |
| } |
| |
| BT_CONN_CB_DEFINE(conn_callbacks) = { |
| .connected = connected, |
| .disconnected = disconnected, |
| .security_changed = security_changed_cb |
| }; |
| |
| int main(void) |
| { |
| int err; |
| struct bt_bap_bass_subgroup subgroup = { 0 }; |
| struct bt_bap_broadcast_assistant_add_src_param param = { 0 }; |
| |
| err = bt_enable(NULL); |
| if (err) { |
| printk("Bluetooth init failed (err %d)\n", err); |
| return 0; |
| } |
| |
| printk("Bluetooth initialized\n"); |
| |
| bt_le_scan_cb_register(&scan_callbacks); |
| bt_bap_broadcast_assistant_register_cb(&ba_cbs); |
| |
| while (true) { |
| scan_for_broadcast_sink(); |
| |
| err = k_sem_take(&sem_sink_connected, K_FOREVER); |
| if (err != 0) { |
| printk("Failed to take sem_sink_connected (err %d)\n", err); |
| } |
| |
| err = bt_bap_broadcast_assistant_discover(broadcast_sink_conn); |
| if (err != 0) { |
| printk("Failed to discover BASS on the sink (err %d)\n", err); |
| } |
| |
| err = k_sem_take(&sem_security_updated, K_SECONDS(10)); |
| if (err != 0) { |
| printk("Failed to take sem_security_updated (err %d), resetting\n", err); |
| bt_conn_disconnect(broadcast_sink_conn, BT_HCI_ERR_AUTH_FAIL); |
| |
| if (k_sem_take(&sem_sink_disconnected, K_SECONDS(10)) != 0) { |
| /* This should not happen */ |
| return -ETIMEDOUT; |
| } |
| |
| reset(); |
| continue; |
| } |
| |
| err = k_sem_take(&sem_bass_discovered, K_SECONDS(10)); |
| if (err != 0) { |
| if (err == -EAGAIN) { |
| printk("Failed to take sem_bass_discovered (err %d)\n", err); |
| } |
| bt_conn_disconnect(broadcast_sink_conn, BT_HCI_ERR_UNSUPP_REMOTE_FEATURE); |
| |
| if (k_sem_take(&sem_sink_disconnected, K_SECONDS(10)) != 0) { |
| /* This should not happen */ |
| return -ETIMEDOUT; |
| } |
| |
| reset(); |
| continue; |
| } |
| |
| /* TODO: Discover and parse the PACS on the sink and use the information |
| * when discovering and adding a source to the sink. |
| * Also, before populating the parameters to sync to the broadcast source |
| * first, parse the source BASE and determine if the sink supports the source. |
| * If not, then look for another source. |
| */ |
| |
| scan_for_broadcast_source(); |
| |
| /* FIX NEEDED: It should be valid to assign BT_BAP_BIS_SYNC_NO_PREF |
| * to bis_sync, but currently (2024-01-30), the broadcast_audio_sink |
| * sample seems to reject it (err=19) while other sinks don't. |
| * |
| * Also, if the source contains more than one stream (e.g. stereo), |
| * some sinks have been observed to have issues. In this case, |
| * set only one bit in bis_sync, e.g. subgroup.bis_sync = BIT(1). |
| * |
| * When PA sync and BASE is parsed (see note in the scan_recv_cb function), |
| * the available bits can be used for proper selection. |
| */ |
| subgroup.bis_sync = BT_BAP_BIS_SYNC_NO_PREF; |
| |
| bt_addr_le_copy(¶m.addr, &selected_addr); |
| param.adv_sid = selected_sid; |
| param.pa_interval = selected_pa_interval; |
| param.broadcast_id = selected_broadcast_id; |
| param.pa_sync = true; |
| |
| /* TODO: Obtain the and set the correct subgroup information. |
| * See above in the broadcast audio source discovery part |
| * of the scan_recv_cb function. |
| */ |
| param.num_subgroups = 1; |
| param.subgroups = &subgroup; |
| |
| err = bt_bap_broadcast_assistant_add_src(broadcast_sink_conn, ¶m); |
| if (err) { |
| printk("Failed to add source: %d\n", err); |
| bt_conn_disconnect(broadcast_sink_conn, BT_HCI_ERR_UNSUPP_REMOTE_FEATURE); |
| |
| if (k_sem_take(&sem_sink_disconnected, K_SECONDS(10)) != 0) { |
| /* This should not happen */ |
| return -ETIMEDOUT; |
| } |
| |
| reset(); |
| continue; |
| } |
| |
| /* Reset if the sink disconnects */ |
| err = k_sem_take(&sem_sink_disconnected, K_FOREVER); |
| if (err != 0) { |
| printk("Failed to take sem_sink_disconnected (err %d)\n", err); |
| } |
| |
| reset(); |
| } |
| |
| return 0; |
| } |