samples: Add Broadcast Assistant sample

First version of Broadcast Assistant sample
with hard coded selection of sink and source.

Signed-off-by: Lars Knudsen <LAKD@demant.com>
This commit is contained in:
Lars Knudsen 2024-01-29 14:21:12 +01:00 committed by Carles Cufí
parent 9cd0ad0ae4
commit b77df76e98
6 changed files with 640 additions and 0 deletions

View file

@ -0,0 +1,11 @@
# SPDX-License-Identifier: Apache-2.0
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(broadcast_audio_assistant)
target_sources(app PRIVATE
src/main.c
)
zephyr_library_include_directories(${ZEPHYR_BASE}/samples/bluetooth)

View file

@ -0,0 +1,24 @@
# Copyright (c) 2024 Demant A/S
# SPDX-License-Identifier: Apache-2.0
mainmenu "Bluetooth: Broadcast Audio Assistant"
config SELECT_SOURCE_NAME
string "Selected Broadcast Source name"
default ""
help
Name of broadcast source device to select. This will be used as a
substring matched against both BT device name and broadcast name.
If empty, the first broadcast source found will be chosen.
Matching is not case sensitive.
config SELECT_SINK_NAME
string "Selected Broadcast Sink name"
default ""
help
Name of broadcast sink device to select. This will be used as a
substring matched against the BT device name.
If empty, the first broadcast sink found will be chosen.
Matching is not case sensitive.
source "Kconfig.zephyr"

View file

@ -0,0 +1,56 @@
.. zephyr:code-sample:: bluetooth_broadcast_audio_assistant
:name: Bluetooth: Broadcast Audio Assistant
:relevant-api: bt_bap
Use LE Audio Broadcast Assistant functionality
Overview
********
Application demonstrating the LE Audio broadcast assistant functionality.
The sample will automatically try to connect to a device in the BAP Scan Delegator
role (advertising support for the Broadcast Audio Scan Service (BASS)).
It will then search for a broadcast source and (if found) add the broadcast ID to
the BAP Scan Delegator.
Practical use of this sample requires a sink (e.g. the Broadcast Audio Sink sample or
a set of LE Audio Broadcast capable earbuds) and a source (e.g. the Broadcast Audio
Source sample).
This sample can be found under
:zephyr_file:`samples/bluetooth/broadcast_audio_assistant` in the Zephyr tree.
Check the :ref:`bluetooth samples section <bluetooth-samples>` for general information.
Requirements
************
* BlueZ running on the host, or
* A board with Bluetooth Low Energy 5.2 support
* Broadcast Audio Source and Sink devices
Building and Running
********************
The application will act as a broadcast assistant with optionally preconfigured
filtering of broadcast sink and broadcast source names. By default, the application will
search for and connect to the first broadcast audio sink found (advertising PACS and
BASS UUIDs) and then search for and select the first broadcast audio source found
(advertising a broadcast ID).
Filter these by modifying the following configs:
``CONFIG_SELECT_SINK_NAME``: Substring of BT name of the sink.
and
``CONFIG_SELECT_SOURCE_NAME``: Substring of BT name or broadcast name of the source.
Building for an nrf52840dk
--------------------------
.. zephyr-app-commands::
:zephyr-app: samples/bluetooth/broadcast_audio_assistant/
:board: nrf52840dk_nrf52840
:goals: build

View file

@ -0,0 +1,18 @@
CONFIG_BT=y
CONFIG_LOG=y
CONFIG_BT_CENTRAL=y
CONFIG_BT_AUDIO=y
CONFIG_BT_SMP=y
CONFIG_BT_BONDABLE=n
CONFIG_BT_BUF_ACL_RX_SIZE=255
CONFIG_BT_BUF_ACL_TX_SIZE=251
CONFIG_BT_CTLR_SCAN_DATA_LEN_MAX=191
CONFIG_BT_TINYCRYPT_ECC=y
CONFIG_BT_EXT_ADV=y
CONFIG_BT_BAP_BROADCAST_ASSISTANT=y
# CONFIG_BT_BAP_SCAN_DELEGATOR=y is required until the following
# bug is fixed: https://github.com/zephyrproject-rtos/zephyr/issues/68338
CONFIG_BT_BAP_SCAN_DELEGATOR=y

View file

@ -0,0 +1,11 @@
sample:
description: Bluetooth Low Energy Broadcast Assistant sample
name: Bluetooth Low Energy Broadcast Assistant sample
tests:
sample.bluetooth.broadcast_audio_assistant:
harness: bluetooth
platform_allow:
- nrf52840dk_nrf52840
integration_platforms:
- nrf52840dk_nrf52840
tags: bluetooth

View file

@ -0,0 +1,520 @@
/*
* 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_scan_delegator_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(&param.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, &param);
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;
}