You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
4529 lines
120 KiB
4529 lines
120 KiB
/* @file |
|
* @brief Bluetooth Unicast Client |
|
*/ |
|
|
|
/* |
|
* Copyright (c) 2020 Intel Corporation |
|
* Copyright (c) 2022-2025 Nordic Semiconductor ASA |
|
* |
|
* SPDX-License-Identifier: Apache-2.0 |
|
*/ |
|
|
|
#include <errno.h> |
|
#include <stdbool.h> |
|
#include <stddef.h> |
|
#include <stdint.h> |
|
#include <string.h> |
|
|
|
#include <zephyr/autoconf.h> |
|
#include <zephyr/bluetooth/att.h> |
|
#include <zephyr/bluetooth/bluetooth.h> |
|
#include <zephyr/bluetooth/conn.h> |
|
#include <zephyr/bluetooth/gap.h> |
|
#include <zephyr/bluetooth/gatt.h> |
|
#include <zephyr/bluetooth/hci.h> |
|
#include <zephyr/bluetooth/audio/audio.h> |
|
#include <zephyr/bluetooth/audio/bap.h> |
|
#include <zephyr/bluetooth/hci_types.h> |
|
#include <zephyr/bluetooth/iso.h> |
|
#include <zephyr/bluetooth/uuid.h> |
|
#include <zephyr/kernel.h> |
|
#include <zephyr/logging/log.h> |
|
#include <zephyr/net_buf.h> |
|
#include <zephyr/sys/__assert.h> |
|
#include <zephyr/sys/atomic.h> |
|
#include <zephyr/sys/byteorder.h> |
|
#include <zephyr/sys/check.h> |
|
#include <zephyr/sys/slist.h> |
|
#include <zephyr/sys/util.h> |
|
#include <zephyr/sys/util_macro.h> |
|
#include <zephyr/toolchain.h> |
|
|
|
#include "../host/hci_core.h" |
|
#include "../host/conn_internal.h" |
|
#include "../host/iso_internal.h" |
|
|
|
#include "ascs_internal.h" |
|
#include "audio_internal.h" |
|
#include "bap_iso.h" |
|
#include "bap_endpoint.h" |
|
#include "bap_unicast_client_internal.h" |
|
#include "pacs_internal.h" |
|
|
|
BUILD_ASSERT(CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT > 0 || |
|
CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT > 0, |
|
"CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT or " |
|
"CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT shall be non-zero"); |
|
|
|
BUILD_ASSERT(CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT == 0 || |
|
CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT > 1, |
|
"CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT shall be either 0 or > 1"); |
|
|
|
BUILD_ASSERT(CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT == 0 || |
|
CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT > 1, |
|
"CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT shall be either 0 or > 1"); |
|
|
|
LOG_MODULE_REGISTER(bt_bap_unicast_client, CONFIG_BT_BAP_UNICAST_CLIENT_LOG_LEVEL); |
|
|
|
#define PAC_DIR_UNUSED(dir) ((dir) != BT_AUDIO_DIR_SINK && (dir) != BT_AUDIO_DIR_SOURCE) |
|
#define BAP_HANDLE_UNUSED 0x0000U |
|
struct bt_bap_unicast_client_ep { |
|
uint16_t handle; |
|
uint16_t cp_handle; |
|
struct bt_gatt_subscribe_params subscribe; |
|
struct bt_gatt_discover_params discover; |
|
struct bt_bap_ep ep; |
|
struct k_work_delayable ase_read_work; |
|
|
|
/* Bool to help handle different order of CP and ASE notification when releasing */ |
|
bool release_requested; |
|
bool cp_ntf_pending; |
|
}; |
|
|
|
static const struct bt_uuid *snk_uuid = BT_UUID_PACS_SNK; |
|
static const struct bt_uuid *src_uuid = BT_UUID_PACS_SRC; |
|
static const struct bt_uuid *pacs_context_uuid = BT_UUID_PACS_SUPPORTED_CONTEXT; |
|
static const struct bt_uuid *pacs_snk_loc_uuid = BT_UUID_PACS_SNK_LOC; |
|
static const struct bt_uuid *pacs_src_loc_uuid = BT_UUID_PACS_SRC_LOC; |
|
static const struct bt_uuid *pacs_avail_ctx_uuid = BT_UUID_PACS_AVAILABLE_CONTEXT; |
|
static const struct bt_uuid *ase_snk_uuid = BT_UUID_ASCS_ASE_SNK; |
|
static const struct bt_uuid *ase_src_uuid = BT_UUID_ASCS_ASE_SRC; |
|
static const struct bt_uuid *cp_uuid = BT_UUID_ASCS_ASE_CP; |
|
|
|
static struct bt_bap_unicast_group unicast_groups[UNICAST_GROUP_CNT]; |
|
|
|
enum unicast_client_flag { |
|
UNICAST_CLIENT_FLAG_BUSY, |
|
|
|
UNICAST_CLIENT_FLAG_NUM_FLAGS, /* keep as last */ |
|
}; |
|
|
|
static struct unicast_client { |
|
#if CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT > 0 |
|
struct bt_bap_unicast_client_ep snks[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT]; |
|
#endif /* CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT > 0 */ |
|
#if CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT > 0 |
|
struct bt_bap_unicast_client_ep srcs[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT]; |
|
#endif /* CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT > 0 */ |
|
|
|
struct bt_gatt_subscribe_params cp_subscribe; |
|
struct bt_gatt_subscribe_params snk_loc_subscribe; |
|
struct bt_gatt_subscribe_params src_loc_subscribe; |
|
struct bt_gatt_subscribe_params avail_ctx_subscribe; |
|
|
|
/* TODO: We should be able to reduce the number of discover params for |
|
* CCCD discovery, but requires additional work if it is to support an |
|
* arbitrary number of EATT bearers, as control point and location CCCD |
|
* discovery needs to be done in serial to avoid using the same discover |
|
* parameters twice |
|
*/ |
|
struct bt_gatt_discover_params loc_cc_disc; |
|
struct bt_gatt_discover_params avail_ctx_cc_disc; |
|
|
|
/* Discovery parameters */ |
|
enum bt_audio_dir dir; |
|
union { |
|
struct bt_gatt_read_params read_params; |
|
struct bt_gatt_discover_params disc_params; |
|
struct bt_gatt_write_params write_params; |
|
}; |
|
|
|
/* The att_buf needs to use the maximum ATT attribute size as a single |
|
* PAC record may use the full size |
|
*/ |
|
uint8_t att_buf[BT_ATT_MAX_ATTRIBUTE_LEN]; |
|
struct net_buf_simple net_buf; |
|
|
|
ATOMIC_DEFINE(flags, UNICAST_CLIENT_FLAG_NUM_FLAGS); |
|
} uni_cli_insts[CONFIG_BT_MAX_CONN]; |
|
|
|
static sys_slist_t unicast_client_cbs = SYS_SLIST_STATIC_INIT(&unicast_client_cbs); |
|
|
|
/* TODO: Move the functions to avoid these prototypes */ |
|
static int unicast_client_ep_set_metadata(struct bt_bap_ep *ep, void *data, uint8_t len, |
|
struct bt_audio_codec_cfg *codec_cfg); |
|
|
|
static int unicast_client_ep_set_codec_cfg(struct bt_bap_ep *ep, uint8_t id, uint16_t cid, |
|
uint16_t vid, void *data, uint8_t len, |
|
struct bt_audio_codec_cfg *codec_cfg); |
|
static int unicast_client_ep_start(struct bt_bap_ep *ep, |
|
struct net_buf_simple *buf); |
|
|
|
static int unicast_client_ase_discover(struct bt_conn *conn, uint16_t start_handle); |
|
|
|
static void unicast_client_reset(struct bt_bap_ep *ep, uint8_t reason); |
|
|
|
static void delayed_ase_read_handler(struct k_work *work); |
|
static void unicast_client_ep_set_status(struct bt_bap_ep *ep, struct net_buf_simple *buf); |
|
|
|
static int unicast_client_send_start(struct bt_bap_ep *ep) |
|
{ |
|
if (ep->receiver_ready != true || ep->dir != BT_AUDIO_DIR_SOURCE) { |
|
LOG_DBG("Invalid ep %p %u %s", |
|
ep, ep->receiver_ready, bt_audio_dir_str(ep->dir)); |
|
|
|
return -EINVAL; |
|
} |
|
|
|
struct bt_ascs_start_op *req; |
|
struct net_buf_simple *buf; |
|
int err; |
|
|
|
buf = bt_bap_unicast_client_ep_create_pdu(ep->stream->conn, BT_ASCS_START_OP); |
|
if (buf == NULL) { |
|
LOG_DBG("Could not create PDU"); |
|
return -EBUSY; |
|
} |
|
|
|
req = net_buf_simple_add(buf, sizeof(*req)); |
|
req->num_ases = 1U; |
|
|
|
err = unicast_client_ep_start(ep, buf); |
|
if (err != 0) { |
|
LOG_DBG("unicast_client_ep_start failed: %d", |
|
err); |
|
|
|
return err; |
|
} |
|
|
|
err = bt_bap_unicast_client_ep_send(ep->stream->conn, ep, buf); |
|
if (err != 0) { |
|
LOG_DBG("bt_bap_unicast_client_ep_send failed: %d", err); |
|
|
|
return err; |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
static void unicast_client_ep_idle_state(struct bt_bap_ep *ep); |
|
|
|
static struct bt_bap_stream *audio_stream_by_ep_id(const struct bt_conn *conn, |
|
uint8_t id) |
|
{ |
|
#if CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT > 0 || CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT > 0 |
|
const uint8_t conn_index = bt_conn_index(conn); |
|
#endif /* CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT > 0 || \ |
|
* CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT > 0 \ |
|
*/ |
|
|
|
#if CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT > 0 |
|
for (size_t i = 0U; i < ARRAY_SIZE(uni_cli_insts[conn_index].snks); i++) { |
|
const struct bt_bap_unicast_client_ep *client_ep = |
|
&uni_cli_insts[conn_index].snks[i]; |
|
|
|
if (client_ep->ep.status.id == id) { |
|
return client_ep->ep.stream; |
|
} |
|
} |
|
#endif /* CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT > 0 */ |
|
|
|
#if CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT > 0 |
|
for (size_t i = 0U; i < ARRAY_SIZE(uni_cli_insts[conn_index].srcs); i++) { |
|
const struct bt_bap_unicast_client_ep *client_ep = |
|
&uni_cli_insts[conn_index].srcs[i]; |
|
|
|
if (client_ep->ep.status.id == id) { |
|
return client_ep->ep.stream; |
|
} |
|
} |
|
#endif /* CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT > 0 */ |
|
|
|
return NULL; |
|
} |
|
|
|
static void reset_att_buf(struct unicast_client *client) |
|
{ |
|
net_buf_simple_init_with_data(&client->net_buf, &client->att_buf, sizeof(client->att_buf)); |
|
net_buf_simple_reset(&client->net_buf); |
|
} |
|
|
|
#if defined(CONFIG_BT_AUDIO_RX) |
|
static void unicast_client_ep_iso_recv(struct bt_iso_chan *chan, |
|
const struct bt_iso_recv_info *info, struct net_buf *buf) |
|
{ |
|
struct bt_bap_iso *iso = CONTAINER_OF(chan, struct bt_bap_iso, chan); |
|
const struct bt_bap_stream_ops *ops; |
|
struct bt_bap_stream *stream; |
|
struct bt_bap_ep *ep = iso->rx.ep; |
|
|
|
if (ep == NULL) { |
|
/* In the case that the CIS has been setup as bidirectional, and |
|
* only one of the directions have an ASE configured yet, |
|
* we should only care about valid ISO packets when doing this |
|
* check. The reason is that some controllers send HCI ISO data |
|
* packets to the host, even if no SDU was sent on the remote |
|
* side. This basically means that empty PDUs are sent to the |
|
* host as HCI ISO data packets, which we should just ignore |
|
*/ |
|
if ((info->flags & BT_ISO_FLAGS_VALID) != 0) { |
|
LOG_DBG("Valid ISO packet of len %zu received for iso %p not bound with ep", |
|
net_buf_frags_len(buf), chan); |
|
} |
|
|
|
return; |
|
} |
|
|
|
if (ep->status.state != BT_BAP_EP_STATE_STREAMING) { |
|
if (IS_ENABLED(CONFIG_BT_BAP_DEBUG_STREAM_DATA)) { |
|
LOG_DBG("ep %p is not in the streaming state: %s", ep, |
|
bt_bap_ep_state_str(ep->status.state)); |
|
} |
|
|
|
return; |
|
} |
|
|
|
stream = ep->stream; |
|
if (stream == NULL) { |
|
LOG_ERR("No stream for ep %p", ep); |
|
return; |
|
} |
|
|
|
ops = stream->ops; |
|
|
|
if (IS_ENABLED(CONFIG_BT_BAP_DEBUG_STREAM_DATA)) { |
|
LOG_DBG("stream %p ep %p len %zu", stream, ep, net_buf_frags_len(buf)); |
|
} |
|
|
|
if (ops != NULL && ops->recv != NULL) { |
|
ops->recv(stream, info, buf); |
|
} else { |
|
LOG_WRN("No callback for recv set"); |
|
} |
|
} |
|
#endif /* CONFIG_BT_AUDIO_RX */ |
|
|
|
#if defined(CONFIG_BT_AUDIO_TX) |
|
static void unicast_client_ep_iso_sent(struct bt_iso_chan *chan) |
|
{ |
|
struct bt_bap_iso *iso = CONTAINER_OF(chan, struct bt_bap_iso, chan); |
|
struct bt_bap_stream *stream; |
|
struct bt_bap_ep *ep = iso->tx.ep; |
|
|
|
if (ep == NULL) { |
|
LOG_ERR("iso %p not bound with ep", chan); |
|
return; |
|
} |
|
|
|
stream = ep->stream; |
|
if (stream == NULL) { |
|
LOG_ERR("No stream for ep %p", ep); |
|
return; |
|
} |
|
|
|
if (IS_ENABLED(CONFIG_BT_BAP_DEBUG_STREAM_DATA)) { |
|
LOG_DBG("stream %p ep %p", stream, ep); |
|
} |
|
|
|
if (stream->ops != NULL && stream->ops->sent != NULL) { |
|
stream->ops->sent(stream); |
|
} |
|
} |
|
#endif /* CONFIG_BT_AUDIO_TX */ |
|
|
|
static void unicast_client_ep_iso_connected(struct bt_bap_ep *ep) |
|
{ |
|
const struct bt_bap_stream_ops *stream_ops; |
|
struct bt_bap_stream *stream; |
|
|
|
if (ep->unicast_group != NULL) { |
|
ep->unicast_group->has_been_connected = true; |
|
} |
|
|
|
if (ep->status.state != BT_BAP_EP_STATE_ENABLING) { |
|
LOG_DBG("endpoint not in enabling state: %s", |
|
bt_bap_ep_state_str(ep->status.state)); |
|
return; |
|
} |
|
|
|
stream = ep->stream; |
|
if (stream == NULL) { |
|
LOG_ERR("No stream for ep %p", ep); |
|
return; |
|
} |
|
|
|
LOG_DBG("stream %p ep %p dir %s receiver_ready %u", |
|
stream, ep, bt_audio_dir_str(ep->dir), ep->receiver_ready); |
|
|
|
#if defined(CONFIG_BT_BAP_DEBUG_STREAM_SEQ_NUM) |
|
/* reset sequence number */ |
|
stream->_prev_seq_num = 0U; |
|
#endif /* CONFIG_BT_BAP_DEBUG_STREAM_SEQ_NUM */ |
|
|
|
stream_ops = stream->ops; |
|
if (stream_ops != NULL && stream_ops->connected != NULL) { |
|
stream_ops->connected(stream); |
|
} |
|
} |
|
|
|
static void unicast_client_iso_connected(struct bt_iso_chan *chan) |
|
{ |
|
struct bt_bap_iso *iso = CONTAINER_OF(chan, struct bt_bap_iso, chan); |
|
|
|
if (iso->rx.ep == NULL && iso->tx.ep == NULL) { |
|
LOG_ERR("iso %p not bound with ep", chan); |
|
return; |
|
} |
|
|
|
if (iso->rx.ep != NULL) { |
|
unicast_client_ep_iso_connected(iso->rx.ep); |
|
} |
|
|
|
if (iso->tx.ep != NULL) { |
|
unicast_client_ep_iso_connected(iso->tx.ep); |
|
} |
|
} |
|
|
|
static void unicast_client_ep_iso_disconnected(struct bt_bap_ep *ep, uint8_t reason) |
|
{ |
|
const struct bt_bap_stream_ops *stream_ops; |
|
struct bt_bap_stream *stream; |
|
|
|
stream = ep->stream; |
|
if (stream == NULL) { |
|
LOG_ERR("Stream not associated with an ep"); |
|
return; |
|
} |
|
|
|
LOG_DBG("stream %p ep %p reason 0x%02x", stream, ep, reason); |
|
ep->reason = reason; |
|
|
|
stream_ops = stream->ops; |
|
if (stream_ops != NULL && stream_ops->disconnected != NULL) { |
|
stream_ops->disconnected(stream, reason); |
|
} |
|
|
|
/* If we were in the idle state when we started the ISO disconnection |
|
* then we need to call unicast_client_ep_idle_state again when |
|
* the ISO has finalized the disconnection |
|
*/ |
|
if (ep->status.state == BT_BAP_EP_STATE_IDLE) { |
|
|
|
unicast_client_ep_idle_state(ep); |
|
|
|
if (stream->conn != NULL) { |
|
struct bt_conn_info conn_info; |
|
int err; |
|
|
|
err = bt_conn_get_info(stream->conn, &conn_info); |
|
if (err != 0 || conn_info.state == BT_CONN_STATE_DISCONNECTED) { |
|
/* Retrigger the reset of the EP if the ACL is disconnected before |
|
* the ISO is disconnected |
|
*/ |
|
unicast_client_reset(ep, reason); |
|
} |
|
} |
|
} |
|
} |
|
|
|
static void unicast_client_iso_disconnected(struct bt_iso_chan *chan, uint8_t reason) |
|
{ |
|
struct bt_bap_iso *iso = CONTAINER_OF(chan, struct bt_bap_iso, chan); |
|
|
|
if (iso->rx.ep == NULL && iso->tx.ep == NULL) { |
|
LOG_ERR("iso %p not bound with ep", chan); |
|
return; |
|
} |
|
|
|
if (iso->rx.ep != NULL) { |
|
unicast_client_ep_iso_disconnected(iso->rx.ep, reason); |
|
} |
|
|
|
if (iso->tx.ep != NULL) { |
|
unicast_client_ep_iso_disconnected(iso->tx.ep, reason); |
|
} |
|
} |
|
|
|
static struct bt_iso_chan_ops unicast_client_iso_ops = { |
|
#if defined(CONFIG_BT_AUDIO_RX) |
|
.recv = unicast_client_ep_iso_recv, |
|
#endif /* CONFIG_BT_AUDIO_RX */ |
|
#if defined(CONFIG_BT_AUDIO_TX) |
|
.sent = unicast_client_ep_iso_sent, |
|
#endif /* CONFIG_BT_AUDIO_TX */ |
|
.connected = unicast_client_iso_connected, |
|
.disconnected = unicast_client_iso_disconnected, |
|
}; |
|
|
|
bool bt_bap_ep_is_unicast_client(const struct bt_bap_ep *ep) |
|
{ |
|
for (size_t i = 0U; i < ARRAY_SIZE(uni_cli_insts); i++) { |
|
#if CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT > 0 |
|
if (PART_OF_ARRAY(uni_cli_insts[i].snks, ep)) { |
|
return true; |
|
} |
|
#endif /* CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT > 0 */ |
|
|
|
#if CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT > 0 |
|
if (PART_OF_ARRAY(uni_cli_insts[i].srcs, ep)) { |
|
return true; |
|
} |
|
#endif /* CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT > 0 */ |
|
} |
|
|
|
return false; |
|
} |
|
|
|
static void unicast_client_ep_init(struct bt_bap_ep *ep, uint16_t handle, uint8_t dir) |
|
{ |
|
struct bt_bap_unicast_client_ep *client_ep; |
|
|
|
LOG_DBG("ep %p dir %s handle 0x%04x", ep, bt_audio_dir_str(dir), handle); |
|
|
|
client_ep = CONTAINER_OF(ep, struct bt_bap_unicast_client_ep, ep); |
|
|
|
(void)memset(ep, 0, sizeof(*ep)); |
|
client_ep->handle = handle; |
|
ep->status.id = 0U; |
|
ep->dir = dir; |
|
ep->reason = BT_HCI_ERR_SUCCESS; |
|
k_work_init_delayable(&client_ep->ase_read_work, delayed_ase_read_handler); |
|
} |
|
|
|
static struct bt_bap_ep *unicast_client_ep_find(struct bt_conn *conn, uint16_t handle) |
|
{ |
|
uint8_t index; |
|
|
|
index = bt_conn_index(conn); |
|
|
|
#if CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT > 0 |
|
for (size_t i = 0U; i < ARRAY_SIZE(uni_cli_insts[index].snks); i++) { |
|
struct bt_bap_unicast_client_ep *client_ep = &uni_cli_insts[index].snks[i]; |
|
|
|
if ((handle && client_ep->handle == handle) || (!handle && client_ep->handle)) { |
|
return &client_ep->ep; |
|
} |
|
} |
|
#endif /* CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT > 0 */ |
|
|
|
#if CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT > 0 |
|
for (size_t i = 0U; i < ARRAY_SIZE(uni_cli_insts[index].srcs); i++) { |
|
struct bt_bap_unicast_client_ep *client_ep = &uni_cli_insts[index].srcs[i]; |
|
|
|
if ((handle && client_ep->handle == handle) || (!handle && client_ep->handle)) { |
|
return &client_ep->ep; |
|
} |
|
} |
|
#endif /* CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT > 0 */ |
|
|
|
return NULL; |
|
} |
|
|
|
struct bt_bap_iso *bt_bap_unicast_client_new_audio_iso(void) |
|
{ |
|
struct bt_bap_iso *bap_iso; |
|
|
|
bap_iso = bt_bap_iso_new(); |
|
if (bap_iso == NULL) { |
|
return NULL; |
|
} |
|
|
|
bt_bap_iso_init(bap_iso, &unicast_client_iso_ops); |
|
|
|
LOG_DBG("New bap_iso %p", bap_iso); |
|
|
|
return bap_iso; |
|
} |
|
|
|
static struct bt_bap_ep *unicast_client_ep_new(struct bt_conn *conn, enum bt_audio_dir dir, |
|
uint16_t handle) |
|
{ |
|
size_t i, size; |
|
uint8_t index; |
|
struct bt_bap_unicast_client_ep *cache; |
|
|
|
index = bt_conn_index(conn); |
|
|
|
switch (dir) { |
|
#if CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT > 0 |
|
case BT_AUDIO_DIR_SINK: |
|
cache = uni_cli_insts[index].snks; |
|
size = ARRAY_SIZE(uni_cli_insts[index].snks); |
|
break; |
|
#endif /* CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT > 0 */ |
|
#if CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT > 0 |
|
case BT_AUDIO_DIR_SOURCE: |
|
cache = uni_cli_insts[index].srcs; |
|
size = ARRAY_SIZE(uni_cli_insts[index].srcs); |
|
break; |
|
#endif /* CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT > 0 */ |
|
default: |
|
return NULL; |
|
} |
|
|
|
for (i = 0; i < size; i++) { |
|
struct bt_bap_unicast_client_ep *client_ep = &cache[i]; |
|
|
|
if (!client_ep->handle) { |
|
unicast_client_ep_init(&client_ep->ep, handle, dir); |
|
return &client_ep->ep; |
|
} |
|
} |
|
|
|
return NULL; |
|
} |
|
|
|
static struct bt_bap_ep *unicast_client_ep_get(struct bt_conn *conn, enum bt_audio_dir dir, |
|
uint16_t handle) |
|
{ |
|
struct bt_bap_ep *ep; |
|
|
|
ep = unicast_client_ep_find(conn, handle); |
|
if (ep || !handle) { |
|
return ep; |
|
} |
|
|
|
return unicast_client_ep_new(conn, dir, handle); |
|
} |
|
|
|
static void unicast_client_ep_set_local_idle_state(struct bt_bap_ep *ep) |
|
{ |
|
struct bt_ascs_ase_status status = { |
|
.id = ep->status.id, |
|
.state = BT_BAP_EP_STATE_IDLE, |
|
}; |
|
struct net_buf_simple buf; |
|
|
|
net_buf_simple_init_with_data(&buf, &status, sizeof(status)); |
|
|
|
unicast_client_ep_set_status(ep, &buf); |
|
} |
|
|
|
static void unicast_client_notify_location(struct bt_conn *conn, enum bt_audio_dir dir, |
|
enum bt_audio_location loc) |
|
{ |
|
struct bt_bap_unicast_client_cb *listener, *next; |
|
|
|
SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&unicast_client_cbs, listener, next, _node) { |
|
if (listener->location != NULL) { |
|
listener->location(conn, dir, loc); |
|
} |
|
} |
|
} |
|
|
|
static void unicast_client_notify_available_contexts(struct bt_conn *conn, |
|
enum bt_audio_context snk_ctx, |
|
enum bt_audio_context src_ctx) |
|
{ |
|
struct bt_bap_unicast_client_cb *listener, *next; |
|
|
|
SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&unicast_client_cbs, listener, next, _node) { |
|
if (listener->available_contexts != NULL) { |
|
listener->available_contexts(conn, snk_ctx, src_ctx); |
|
} |
|
} |
|
} |
|
|
|
static void unicast_client_notify_pac_record(struct bt_conn *conn, |
|
const struct bt_audio_codec_cap *codec_cap) |
|
{ |
|
const struct unicast_client *client = &uni_cli_insts[bt_conn_index(conn)]; |
|
struct bt_bap_unicast_client_cb *listener, *next; |
|
const enum bt_audio_dir dir = client->dir; |
|
|
|
/* TBD: Since the PAC records are optionally notifiable we may want to supply the |
|
* index and total count of records in the callback, so that it easier for the |
|
* upper layers to determine when a new set of PAC records is being reported. |
|
*/ |
|
|
|
SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&unicast_client_cbs, listener, next, _node) { |
|
if (listener->pac_record != NULL) { |
|
listener->pac_record(conn, dir, codec_cap); |
|
} |
|
} |
|
} |
|
|
|
static void unicast_client_notify_endpoint(struct bt_conn *conn, struct bt_bap_ep *ep) |
|
{ |
|
const struct unicast_client *client = &uni_cli_insts[bt_conn_index(conn)]; |
|
struct bt_bap_unicast_client_cb *listener, *next; |
|
const enum bt_audio_dir dir = client->dir; |
|
|
|
SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&unicast_client_cbs, listener, next, _node) { |
|
if (listener->endpoint != NULL) { |
|
listener->endpoint(conn, dir, ep); |
|
} |
|
} |
|
} |
|
|
|
static void unicast_client_discover_complete(struct bt_conn *conn, int err) |
|
{ |
|
struct unicast_client *client = &uni_cli_insts[bt_conn_index(conn)]; |
|
struct bt_bap_unicast_client_cb *listener, *next; |
|
const enum bt_audio_dir dir = client->dir; |
|
|
|
/* Discover complete - Reset discovery values */ |
|
client->dir = 0U; |
|
reset_att_buf(client); |
|
atomic_clear_bit(client->flags, UNICAST_CLIENT_FLAG_BUSY); |
|
|
|
SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&unicast_client_cbs, listener, next, _node) { |
|
if (listener->discover != NULL) { |
|
listener->discover(conn, err, dir); |
|
} |
|
} |
|
} |
|
|
|
static void unicast_client_notify_ep_config(struct bt_bap_stream *stream, |
|
enum bt_bap_ascs_rsp_code rsp_code, |
|
enum bt_bap_ascs_reason reason) |
|
{ |
|
struct bt_bap_unicast_client_cb *listener, *next; |
|
|
|
SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&unicast_client_cbs, listener, next, _node) { |
|
if (listener->config != NULL) { |
|
listener->config(stream, rsp_code, reason); |
|
} |
|
} |
|
} |
|
|
|
static void unicast_client_notify_ep_qos(struct bt_bap_stream *stream, |
|
enum bt_bap_ascs_rsp_code rsp_code, |
|
enum bt_bap_ascs_reason reason) |
|
{ |
|
struct bt_bap_unicast_client_cb *listener, *next; |
|
|
|
SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&unicast_client_cbs, listener, next, _node) { |
|
if (listener->qos != NULL) { |
|
listener->qos(stream, rsp_code, reason); |
|
} |
|
} |
|
} |
|
|
|
static void unicast_client_notify_ep_enable(struct bt_bap_stream *stream, |
|
enum bt_bap_ascs_rsp_code rsp_code, |
|
enum bt_bap_ascs_reason reason) |
|
{ |
|
struct bt_bap_unicast_client_cb *listener, *next; |
|
|
|
SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&unicast_client_cbs, listener, next, _node) { |
|
if (listener->enable != NULL) { |
|
listener->enable(stream, rsp_code, reason); |
|
} |
|
} |
|
} |
|
|
|
static void unicast_client_notify_ep_start(struct bt_bap_stream *stream, |
|
enum bt_bap_ascs_rsp_code rsp_code, |
|
enum bt_bap_ascs_reason reason) |
|
{ |
|
struct bt_bap_unicast_client_cb *listener, *next; |
|
|
|
SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&unicast_client_cbs, listener, next, _node) { |
|
if (listener->start != NULL) { |
|
listener->start(stream, rsp_code, reason); |
|
} |
|
} |
|
} |
|
|
|
static void unicast_client_notify_ep_stop(struct bt_bap_stream *stream, |
|
enum bt_bap_ascs_rsp_code rsp_code, |
|
enum bt_bap_ascs_reason reason) |
|
{ |
|
struct bt_bap_unicast_client_cb *listener, *next; |
|
|
|
SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&unicast_client_cbs, listener, next, _node) { |
|
if (listener->stop != NULL) { |
|
listener->stop(stream, rsp_code, reason); |
|
} |
|
} |
|
} |
|
|
|
static void unicast_client_notify_ep_disable(struct bt_bap_stream *stream, |
|
enum bt_bap_ascs_rsp_code rsp_code, |
|
enum bt_bap_ascs_reason reason) |
|
{ |
|
struct bt_bap_unicast_client_cb *listener, *next; |
|
|
|
SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&unicast_client_cbs, listener, next, _node) { |
|
if (listener->disable != NULL) { |
|
listener->disable(stream, rsp_code, reason); |
|
} |
|
} |
|
} |
|
|
|
static void unicast_client_notify_ep_metadata(struct bt_bap_stream *stream, |
|
enum bt_bap_ascs_rsp_code rsp_code, |
|
enum bt_bap_ascs_reason reason) |
|
{ |
|
struct bt_bap_unicast_client_cb *listener, *next; |
|
|
|
SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&unicast_client_cbs, listener, next, _node) { |
|
if (listener->metadata != NULL) { |
|
listener->metadata(stream, rsp_code, reason); |
|
} |
|
} |
|
} |
|
|
|
static void unicast_client_notify_ep_released(struct bt_bap_stream *stream, |
|
enum bt_bap_ascs_rsp_code rsp_code, |
|
enum bt_bap_ascs_reason reason) |
|
{ |
|
struct bt_bap_unicast_client_cb *listener, *next; |
|
|
|
SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&unicast_client_cbs, listener, next, _node) { |
|
if (listener->release != NULL) { |
|
listener->release(stream, rsp_code, reason); |
|
} |
|
} |
|
} |
|
|
|
static void unicast_client_ep_idle_state(struct bt_bap_ep *ep) |
|
{ |
|
struct bt_bap_unicast_client_ep *client_ep = |
|
CONTAINER_OF(ep, struct bt_bap_unicast_client_ep, ep); |
|
struct bt_bap_stream *stream = ep->stream; |
|
const struct bt_bap_stream_ops *ops; |
|
|
|
ep->receiver_ready = false; |
|
|
|
if (stream == NULL) { |
|
return; |
|
} |
|
|
|
/* If CIS is connected, disconnect and wait for CIS disconnection */ |
|
if (bt_bap_stream_can_disconnect(stream)) { |
|
int err; |
|
|
|
LOG_DBG("Disconnecting stream"); |
|
err = bt_bap_stream_disconnect(stream); |
|
|
|
if (err != 0) { |
|
LOG_ERR("Failed to disconnect stream: %d", err); |
|
} |
|
|
|
return; |
|
} else if (ep->iso != NULL && ep->iso->chan.state == BT_ISO_STATE_DISCONNECTING) { |
|
/* Wait for disconnection */ |
|
return; |
|
} |
|
|
|
bt_bap_stream_reset(stream); |
|
|
|
/* Notify upper layer */ |
|
if (client_ep->release_requested) { |
|
client_ep->release_requested = false; |
|
|
|
if (client_ep->cp_ntf_pending) { |
|
/* In case that we get the idle state notification before the CP |
|
* notification we trigger the CP callback now, as after this we won't be |
|
* able to find the stream by the ASE ID |
|
*/ |
|
client_ep->cp_ntf_pending = false; |
|
|
|
unicast_client_notify_ep_released(stream, BT_BAP_ASCS_RSP_CODE_SUCCESS, |
|
BT_BAP_ASCS_REASON_NONE); |
|
} |
|
} |
|
|
|
ops = stream->ops; |
|
if (ops != NULL && ops->released != NULL) { |
|
ops->released(stream); |
|
} else { |
|
LOG_WRN("No callback for released set"); |
|
} |
|
} |
|
|
|
static void unicast_client_ep_qos_update(struct bt_bap_ep *ep, |
|
const struct bt_ascs_ase_status_qos *qos) |
|
{ |
|
struct bt_iso_chan_io_qos *iso_io_qos; |
|
|
|
LOG_DBG("ep %p dir %s bap_iso %p", ep, bt_audio_dir_str(ep->dir), ep->iso); |
|
|
|
if (ep->dir == BT_AUDIO_DIR_SOURCE) { |
|
/* If the endpoint is a source, then we need to |
|
* reset our RX parameters |
|
*/ |
|
iso_io_qos = &ep->iso->rx.qos; |
|
} else if (ep->dir == BT_AUDIO_DIR_SINK) { |
|
/* If the endpoint is a sink, then we need to |
|
* reset our TX parameters |
|
*/ |
|
iso_io_qos = &ep->iso->tx.qos; |
|
} else { |
|
__ASSERT(false, "Invalid ep->dir: %u", ep->dir); |
|
return; |
|
} |
|
|
|
iso_io_qos->phy = qos->phy; |
|
iso_io_qos->sdu = sys_le16_to_cpu(qos->sdu); |
|
iso_io_qos->rtn = qos->rtn; |
|
} |
|
|
|
static void unicast_client_ep_config_state(struct bt_bap_ep *ep, struct net_buf_simple *buf) |
|
{ |
|
struct bt_bap_unicast_client_ep *client_ep = |
|
CONTAINER_OF(ep, struct bt_bap_unicast_client_ep, ep); |
|
struct bt_ascs_ase_status_config *cfg; |
|
struct bt_bap_qos_cfg_pref *pref; |
|
struct bt_bap_stream *stream; |
|
void *cc; |
|
|
|
ep->receiver_ready = false; |
|
|
|
if (client_ep->release_requested) { |
|
LOG_DBG("Released was requested, change local state to idle"); |
|
ep->reason = BT_HCI_ERR_LOCALHOST_TERM_CONN; |
|
unicast_client_ep_set_local_idle_state(ep); |
|
return; |
|
} |
|
|
|
if (buf->len < sizeof(*cfg)) { |
|
LOG_ERR("Config status too short"); |
|
return; |
|
} |
|
|
|
stream = ep->stream; |
|
if (stream == NULL) { |
|
LOG_WRN("No stream active for endpoint"); |
|
return; |
|
} |
|
|
|
cfg = net_buf_simple_pull_mem(buf, sizeof(*cfg)); |
|
|
|
if (stream->codec_cfg == NULL) { |
|
LOG_ERR("Stream %p does not have a codec configured", stream); |
|
return; |
|
} else if (stream->codec_cfg->id != cfg->codec.id) { |
|
LOG_ERR("Codec configuration mismatched: %u, %u", stream->codec_cfg->id, |
|
cfg->codec.id); |
|
/* TODO: Release the stream? */ |
|
return; |
|
} |
|
|
|
if (buf->len < cfg->cc_len) { |
|
LOG_ERR("Malformed ASE Config status: buf->len %u < %u cc_len", buf->len, |
|
cfg->cc_len); |
|
return; |
|
} |
|
|
|
cc = net_buf_simple_pull_mem(buf, cfg->cc_len); |
|
|
|
pref = &stream->ep->qos_pref; |
|
|
|
/* Convert to interval representation so they can be matched by QoS */ |
|
pref->unframed_supported = cfg->framing == BT_ASCS_QOS_FRAMING_UNFRAMED; |
|
pref->phy = cfg->phy; |
|
pref->rtn = cfg->rtn; |
|
pref->latency = sys_le16_to_cpu(cfg->latency); |
|
pref->pd_min = sys_get_le24(cfg->pd_min); |
|
pref->pd_max = sys_get_le24(cfg->pd_max); |
|
pref->pref_pd_min = sys_get_le24(cfg->prefer_pd_min); |
|
pref->pref_pd_max = sys_get_le24(cfg->prefer_pd_max); |
|
|
|
LOG_DBG("dir %s unframed_supported 0x%02x phy 0x%02x rtn %u " |
|
"latency %u pd_min %u pd_max %u pref_pd_min %u pref_pd_max %u codec 0x%02x ", |
|
bt_audio_dir_str(ep->dir), pref->unframed_supported, pref->phy, pref->rtn, |
|
pref->latency, pref->pd_min, pref->pd_max, pref->pref_pd_min, pref->pref_pd_max, |
|
stream->codec_cfg->id); |
|
|
|
if (!bt_bap_valid_qos_pref(pref)) { |
|
LOG_DBG("Invalid QoS preferences"); |
|
memset(pref, 0, sizeof(*pref)); |
|
|
|
/* If the sever provide an invalid QoS preferences we treat it as an error and do |
|
* nothing |
|
*/ |
|
return; |
|
} |
|
|
|
unicast_client_ep_set_codec_cfg(ep, cfg->codec.id, sys_le16_to_cpu(cfg->codec.cid), |
|
sys_le16_to_cpu(cfg->codec.vid), cc, cfg->cc_len, NULL); |
|
|
|
/* Notify upper layer */ |
|
if (stream->ops != NULL && stream->ops->configured != NULL) { |
|
stream->ops->configured(stream, pref); |
|
} else { |
|
LOG_WRN("No callback for configured set"); |
|
} |
|
} |
|
|
|
static void unicast_client_ep_qos_state(struct bt_bap_ep *ep, struct net_buf_simple *buf, |
|
uint8_t old_state) |
|
{ |
|
const struct bt_bap_stream_ops *ops; |
|
struct bt_ascs_ase_status_qos *qos; |
|
struct bt_bap_stream *stream; |
|
|
|
ep->receiver_ready = false; |
|
|
|
if (buf->len < sizeof(*qos)) { |
|
LOG_ERR("QoS status too short"); |
|
return; |
|
} |
|
|
|
stream = ep->stream; |
|
if (stream == NULL) { |
|
LOG_ERR("No stream active for endpoint"); |
|
return; |
|
} |
|
ops = stream->ops; |
|
|
|
if (ops != NULL) { |
|
if (ep->dir == BT_AUDIO_DIR_SINK && ops->disabled != NULL) { |
|
/* If the old state was enabling or streaming, then the sink |
|
* ASE has been disabled. Since the sink ASE does not have a |
|
* disabling state, we can check if by comparing the old_state |
|
*/ |
|
const bool disabled = old_state == BT_BAP_EP_STATE_ENABLING || |
|
old_state == BT_BAP_EP_STATE_STREAMING; |
|
|
|
if (disabled) { |
|
ops->disabled(stream); |
|
} |
|
} else if (ep->dir == BT_AUDIO_DIR_SOURCE && |
|
old_state == BT_BAP_EP_STATE_DISABLING && ops->stopped != NULL) { |
|
/* We left the disabling state, let the upper layers know that the stream is |
|
* stopped |
|
*/ |
|
uint8_t reason = ep->reason; |
|
|
|
if (reason == BT_HCI_ERR_SUCCESS) { |
|
/* Default to BT_HCI_ERR_UNSPECIFIED if no other reason is set */ |
|
reason = BT_HCI_ERR_UNSPECIFIED; |
|
} else { |
|
/* Reset reason */ |
|
ep->reason = BT_HCI_ERR_SUCCESS; |
|
} |
|
|
|
ops->stopped(stream, reason); |
|
} |
|
} |
|
|
|
qos = net_buf_simple_pull_mem(buf, sizeof(*qos)); |
|
|
|
/* Update existing QoS configuration */ |
|
unicast_client_ep_qos_update(ep, qos); |
|
|
|
ep->cig_id = qos->cig_id; |
|
ep->cis_id = qos->cis_id; |
|
(void)memcpy(&stream->qos->interval, sys_le24_to_cpu(qos->interval), sizeof(qos->interval)); |
|
stream->qos->framing = qos->framing; |
|
stream->qos->phy = qos->phy; |
|
stream->qos->sdu = sys_le16_to_cpu(qos->sdu); |
|
stream->qos->rtn = qos->rtn; |
|
stream->qos->latency = sys_le16_to_cpu(qos->latency); |
|
(void)memcpy(&stream->qos->pd, sys_le24_to_cpu(qos->pd), sizeof(qos->pd)); |
|
|
|
LOG_DBG("dir %s cig 0x%02x cis 0x%02x codec 0x%02x interval %u " |
|
"framing 0x%02x phy 0x%02x rtn %u latency %u pd %u", |
|
bt_audio_dir_str(ep->dir), ep->cig_id, ep->cis_id, stream->codec_cfg->id, |
|
stream->qos->interval, stream->qos->framing, stream->qos->phy, stream->qos->rtn, |
|
stream->qos->latency, stream->qos->pd); |
|
|
|
/* Disconnect ISO if connected */ |
|
if (bt_bap_stream_can_disconnect(stream)) { |
|
const int err = bt_bap_stream_disconnect(stream); |
|
|
|
if (err != 0) { |
|
LOG_ERR("Failed to disconnect stream: %d", err); |
|
} |
|
} |
|
|
|
/* Notify upper layer */ |
|
if (stream->ops != NULL && stream->ops->qos_set != NULL) { |
|
stream->ops->qos_set(stream); |
|
} else { |
|
LOG_WRN("No callback for qos_set set"); |
|
} |
|
} |
|
|
|
static void unicast_client_ep_enabling_state(struct bt_bap_ep *ep, struct net_buf_simple *buf, |
|
bool state_changed) |
|
{ |
|
struct bt_ascs_ase_status_enable *enable; |
|
struct bt_bap_stream *stream; |
|
void *metadata; |
|
|
|
if (buf->len < sizeof(*enable)) { |
|
LOG_ERR("Enabling status too short"); |
|
return; |
|
} |
|
|
|
stream = ep->stream; |
|
if (stream == NULL) { |
|
LOG_ERR("No stream active for endpoint"); |
|
return; |
|
} |
|
|
|
enable = net_buf_simple_pull_mem(buf, sizeof(*enable)); |
|
|
|
if (buf->len < enable->metadata_len) { |
|
LOG_ERR("Malformed PDU: remaining len %u expected %u", buf->len, |
|
enable->metadata_len); |
|
return; |
|
} |
|
|
|
metadata = net_buf_simple_pull_mem(buf, enable->metadata_len); |
|
|
|
LOG_DBG("dir %s cig 0x%02x cis 0x%02x", bt_audio_dir_str(ep->dir), ep->cig_id, ep->cis_id); |
|
|
|
unicast_client_ep_set_metadata(ep, metadata, enable->metadata_len, NULL); |
|
|
|
/* Notify upper layer |
|
* |
|
* If the state did not change then only the metadata was changed |
|
*/ |
|
if (state_changed) { |
|
if (stream->ops != NULL && stream->ops->enabled != NULL) { |
|
stream->ops->enabled(stream); |
|
} else { |
|
LOG_WRN("No callback for enabled set"); |
|
} |
|
} else { |
|
if (stream->ops != NULL && stream->ops->metadata_updated != NULL) { |
|
stream->ops->metadata_updated(stream); |
|
} else { |
|
LOG_WRN("No callback for metadata_updated set"); |
|
} |
|
} |
|
} |
|
|
|
static void unicast_client_ep_streaming_state(struct bt_bap_ep *ep, struct net_buf_simple *buf, |
|
bool state_changed) |
|
{ |
|
struct bt_ascs_ase_status_stream *stream_status; |
|
struct bt_bap_stream *stream; |
|
|
|
if (buf->len < sizeof(*stream_status)) { |
|
LOG_ERR("Streaming status too short"); |
|
return; |
|
} |
|
|
|
stream = ep->stream; |
|
if (stream == NULL) { |
|
LOG_ERR("No stream active for endpoint"); |
|
return; |
|
} |
|
|
|
stream_status = net_buf_simple_pull_mem(buf, sizeof(*stream_status)); |
|
|
|
LOG_DBG("dir %s cig 0x%02x cis 0x%02x", bt_audio_dir_str(ep->dir), ep->cig_id, ep->cis_id); |
|
|
|
/* Setup the ISO data path when the stream is started. We could do it earlier when the CIS |
|
* is connected, but then we would just receive audio data that we would then just discard |
|
*/ |
|
bt_bap_setup_iso_data_path(stream); |
|
|
|
/* Notify upper layer |
|
* |
|
* If the state did not change then only the metadata was changed |
|
*/ |
|
if (state_changed) { |
|
if (stream->ops != NULL && stream->ops->started != NULL) { |
|
stream->ops->started(stream); |
|
} else { |
|
LOG_WRN("No callback for started set"); |
|
} |
|
} else { |
|
if (stream->ops != NULL && stream->ops->metadata_updated != NULL) { |
|
stream->ops->metadata_updated(stream); |
|
} else { |
|
LOG_WRN("No callback for metadata_updated set"); |
|
} |
|
} |
|
} |
|
|
|
static void unicast_client_ep_disabling_state(struct bt_bap_ep *ep, struct net_buf_simple *buf) |
|
{ |
|
struct bt_ascs_ase_status_disable *disable; |
|
struct bt_bap_stream *stream; |
|
|
|
ep->receiver_ready = false; |
|
|
|
if (buf->len < sizeof(*disable)) { |
|
LOG_ERR("Disabling status too short"); |
|
return; |
|
} |
|
|
|
stream = ep->stream; |
|
if (stream == NULL) { |
|
LOG_ERR("No stream active for endpoint"); |
|
return; |
|
} |
|
|
|
disable = net_buf_simple_pull_mem(buf, sizeof(*disable)); |
|
|
|
LOG_DBG("dir %s cig 0x%02x cis 0x%02x", bt_audio_dir_str(ep->dir), ep->cig_id, ep->cis_id); |
|
|
|
/* Notify upper layer */ |
|
if (stream->ops != NULL && stream->ops->disabled != NULL) { |
|
stream->ops->disabled(stream); |
|
} else { |
|
LOG_WRN("No callback for disabled set"); |
|
} |
|
} |
|
|
|
static void unicast_client_ep_releasing_state(struct bt_bap_ep *ep, struct net_buf_simple *buf) |
|
{ |
|
struct bt_bap_stream *stream; |
|
|
|
ep->receiver_ready = false; |
|
|
|
stream = ep->stream; |
|
if (stream == NULL) { |
|
LOG_ERR("No stream active for endpoint"); |
|
return; |
|
} |
|
|
|
LOG_DBG("dir %s", bt_audio_dir_str(ep->dir)); |
|
|
|
if (bt_bap_stream_can_disconnect(stream)) { |
|
/* The Unicast Client shall terminate any CIS established for |
|
* that ASE by following the Connected Isochronous Stream |
|
* Terminate procedure defined in Volume 3, Part C, |
|
* Section 9.3.15 in when the Unicast Client has determined |
|
* that the ASE is in the Releasing state. |
|
*/ |
|
const int err = bt_bap_stream_disconnect(stream); |
|
|
|
if (err != 0) { |
|
LOG_ERR("Failed to disconnect stream: %d", err); |
|
} |
|
} |
|
} |
|
|
|
static void unicast_client_ep_set_status(struct bt_bap_ep *ep, struct net_buf_simple *buf) |
|
{ |
|
struct bt_ascs_ase_status *status; |
|
struct bt_bap_unicast_client_ep *client_ep; |
|
bool state_changed; |
|
uint8_t old_state; |
|
|
|
if (!ep) { |
|
return; |
|
} |
|
|
|
client_ep = CONTAINER_OF(ep, struct bt_bap_unicast_client_ep, ep); |
|
|
|
status = net_buf_simple_pull_mem(buf, sizeof(*status)); |
|
|
|
old_state = ep->status.state; |
|
ep->status = *status; |
|
state_changed = old_state != ep->status.state; |
|
|
|
if (state_changed && old_state == BT_BAP_EP_STATE_STREAMING) { |
|
/* We left the streaming state, let the upper layers know that the stream is stopped |
|
*/ |
|
struct bt_bap_stream *stream = ep->stream; |
|
|
|
if (stream != NULL) { |
|
struct bt_bap_stream_ops *ops = stream->ops; |
|
uint8_t reason = ep->reason; |
|
|
|
if (reason == BT_HCI_ERR_SUCCESS) { |
|
/* Default to BT_HCI_ERR_UNSPECIFIED if no other reason is set */ |
|
reason = BT_HCI_ERR_UNSPECIFIED; |
|
} else { |
|
/* Reset reason */ |
|
ep->reason = BT_HCI_ERR_SUCCESS; |
|
} |
|
|
|
if (ep->iso != NULL) { |
|
/* Remove the ISO data path as we no longer want to process any ISO |
|
* data for this stream. |
|
*/ |
|
bt_bap_remove_iso_data_path(stream); |
|
} |
|
|
|
if (ops != NULL && ops->stopped != NULL) { |
|
ops->stopped(stream, reason); |
|
} else { |
|
LOG_WRN("No callback for stopped set"); |
|
} |
|
} |
|
} |
|
|
|
LOG_DBG("ep %p handle 0x%04x id 0x%02x dir %s state %s -> %s", ep, client_ep->handle, |
|
status->id, bt_audio_dir_str(ep->dir), bt_bap_ep_state_str(old_state), |
|
bt_bap_ep_state_str(status->state)); |
|
|
|
switch (status->state) { |
|
case BT_BAP_EP_STATE_IDLE: |
|
unicast_client_ep_idle_state(ep); |
|
break; |
|
case BT_BAP_EP_STATE_CODEC_CONFIGURED: |
|
switch (old_state) { |
|
/* Valid only if ASE_State field = 0x00 (Idle) */ |
|
case BT_BAP_EP_STATE_IDLE: |
|
/* or 0x01 (Codec Configured) */ |
|
case BT_BAP_EP_STATE_CODEC_CONFIGURED: |
|
/* or 0x02 (QoS Configured) */ |
|
case BT_BAP_EP_STATE_QOS_CONFIGURED: |
|
/* or 0x06 (Releasing) */ |
|
case BT_BAP_EP_STATE_RELEASING: |
|
break; |
|
default: |
|
LOG_WRN("Invalid state transition: %s -> %s", |
|
bt_bap_ep_state_str(old_state), |
|
bt_bap_ep_state_str(ep->status.state)); |
|
return; |
|
} |
|
|
|
unicast_client_ep_config_state(ep, buf); |
|
break; |
|
case BT_BAP_EP_STATE_QOS_CONFIGURED: |
|
/* QoS configured have different allowed states depending on the endpoint type */ |
|
if (ep->dir == BT_AUDIO_DIR_SOURCE) { |
|
switch (old_state) { |
|
/* Valid only if ASE_State field = 0x01 (Codec Configured) */ |
|
case BT_BAP_EP_STATE_CODEC_CONFIGURED: |
|
/* or 0x02 (QoS Configured) */ |
|
case BT_BAP_EP_STATE_QOS_CONFIGURED: |
|
/* or 0x04 (Streaming) if there is a disconnect */ |
|
case BT_BAP_EP_STATE_STREAMING: |
|
/* or 0x05 (Disabling) */ |
|
case BT_BAP_EP_STATE_DISABLING: |
|
break; |
|
default: |
|
LOG_WRN("Invalid state transition: %s -> %s", |
|
bt_bap_ep_state_str(old_state), |
|
bt_bap_ep_state_str(ep->status.state)); |
|
return; |
|
} |
|
} else { |
|
switch (old_state) { |
|
/* Valid only if ASE_State field = 0x01 (Codec Configured) */ |
|
case BT_BAP_EP_STATE_CODEC_CONFIGURED: |
|
/* or 0x02 (QoS Configured) */ |
|
case BT_BAP_EP_STATE_QOS_CONFIGURED: |
|
/* or 0x03 (Enabling) */ |
|
case BT_BAP_EP_STATE_ENABLING: |
|
/* or 0x04 (Streaming)*/ |
|
case BT_BAP_EP_STATE_STREAMING: |
|
break; |
|
default: |
|
LOG_WRN("Invalid state transition: %s -> %s", |
|
bt_bap_ep_state_str(old_state), |
|
bt_bap_ep_state_str(ep->status.state)); |
|
return; |
|
} |
|
} |
|
|
|
unicast_client_ep_qos_state(ep, buf, old_state); |
|
break; |
|
case BT_BAP_EP_STATE_ENABLING: |
|
switch (old_state) { |
|
/* Valid only if ASE_State field = 0x02 (QoS Configured) */ |
|
case BT_BAP_EP_STATE_QOS_CONFIGURED: |
|
/* or 0x03 (Enabling) */ |
|
case BT_BAP_EP_STATE_ENABLING: |
|
break; |
|
default: |
|
LOG_WRN("Invalid state transition: %s -> %s", |
|
bt_bap_ep_state_str(old_state), |
|
bt_bap_ep_state_str(ep->status.state)); |
|
return; |
|
} |
|
|
|
unicast_client_ep_enabling_state(ep, buf, state_changed); |
|
break; |
|
case BT_BAP_EP_STATE_STREAMING: |
|
switch (old_state) { |
|
/* Valid only if ASE_State field = 0x03 (Enabling)*/ |
|
case BT_BAP_EP_STATE_ENABLING: |
|
/* or 0x04 (Streaming)*/ |
|
case BT_BAP_EP_STATE_STREAMING: |
|
break; |
|
default: |
|
LOG_WRN("Invalid state transition: %s -> %s", |
|
bt_bap_ep_state_str(old_state), |
|
bt_bap_ep_state_str(ep->status.state)); |
|
return; |
|
} |
|
|
|
unicast_client_ep_streaming_state(ep, buf, state_changed); |
|
break; |
|
case BT_BAP_EP_STATE_DISABLING: |
|
if (ep->dir == BT_AUDIO_DIR_SOURCE) { |
|
switch (old_state) { |
|
/* Valid only if ASE_State field = 0x03 (Enabling) */ |
|
case BT_BAP_EP_STATE_ENABLING: |
|
/* or 0x04 (Streaming) */ |
|
case BT_BAP_EP_STATE_STREAMING: |
|
break; |
|
default: |
|
LOG_WRN("Invalid state transition: %s -> %s", |
|
bt_bap_ep_state_str(old_state), |
|
bt_bap_ep_state_str(ep->status.state)); |
|
return; |
|
} |
|
} else { |
|
/* Sinks cannot go into the disabling state */ |
|
LOG_WRN("Invalid state transition: %s -> %s", |
|
bt_bap_ep_state_str(old_state), |
|
bt_bap_ep_state_str(ep->status.state)); |
|
return; |
|
} |
|
|
|
unicast_client_ep_disabling_state(ep, buf); |
|
break; |
|
case BT_BAP_EP_STATE_RELEASING: |
|
switch (old_state) { |
|
/* Valid only if ASE_State field = 0x01 (Codec Configured) */ |
|
case BT_BAP_EP_STATE_CODEC_CONFIGURED: |
|
/* or 0x02 (QoS Configured) */ |
|
case BT_BAP_EP_STATE_QOS_CONFIGURED: |
|
/* or 0x03 (Enabling) */ |
|
case BT_BAP_EP_STATE_ENABLING: |
|
/* or 0x04 (Streaming) */ |
|
case BT_BAP_EP_STATE_STREAMING: |
|
break; |
|
/* or 0x04 (Disabling) */ |
|
case BT_BAP_EP_STATE_DISABLING: |
|
if (ep->dir == BT_AUDIO_DIR_SOURCE) { |
|
break; |
|
} /* else fall through for sink */ |
|
|
|
/* fall through */ |
|
default: |
|
LOG_WRN("Invalid state transition: %s -> %s", |
|
bt_bap_ep_state_str(old_state), |
|
bt_bap_ep_state_str(ep->status.state)); |
|
return; |
|
} |
|
|
|
unicast_client_ep_releasing_state(ep, buf); |
|
break; |
|
} |
|
} |
|
|
|
static bool valid_ltv_cb(struct bt_data *data, void *user_data) |
|
{ |
|
/* just return true to continue parsing as bt_data_parse will validate for us */ |
|
return true; |
|
} |
|
|
|
static int unicast_client_ep_set_codec_cfg(struct bt_bap_ep *ep, uint8_t id, uint16_t cid, |
|
uint16_t vid, void *data, uint8_t len, |
|
struct bt_audio_codec_cfg *codec_cfg) |
|
{ |
|
if (!ep && !codec_cfg) { |
|
return -EINVAL; |
|
} |
|
|
|
LOG_DBG("ep %p codec id 0x%02x cid 0x%04x vid 0x%04x len %u", ep, id, cid, vid, len); |
|
|
|
if (!codec_cfg) { |
|
codec_cfg = &ep->codec_cfg; |
|
} |
|
|
|
if (len > sizeof(codec_cfg->data)) { |
|
LOG_DBG("Cannot store %u octets of codec data", len); |
|
|
|
return -ENOMEM; |
|
} |
|
|
|
codec_cfg->id = id; |
|
codec_cfg->cid = cid; |
|
codec_cfg->vid = vid; |
|
|
|
codec_cfg->data_len = len; |
|
memcpy(codec_cfg->data, data, len); |
|
|
|
return 0; |
|
} |
|
|
|
static int unicast_client_set_codec_cap(uint8_t id, uint16_t cid, uint16_t vid, void *data, |
|
uint8_t data_len, void *meta, uint8_t meta_len, |
|
struct bt_audio_codec_cap *codec_cap) |
|
{ |
|
struct net_buf_simple buf; |
|
|
|
if (!codec_cap) { |
|
return -EINVAL; |
|
} |
|
|
|
LOG_DBG("codec id 0x%02x cid 0x%04x vid 0x%04x data_len %u meta_len %u", id, cid, vid, |
|
data_len, meta_len); |
|
|
|
/* Reset current data */ |
|
(void)memset(codec_cap, 0, sizeof(*codec_cap)); |
|
|
|
codec_cap->id = id; |
|
codec_cap->cid = cid; |
|
codec_cap->vid = vid; |
|
|
|
if (data_len > 0U) { |
|
if (data_len > sizeof(codec_cap->data)) { |
|
return -ENOMEM; |
|
} |
|
|
|
net_buf_simple_init_with_data(&buf, data, data_len); |
|
|
|
/* If codec is LC3, then it shall be LTV encoded - We verify this before storing the |
|
* data For any non-LC3 codecs, we cannot verify anything |
|
*/ |
|
if (id == BT_HCI_CODING_FORMAT_LC3) { |
|
bt_data_parse(&buf, valid_ltv_cb, NULL); |
|
|
|
/* Check if all entries could be parsed */ |
|
if (buf.len) { |
|
LOG_ERR("Unable to parse Codec capabilities: len %u", buf.len); |
|
return -EINVAL; |
|
} |
|
} |
|
memcpy(codec_cap->data, data, data_len); |
|
codec_cap->data_len = data_len; |
|
} |
|
|
|
if (meta_len > 0U) { |
|
if (meta_len > sizeof(codec_cap->meta)) { |
|
return -ENOMEM; |
|
} |
|
|
|
net_buf_simple_init_with_data(&buf, meta, meta_len); |
|
|
|
bt_data_parse(&buf, valid_ltv_cb, NULL); |
|
|
|
/* Check if all entries could be parsed */ |
|
if (buf.len) { |
|
LOG_ERR("Unable to parse Codec metadata: len %u", buf.len); |
|
return -EINVAL; |
|
} |
|
|
|
memcpy(codec_cap->meta, meta, meta_len); |
|
codec_cap->meta_len = meta_len; |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
static int unicast_client_ep_set_metadata(struct bt_bap_ep *ep, void *data, uint8_t len, |
|
struct bt_audio_codec_cfg *codec_cfg) |
|
{ |
|
if (!ep && !codec_cfg) { |
|
return -EINVAL; |
|
} |
|
|
|
LOG_DBG("ep %p len %u codec_cfg %p", ep, len, codec_cfg); |
|
|
|
if (!codec_cfg) { |
|
codec_cfg = &ep->codec_cfg; |
|
} |
|
|
|
if (len > sizeof(codec_cfg->meta)) { |
|
LOG_DBG("Cannot store %u octets of metadata", len); |
|
|
|
return -ENOMEM; |
|
} |
|
|
|
/* Reset current metadata */ |
|
codec_cfg->meta_len = len; |
|
(void)memcpy(codec_cfg->meta, data, len); |
|
|
|
return 0; |
|
} |
|
|
|
static uint8_t unicast_client_cp_notify(struct bt_conn *conn, |
|
struct bt_gatt_subscribe_params *params, const void *data, |
|
uint16_t length) |
|
{ |
|
struct bt_ascs_cp_rsp *rsp; |
|
struct net_buf_simple buf; |
|
|
|
LOG_DBG("conn %p len %u", conn, length); |
|
|
|
if (!data) { |
|
LOG_DBG("Unsubscribed"); |
|
params->value_handle = BAP_HANDLE_UNUSED; |
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
net_buf_simple_init_with_data(&buf, (void *)data, length); |
|
|
|
if (buf.len < sizeof(*rsp)) { |
|
LOG_ERR("Control Point Notification too small"); |
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
rsp = net_buf_simple_pull_mem(&buf, sizeof(*rsp)); |
|
|
|
if (rsp->num_ase == BT_ASCS_UNSUPP_OR_LENGTH_ERR_NUM_ASE) { |
|
/* This is a special case where num_ase == BT_ASCS_UNSUPP_OR_LENGTH_ERR_NUM_ASE |
|
* but really there is only one ASE response |
|
*/ |
|
rsp->num_ase = 1U; |
|
} |
|
|
|
for (uint8_t i = 0U; i < rsp->num_ase; i++) { |
|
struct bt_bap_unicast_client_ep *client_ep = NULL; |
|
struct bt_ascs_cp_ase_rsp *ase_rsp; |
|
struct bt_bap_stream *stream; |
|
|
|
if (buf.len < sizeof(*ase_rsp)) { |
|
LOG_ERR("Control Point Notification too small: %u", buf.len); |
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
ase_rsp = net_buf_simple_pull_mem(&buf, sizeof(*ase_rsp)); |
|
|
|
LOG_DBG("op %s (0x%02x) id 0x%02x code %s (0x%02x) " |
|
"reason %s (0x%02x)", bt_ascs_op_str(rsp->op), rsp->op, |
|
ase_rsp->id, bt_ascs_rsp_str(ase_rsp->code), |
|
ase_rsp->code, bt_ascs_reason_str(ase_rsp->reason), |
|
ase_rsp->reason); |
|
|
|
stream = audio_stream_by_ep_id(conn, ase_rsp->id); |
|
if (stream == NULL) { |
|
LOG_DBG("Could not find stream by id %u", ase_rsp->id); |
|
} else { |
|
client_ep = CONTAINER_OF(stream->ep, struct bt_bap_unicast_client_ep, ep); |
|
client_ep->cp_ntf_pending = false; |
|
} |
|
|
|
switch (rsp->op) { |
|
case BT_ASCS_CONFIG_OP: |
|
unicast_client_notify_ep_config(stream, ase_rsp->code, ase_rsp->reason); |
|
break; |
|
case BT_ASCS_QOS_OP: |
|
unicast_client_notify_ep_qos(stream, ase_rsp->code, ase_rsp->reason); |
|
break; |
|
case BT_ASCS_ENABLE_OP: |
|
unicast_client_notify_ep_enable(stream, ase_rsp->code, ase_rsp->reason); |
|
break; |
|
case BT_ASCS_START_OP: |
|
unicast_client_notify_ep_start(stream, ase_rsp->code, ase_rsp->reason); |
|
break; |
|
case BT_ASCS_DISABLE_OP: |
|
unicast_client_notify_ep_disable(stream, ase_rsp->code, ase_rsp->reason); |
|
break; |
|
case BT_ASCS_STOP_OP: |
|
unicast_client_notify_ep_stop(stream, ase_rsp->code, ase_rsp->reason); |
|
break; |
|
case BT_ASCS_METADATA_OP: |
|
unicast_client_notify_ep_metadata(stream, ase_rsp->code, ase_rsp->reason); |
|
break; |
|
case BT_ASCS_RELEASE_OP: |
|
/* client_ep->release_requested is set to false if handled by the |
|
* endpoint notification handler |
|
*/ |
|
if (client_ep != NULL && client_ep->release_requested) { |
|
/* If request was reject, do not expect endpoint notifications */ |
|
if (ase_rsp->code != BT_BAP_ASCS_RSP_CODE_SUCCESS) { |
|
client_ep->cp_ntf_pending = false; |
|
client_ep->release_requested = false; |
|
} |
|
|
|
unicast_client_notify_ep_released(stream, ase_rsp->code, |
|
ase_rsp->reason); |
|
} |
|
break; |
|
default: |
|
break; |
|
} |
|
} |
|
|
|
return BT_GATT_ITER_CONTINUE; |
|
} |
|
|
|
static uint8_t unicast_client_ase_ntf_read_func(struct bt_conn *conn, uint8_t err, |
|
struct bt_gatt_read_params *read, const void *data, |
|
uint16_t length) |
|
{ |
|
uint16_t handle = read->single.handle; |
|
struct net_buf_simple buf_clone; |
|
struct unicast_client *client; |
|
struct net_buf_simple *buf; |
|
struct bt_bap_ep *ep; |
|
|
|
LOG_DBG("conn %p err 0x%02x len %u", conn, err, length); |
|
|
|
if (err) { |
|
LOG_DBG("Failed to read ASE: %u", err); |
|
|
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
LOG_DBG("handle 0x%04x", handle); |
|
|
|
client = &uni_cli_insts[bt_conn_index(conn)]; |
|
buf = &client->net_buf; |
|
|
|
if (data != NULL) { |
|
if (net_buf_simple_tailroom(buf) < length) { |
|
LOG_DBG("Buffer full, invalid server response of size %u", |
|
length + client->net_buf.len); |
|
reset_att_buf(client); |
|
atomic_clear_bit(client->flags, UNICAST_CLIENT_FLAG_BUSY); |
|
|
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
/* store data*/ |
|
net_buf_simple_add_mem(buf, data, length); |
|
|
|
return BT_GATT_ITER_CONTINUE; |
|
} |
|
|
|
memset(read, 0, sizeof(*read)); |
|
|
|
if (buf->len < sizeof(struct bt_ascs_ase_status)) { |
|
LOG_DBG("Read response too small (%u)", buf->len); |
|
reset_att_buf(client); |
|
atomic_clear_bit(client->flags, UNICAST_CLIENT_FLAG_BUSY); |
|
|
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
/* Clone the buffer so that we can reset it while still providing the data to the upper |
|
* layers |
|
*/ |
|
net_buf_simple_clone(buf, &buf_clone); |
|
reset_att_buf(client); |
|
atomic_clear_bit(client->flags, UNICAST_CLIENT_FLAG_BUSY); |
|
|
|
ep = unicast_client_ep_get(conn, client->dir, handle); |
|
if (!ep) { |
|
LOG_DBG("Unknown %s ep for handle 0x%04X", bt_audio_dir_str(client->dir), handle); |
|
} else { |
|
/* Set reason in case this exits the streaming state, unless already set */ |
|
if (ep->reason == BT_HCI_ERR_SUCCESS) { |
|
ep->reason = BT_HCI_ERR_REMOTE_USER_TERM_CONN; |
|
} |
|
|
|
unicast_client_ep_set_status(ep, &buf_clone); |
|
} |
|
|
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
static void long_ase_read(struct bt_bap_unicast_client_ep *client_ep) |
|
{ |
|
/* Perform long read if notification is maximum size */ |
|
struct bt_conn *conn = client_ep->ep.stream->conn; |
|
struct unicast_client *client = &uni_cli_insts[bt_conn_index(conn)]; |
|
struct net_buf_simple *long_read_buf = &client->net_buf; |
|
int err; |
|
|
|
LOG_DBG("conn %p ep %p 0x%04X", conn, &client_ep->ep, client_ep->handle); |
|
|
|
if (atomic_test_and_set_bit(client->flags, UNICAST_CLIENT_FLAG_BUSY)) { |
|
/* If the client is busy reading or writing something else, reschedule the |
|
* long read. |
|
*/ |
|
struct bt_conn_info conn_info; |
|
|
|
err = bt_conn_get_info(conn, &conn_info); |
|
if (err != 0) { |
|
LOG_DBG("Failed to get conn info, use default interval"); |
|
|
|
conn_info.le.interval = BT_GAP_INIT_CONN_INT_MIN; |
|
} |
|
|
|
/* Wait a connection interval to retry */ |
|
err = k_work_reschedule(&client_ep->ase_read_work, |
|
K_USEC(BT_CONN_INTERVAL_TO_US(conn_info.le.interval))); |
|
if (err < 0) { |
|
LOG_DBG("Failed to reschedule ASE long read work: %d", err); |
|
} |
|
|
|
return; |
|
} |
|
|
|
client->read_params.func = unicast_client_ase_ntf_read_func; |
|
client->read_params.handle_count = 1U; |
|
client->read_params.single.handle = client_ep->handle; |
|
client->read_params.single.offset = long_read_buf->len; |
|
|
|
err = bt_gatt_read(conn, &client->read_params); |
|
if (err != 0) { |
|
LOG_DBG("Failed to read ASE: %d", err); |
|
atomic_clear_bit(client->flags, UNICAST_CLIENT_FLAG_BUSY); |
|
} |
|
} |
|
|
|
static void delayed_ase_read_handler(struct k_work *work) |
|
{ |
|
struct k_work_delayable *dwork = k_work_delayable_from_work(work); |
|
struct bt_bap_unicast_client_ep *client_ep = |
|
CONTAINER_OF(dwork, struct bt_bap_unicast_client_ep, ase_read_work); |
|
|
|
LOG_DBG("ep %p 0x%04X", &client_ep->ep, client_ep->handle); |
|
|
|
/* Try reading again */ |
|
long_ase_read(client_ep); |
|
} |
|
|
|
static uint8_t unicast_client_ep_notify(struct bt_conn *conn, |
|
struct bt_gatt_subscribe_params *params, const void *data, |
|
uint16_t length) |
|
{ |
|
struct net_buf_simple buf; |
|
struct bt_bap_unicast_client_ep *client_ep; |
|
uint16_t max_ntf_size; |
|
struct bt_bap_ep *ep; |
|
|
|
client_ep = CONTAINER_OF(params, struct bt_bap_unicast_client_ep, subscribe); |
|
ep = &client_ep->ep; |
|
|
|
LOG_DBG("conn %p ep %p len %u", conn, ep, length); |
|
|
|
/* Cancel any pending long reads for the endpoint */ |
|
(void)k_work_cancel_delayable(&client_ep->ase_read_work); |
|
|
|
if (!data) { |
|
LOG_DBG("Unsubscribed"); |
|
params->value_handle = BAP_HANDLE_UNUSED; |
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
max_ntf_size = bt_audio_get_max_ntf_size(conn); |
|
|
|
if (length == max_ntf_size) { |
|
struct unicast_client *client = &uni_cli_insts[bt_conn_index(conn)]; |
|
|
|
if (!atomic_test_bit(client->flags, UNICAST_CLIENT_FLAG_BUSY)) { |
|
struct net_buf_simple *long_read_buf = &client->net_buf; |
|
|
|
/* store data*/ |
|
net_buf_simple_add_mem(long_read_buf, data, length); |
|
} |
|
|
|
long_ase_read(client_ep); |
|
|
|
return BT_GATT_ITER_CONTINUE; |
|
} |
|
|
|
net_buf_simple_init_with_data(&buf, (void *)data, length); |
|
|
|
if (buf.len < sizeof(struct bt_ascs_ase_status)) { |
|
LOG_ERR("Notification too small"); |
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
/* Set reason in case this exits the streaming state, unless already set */ |
|
if (ep->reason == BT_HCI_ERR_SUCCESS) { |
|
ep->reason = BT_HCI_ERR_REMOTE_USER_TERM_CONN; |
|
} |
|
|
|
unicast_client_ep_set_status(ep, &buf); |
|
|
|
return BT_GATT_ITER_CONTINUE; |
|
} |
|
|
|
static int unicast_client_ep_subscribe(struct bt_conn *conn, struct bt_bap_ep *ep) |
|
{ |
|
struct bt_bap_unicast_client_ep *client_ep; |
|
int err; |
|
|
|
client_ep = CONTAINER_OF(ep, struct bt_bap_unicast_client_ep, ep); |
|
|
|
LOG_DBG("ep %p handle 0x%02x", ep, client_ep->handle); |
|
|
|
if (client_ep->subscribe.value_handle) { |
|
return 0; |
|
} |
|
|
|
client_ep->subscribe.value_handle = client_ep->handle; |
|
client_ep->subscribe.ccc_handle = BT_GATT_AUTO_DISCOVER_CCC_HANDLE; |
|
client_ep->subscribe.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE; |
|
client_ep->subscribe.disc_params = &client_ep->discover; |
|
client_ep->subscribe.notify = unicast_client_ep_notify; |
|
client_ep->subscribe.value = BT_GATT_CCC_NOTIFY; |
|
atomic_set_bit(client_ep->subscribe.flags, BT_GATT_SUBSCRIBE_FLAG_VOLATILE); |
|
|
|
err = bt_gatt_subscribe(conn, &client_ep->subscribe); |
|
if (err != 0 && err != -EALREADY) { |
|
return err; |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
static void unicast_client_cp_sub_cb(struct bt_conn *conn, uint8_t err, |
|
struct bt_gatt_subscribe_params *sub_params) |
|
{ |
|
|
|
LOG_DBG("conn %p err %u", conn, err); |
|
|
|
unicast_client_discover_complete(conn, err); |
|
} |
|
|
|
static void unicast_client_ep_set_cp(struct bt_conn *conn, uint16_t handle) |
|
{ |
|
struct unicast_client *client = &uni_cli_insts[bt_conn_index(conn)]; |
|
|
|
LOG_DBG("conn %p 0x%04x", conn, handle); |
|
|
|
#if CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT > 0 |
|
for (size_t i = 0U; i < ARRAY_SIZE(client->snks); i++) { |
|
struct bt_bap_unicast_client_ep *client_ep = &client->snks[i]; |
|
|
|
if (client_ep->handle) { |
|
client_ep->cp_handle = handle; |
|
} |
|
} |
|
#endif /* CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT > 0 */ |
|
|
|
#if CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT > 0 |
|
for (size_t i = 0U; i < ARRAY_SIZE(client->srcs); i++) { |
|
struct bt_bap_unicast_client_ep *client_ep = &client->srcs[i]; |
|
|
|
if (client_ep->handle) { |
|
client_ep->cp_handle = handle; |
|
} |
|
} |
|
#endif /* CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT > 0 */ |
|
|
|
if (!client->cp_subscribe.value_handle) { |
|
int err; |
|
|
|
client->cp_subscribe.value_handle = handle; |
|
client->cp_subscribe.ccc_handle = BT_GATT_AUTO_DISCOVER_CCC_HANDLE; |
|
client->cp_subscribe.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE; |
|
client->cp_subscribe.disc_params = &client->disc_params; |
|
client->cp_subscribe.notify = unicast_client_cp_notify; |
|
client->cp_subscribe.value = BT_GATT_CCC_NOTIFY; |
|
client->cp_subscribe.subscribe = unicast_client_cp_sub_cb; |
|
atomic_set_bit(client->cp_subscribe.flags, BT_GATT_SUBSCRIBE_FLAG_VOLATILE); |
|
|
|
err = bt_gatt_subscribe(conn, &client->cp_subscribe); |
|
if (err != 0 && err != -EALREADY) { |
|
LOG_DBG("Failed to subscribe: %d", err); |
|
|
|
unicast_client_discover_complete(conn, err); |
|
|
|
return; |
|
} |
|
} else { /* already subscribed */ |
|
unicast_client_discover_complete(conn, 0); |
|
} |
|
} |
|
|
|
struct net_buf_simple *bt_bap_unicast_client_ep_create_pdu(struct bt_conn *conn, uint8_t op) |
|
{ |
|
struct unicast_client *client = &uni_cli_insts[bt_conn_index(conn)]; |
|
struct bt_ascs_ase_cp *hdr; |
|
|
|
if (atomic_test_bit(client->flags, UNICAST_CLIENT_FLAG_BUSY)) { |
|
return NULL; |
|
} |
|
|
|
hdr = net_buf_simple_add(&client->net_buf, sizeof(*hdr)); |
|
hdr->op = op; |
|
|
|
return &client->net_buf; |
|
} |
|
|
|
static int unicast_client_ep_config(struct bt_bap_ep *ep, struct net_buf_simple *buf, |
|
const struct bt_audio_codec_cfg *codec_cfg) |
|
{ |
|
struct bt_ascs_config *req; |
|
|
|
LOG_DBG("ep %p buf %p codec %p", ep, buf, codec_cfg); |
|
|
|
if (!ep) { |
|
return -EINVAL; |
|
} |
|
|
|
switch (ep->status.state) { |
|
/* Valid only if ASE_State field = 0x00 (Idle) */ |
|
case BT_BAP_EP_STATE_IDLE: |
|
/* or 0x01 (Codec Configured) */ |
|
case BT_BAP_EP_STATE_CODEC_CONFIGURED: |
|
/* or 0x02 (QoS Configured) */ |
|
case BT_BAP_EP_STATE_QOS_CONFIGURED: |
|
break; |
|
default: |
|
LOG_ERR("Invalid state: %s", bt_bap_ep_state_str(ep->status.state)); |
|
return -EINVAL; |
|
} |
|
|
|
LOG_DBG("id 0x%02x dir %s codec 0x%02x", ep->status.id, bt_audio_dir_str(ep->dir), |
|
codec_cfg->id); |
|
|
|
req = net_buf_simple_add(buf, sizeof(*req)); |
|
req->ase = ep->status.id; |
|
req->latency = 0x02; /* TODO: Select target latency based on additional input? */ |
|
req->phy = 0x02; /* TODO: Select target PHY based on additional input? */ |
|
req->codec.id = codec_cfg->id; |
|
req->codec.cid = codec_cfg->cid; |
|
req->codec.vid = codec_cfg->vid; |
|
|
|
req->cc_len = codec_cfg->data_len; |
|
net_buf_simple_add_mem(buf, codec_cfg->data, codec_cfg->data_len); |
|
|
|
return 0; |
|
} |
|
|
|
int bt_bap_unicast_client_ep_qos(struct bt_bap_ep *ep, struct net_buf_simple *buf, |
|
struct bt_bap_qos_cfg *qos) |
|
{ |
|
struct bt_ascs_qos *req; |
|
struct bt_conn_iso *conn_iso; |
|
|
|
LOG_DBG("ep %p buf %p qos %p", ep, buf, qos); |
|
|
|
if (!ep) { |
|
return -EINVAL; |
|
} |
|
|
|
switch (ep->status.state) { |
|
/* Valid only if ASE_State field = 0x01 (Codec Configured) */ |
|
case BT_BAP_EP_STATE_CODEC_CONFIGURED: |
|
/* or 0x02 (QoS Configured) */ |
|
case BT_BAP_EP_STATE_QOS_CONFIGURED: |
|
break; |
|
default: |
|
LOG_ERR("Invalid state: %s", bt_bap_ep_state_str(ep->status.state)); |
|
return -EINVAL; |
|
} |
|
|
|
conn_iso = &ep->iso->chan.iso->iso; |
|
|
|
LOG_DBG("id 0x%02x cig 0x%02x cis 0x%02x interval %u framing 0x%02x " |
|
"phy 0x%02x sdu %u rtn %u latency %u pd %u", |
|
ep->status.id, conn_iso->cig_id, conn_iso->cis_id, qos->interval, qos->framing, |
|
qos->phy, qos->sdu, qos->rtn, qos->latency, qos->pd); |
|
|
|
req = net_buf_simple_add(buf, sizeof(*req)); |
|
req->ase = ep->status.id; |
|
/* TODO: don't hardcode CIG and CIS, they should come from ISO */ |
|
req->cig = conn_iso->cig_id; |
|
req->cis = conn_iso->cis_id; |
|
sys_put_le24(qos->interval, req->interval); |
|
req->framing = qos->framing; |
|
req->phy = qos->phy; |
|
req->sdu = qos->sdu; |
|
req->rtn = qos->rtn; |
|
req->latency = sys_cpu_to_le16(qos->latency); |
|
sys_put_le24(qos->pd, req->pd); |
|
|
|
return 0; |
|
} |
|
|
|
static int unicast_client_ep_enable(struct bt_bap_ep *ep, struct net_buf_simple *buf, |
|
const uint8_t meta[], size_t meta_len) |
|
{ |
|
struct bt_ascs_metadata *req; |
|
|
|
LOG_DBG("ep %p buf %p meta_len %zu", ep, buf, meta_len); |
|
|
|
if (!ep) { |
|
return -EINVAL; |
|
} |
|
|
|
if (ep->status.state != BT_BAP_EP_STATE_QOS_CONFIGURED) { |
|
LOG_ERR("Invalid state: %s", bt_bap_ep_state_str(ep->status.state)); |
|
return -EINVAL; |
|
} |
|
|
|
LOG_DBG("id 0x%02x", ep->status.id); |
|
|
|
req = net_buf_simple_add(buf, sizeof(*req)); |
|
req->ase = ep->status.id; |
|
|
|
req->len = meta_len; |
|
net_buf_simple_add_mem(buf, meta, meta_len); |
|
|
|
return 0; |
|
} |
|
|
|
static int unicast_client_ep_metadata(struct bt_bap_ep *ep, struct net_buf_simple *buf, |
|
const uint8_t meta[], size_t meta_len) |
|
{ |
|
struct bt_ascs_metadata *req; |
|
|
|
LOG_DBG("ep %p buf %p meta_len %zu", ep, buf, meta_len); |
|
|
|
if (!ep) { |
|
return -EINVAL; |
|
} |
|
|
|
switch (ep->status.state) { |
|
/* Valid for an ASE only if ASE_State field = 0x03 (Enabling) */ |
|
case BT_BAP_EP_STATE_ENABLING: |
|
/* or 0x04 (Streaming) */ |
|
case BT_BAP_EP_STATE_STREAMING: |
|
break; |
|
default: |
|
LOG_ERR("Invalid state: %s", bt_bap_ep_state_str(ep->status.state)); |
|
return -EINVAL; |
|
} |
|
|
|
LOG_DBG("id 0x%02x", ep->status.id); |
|
|
|
req = net_buf_simple_add(buf, sizeof(*req)); |
|
req->ase = ep->status.id; |
|
|
|
req->len = meta_len; |
|
net_buf_simple_add_mem(buf, meta, meta_len); |
|
|
|
return 0; |
|
} |
|
|
|
static int unicast_client_ep_start(struct bt_bap_ep *ep, struct net_buf_simple *buf) |
|
{ |
|
LOG_DBG("ep %p buf %p", ep, buf); |
|
|
|
if (!ep) { |
|
return -EINVAL; |
|
} |
|
|
|
if (ep->status.state != BT_BAP_EP_STATE_ENABLING && |
|
ep->status.state != BT_BAP_EP_STATE_DISABLING) { |
|
LOG_ERR("Invalid state: %s", bt_bap_ep_state_str(ep->status.state)); |
|
return -EINVAL; |
|
} |
|
|
|
LOG_DBG("id 0x%02x", ep->status.id); |
|
|
|
net_buf_simple_add_u8(buf, ep->status.id); |
|
|
|
return 0; |
|
} |
|
|
|
static int unicast_client_ep_disable(struct bt_bap_ep *ep, struct net_buf_simple *buf) |
|
{ |
|
LOG_DBG("ep %p buf %p", ep, buf); |
|
|
|
if (!ep) { |
|
return -EINVAL; |
|
} |
|
|
|
switch (ep->status.state) { |
|
/* Valid only if ASE_State field = 0x03 (Enabling) */ |
|
case BT_BAP_EP_STATE_ENABLING: |
|
/* or 0x04 (Streaming) */ |
|
case BT_BAP_EP_STATE_STREAMING: |
|
break; |
|
default: |
|
LOG_ERR("Invalid state: %s", bt_bap_ep_state_str(ep->status.state)); |
|
return -EINVAL; |
|
} |
|
|
|
LOG_DBG("id 0x%02x", ep->status.id); |
|
|
|
net_buf_simple_add_u8(buf, ep->status.id); |
|
|
|
return 0; |
|
} |
|
|
|
static int unicast_client_ep_stop(struct bt_bap_ep *ep, struct net_buf_simple *buf) |
|
{ |
|
LOG_DBG("ep %p buf %p", ep, buf); |
|
|
|
if (!ep) { |
|
return -EINVAL; |
|
} |
|
|
|
/* Valid only if ASE_State field value = 0x05 (Disabling). */ |
|
if (ep->status.state != BT_BAP_EP_STATE_DISABLING) { |
|
LOG_ERR("Invalid state: %s", bt_bap_ep_state_str(ep->status.state)); |
|
return -EINVAL; |
|
} |
|
|
|
LOG_DBG("id 0x%02x", ep->status.id); |
|
|
|
net_buf_simple_add_u8(buf, ep->status.id); |
|
|
|
return 0; |
|
} |
|
|
|
static int unicast_client_ep_release(struct bt_bap_ep *ep, struct net_buf_simple *buf) |
|
{ |
|
LOG_DBG("ep %p buf %p", ep, buf); |
|
|
|
if (!ep) { |
|
return -EINVAL; |
|
} |
|
|
|
switch (ep->status.state) { |
|
/* Valid only if ASE_State field = 0x01 (Codec Configured) */ |
|
case BT_BAP_EP_STATE_CODEC_CONFIGURED: |
|
/* or 0x02 (QoS Configured) */ |
|
case BT_BAP_EP_STATE_QOS_CONFIGURED: |
|
/* or 0x03 (Enabling) */ |
|
case BT_BAP_EP_STATE_ENABLING: |
|
/* or 0x04 (Streaming) */ |
|
case BT_BAP_EP_STATE_STREAMING: |
|
/* or 0x05 (Disabling) */ |
|
case BT_BAP_EP_STATE_DISABLING: |
|
break; |
|
default: |
|
LOG_ERR("Invalid state: %s", bt_bap_ep_state_str(ep->status.state)); |
|
return -EINVAL; |
|
} |
|
|
|
LOG_DBG("id 0x%02x", ep->status.id); |
|
|
|
net_buf_simple_add_u8(buf, ep->status.id); |
|
|
|
return 0; |
|
} |
|
|
|
static void gatt_write_cb(struct bt_conn *conn, uint8_t err, struct bt_gatt_write_params *params) |
|
{ |
|
struct unicast_client *client = &uni_cli_insts[bt_conn_index(conn)]; |
|
|
|
LOG_DBG("conn %p err %u", conn, err); |
|
|
|
memset(params, 0, sizeof(*params)); |
|
reset_att_buf(client); |
|
atomic_clear_bit(client->flags, UNICAST_CLIENT_FLAG_BUSY); |
|
|
|
/* TBD: Should we do anything in case of error here? */ |
|
} |
|
|
|
int bt_bap_unicast_client_ep_send(struct bt_conn *conn, struct bt_bap_ep *ep, |
|
struct net_buf_simple *buf) |
|
{ |
|
const uint16_t max_write_size = bt_audio_get_max_ntf_size(conn); |
|
struct unicast_client *client = &uni_cli_insts[bt_conn_index(conn)]; |
|
struct bt_bap_unicast_client_ep *client_ep = |
|
CONTAINER_OF(ep, struct bt_bap_unicast_client_ep, ep); |
|
int err; |
|
|
|
LOG_DBG("conn %p ep %p buf %p len %u", conn, ep, buf, buf->len); |
|
|
|
if (buf->len > max_write_size) { |
|
if (atomic_test_and_set_bit(client->flags, UNICAST_CLIENT_FLAG_BUSY)) { |
|
LOG_DBG("Client connection is busy"); |
|
return -EBUSY; |
|
} |
|
|
|
client->write_params.func = gatt_write_cb; |
|
client->write_params.handle = client_ep->cp_handle; |
|
client->write_params.offset = 0U; |
|
client->write_params.data = buf->data; |
|
client->write_params.length = buf->len; |
|
#if defined(CONFIG_BT_EATT) |
|
client->write_params.chan_opt = BT_ATT_CHAN_OPT_NONE; |
|
#endif /* CONFIG_BT_EATT */ |
|
|
|
err = bt_gatt_write(conn, &client->write_params); |
|
if (err != 0) { |
|
LOG_DBG("bt_gatt_write failed: %d", err); |
|
atomic_clear_bit(client->flags, UNICAST_CLIENT_FLAG_BUSY); |
|
} |
|
} else { |
|
err = bt_gatt_write_without_response(conn, client_ep->cp_handle, buf->data, |
|
buf->len, false); |
|
if (err != 0) { |
|
LOG_DBG("bt_gatt_write_without_response failed: %d", err); |
|
} |
|
|
|
/* No callback for writing without response, so reset the buffer here */ |
|
reset_att_buf(client); |
|
} |
|
|
|
if (err == 0) { |
|
client_ep->cp_ntf_pending = true; |
|
} |
|
|
|
return err; |
|
} |
|
|
|
static void unicast_client_reset(struct bt_bap_ep *ep, uint8_t reason) |
|
{ |
|
struct bt_bap_unicast_client_ep *client_ep = |
|
CONTAINER_OF(ep, struct bt_bap_unicast_client_ep, ep); |
|
|
|
LOG_DBG("ep %p", ep); |
|
ep->reason = reason; |
|
|
|
/* Pretend we received an idle state notification from the server to trigger all cleanup */ |
|
unicast_client_ep_set_local_idle_state(ep); |
|
|
|
if (ep->iso != NULL && ep->iso->chan.state == BT_ISO_STATE_DISCONNECTING) { |
|
/* Wait for ISO disconnected event */ |
|
return; |
|
} |
|
|
|
(void)k_work_cancel_delayable(&client_ep->ase_read_work); |
|
(void)memset(ep, 0, sizeof(*ep)); |
|
|
|
client_ep->cp_handle = BAP_HANDLE_UNUSED; |
|
client_ep->handle = BAP_HANDLE_UNUSED; |
|
(void)memset(&client_ep->discover, 0, sizeof(client_ep->discover)); |
|
client_ep->release_requested = false; |
|
client_ep->cp_ntf_pending = false; |
|
/* Need to keep the subscribe params intact for the callback */ |
|
} |
|
|
|
static void unicast_client_ep_reset(struct bt_conn *conn, uint8_t reason) |
|
{ |
|
struct unicast_client *client; |
|
uint8_t index; |
|
|
|
LOG_DBG("conn %p", conn); |
|
|
|
index = bt_conn_index(conn); |
|
|
|
#if CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT > 0 |
|
for (size_t i = 0U; i < ARRAY_SIZE(uni_cli_insts[index].snks); i++) { |
|
struct bt_bap_ep *ep = &uni_cli_insts[index].snks[i].ep; |
|
|
|
unicast_client_reset(ep, reason); |
|
} |
|
#endif /* CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT > 0 */ |
|
|
|
#if CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT > 0 |
|
for (size_t i = 0U; i < ARRAY_SIZE(uni_cli_insts[index].srcs); i++) { |
|
struct bt_bap_ep *ep = &uni_cli_insts[index].srcs[i].ep; |
|
|
|
unicast_client_reset(ep, reason); |
|
} |
|
#endif /* CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT > 0 */ |
|
|
|
client = &uni_cli_insts[index]; |
|
atomic_clear_bit(client->flags, UNICAST_CLIENT_FLAG_BUSY); |
|
client->dir = 0U; |
|
reset_att_buf(client); |
|
} |
|
|
|
static void bt_bap_qos_cfg_to_cig_param(struct bt_iso_cig_param *cig_param, |
|
const struct bt_bap_unicast_group *group) |
|
{ |
|
cig_param->framing = group->cig_param.framing; |
|
cig_param->c_to_p_interval = group->cig_param.c_to_p_interval; |
|
cig_param->p_to_c_interval = group->cig_param.p_to_c_interval; |
|
cig_param->c_to_p_latency = group->cig_param.c_to_p_latency; |
|
cig_param->p_to_c_latency = group->cig_param.p_to_c_latency; |
|
cig_param->packing = group->cig_param.packing; |
|
cig_param->sca = BT_GAP_SCA_UNKNOWN; |
|
|
|
IF_ENABLED(CONFIG_BT_ISO_TEST_PARAMS, ({ |
|
cig_param->c_to_p_ft = group->cig_param.c_to_p_ft; |
|
cig_param->p_to_c_ft = group->cig_param.p_to_c_ft; |
|
cig_param->iso_interval = group->cig_param.iso_interval; |
|
})); |
|
|
|
/* In the case that we only setup a single direction, we still need |
|
* (as per section 7.8.97 LE Set CIG Parameters command) to set the interval and latency on |
|
* both sides. The value shall be ignored, but the value may not always be ignored correctly |
|
* by all controllers, so we always set it here. |
|
* If it is at this point unset, then we set the opposing direction to the same value. |
|
*/ |
|
if (cig_param->c_to_p_interval == 0U) { |
|
cig_param->c_to_p_interval = cig_param->p_to_c_interval; |
|
} else if (cig_param->p_to_c_interval == 0U) { |
|
cig_param->p_to_c_interval = cig_param->c_to_p_interval; |
|
} |
|
|
|
if (cig_param->c_to_p_latency == 0U) { |
|
cig_param->c_to_p_latency = cig_param->p_to_c_latency; |
|
} else if (cig_param->p_to_c_latency == 0U) { |
|
cig_param->p_to_c_latency = cig_param->c_to_p_latency; |
|
} |
|
} |
|
|
|
static uint8_t unicast_group_get_cis_count(const struct bt_bap_unicast_group *unicast_group) |
|
{ |
|
uint8_t cis_count = 0U; |
|
|
|
for (size_t i = 0U; i < ARRAY_SIZE(unicast_group->cis); i++) { |
|
if (unicast_group->cis[i] == NULL) { |
|
/* A NULL CIS acts as a NULL terminator */ |
|
break; |
|
} |
|
|
|
cis_count++; |
|
} |
|
|
|
return cis_count; |
|
} |
|
|
|
static int bt_audio_cig_create(struct bt_bap_unicast_group *group) |
|
{ |
|
struct bt_iso_cig_param param = {0}; |
|
int err; |
|
|
|
LOG_DBG("group %p", group); |
|
|
|
param.num_cis = unicast_group_get_cis_count(group); |
|
param.cis_channels = group->cis; |
|
bt_bap_qos_cfg_to_cig_param(¶m, group); |
|
|
|
err = bt_iso_cig_create(¶m, &group->cig); |
|
if (err != 0) { |
|
LOG_ERR("bt_iso_cig_create failed: %d", err); |
|
return err; |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
static int bt_audio_cig_reconfigure(struct bt_bap_unicast_group *group) |
|
{ |
|
struct bt_iso_cig_param param; |
|
uint8_t cis_count; |
|
int err; |
|
|
|
LOG_DBG("group %p ", group); |
|
|
|
cis_count = 0U; |
|
for (size_t i = 0U; i < ARRAY_SIZE(group->cis); i++) { |
|
if (group->cis[i] == NULL) { |
|
/* A NULL CIS acts as a NULL terminator */ |
|
break; |
|
} |
|
|
|
cis_count++; |
|
} |
|
|
|
param.num_cis = cis_count; |
|
param.cis_channels = group->cis; |
|
bt_bap_qos_cfg_to_cig_param(¶m, group); |
|
|
|
err = bt_iso_cig_reconfigure(group->cig, ¶m); |
|
if (err != 0) { |
|
LOG_ERR("bt_iso_cig_create failed: %d", err); |
|
return err; |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
static void audio_stream_qos_cleanup(const struct bt_conn *conn, |
|
struct bt_bap_unicast_group *group) |
|
{ |
|
struct bt_bap_stream *stream; |
|
|
|
SYS_SLIST_FOR_EACH_CONTAINER(&group->streams, stream, _node) { |
|
if (stream->conn != conn) { |
|
/* Channel not part of this ACL, skip */ |
|
continue; |
|
} |
|
|
|
if (stream->ep == NULL) { |
|
/* Stream did not have a endpoint configured yet */ |
|
continue; |
|
} |
|
|
|
bt_bap_iso_unbind_ep(stream->ep->iso, stream->ep); |
|
} |
|
} |
|
|
|
static int unicast_client_cig_terminate(struct bt_bap_unicast_group *group) |
|
{ |
|
LOG_DBG("group %p", group); |
|
|
|
return bt_iso_cig_terminate(group->cig); |
|
} |
|
|
|
static int unicast_group_add_iso(struct bt_bap_unicast_group *group, struct bt_bap_iso *iso) |
|
{ |
|
struct bt_iso_chan **chan_slot = NULL; |
|
|
|
__ASSERT_NO_MSG(group != NULL); |
|
__ASSERT_NO_MSG(iso != NULL); |
|
|
|
/* Append iso channel to the group->cis array */ |
|
for (size_t i = 0U; i < ARRAY_SIZE(group->cis); i++) { |
|
/* Return if already there */ |
|
if (group->cis[i] == &iso->chan) { |
|
return 0; |
|
} |
|
|
|
if (chan_slot == NULL && group->cis[i] == NULL) { |
|
chan_slot = &group->cis[i]; |
|
} |
|
} |
|
|
|
if (chan_slot == NULL) { |
|
return -ENOMEM; |
|
} |
|
|
|
*chan_slot = &iso->chan; |
|
|
|
return 0; |
|
} |
|
|
|
static void unicast_group_del_iso(struct bt_bap_unicast_group *group, struct bt_bap_iso *iso) |
|
{ |
|
struct bt_bap_stream *stream; |
|
|
|
__ASSERT_NO_MSG(group != NULL); |
|
__ASSERT_NO_MSG(iso != NULL); |
|
|
|
SYS_SLIST_FOR_EACH_CONTAINER(&group->streams, stream, _node) { |
|
if (stream->ep->iso == iso) { |
|
/* still in use by some other stream */ |
|
return; |
|
} |
|
} |
|
|
|
for (size_t i = 0U; i < ARRAY_SIZE(group->cis); i++) { |
|
if (group->cis[i] == &iso->chan) { |
|
group->cis[i] = NULL; |
|
return; |
|
} |
|
} |
|
} |
|
|
|
static void unicast_client_qos_cfg_to_iso_qos(struct bt_bap_iso *iso, |
|
const struct bt_bap_qos_cfg *qos, |
|
enum bt_audio_dir dir) |
|
{ |
|
struct bt_iso_chan_io_qos *io_qos; |
|
struct bt_iso_chan_io_qos *other_io_qos; |
|
|
|
if (dir == BT_AUDIO_DIR_SINK) { |
|
/* If the endpoint is a sink, then we need to |
|
* configure our TX parameters |
|
*/ |
|
io_qos = iso->chan.qos->tx; |
|
if (bt_bap_iso_get_ep(true, iso, BT_AUDIO_DIR_SOURCE) == NULL) { |
|
other_io_qos = iso->chan.qos->rx; |
|
} else { |
|
other_io_qos = NULL; |
|
} |
|
} else { |
|
/* If the endpoint is a source, then we need to |
|
* configure our RX parameters |
|
*/ |
|
io_qos = iso->chan.qos->rx; |
|
if (bt_bap_iso_get_ep(true, iso, BT_AUDIO_DIR_SINK) == NULL) { |
|
other_io_qos = iso->chan.qos->tx; |
|
} else { |
|
other_io_qos = NULL; |
|
} |
|
} |
|
|
|
bt_bap_qos_cfg_to_iso_qos(io_qos, qos); |
|
#if defined(CONFIG_BT_ISO_TEST_PARAMS) |
|
iso->chan.qos->num_subevents = qos->num_subevents; |
|
#endif /* CONFIG_BT_ISO_TEST_PARAMS */ |
|
|
|
if (other_io_qos != NULL) { |
|
/* If the opposing ASE of the CIS is not yet configured, we |
|
* still need to set the PHY value when creating the CIG. |
|
*/ |
|
other_io_qos->phy = io_qos->phy; |
|
} |
|
} |
|
|
|
static void unicast_group_set_iso_stream_param(struct bt_bap_unicast_group *group, |
|
struct bt_bap_iso *iso, |
|
struct bt_bap_qos_cfg *qos, |
|
enum bt_audio_dir dir) |
|
{ |
|
/* Store the stream Codec QoS in the bap_iso */ |
|
unicast_client_qos_cfg_to_iso_qos(iso, qos, dir); |
|
|
|
/* Store the group Codec QoS in the group - This assume thats the parameters have been |
|
* verified first |
|
*/ |
|
group->cig_param.framing = qos->framing; |
|
if (dir == BT_AUDIO_DIR_SOURCE) { |
|
group->cig_param.p_to_c_interval = qos->interval; |
|
group->cig_param.p_to_c_latency = qos->latency; |
|
} else { |
|
group->cig_param.c_to_p_interval = qos->interval; |
|
group->cig_param.c_to_p_latency = qos->latency; |
|
} |
|
} |
|
|
|
static void unicast_group_add_stream(struct bt_bap_unicast_group *group, |
|
struct bt_bap_unicast_group_stream_param *param, |
|
struct bt_bap_iso *iso, enum bt_audio_dir dir) |
|
{ |
|
struct bt_bap_stream *stream = param->stream; |
|
struct bt_bap_qos_cfg *qos = param->qos; |
|
|
|
LOG_DBG("group %p stream %p qos %p iso %p dir %u", group, stream, qos, iso, dir); |
|
|
|
__ASSERT_NO_MSG(stream->ep == NULL || (stream->ep != NULL && stream->ep->iso == NULL)); |
|
|
|
stream->qos = qos; |
|
stream->group = group; |
|
|
|
/* iso initialized already */ |
|
bt_bap_iso_bind_stream(iso, stream, dir); |
|
if (stream->ep != NULL) { |
|
bt_bap_iso_bind_ep(iso, stream->ep); |
|
} |
|
|
|
unicast_group_set_iso_stream_param(group, iso, qos, dir); |
|
|
|
sys_slist_append(&group->streams, &stream->_node); |
|
} |
|
|
|
static int unicast_group_add_stream_pair(struct bt_bap_unicast_group *group, |
|
struct bt_bap_unicast_group_stream_pair_param *param) |
|
{ |
|
struct bt_bap_iso *iso; |
|
int err; |
|
|
|
__ASSERT_NO_MSG(group != NULL); |
|
__ASSERT_NO_MSG(param != NULL); |
|
__ASSERT_NO_MSG(param->rx_param != NULL || param->tx_param != NULL); |
|
|
|
iso = bt_bap_unicast_client_new_audio_iso(); |
|
if (iso == NULL) { |
|
return -ENOMEM; |
|
} |
|
|
|
err = unicast_group_add_iso(group, iso); |
|
if (err < 0) { |
|
bt_bap_iso_unref(iso); |
|
return err; |
|
} |
|
|
|
if (param->rx_param != NULL) { |
|
unicast_group_add_stream(group, param->rx_param, iso, BT_AUDIO_DIR_SOURCE); |
|
} |
|
|
|
if (param->tx_param != NULL) { |
|
unicast_group_add_stream(group, param->tx_param, iso, BT_AUDIO_DIR_SINK); |
|
} |
|
|
|
bt_bap_iso_unref(iso); |
|
|
|
return 0; |
|
} |
|
|
|
static void unicast_group_del_stream(struct bt_bap_unicast_group *group, |
|
struct bt_bap_stream *stream, enum bt_audio_dir dir) |
|
{ |
|
__ASSERT_NO_MSG(group != NULL); |
|
__ASSERT_NO_MSG(stream != NULL); |
|
|
|
if (sys_slist_find_and_remove(&group->streams, &stream->_node)) { |
|
struct bt_bap_ep *ep = stream->ep; |
|
|
|
if (stream->bap_iso != NULL) { |
|
bt_bap_iso_unbind_stream(stream->bap_iso, stream, dir); |
|
} |
|
|
|
if (ep != NULL && ep->iso != NULL) { |
|
unicast_group_del_iso(group, ep->iso); |
|
|
|
bt_bap_iso_unbind_ep(ep->iso, ep); |
|
} |
|
|
|
stream->group = NULL; |
|
} |
|
} |
|
|
|
static void unicast_group_del_stream_pair(struct bt_bap_unicast_group *group, |
|
struct bt_bap_unicast_group_stream_pair_param *param) |
|
{ |
|
__ASSERT_NO_MSG(group != NULL); |
|
__ASSERT_NO_MSG(param != NULL); |
|
__ASSERT_NO_MSG(param->rx_param != NULL || param->tx_param != NULL); |
|
|
|
if (param->rx_param != NULL) { |
|
__ASSERT_NO_MSG(param->rx_param->stream); |
|
unicast_group_del_stream(group, param->rx_param->stream, BT_AUDIO_DIR_SOURCE); |
|
} |
|
|
|
if (param->tx_param != NULL) { |
|
__ASSERT_NO_MSG(param->tx_param->stream); |
|
unicast_group_del_stream(group, param->tx_param->stream, BT_AUDIO_DIR_SINK); |
|
} |
|
} |
|
|
|
static struct bt_bap_unicast_group *unicast_group_alloc(void) |
|
{ |
|
struct bt_bap_unicast_group *group = NULL; |
|
|
|
for (size_t i = 0U; i < ARRAY_SIZE(unicast_groups); i++) { |
|
if (!unicast_groups[i].allocated) { |
|
group = &unicast_groups[i]; |
|
|
|
(void)memset(group, 0, sizeof(*group)); |
|
|
|
group->allocated = true; |
|
group->index = i; |
|
|
|
break; |
|
} |
|
} |
|
|
|
return group; |
|
} |
|
|
|
static void unicast_group_free(struct bt_bap_unicast_group *group) |
|
{ |
|
struct bt_bap_stream *stream, *next; |
|
|
|
__ASSERT_NO_MSG(group != NULL); |
|
|
|
SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&group->streams, stream, next, _node) { |
|
struct bt_bap_iso *bap_iso = stream->bap_iso; |
|
struct bt_bap_ep *ep = stream->ep; |
|
|
|
stream->group = NULL; |
|
if (bap_iso != NULL) { |
|
if (bap_iso->rx.stream == stream) { |
|
bt_bap_iso_unbind_stream(stream->bap_iso, stream, |
|
BT_AUDIO_DIR_SOURCE); |
|
} else if (bap_iso->tx.stream == stream) { |
|
bt_bap_iso_unbind_stream(stream->bap_iso, stream, |
|
BT_AUDIO_DIR_SINK); |
|
} else { |
|
__ASSERT_PRINT("stream %p has invalid bap_iso %p", stream, bap_iso); |
|
} |
|
} |
|
|
|
if (ep != NULL && ep->iso != NULL) { |
|
bt_bap_iso_unbind_ep(ep->iso, ep); |
|
} |
|
|
|
sys_slist_remove(&group->streams, NULL, &stream->_node); |
|
} |
|
|
|
group->allocated = false; |
|
} |
|
|
|
static int stream_param_check(const struct bt_bap_unicast_group_stream_param *param) |
|
{ |
|
CHECKIF(param->stream == NULL) |
|
{ |
|
LOG_DBG("param->stream is NULL"); |
|
return -EINVAL; |
|
} |
|
|
|
CHECKIF(param->qos == NULL) |
|
{ |
|
LOG_DBG("param->qos is NULL"); |
|
return -EINVAL; |
|
} |
|
|
|
if (param->stream != NULL && param->stream->group != NULL) { |
|
LOG_DBG("stream %p already part of group %p", param->stream, param->stream->group); |
|
return -EALREADY; |
|
} |
|
|
|
CHECKIF(bt_audio_verify_qos(param->qos) != BT_BAP_ASCS_REASON_NONE) |
|
{ |
|
LOG_DBG("Invalid QoS"); |
|
return -EINVAL; |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
static int stream_pair_param_check(const struct bt_bap_unicast_group_stream_pair_param *param) |
|
{ |
|
int err; |
|
|
|
CHECKIF(param->rx_param == NULL && param->tx_param == NULL) |
|
{ |
|
LOG_DBG("Invalid stream parameters"); |
|
return -EINVAL; |
|
} |
|
|
|
if (param->rx_param != NULL) { |
|
err = stream_param_check(param->rx_param); |
|
if (err < 0) { |
|
return err; |
|
} |
|
} |
|
|
|
if (param->tx_param != NULL) { |
|
err = stream_param_check(param->tx_param); |
|
if (err < 0) { |
|
return err; |
|
} |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
/** Validates that the stream parameter does not contain invalid values */ |
|
static bool valid_unicast_group_stream_param(const struct bt_bap_unicast_group *unicast_group, |
|
const struct bt_bap_unicast_group_stream_param *param, |
|
struct bt_bap_unicast_group_cig_param *cig_param, |
|
enum bt_audio_dir dir) |
|
{ |
|
const struct bt_bap_qos_cfg *qos; |
|
|
|
CHECKIF(param->stream == NULL) { |
|
LOG_DBG("param->stream is NULL"); |
|
return -EINVAL; |
|
} |
|
|
|
CHECKIF(param->qos == NULL) { |
|
LOG_DBG("param->qos is NULL"); |
|
return -EINVAL; |
|
} |
|
|
|
if (param->stream != NULL && param->stream->group != NULL) { |
|
if (unicast_group != NULL && param->stream->group != unicast_group) { |
|
LOG_DBG("stream %p not part of group %p (%p)", param->stream, unicast_group, |
|
param->stream->group); |
|
} else { |
|
LOG_DBG("stream %p already part of group %p", param->stream, |
|
param->stream->group); |
|
} |
|
return -EALREADY; |
|
} |
|
|
|
CHECKIF(bt_audio_verify_qos(param->qos) != BT_BAP_ASCS_REASON_NONE) { |
|
LOG_DBG("Invalid QoS"); |
|
return -EINVAL; |
|
} |
|
|
|
qos = param->qos; |
|
|
|
/* If unset we set the interval else we verify that all streams use the same interval and |
|
* latency in the same direction, as that is required when creating a CIG |
|
*/ |
|
if (dir == BT_AUDIO_DIR_SINK) { |
|
if (cig_param->c_to_p_interval == 0) { |
|
cig_param->c_to_p_interval = qos->interval; |
|
} else if (cig_param->c_to_p_interval != qos->interval) { |
|
return false; |
|
} |
|
|
|
if (cig_param->c_to_p_latency == 0) { |
|
cig_param->c_to_p_latency = qos->latency; |
|
} else if (cig_param->c_to_p_latency != qos->latency) { |
|
return false; |
|
} |
|
} else { |
|
if (cig_param->p_to_c_interval == 0) { |
|
cig_param->p_to_c_interval = qos->interval; |
|
} else if (cig_param->p_to_c_interval != qos->interval) { |
|
return false; |
|
} |
|
|
|
if (cig_param->p_to_c_latency == 0) { |
|
cig_param->p_to_c_latency = qos->latency; |
|
} else if (cig_param->p_to_c_latency != qos->latency) { |
|
return false; |
|
} |
|
} |
|
|
|
if (cig_param->framing == 0) { |
|
if (qos->framing == BT_BAP_QOS_CFG_FRAMING_UNFRAMED) { |
|
cig_param->framing = BT_ISO_FRAMING_UNFRAMED; |
|
} else if (qos->framing == BT_BAP_QOS_CFG_FRAMING_FRAMED) { |
|
cig_param->framing = BT_ISO_FRAMING_FRAMED; |
|
} |
|
} else if ((qos->framing == BT_BAP_QOS_CFG_FRAMING_UNFRAMED && |
|
cig_param->framing != BT_ISO_FRAMING_UNFRAMED) || |
|
(qos->framing == BT_BAP_QOS_CFG_FRAMING_FRAMED && |
|
cig_param->framing != BT_ISO_FRAMING_FRAMED)) { |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
static bool |
|
valid_group_stream_pair_param(const struct bt_bap_unicast_group *unicast_group, |
|
const struct bt_bap_unicast_group_stream_pair_param *pair_param) |
|
{ |
|
struct bt_bap_unicast_group_cig_param cig_param = {0}; |
|
|
|
CHECKIF(pair_param == NULL) { |
|
LOG_DBG("pair_param is NULL"); |
|
return false; |
|
} |
|
|
|
if (pair_param->rx_param != NULL) { |
|
if (!valid_unicast_group_stream_param(unicast_group, pair_param->rx_param, |
|
&cig_param, BT_AUDIO_DIR_SOURCE)) { |
|
return false; |
|
} |
|
} |
|
|
|
if (pair_param->tx_param != NULL) { |
|
if (!valid_unicast_group_stream_param(unicast_group, pair_param->tx_param, |
|
&cig_param, BT_AUDIO_DIR_SINK)) { |
|
return false; |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
static bool valid_unicast_group_param(struct bt_bap_unicast_group *unicast_group, |
|
const struct bt_bap_unicast_group_param *param) |
|
{ |
|
CHECKIF(param == NULL) { |
|
LOG_DBG("streams is NULL"); |
|
return false; |
|
} |
|
|
|
CHECKIF(param->params_count > UNICAST_GROUP_STREAM_CNT) { |
|
LOG_DBG("Too many streams provided: %u/%u", param->params_count, |
|
UNICAST_GROUP_STREAM_CNT); |
|
return false; |
|
} |
|
|
|
if (unicast_group != NULL) { |
|
const size_t group_cis_cnt = unicast_group_get_cis_count(unicast_group); |
|
|
|
if (param->params_count != group_cis_cnt) { |
|
LOG_DBG("Mismatch between group CIS count (%zu) and params_count (%zu)", |
|
group_cis_cnt, param->params_count); |
|
|
|
return false; |
|
} |
|
} |
|
|
|
for (size_t i = 0U; i < param->params_count; i++) { |
|
if (!valid_group_stream_pair_param(unicast_group, ¶m->params[i])) { |
|
return false; |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
int bt_bap_unicast_group_create(struct bt_bap_unicast_group_param *param, |
|
struct bt_bap_unicast_group **out_unicast_group) |
|
{ |
|
struct bt_bap_unicast_group *unicast_group; |
|
int err; |
|
|
|
CHECKIF(out_unicast_group == NULL) |
|
{ |
|
LOG_DBG("out_unicast_group is NULL"); |
|
return -EINVAL; |
|
} |
|
/* Set out_unicast_group to NULL until the source has actually been created */ |
|
*out_unicast_group = NULL; |
|
|
|
unicast_group = unicast_group_alloc(); |
|
if (unicast_group == NULL) { |
|
LOG_DBG("Could not allocate any more unicast groups"); |
|
return -ENOMEM; |
|
} |
|
|
|
if (!valid_unicast_group_param(NULL, param)) { |
|
unicast_group_free(unicast_group); |
|
|
|
return -EINVAL; |
|
} |
|
|
|
for (size_t i = 0U; i < param->params_count; i++) { |
|
struct bt_bap_unicast_group_stream_pair_param *stream_param; |
|
|
|
stream_param = ¶m->params[i]; |
|
|
|
unicast_group->cig_param.packing = param->packing; |
|
IF_ENABLED(CONFIG_BT_ISO_TEST_PARAMS, ({ |
|
unicast_group->cig_param.c_to_p_ft = param->c_to_p_ft; |
|
unicast_group->cig_param.p_to_c_ft = param->p_to_c_ft; |
|
unicast_group->cig_param.iso_interval = param->iso_interval; |
|
})); |
|
|
|
err = unicast_group_add_stream_pair(unicast_group, stream_param); |
|
if (err < 0) { |
|
LOG_DBG("unicast_group_add_stream failed: %d", err); |
|
unicast_group_free(unicast_group); |
|
|
|
return err; |
|
} |
|
} |
|
|
|
err = bt_audio_cig_create(unicast_group); |
|
if (err != 0) { |
|
LOG_DBG("bt_audio_cig_create failed: %d", err); |
|
unicast_group_free(unicast_group); |
|
|
|
return err; |
|
} |
|
|
|
*out_unicast_group = unicast_group; |
|
|
|
return 0; |
|
} |
|
|
|
int bt_bap_unicast_group_reconfig(struct bt_bap_unicast_group *unicast_group, |
|
const struct bt_bap_unicast_group_param *param) |
|
{ |
|
struct bt_iso_chan_io_qos rx_io_qos_backup[UNICAST_GROUP_STREAM_CNT]; |
|
struct bt_iso_chan_io_qos tx_io_qos_backup[UNICAST_GROUP_STREAM_CNT]; |
|
struct bt_bap_unicast_group_cig_param cig_param_backup; |
|
struct bt_bap_stream *tmp_stream; |
|
size_t idx; |
|
int err; |
|
|
|
IF_ENABLED(CONFIG_BT_ISO_TEST_PARAMS, |
|
(uint8_t num_subevents_backup[UNICAST_GROUP_STREAM_CNT])); |
|
|
|
CHECKIF(unicast_group == NULL) { |
|
LOG_DBG("unicast_group is NULL"); |
|
return -EINVAL; |
|
} |
|
|
|
if (unicast_group->has_been_connected) { |
|
LOG_DBG("Cannot modify a unicast_group where a CIS has been connected"); |
|
return -EINVAL; |
|
} |
|
|
|
if (!valid_unicast_group_param(unicast_group, param)) { |
|
return -EINVAL; |
|
} |
|
|
|
/* Make backups of the values in case that the reconfigure request is rejected by e.g. the |
|
* controller |
|
*/ |
|
idx = 0U; |
|
SYS_SLIST_FOR_EACH_CONTAINER(&unicast_group->streams, tmp_stream, _node) { |
|
memcpy(&rx_io_qos_backup[idx], tmp_stream->bap_iso->chan.qos->rx, |
|
sizeof(rx_io_qos_backup[idx])); |
|
memcpy(&tx_io_qos_backup[idx], tmp_stream->bap_iso->chan.qos->tx, |
|
sizeof(tx_io_qos_backup[idx])); |
|
IF_ENABLED( |
|
CONFIG_BT_ISO_TEST_PARAMS, |
|
(num_subevents_backup[idx] = tmp_stream->bap_iso->chan.qos->num_subevents)); |
|
idx++; |
|
} |
|
memcpy(&cig_param_backup, &unicast_group->cig_param, sizeof(cig_param_backup)); |
|
|
|
/* Update the stream and group parameters */ |
|
for (size_t i = 0U; i < param->params_count; i++) { |
|
struct bt_bap_unicast_group_stream_pair_param *stream_param = ¶m->params[i]; |
|
struct bt_bap_unicast_group_stream_param *rx_param = stream_param->rx_param; |
|
struct bt_bap_unicast_group_stream_param *tx_param = stream_param->tx_param; |
|
|
|
if (rx_param != NULL) { |
|
unicast_group_set_iso_stream_param(unicast_group, rx_param->stream->bap_iso, |
|
rx_param->qos, BT_AUDIO_DIR_SOURCE); |
|
} |
|
|
|
if (tx_param != NULL) { |
|
unicast_group_set_iso_stream_param(unicast_group, tx_param->stream->bap_iso, |
|
tx_param->qos, BT_AUDIO_DIR_SOURCE); |
|
} |
|
} |
|
|
|
/* Reconfigure the CIG based on the above new values */ |
|
err = bt_audio_cig_reconfigure(unicast_group); |
|
if (err != 0) { |
|
LOG_DBG("bt_audio_cig_reconfigure failed: %d", err); |
|
|
|
/* Revert any changes above */ |
|
memcpy(&unicast_group->cig_param, &cig_param_backup, sizeof(cig_param_backup)); |
|
idx = 0U; |
|
SYS_SLIST_FOR_EACH_CONTAINER(&unicast_group->streams, tmp_stream, _node) { |
|
memcpy(tmp_stream->bap_iso->chan.qos->rx, &rx_io_qos_backup[idx], |
|
sizeof(rx_io_qos_backup[idx])); |
|
memcpy(tmp_stream->bap_iso->chan.qos->tx, &tx_io_qos_backup[idx], |
|
sizeof(tx_io_qos_backup[idx])); |
|
IF_ENABLED(CONFIG_BT_ISO_TEST_PARAMS, |
|
(tmp_stream->bap_iso->chan.qos->num_subevents = |
|
num_subevents_backup[idx])); |
|
idx++; |
|
} |
|
|
|
return err; |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
int bt_bap_unicast_group_add_streams(struct bt_bap_unicast_group *unicast_group, |
|
struct bt_bap_unicast_group_stream_pair_param params[], |
|
size_t num_param) |
|
{ |
|
struct bt_bap_stream *tmp_stream; |
|
size_t total_stream_cnt; |
|
struct bt_iso_cig *cig; |
|
size_t num_added; |
|
int err; |
|
|
|
CHECKIF(unicast_group == NULL) |
|
{ |
|
LOG_DBG("unicast_group is NULL"); |
|
return -EINVAL; |
|
} |
|
|
|
if (unicast_group->has_been_connected) { |
|
LOG_DBG("Cannot modify a unicast_group where a CIS has been connected"); |
|
return -EINVAL; |
|
} |
|
|
|
CHECKIF(params == NULL) |
|
{ |
|
LOG_DBG("params is NULL"); |
|
return -EINVAL; |
|
} |
|
|
|
CHECKIF(num_param == 0) |
|
{ |
|
LOG_DBG("num_param is 0"); |
|
return -EINVAL; |
|
} |
|
|
|
total_stream_cnt = num_param; |
|
SYS_SLIST_FOR_EACH_CONTAINER(&unicast_group->streams, tmp_stream, _node) { |
|
total_stream_cnt++; |
|
} |
|
|
|
if (total_stream_cnt > UNICAST_GROUP_STREAM_CNT) { |
|
LOG_DBG("Too many streams provided: %u/%u", total_stream_cnt, |
|
UNICAST_GROUP_STREAM_CNT); |
|
return -EINVAL; |
|
} |
|
|
|
for (size_t i = 0U; i < num_param; i++) { |
|
if (!valid_group_stream_pair_param(unicast_group, ¶ms[i])) { |
|
return -EINVAL; |
|
} |
|
} |
|
|
|
/* We can just check the CIG state to see if any streams have started as |
|
* that would start the ISO connection procedure |
|
*/ |
|
cig = unicast_group->cig; |
|
if (cig != NULL && cig->state != BT_ISO_CIG_STATE_CONFIGURED) { |
|
LOG_DBG("At least one unicast group stream is started"); |
|
return -EBADMSG; |
|
} |
|
|
|
for (num_added = 0U; num_added < num_param; num_added++) { |
|
struct bt_bap_unicast_group_stream_pair_param *stream_param; |
|
|
|
stream_param = ¶ms[num_added]; |
|
|
|
err = stream_pair_param_check(stream_param); |
|
if (err < 0) { |
|
return err; |
|
} |
|
|
|
err = unicast_group_add_stream_pair(unicast_group, stream_param); |
|
if (err < 0) { |
|
LOG_DBG("unicast_group_add_stream failed: %d", err); |
|
goto fail; |
|
} |
|
} |
|
|
|
err = bt_audio_cig_reconfigure(unicast_group); |
|
if (err != 0) { |
|
LOG_DBG("bt_audio_cig_reconfigure failed: %d", err); |
|
goto fail; |
|
} |
|
|
|
return 0; |
|
|
|
fail: |
|
/* Restore group by removing the newly added streams */ |
|
while (num_added--) { |
|
unicast_group_del_stream_pair(unicast_group, ¶ms[num_added]); |
|
} |
|
|
|
return err; |
|
} |
|
|
|
int bt_bap_unicast_group_delete(struct bt_bap_unicast_group *unicast_group) |
|
{ |
|
struct bt_bap_stream *stream; |
|
|
|
CHECKIF(unicast_group == NULL) |
|
{ |
|
LOG_DBG("unicast_group is NULL"); |
|
return -EINVAL; |
|
} |
|
|
|
SYS_SLIST_FOR_EACH_CONTAINER(&unicast_group->streams, stream, _node) { |
|
/* If a stream has an endpoint, it is not ready to be removed |
|
* from a group, as it is not in an idle state |
|
*/ |
|
if (stream->ep != NULL) { |
|
LOG_DBG("stream %p is not released", stream); |
|
return -EINVAL; |
|
} |
|
} |
|
|
|
if (unicast_group->cig != NULL) { |
|
const int err = unicast_client_cig_terminate(unicast_group); |
|
|
|
if (err != 0) { |
|
LOG_DBG("unicast_client_cig_terminate failed with err %d", err); |
|
|
|
return err; |
|
} |
|
} |
|
|
|
unicast_group_free(unicast_group); |
|
|
|
return 0; |
|
} |
|
|
|
int bt_bap_unicast_client_config(struct bt_bap_stream *stream, |
|
const struct bt_audio_codec_cfg *codec_cfg) |
|
{ |
|
struct bt_bap_ep *ep = stream->ep; |
|
struct bt_ascs_config_op *op; |
|
struct net_buf_simple *buf; |
|
int err; |
|
|
|
LOG_DBG("stream %p", stream); |
|
|
|
if (stream->conn == NULL) { |
|
LOG_DBG("Stream %p does not have a connection", stream); |
|
|
|
return -ENOTCONN; |
|
} |
|
|
|
buf = bt_bap_unicast_client_ep_create_pdu(stream->conn, BT_ASCS_CONFIG_OP); |
|
if (buf == NULL) { |
|
LOG_DBG("Could not create PDU"); |
|
return -EBUSY; |
|
} |
|
|
|
op = net_buf_simple_add(buf, sizeof(*op)); |
|
op->num_ases = 0x01; |
|
|
|
err = unicast_client_ep_config(ep, buf, codec_cfg); |
|
if (err != 0) { |
|
return err; |
|
} |
|
|
|
err = bt_bap_unicast_client_ep_send(stream->conn, ep, buf); |
|
if (err != 0) { |
|
return err; |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
int bt_bap_unicast_client_qos(struct bt_conn *conn, struct bt_bap_unicast_group *group) |
|
{ |
|
struct bt_bap_stream *stream; |
|
struct bt_ascs_config_op *op; |
|
struct net_buf_simple *buf; |
|
struct bt_bap_ep *ep; |
|
bool conn_stream_found; |
|
int err; |
|
|
|
if (conn == NULL) { |
|
LOG_DBG("conn is NULL"); |
|
|
|
return -ENOTCONN; |
|
} |
|
|
|
/* Used to determine if a stream for the supplied connection pointer |
|
* was actually found |
|
*/ |
|
conn_stream_found = false; |
|
|
|
/* Validate streams before starting the QoS execution */ |
|
SYS_SLIST_FOR_EACH_CONTAINER(&group->streams, stream, _node) { |
|
if (stream->conn != conn) { |
|
/* Channel not part of this ACL, skip */ |
|
continue; |
|
} |
|
conn_stream_found = true; |
|
|
|
ep = stream->ep; |
|
if (ep == NULL) { |
|
LOG_DBG("stream->ep is NULL"); |
|
return -EINVAL; |
|
} |
|
|
|
/* Can only be done if all the streams are in the codec |
|
* configured state or the QoS configured state |
|
*/ |
|
switch (ep->status.state) { |
|
case BT_BAP_EP_STATE_CODEC_CONFIGURED: |
|
case BT_BAP_EP_STATE_QOS_CONFIGURED: |
|
break; |
|
default: |
|
LOG_DBG("Invalid state: %s", bt_bap_ep_state_str(stream->ep->status.state)); |
|
return -EINVAL; |
|
} |
|
|
|
if (bt_bap_stream_verify_qos(stream, stream->qos) != BT_BAP_ASCS_REASON_NONE) { |
|
return -EINVAL; |
|
} |
|
|
|
/* Verify ep->dir */ |
|
switch (ep->dir) { |
|
case BT_AUDIO_DIR_SINK: |
|
case BT_AUDIO_DIR_SOURCE: |
|
break; |
|
default: |
|
__ASSERT(false, "invalid endpoint dir: %u", ep->dir); |
|
return -EINVAL; |
|
} |
|
|
|
if (stream->bap_iso == NULL) { |
|
/* This can only happen if the stream was somehow added |
|
* to a group without the bap_iso being bound to it |
|
*/ |
|
LOG_ERR("Could not find bap_iso for stream %p", stream); |
|
return -EINVAL; |
|
} |
|
} |
|
|
|
if (!conn_stream_found) { |
|
LOG_DBG("No streams in the group %p for conn %p", group, conn); |
|
return -EINVAL; |
|
} |
|
|
|
/* Generate the control point write */ |
|
buf = bt_bap_unicast_client_ep_create_pdu(conn, BT_ASCS_QOS_OP); |
|
if (buf == NULL) { |
|
LOG_DBG("Could not create PDU"); |
|
return -EBUSY; |
|
} |
|
|
|
op = net_buf_simple_add(buf, sizeof(*op)); |
|
|
|
(void)memset(op, 0, sizeof(*op)); |
|
ep = NULL; /* Needed to find the control point handle */ |
|
SYS_SLIST_FOR_EACH_CONTAINER(&group->streams, stream, _node) { |
|
if (stream->conn != conn) { |
|
/* Channel not part of this ACL, skip */ |
|
continue; |
|
} |
|
|
|
op->num_ases++; |
|
|
|
if (stream->ep->iso == NULL) { |
|
/* Not yet bound with the bap_iso */ |
|
bt_bap_iso_bind_ep(stream->bap_iso, stream->ep); |
|
} |
|
|
|
err = bt_bap_unicast_client_ep_qos(stream->ep, buf, stream->qos); |
|
if (err != 0) { |
|
audio_stream_qos_cleanup(conn, group); |
|
|
|
return err; |
|
} |
|
|
|
if (ep == NULL) { |
|
ep = stream->ep; |
|
} |
|
} |
|
|
|
err = bt_bap_unicast_client_ep_send(conn, ep, buf); |
|
if (err != 0) { |
|
LOG_DBG("Could not send config QoS: %d", err); |
|
audio_stream_qos_cleanup(conn, group); |
|
|
|
return err; |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
int bt_bap_unicast_client_enable(struct bt_bap_stream *stream, const uint8_t meta[], |
|
size_t meta_len) |
|
{ |
|
struct bt_bap_ep *ep = stream->ep; |
|
struct net_buf_simple *buf; |
|
struct bt_ascs_enable_op *req; |
|
int err; |
|
|
|
LOG_DBG("stream %p", stream); |
|
|
|
if (stream->conn == NULL) { |
|
LOG_DBG("Stream %p does not have a connection", stream); |
|
|
|
return -ENOTCONN; |
|
} |
|
|
|
buf = bt_bap_unicast_client_ep_create_pdu(stream->conn, BT_ASCS_ENABLE_OP); |
|
if (buf == NULL) { |
|
LOG_DBG("Could not create PDU"); |
|
return -EBUSY; |
|
} |
|
|
|
req = net_buf_simple_add(buf, sizeof(*req)); |
|
req->num_ases = 0x01; |
|
|
|
err = unicast_client_ep_enable(ep, buf, meta, meta_len); |
|
if (err) { |
|
return err; |
|
} |
|
|
|
return bt_bap_unicast_client_ep_send(stream->conn, ep, buf); |
|
} |
|
|
|
int bt_bap_unicast_client_metadata(struct bt_bap_stream *stream, const uint8_t meta[], |
|
size_t meta_len) |
|
{ |
|
struct bt_bap_ep *ep = stream->ep; |
|
struct net_buf_simple *buf; |
|
struct bt_ascs_enable_op *req; |
|
int err; |
|
|
|
LOG_DBG("stream %p", stream); |
|
|
|
if (stream->conn == NULL) { |
|
LOG_DBG("Stream %p does not have a connection", stream); |
|
|
|
return -ENOTCONN; |
|
} |
|
|
|
buf = bt_bap_unicast_client_ep_create_pdu(stream->conn, BT_ASCS_METADATA_OP); |
|
if (buf == NULL) { |
|
LOG_DBG("Could not create PDU"); |
|
return -EBUSY; |
|
} |
|
|
|
req = net_buf_simple_add(buf, sizeof(*req)); |
|
req->num_ases = 0x01; |
|
|
|
err = unicast_client_ep_metadata(ep, buf, meta, meta_len); |
|
if (err) { |
|
return err; |
|
} |
|
|
|
return bt_bap_unicast_client_ep_send(stream->conn, ep, buf); |
|
} |
|
|
|
int bt_bap_unicast_client_connect(struct bt_bap_stream *stream) |
|
{ |
|
struct bt_iso_connect_param param; |
|
struct bt_iso_chan *iso_chan; |
|
enum bt_iso_state iso_state; |
|
|
|
LOG_DBG("stream %p", stream); |
|
|
|
if (stream == NULL) { |
|
LOG_DBG("stream is NULL"); |
|
|
|
return -EINVAL; |
|
} |
|
|
|
iso_chan = bt_bap_stream_iso_chan_get(stream); |
|
if (iso_chan == NULL) { |
|
LOG_DBG("iso_chan is NULL"); |
|
|
|
return -EINVAL; |
|
} |
|
|
|
LOG_DBG("stream %p iso %p", stream, iso_chan); |
|
|
|
param.acl = stream->conn; |
|
param.iso_chan = iso_chan; |
|
|
|
iso_state = iso_chan->state; |
|
if (iso_state == BT_ISO_STATE_DISCONNECTED) { |
|
const int err = bt_iso_chan_connect(¶m, 1); |
|
|
|
if (err == 0 || err == -EBUSY) { /* Expected / known return values */ |
|
return err; |
|
} |
|
|
|
/* unknown return value*/ |
|
LOG_DBG("Unexpected err %d from bt_iso_chan_connect", err); |
|
|
|
return -ENOEXEC; |
|
} else if (iso_state == BT_ISO_STATE_CONNECTING || iso_state == BT_ISO_STATE_CONNECTED) { |
|
LOG_DBG("iso_chan %p already connecting or connected (%u)", iso_chan, iso_state); |
|
|
|
return -EALREADY; |
|
} |
|
|
|
LOG_DBG("iso_chan %p in invalid state state (%u), cannot connect", iso_chan, iso_state); |
|
|
|
return -EBADMSG; |
|
} |
|
|
|
int bt_bap_unicast_client_start(struct bt_bap_stream *stream) |
|
{ |
|
struct bt_bap_ep *ep = stream->ep; |
|
int err; |
|
|
|
LOG_DBG("stream %p", stream); |
|
|
|
if (stream->conn == NULL) { |
|
LOG_DBG("Stream %p does not have a connection", stream); |
|
|
|
return -ENOTCONN; |
|
} |
|
|
|
/* As per the ASCS spec, only source streams can be started by the client */ |
|
if (ep->dir == BT_AUDIO_DIR_SINK) { |
|
LOG_DBG("Stream %p is not a source stream", stream); |
|
|
|
return -EINVAL; |
|
} |
|
|
|
ep->receiver_ready = true; |
|
|
|
err = unicast_client_send_start(ep); |
|
if (err != 0) { |
|
LOG_DBG("Failed to send start: %d", err); |
|
|
|
/* TBD: Should we release the stream then? */ |
|
ep->receiver_ready = false; |
|
|
|
return err; |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
int bt_bap_unicast_client_disable(struct bt_bap_stream *stream) |
|
{ |
|
struct bt_bap_ep *ep = stream->ep; |
|
struct net_buf_simple *buf; |
|
struct bt_ascs_disable_op *req; |
|
int err; |
|
|
|
LOG_DBG("stream %p", stream); |
|
|
|
if (stream->conn == NULL) { |
|
LOG_DBG("Stream %p does not have a connection", stream); |
|
|
|
return -ENOTCONN; |
|
} |
|
|
|
buf = bt_bap_unicast_client_ep_create_pdu(stream->conn, BT_ASCS_DISABLE_OP); |
|
if (buf == NULL) { |
|
LOG_DBG("Could not create PDU"); |
|
return -EBUSY; |
|
} |
|
|
|
req = net_buf_simple_add(buf, sizeof(*req)); |
|
req->num_ases = 0x01; |
|
|
|
err = unicast_client_ep_disable(ep, buf); |
|
if (err) { |
|
return err; |
|
} |
|
|
|
return bt_bap_unicast_client_ep_send(stream->conn, ep, buf); |
|
} |
|
|
|
int bt_bap_unicast_client_stop(struct bt_bap_stream *stream) |
|
{ |
|
struct bt_bap_ep *ep = stream->ep; |
|
enum bt_iso_state iso_state; |
|
struct net_buf_simple *buf; |
|
struct bt_ascs_start_op *req; |
|
int err; |
|
|
|
LOG_DBG("stream %p", stream); |
|
|
|
if (stream->conn == NULL) { |
|
LOG_DBG("Stream %p does not have a connection", stream); |
|
|
|
return -ENOTCONN; |
|
} |
|
|
|
/* ASCS_v1.0 3.2 ASE state machine transitions |
|
* |
|
* If the server detects link loss of a CIS for an ASE in the Streaming state or the |
|
* Disabling state, the server shall immediately transition that ASE to the QoS Configured |
|
* state. |
|
* |
|
* This effectively means that if an ASE no longer has a connected CIS, the server shall |
|
* bring it to the QoS Configured state. That means that we, as a unicast client, should not |
|
* attempt to stop it |
|
*/ |
|
if (ep->iso == NULL) { |
|
LOG_DBG("Stream endpoint does not have a CIS, server will stop the ASE"); |
|
return -EALREADY; |
|
} |
|
|
|
iso_state = ep->iso->chan.state; |
|
if (iso_state != BT_ISO_STATE_CONNECTED && iso_state != BT_ISO_STATE_CONNECTING) { |
|
LOG_DBG("Stream endpoint CIS is not connected, server will stop the ASE"); |
|
return -EALREADY; |
|
} |
|
|
|
buf = bt_bap_unicast_client_ep_create_pdu(stream->conn, BT_ASCS_STOP_OP); |
|
if (buf == NULL) { |
|
LOG_DBG("Could not create PDU"); |
|
return -EBUSY; |
|
} |
|
|
|
req = net_buf_simple_add(buf, sizeof(*req)); |
|
req->num_ases = 0x00; |
|
|
|
/* When initiated by the client, valid only if Direction field |
|
* parameter value = 0x02 (Server is Audio Source) |
|
*/ |
|
if (ep->dir == BT_AUDIO_DIR_SOURCE) { |
|
err = unicast_client_ep_stop(ep, buf); |
|
if (err) { |
|
return err; |
|
} |
|
req->num_ases++; |
|
|
|
err = bt_bap_unicast_client_ep_send(stream->conn, ep, buf); |
|
if (err != 0) { |
|
/* Return expected error directly */ |
|
if (err == -ENOTCONN || err == -ENOMEM) { |
|
return err; |
|
} |
|
|
|
LOG_DBG("bt_bap_unicast_client_ep_send failed with unexpected error %d", |
|
err); |
|
|
|
return -ENOEXEC; |
|
} |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
int bt_bap_unicast_client_release(struct bt_bap_stream *stream) |
|
{ |
|
struct bt_bap_unicast_client_ep *client_ep; |
|
struct bt_bap_ep *ep = stream->ep; |
|
struct net_buf_simple *buf; |
|
struct bt_ascs_disable_op *req; |
|
int err, len; |
|
|
|
LOG_DBG("stream %p", stream); |
|
|
|
if (stream->conn == NULL) { |
|
LOG_DBG("Stream %p does not have a connection", stream); |
|
|
|
return -ENOTCONN; |
|
} |
|
|
|
buf = bt_bap_unicast_client_ep_create_pdu(stream->conn, BT_ASCS_RELEASE_OP); |
|
if (buf == NULL) { |
|
LOG_DBG("Could not create PDU"); |
|
return -EBUSY; |
|
} |
|
|
|
req = net_buf_simple_add(buf, sizeof(*req)); |
|
req->num_ases = 0x01; |
|
len = buf->len; |
|
|
|
/* Only attempt to release if not IDLE already */ |
|
if (stream->ep->status.state == BT_BAP_EP_STATE_IDLE) { |
|
bt_bap_stream_reset(stream); |
|
} else { |
|
err = unicast_client_ep_release(ep, buf); |
|
if (err) { |
|
return err; |
|
} |
|
} |
|
|
|
/* Check if anything needs to be send */ |
|
if (len == buf->len) { |
|
return 0; |
|
} |
|
|
|
err = bt_bap_unicast_client_ep_send(stream->conn, ep, buf); |
|
if (err != 0) { |
|
return err; |
|
} |
|
|
|
client_ep = CONTAINER_OF(ep, struct bt_bap_unicast_client_ep, ep); |
|
client_ep->release_requested = true; |
|
|
|
return 0; |
|
} |
|
|
|
static uint8_t unicast_client_cp_discover_func(struct bt_conn *conn, |
|
const struct bt_gatt_attr *attr, |
|
struct bt_gatt_discover_params *discover) |
|
{ |
|
struct bt_gatt_chrc *chrc; |
|
uint16_t value_handle; |
|
|
|
if (!attr) { |
|
LOG_ERR("Unable to find ASE Control Point"); |
|
|
|
unicast_client_discover_complete(conn, BT_ATT_ERR_ATTRIBUTE_NOT_FOUND); |
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
chrc = attr->user_data; |
|
value_handle = chrc->value_handle; |
|
memset(discover, 0, sizeof(*discover)); |
|
|
|
LOG_DBG("conn %p attr %p handle 0x%04x", conn, attr, value_handle); |
|
|
|
unicast_client_ep_set_cp(conn, value_handle); |
|
|
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
static int unicast_client_ase_cp_discover(struct bt_conn *conn) |
|
{ |
|
struct unicast_client *client = &uni_cli_insts[bt_conn_index(conn)]; |
|
|
|
LOG_DBG("conn %p", conn); |
|
|
|
client->disc_params.uuid = cp_uuid; |
|
client->disc_params.func = unicast_client_cp_discover_func; |
|
client->disc_params.start_handle = BT_ATT_FIRST_ATTRIBUTE_HANDLE; |
|
client->disc_params.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE; |
|
client->disc_params.type = BT_GATT_DISCOVER_CHARACTERISTIC; |
|
|
|
return bt_gatt_discover(conn, &client->disc_params); |
|
} |
|
|
|
static uint8_t unicast_client_ase_read_func(struct bt_conn *conn, uint8_t err, |
|
struct bt_gatt_read_params *read, const void *data, |
|
uint16_t length) |
|
{ |
|
uint16_t handle = read->single.handle; |
|
struct unicast_client *client; |
|
struct net_buf_simple *buf; |
|
struct bt_bap_ep *ep; |
|
int cb_err; |
|
|
|
LOG_DBG("conn %p err 0x%02x len %u", conn, err, length); |
|
|
|
if (err) { |
|
cb_err = err; |
|
goto fail; |
|
} |
|
|
|
LOG_DBG("handle 0x%04x", handle); |
|
|
|
client = &uni_cli_insts[bt_conn_index(conn)]; |
|
buf = &client->net_buf; |
|
|
|
if (data != NULL) { |
|
if (net_buf_simple_tailroom(buf) < length) { |
|
LOG_DBG("Buffer full, invalid server response of size %u", |
|
length + client->net_buf.len); |
|
|
|
cb_err = BT_ATT_ERR_INVALID_ATTRIBUTE_LEN; |
|
|
|
goto fail; |
|
} |
|
|
|
/* store data*/ |
|
net_buf_simple_add_mem(buf, data, length); |
|
|
|
return BT_GATT_ITER_CONTINUE; |
|
} |
|
|
|
memset(read, 0, sizeof(*read)); |
|
|
|
if (buf->len < sizeof(struct bt_ascs_ase_status)) { |
|
LOG_DBG("Read response too small (%u)", buf->len); |
|
|
|
cb_err = BT_ATT_ERR_INVALID_ATTRIBUTE_LEN; |
|
|
|
goto fail; |
|
} |
|
|
|
ep = unicast_client_ep_get(conn, client->dir, handle); |
|
if (!ep) { |
|
/* The BAP spec declares that the unicast client shall subscribe to all ASEs. |
|
* In case that we cannot support this due to memory restrictions, we should |
|
* consider the discovery procedure as failing. |
|
*/ |
|
LOG_WRN("No space left to parse ASE"); |
|
cb_err = -ENOMEM; |
|
|
|
goto fail; |
|
} |
|
|
|
unicast_client_ep_set_status(ep, buf); |
|
cb_err = unicast_client_ep_subscribe(conn, ep); |
|
if (cb_err != 0) { |
|
LOG_DBG("Failed to subscribe to ep %p: %d", ep, cb_err); |
|
goto fail; |
|
} |
|
|
|
reset_att_buf(client); |
|
|
|
unicast_client_notify_endpoint(conn, ep); |
|
|
|
cb_err = unicast_client_ase_discover(conn, handle); |
|
if (cb_err != 0) { |
|
LOG_DBG("Failed to read ASE: %d", cb_err); |
|
goto fail; |
|
} |
|
|
|
return BT_GATT_ITER_STOP; |
|
|
|
fail: |
|
unicast_client_discover_complete(conn, cb_err); |
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
static bool any_ases_found(const struct unicast_client *client) |
|
{ |
|
/* We always allocate ases from 0 to X, so to verify if any sink or source ASEs have been |
|
* found we can just check the first index |
|
*/ |
|
#if CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT > 0 |
|
if (client->dir == BT_AUDIO_DIR_SINK && client->snks[0].handle == BAP_HANDLE_UNUSED) { |
|
LOG_DBG("No sink ASEs found"); |
|
return false; |
|
} |
|
#endif /* CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT > 0 */ |
|
#if CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT > 0 |
|
if (client->dir == BT_AUDIO_DIR_SOURCE && client->srcs[0].handle == BAP_HANDLE_UNUSED) { |
|
LOG_DBG("No source ASEs found"); |
|
return false; |
|
} |
|
#endif /* CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT > 0 */ |
|
|
|
return true; |
|
} |
|
|
|
static uint8_t unicast_client_ase_discover_cb(struct bt_conn *conn, |
|
const struct bt_gatt_attr *attr, |
|
struct bt_gatt_discover_params *discover) |
|
{ |
|
struct unicast_client *client; |
|
struct bt_gatt_chrc *chrc; |
|
uint16_t value_handle; |
|
int err; |
|
|
|
client = &uni_cli_insts[bt_conn_index(conn)]; |
|
|
|
if (attr == NULL) { |
|
if (!any_ases_found(client)) { |
|
unicast_client_discover_complete(conn, BT_ATT_ERR_ATTRIBUTE_NOT_FOUND); |
|
} else { |
|
err = unicast_client_ase_cp_discover(conn); |
|
if (err != 0) { |
|
LOG_ERR("Unable to discover ASE Control Point"); |
|
|
|
unicast_client_discover_complete(conn, err); |
|
} |
|
} |
|
|
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
chrc = attr->user_data; |
|
value_handle = chrc->value_handle; |
|
memset(discover, 0, sizeof(*discover)); |
|
|
|
LOG_DBG("conn %p attr %p handle 0x%04x dir %s", conn, attr, value_handle, |
|
bt_audio_dir_str(client->dir)); |
|
|
|
client->read_params.func = unicast_client_ase_read_func; |
|
client->read_params.handle_count = 1U; |
|
client->read_params.single.handle = value_handle; |
|
client->read_params.single.offset = 0U; |
|
|
|
err = bt_gatt_read(conn, &client->read_params); |
|
if (err != 0) { |
|
LOG_DBG("Failed to read PAC records: %d", err); |
|
|
|
unicast_client_discover_complete(conn, err); |
|
} |
|
|
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
static int unicast_client_ase_discover(struct bt_conn *conn, uint16_t start_handle) |
|
{ |
|
struct unicast_client *client = &uni_cli_insts[bt_conn_index(conn)]; |
|
|
|
LOG_DBG("conn %p ", conn); |
|
|
|
if (client->dir == BT_AUDIO_DIR_SINK) { |
|
client->disc_params.uuid = ase_snk_uuid; |
|
} else if (client->dir == BT_AUDIO_DIR_SOURCE) { |
|
client->disc_params.uuid = ase_src_uuid; |
|
} else { |
|
return -EINVAL; |
|
} |
|
|
|
client->disc_params.func = unicast_client_ase_discover_cb; |
|
client->disc_params.type = BT_GATT_DISCOVER_CHARACTERISTIC; |
|
client->disc_params.start_handle = start_handle; |
|
client->disc_params.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE; |
|
|
|
return bt_gatt_discover(conn, &client->disc_params); |
|
} |
|
|
|
static uint8_t unicast_client_pacs_avail_ctx_read_func(struct bt_conn *conn, uint8_t err, |
|
struct bt_gatt_read_params *read, |
|
const void *data, uint16_t length) |
|
{ |
|
struct bt_pacs_context context; |
|
struct net_buf_simple buf; |
|
int cb_err; |
|
|
|
memset(read, 0, sizeof(*read)); |
|
|
|
LOG_DBG("conn %p err 0x%02x len %u", conn, err, length); |
|
|
|
if (err || data == NULL || length != sizeof(context)) { |
|
LOG_DBG("Could not read available context: %d, %p, %u", err, data, length); |
|
|
|
if (err == BT_ATT_ERR_SUCCESS) { |
|
err = BT_ATT_ERR_INVALID_ATTRIBUTE_LEN; |
|
} |
|
|
|
unicast_client_discover_complete(conn, err); |
|
|
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
net_buf_simple_init_with_data(&buf, (void *)data, length); |
|
context.snk = net_buf_simple_pull_le16(&buf); |
|
context.src = net_buf_simple_pull_le16(&buf); |
|
|
|
LOG_DBG("sink context %u, source context %u", context.snk, context.src); |
|
|
|
unicast_client_notify_available_contexts(conn, context.snk, context.src); |
|
|
|
/* Read ASE instances */ |
|
cb_err = unicast_client_ase_discover(conn, BT_ATT_FIRST_ATTRIBUTE_HANDLE); |
|
if (cb_err != 0) { |
|
LOG_ERR("Unable to read ASE: %d", cb_err); |
|
|
|
unicast_client_discover_complete(conn, cb_err); |
|
} |
|
|
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
static uint8_t unicast_client_pacs_avail_ctx_notify_cb(struct bt_conn *conn, |
|
struct bt_gatt_subscribe_params *params, |
|
const void *data, uint16_t length) |
|
{ |
|
struct bt_pacs_context context; |
|
struct net_buf_simple buf; |
|
|
|
LOG_DBG("conn %p len %u", conn, length); |
|
|
|
if (!data) { |
|
LOG_DBG("Unsubscribed"); |
|
params->value_handle = BAP_HANDLE_UNUSED; |
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
net_buf_simple_init_with_data(&buf, (void *)data, length); |
|
|
|
if (buf.len != sizeof(context)) { |
|
LOG_ERR("Avail_ctx notification incorrect size: %u", length); |
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
net_buf_simple_init_with_data(&buf, (void *)data, length); |
|
context.snk = net_buf_simple_pull_le16(&buf); |
|
context.src = net_buf_simple_pull_le16(&buf); |
|
|
|
LOG_DBG("sink context %u, source context %u", context.snk, context.src); |
|
|
|
unicast_client_notify_available_contexts(conn, context.snk, context.src); |
|
|
|
return BT_GATT_ITER_CONTINUE; |
|
} |
|
|
|
static int unicast_client_pacs_avail_ctx_read(struct bt_conn *conn, uint16_t handle) |
|
{ |
|
struct unicast_client *client = &uni_cli_insts[bt_conn_index(conn)]; |
|
|
|
LOG_DBG("conn %p", conn); |
|
|
|
client->read_params.func = unicast_client_pacs_avail_ctx_read_func; |
|
client->read_params.handle_count = 1U; |
|
client->read_params.single.handle = handle; |
|
client->read_params.single.offset = 0U; |
|
|
|
return bt_gatt_read(conn, &client->read_params); |
|
} |
|
|
|
static uint8_t unicast_client_pacs_avail_ctx_discover_cb(struct bt_conn *conn, |
|
const struct bt_gatt_attr *attr, |
|
struct bt_gatt_discover_params *discover) |
|
{ |
|
uint8_t index = bt_conn_index(conn); |
|
const struct bt_gatt_chrc *chrc; |
|
uint8_t chrc_properties; |
|
uint16_t value_handle; |
|
int err; |
|
|
|
if (!attr) { |
|
/* If available_ctx is not found, we terminate the discovery as |
|
* the characteristic is mandatory |
|
*/ |
|
|
|
unicast_client_discover_complete(conn, BT_ATT_ERR_ATTRIBUTE_NOT_FOUND); |
|
|
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
chrc = attr->user_data; |
|
chrc_properties = chrc->properties; |
|
value_handle = chrc->value_handle; |
|
memset(discover, 0, sizeof(*discover)); |
|
|
|
LOG_DBG("conn %p attr %p handle 0x%04x", conn, attr, value_handle); |
|
|
|
if (chrc_properties & BT_GATT_CHRC_NOTIFY) { |
|
struct bt_gatt_subscribe_params *sub_params; |
|
|
|
sub_params = &uni_cli_insts[index].avail_ctx_subscribe; |
|
|
|
if (sub_params->value_handle == 0) { |
|
LOG_DBG("Subscribing to handle %u", value_handle); |
|
sub_params->value_handle = value_handle; |
|
sub_params->ccc_handle = BT_GATT_AUTO_DISCOVER_CCC_HANDLE; |
|
sub_params->end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE; |
|
sub_params->disc_params = &uni_cli_insts[index].avail_ctx_cc_disc; |
|
sub_params->notify = unicast_client_pacs_avail_ctx_notify_cb; |
|
sub_params->value = BT_GATT_CCC_NOTIFY; |
|
atomic_set_bit(sub_params->flags, BT_GATT_SUBSCRIBE_FLAG_VOLATILE); |
|
|
|
err = bt_gatt_subscribe(conn, sub_params); |
|
if (err != 0 && err != -EALREADY) { |
|
LOG_ERR("Failed to subscribe to avail_ctx: %d", err); |
|
} |
|
} /* else already subscribed */ |
|
} else { |
|
LOG_DBG("Invalid chrc->properties: %u", chrc_properties); |
|
/* If the characteristic is not subscribable we terminate the |
|
* discovery as BT_GATT_CHRC_NOTIFY is mandatory |
|
*/ |
|
unicast_client_discover_complete(conn, BT_ATT_ERR_NOT_SUPPORTED); |
|
|
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
err = unicast_client_pacs_avail_ctx_read(conn, value_handle); |
|
if (err != 0) { |
|
LOG_DBG("Failed to read PACS avail_ctx: %d", err); |
|
|
|
unicast_client_discover_complete(conn, err); |
|
} |
|
|
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
static int unicast_client_pacs_avail_ctx_discover(struct bt_conn *conn) |
|
{ |
|
struct unicast_client *client = &uni_cli_insts[bt_conn_index(conn)]; |
|
|
|
LOG_DBG("conn %p", conn); |
|
|
|
client->disc_params.uuid = pacs_avail_ctx_uuid; |
|
client->disc_params.func = unicast_client_pacs_avail_ctx_discover_cb; |
|
client->disc_params.start_handle = BT_ATT_FIRST_ATTRIBUTE_HANDLE; |
|
client->disc_params.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE; |
|
client->disc_params.type = BT_GATT_DISCOVER_CHARACTERISTIC; |
|
|
|
return bt_gatt_discover(conn, &client->disc_params); |
|
} |
|
|
|
static uint8_t unicast_client_pacs_location_read_func(struct bt_conn *conn, uint8_t err, |
|
struct bt_gatt_read_params *read, |
|
const void *data, uint16_t length) |
|
{ |
|
struct unicast_client *client; |
|
struct net_buf_simple buf; |
|
uint32_t location; |
|
int cb_err; |
|
|
|
memset(read, 0, sizeof(*read)); |
|
|
|
client = &uni_cli_insts[bt_conn_index(conn)]; |
|
|
|
LOG_DBG("conn %p err 0x%02x len %u", conn, err, length); |
|
|
|
if (err || data == NULL || length != sizeof(location)) { |
|
LOG_DBG("Unable to read PACS location for dir %s: %u, %p, %u", |
|
bt_audio_dir_str(client->dir), err, data, length); |
|
|
|
if (err == BT_ATT_ERR_SUCCESS) { |
|
err = BT_ATT_ERR_INVALID_ATTRIBUTE_LEN; |
|
} |
|
|
|
unicast_client_discover_complete(conn, err); |
|
|
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
net_buf_simple_init_with_data(&buf, (void *)data, length); |
|
location = net_buf_simple_pull_le32(&buf); |
|
|
|
LOG_DBG("dir %s loc %X", bt_audio_dir_str(client->dir), location); |
|
|
|
unicast_client_notify_location(conn, client->dir, (enum bt_audio_location)location); |
|
|
|
/* Read available contexts */ |
|
cb_err = unicast_client_pacs_avail_ctx_discover(conn); |
|
if (cb_err != 0) { |
|
LOG_ERR("Unable to read available contexts: %d", cb_err); |
|
|
|
unicast_client_discover_complete(conn, cb_err); |
|
} |
|
|
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
static uint8_t unicast_client_pacs_location_notify_cb(struct bt_conn *conn, |
|
struct bt_gatt_subscribe_params *params, |
|
const void *data, uint16_t length) |
|
{ |
|
struct net_buf_simple buf; |
|
enum bt_audio_dir dir; |
|
uint32_t location; |
|
|
|
LOG_DBG("conn %p len %u", conn, length); |
|
|
|
if (!data) { |
|
LOG_DBG("Unsubscribed"); |
|
params->value_handle = BAP_HANDLE_UNUSED; |
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
net_buf_simple_init_with_data(&buf, (void *)data, length); |
|
|
|
if (buf.len != sizeof(location)) { |
|
LOG_ERR("Location notification incorrect size: %u", length); |
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
if (params == &uni_cli_insts[bt_conn_index(conn)].snk_loc_subscribe) { |
|
dir = BT_AUDIO_DIR_SINK; |
|
} else if (params == &uni_cli_insts[bt_conn_index(conn)].src_loc_subscribe) { |
|
dir = BT_AUDIO_DIR_SOURCE; |
|
} else { |
|
LOG_ERR("Invalid notification"); |
|
|
|
return BT_GATT_ITER_CONTINUE; |
|
} |
|
|
|
net_buf_simple_init_with_data(&buf, (void *)data, length); |
|
location = net_buf_simple_pull_le32(&buf); |
|
|
|
LOG_DBG("dir %s loc %X", bt_audio_dir_str(dir), location); |
|
|
|
unicast_client_notify_location(conn, dir, (enum bt_audio_location)location); |
|
|
|
return BT_GATT_ITER_CONTINUE; |
|
} |
|
|
|
static int unicast_client_pacs_location_read(struct bt_conn *conn, uint16_t handle) |
|
{ |
|
struct unicast_client *client = &uni_cli_insts[bt_conn_index(conn)]; |
|
|
|
LOG_DBG("conn %p", conn); |
|
|
|
client->read_params.func = unicast_client_pacs_location_read_func; |
|
client->read_params.handle_count = 1U; |
|
client->read_params.single.handle = handle; |
|
client->read_params.single.offset = 0U; |
|
|
|
return bt_gatt_read(conn, &client->read_params); |
|
} |
|
|
|
static uint8_t unicast_client_pacs_location_discover_cb(struct bt_conn *conn, |
|
const struct bt_gatt_attr *attr, |
|
struct bt_gatt_discover_params *discover) |
|
{ |
|
uint8_t index = bt_conn_index(conn); |
|
const struct bt_gatt_chrc *chrc; |
|
uint16_t value_handle; |
|
int err; |
|
|
|
if (!attr) { |
|
/* If location is not found, we just continue reading the |
|
* available contexts, as location is optional. |
|
*/ |
|
err = unicast_client_pacs_avail_ctx_discover(conn); |
|
if (err != 0) { |
|
LOG_ERR("Unable to read available contexts: %d", err); |
|
|
|
unicast_client_discover_complete(conn, err); |
|
} |
|
|
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
chrc = attr->user_data; |
|
value_handle = chrc->value_handle; |
|
memset(discover, 0, sizeof(*discover)); |
|
|
|
LOG_DBG("conn %p attr %p handle 0x%04x", conn, attr, value_handle); |
|
|
|
if (chrc->properties & BT_GATT_CHRC_NOTIFY) { |
|
const struct unicast_client *client = &uni_cli_insts[index]; |
|
struct bt_gatt_subscribe_params *sub_params; |
|
|
|
if (client->dir == BT_AUDIO_DIR_SINK) { |
|
sub_params = &uni_cli_insts[index].snk_loc_subscribe; |
|
} else { |
|
sub_params = &uni_cli_insts[index].src_loc_subscribe; |
|
} |
|
|
|
sub_params->value_handle = value_handle; |
|
sub_params->ccc_handle = BT_GATT_AUTO_DISCOVER_CCC_HANDLE; |
|
sub_params->end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE; |
|
sub_params->disc_params = &uni_cli_insts[index].loc_cc_disc; |
|
sub_params->notify = unicast_client_pacs_location_notify_cb; |
|
sub_params->value = BT_GATT_CCC_NOTIFY; |
|
atomic_set_bit(sub_params->flags, BT_GATT_SUBSCRIBE_FLAG_VOLATILE); |
|
|
|
err = bt_gatt_subscribe(conn, sub_params); |
|
if (err != 0 && err != -EALREADY) { |
|
LOG_ERR("Failed to subscribe to location: %d", err); |
|
} |
|
} |
|
|
|
err = unicast_client_pacs_location_read(conn, value_handle); |
|
if (err != 0) { |
|
LOG_DBG("Failed to read PACS location: %d", err); |
|
|
|
unicast_client_discover_complete(conn, err); |
|
} |
|
|
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
static int unicast_client_pacs_location_discover(struct bt_conn *conn) |
|
{ |
|
struct unicast_client *client = &uni_cli_insts[bt_conn_index(conn)]; |
|
|
|
LOG_DBG("conn %p dir %s", conn, bt_audio_dir_str(client->dir)); |
|
|
|
if (client->dir == BT_AUDIO_DIR_SINK) { |
|
client->disc_params.uuid = pacs_snk_loc_uuid; |
|
} else if (client->dir == BT_AUDIO_DIR_SOURCE) { |
|
client->disc_params.uuid = pacs_src_loc_uuid; |
|
} else { |
|
return -EINVAL; |
|
} |
|
|
|
client->disc_params.func = unicast_client_pacs_location_discover_cb; |
|
client->disc_params.start_handle = BT_ATT_FIRST_ATTRIBUTE_HANDLE; |
|
client->disc_params.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE; |
|
client->disc_params.type = BT_GATT_DISCOVER_CHARACTERISTIC; |
|
|
|
return bt_gatt_discover(conn, &client->disc_params); |
|
} |
|
|
|
static uint8_t unicast_client_pacs_context_read_func(struct bt_conn *conn, uint8_t err, |
|
struct bt_gatt_read_params *read, |
|
const void *data, uint16_t length) |
|
{ |
|
struct net_buf_simple buf; |
|
struct bt_pacs_context *context; |
|
int cb_err; |
|
|
|
memset(read, 0, sizeof(*read)); |
|
|
|
LOG_DBG("conn %p err 0x%02x len %u", conn, err, length); |
|
|
|
if (err || length < sizeof(uint16_t) * 2) { |
|
goto discover_loc; |
|
} |
|
|
|
net_buf_simple_init_with_data(&buf, (void *)data, length); |
|
context = net_buf_simple_pull_mem(&buf, sizeof(*context)); |
|
|
|
discover_loc: |
|
/* Read ASE instances */ |
|
cb_err = unicast_client_pacs_location_discover(conn); |
|
if (cb_err != 0) { |
|
LOG_ERR("Unable to read PACS location: %d", cb_err); |
|
|
|
unicast_client_discover_complete(conn, cb_err); |
|
} |
|
|
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
static uint8_t unicast_client_pacs_context_discover_cb(struct bt_conn *conn, |
|
const struct bt_gatt_attr *attr, |
|
struct bt_gatt_discover_params *discover) |
|
{ |
|
struct unicast_client *client = &uni_cli_insts[bt_conn_index(conn)]; |
|
struct bt_gatt_chrc *chrc; |
|
uint16_t value_handle; |
|
int err; |
|
|
|
if (attr == NULL) { |
|
LOG_ERR("Unable to find %s PAC context", bt_audio_dir_str(client->dir)); |
|
|
|
unicast_client_discover_complete(conn, BT_ATT_ERR_ATTRIBUTE_NOT_FOUND); |
|
|
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
chrc = attr->user_data; |
|
value_handle = chrc->value_handle; |
|
memset(discover, 0, sizeof(*discover)); |
|
|
|
LOG_DBG("conn %p attr %p handle 0x%04x dir %s", conn, attr, value_handle, |
|
bt_audio_dir_str(client->dir)); |
|
|
|
/* TODO: Subscribe to PAC context */ |
|
|
|
client->read_params.func = unicast_client_pacs_context_read_func; |
|
client->read_params.handle_count = 1U; |
|
client->read_params.single.handle = value_handle; |
|
client->read_params.single.offset = 0U; |
|
|
|
err = bt_gatt_read(conn, &client->read_params); |
|
if (err != 0) { |
|
LOG_DBG("Failed to read PAC records: %d", err); |
|
|
|
unicast_client_discover_complete(conn, err); |
|
} |
|
|
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
static int unicast_client_pacs_context_discover(struct bt_conn *conn) |
|
{ |
|
struct unicast_client *client = &uni_cli_insts[bt_conn_index(conn)]; |
|
|
|
LOG_DBG("conn %p", conn); |
|
|
|
client->disc_params.uuid = pacs_context_uuid; |
|
client->disc_params.func = unicast_client_pacs_context_discover_cb; |
|
client->disc_params.type = BT_GATT_DISCOVER_CHARACTERISTIC; |
|
client->disc_params.start_handle = BT_ATT_FIRST_ATTRIBUTE_HANDLE; |
|
client->disc_params.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE; |
|
|
|
return bt_gatt_discover(conn, &client->disc_params); |
|
} |
|
|
|
static uint8_t unicast_client_read_func(struct bt_conn *conn, uint8_t err, |
|
struct bt_gatt_read_params *read, const void *data, |
|
uint16_t length) |
|
{ |
|
uint16_t handle = read->single.handle; |
|
struct unicast_client *client; |
|
struct bt_pacs_read_rsp *rsp; |
|
struct net_buf_simple *buf; |
|
int cb_err = err; |
|
uint8_t i; |
|
|
|
LOG_DBG("conn %p err 0x%02x len %u", conn, err, length); |
|
|
|
if (cb_err != BT_ATT_ERR_SUCCESS) { |
|
goto fail; |
|
} |
|
|
|
LOG_DBG("handle 0x%04x", handle); |
|
|
|
client = &uni_cli_insts[bt_conn_index(conn)]; |
|
buf = &client->net_buf; |
|
|
|
if (data != NULL) { |
|
if (net_buf_simple_tailroom(buf) < length) { |
|
LOG_DBG("Buffer full, invalid server response of size %u", |
|
length + client->net_buf.len); |
|
|
|
cb_err = BT_ATT_ERR_INVALID_ATTRIBUTE_LEN; |
|
|
|
goto fail; |
|
} |
|
|
|
/* store data*/ |
|
net_buf_simple_add_mem(buf, data, length); |
|
|
|
return BT_GATT_ITER_CONTINUE; |
|
} |
|
|
|
memset(read, 0, sizeof(*read)); |
|
|
|
if (buf->len < sizeof(*rsp)) { |
|
LOG_DBG("Read response too small (%u)", buf->len); |
|
|
|
cb_err = BT_ATT_ERR_INVALID_ATTRIBUTE_LEN; |
|
|
|
goto fail; |
|
} |
|
|
|
rsp = net_buf_simple_pull_mem(buf, sizeof(*rsp)); |
|
|
|
/* If no PAC was found don't bother discovering ASE and ASE CP */ |
|
if (!rsp->num_pac) { |
|
goto fail; |
|
} |
|
|
|
for (i = 0U; i < rsp->num_pac; i++) { |
|
struct bt_audio_codec_cap codec_cap; |
|
struct bt_pac_codec *pac_codec; |
|
struct bt_pac_ltv_data *meta, *cc; |
|
void *cc_ltv, *meta_ltv; |
|
|
|
LOG_DBG("pac #%u/%u", i + 1, rsp->num_pac); |
|
|
|
if (buf->len < sizeof(*pac_codec)) { |
|
LOG_ERR("Malformed PAC: remaining len %u expected %zu", |
|
buf->len, sizeof(*pac_codec)); |
|
break; |
|
} |
|
|
|
pac_codec = net_buf_simple_pull_mem(buf, sizeof(*pac_codec)); |
|
|
|
if (buf->len < sizeof(*cc)) { |
|
LOG_ERR("Malformed PAC: remaining len %u expected %zu", |
|
buf->len, sizeof(*cc)); |
|
break; |
|
} |
|
|
|
cc = net_buf_simple_pull_mem(buf, sizeof(*cc)); |
|
if (buf->len < cc->len) { |
|
LOG_ERR("Malformed PAC: remaining len %u expected %zu", |
|
buf->len, cc->len); |
|
break; |
|
} |
|
|
|
cc_ltv = net_buf_simple_pull_mem(buf, cc->len); |
|
|
|
if (buf->len < sizeof(*meta)) { |
|
LOG_ERR("Malformed PAC: remaining len %u expected %zu", |
|
buf->len, sizeof(*meta)); |
|
break; |
|
} |
|
|
|
meta = net_buf_simple_pull_mem(buf, sizeof(*meta)); |
|
if (buf->len < meta->len) { |
|
LOG_ERR("Malformed PAC: remaining len %u expected %u", |
|
buf->len, meta->len); |
|
break; |
|
} |
|
|
|
meta_ltv = net_buf_simple_pull_mem(buf, meta->len); |
|
|
|
if (unicast_client_set_codec_cap(pac_codec->id, sys_le16_to_cpu(pac_codec->cid), |
|
sys_le16_to_cpu(pac_codec->vid), cc_ltv, cc->len, |
|
meta_ltv, meta->len, &codec_cap)) { |
|
LOG_ERR("Unable to parse Codec"); |
|
break; |
|
} |
|
|
|
LOG_DBG("codec 0x%02x capabilities len %u meta len %u ", codec_cap.id, |
|
codec_cap.data_len, codec_cap.meta_len); |
|
|
|
unicast_client_notify_pac_record(conn, &codec_cap); |
|
} |
|
|
|
reset_att_buf(client); |
|
|
|
if (i != rsp->num_pac) { |
|
LOG_DBG("Failed to process all PAC records (%u/%u)", i, rsp->num_pac); |
|
cb_err = BT_ATT_ERR_INVALID_PDU; |
|
goto fail; |
|
} |
|
|
|
/* Read PACS contexts */ |
|
cb_err = unicast_client_pacs_context_discover(conn); |
|
if (cb_err != 0) { |
|
LOG_ERR("Unable to read PACS context: %d", cb_err); |
|
goto fail; |
|
} |
|
|
|
return BT_GATT_ITER_STOP; |
|
|
|
fail: |
|
unicast_client_discover_complete(conn, cb_err); |
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
static uint8_t unicast_client_pac_discover_cb(struct bt_conn *conn, |
|
const struct bt_gatt_attr *attr, |
|
struct bt_gatt_discover_params *discover) |
|
{ |
|
struct unicast_client *client = &uni_cli_insts[bt_conn_index(conn)]; |
|
struct bt_gatt_chrc *chrc; |
|
uint16_t value_handle; |
|
int err; |
|
|
|
if (attr == NULL) { |
|
LOG_DBG("Unable to find %s PAC", bt_audio_dir_str(client->dir)); |
|
|
|
unicast_client_discover_complete(conn, BT_ATT_ERR_ATTRIBUTE_NOT_FOUND); |
|
|
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
chrc = attr->user_data; |
|
value_handle = chrc->value_handle; |
|
memset(discover, 0, sizeof(*discover)); |
|
|
|
LOG_DBG("conn %p attr %p handle 0x%04x dir %s", conn, attr, value_handle, |
|
bt_audio_dir_str(client->dir)); |
|
|
|
/* TODO: Subscribe to PAC */ |
|
|
|
/* Reset to use for long read */ |
|
reset_att_buf(client); |
|
|
|
client->read_params.func = unicast_client_read_func; |
|
client->read_params.handle_count = 1U; |
|
client->read_params.single.handle = value_handle; |
|
client->read_params.single.offset = 0U; |
|
|
|
err = bt_gatt_read(conn, &client->read_params); |
|
if (err != 0) { |
|
LOG_DBG("Failed to read PAC records: %d", err); |
|
|
|
unicast_client_discover_complete(conn, err); |
|
} |
|
|
|
return BT_GATT_ITER_STOP; |
|
} |
|
|
|
static void unicast_client_disconnected(struct bt_conn *conn, uint8_t reason) |
|
{ |
|
LOG_DBG("conn %p reason 0x%02x", conn, reason); |
|
|
|
unicast_client_ep_reset(conn, reason); |
|
} |
|
|
|
static struct bt_conn_cb conn_cbs = { |
|
.disconnected = unicast_client_disconnected, |
|
}; |
|
|
|
int bt_bap_unicast_client_discover(struct bt_conn *conn, enum bt_audio_dir dir) |
|
{ |
|
struct unicast_client *client; |
|
static bool conn_cb_registered; |
|
uint8_t role; |
|
int err; |
|
|
|
if (!conn || conn->state != BT_CONN_CONNECTED) { |
|
return -ENOTCONN; |
|
} |
|
|
|
role = conn->role; |
|
if (role != BT_HCI_ROLE_CENTRAL) { |
|
LOG_DBG("Invalid conn role: %u, shall be central", role); |
|
return -EINVAL; |
|
} |
|
|
|
client = &uni_cli_insts[bt_conn_index(conn)]; |
|
if (atomic_test_and_set_bit(client->flags, UNICAST_CLIENT_FLAG_BUSY)) { |
|
LOG_DBG("Client connection is busy"); |
|
return -EBUSY; |
|
} |
|
|
|
if (dir == BT_AUDIO_DIR_SINK) { |
|
client->disc_params.uuid = snk_uuid; |
|
} else if (dir == BT_AUDIO_DIR_SOURCE) { |
|
client->disc_params.uuid = src_uuid; |
|
} else { |
|
return -EINVAL; |
|
} |
|
|
|
client->disc_params.func = unicast_client_pac_discover_cb; |
|
client->disc_params.type = BT_GATT_DISCOVER_CHARACTERISTIC; |
|
client->disc_params.start_handle = BT_ATT_FIRST_ATTRIBUTE_HANDLE; |
|
client->disc_params.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE; |
|
|
|
if (!conn_cb_registered) { |
|
bt_conn_cb_register(&conn_cbs); |
|
conn_cb_registered = true; |
|
} |
|
|
|
err = bt_gatt_discover(conn, &client->disc_params); |
|
if (err != 0) { |
|
atomic_clear_bit(client->flags, UNICAST_CLIENT_FLAG_BUSY); |
|
return err; |
|
} |
|
|
|
client->dir = dir; |
|
|
|
return 0; |
|
} |
|
|
|
int bt_bap_unicast_client_register_cb(struct bt_bap_unicast_client_cb *cb) |
|
{ |
|
CHECKIF(cb == NULL) { |
|
LOG_DBG("cb is NULL"); |
|
return -EINVAL; |
|
} |
|
|
|
if (sys_slist_find(&unicast_client_cbs, &cb->_node, NULL)) { |
|
return -EEXIST; |
|
} |
|
|
|
sys_slist_append(&unicast_client_cbs, &cb->_node); |
|
|
|
return 0; |
|
}
|
|
|