zephyr/subsys/bluetooth/audio/bap_unicast_client.c
Emil Gydesen 27709609f3 Bluetooth: BAP: Unicast Client: Replace busy bool with atomic
Replace the busy boolean flag with an atomic value.
This also modifies the order of how this value is
controlled to avoid any race conditions.

Signed-off-by: Emil Gydesen <emil.gydesen@nordicsemi.no>
2024-09-20 09:04:23 +02:00

4360 lines
114 KiB
C

/* @file
* @brief Bluetooth Unicast Client
*/
/*
* Copyright (c) 2020 Intel Corporation
* Copyright (c) 2022-2023 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)
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 const struct bt_bap_unicast_client_cb *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_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;
if (unicast_client_cbs != NULL && unicast_client_cbs->release != NULL) {
unicast_client_cbs->release(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_audio_codec_qos_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);
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);
}
} else {
/* We setup the data path here, as this is the earliest where
* we have the ISO <-> EP coupling completed (due to setting
* the CIS ID in the QoS procedure).
*/
bt_bap_iso_configure_data_path(ep, stream->codec_cfg);
}
/* 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);
/* 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 (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 = 0x0000;
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);
if (unicast_client_cbs == NULL) {
continue;
}
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:
if (unicast_client_cbs->config != NULL) {
unicast_client_cbs->config(stream, ase_rsp->code, ase_rsp->reason);
}
break;
case BT_ASCS_QOS_OP:
if (unicast_client_cbs->qos != NULL) {
unicast_client_cbs->qos(stream, ase_rsp->code, ase_rsp->reason);
}
break;
case BT_ASCS_ENABLE_OP:
if (unicast_client_cbs->enable != NULL) {
unicast_client_cbs->enable(stream, ase_rsp->code, ase_rsp->reason);
}
break;
case BT_ASCS_START_OP:
if (unicast_client_cbs->start != NULL) {
unicast_client_cbs->start(stream, ase_rsp->code, ase_rsp->reason);
}
break;
case BT_ASCS_DISABLE_OP:
if (unicast_client_cbs->disable != NULL) {
unicast_client_cbs->disable(stream, ase_rsp->code, ase_rsp->reason);
}
break;
case BT_ASCS_STOP_OP:
if (unicast_client_cbs->stop != NULL) {
unicast_client_cbs->stop(stream, ase_rsp->code, ase_rsp->reason);
}
break;
case BT_ASCS_METADATA_OP:
if (unicast_client_cbs->metadata != NULL) {
unicast_client_cbs->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;
}
if (unicast_client_cbs->release != NULL) {
unicast_client_cbs->release(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;
const uint8_t att_ntf_header_size = 3; /* opcode (1) + handle (2) */
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 = 0x0000;
return BT_GATT_ITER_STOP;
}
max_ntf_size = bt_gatt_get_mtu(conn) - att_ntf_header_size;
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 = 0x0000;
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 pac_record_cb(struct bt_conn *conn, const struct bt_audio_codec_cap *codec_cap)
{
if (unicast_client_cbs != NULL && unicast_client_cbs->pac_record != NULL) {
struct unicast_client *client = &uni_cli_insts[bt_conn_index(conn)];
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.
*/
unicast_client_cbs->pac_record(conn, dir, codec_cap);
}
}
static void endpoint_cb(struct bt_conn *conn, struct bt_bap_ep *ep)
{
if (unicast_client_cbs != NULL && unicast_client_cbs->endpoint != NULL) {
struct unicast_client *client = &uni_cli_insts[bt_conn_index(conn)];
const enum bt_audio_dir dir = client->dir;
unicast_client_cbs->endpoint(conn, dir, ep);
}
}
static void discover_cb(struct bt_conn *conn, int err)
{
struct unicast_client *client = &uni_cli_insts[bt_conn_index(conn)];
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);
if (unicast_client_cbs != NULL && unicast_client_cbs->discover != NULL) {
unicast_client_cbs->discover(conn, err, dir);
}
}
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);
discover_cb(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 = 0x0000;
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);
discover_cb(conn, err);
return;
}
} else { /* already subscribed */
discover_cb(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_audio_codec_qos *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 uint8_t att_write_header_size = 3; /* opcode (1) + handle (2) */
const uint16_t max_write_size = bt_gatt_get_mtu(conn) - att_write_header_size;
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 = 0U;
client_ep->handle = 0U;
(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_audio_codec_qos_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_audio_codec_qos_to_cig_param(&param, group);
err = bt_iso_cig_create(&param, &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_audio_codec_qos_to_cig_param(&param, group);
err = bt_iso_cig_reconfigure(group->cig, &param);
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_codec_qos_to_iso_qos(struct bt_bap_iso *iso,
const struct bt_audio_codec_qos *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_audio_codec_qos_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_audio_codec_qos *qos,
enum bt_audio_dir dir)
{
/* Store the stream Codec QoS in the bap_iso */
unicast_client_codec_qos_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_audio_codec_qos *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_audio_codec_qos *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_AUDIO_CODEC_QOS_FRAMING_UNFRAMED) {
cig_param->framing = BT_ISO_FRAMING_UNFRAMED;
} else if (qos->framing == BT_AUDIO_CODEC_QOS_FRAMING_FRAMED) {
cig_param->framing = BT_ISO_FRAMING_FRAMED;
}
} else if ((qos->framing == BT_AUDIO_CODEC_QOS_FRAMING_UNFRAMED &&
cig_param->framing != BT_ISO_FRAMING_UNFRAMED) ||
(qos->framing == BT_AUDIO_CODEC_QOS_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, &param->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 = &param->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 = &param->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, &params[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 = &params[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, &params[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(&param, 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;
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;
}
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++;
return bt_bap_unicast_client_ep_send(stream->conn, ep, buf);
}
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");
discover_cb(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);
endpoint_cb(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:
discover_cb(conn, cb_err);
return BT_GATT_ITER_STOP;
}
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;
if (attr == NULL) {
err = unicast_client_ase_cp_discover(conn);
if (err != 0) {
LOG_ERR("Unable to discover ASE Control Point");
discover_cb(conn, err);
}
return BT_GATT_ITER_STOP;
}
chrc = attr->user_data;
value_handle = chrc->value_handle;
memset(discover, 0, sizeof(*discover));
client = &uni_cli_insts[bt_conn_index(conn)];
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);
discover_cb(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;
}
discover_cb(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);
if (unicast_client_cbs != NULL && unicast_client_cbs->available_contexts != NULL) {
unicast_client_cbs->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);
discover_cb(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 = 0x0000;
return BT_GATT_ITER_STOP;
}
/* Terminate early if there's no callbacks */
if (unicast_client_cbs == NULL || unicast_client_cbs->available_contexts == NULL) {
return BT_GATT_ITER_CONTINUE;
}
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);
if (unicast_client_cbs != NULL && unicast_client_cbs->available_contexts != NULL) {
unicast_client_cbs->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
*/
discover_cb(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 = 0x0000; /* auto discover ccc */
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
*/
discover_cb(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);
discover_cb(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;
}
discover_cb(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);
if (unicast_client_cbs != NULL && unicast_client_cbs->location != NULL) {
unicast_client_cbs->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);
discover_cb(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 = 0x0000;
return BT_GATT_ITER_STOP;
}
/* Terminate early if there's no callbacks */
if (unicast_client_cbs == NULL || unicast_client_cbs->location == NULL) {
return BT_GATT_ITER_CONTINUE;
}
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);
if (unicast_client_cbs != NULL && unicast_client_cbs->location != NULL) {
unicast_client_cbs->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);
discover_cb(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 = 0x0000; /* auto discover ccc */
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);
discover_cb(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);
discover_cb(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));
discover_cb(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);
discover_cb(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);
pac_record_cb(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:
discover_cb(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));
discover_cb(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);
discover_cb(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(const struct bt_bap_unicast_client_cb *cbs)
{
CHECKIF(cbs == NULL) {
LOG_DBG("cbs is NULL");
return -EINVAL;
}
if (unicast_client_cbs != NULL) {
LOG_DBG("Callbacks already registered");
return -EALREADY;
}
unicast_client_cbs = cbs;
return 0;
}