| /* @file |
| * @brief Bluetooth Unicast Client |
| */ |
| |
| /* |
| * Copyright (c) 2020 Intel Corporation |
| * Copyright (c) 2022 Nordic Semiconductor ASA |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #include <zephyr/zephyr.h> |
| #include <zephyr/sys/byteorder.h> |
| |
| #include <zephyr/bluetooth/bluetooth.h> |
| #include <zephyr/bluetooth/conn.h> |
| #include <zephyr/bluetooth/gatt.h> |
| #include <zephyr/bluetooth/audio/audio.h> |
| |
| #include "../host/hci_core.h" |
| #include "../host/conn_internal.h" |
| |
| #include "endpoint.h" |
| #include "pacs_internal.h" |
| #include "unicast_client_internal.h" |
| |
| #define BT_DBG_ENABLED IS_ENABLED(CONFIG_BT_AUDIO_DEBUG_UNICAST_CLIENT) |
| #define LOG_MODULE_NAME bt_unicast_client |
| #include "common/log.h" |
| |
| #if defined(CONFIG_BT_AUDIO_UNICAST_CLIENT) |
| |
| #define PAC_DIR_UNUSED(dir) ((dir) != BT_AUDIO_DIR_SINK && (dir) != BT_AUDIO_DIR_SOURCE) |
| |
| static struct unicast_client_pac { |
| enum bt_audio_dir dir; |
| uint16_t context; |
| struct bt_codec codec; |
| } cache[CONFIG_BT_MAX_CONN][CONFIG_BT_AUDIO_UNICAST_CLIENT_PAC_COUNT]; |
| |
| 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 *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_audio_ep snks[CONFIG_BT_MAX_CONN][CONFIG_BT_AUDIO_UNICAST_CLIENT_ASE_SNK_COUNT]; |
| static struct bt_audio_ep srcs[CONFIG_BT_MAX_CONN][CONFIG_BT_AUDIO_UNICAST_CLIENT_ASE_SRC_COUNT]; |
| static struct bt_audio_iso isos[CONFIG_BT_MAX_CONN][CONFIG_BT_AUDIO_UNICAST_CLIENT_ASE_SNK_COUNT + |
| CONFIG_BT_AUDIO_UNICAST_CLIENT_ASE_SRC_COUNT]; |
| |
| static struct bt_gatt_subscribe_params cp_subscribe[CONFIG_BT_MAX_CONN]; |
| static struct bt_gatt_discover_params cp_disc[CONFIG_BT_MAX_CONN]; |
| |
| |
| /* TODO: Move the functions to avoid these prototypes */ |
| static int unicast_client_ep_set_metadata(struct bt_audio_ep *ep, |
| struct net_buf_simple *buf, |
| uint8_t len, struct bt_codec *codec); |
| |
| static int unicast_client_ep_set_codec(struct bt_audio_ep *ep, uint8_t id, |
| uint16_t cid, uint16_t vid, |
| struct net_buf_simple *buf, |
| uint8_t len, struct bt_codec *codec); |
| |
| 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_audio_iso *audio_iso = CONTAINER_OF(chan, struct bt_audio_iso, |
| iso_chan); |
| struct bt_audio_ep *ep = audio_iso->sink_ep; |
| const struct bt_audio_stream_ops *ops; |
| |
| if (ep == NULL) { |
| BT_ERR("Could not lookup ep by iso %p", chan); |
| return; |
| } |
| |
| ops = ep->stream->ops; |
| |
| BT_DBG("stream %p ep %p len %zu", chan, ep, net_buf_frags_len(buf)); |
| |
| if (ops != NULL && ops->recv != NULL) { |
| ops->recv(ep->stream, info, buf); |
| } else { |
| BT_WARN("No callback for recv set"); |
| } |
| } |
| |
| static void unicast_client_ep_iso_sent(struct bt_iso_chan *chan) |
| { |
| struct bt_audio_iso *audio_iso = CONTAINER_OF(chan, struct bt_audio_iso, |
| iso_chan); |
| struct bt_audio_ep *ep = audio_iso->source_ep; |
| struct bt_audio_stream_ops *ops = ep->stream->ops; |
| |
| BT_DBG("stream %p ep %p", chan, ep); |
| |
| if (ops != NULL && ops->sent != NULL) { |
| ops->sent(ep->stream); |
| } |
| } |
| |
| static void unicast_client_ep_iso_connected(struct bt_iso_chan *chan) |
| { |
| struct bt_audio_iso *audio_iso = CONTAINER_OF(chan, struct bt_audio_iso, |
| iso_chan); |
| struct bt_audio_ep *source_ep = audio_iso->source_ep; |
| struct bt_audio_ep *sink_ep = audio_iso->sink_ep; |
| struct bt_audio_ep *ep; |
| |
| if (&sink_ep->iso->iso_chan == chan) { |
| ep = sink_ep; |
| } else { |
| ep = source_ep; |
| } |
| |
| if (ep == NULL) { |
| BT_ERR("Could not lookup ep by iso %p", chan); |
| return; |
| } |
| |
| BT_DBG("stream %p ep %p dir %u", chan, ep, ep != NULL ? ep->dir : 0); |
| |
| ep->seq_num = 0U; |
| |
| if (ep->status.state != BT_AUDIO_EP_STATE_ENABLING) { |
| BT_DBG("endpoint not in enabling state: %s", |
| bt_audio_ep_state_str(ep->status.state)); |
| return; |
| } |
| } |
| |
| static void unicast_client_ep_iso_disconnected(struct bt_iso_chan *chan, |
| uint8_t reason) |
| { |
| struct bt_audio_iso *audio_iso = CONTAINER_OF(chan, struct bt_audio_iso, |
| iso_chan); |
| struct bt_audio_ep *source_ep = audio_iso->source_ep; |
| struct bt_audio_ep *sink_ep = audio_iso->sink_ep; |
| const struct bt_audio_stream_ops *ops; |
| struct bt_audio_stream *stream; |
| struct bt_audio_ep *ep; |
| |
| if (&sink_ep->iso->iso_chan == chan) { |
| ep = sink_ep; |
| } else { |
| ep = source_ep; |
| } |
| |
| if (ep == NULL) { |
| BT_ERR("Could not lookup ep by iso %p", chan); |
| return; |
| } |
| |
| ops = ep->stream->ops; |
| stream = ep->stream; |
| |
| BT_DBG("stream %p ep %p reason 0x%02x", chan, ep, reason); |
| |
| if (ops != NULL && ops->stopped != NULL) { |
| ops->stopped(stream); |
| } else { |
| BT_WARN("No callback for stopped set"); |
| } |
| |
| bt_unicast_client_ep_set_state(ep, BT_AUDIO_EP_STATE_QOS_CONFIGURED); |
| } |
| |
| static struct bt_iso_chan_ops unicast_client_iso_ops = { |
| .recv = unicast_client_ep_iso_recv, |
| .sent = unicast_client_ep_iso_sent, |
| .connected = unicast_client_ep_iso_connected, |
| .disconnected = unicast_client_ep_iso_disconnected, |
| }; |
| |
| void bt_unicast_client_ep_unbind_audio_iso(struct bt_audio_ep *ep) |
| { |
| struct bt_audio_iso *audio_iso = ep->iso; |
| struct bt_iso_chan_qos *qos; |
| const uint8_t dir = ep->dir; |
| |
| BT_DBG("ep %p dir %u audio_iso %p", ep, ep->dir, audio_iso); |
| |
| if (audio_iso == NULL) { |
| return; |
| } |
| |
| ep->iso = NULL; |
| qos = audio_iso->iso_chan.qos; |
| |
| if (dir == BT_AUDIO_DIR_SOURCE) { |
| /* If the endpoint is a source, then we need to |
| * reset our RX parameters |
| */ |
| audio_iso->sink_ep = NULL; |
| qos->rx = NULL; |
| } else if (dir == BT_AUDIO_DIR_SINK) { |
| /* If the endpoint is a sink, then we need to |
| * reset our TX parameters |
| */ |
| audio_iso->source_ep = NULL; |
| qos->tx = NULL; |
| } else { |
| __ASSERT(false, "Invalid dir: %u", dir); |
| } |
| } |
| |
| void bt_unicast_client_ep_bind_audio_iso(struct bt_audio_ep *ep, |
| struct bt_audio_iso *audio_iso) |
| { |
| struct bt_iso_chan *iso_chan; |
| struct bt_iso_chan_qos *qos; |
| const uint8_t dir = ep->dir; |
| |
| ep->iso = audio_iso; |
| |
| iso_chan = &ep->iso->iso_chan; |
| |
| iso_chan->ops = &unicast_client_iso_ops; |
| qos = iso_chan->qos = &ep->iso->iso_qos; |
| |
| BT_DBG("ep %p, audio_iso %p, iso_chan %p, dir %u", |
| ep, audio_iso, iso_chan, ep->dir); |
| |
| if (dir == BT_AUDIO_DIR_SOURCE) { |
| /* If the endpoint is a source, then we need to |
| * configure our RX parameters |
| */ |
| audio_iso->sink_ep = ep; |
| qos->rx = &ep->iso_io_qos; |
| } else if (dir == BT_AUDIO_DIR_SINK) { |
| /* If the endpoint is a sink, then we need to |
| * configure our TX parameters |
| */ |
| audio_iso->source_ep = ep; |
| qos->tx = &ep->iso_io_qos; |
| } else { |
| __ASSERT(false, "Invalid dir: %u", dir); |
| } |
| |
| ep->stream->iso = iso_chan; |
| } |
| |
| static void unicast_client_ep_init(struct bt_audio_ep *ep, uint16_t handle, |
| uint8_t dir) |
| { |
| |
| BT_DBG("ep %p dir 0x%02x handle 0x%04x", ep, dir, handle); |
| |
| (void)memset(ep, 0, sizeof(*ep)); |
| ep->handle = handle; |
| ep->status.id = 0U; |
| ep->dir = dir; |
| } |
| |
| static struct bt_audio_ep *unicast_client_ep_find(struct bt_conn *conn, |
| uint16_t handle) |
| { |
| size_t i; |
| uint8_t index; |
| |
| index = bt_conn_index(conn); |
| |
| for (i = 0; i < ARRAY_SIZE(snks[index]); i++) { |
| struct bt_audio_ep *ep = &snks[index][i]; |
| |
| if ((handle && ep->handle == handle) || |
| (!handle && ep->handle)) { |
| return ep; |
| } |
| } |
| |
| for (i = 0; i < ARRAY_SIZE(srcs[index]); i++) { |
| struct bt_audio_ep *ep = &srcs[index][i]; |
| |
| if ((handle && ep->handle == handle) || |
| (!handle && ep->handle)) { |
| return ep; |
| } |
| } |
| |
| return NULL; |
| } |
| |
| struct bt_audio_iso *bt_unicast_client_new_audio_iso(const struct bt_audio_stream *stream) |
| { |
| const struct bt_audio_unicast_group *unicast_group = stream->unicast_group; |
| struct bt_conn *acl_conn = stream->conn; |
| const uint8_t index = bt_conn_index(acl_conn); |
| struct bt_audio_iso *cache = isos[index]; |
| struct bt_audio_iso *free_audio_iso; |
| |
| free_audio_iso = NULL; |
| for (size_t i = 0; i < ARRAY_SIZE(isos[index]); i++) { |
| struct bt_audio_iso *audio_iso = &cache[i]; |
| struct bt_audio_ep *ep; |
| |
| switch (stream->ep->dir) { |
| case BT_AUDIO_DIR_SINK: |
| ep = audio_iso->source_ep; |
| |
| if (ep == NULL) { |
| if (free_audio_iso == NULL) { |
| free_audio_iso = audio_iso; |
| } |
| } else if (ep->unicast_group == unicast_group && |
| ep->stream->conn == acl_conn) { |
| return audio_iso; |
| } |
| break; |
| case BT_AUDIO_DIR_SOURCE: |
| ep = audio_iso->sink_ep; |
| |
| if (ep == NULL) { |
| if (free_audio_iso == NULL) { |
| free_audio_iso = audio_iso; |
| } |
| } else if (ep->unicast_group == unicast_group && |
| ep->stream->conn == acl_conn) { |
| return audio_iso; |
| } |
| break; |
| default: |
| return NULL; |
| } |
| } |
| |
| return free_audio_iso; |
| } |
| |
| static struct bt_audio_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_audio_ep *cache; |
| |
| index = bt_conn_index(conn); |
| |
| switch (dir) { |
| case BT_AUDIO_DIR_SINK: |
| cache = snks[index]; |
| size = ARRAY_SIZE(snks[index]); |
| break; |
| case BT_AUDIO_DIR_SOURCE: |
| cache = srcs[index]; |
| size = ARRAY_SIZE(srcs[index]); |
| break; |
| default: |
| return NULL; |
| } |
| |
| for (i = 0; i < size; i++) { |
| struct bt_audio_ep *ep = &cache[i]; |
| |
| if (!ep->handle) { |
| unicast_client_ep_init(ep, handle, dir); |
| return ep; |
| } |
| } |
| |
| return NULL; |
| } |
| |
| static struct bt_audio_ep *unicast_client_ep_get(struct bt_conn *conn, |
| enum bt_audio_dir dir, |
| uint16_t handle) |
| { |
| struct bt_audio_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_idle_state(struct bt_audio_ep *ep, |
| struct net_buf_simple *buf) |
| { |
| struct bt_audio_stream *stream = ep->stream; |
| const struct bt_audio_stream_ops *ops; |
| |
| if (stream == NULL) { |
| return; |
| } |
| |
| /* Notify upper layer */ |
| ops = stream->ops; |
| if (ops != NULL && ops->released != NULL) { |
| ops->released(stream); |
| } else { |
| BT_WARN("No callback for released set"); |
| } |
| |
| bt_audio_stream_reset(stream); |
| } |
| |
| static void unicast_client_ep_qos_reset(struct bt_audio_ep *ep) |
| { |
| BT_DBG("ep %p dir %u", ep, ep->dir); |
| |
| if (ep->dir == BT_AUDIO_DIR_SOURCE) { |
| /* If the endpoint is a source, then we need to |
| * configure our RX parameters |
| */ |
| ep->iso->iso_qos.rx = memset(&ep->iso_io_qos, 0, |
| sizeof(ep->iso_io_qos)); |
| } else if (ep->dir == BT_AUDIO_DIR_SINK) { |
| /* If the endpoint is a sink, then we need to |
| * configure our TX parameters |
| */ |
| ep->iso->iso_qos.tx = memset(&ep->iso_io_qos, 0, |
| sizeof(ep->iso_io_qos)); |
| } else { |
| __ASSERT(false, "Invalid ep->dir: %u", ep->dir); |
| } |
| } |
| |
| static void unicast_client_ep_config_state(struct bt_audio_ep *ep, |
| struct net_buf_simple *buf) |
| { |
| struct bt_ascs_ase_status_config *cfg; |
| struct bt_codec_qos_pref *pref; |
| struct bt_audio_stream *stream; |
| |
| if (buf->len < sizeof(*cfg)) { |
| BT_ERR("Config status too short"); |
| return; |
| } |
| |
| stream = ep->stream; |
| if (stream == NULL) { |
| BT_ERR("No stream active for endpoint"); |
| return; |
| } |
| |
| cfg = net_buf_simple_pull_mem(buf, sizeof(*cfg)); |
| |
| if (stream->codec == NULL || stream->codec->id != cfg->codec.id) { |
| BT_ERR("Codec configuration mismatched"); |
| /* TODO: Release the stream? */ |
| return; |
| } |
| |
| if (buf->len < cfg->cc_len) { |
| BT_ERR("Malformed ASE Config status: buf->len %u < %u cc_len", |
| buf->len, cfg->cc_len); |
| return; |
| } |
| |
| 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); |
| |
| BT_DBG("dir 0x%02x unframed_supported 0x%02x phy 0x%02x rtn %u " |
| "latency %u pd_min %u pd_max %u codec 0x%02x ", |
| ep->dir, pref->unframed_supported, pref->phy, pref->rtn, |
| pref->latency, pref->pd_min, pref->pd_max, stream->codec->id); |
| |
| unicast_client_ep_set_codec(ep, cfg->codec.id, |
| sys_le16_to_cpu(cfg->codec.cid), |
| sys_le16_to_cpu(cfg->codec.vid), |
| buf, cfg->cc_len, NULL); |
| |
| /* Notify upper layer */ |
| if (stream->ops != NULL && stream->ops->configured != NULL) { |
| stream->ops->configured(stream, pref); |
| } else { |
| BT_WARN("No callback for configured set"); |
| } |
| } |
| |
| static void unicast_client_ep_qos_state(struct bt_audio_ep *ep, |
| struct net_buf_simple *buf) |
| { |
| struct bt_ascs_ase_status_qos *qos; |
| struct bt_audio_stream *stream; |
| |
| if (buf->len < sizeof(*qos)) { |
| BT_ERR("QoS status too short"); |
| return; |
| } |
| |
| stream = ep->stream; |
| if (stream == NULL) { |
| BT_ERR("No stream active for endpoint"); |
| return; |
| } |
| |
| qos = net_buf_simple_pull_mem(buf, sizeof(*qos)); |
| |
| /* Reset any existing QoS configuration */ |
| unicast_client_ep_qos_reset(ep); |
| |
| 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)); |
| |
| BT_DBG("dir 0x%02x cig 0x%02x cis 0x%02x codec 0x%02x interval %u " |
| "framing 0x%02x phy 0x%02x rtn %u latency %u pd %u", |
| ep->dir, ep->cig_id, ep->cis_id, stream->codec->id, |
| stream->qos->interval, stream->qos->framing, |
| stream->qos->phy, stream->qos->rtn, stream->qos->latency, |
| stream->qos->pd); |
| |
| /* Disconnect ISO if connected */ |
| if (stream->iso->state == BT_ISO_STATE_CONNECTED) { |
| bt_audio_stream_disconnect(stream); |
| } |
| |
| /* Notify upper layer */ |
| if (stream->ops != NULL && stream->ops->qos_set != NULL) { |
| stream->ops->qos_set(stream); |
| } else { |
| BT_WARN("No callback for qos_set set"); |
| } |
| } |
| |
| static void unicast_client_ep_enabling_state(struct bt_audio_ep *ep, |
| struct net_buf_simple *buf) |
| { |
| struct bt_ascs_ase_status_enable *enable; |
| struct bt_audio_stream *stream; |
| |
| if (buf->len < sizeof(*enable)) { |
| BT_ERR("Enabling status too short"); |
| return; |
| } |
| |
| stream = ep->stream; |
| if (stream == NULL) { |
| BT_ERR("No stream active for endpoint"); |
| return; |
| } |
| |
| enable = net_buf_simple_pull_mem(buf, sizeof(*enable)); |
| |
| BT_DBG("dir 0x%02x cig 0x%02x cis 0x%02x", |
| ep->dir, ep->cig_id, ep->cis_id); |
| |
| unicast_client_ep_set_metadata(ep, buf, enable->metadata_len, NULL); |
| |
| /* Notify upper layer */ |
| if (stream->ops != NULL && stream->ops->enabled != NULL) { |
| stream->ops->enabled(stream); |
| } else { |
| BT_WARN("No callback for enabled set"); |
| } |
| } |
| |
| static void unicast_client_ep_streaming_state(struct bt_audio_ep *ep, |
| struct net_buf_simple *buf) |
| { |
| struct bt_ascs_ase_status_stream *stream_status; |
| struct bt_audio_stream *stream; |
| |
| if (buf->len < sizeof(*stream_status)) { |
| BT_ERR("Streaming status too short"); |
| return; |
| } |
| |
| stream = ep->stream; |
| if (stream == NULL) { |
| BT_ERR("No stream active for endpoint"); |
| return; |
| } |
| |
| stream_status = net_buf_simple_pull_mem(buf, sizeof(*stream_status)); |
| |
| BT_DBG("dir 0x%02x cig 0x%02x cis 0x%02x", |
| ep->dir, ep->cig_id, ep->cis_id); |
| |
| /* Notify upper layer */ |
| if (stream->ops != NULL && stream->ops->started != NULL) { |
| stream->ops->started(stream); |
| } else { |
| BT_WARN("No callback for started set"); |
| } |
| } |
| |
| static void unicast_client_ep_disabling_state(struct bt_audio_ep *ep, |
| struct net_buf_simple *buf) |
| { |
| struct bt_ascs_ase_status_disable *disable; |
| struct bt_audio_stream *stream; |
| |
| if (buf->len < sizeof(*disable)) { |
| BT_ERR("Disabling status too short"); |
| return; |
| } |
| |
| stream = ep->stream; |
| if (stream == NULL) { |
| BT_ERR("No stream active for endpoint"); |
| return; |
| } |
| |
| disable = net_buf_simple_pull_mem(buf, sizeof(*disable)); |
| |
| BT_DBG("dir 0x%02x cig 0x%02x cis 0x%02x", |
| ep->dir, ep->cig_id, ep->cis_id); |
| |
| /* Notify upper layer */ |
| if (stream->ops != NULL && stream->ops->disabled != NULL) { |
| stream->ops->disabled(stream); |
| } else { |
| BT_WARN("No callback for disabled set"); |
| } |
| } |
| |
| static void unicast_client_ep_releasing_state(struct bt_audio_ep *ep, |
| struct net_buf_simple *buf) |
| { |
| struct bt_audio_stream *stream; |
| |
| stream = ep->stream; |
| if (stream == NULL) { |
| BT_ERR("No stream active for endpoint"); |
| return; |
| } |
| |
| BT_DBG("dir 0x%02x", ep->dir); |
| |
| /* 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. |
| */ |
| bt_audio_stream_disconnect(stream); |
| } |
| |
| static void unicast_client_ep_set_status(struct bt_audio_ep *ep, |
| struct net_buf_simple *buf) |
| { |
| struct bt_ascs_ase_status *status; |
| uint8_t old_state; |
| |
| if (!ep) { |
| return; |
| } |
| |
| status = net_buf_simple_pull_mem(buf, sizeof(*status)); |
| |
| |
| old_state = ep->status.state; |
| ep->status = *status; |
| |
| BT_DBG("ep %p handle 0x%04x id 0x%02x state %s -> %s", ep, ep->handle, |
| status->id, bt_audio_ep_state_str(old_state), |
| bt_audio_ep_state_str(status->state)); |
| |
| switch (status->state) { |
| case BT_AUDIO_EP_STATE_IDLE: |
| unicast_client_ep_idle_state(ep, buf); |
| break; |
| case BT_AUDIO_EP_STATE_CODEC_CONFIGURED: |
| switch (old_state) { |
| /* Valid only if ASE_State field = 0x00 (Idle) */ |
| case BT_AUDIO_EP_STATE_IDLE: |
| /* or 0x01 (Codec Configured) */ |
| case BT_AUDIO_EP_STATE_CODEC_CONFIGURED: |
| /* or 0x02 (QoS Configured) */ |
| case BT_AUDIO_EP_STATE_QOS_CONFIGURED: |
| /* or 0x06 (Releasing) */ |
| case BT_AUDIO_EP_STATE_RELEASING: |
| break; |
| default: |
| BT_WARN("Invalid state transition: %s -> %s", |
| bt_audio_ep_state_str(old_state), |
| bt_audio_ep_state_str(ep->status.state)); |
| } |
| unicast_client_ep_config_state(ep, buf); |
| break; |
| case BT_AUDIO_EP_STATE_QOS_CONFIGURED: |
| switch (old_state) { |
| /* Valid only if ASE_State field = 0x01 (Codec Configured) */ |
| case BT_AUDIO_EP_STATE_CODEC_CONFIGURED: |
| /* or 0x02 (QoS Configured) */ |
| case BT_AUDIO_EP_STATE_QOS_CONFIGURED: |
| break; |
| default: |
| BT_WARN("Invalid state transition: %s -> %s", |
| bt_audio_ep_state_str(old_state), |
| bt_audio_ep_state_str(ep->status.state)); |
| } |
| unicast_client_ep_qos_state(ep, buf); |
| break; |
| case BT_AUDIO_EP_STATE_ENABLING: |
| /* Valid only if ASE_State field = 0x02 (QoS Configured) */ |
| if (old_state != BT_AUDIO_EP_STATE_QOS_CONFIGURED) { |
| BT_WARN("Invalid state transition: %s -> %s", |
| bt_audio_ep_state_str(old_state), |
| bt_audio_ep_state_str(ep->status.state)); |
| } |
| unicast_client_ep_enabling_state(ep, buf); |
| break; |
| case BT_AUDIO_EP_STATE_STREAMING: |
| switch (old_state) { |
| /* Valid only if ASE_State field = 0x02 (QoS Configured) */ |
| case BT_AUDIO_EP_STATE_QOS_CONFIGURED: |
| /* or 0x03 (Enabling)*/ |
| case BT_AUDIO_EP_STATE_ENABLING: |
| break; |
| default: |
| BT_WARN("Invalid state transition: %s -> %s", |
| bt_audio_ep_state_str(old_state), |
| bt_audio_ep_state_str(ep->status.state)); |
| } |
| unicast_client_ep_streaming_state(ep, buf); |
| break; |
| case BT_AUDIO_EP_STATE_DISABLING: |
| switch (old_state) { |
| /* Valid only if ASE_State field = 0x03 (Enabling) */ |
| case BT_AUDIO_EP_STATE_ENABLING: |
| /* or 0x04 (Streaming) */ |
| case BT_AUDIO_EP_STATE_STREAMING: |
| break; |
| default: |
| BT_WARN("Invalid state transition: %s -> %s", |
| bt_audio_ep_state_str(old_state), |
| bt_audio_ep_state_str(ep->status.state)); |
| } |
| unicast_client_ep_disabling_state(ep, buf); |
| break; |
| case BT_AUDIO_EP_STATE_RELEASING: |
| switch (old_state) { |
| /* Valid only if ASE_State field = 0x01 (Codec Configured) */ |
| case BT_AUDIO_EP_STATE_CODEC_CONFIGURED: |
| /* or 0x02 (QoS Configured) */ |
| case BT_AUDIO_EP_STATE_QOS_CONFIGURED: |
| /* or 0x03 (Enabling) */ |
| case BT_AUDIO_EP_STATE_ENABLING: |
| /* or 0x04 (Streaming) */ |
| case BT_AUDIO_EP_STATE_STREAMING: |
| /* or 0x04 (Disabling) */ |
| case BT_AUDIO_EP_STATE_DISABLING: |
| break; |
| default: |
| BT_WARN("Invalid state transition: %s -> %s", |
| bt_audio_ep_state_str(old_state), |
| bt_audio_ep_state_str(ep->status.state)); |
| } |
| unicast_client_ep_releasing_state(ep, buf); |
| break; |
| } |
| } |
| |
| static void unicast_client_codec_data_add(struct net_buf_simple *buf, |
| const char *prefix, |
| size_t num, |
| struct bt_codec_data *data) |
| { |
| for (size_t i = 0; i < num; i++) { |
| struct bt_data *d = &data[i].data; |
| struct bt_ascs_codec_config *cc; |
| |
| BT_DBG("#%u: %s type 0x%02x len %u", i, prefix, d->type, |
| d->data_len); |
| BT_HEXDUMP_DBG(d->data, d->data_len, prefix); |
| |
| cc = net_buf_simple_add(buf, sizeof(*cc)); |
| cc->len = d->data_len + sizeof(cc->type); |
| cc->type = d->type; |
| net_buf_simple_add_mem(buf, d->data, d->data_len); |
| } |
| } |
| |
| static bool unicast_client_codec_config_store(struct bt_data *data, |
| void *user_data) |
| { |
| struct bt_codec *codec = user_data; |
| struct bt_codec_data *cdata; |
| |
| if (codec->data_count >= ARRAY_SIZE(codec->data)) { |
| BT_ERR("No slot available for Codec Config"); |
| return false; |
| } |
| |
| cdata = &codec->data[codec->data_count]; |
| |
| if (data->data_len > sizeof(cdata->value)) { |
| BT_ERR("Not enough space for Codec Config: %u > %zu", |
| data->data_len, sizeof(cdata->value)); |
| return false; |
| } |
| |
| BT_DBG("#%u type 0x%02x len %u", codec->data_count, data->type, |
| data->data_len); |
| |
| cdata->data.type = data->type; |
| cdata->data.data_len = data->data_len; |
| |
| /* Deep copy data contents */ |
| cdata->data.data = cdata->value; |
| (void)memcpy(cdata->value, data->data, data->data_len); |
| |
| BT_HEXDUMP_DBG(cdata->value, data->data_len, "data"); |
| |
| codec->data_count++; |
| |
| return true; |
| } |
| |
| static int unicast_client_ep_set_codec(struct bt_audio_ep *ep, uint8_t id, |
| uint16_t cid, uint16_t vid, |
| struct net_buf_simple *buf, uint8_t len, |
| struct bt_codec *codec) |
| { |
| struct net_buf_simple ad; |
| |
| if (!ep && !codec) { |
| return -EINVAL; |
| } |
| |
| BT_DBG("ep %p codec id 0x%02x cid 0x%04x vid 0x%04x len %u", ep, id, |
| cid, vid, len); |
| |
| if (!codec) { |
| codec = &ep->codec; |
| } |
| |
| codec->id = id; |
| codec->cid = cid; |
| codec->vid = vid; |
| |
| /* Reset current metadata */ |
| codec->data_count = 0; |
| (void)memset(codec->data, 0, sizeof(codec->data)); |
| |
| if (!len) { |
| return 0; |
| } |
| |
| net_buf_simple_init_with_data(&ad, net_buf_simple_pull_mem(buf, len), |
| len); |
| |
| /* Parse LTV entries */ |
| bt_data_parse(&ad, unicast_client_codec_config_store, codec); |
| |
| /* Check if all entries could be parsed */ |
| if (ad.len) { |
| BT_ERR("Unable to parse Codec Config: len %u", ad.len); |
| goto fail; |
| } |
| |
| return 0; |
| |
| fail: |
| (void)memset(codec, 0, sizeof(*codec)); |
| return -EINVAL; |
| } |
| |
| static bool unicast_client_codec_metadata_store(struct bt_data *data, |
| void *user_data) |
| { |
| struct bt_codec *codec = user_data; |
| struct bt_codec_data *meta; |
| |
| if (codec->meta_count >= ARRAY_SIZE(codec->meta)) { |
| BT_ERR("No slot available for Codec Config Metadata"); |
| return false; |
| } |
| |
| meta = &codec->meta[codec->meta_count]; |
| |
| if (data->data_len > sizeof(meta->value)) { |
| BT_ERR("Not enough space for Codec Config Metadata: %u > %zu", |
| data->data_len, sizeof(meta->value)); |
| return false; |
| } |
| |
| BT_DBG("#%u type 0x%02x len %u", codec->meta_count, data->type, |
| data->data_len); |
| |
| meta->data.type = data->type; |
| meta->data.data_len = data->data_len; |
| |
| /* Deep copy data contents */ |
| meta->data.data = meta->value; |
| (void)memcpy(meta->value, data->data, data->data_len); |
| |
| BT_HEXDUMP_DBG(meta->value, data->data_len, "data"); |
| |
| codec->meta_count++; |
| |
| return true; |
| } |
| |
| static int unicast_client_ep_set_metadata(struct bt_audio_ep *ep, |
| struct net_buf_simple *buf, |
| uint8_t len, struct bt_codec *codec) |
| { |
| struct net_buf_simple meta; |
| int err; |
| |
| if (!ep && !codec) { |
| return -EINVAL; |
| } |
| |
| BT_DBG("ep %p len %u codec %p", ep, len, codec); |
| |
| if (!codec) { |
| codec = &ep->codec; |
| } |
| |
| /* Reset current metadata */ |
| codec->meta_count = 0; |
| (void)memset(codec->meta, 0, sizeof(codec->meta)); |
| |
| if (!len) { |
| return 0; |
| } |
| |
| net_buf_simple_init_with_data(&meta, net_buf_simple_pull_mem(buf, len), |
| len); |
| |
| /* Parse LTV entries */ |
| bt_data_parse(&meta, unicast_client_codec_metadata_store, codec); |
| |
| /* Check if all entries could be parsed */ |
| if (meta.len) { |
| BT_ERR("Unable to parse Metadata: len %u", meta.len); |
| err = -EINVAL; |
| |
| if (meta.len > 2) { |
| /* Value of the Metadata Type field in error */ |
| err = meta.data[2]; |
| } |
| |
| goto fail; |
| } |
| |
| return 0; |
| |
| fail: |
| codec->meta_count = 0; |
| (void)memset(codec->meta, 0, sizeof(codec->meta)); |
| return err; |
| } |
| |
| void bt_unicast_client_ep_set_state(struct bt_audio_ep *ep, uint8_t state) |
| { |
| uint8_t old_state; |
| |
| if (!ep) { |
| return; |
| } |
| |
| BT_DBG("ep %p id 0x%02x %s -> %s", ep, ep->status.id, |
| bt_audio_ep_state_str(ep->status.state), |
| bt_audio_ep_state_str(state)); |
| |
| old_state = ep->status.state; |
| ep->status.state = state; |
| |
| if (!ep->stream || old_state == state) { |
| return; |
| } |
| |
| if (state == BT_AUDIO_EP_STATE_CODEC_CONFIGURED) { |
| bt_unicast_client_ep_unbind_audio_iso(ep); |
| } else if (state == BT_AUDIO_EP_STATE_IDLE) { |
| bt_unicast_client_ep_unbind_audio_iso(ep); |
| bt_audio_stream_detach(ep->stream); |
| } |
| } |
| |
| 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; |
| int i; |
| |
| struct net_buf_simple buf; |
| |
| BT_DBG("conn %p len %u", conn, length); |
| |
| if (!data) { |
| BT_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)) { |
| BT_ERR("Control Point Notification too small"); |
| return BT_GATT_ITER_STOP; |
| } |
| |
| rsp = net_buf_simple_pull_mem(&buf, sizeof(*rsp)); |
| |
| for (i = 0; i < rsp->num_ase; i++) { |
| struct bt_ascs_cp_ase_rsp *ase_rsp; |
| |
| if (buf.len < sizeof(*ase_rsp)) { |
| BT_ERR("Control Point Notification too small"); |
| return BT_GATT_ITER_STOP; |
| } |
| |
| ase_rsp = net_buf_simple_pull_mem(&buf, sizeof(*ase_rsp)); |
| |
| BT_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); |
| } |
| |
| return BT_GATT_ITER_CONTINUE; |
| } |
| |
| 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_audio_ep *ep; |
| |
| ep = CONTAINER_OF(params, struct bt_audio_ep, subscribe); |
| |
| BT_DBG("conn %p ep %p len %u", conn, ep, length); |
| |
| if (!data) { |
| BT_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(struct bt_ascs_ase_status)) { |
| BT_ERR("Notification too small"); |
| return BT_GATT_ITER_STOP; |
| } |
| |
| unicast_client_ep_set_status(ep, &buf); |
| |
| return BT_GATT_ITER_CONTINUE; |
| } |
| |
| static int unicast_client_ep_subscribe(struct bt_conn *conn, |
| struct bt_audio_ep *ep) |
| { |
| BT_DBG("ep %p handle 0x%02x", ep, ep->handle); |
| |
| if (ep->subscribe.value_handle) { |
| return 0; |
| } |
| |
| ep->subscribe.value_handle = ep->handle; |
| ep->subscribe.ccc_handle = 0x0000; |
| ep->subscribe.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE; |
| ep->subscribe.disc_params = &ep->discover; |
| ep->subscribe.notify = unicast_client_ep_notify; |
| ep->subscribe.value = BT_GATT_CCC_NOTIFY; |
| atomic_set_bit(ep->subscribe.flags, BT_GATT_SUBSCRIBE_FLAG_VOLATILE); |
| |
| return bt_gatt_subscribe(conn, &ep->subscribe); |
| } |
| |
| static void unicast_client_ep_set_cp(struct bt_conn *conn, uint16_t handle) |
| { |
| size_t i; |
| uint8_t index; |
| |
| BT_DBG("conn %p 0x%04x", conn, handle); |
| |
| index = bt_conn_index(conn); |
| |
| if (!cp_subscribe[index].value_handle) { |
| cp_subscribe[index].value_handle = handle; |
| cp_subscribe[index].ccc_handle = 0x0000; |
| cp_subscribe[index].end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE; |
| cp_subscribe[index].disc_params = &cp_disc[index]; |
| cp_subscribe[index].notify = unicast_client_cp_notify; |
| cp_subscribe[index].value = BT_GATT_CCC_NOTIFY; |
| atomic_set_bit(cp_subscribe[index].flags, |
| BT_GATT_SUBSCRIBE_FLAG_VOLATILE); |
| |
| bt_gatt_subscribe(conn, &cp_subscribe[index]); |
| } |
| |
| for (i = 0; i < ARRAY_SIZE(snks[index]); i++) { |
| struct bt_audio_ep *ep = &snks[index][i]; |
| |
| if (ep->handle) { |
| ep->cp_handle = handle; |
| } |
| } |
| |
| for (i = 0; i < ARRAY_SIZE(srcs[index]); i++) { |
| struct bt_audio_ep *ep = &srcs[index][i]; |
| |
| if (ep->handle) { |
| ep->cp_handle = handle; |
| } |
| } |
| } |
| |
| NET_BUF_SIMPLE_DEFINE_STATIC(ep_buf, CONFIG_BT_L2CAP_TX_MTU); |
| |
| struct net_buf_simple *bt_unicast_client_ep_create_pdu(uint8_t op) |
| { |
| struct bt_ascs_ase_cp *hdr; |
| |
| /* Reset buffer before using */ |
| net_buf_simple_reset(&ep_buf); |
| |
| hdr = net_buf_simple_add(&ep_buf, sizeof(*hdr)); |
| hdr->op = op; |
| |
| return &ep_buf; |
| } |
| |
| static int unicast_client_ep_config(struct bt_audio_ep *ep, |
| struct net_buf_simple *buf, |
| struct bt_codec *codec) |
| { |
| struct bt_ascs_config *req; |
| uint8_t cc_len; |
| |
| BT_DBG("ep %p buf %p codec %p", ep, buf, codec); |
| |
| if (!ep) { |
| return -EINVAL; |
| } |
| |
| switch (ep->status.state) { |
| /* Valid only if ASE_State field = 0x00 (Idle) */ |
| case BT_AUDIO_EP_STATE_IDLE: |
| /* or 0x01 (Codec Configured) */ |
| case BT_AUDIO_EP_STATE_CODEC_CONFIGURED: |
| /* or 0x02 (QoS Configured) */ |
| case BT_AUDIO_EP_STATE_QOS_CONFIGURED: |
| break; |
| default: |
| BT_ERR("Invalid state: %s", |
| bt_audio_ep_state_str(ep->status.state)); |
| return -EINVAL; |
| } |
| |
| BT_DBG("id 0x%02x dir 0x%02x codec 0x%02x", ep->status.id, |
| ep->dir, codec->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->id; |
| req->codec.cid = codec->cid; |
| req->codec.vid = codec->vid; |
| |
| cc_len = buf->len; |
| unicast_client_codec_data_add(buf, "data", codec->data_count, |
| codec->data); |
| req->cc_len = buf->len - cc_len; |
| |
| return 0; |
| } |
| |
| int bt_unicast_client_ep_qos(struct bt_audio_ep *ep, struct net_buf_simple *buf, |
| struct bt_codec_qos *qos) |
| { |
| struct bt_ascs_qos *req; |
| struct bt_conn_iso *conn_iso; |
| |
| BT_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_AUDIO_EP_STATE_CODEC_CONFIGURED: |
| /* or 0x02 (QoS Configured) */ |
| case BT_AUDIO_EP_STATE_QOS_CONFIGURED: |
| break; |
| default: |
| BT_ERR("Invalid state: %s", |
| bt_audio_ep_state_str(ep->status.state)); |
| return -EINVAL; |
| } |
| |
| conn_iso = &ep->iso->iso_chan.iso->iso; |
| |
| BT_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_audio_ep *ep, |
| struct net_buf_simple *buf, |
| struct bt_codec_data *meta, |
| size_t meta_count) |
| { |
| struct bt_ascs_metadata *req; |
| |
| BT_DBG("ep %p buf %p metadata count %zu", ep, buf, meta_count); |
| |
| if (!ep) { |
| return -EINVAL; |
| } |
| |
| if (ep->status.state != BT_AUDIO_EP_STATE_QOS_CONFIGURED) { |
| BT_ERR("Invalid state: %s", |
| bt_audio_ep_state_str(ep->status.state)); |
| return -EINVAL; |
| } |
| |
| BT_DBG("id 0x%02x", ep->status.id); |
| |
| req = net_buf_simple_add(buf, sizeof(*req)); |
| req->ase = ep->status.id; |
| |
| req->len = buf->len; |
| unicast_client_codec_data_add(buf, "meta", meta_count, meta); |
| req->len = buf->len - req->len; |
| |
| return 0; |
| } |
| |
| static int unicast_client_ep_metadata(struct bt_audio_ep *ep, |
| struct net_buf_simple *buf, |
| struct bt_codec_data *meta, |
| size_t meta_count) |
| { |
| struct bt_ascs_metadata *req; |
| |
| BT_DBG("ep %p buf %p metadata count %zu", ep, buf, meta_count); |
| |
| if (!ep) { |
| return -EINVAL; |
| } |
| |
| switch (ep->status.state) { |
| /* Valid for an ASE only if ASE_State field = 0x03 (Enabling) */ |
| case BT_AUDIO_EP_STATE_ENABLING: |
| /* or 0x04 (Streaming) */ |
| case BT_AUDIO_EP_STATE_STREAMING: |
| break; |
| default: |
| BT_ERR("Invalid state: %s", |
| bt_audio_ep_state_str(ep->status.state)); |
| return -EINVAL; |
| } |
| |
| BT_DBG("id 0x%02x", ep->status.id); |
| |
| req = net_buf_simple_add(buf, sizeof(*req)); |
| req->ase = ep->status.id; |
| |
| req->len = buf->len; |
| unicast_client_codec_data_add(buf, "meta", meta_count, meta); |
| req->len = buf->len - req->len; |
| |
| return 0; |
| } |
| |
| static int unicast_client_ep_start(struct bt_audio_ep *ep, |
| struct net_buf_simple *buf) |
| { |
| BT_DBG("ep %p buf %p", ep, buf); |
| |
| if (!ep) { |
| return -EINVAL; |
| } |
| |
| if (ep->status.state != BT_AUDIO_EP_STATE_ENABLING && |
| ep->status.state != BT_AUDIO_EP_STATE_DISABLING) { |
| BT_ERR("Invalid state: %s", |
| bt_audio_ep_state_str(ep->status.state)); |
| return -EINVAL; |
| } |
| |
| BT_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_audio_ep *ep, |
| struct net_buf_simple *buf) |
| { |
| BT_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_AUDIO_EP_STATE_ENABLING: |
| /* or 0x04 (Streaming) */ |
| case BT_AUDIO_EP_STATE_STREAMING: |
| break; |
| default: |
| BT_ERR("Invalid state: %s", |
| bt_audio_ep_state_str(ep->status.state)); |
| return -EINVAL; |
| } |
| |
| BT_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_audio_ep *ep, |
| struct net_buf_simple *buf) |
| { |
| BT_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_AUDIO_EP_STATE_DISABLING) { |
| BT_ERR("Invalid state: %s", |
| bt_audio_ep_state_str(ep->status.state)); |
| return -EINVAL; |
| } |
| |
| BT_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_audio_ep *ep, |
| struct net_buf_simple *buf) |
| { |
| BT_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_AUDIO_EP_STATE_CODEC_CONFIGURED: |
| /* or 0x02 (QoS Configured) */ |
| case BT_AUDIO_EP_STATE_QOS_CONFIGURED: |
| /* or 0x03 (Enabling) */ |
| case BT_AUDIO_EP_STATE_ENABLING: |
| /* or 0x04 (Streaming) */ |
| case BT_AUDIO_EP_STATE_STREAMING: |
| /* or 0x05 (Disabling) */ |
| case BT_AUDIO_EP_STATE_DISABLING: |
| break; |
| default: |
| BT_ERR("Invalid state: %s", |
| bt_audio_ep_state_str(ep->status.state)); |
| return -EINVAL; |
| } |
| |
| BT_DBG("id 0x%02x", ep->status.id); |
| |
| net_buf_simple_add_u8(buf, ep->status.id); |
| |
| return 0; |
| } |
| |
| int bt_unicast_client_ep_send(struct bt_conn *conn, struct bt_audio_ep *ep, |
| struct net_buf_simple *buf) |
| { |
| BT_DBG("conn %p ep %p buf %p len %u", conn, ep, buf, buf->len); |
| |
| return bt_gatt_write_without_response(conn, ep->cp_handle, |
| buf->data, buf->len, false); |
| } |
| |
| static void unicast_client_reset(struct bt_audio_ep *ep) |
| { |
| BT_DBG("ep %p", ep); |
| |
| bt_audio_stream_reset(ep->stream); |
| |
| switch (ep->dir) { |
| case BT_AUDIO_DIR_SINK: |
| ep->iso->source_ep = NULL; |
| break; |
| case BT_AUDIO_DIR_SOURCE: |
| ep->iso->sink_ep = NULL; |
| break; |
| default: |
| BT_DBG("Invalid ep->dir %u", ep->dir); |
| break; |
| } |
| |
| (void)memset(ep, 0, offsetof(struct bt_audio_ep, subscribe)); |
| } |
| |
| static void unicast_client_ep_reset(struct bt_conn *conn) |
| { |
| size_t i; |
| uint8_t index; |
| |
| BT_DBG("conn %p", conn); |
| |
| index = bt_conn_index(conn); |
| |
| for (i = 0; i < ARRAY_SIZE(snks[index]); i++) { |
| struct bt_audio_ep *ep = &snks[index][i]; |
| |
| unicast_client_reset(ep); |
| } |
| |
| for (i = 0; i < ARRAY_SIZE(srcs[index]); i++) { |
| struct bt_audio_ep *ep = &srcs[index][i]; |
| |
| unicast_client_reset(ep); |
| } |
| } |
| |
| void bt_unicast_client_ep_attach(struct bt_audio_ep *ep, |
| struct bt_audio_stream *stream) |
| { |
| BT_DBG("ep %p stream %p", ep, stream); |
| |
| if (ep == NULL || stream == NULL || ep->stream == stream) { |
| return; |
| } |
| |
| if (ep->stream) { |
| BT_ERR("ep %p attached to another stream %p", ep, ep->stream); |
| return; |
| } |
| |
| ep->stream = stream; |
| stream->ep = ep; |
| |
| if (stream->iso == NULL) { |
| stream->iso = &ep->iso->iso_chan; |
| } |
| } |
| |
| void bt_unicast_client_ep_detach(struct bt_audio_ep *ep, |
| struct bt_audio_stream *stream) |
| { |
| BT_DBG("ep %p stream %p", ep, stream); |
| |
| if (ep == NULL || stream == NULL || !ep->stream) { |
| return; |
| } |
| |
| if (ep->stream != stream) { |
| BT_ERR("ep %p attached to another stream %p", ep, ep->stream); |
| return; |
| } |
| |
| ep->stream = NULL; |
| stream->ep = NULL; |
| } |
| |
| int bt_unicast_client_config(struct bt_audio_stream *stream, |
| struct bt_codec *codec) |
| { |
| struct bt_audio_ep *ep = stream->ep; |
| struct bt_ascs_config_op *op; |
| struct net_buf_simple *buf; |
| int err; |
| |
| buf = bt_unicast_client_ep_create_pdu(BT_ASCS_CONFIG_OP); |
| |
| op = net_buf_simple_add(buf, sizeof(*op)); |
| op->num_ases = 0x01; |
| |
| err = unicast_client_ep_config(ep, buf, codec); |
| if (err) { |
| return err; |
| } |
| |
| err = bt_unicast_client_ep_send(stream->conn, ep, buf); |
| if (err) { |
| return err; |
| } |
| |
| return 0; |
| } |
| |
| int bt_unicast_client_enable(struct bt_audio_stream *stream, |
| struct bt_codec_data *meta, |
| size_t meta_count) |
| { |
| struct bt_audio_ep *ep = stream->ep; |
| struct net_buf_simple *buf; |
| struct bt_ascs_enable_op *req; |
| int err; |
| |
| BT_DBG("stream %p", stream); |
| |
| buf = bt_unicast_client_ep_create_pdu(BT_ASCS_ENABLE_OP); |
| |
| req = net_buf_simple_add(buf, sizeof(*req)); |
| req->num_ases = 0x01; |
| |
| err = unicast_client_ep_enable(ep, buf, meta, meta_count); |
| if (err) { |
| return err; |
| } |
| |
| return bt_unicast_client_ep_send(stream->conn, ep, buf); |
| } |
| |
| int bt_unicast_client_metadata(struct bt_audio_stream *stream, |
| struct bt_codec_data *meta, |
| size_t meta_count) |
| { |
| struct bt_audio_ep *ep = stream->ep; |
| struct net_buf_simple *buf; |
| struct bt_ascs_enable_op *req; |
| int err; |
| |
| BT_DBG("stream %p", stream); |
| |
| buf = bt_unicast_client_ep_create_pdu(BT_ASCS_METADATA_OP); |
| |
| req = net_buf_simple_add(buf, sizeof(*req)); |
| req->num_ases = 0x01; |
| |
| err = unicast_client_ep_metadata(ep, buf, meta, meta_count); |
| if (err) { |
| return err; |
| } |
| |
| return bt_unicast_client_ep_send(stream->conn, ep, buf); |
| } |
| |
| int bt_unicast_client_start(struct bt_audio_stream *stream) |
| { |
| struct bt_audio_ep *ep = stream->ep; |
| struct net_buf_simple *buf; |
| struct bt_ascs_start_op *req; |
| int err; |
| |
| BT_DBG("stream %p", stream); |
| |
| buf = bt_unicast_client_ep_create_pdu(BT_ASCS_START_OP); |
| |
| req = net_buf_simple_add(buf, sizeof(*req)); |
| req->num_ases = 0x00; |
| |
| /* If an ASE is in the Enabling state, and if the Unicast Client has |
| * not yet established a CIS for that ASE, the Unicast Client shall |
| * attempt to establish a CIS by using the Connected Isochronous Stream |
| * Central Establishment procedure. |
| */ |
| err = bt_audio_stream_connect(stream); |
| if (!err) { |
| return 0; |
| } |
| |
| if (err && err != -EALREADY) { |
| BT_DBG("bt_audio_stream_connect failed: %d", err); |
| return err; |
| } else if (err == -EALREADY) { |
| BT_DBG("ISO %p already connected", stream->iso); |
| } |
| |
| /* 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_start(ep, buf); |
| if (err) { |
| return err; |
| } |
| req->num_ases++; |
| |
| return bt_unicast_client_ep_send(stream->conn, ep, buf); |
| } |
| |
| return 0; |
| } |
| |
| int bt_unicast_client_disable(struct bt_audio_stream *stream) |
| { |
| struct bt_audio_ep *ep = stream->ep; |
| struct net_buf_simple *buf; |
| struct bt_ascs_disable_op *req; |
| int err; |
| |
| BT_DBG("stream %p", stream); |
| |
| buf = bt_unicast_client_ep_create_pdu(BT_ASCS_DISABLE_OP); |
| |
| 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_unicast_client_ep_send(stream->conn, ep, buf); |
| } |
| |
| int bt_unicast_client_stop(struct bt_audio_stream *stream) |
| { |
| struct bt_audio_ep *ep = stream->ep; |
| struct net_buf_simple *buf; |
| struct bt_ascs_start_op *req; |
| int err; |
| |
| BT_DBG("stream %p", stream); |
| |
| buf = bt_unicast_client_ep_create_pdu(BT_ASCS_STOP_OP); |
| |
| 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_unicast_client_ep_send(stream->conn, ep, buf); |
| } |
| |
| return 0; |
| } |
| |
| int bt_unicast_client_release(struct bt_audio_stream *stream) |
| { |
| struct bt_audio_ep *ep = stream->ep; |
| struct net_buf_simple *buf; |
| struct bt_ascs_disable_op *req; |
| int err, len; |
| |
| BT_DBG("stream %p", stream); |
| |
| if (stream->conn == NULL || stream->conn->state != BT_CONN_CONNECTED) { |
| return -ENOTCONN; |
| } |
| |
| buf = bt_unicast_client_ep_create_pdu(BT_ASCS_RELEASE_OP); |
| |
| 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_AUDIO_EP_STATE_IDLE) { |
| bt_audio_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; |
| } |
| |
| return bt_unicast_client_ep_send(stream->conn, ep, buf); |
| } |
| |
| 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_audio_discover_params *params; |
| struct bt_gatt_chrc *chrc; |
| |
| params = CONTAINER_OF(discover, struct bt_audio_discover_params, |
| discover); |
| |
| if (!attr) { |
| if (params->err) { |
| BT_ERR("Unable to find ASE Control Point"); |
| } |
| params->func(conn, NULL, NULL, params); |
| return BT_GATT_ITER_STOP; |
| } |
| |
| chrc = attr->user_data; |
| |
| BT_DBG("conn %p attr %p handle 0x%04x", conn, attr, chrc->value_handle); |
| |
| params->err = 0; |
| unicast_client_ep_set_cp(conn, chrc->value_handle); |
| |
| params->func(conn, NULL, NULL, params); |
| |
| return BT_GATT_ITER_STOP; |
| } |
| |
| static int unicast_client_ase_cp_discover(struct bt_conn *conn, |
| struct bt_audio_discover_params *params) |
| { |
| BT_DBG("conn %p params %p", conn, params); |
| |
| params->err = BT_ATT_ERR_ATTRIBUTE_NOT_FOUND; |
| params->discover.uuid = cp_uuid; |
| params->discover.func = unicast_client_cp_discover_func; |
| params->discover.start_handle = BT_ATT_FIRST_ATTRIBUTE_HANDLE; |
| params->discover.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE; |
| params->discover.type = BT_GATT_DISCOVER_CHARACTERISTIC; |
| |
| return bt_gatt_discover(conn, ¶ms->discover); |
| } |
| |
| 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) |
| { |
| struct bt_audio_discover_params *params; |
| struct net_buf_simple buf; |
| struct bt_audio_ep *ep; |
| |
| params = CONTAINER_OF(read, struct bt_audio_discover_params, read); |
| |
| BT_DBG("conn %p err 0x%02x len %u", conn, err, length); |
| |
| if (err) { |
| if (err == BT_ATT_ERR_ATTRIBUTE_NOT_FOUND && params->num_eps) { |
| if (unicast_client_ase_cp_discover(conn, params) < 0) { |
| BT_ERR("Unable to discover ASE Control Point"); |
| err = BT_ATT_ERR_UNLIKELY; |
| goto fail; |
| } |
| return BT_GATT_ITER_STOP; |
| } |
| params->err = err; |
| params->func(conn, NULL, NULL, params); |
| return BT_GATT_ITER_STOP; |
| } |
| |
| if (!data) { |
| if (params->num_eps && |
| unicast_client_ase_cp_discover(conn, params) < 0) { |
| BT_ERR("Unable to discover ASE Control Point"); |
| err = BT_ATT_ERR_UNLIKELY; |
| goto fail; |
| } |
| return BT_GATT_ITER_STOP; |
| } |
| |
| BT_DBG("handle 0x%04x", read->by_uuid.start_handle); |
| |
| net_buf_simple_init_with_data(&buf, (void *)data, length); |
| |
| if (buf.len < sizeof(struct bt_ascs_ase_status)) { |
| BT_ERR("Read response too small"); |
| goto fail; |
| } |
| |
| ep = unicast_client_ep_get(conn, params->dir, |
| read->by_uuid.start_handle); |
| if (!ep) { |
| BT_WARN("No space left to parse ASE"); |
| if (params->num_eps) { |
| if (unicast_client_ase_cp_discover(conn, params) < 0) { |
| BT_ERR("Unable to discover ASE Control Point"); |
| err = BT_ATT_ERR_UNLIKELY; |
| goto fail; |
| } |
| return BT_GATT_ITER_STOP; |
| } |
| goto fail; |
| } |
| |
| unicast_client_ep_set_status(ep, &buf); |
| unicast_client_ep_subscribe(conn, ep); |
| |
| params->func(conn, NULL, ep, params); |
| |
| params->num_eps++; |
| |
| return BT_GATT_ITER_CONTINUE; |
| |
| fail: |
| params->func(conn, NULL, NULL, params); |
| return BT_GATT_ITER_STOP; |
| } |
| |
| static int unicast_client_ase_discover(struct bt_conn *conn, |
| struct bt_audio_discover_params *params) |
| { |
| BT_DBG("conn %p params %p", conn, params); |
| |
| params->read.func = unicast_client_ase_read_func; |
| params->read.handle_count = 0u; |
| |
| if (params->dir == BT_AUDIO_DIR_SINK) { |
| params->read.by_uuid.uuid = ase_snk_uuid; |
| } else if (params->dir == BT_AUDIO_DIR_SOURCE) { |
| params->read.by_uuid.uuid = ase_src_uuid; |
| } else { |
| return -EINVAL; |
| } |
| |
| params->read.by_uuid.start_handle = BT_ATT_FIRST_ATTRIBUTE_HANDLE; |
| params->read.by_uuid.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE; |
| |
| return bt_gatt_read(conn, ¶ms->read); |
| } |
| |
| 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 bt_audio_discover_params *params; |
| struct net_buf_simple buf; |
| struct bt_pacs_context *context; |
| int i, index; |
| |
| params = CONTAINER_OF(read, struct bt_audio_discover_params, read); |
| |
| BT_DBG("conn %p err 0x%02x len %u", conn, err, length); |
| |
| if (err || length < sizeof(uint16_t) * 2) { |
| goto discover_ase; |
| } |
| |
| net_buf_simple_init_with_data(&buf, (void *)data, length); |
| context = net_buf_simple_pull_mem(&buf, sizeof(*context)); |
| |
| index = bt_conn_index(conn); |
| |
| for (i = 0; i < CONFIG_BT_AUDIO_UNICAST_CLIENT_PAC_COUNT; i++) { |
| struct unicast_client_pac *pac = &cache[index][i]; |
| |
| if (PAC_DIR_UNUSED(pac->dir)) { |
| continue; |
| } |
| |
| switch (pac->dir) { |
| case BT_AUDIO_DIR_SINK: |
| pac->context = sys_le16_to_cpu(context->snk); |
| break; |
| case BT_AUDIO_DIR_SOURCE: |
| pac->context = sys_le16_to_cpu(context->src); |
| break; |
| default: |
| BT_WARN("Cached pac with invalid dir: %u", |
| pac->dir); |
| } |
| |
| BT_DBG("pac %p context 0x%04x", pac, pac->context); |
| } |
| |
| discover_ase: |
| /* Read ASE instances */ |
| if (unicast_client_ase_discover(conn, params) < 0) { |
| BT_ERR("Unable to read ASE"); |
| |
| params->func(conn, NULL, NULL, params); |
| } |
| |
| return BT_GATT_ITER_STOP; |
| } |
| |
| static int unicast_client_pacs_context_discover(struct bt_conn *conn, |
| struct bt_audio_discover_params *params) |
| { |
| BT_DBG("conn %p params %p", conn, params); |
| |
| params->read.func = unicast_client_pacs_context_read_func; |
| params->read.handle_count = 0u; |
| params->read.by_uuid.uuid = pacs_context_uuid; |
| params->read.by_uuid.start_handle = BT_ATT_FIRST_ATTRIBUTE_HANDLE; |
| params->read.by_uuid.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE; |
| |
| return bt_gatt_read(conn, ¶ms->read); |
| } |
| |
| static struct unicast_client_pac *unicast_client_pac_alloc(struct bt_conn *conn, |
| enum bt_audio_dir dir) |
| { |
| int i, index; |
| |
| index = bt_conn_index(conn); |
| |
| for (i = 0; i < CONFIG_BT_AUDIO_UNICAST_CLIENT_PAC_COUNT; i++) { |
| struct unicast_client_pac *pac = &cache[index][i]; |
| |
| if (PAC_DIR_UNUSED(pac->dir)) { |
| pac->dir = dir; |
| return pac; |
| } |
| } |
| |
| return NULL; |
| } |
| |
| 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) |
| { |
| struct bt_audio_discover_params *params; |
| struct net_buf_simple buf; |
| struct bt_pacs_read_rsp *rsp; |
| |
| params = CONTAINER_OF(read, struct bt_audio_discover_params, read); |
| |
| BT_DBG("conn %p err 0x%02x len %u", conn, err, length); |
| |
| if (err || !data) { |
| params->err = err; |
| goto fail; |
| } |
| |
| BT_DBG("handle 0x%04x", read->by_uuid.start_handle); |
| |
| net_buf_simple_init_with_data(&buf, (void *)data, length); |
| |
| if (buf.len < sizeof(*rsp)) { |
| BT_ERR("Read response too small"); |
| 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; |
| } |
| |
| while (rsp->num_pac) { |
| struct unicast_client_pac *bpac; |
| struct bt_pac *pac; |
| struct bt_pac_meta *meta; |
| |
| if (buf.len < sizeof(*pac)) { |
| BT_ERR("Malformed PAC: remaining len %u expected %zu", |
| buf.len, sizeof(*pac)); |
| break; |
| } |
| |
| pac = net_buf_simple_pull_mem(&buf, sizeof(*pac)); |
| |
| BT_DBG("pac #%u: cc len %u", params->num_caps, pac->cc_len); |
| |
| if (buf.len < pac->cc_len) { |
| BT_ERR("Malformed PAC: buf->len %u < %u pac->cc_len", |
| buf.len, pac->cc_len); |
| break; |
| } |
| |
| bpac = unicast_client_pac_alloc(conn, params->dir); |
| if (!bpac) { |
| BT_WARN("No space left to parse PAC"); |
| break; |
| } |
| |
| bpac->codec.id = pac->codec.id; |
| bpac->codec.cid = sys_le16_to_cpu(pac->codec.cid); |
| bpac->codec.vid = sys_le16_to_cpu(pac->codec.vid); |
| |
| if (unicast_client_ep_set_codec(NULL, pac->codec.id, |
| sys_le16_to_cpu(pac->codec.cid), |
| sys_le16_to_cpu(pac->codec.vid), |
| &buf, pac->cc_len, |
| &bpac->codec)) { |
| BT_ERR("Unable to parse Codec"); |
| break; |
| } |
| |
| meta = net_buf_simple_pull_mem(&buf, sizeof(*meta)); |
| |
| if (buf.len < meta->len) { |
| BT_ERR("Malformed PAC: remaining len %u expected %u", |
| buf.len, meta->len); |
| break; |
| } |
| |
| if (unicast_client_ep_set_metadata(NULL, &buf, meta->len, |
| &bpac->codec)) { |
| BT_ERR("Unable to parse Codec Metadata"); |
| break; |
| } |
| |
| BT_DBG("pac %p codec 0x%02x config count %u meta count %u ", |
| pac, bpac->codec.id, bpac->codec.data_count, |
| bpac->codec.meta_count); |
| |
| params->func(conn, &bpac->codec, NULL, params); |
| |
| rsp->num_pac--; |
| params->num_caps++; |
| } |
| |
| if (!params->num_caps) { |
| goto fail; |
| } |
| |
| /* Read PACS contexts */ |
| if (unicast_client_pacs_context_discover(conn, params) < 0) { |
| BT_ERR("Unable to read PACS context"); |
| goto fail; |
| } |
| |
| return BT_GATT_ITER_STOP; |
| |
| fail: |
| params->func(conn, NULL, NULL, params); |
| return BT_GATT_ITER_STOP; |
| } |
| |
| static void unicast_client_pac_reset(struct bt_conn *conn) |
| { |
| int index = bt_conn_index(conn); |
| int i; |
| |
| for (i = 0; i < CONFIG_BT_AUDIO_UNICAST_CLIENT_PAC_COUNT; i++) { |
| struct unicast_client_pac *pac = &cache[index][i]; |
| |
| if (!PAC_DIR_UNUSED(pac->dir)) { |
| (void)memset(pac, 0, sizeof(*pac)); |
| } |
| } |
| } |
| |
| static void unicast_client_disconnected(struct bt_conn *conn, uint8_t reason) |
| { |
| BT_DBG("conn %p reason 0x%02x", conn, reason); |
| |
| unicast_client_ep_reset(conn); |
| unicast_client_pac_reset(conn); |
| } |
| |
| static struct bt_conn_cb conn_cbs = { |
| .disconnected = unicast_client_disconnected, |
| }; |
| |
| int bt_audio_discover(struct bt_conn *conn, |
| struct bt_audio_discover_params *params) |
| { |
| static bool conn_cb_registered; |
| uint8_t role; |
| |
| if (!conn || conn->state != BT_CONN_CONNECTED) { |
| return -ENOTCONN; |
| } |
| |
| role = conn->role; |
| if (role != BT_HCI_ROLE_CENTRAL) { |
| BT_DBG("Invalid conn role: %u, shall be central", role); |
| return -EINVAL; |
| } |
| |
| if (params->dir == BT_AUDIO_DIR_SINK) { |
| params->read.by_uuid.uuid = snk_uuid; |
| } else if (params->dir == BT_AUDIO_DIR_SOURCE) { |
| params->read.by_uuid.uuid = src_uuid; |
| } else { |
| return -EINVAL; |
| } |
| |
| params->read.func = unicast_client_read_func; |
| params->read.handle_count = 0u; |
| |
| if (!params->read.by_uuid.start_handle) { |
| params->read.by_uuid.start_handle = BT_ATT_FIRST_ATTRIBUTE_HANDLE; |
| } |
| |
| if (!params->read.by_uuid.end_handle) { |
| params->read.by_uuid.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE; |
| } |
| |
| if (!conn_cb_registered) { |
| bt_conn_cb_register(&conn_cbs); |
| conn_cb_registered = true; |
| } |
| |
| params->num_caps = 0u; |
| params->num_eps = 0u; |
| |
| return bt_gatt_read(conn, ¶ms->read); |
| } |
| |
| #endif /* CONFIG_BT_AUDIO_UNICAST_CLIENT */ |