In order to bring consistency in-tree, migrate all subsystems code to the new prefix <zephyr/...>. Note that the conversion has been scripted, refer to zephyrproject-rtos#45388 for more details. Signed-off-by: Gerard Marull-Paretas <gerard.marull@nordicsemi.no>
1062 lines
26 KiB
C
1062 lines
26 KiB
C
/* Bluetooth Audio Broadcast Sink */
|
|
|
|
/*
|
|
* Copyright (c) 2021-2022 Nordic Semiconductor ASA
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#include <zephyr/zephyr.h>
|
|
#include <zephyr/sys/byteorder.h>
|
|
#include <zephyr/sys/check.h>
|
|
|
|
#include <zephyr/bluetooth/bluetooth.h>
|
|
#include <zephyr/bluetooth/conn.h>
|
|
#include <zephyr/bluetooth/gatt.h>
|
|
#include <zephyr/bluetooth/audio/audio.h>
|
|
|
|
#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);
|
|
} else {
|
|
BT_WARN("No callback for recv set");
|
|
}
|
|
}
|
|
|
|
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->started != NULL) {
|
|
ops->started(ep->stream);
|
|
} else {
|
|
BT_WARN("No callback for connected set");
|
|
}
|
|
}
|
|
|
|
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->stopped != NULL) {
|
|
ops->stopped(stream);
|
|
} else {
|
|
BT_WARN("No callback for stopped set");
|
|
}
|
|
}
|
|
|
|
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 (!IN_RANGE(bis->index, BT_ISO_BIS_INDEX_MIN, BT_ISO_BIS_INDEX_MAX)) {
|
|
BT_DBG("Invalid BIS index %u", bis->index);
|
|
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);
|
|
|
|
if (sink->subgroup_count > ARRAY_SIZE(base.subgroups)) {
|
|
BT_DBG("Cannot decode BASE with %u subgroups (max supported is %zu)",
|
|
sink->subgroup_count, ARRAY_SIZE(base.subgroups));
|
|
|
|
return false;
|
|
}
|
|
|
|
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);
|
|
|
|
pa_cb_registered = true;
|
|
}
|
|
|
|
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;
|
|
ep->dir = BT_AUDIO_SINK;
|
|
}
|
|
|
|
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;
|
|
stream->qos = &codec_qos;
|
|
err = bt_audio_codec_qos_to_iso_qos(stream->iso->qos->rx, &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];
|
|
if (stream == NULL) {
|
|
continue;
|
|
}
|
|
|
|
if (stream->ep != NULL) {
|
|
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++;
|
|
}
|
|
}
|
|
|
|
for (size_t i = 0; i < stream_count; i++) {
|
|
CHECKIF(streams[i] == NULL) {
|
|
BT_DBG("streams[%zu] is NULL", i);
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
|
|
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;
|
|
sink->streams = NULL;
|
|
/* 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 **streams;
|
|
int err;
|
|
|
|
CHECKIF(sink == NULL) {
|
|
BT_DBG("sink is NULL");
|
|
return -EINVAL;
|
|
}
|
|
|
|
streams = sink->streams;
|
|
if (streams != NULL && streams[0] != NULL && streams[0]->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;
|
|
}
|