From cf06fa85f2eeb4e50892f816fd05c624d76f3944 Mon Sep 17 00:00:00 2001 From: Emil Gydesen Date: Mon, 10 Jan 2022 15:34:37 +0100 Subject: [PATCH] Bluetooth: Audio: Add BAP broadcast sink support Add support for the BAP broadcast sink role. This role allows a device to sync to a broadcast ISO stream. Signed-off-by: Emil Gydesen --- include/bluetooth/audio/audio.h | 5 + subsys/bluetooth/audio/CMakeLists.txt | 1 + subsys/bluetooth/audio/Kconfig.baps | 46 +- subsys/bluetooth/audio/broadcast_sink.c | 1033 +++++++++++++++++++++++ subsys/bluetooth/audio/stream.c | 5 +- tests/bluetooth/shell/audio.conf | 1 + 6 files changed, 1087 insertions(+), 4 deletions(-) create mode 100644 subsys/bluetooth/audio/broadcast_sink.c diff --git a/include/bluetooth/audio/audio.h b/include/bluetooth/audio/audio.h index 6a2bb541998..45d6cc2ee29 100644 --- a/include/bluetooth/audio/audio.h +++ b/include/bluetooth/audio/audio.h @@ -51,8 +51,13 @@ extern "C" { #define BT_AUDIO_UNICAST_ANNOUNCEMENT_GENERAL 0x00 #define BT_AUDIO_UNICAST_ANNOUNCEMENT_TARGETED 0x01 +#if defined(CONFIG_BT_AUDIO_BROADCAST_SINK) +#define BROADCAST_SNK_STREAM_CNT CONFIG_BT_AUDIO_BROADCAST_SNK_STREAM_COUNT +#define BROADCAST_SUBGROUP_CNT CONFIG_BT_AUDIO_BROADCAST_SUBGROUP_COUNT +#else /* !CONFIG_BT_AUDIO_BROADCAST_SINK */ #define BROADCAST_SNK_STREAM_CNT 0 #define BROADCAST_SUBGROUP_CNT 0 +#endif /* CONFIG_BT_AUDIO_BROADCAST_SINK*/ /** @brief Abstract Audio Unicast Group structure. */ struct bt_audio_unicast_group; diff --git a/subsys/bluetooth/audio/CMakeLists.txt b/subsys/bluetooth/audio/CMakeLists.txt index e66c0a6bb5a..fac2b0e5a6b 100644 --- a/subsys/bluetooth/audio/CMakeLists.txt +++ b/subsys/bluetooth/audio/CMakeLists.txt @@ -49,3 +49,4 @@ zephyr_library_sources_ifdef(CONFIG_BT_AUDIO_UNICAST_SERVER unicast_server.c) zephyr_library_sources_ifdef(CONFIG_BT_AUDIO_CAPABILITIES capabilities.c) zephyr_library_sources_ifdef(CONFIG_BT_AUDIO_UNICAST_CLIENT unicast_client.c) zephyr_library_sources_ifdef(CONFIG_BT_AUDIO_BROADCAST_SOURCE broadcast_source.c) +zephyr_library_sources_ifdef(CONFIG_BT_AUDIO_BROADCAST_SINK broadcast_sink.c) diff --git a/subsys/bluetooth/audio/Kconfig.baps b/subsys/bluetooth/audio/Kconfig.baps index 1a3ab918116..bf6c08e44df 100644 --- a/subsys/bluetooth/audio/Kconfig.baps +++ b/subsys/bluetooth/audio/Kconfig.baps @@ -149,6 +149,40 @@ config BT_AUDIO_BROADCAST_SRC_STREAM_COUNT endif # BT_AUDIO_BROADCAST_SOURCE +config BT_AUDIO_BROADCAST_SINK + bool "Bluetooth Broadcast Sink Audio Support [EXPERIMENTAL]" + select EXPERIMENTAL + select BT_ISO_SYNC_RECEIVER + help + This option enables support for Bluetooth Broadcast Sink Audio using + Isochronous channels. + +if BT_AUDIO_BROADCAST_SINK + +config BT_AUDIO_BROADCAST_SUBGROUP_COUNT + int # hidden: TODO: Update once the API supports it + default 1 + +config BT_AUDIO_BROADCAST_SNK_COUNT + int "Basic Audio Broadcaster Sink count" + default 1 + range 0 BT_ISO_MAX_BIG + help + This option sets the number of broadcast sinks to support. + One broadcast sink can receive multiple streams + (up to BT_AUDIO_BROADCAST_SNK_STREAM_COUNT per broadcast sink). + +config BT_AUDIO_BROADCAST_SNK_STREAM_COUNT + int "Basic Audio Broadcast Sink Stream count" + depends on BT_AUDIO_BROADCAST_SNK_COUNT > 0 + default 1 + range 1 BT_ISO_MAX_CHAN + help + This option sets the maximum number of streams per broadcast sink + to support. + +endif # BT_AUDIO_BROADCAST_SINK + config BT_AUDIO_DEBUG bool "Enable debug logs" depends on BT_DEBUG @@ -207,17 +241,25 @@ config BT_AUDIO_DEBUG_BROADCAST_SOURCE Use this option to enable Bluetooth Audio Broadcast Source debug logs for the Bluetooth Audio functionality. +config BT_AUDIO_DEBUG_BROADCAST_SINK + bool "Bluetooth Audio Broadcast Sink debug" + depends on BT_AUDIO_BROADCAST_SINK + help + Use this option to enable Bluetooth Audio Broadcast Sink debug logs + for the Bluetooth Audio functionality. + endif # BT_AUDIO_DEBUG config BT_AUDIO_STREAM # Virtual/hidden option bool - default y if BT_ASCS || BT_AUDIO_UNICAST_CLIENT || BT_AUDIO_BROADCAST_SOURCE + default y if BT_ASCS || BT_AUDIO_UNICAST_CLIENT || \ + BT_AUDIO_BROADCAST_SOURCE || BT_AUDIO_BROADCAST_SINK config BT_AUDIO_CAPABILITIES # Virtual/hidden option bool - default y if BT_ASCS + default y if BT_ASCS || BT_AUDIO_BROADCAST_SINK rsource "Kconfig.pacs" rsource "Kconfig.ascs" diff --git a/subsys/bluetooth/audio/broadcast_sink.c b/subsys/bluetooth/audio/broadcast_sink.c new file mode 100644 index 00000000000..cc6c38ad355 --- /dev/null +++ b/subsys/bluetooth/audio/broadcast_sink.c @@ -0,0 +1,1033 @@ +/* Bluetooth Audio Broadcast Sink */ + +/* + * Copyright (c) 2021-2022 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include + +#include "../host/conn_internal.h" +#include "../host/iso_internal.h" + +#include "endpoint.h" + +#define BT_DBG_ENABLED IS_ENABLED(CONFIG_BT_AUDIO_DEBUG_BROADCAST_SINK) +#define LOG_MODULE_NAME bt_audio_broadcast_sink +#include "common/log.h" + +#define PA_SYNC_SKIP 5 +#define SYNC_RETRY_COUNT 6 /* similar to retries for connections */ +#define BASE_MIN_SIZE 17 +#define BASE_BIS_DATA_MIN_SIZE 2 /* index and length */ +#define BROADCAST_SYNC_MIN_INDEX (BIT(1)) + +static struct bt_audio_ep broadcast_sink_eps + [CONFIG_BT_AUDIO_BROADCAST_SNK_COUNT][BROADCAST_SNK_STREAM_CNT]; +static struct bt_audio_broadcast_sink broadcast_sinks[CONFIG_BT_AUDIO_BROADCAST_SNK_COUNT]; +static struct bt_le_scan_cb broadcast_scan_cb; + +static sys_slist_t sink_cbs = SYS_SLIST_STATIC_INIT(&sink_cbs); + +static void broadcast_sink_cleanup(struct bt_audio_broadcast_sink *sink); + +static void broadcast_sink_set_ep_state(struct bt_audio_ep *ep, uint8_t state) +{ + uint8_t old_state; + + old_state = ep->status.state; + + BT_DBG("ep %p id 0x%02x %s -> %s", ep, ep->status.id, + bt_audio_ep_state_str(old_state), + bt_audio_ep_state_str(state)); + + + switch (old_state) { + case BT_AUDIO_EP_STATE_IDLE: + if (state != BT_AUDIO_EP_STATE_QOS_CONFIGURED) { + BT_DBG("Invalid broadcast sync endpoint state transition"); + return; + } + break; + case BT_AUDIO_EP_STATE_QOS_CONFIGURED: + if (state != BT_AUDIO_EP_STATE_IDLE && + state != BT_AUDIO_EP_STATE_STREAMING) { + BT_DBG("Invalid broadcast sync endpoint state transition"); + return; + } + break; + case BT_AUDIO_EP_STATE_STREAMING: + if (state != BT_AUDIO_EP_STATE_IDLE) { + BT_DBG("Invalid broadcast sync endpoint state transition"); + return; + } + break; + default: + BT_ERR("Invalid broadcast sync endpoint state: %s", + bt_audio_ep_state_str(old_state)); + return; + } + + ep->status.state = state; + + if (state == BT_AUDIO_EP_STATE_IDLE) { + struct bt_audio_stream *stream = ep->stream; + + if (stream != NULL) { + stream->ep = NULL; + stream->codec = NULL; + ep->stream = NULL; + } + } +} + +static void broadcast_sink_iso_recv(struct bt_iso_chan *chan, + const struct bt_iso_recv_info *info, + struct net_buf *buf) +{ + struct bt_audio_ep *ep = CONTAINER_OF(chan, struct bt_audio_ep, iso); + struct bt_audio_stream_ops *ops = ep->stream->ops; + + BT_DBG("stream %p ep %p len %zu", chan, ep, net_buf_frags_len(buf)); + + if (ops != NULL && ops->recv != NULL) { + ops->recv(ep->stream, buf); + } +} + +static void broadcast_sink_iso_connected(struct bt_iso_chan *chan) +{ + struct bt_audio_ep *ep = CONTAINER_OF(chan, struct bt_audio_ep, iso); + struct bt_audio_stream_ops *ops = ep->stream->ops; + + BT_DBG("stream %p ep %p", chan, ep); + + broadcast_sink_set_ep_state(ep, BT_AUDIO_EP_STATE_STREAMING); + + if (ops != NULL && ops->connected != NULL) { + ops->connected(ep->stream); + } +} + +static void broadcast_sink_iso_disconnected(struct bt_iso_chan *chan, + uint8_t reason) +{ + struct bt_audio_ep *ep = CONTAINER_OF(chan, struct bt_audio_ep, iso); + struct bt_audio_stream *stream = ep->stream; + struct bt_audio_stream_ops *ops = stream->ops; + + BT_DBG("stream %p ep %p reason 0x%02x", chan, ep, reason); + + broadcast_sink_set_ep_state(ep, BT_AUDIO_EP_STATE_IDLE); + + if (ops != NULL && ops->disconnected != NULL) { + ops->disconnected(stream, reason); + } +} + +static struct bt_iso_chan_ops broadcast_sink_iso_ops = { + .recv = broadcast_sink_iso_recv, + .connected = broadcast_sink_iso_connected, + .disconnected = broadcast_sink_iso_disconnected, +}; + +static struct bt_audio_broadcast_sink *broadcast_sink_syncing_get(void) +{ + for (int i = 0; i < ARRAY_SIZE(broadcast_sinks); i++) { + if (broadcast_sinks[i].syncing) { + return &broadcast_sinks[i]; + } + } + + return NULL; +} + +static struct bt_audio_broadcast_sink *broadcast_sink_free_get(void) +{ + /* Find free entry */ + for (int i = 0; i < ARRAY_SIZE(broadcast_sinks); i++) { + if (broadcast_sinks[i].pa_sync == NULL) { + broadcast_sinks[i].index = i; + return &broadcast_sinks[i]; + } + } + + return NULL; +} + +static struct bt_audio_broadcast_sink *broadcast_sink_get_by_pa(struct bt_le_per_adv_sync *sync) +{ + for (int i = 0; i < ARRAY_SIZE(broadcast_sinks); i++) { + if (broadcast_sinks[i].pa_sync == sync) { + return &broadcast_sinks[i]; + } + } + + return NULL; +} + +static void pa_synced(struct bt_le_per_adv_sync *sync, + struct bt_le_per_adv_sync_synced_info *info) +{ + struct bt_audio_broadcast_sink_cb *listener; + struct bt_audio_broadcast_sink *sink; + + sink = broadcast_sink_syncing_get(); + if (sink == NULL || sync != sink->pa_sync) { + /* Not ours */ + return; + } + + BT_DBG("Synced to broadcast source with ID 0x%06X", sink->broadcast_id); + + sink->syncing = false; + + bt_audio_broadcast_sink_scan_stop(); + + SYS_SLIST_FOR_EACH_CONTAINER(&sink_cbs, listener, node) { + if (listener->pa_synced != NULL) { + listener->pa_synced(sink, sink->pa_sync, sink->broadcast_id); + } + } + + /* TBD: What if sync to a bad broadcast source that does not send + * properly formatted (or any) BASE? + */ +} + +static void pa_term(struct bt_le_per_adv_sync *sync, + const struct bt_le_per_adv_sync_term_info *info) +{ + struct bt_audio_broadcast_sink_cb *listener; + struct bt_audio_broadcast_sink *sink; + + sink = broadcast_sink_get_by_pa(sync); + if (sink == NULL) { + /* Not ours */ + return; + } + + BT_DBG("PA sync with broadcast source with ID 0x%06X lost", + sink->broadcast_id); + broadcast_sink_cleanup(sink); + SYS_SLIST_FOR_EACH_CONTAINER(&sink_cbs, listener, node) { + if (listener->pa_sync_lost != NULL) { + listener->pa_sync_lost(sink); + } + } +} + +static bool net_buf_decode_codec_ltv(struct net_buf_simple *buf, + struct bt_codec_data *codec_data) +{ + size_t value_len; + void *value; + + if (buf->len < sizeof(codec_data->data.data_len)) { + BT_DBG("Not enough data for LTV length field: %u", buf->len); + return false; + } + codec_data->data.data_len = net_buf_simple_pull_u8(buf); + + if (buf->len < sizeof(codec_data->data.type)) { + BT_DBG("Not enough data for LTV type field: %u", buf->len); + return false; + } + codec_data->data.type = net_buf_simple_pull_u8(buf); + codec_data->data.data = codec_data->value; + + value_len = codec_data->data.data_len - sizeof(codec_data->data.type); + if (buf->len < value_len) { + BT_DBG("Not enough data for LTV value field: %u/%zu", + buf->len, value_len); + return false; + } + value = net_buf_simple_pull_mem(buf, value_len); + memcpy(codec_data->value, value, value_len); + + return true; +} + +static bool net_buf_decode_bis_data(struct net_buf_simple *buf, + struct bt_audio_base_bis_data *bis, + bool codec_data_already_found) +{ + uint8_t len; + + if (buf->len < BASE_BIS_DATA_MIN_SIZE) { + BT_DBG("Not enough bytes (%u) to decode BIS data", buf->len); + return false; + } + + bis->index = net_buf_simple_pull_u8(buf); + if (bis->index == 0) { + BT_DBG("BIS index was 0"); + return false; + } + + /* codec config data length */ + len = net_buf_simple_pull_u8(buf); + if (len > buf->len) { + BT_DBG("Invalid BIS specific codec config data length: " + "%u (buf is %u)", len, buf->len); + return false; + } + + if (len > 0) { + struct net_buf_simple ltv_buf; + void *ltv_data; + + if (codec_data_already_found) { + /* Codec config can either be specific to each + * BIS or for all, but not both + */ + BT_DBG("BASE contains both codec config data and BIS " + "codec config data. Aborting."); + return false; + } + + /* TODO: Support codec configuration data per bis */ + BT_WARN("BIS specific codec config data of length %u " + "was found but is not supported yet", len); + + /* Use an extra net_buf_simple to be able to decode until it + * is empty (len = 0) + */ + ltv_data = net_buf_simple_pull_mem(buf, len); + net_buf_simple_init_with_data(<v_buf, ltv_data, len); + + while (ltv_buf.len != 0) { + struct bt_codec_data *bis_codec_data; + + bis_codec_data = &bis->data[bis->data_count]; + + if (!net_buf_decode_codec_ltv(<v_buf, + bis_codec_data)) { + BT_DBG("Failed to decode BIS config data for entry %u", + bis->data_count); + return false; + } + bis->data_count++; + } + } + + return true; +} + +static bool net_buf_decode_subgroup(struct net_buf_simple *buf, + struct bt_audio_base_subgroup *subgroup) +{ + struct net_buf_simple ltv_buf; + struct bt_codec *codec; + void *ltv_data; + uint8_t len; + + codec = &subgroup->codec; + + subgroup->bis_count = net_buf_simple_pull_u8(buf); + if (subgroup->bis_count > ARRAY_SIZE(subgroup->bis_data)) { + BT_DBG("BASE has more BIS %u than we support %u", + subgroup->bis_count, + (uint8_t)ARRAY_SIZE(subgroup->bis_data)); + return false; + } + codec->id = net_buf_simple_pull_u8(buf); + codec->cid = net_buf_simple_pull_le16(buf); + codec->vid = net_buf_simple_pull_le16(buf); + + /* codec configuration data length */ + len = net_buf_simple_pull_u8(buf); + if (len > buf->len) { + BT_DBG("Invalid codec config data length: %u (buf is %u)", + len, buf->len); + return false; + } + + /* Use an extra net_buf_simple to be able to decode until it + * is empty (len = 0) + */ + ltv_data = net_buf_simple_pull_mem(buf, len); + net_buf_simple_init_with_data(<v_buf, ltv_data, len); + + /* The loop below is very similar to codec_config_store with notable + * exceptions that it can do early termination, and also does not log + * every LTV entry, which would simply be too much for handling + * broadcasted BASEs + */ + while (ltv_buf.len != 0) { + struct bt_codec_data *codec_data = &codec->data[codec->data_count++]; + + if (!net_buf_decode_codec_ltv(<v_buf, codec_data)) { + BT_DBG("Failed to decode codec config data for entry %u", + codec->data_count - 1); + return false; + } + } + + if (buf->len < sizeof(len)) { + return false; + } + + /* codec metadata length */ + len = net_buf_simple_pull_u8(buf); + if (len > buf->len) { + BT_DBG("Invalid codec config data length: %u (buf is %u)", + len, buf->len); + return false; + } + + + /* Use an extra net_buf_simple to be able to decode until it + * is empty (len = 0) + */ + ltv_data = net_buf_simple_pull_mem(buf, len); + net_buf_simple_init_with_data(<v_buf, ltv_data, len); + + /* The loop below is very similar to codec_config_store with notable + * exceptions that it can do early termination, and also does not log + * every LTV entry, which would simply be too much for handling + * broadcasted BASEs + */ + while (ltv_buf.len != 0) { + struct bt_codec_data *metadata = &codec->meta[codec->meta_count++]; + + if (!net_buf_decode_codec_ltv(<v_buf, metadata)) { + BT_DBG("Failed to decode codec metadata for entry %u", + codec->meta_count - 1); + return false; + } + } + + for (int i = 0; i < subgroup->bis_count; i++) { + if (!net_buf_decode_bis_data(buf, &subgroup->bis_data[i], + codec->data_count > 0)) { + BT_DBG("Failed to decode BIS data for bis %d", i); + return false; + } + } + + return true; +} + +static bool pa_decode_base(struct bt_data *data, void *user_data) +{ + struct bt_audio_broadcast_sink *sink = (struct bt_audio_broadcast_sink *)user_data; + struct bt_audio_broadcast_sink_cb *listener; + struct bt_codec_qos codec_qos = { 0 }; + struct bt_audio_base base = { 0 }; + struct bt_uuid_16 broadcast_uuid; + struct net_buf_simple net_buf; + void *uuid; + + if (sys_slist_is_empty(&sink_cbs)) { + /* Terminate early if we do not have any broadcast sink listeners */ + return false; + } + + if (data->type != BT_DATA_SVC_DATA16) { + return true; + } + + if (data->data_len < BASE_MIN_SIZE) { + return true; + } + + net_buf_simple_init_with_data(&net_buf, (void *)data->data, + data->data_len); + + uuid = net_buf_simple_pull_mem(&net_buf, BT_UUID_SIZE_16); + + if (!bt_uuid_create(&broadcast_uuid.uuid, uuid, BT_UUID_SIZE_16)) { + BT_ERR("bt_uuid_create failed"); + return false; + } + + if (bt_uuid_cmp(&broadcast_uuid.uuid, BT_UUID_BASIC_AUDIO) != 0) { + /* Continue parsing */ + return true; + } + + codec_qos.pd = net_buf_simple_pull_le24(&net_buf); + sink->subgroup_count = net_buf_simple_pull_u8(&net_buf); + base.subgroup_count = sink->subgroup_count; + for (int i = 0; i < base.subgroup_count; i++) { + if (!net_buf_decode_subgroup(&net_buf, &base.subgroups[i])) { + BT_DBG("Failed to decode subgroup %d", i); + return false; + } + } + + if (sink->biginfo_received) { + uint8_t num_bis = 0; + + for (int i = 0; i < base.subgroup_count; i++) { + num_bis += base.subgroups[i].bis_count; + } + + if (num_bis > sink->biginfo_num_bis) { + BT_WARN("BASE contains more BIS than reported by BIGInfo"); + return false; + } + } + + SYS_SLIST_FOR_EACH_CONTAINER(&sink_cbs, listener, node) { + if (listener->base_recv != NULL) { + listener->base_recv(sink, &base); + } + } + + return false; +} + +static void pa_recv(struct bt_le_per_adv_sync *sync, + const struct bt_le_per_adv_sync_recv_info *info, + struct net_buf_simple *buf) +{ + struct bt_audio_broadcast_sink *sink = broadcast_sink_get_by_pa(sync); + + if (sink == NULL) { + /* Not a PA sync that we control */ + return; + } + + bt_data_parse(buf, pa_decode_base, (void *)sink); +} + +static void biginfo_recv(struct bt_le_per_adv_sync *sync, + const struct bt_iso_biginfo *biginfo) +{ + struct bt_audio_broadcast_sink_cb *listener; + struct bt_audio_broadcast_sink *sink; + + sink = broadcast_sink_get_by_pa(sync); + if (sink == NULL) { + /* Not ours */ + return; + } + + sink->biginfo_received = true; + sink->iso_interval = biginfo->iso_interval; + sink->biginfo_num_bis = biginfo->num_bis; + sink->big_encrypted = biginfo->encryption; + + SYS_SLIST_FOR_EACH_CONTAINER(&sink_cbs, listener, node) { + if (listener->syncable != NULL) { + listener->syncable(sink, biginfo->encryption); + } + } +} + +static uint16_t interval_to_sync_timeout(uint16_t interval) +{ + uint16_t timeout; + + /* Ensure that the following calculation does not overflow silently */ + __ASSERT(SYNC_RETRY_COUNT < 10, "SYNC_RETRY_COUNT shall be less than 10"); + + /* Add retries and convert to unit in 10's of ms */ + timeout = ((uint32_t)interval * SYNC_RETRY_COUNT) / 10; + + /* Enforce restraints */ + timeout = CLAMP(timeout, BT_GAP_PER_ADV_MIN_TIMEOUT, + BT_GAP_PER_ADV_MAX_TIMEOUT); + + return timeout; +} + +static void sync_broadcast_pa(const struct bt_le_scan_recv_info *info, + uint32_t broadcast_id) +{ + struct bt_audio_broadcast_sink_cb *listener; + struct bt_le_per_adv_sync_param param; + struct bt_audio_broadcast_sink *sink; + static bool pa_cb_registered; + int err; + + if (!pa_cb_registered) { + static struct bt_le_per_adv_sync_cb cb = { + .synced = pa_synced, + .recv = pa_recv, + .term = pa_term, + .biginfo = biginfo_recv + }; + + bt_le_per_adv_sync_cb_register(&cb); + } + + sink = broadcast_sink_free_get(); + /* Should never happen as we check for free entry before + * scanning + */ + __ASSERT(sink != NULL, "sink is NULL"); + + bt_addr_le_copy(¶m.addr, info->addr); + param.options = 0; + param.sid = info->sid; + param.skip = PA_SYNC_SKIP; + param.timeout = interval_to_sync_timeout(info->interval); + err = bt_le_per_adv_sync_create(¶m, &sink->pa_sync); + if (err != 0) { + BT_ERR("Could not sync to PA: %d", err); + err = bt_le_scan_stop(); + if (err != 0 && err != -EALREADY) { + BT_ERR("Could not stop scan: %d", err); + } + + SYS_SLIST_FOR_EACH_CONTAINER(&sink_cbs, listener, node) { + if (listener->scan_term != NULL) { + listener->scan_term(err); + } + } + } else { + sink->syncing = true; + sink->pa_interval = info->interval; + sink->broadcast_id = broadcast_id; + } +} + +static bool scan_check_and_sync_broadcast(struct bt_data *data, void *user_data) +{ + const struct bt_le_scan_recv_info *info = user_data; + struct bt_audio_broadcast_sink_cb *listener; + struct bt_uuid_16 adv_uuid; + uint32_t broadcast_id; + + if (sys_slist_is_empty(&sink_cbs)) { + /* Terminate early if we do not have any broadcast sink listeners */ + return false; + } + + if (data->type != BT_DATA_SVC_DATA16) { + return true; + } + + 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)) { + return true; + } + + if (broadcast_sink_syncing_get() != NULL) { + /* Already syncing, can maximum sync one */ + return true; + } + + broadcast_id = sys_get_le24(data->data + BT_UUID_SIZE_16); + + BT_DBG("Found broadcast source with address %s and id 0x%6X", + bt_addr_le_str(info->addr), broadcast_id); + + SYS_SLIST_FOR_EACH_CONTAINER(&sink_cbs, listener, node) { + if (listener->scan_recv != NULL) { + bool sync_pa = listener->scan_recv(info, broadcast_id); + + if (sync_pa) { + sync_broadcast_pa(info, broadcast_id); + break; + } + } + } + + /* Stop parsing */ + return false; +} + +static void broadcast_scan_recv(const struct bt_le_scan_recv_info *info, + struct net_buf_simple *ad) +{ + /* We are only interested in non-connectable periodic advertisers */ + if ((info->adv_props & BT_GAP_ADV_PROP_CONNECTABLE) || + info->interval == 0) { + return; + } + + bt_data_parse(ad, scan_check_and_sync_broadcast, (void *)info); +} + +static void broadcast_scan_timeout(void) +{ + struct bt_audio_broadcast_sink_cb *listener; + + bt_le_scan_cb_unregister(&broadcast_scan_cb); + + SYS_SLIST_FOR_EACH_CONTAINER(&sink_cbs, listener, node) { + if (listener->scan_term != NULL) { + listener->scan_term(-ETIME); + } + } +} + +void bt_audio_broadcast_sink_register_cb(struct bt_audio_broadcast_sink_cb *cb) +{ + sys_slist_append(&sink_cbs, &cb->node); +} + +int bt_audio_broadcast_sink_scan_start(const struct bt_le_scan_param *param) +{ + int err; + + CHECKIF(param == NULL) { + BT_DBG("param is NULL"); + return -EINVAL; + } + + CHECKIF(param->timeout != 0) { + /* This is to avoid having to re-implement the scan timeout + * callback as well, and can be modified later if requested + */ + BT_DBG("Scan param shall not have a timeout"); + return -EINVAL; + } + + if (sys_slist_is_empty(&sink_cbs)) { + BT_WARN("No broadcast sink callbacks registered"); + return -EINVAL; + } + + if (broadcast_sink_free_get() == NULL) { + BT_DBG("No more free broadcast sinks"); + return -ENOMEM; + } + + /* TODO: check for scan callback */ + err = bt_le_scan_start(param, NULL); + if (err == 0) { + broadcast_scan_cb.recv = broadcast_scan_recv; + broadcast_scan_cb.timeout = broadcast_scan_timeout; + bt_le_scan_cb_register(&broadcast_scan_cb); + } + + return err; +} + +int bt_audio_broadcast_sink_scan_stop(void) +{ + struct bt_audio_broadcast_sink_cb *listener; + struct bt_audio_broadcast_sink *sink; + int err; + + sink = broadcast_sink_syncing_get(); + if (sink != NULL) { + err = bt_le_per_adv_sync_delete(sink->pa_sync); + if (err != 0) { + BT_DBG("Could not delete PA sync: %d", err); + return err; + } + sink->pa_sync = NULL; + sink->syncing = false; + } + + err = bt_le_scan_stop(); + if (err == 0) { + bt_le_scan_cb_unregister(&broadcast_scan_cb); + } + + SYS_SLIST_FOR_EACH_CONTAINER(&sink_cbs, listener, node) { + if (listener->scan_term != NULL) { + listener->scan_term(0); + } + } + + return err; +} + +bool bt_audio_ep_is_broadcast_snk(const struct bt_audio_ep *ep) +{ + for (int i = 0; i < ARRAY_SIZE(broadcast_sink_eps); i++) { + if (PART_OF_ARRAY(broadcast_sink_eps[i], ep)) { + return true; + } + } + + return false; +} + +static void broadcast_sink_ep_init(struct bt_audio_ep *ep) +{ + BT_DBG("ep %p", ep); + + (void)memset(ep, 0, sizeof(*ep)); + ep->iso.ops = &broadcast_sink_iso_ops; + ep->iso.qos = &ep->iso_qos; + ep->iso.qos->rx = &ep->iso_rx; + ep->iso.qos->tx = &ep->iso_tx; +} + +static struct bt_audio_ep *broadcast_sink_new_ep(uint8_t index) +{ + struct bt_audio_ep *cache = NULL; + size_t size; + + cache = broadcast_sink_eps[index]; + size = ARRAY_SIZE(broadcast_sink_eps[index]); + + for (size_t i = 0; i < ARRAY_SIZE(broadcast_sink_eps[index]); i++) { + struct bt_audio_ep *ep = &cache[i]; + + /* If ep->stream is NULL the endpoint is unallocated */ + if (ep->stream == NULL) { + /* Initialize - It is up to the caller to allocate the + * stream pointer. + */ + broadcast_sink_ep_init(ep); + return ep; + } + } + + return NULL; +} + +static int bt_audio_broadcast_sink_setup_stream(uint8_t index, + struct bt_audio_stream *stream, + struct bt_codec *codec) +{ + static struct bt_iso_chan_io_qos sink_chan_io_qos; + static struct bt_codec_qos codec_qos; + struct bt_audio_ep *ep; + int err; + + if (stream->group != NULL) { + BT_DBG("Stream %p already in group %p", stream, stream->group); + return -EALREADY; + } + + ep = broadcast_sink_new_ep(index); + if (ep == NULL) { + BT_DBG("Could not allocate new broadcast endpoint"); + return -ENOMEM; + } + + bt_audio_stream_attach(NULL, stream, ep, codec); + /* TODO: The values of sink_chan_io_qos and codec_qos are not used, + * but the `rx` and `qos` pointers need to be set. This should be fixed. + */ + stream->iso->qos->rx = &sink_chan_io_qos; + stream->iso->qos->tx = NULL; + codec_qos.dir = BT_CODEC_QOS_IN; + stream->qos = &codec_qos; + err = bt_audio_codec_qos_to_iso_qos(stream->iso->qos, &codec_qos); + if (err) { + BT_ERR("Unable to convert codec QoS to ISO QoS"); + return err; + } + + return 0; +} + +static void broadcast_sink_cleanup_streams(struct bt_audio_broadcast_sink *sink) +{ + for (size_t i = 0; i < sink->stream_count; i++) { + struct bt_audio_stream *stream; + + stream = &sink->streams[i]; + + stream->ep->stream = NULL; + stream->ep = NULL; + stream->qos = NULL; + stream->codec = NULL; + stream->iso = NULL; + stream->group = NULL; + } +} + +static void broadcast_sink_cleanup(struct bt_audio_broadcast_sink *sink) +{ + broadcast_sink_cleanup_streams(sink); + (void)memset(sink, 0, sizeof(*sink)); +} + +int bt_audio_broadcast_sink_sync(struct bt_audio_broadcast_sink *sink, + uint32_t indexes_bitfield, + struct bt_audio_stream *streams, + struct bt_codec *codec, + const uint8_t broadcast_code[16]) +{ + struct bt_iso_big_sync_param param; + uint8_t stream_count; + int err; + + CHECKIF(sink == NULL) { + BT_DBG("sink is NULL"); + return -EINVAL; + } + + CHECKIF(indexes_bitfield == 0) { + BT_DBG("indexes_bitfield is 0"); + return -EINVAL; + } + + CHECKIF(indexes_bitfield & BIT(0)) { + BT_DBG("BIT(0) is not a valid BIS index"); + return -EINVAL; + } + + CHECKIF(streams == NULL) { + BT_DBG("streams is NULL"); + return -EINVAL; + } + + if (sink->pa_sync == NULL) { + BT_DBG("Sink is not PA synced"); + return -EINVAL; + } + + if (!sink->biginfo_received) { + /* TODO: We could store the request to sync and start the sync + * once the BIGInfo has been received, and then do the sync + * then. This would be similar how LE Create Connection works. + */ + BT_DBG("BIGInfo not received, cannot sync yet"); + return -EAGAIN; + } + + CHECKIF(sink->big_encrypted && broadcast_code == NULL) { + BT_DBG("Broadcast code required"); + return -EINVAL; + } + + /* Validate that number of bits set is less than number of streams */ + stream_count = 0; + for (int i = 1; i < BT_ISO_MAX_GROUP_ISO_COUNT; i++) { + if ((indexes_bitfield & BIT(i)) != 0) { + stream_count++; + } + } + + sink->stream_count = stream_count; + sink->streams = streams; + sink->codec = codec; + for (size_t i = 0; i < stream_count; i++) { + struct bt_audio_stream *stream; + + stream = &streams[i]; + + err = bt_audio_broadcast_sink_setup_stream(sink->index, stream, + sink->codec); + if (err != 0) { + BT_DBG("Failed to setup streams[%zu]: %d", i, err); + broadcast_sink_cleanup_streams(sink); + return err; + } + + sink->bis[i] = &stream->ep->iso; + } + + param.bis_channels = sink->bis; + param.num_bis = sink->stream_count; + param.bis_bitfield = indexes_bitfield; + param.mse = 0; /* Let controller decide */ + param.sync_timeout = interval_to_sync_timeout(sink->iso_interval); + param.encryption = sink->big_encrypted; /* TODO */ + if (param.encryption) { + memcpy(param.bcode, broadcast_code, sizeof(param.bcode)); + } else { + memset(param.bcode, 0, sizeof(param.bcode)); + } + + err = bt_iso_big_sync(sink->pa_sync, ¶m, &sink->big); + if (err != 0) { + broadcast_sink_cleanup_streams(sink); + return err; + } + + for (size_t i = 0; i < stream_count; i++) { + struct bt_audio_ep *ep = streams[i].ep; + + ep->broadcast_sink = sink; + broadcast_sink_set_ep_state(ep, + BT_AUDIO_EP_STATE_QOS_CONFIGURED); + } + + return 0; +} + +int bt_audio_broadcast_sink_stop(struct bt_audio_broadcast_sink *sink) +{ + struct bt_audio_stream *stream; + int err; + + CHECKIF(sink == NULL) { + BT_DBG("sink is NULL"); + return -EINVAL; + } + + stream = &sink->streams[0]; + + if (stream == NULL) { + BT_DBG("stream is NULL"); + return -EINVAL; + } + + if (stream->ep == NULL) { + BT_DBG("stream->ep is NULL"); + return -EINVAL; + } + + if (stream->ep->status.state != BT_AUDIO_EP_STATE_STREAMING && + stream->ep->status.state != BT_AUDIO_EP_STATE_QOS_CONFIGURED) { + BT_DBG("Broadcast sink stream %p invalid state: %u", + stream, stream->ep->status.state); + return -EBADMSG; + } + + err = bt_iso_big_terminate(sink->big); + if (err) { + BT_DBG("Failed to terminate BIG (err %d)", err); + return err; + } + + sink->big = NULL; + sink->stream_count = 0; + /* Channel states will be updated in the ep_iso_disconnected function */ + + return 0; +} + +int bt_audio_broadcast_sink_delete(struct bt_audio_broadcast_sink *sink) +{ + struct bt_audio_stream *stream; + int err; + + CHECKIF(sink == NULL) { + BT_DBG("sink is NULL"); + return -EINVAL; + } + + stream = &sink->streams[0]; + + if (stream != NULL && stream->ep != NULL) { + BT_DBG("Sink is not stopped"); + return -EBADMSG; + } + + if (sink->pa_sync == NULL) { + BT_DBG("Broadcast sink is already deleted"); + return -EALREADY; + } + + err = bt_le_per_adv_sync_delete(sink->pa_sync); + if (err != 0) { + BT_DBG("Failed to delete periodic advertising sync (err %d)", + err); + return err; + } + + /* Reset the broadcast sink */ + broadcast_sink_cleanup(sink); + + return 0; +} diff --git a/subsys/bluetooth/audio/stream.c b/subsys/bluetooth/audio/stream.c index 41cd8f14e28..ac84870bcac 100644 --- a/subsys/bluetooth/audio/stream.c +++ b/subsys/bluetooth/audio/stream.c @@ -185,9 +185,10 @@ bool bt_audio_valid_qos(const struct bt_codec_qos *qos) static bool bt_audio_stream_is_broadcast(const struct bt_audio_stream *stream) { - /* TODO: Add broadcast sink */ return (IS_ENABLED(CONFIG_BT_AUDIO_BROADCAST_SOURCE) && - bt_audio_ep_is_broadcast_src(stream->ep)); + bt_audio_ep_is_broadcast_src(stream->ep)) || + (IS_ENABLED(CONFIG_BT_AUDIO_BROADCAST_SINK) && + bt_audio_ep_is_broadcast_snk(stream->ep)); } bool bt_audio_valid_stream_qos(const struct bt_audio_stream *stream, diff --git a/tests/bluetooth/shell/audio.conf b/tests/bluetooth/shell/audio.conf index 39e39b55662..d874a4fa439 100644 --- a/tests/bluetooth/shell/audio.conf +++ b/tests/bluetooth/shell/audio.conf @@ -39,6 +39,7 @@ CONFIG_BT_AUDIO=y CONFIG_BT_AUDIO_UNICAST_SERVER=y CONFIG_BT_AUDIO_UNICAST_CLIENT=y CONFIG_BT_AUDIO_BROADCAST_SOURCE=y +CONFIG_BT_AUDIO_BROADCAST_SINK=y CONFIG_BT_ISO_TX_BUF_COUNT=5 CONFIG_BT_VOCS_MAX_INSTANCE_COUNT=1 CONFIG_BT_VOCS_CLIENT_MAX_INSTANCE_COUNT=1