/* @file * @brief Bluetooth Unicast Client */ /* * Copyright (c) 2020 Intel Corporation * Copyright (c) 2022-2023 Nordic Semiconductor ASA * * SPDX-License-Identifier: Apache-2.0 */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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]; 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; bool busy; 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; } 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->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); client->busy = false; reset_att_buf(client); 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); client->busy = false; 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); client->busy = false; 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 busy %u", conn, &client_ep->ep, client_ep->handle, client->busy); if (client->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); } else { client->busy = true; } } 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 (!client->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); client->busy = false; 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 (client->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); client->busy = false; /* 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 (client->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); } client->busy = true; } 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]; client->busy = false; 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_audio_codec_qos *qos, const struct bt_bap_unicast_group_param *group_param) { cig_param->framing = qos->framing; cig_param->packing = BT_ISO_PACKING_SEQUENTIAL; /* TODO: Add to QoS struct */ cig_param->c_to_p_interval = qos->interval; cig_param->p_to_c_interval = qos->interval; cig_param->c_to_p_latency = qos->latency; cig_param->p_to_c_latency = qos->latency; cig_param->sca = BT_GAP_SCA_UNKNOWN; if (group_param != NULL) { cig_param->packing = group_param->packing; #if defined(CONFIG_BT_ISO_TEST_PARAMS) cig_param->c_to_p_ft = group_param->c_to_p_ft; cig_param->p_to_c_ft = group_param->p_to_c_ft; cig_param->iso_interval = group_param->iso_interval; #endif /* CONFIG_BT_ISO_TEST_PARAMS */ } } /* FIXME: Remove `qos` parameter. Some of the QoS related CIG can be different * between CIS'es. The implementation shall take the CIG parameters from * unicast_group instead. */ static int bt_audio_cig_create(struct bt_bap_unicast_group *group, const struct bt_audio_codec_qos *qos, const struct bt_bap_unicast_group_param *group_param) { struct bt_iso_cig_param param; uint8_t cis_count; int err; LOG_DBG("group %p qos %p", group, qos); 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(¶m, qos, group_param); err = bt_iso_cig_create(¶m, &group->cig); if (err != 0) { LOG_ERR("bt_iso_cig_create failed: %d", err); return err; } group->qos = qos; return 0; } static int bt_audio_cig_reconfigure(struct bt_bap_unicast_group *group, const struct bt_audio_codec_qos *qos) { struct bt_iso_cig_param param; uint8_t cis_count; int err; LOG_DBG("group %p qos %p", group, qos); 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(¶m, qos, NULL); err = bt_iso_cig_reconfigure(group->cig, ¶m); if (err != 0) { LOG_ERR("bt_iso_cig_create failed: %d", err); return err; } group->qos = qos; 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_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); } /* Store the Codec QoS in the bap_iso */ unicast_client_codec_qos_to_iso_qos(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; } static int group_qos_common_set(const struct bt_audio_codec_qos **group_qos, const struct bt_bap_unicast_group_stream_pair_param *param) { if (param->rx_param != NULL && *group_qos == NULL) { *group_qos = param->rx_param->qos; } if (param->tx_param != NULL && *group_qos == NULL) { *group_qos = param->tx_param->qos; } return 0; } 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; const struct bt_audio_codec_qos *group_qos = NULL; 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; CHECKIF(param == NULL) { LOG_DBG("streams is NULL"); return -EINVAL; } CHECKIF(param->params_count > UNICAST_GROUP_STREAM_CNT) { LOG_DBG("Too many streams provided: %u/%u", param->params_count, UNICAST_GROUP_STREAM_CNT); return -EINVAL; } unicast_group = unicast_group_alloc(); if (unicast_group == NULL) { LOG_DBG("Could not allocate any more unicast groups"); return -ENOMEM; } for (size_t i = 0U; i < param->params_count; i++) { struct bt_bap_unicast_group_stream_pair_param *stream_param; stream_param = ¶m->params[i]; err = stream_pair_param_check(stream_param); if (err < 0) { return err; } err = group_qos_common_set(&group_qos, 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); unicast_group_free(unicast_group); return err; } } err = bt_audio_cig_create(unicast_group, group_qos, param); 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_add_streams(struct bt_bap_unicast_group *unicast_group, struct bt_bap_unicast_group_stream_pair_param params[], size_t num_param) { const struct bt_audio_codec_qos *group_qos = unicast_group->qos; 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; } 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; } /* We can just check the CIG state to see if any streams have started as * that would start the ISO connection procedure */ cig = unicast_group->cig; if (cig != NULL && cig->state != BT_ISO_CIG_STATE_CONFIGURED) { LOG_DBG("At least one unicast group stream is started"); return -EBADMSG; } for (num_added = 0U; num_added < num_param; num_added++) { struct bt_bap_unicast_group_stream_pair_param *stream_param; stream_param = ¶ms[num_added]; err = stream_pair_param_check(stream_param); if (err < 0) { return err; } err = group_qos_common_set(&group_qos, 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, group_qos); if (err != 0) { LOG_DBG("bt_audio_cig_reconfigure failed: %d", err); goto fail; } return 0; fail: /* Restore group by removing the newly added streams */ while (num_added--) { unicast_group_del_stream_pair(unicast_group, ¶ms[num_added]); } return err; } int bt_bap_unicast_group_delete(struct bt_bap_unicast_group *unicast_group) { struct bt_bap_stream *stream; CHECKIF(unicast_group == NULL) { LOG_DBG("unicast_group is NULL"); return -EINVAL; } SYS_SLIST_FOR_EACH_CONTAINER(&unicast_group->streams, stream, _node) { /* If a stream has an endpoint, it is not ready to be removed * from a group, as it is not in an idle state */ if (stream->ep != NULL) { LOG_DBG("stream %p is not released", stream); return -EINVAL; } } if (unicast_group->cig != NULL) { const int err = unicast_client_cig_terminate(unicast_group); if (err != 0) { LOG_DBG("unicast_client_cig_terminate failed with err %d", err); return err; } } unicast_group_free(unicast_group); return 0; } int bt_bap_unicast_client_config(struct bt_bap_stream *stream, const struct bt_audio_codec_cfg *codec_cfg) { struct bt_bap_ep *ep = stream->ep; struct bt_ascs_config_op *op; struct net_buf_simple *buf; int err; LOG_DBG("stream %p", stream); if (stream->conn == NULL) { LOG_DBG("Stream %p does not have a connection", stream); return -ENOTCONN; } buf = bt_bap_unicast_client_ep_create_pdu(stream->conn, BT_ASCS_CONFIG_OP); if (buf == NULL) { LOG_DBG("Could not create PDU"); return -EBUSY; } op = net_buf_simple_add(buf, sizeof(*op)); op->num_ases = 0x01; err = unicast_client_ep_config(ep, buf, codec_cfg); if (err != 0) { return err; } err = bt_bap_unicast_client_ep_send(stream->conn, ep, buf); if (err != 0) { return err; } return 0; } int bt_bap_unicast_client_qos(struct bt_conn *conn, struct bt_bap_unicast_group *group) { struct bt_bap_stream *stream; struct bt_ascs_config_op *op; struct net_buf_simple *buf; struct bt_bap_ep *ep; bool conn_stream_found; int err; if (conn == NULL) { LOG_DBG("conn is NULL"); return -ENOTCONN; } /* Used to determine if a stream for the supplied connection pointer * was actually found */ conn_stream_found = false; /* Validate streams before starting the QoS execution */ SYS_SLIST_FOR_EACH_CONTAINER(&group->streams, stream, _node) { if (stream->conn != conn) { /* Channel not part of this ACL, skip */ continue; } conn_stream_found = true; ep = stream->ep; if (ep == NULL) { LOG_DBG("stream->ep is NULL"); return -EINVAL; } /* Can only be done if all the streams are in the codec * configured state or the QoS configured state */ switch (ep->status.state) { case BT_BAP_EP_STATE_CODEC_CONFIGURED: case BT_BAP_EP_STATE_QOS_CONFIGURED: break; default: LOG_DBG("Invalid state: %s", bt_bap_ep_state_str(stream->ep->status.state)); return -EINVAL; } if (bt_bap_stream_verify_qos(stream, stream->qos) != BT_BAP_ASCS_REASON_NONE) { return -EINVAL; } /* Verify ep->dir */ switch (ep->dir) { case BT_AUDIO_DIR_SINK: case BT_AUDIO_DIR_SOURCE: break; default: __ASSERT(false, "invalid endpoint dir: %u", ep->dir); return -EINVAL; } if (stream->bap_iso == NULL) { /* This can only happen if the stream was somehow added * to a group without the bap_iso being bound to it */ LOG_ERR("Could not find bap_iso for stream %p", stream); return -EINVAL; } } if (!conn_stream_found) { LOG_DBG("No streams in the group %p for conn %p", group, conn); return -EINVAL; } /* Generate the control point write */ buf = bt_bap_unicast_client_ep_create_pdu(conn, BT_ASCS_QOS_OP); if (buf == NULL) { LOG_DBG("Could not create PDU"); return -EBUSY; } op = net_buf_simple_add(buf, sizeof(*op)); (void)memset(op, 0, sizeof(*op)); ep = NULL; /* Needed to find the control point handle */ SYS_SLIST_FOR_EACH_CONTAINER(&group->streams, stream, _node) { if (stream->conn != conn) { /* Channel not part of this ACL, skip */ continue; } op->num_ases++; if (stream->ep->iso == NULL) { /* Not yet bound with the bap_iso */ bt_bap_iso_bind_ep(stream->bap_iso, stream->ep); } err = bt_bap_unicast_client_ep_qos(stream->ep, buf, stream->qos); if (err != 0) { audio_stream_qos_cleanup(conn, group); return err; } if (ep == NULL) { ep = stream->ep; } } err = bt_bap_unicast_client_ep_send(conn, ep, buf); if (err != 0) { LOG_DBG("Could not send config QoS: %d", err); audio_stream_qos_cleanup(conn, group); return err; } return 0; } int bt_bap_unicast_client_enable(struct bt_bap_stream *stream, const uint8_t meta[], size_t meta_len) { struct bt_bap_ep *ep = stream->ep; struct net_buf_simple *buf; struct bt_ascs_enable_op *req; int err; LOG_DBG("stream %p", stream); if (stream->conn == NULL) { LOG_DBG("Stream %p does not have a connection", stream); return -ENOTCONN; } buf = bt_bap_unicast_client_ep_create_pdu(stream->conn, BT_ASCS_ENABLE_OP); if (buf == NULL) { LOG_DBG("Could not create PDU"); return -EBUSY; } req = net_buf_simple_add(buf, sizeof(*req)); req->num_ases = 0x01; err = unicast_client_ep_enable(ep, buf, meta, meta_len); if (err) { return err; } return bt_bap_unicast_client_ep_send(stream->conn, ep, buf); } int bt_bap_unicast_client_metadata(struct bt_bap_stream *stream, const uint8_t meta[], size_t meta_len) { struct bt_bap_ep *ep = stream->ep; struct net_buf_simple *buf; struct bt_ascs_enable_op *req; int err; LOG_DBG("stream %p", stream); if (stream->conn == NULL) { LOG_DBG("Stream %p does not have a connection", stream); return -ENOTCONN; } buf = bt_bap_unicast_client_ep_create_pdu(stream->conn, BT_ASCS_METADATA_OP); if (buf == NULL) { LOG_DBG("Could not create PDU"); return -EBUSY; } req = net_buf_simple_add(buf, sizeof(*req)); req->num_ases = 0x01; err = unicast_client_ep_metadata(ep, buf, meta, meta_len); if (err) { return err; } return bt_bap_unicast_client_ep_send(stream->conn, ep, buf); } int bt_bap_unicast_client_connect(struct bt_bap_stream *stream) { struct bt_iso_connect_param param; struct bt_iso_chan *iso_chan; enum bt_iso_state iso_state; LOG_DBG("stream %p", stream); if (stream == NULL) { LOG_DBG("stream is NULL"); return -EINVAL; } iso_chan = bt_bap_stream_iso_chan_get(stream); if (iso_chan == NULL) { LOG_DBG("iso_chan is NULL"); return -EINVAL; } LOG_DBG("stream %p iso %p", stream, iso_chan); param.acl = stream->conn; param.iso_chan = iso_chan; iso_state = iso_chan->state; if (iso_state == BT_ISO_STATE_DISCONNECTED) { const int err = bt_iso_chan_connect(¶m, 1); if (err == 0 || err == -EBUSY) { /* Expected / known return values */ return err; } /* unknown return value*/ LOG_DBG("Unexpected err %d from bt_iso_chan_connect", err); return -ENOEXEC; } else if (iso_state == BT_ISO_STATE_CONNECTING || iso_state == BT_ISO_STATE_CONNECTED) { LOG_DBG("iso_chan %p already connecting or connected (%u)", iso_chan, iso_state); return -EALREADY; } LOG_DBG("iso_chan %p in invalid state state (%u), cannot connect", iso_chan, iso_state); return -EBADMSG; } int bt_bap_unicast_client_start(struct bt_bap_stream *stream) { struct bt_bap_ep *ep = stream->ep; int err; LOG_DBG("stream %p", stream); if (stream->conn == NULL) { LOG_DBG("Stream %p does not have a connection", stream); return -ENOTCONN; } /* As per the ASCS spec, only source streams can be started by the client */ if (ep->dir == BT_AUDIO_DIR_SINK) { LOG_DBG("Stream %p is not a source stream", stream); return -EINVAL; } ep->receiver_ready = true; err = unicast_client_send_start(ep); if (err != 0) { LOG_DBG("Failed to send start: %d", err); /* TBD: Should we release the stream then? */ ep->receiver_ready = false; return err; } return 0; } int bt_bap_unicast_client_disable(struct bt_bap_stream *stream) { struct bt_bap_ep *ep = stream->ep; struct net_buf_simple *buf; struct bt_ascs_disable_op *req; int err; LOG_DBG("stream %p", stream); if (stream->conn == NULL) { LOG_DBG("Stream %p does not have a connection", stream); return -ENOTCONN; } buf = bt_bap_unicast_client_ep_create_pdu(stream->conn, BT_ASCS_DISABLE_OP); if (buf == NULL) { LOG_DBG("Could not create PDU"); return -EBUSY; } req = net_buf_simple_add(buf, sizeof(*req)); req->num_ases = 0x01; err = unicast_client_ep_disable(ep, buf); if (err) { return err; } return bt_bap_unicast_client_ep_send(stream->conn, ep, buf); } int bt_bap_unicast_client_stop(struct bt_bap_stream *stream) { struct bt_bap_ep *ep = stream->ep; 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 (client->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) { return err; } client->dir = dir; client->busy = true; 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; }