| /* @file |
| * @brief Bluetooth ASCS |
| */ |
| /* |
| * Copyright (c) 2020 Intel Corporation |
| * Copyright (c) 2022 Nordic Semiconductor ASA |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #include <zephyr.h> |
| #include <sys/byteorder.h> |
| #include <sys/check.h> |
| |
| #include <device.h> |
| #include <init.h> |
| |
| #include <bluetooth/bluetooth.h> |
| #include <bluetooth/conn.h> |
| #include <bluetooth/gatt.h> |
| #include <bluetooth/audio/audio.h> |
| |
| #define BT_DBG_ENABLED IS_ENABLED(CONFIG_BT_DEBUG_ASCS) |
| #define LOG_MODULE_NAME bt_ascs |
| #include "common/log.h" |
| |
| #include "../host/hci_core.h" |
| #include "../host/conn_internal.h" |
| |
| #include "endpoint.h" |
| #include "unicast_server.h" |
| #include "pacs_internal.h" |
| |
| #if defined(CONFIG_BT_AUDIO_UNICAST_SERVER) |
| |
| #define ASE_ID(_ase) ase->ep.status.id |
| #define ASE_DIR(_id) \ |
| (_id > CONFIG_BT_ASCS_ASE_SNK_COUNT ? BT_AUDIO_SOURCE : BT_AUDIO_SINK) |
| #define ASE_UUID(_id) \ |
| (_id > CONFIG_BT_ASCS_ASE_SNK_COUNT ? BT_UUID_ASCS_ASE_SRC : BT_UUID_ASCS_ASE_SNK) |
| #define ASE_COUNT (CONFIG_BT_ASCS_ASE_SNK_COUNT + CONFIG_BT_ASCS_ASE_SRC_COUNT) |
| |
| struct bt_ascs_ase { |
| struct bt_ascs *ascs; |
| struct bt_audio_ep ep; |
| struct k_work work; |
| }; |
| |
| struct bt_ascs { |
| struct bt_conn *conn; |
| uint8_t id; |
| bt_addr_le_t peer; |
| struct bt_ascs_ase ases[ASE_COUNT]; |
| struct bt_gatt_notify_params params; |
| uint16_t handle; |
| }; |
| |
| static struct bt_ascs sessions[CONFIG_BT_MAX_CONN]; |
| |
| static void ase_status_changed(struct bt_audio_ep *ep, uint8_t old_state, |
| uint8_t state) |
| { |
| struct bt_ascs_ase *ase = CONTAINER_OF(ep, struct bt_ascs_ase, ep); |
| |
| BT_DBG("ase %p conn %p", ase, ase->ascs->conn); |
| |
| if (ase->ascs->conn == NULL || |
| ase->ascs->conn->state != BT_CONN_CONNECTED) { |
| return; |
| } |
| |
| k_work_submit(&ase->work); |
| } |
| |
| void ascs_ep_set_state(struct bt_audio_ep *ep, uint8_t state) |
| { |
| struct bt_audio_stream *stream; |
| uint8_t old_state; |
| |
| if (!ep) { |
| return; |
| } |
| |
| /* TODO: Verify state changes */ |
| |
| old_state = ep->status.state; |
| ep->status.state = state; |
| |
| BT_DBG("ep %p id 0x%02x %s -> %s", ep, ep->status.id, |
| bt_audio_ep_state_str(old_state), |
| bt_audio_ep_state_str(state)); |
| |
| /* Notify clients*/ |
| ase_status_changed(ep, old_state, state); |
| |
| if (ep->stream == NULL || old_state == state) { |
| return; |
| } |
| |
| stream = ep->stream; |
| |
| if (stream->ops != NULL) { |
| const struct bt_audio_stream_ops *ops = stream->ops; |
| |
| switch (state) { |
| case BT_AUDIO_EP_STATE_IDLE: |
| if (ops->released != NULL) { |
| ops->released(stream); |
| } |
| break; |
| case BT_AUDIO_EP_STATE_CODEC_CONFIGURED: |
| if (ops->configured != NULL) { |
| ops->configured(stream, &ep->qos_pref); |
| } |
| break; |
| case BT_AUDIO_EP_STATE_QOS_CONFIGURED: |
| if (ops->qos_set != NULL) { |
| ops->qos_set(stream); |
| } |
| break; |
| case BT_AUDIO_EP_STATE_ENABLING: |
| if (ops->enabled != NULL) { |
| ops->enabled(stream); |
| } |
| break; |
| case BT_AUDIO_EP_STATE_STREAMING: |
| if (ops->started != NULL) { |
| ops->started(stream); |
| } |
| break; |
| case BT_AUDIO_EP_STATE_DISABLING: |
| if (ops->disabled != NULL) { |
| ops->disabled(stream); |
| } |
| break; |
| default: |
| BT_ERR("Invalid state: %u", state); |
| break; |
| } |
| } |
| |
| if (state == BT_AUDIO_EP_STATE_IDLE) { |
| bt_audio_stream_detach(ep->stream); |
| } |
| } |
| |
| static void ascs_codec_data_add(struct net_buf_simple *buf, const char *prefix, |
| uint8_t num, struct bt_codec_data *data) |
| { |
| struct bt_ascs_codec_config *cc; |
| int i; |
| |
| for (i = 0; i < num; i++) { |
| struct bt_data *d = &data[i].data; |
| |
| BT_DBG("#%u: %s type 0x%02x len %u", i, prefix, d->type, |
| d->data_len); |
| BT_HEXDUMP_DBG(d->data, d->data_len, prefix); |
| |
| cc = net_buf_simple_add(buf, sizeof(*cc)); |
| cc->len = d->data_len + sizeof(cc->type); |
| cc->type = d->type; |
| net_buf_simple_add_mem(buf, d->data, d->data_len); |
| } |
| } |
| |
| static void ascs_ep_get_status_config(struct bt_audio_ep *ep, |
| struct net_buf_simple *buf) |
| { |
| struct bt_ascs_ase_status_config *cfg; |
| struct bt_codec_qos_pref *pref = &ep->qos_pref; |
| |
| cfg = net_buf_simple_add(buf, sizeof(*cfg)); |
| cfg->framing = pref->unframed_supported ? BT_ASCS_QOS_FRAMING_UNFRAMED |
| : BT_ASCS_QOS_FRAMING_FRAMED; |
| cfg->phy = pref->phy; |
| cfg->rtn = pref->rtn; |
| cfg->latency = sys_cpu_to_le16(pref->latency); |
| sys_put_le24(pref->pd_min, cfg->pd_min); |
| sys_put_le24(pref->pd_max, cfg->pd_max); |
| sys_put_le24(pref->pref_pd_min, cfg->prefer_pd_min); |
| sys_put_le24(pref->pref_pd_max, cfg->prefer_pd_max); |
| cfg->codec.id = ep->codec.id; |
| cfg->codec.cid = sys_cpu_to_le16(ep->codec.cid); |
| cfg->codec.vid = sys_cpu_to_le16(ep->codec.vid); |
| |
| BT_DBG("dir 0x%02x unframed_supported 0x%02x phy 0x%02x rtn %u " |
| "latency %u pd_min %u pd_max %u codec 0x%02x", |
| ep->dir, pref->unframed_supported, pref->phy, |
| pref->rtn, pref->latency, pref->pd_min, pref->pd_max, |
| ep->stream->codec->id); |
| |
| cfg->cc_len = buf->len; |
| ascs_codec_data_add(buf, "data", ep->codec.data_count, ep->codec.data); |
| cfg->cc_len = buf->len - cfg->cc_len; |
| } |
| |
| static void ascs_ep_get_status_qos(struct bt_audio_ep *ep, |
| struct net_buf_simple *buf) |
| { |
| struct bt_ascs_ase_status_qos *qos; |
| |
| qos = net_buf_simple_add(buf, sizeof(*qos)); |
| qos->cig_id = ep->cig_id; |
| qos->cis_id = ep->cis_id; |
| sys_put_le24(ep->stream->qos->interval, qos->interval); |
| qos->framing = ep->stream->qos->framing; |
| qos->phy = ep->stream->qos->phy; |
| qos->sdu = sys_cpu_to_le16(ep->stream->qos->sdu); |
| qos->rtn = ep->stream->qos->rtn; |
| qos->latency = sys_cpu_to_le16(ep->stream->qos->latency); |
| sys_put_le24(ep->stream->qos->pd, qos->pd); |
| |
| BT_DBG("dir 0x%02x codec 0x%02x interval %u framing 0x%02x phy 0x%02x " |
| "rtn %u latency %u pd %u", |
| ep->dir, ep->stream->codec->id, |
| ep->stream->qos->interval, ep->stream->qos->framing, |
| ep->stream->qos->phy, ep->stream->qos->rtn, |
| ep->stream->qos->latency, ep->stream->qos->pd); |
| } |
| |
| static void ascs_ep_get_status_enable(struct bt_audio_ep *ep, |
| struct net_buf_simple *buf) |
| { |
| struct bt_ascs_ase_status_enable *enable; |
| |
| enable = net_buf_simple_add(buf, sizeof(*enable)); |
| enable->cig_id = ep->cig_id; |
| enable->cis_id = ep->cis_id; |
| |
| enable->metadata_len = buf->len; |
| ascs_codec_data_add(buf, "meta", ep->codec.meta_count, ep->codec.meta); |
| enable->metadata_len = buf->len - enable->metadata_len; |
| |
| BT_DBG("dir 0x%02x cig 0x%02x cis 0x%02x", |
| ep->dir, ep->cig_id, ep->cis_id); |
| } |
| |
| static int ascs_ep_get_status(struct bt_audio_ep *ep, |
| struct net_buf_simple *buf) |
| { |
| struct bt_ascs_ase_status *status; |
| |
| if (!ep || !buf) { |
| return -EINVAL; |
| } |
| |
| BT_DBG("ep %p id 0x%02x state %s", ep, ep->status.id, |
| bt_audio_ep_state_str(ep->status.state)); |
| |
| /* Reset if buffer before using */ |
| net_buf_simple_reset(buf); |
| |
| status = net_buf_simple_add_mem(buf, &ep->status, |
| sizeof(ep->status)); |
| |
| switch (ep->status.state) { |
| case BT_AUDIO_EP_STATE_IDLE: |
| /* Fallthrough */ |
| case BT_AUDIO_EP_STATE_RELEASING: |
| break; |
| case BT_AUDIO_EP_STATE_CODEC_CONFIGURED: |
| ascs_ep_get_status_config(ep, buf); |
| break; |
| case BT_AUDIO_EP_STATE_QOS_CONFIGURED: |
| ascs_ep_get_status_qos(ep, buf); |
| break; |
| case BT_AUDIO_EP_STATE_ENABLING: |
| /* Fallthrough */ |
| case BT_AUDIO_EP_STATE_STREAMING: |
| /* Fallthrough */ |
| case BT_AUDIO_EP_STATE_DISABLING: |
| ascs_ep_get_status_enable(ep, buf); |
| break; |
| default: |
| BT_ERR("Invalid Endpoint state"); |
| break; |
| } |
| |
| return 0; |
| } |
| |
| static void ascs_iso_recv(struct bt_iso_chan *chan, |
| const struct bt_iso_recv_info *info, |
| struct net_buf *buf) |
| { |
| struct bt_audio_ep *ep = CONTAINER_OF(chan, struct bt_audio_ep, iso); |
| struct bt_audio_stream_ops *ops = ep->stream->ops; |
| |
| BT_DBG("stream %p ep %p len %zu", chan, ep, net_buf_frags_len(buf)); |
| |
| if (ops != NULL && ops->recv != NULL) { |
| ops->recv(ep->stream, buf); |
| } else { |
| BT_WARN("No callback for recv set"); |
| } |
| } |
| |
| static void ascs_iso_sent(struct bt_iso_chan *chan) |
| { |
| struct bt_audio_ep *ep = CONTAINER_OF(chan, struct bt_audio_ep, iso); |
| struct bt_audio_stream_ops *ops = ep->stream->ops; |
| |
| BT_DBG("stream %p ep %p", chan, ep); |
| |
| if (ops != NULL && ops->sent != NULL) { |
| ops->sent(ep->stream); |
| } |
| } |
| |
| static void ascs_iso_connected(struct bt_iso_chan *chan) |
| { |
| struct bt_audio_ep *ep = CONTAINER_OF(chan, struct bt_audio_ep, iso); |
| |
| BT_DBG("stream %p ep %p type %u", chan, ep, ep != NULL ? ep->type : 0); |
| |
| if (ep->status.state != BT_AUDIO_EP_STATE_ENABLING) { |
| BT_DBG("endpoint not in enabling state: %s", |
| bt_audio_ep_state_str(ep->status.state)); |
| return; |
| } |
| |
| ascs_ep_set_state(ep, BT_AUDIO_EP_STATE_STREAMING); |
| } |
| |
| static void ascs_iso_disconnected(struct bt_iso_chan *chan, uint8_t reason) |
| { |
| struct bt_audio_ep *ep = CONTAINER_OF(chan, struct bt_audio_ep, iso); |
| struct bt_audio_stream *stream = ep->stream; |
| struct bt_audio_stream_ops *ops = stream->ops; |
| |
| BT_DBG("stream %p ep %p reason 0x%02x", chan, ep, reason); |
| |
| if (ops != NULL && ops->stopped != NULL) { |
| ops->stopped(stream); |
| } else { |
| BT_WARN("No callback for stopped set"); |
| } |
| |
| if (ep->status.state == BT_AUDIO_EP_STATE_RELEASING) { |
| ascs_ep_set_state(ep, BT_AUDIO_EP_STATE_CODEC_CONFIGURED); |
| } else { |
| int err; |
| |
| /* The ASE state machine goes into different states from this operation |
| * based on whether it is a source or a sink ASE. |
| */ |
| if (ep->dir == BT_AUDIO_SOURCE) { |
| ascs_ep_set_state(ep, BT_AUDIO_EP_STATE_DISABLING); |
| } else { |
| ascs_ep_set_state(ep, BT_AUDIO_EP_STATE_QOS_CONFIGURED); |
| } |
| |
| err = bt_audio_stream_iso_listen(stream); |
| if (err != 0) { |
| BT_ERR("Could not make stream listen: %d", err); |
| } |
| } |
| } |
| |
| static struct bt_iso_chan_ops ascs_iso_ops = { |
| .recv = ascs_iso_recv, |
| .sent = ascs_iso_sent, |
| .connected = ascs_iso_connected, |
| .disconnected = ascs_iso_disconnected, |
| }; |
| |
| static void ascs_ase_cfg_changed(const struct bt_gatt_attr *attr, |
| uint16_t value) |
| { |
| BT_DBG("attr %p value 0x%04x", attr, value); |
| } |
| |
| NET_BUF_SIMPLE_DEFINE_STATIC(rsp_buf, CONFIG_BT_L2CAP_TX_MTU); |
| |
| static void ascs_cp_rsp_alloc(uint8_t op) |
| { |
| struct bt_ascs_cp_rsp *rsp; |
| |
| rsp = net_buf_simple_add(&rsp_buf, sizeof(*rsp)); |
| rsp->op = op; |
| rsp->num_ase = 0; |
| } |
| |
| /* Add response to an opcode/ASE ID */ |
| static void ascs_cp_rsp_add(uint8_t id, uint8_t op, uint8_t code, |
| uint8_t reason) |
| { |
| struct bt_ascs_cp_rsp *rsp = (void *)rsp_buf.__buf; |
| struct bt_ascs_cp_ase_rsp *ase_rsp; |
| |
| BT_DBG("id 0x%02x op %s (0x%02x) code %s (0x%02x) reason %s (0x%02x)", |
| id, bt_ascs_op_str(op), op, bt_ascs_rsp_str(code), code, |
| bt_ascs_reason_str(reason), reason); |
| |
| /* Allocate response if buffer is empty */ |
| if (!rsp_buf.len) { |
| ascs_cp_rsp_alloc(op); |
| } |
| |
| if (rsp->num_ase == 0xff) { |
| return; |
| } |
| |
| switch (code) { |
| /* If the Response_Code value is 0x01 or 0x02, Number_of_ASEs shall be |
| * set to 0xFF. |
| */ |
| case BT_ASCS_RSP_NOT_SUPPORTED: |
| case BT_ASCS_RSP_TRUNCATED: |
| rsp->num_ase = 0xff; |
| break; |
| default: |
| rsp->num_ase++; |
| break; |
| } |
| |
| ase_rsp = net_buf_simple_add(&rsp_buf, sizeof(*ase_rsp)); |
| ase_rsp->id = id; |
| ase_rsp->code = code; |
| ase_rsp->reason = reason; |
| } |
| |
| static void ascs_cp_rsp_add_errno(uint8_t id, uint8_t op, int err, |
| uint8_t reason) |
| { |
| switch (err) { |
| case -ENOBUFS: |
| case -ENOMEM: |
| return ascs_cp_rsp_add(id, op, BT_ASCS_RSP_NO_MEM, |
| BT_ASCS_REASON_NONE); |
| case -EINVAL: |
| switch (op) { |
| case BT_ASCS_CONFIG_OP: |
| /* Fallthrough */ |
| case BT_ASCS_QOS_OP: |
| return ascs_cp_rsp_add(id, op, |
| BT_ASCS_RSP_CONF_INVALID, |
| reason); |
| case BT_ASCS_ENABLE_OP: |
| /* Fallthrough */ |
| case BT_ASCS_METADATA_OP: |
| return ascs_cp_rsp_add(id, op, |
| BT_ASCS_RSP_METADATA_INVALID, |
| reason); |
| default: |
| return ascs_cp_rsp_add(id, op, BT_ASCS_RSP_UNSPECIFIED, |
| BT_ASCS_REASON_NONE); |
| } |
| case -ENOTSUP: |
| switch (op) { |
| case BT_ASCS_CONFIG_OP: |
| /* Fallthrough */ |
| case BT_ASCS_QOS_OP: |
| return ascs_cp_rsp_add(id, op, |
| BT_ASCS_RSP_CONF_UNSUPPORTED, |
| reason); |
| case BT_ASCS_ENABLE_OP: |
| /* Fallthrough */ |
| case BT_ASCS_METADATA_OP: |
| return ascs_cp_rsp_add(id, op, |
| BT_ASCS_RSP_METADATA_UNSUPPORTED, |
| reason); |
| default: |
| return ascs_cp_rsp_add(id, op, |
| BT_ASCS_RSP_NOT_SUPPORTED, |
| BT_ASCS_REASON_NONE); |
| } |
| case -EBADMSG: |
| return ascs_cp_rsp_add(id, op, BT_ASCS_RSP_INVALID_ASE_STATE, |
| BT_ASCS_REASON_NONE); |
| default: |
| return ascs_cp_rsp_add(id, op, BT_ASCS_RSP_UNSPECIFIED, |
| BT_ASCS_REASON_NONE); |
| } |
| } |
| |
| static void ascs_cp_rsp_success(uint8_t id, uint8_t op) |
| { |
| ascs_cp_rsp_add(id, op, BT_ASCS_RSP_SUCCESS, BT_ASCS_REASON_NONE); |
| } |
| |
| /* Notify response to control point */ |
| static void ascs_cp_notify(struct bt_ascs *ascs) |
| { |
| struct bt_gatt_attr attr; |
| |
| BT_DBG("ascs %p handle 0x%04x len %u", ascs, ascs->handle, rsp_buf.len); |
| |
| memset(&attr, 0, sizeof(attr)); |
| attr.handle = ascs->handle; |
| attr.uuid = BT_UUID_ASCS_ASE_CP; |
| |
| bt_gatt_notify(ascs->conn, &attr, rsp_buf.data, rsp_buf.len); |
| } |
| |
| static void ase_release(struct bt_ascs_ase *ase, bool cache) |
| { |
| int err; |
| |
| BT_DBG("ase %p", ase); |
| |
| if (unicast_server_cb != NULL && unicast_server_cb->release != NULL) { |
| err = unicast_server_cb->release(ase->ep.stream); |
| } else { |
| err = -ENOTSUP; |
| } |
| |
| if (err) { |
| ascs_cp_rsp_add_errno(ASE_ID(ase), BT_ASCS_RELEASE_OP, err, |
| BT_ASCS_REASON_NONE); |
| return; |
| } |
| |
| if (cache) { |
| ascs_ep_set_state(&ase->ep, BT_AUDIO_EP_STATE_CODEC_CONFIGURED); |
| } else { |
| ascs_ep_set_state(&ase->ep, BT_AUDIO_EP_STATE_RELEASING); |
| } |
| |
| ascs_cp_rsp_success(ASE_ID(ase), BT_ASCS_RELEASE_OP); |
| } |
| |
| static void ascs_clear(struct bt_ascs *ascs) |
| { |
| int i; |
| |
| BT_DBG("ascs %p", ascs); |
| |
| bt_addr_le_copy(&ascs->peer, BT_ADDR_LE_ANY); |
| |
| for (i = 0; i < ASE_COUNT; i++) { |
| struct bt_ascs_ase *ase = &ascs->ases[i]; |
| |
| if (ase->ep.status.state != BT_AUDIO_EP_STATE_IDLE) { |
| ase_release(ase, false); |
| ascs_ep_set_state(&ase->ep, BT_AUDIO_EP_STATE_IDLE); |
| } |
| } |
| |
| bt_conn_unref(ascs->conn); |
| ascs->conn = NULL; |
| } |
| |
| static void ase_disable(struct bt_ascs_ase *ase) |
| { |
| struct bt_audio_stream *stream; |
| struct bt_audio_ep *ep; |
| int err; |
| |
| BT_DBG("ase %p", ase); |
| |
| ep = &ase->ep; |
| |
| switch (ep->status.state) { |
| /* Valid only if ASE_State field = 0x03 (Enabling) */ |
| case BT_AUDIO_EP_STATE_ENABLING: |
| /* or 0x04 (Streaming) */ |
| case BT_AUDIO_EP_STATE_STREAMING: |
| break; |
| default: |
| BT_ERR("Invalid state: %s", |
| bt_audio_ep_state_str(ep->status.state)); |
| ascs_cp_rsp_add_errno(ASE_ID(ase), BT_ASCS_DISABLE_OP, |
| -EBADMSG, BT_ASCS_REASON_NONE); |
| return; |
| } |
| |
| stream = ep->stream; |
| |
| if (unicast_server_cb != NULL && unicast_server_cb->disable != NULL) { |
| err = unicast_server_cb->disable(stream); |
| } else { |
| err = -ENOTSUP; |
| } |
| |
| if (err) { |
| BT_ERR("Disable failed: %d", err); |
| ascs_cp_rsp_add_errno(ASE_ID(ase), BT_ASCS_DISABLE_OP, |
| err, BT_ASCS_REASON_NONE); |
| return; |
| } |
| |
| /* The ASE state machine goes into different states from this operation |
| * based on whether it is a source or a sink ASE. |
| */ |
| if (ep->dir == BT_AUDIO_SOURCE) { |
| ascs_ep_set_state(ep, BT_AUDIO_EP_STATE_DISABLING); |
| } else { |
| ascs_ep_set_state(ep, BT_AUDIO_EP_STATE_QOS_CONFIGURED); |
| } |
| |
| ascs_cp_rsp_success(ASE_ID(ase), BT_ASCS_DISABLE_OP); |
| } |
| |
| static void ascs_detach(struct bt_ascs *ascs) |
| { |
| int i; |
| |
| BT_DBG("ascs %p conn %p", ascs, ascs->conn); |
| |
| /* Update address in case it has changed */ |
| ascs->id = ascs->conn->id; |
| bt_addr_le_copy(&ascs->peer, &ascs->conn->le.dst); |
| |
| /* TODO: Store the ASES in the settings? */ |
| |
| for (i = 0; i < ASE_COUNT; i++) { |
| struct bt_ascs_ase *ase = &ascs->ases[i]; |
| |
| if (ase->ep.status.state != BT_AUDIO_EP_STATE_IDLE) { |
| /* Cache if disconnected with codec configured */ |
| ase_release(ase, true); |
| } |
| } |
| |
| bt_conn_unref(ascs->conn); |
| ascs->conn = NULL; |
| } |
| |
| static void disconnected(struct bt_conn *conn, uint8_t reason) |
| { |
| int i; |
| |
| BT_DBG(""); |
| |
| for (i = 0; i < CONFIG_BT_MAX_CONN; i++) { |
| if (sessions[i].conn != conn) { |
| continue; |
| } |
| |
| /* Release existing ASEs if not bonded */ |
| if (!bt_addr_le_is_bonded(conn->id, &conn->le.dst)) { |
| ascs_clear(&sessions[i]); |
| } else { |
| ascs_detach(&sessions[i]); |
| } |
| } |
| } |
| |
| static struct bt_conn_cb conn_cb = { |
| .disconnected = disconnected, |
| }; |
| |
| static uint8_t ascs_attr_cb(const struct bt_gatt_attr *attr, uint16_t handle, |
| void *user_data) |
| { |
| struct bt_ascs *ascs = user_data; |
| |
| ascs->handle = handle; |
| |
| return BT_GATT_ITER_CONTINUE; |
| } |
| |
| static struct bt_ascs *ascs_new(struct bt_conn *conn) |
| { |
| static bool conn_cb_registered; |
| int i; |
| |
| for (i = 0; i < CONFIG_BT_MAX_CONN; i++) { |
| if (!sessions[i].conn && |
| !bt_addr_le_cmp(&sessions[i].peer, BT_ADDR_LE_ANY)) { |
| struct bt_ascs *ascs = &sessions[i]; |
| |
| memset(ascs->ases, 0, sizeof(ascs->ases)); |
| ascs->conn = bt_conn_ref(conn); |
| |
| /* Register callbacks if not registered */ |
| if (!conn_cb_registered) { |
| bt_conn_cb_register(&conn_cb); |
| conn_cb_registered = true; |
| } |
| |
| if (!ascs->handle) { |
| bt_gatt_foreach_attr_type(0x0001, 0xffff, |
| BT_UUID_ASCS_ASE_CP, |
| NULL, 1, |
| ascs_attr_cb, ascs); |
| } |
| |
| return ascs; |
| } |
| } |
| |
| return NULL; |
| } |
| |
| static void ase_stream_add(struct bt_ascs *ascs, struct bt_ascs_ase *ase, |
| struct bt_audio_stream *stream) |
| { |
| BT_DBG("ase %p stream %p", ase, stream); |
| ase->ep.stream = stream; |
| stream->conn = ascs->conn; |
| stream->ep = &ase->ep; |
| stream->iso = &ase->ep.iso; |
| } |
| |
| static void ascs_attach(struct bt_ascs *ascs, struct bt_conn *conn) |
| { |
| int i; |
| |
| BT_DBG("ascs %p conn %p", ascs, conn); |
| |
| ascs->conn = bt_conn_ref(conn); |
| |
| /* TODO: Load the ASEs from the settings? */ |
| |
| for (i = 0; i < ASE_COUNT; i++) { |
| if (ascs->ases[i].ep.stream) { |
| ase_stream_add(ascs, &ascs->ases[i], |
| ascs->ases[i].ep.stream); |
| } |
| } |
| } |
| |
| static struct bt_ascs *ascs_find(struct bt_conn *conn) |
| { |
| int i; |
| |
| for (i = 0; i < CONFIG_BT_MAX_CONN; i++) { |
| if (sessions[i].conn == conn) { |
| return &sessions[i]; |
| } |
| |
| /* Check if there is an existing session for the peer */ |
| if (!sessions[i].conn && |
| bt_conn_is_peer_addr_le(conn, sessions[i].id, |
| &sessions[i].peer)) { |
| ascs_attach(&sessions[i], conn); |
| return &sessions[i]; |
| } |
| } |
| |
| return NULL; |
| } |
| |
| static struct bt_ascs *ascs_get(struct bt_conn *conn) |
| { |
| struct bt_ascs *ascs; |
| |
| ascs = ascs_find(conn); |
| if (ascs) { |
| return ascs; |
| } |
| |
| return ascs_new(conn); |
| } |
| |
| NET_BUF_SIMPLE_DEFINE_STATIC(ase_buf, CONFIG_BT_L2CAP_TX_MTU); |
| |
| static void ase_process(struct k_work *work) |
| { |
| struct bt_ascs_ase *ase = CONTAINER_OF(work, struct bt_ascs_ase, work); |
| struct bt_gatt_attr attr; |
| |
| ascs_ep_get_status(&ase->ep, &ase_buf); |
| |
| memset(&attr, 0, sizeof(attr)); |
| attr.handle = ase->ep.handle; |
| attr.uuid = ASE_UUID(ase->ep.status.id); |
| |
| bt_gatt_notify(ase->ascs->conn, &attr, ase_buf.data, ase_buf.len); |
| |
| if (ase->ep.status.state == BT_AUDIO_EP_STATE_RELEASING && |
| ase->ep.stream == NULL) { |
| ascs_ep_set_state(&ase->ep, BT_AUDIO_EP_STATE_IDLE); |
| } |
| |
| if (ase->ep.status.state == BT_AUDIO_EP_STATE_IDLE) { |
| return; |
| } |
| } |
| |
| static uint8_t ase_attr_cb(const struct bt_gatt_attr *attr, uint16_t handle, |
| void *user_data) |
| { |
| struct bt_ascs_ase *ase = user_data; |
| |
| ase->ep.handle = handle; |
| |
| return BT_GATT_ITER_CONTINUE; |
| } |
| |
| void ascs_ep_init(struct bt_audio_ep *ep, uint8_t id) |
| { |
| BT_DBG("ep %p id 0x%02x", ep, id); |
| |
| memset(ep, 0, sizeof(*ep)); |
| ep->type = BT_AUDIO_EP_LOCAL; |
| ep->status.id = id; |
| ep->iso.ops = &ascs_iso_ops; |
| ep->iso.qos = &ep->iso_qos; |
| ep->iso.qos->rx = &ep->iso_rx; |
| ep->iso.qos->tx = &ep->iso_tx; |
| ep->dir = ASE_DIR(id); |
| } |
| |
| static void ase_init(struct bt_ascs_ase *ase, uint8_t id) |
| { |
| memset(ase, 0, sizeof(*ase)); |
| ascs_ep_init(&ase->ep, id); |
| bt_gatt_foreach_attr_type(0x0001, 0xffff, ASE_UUID(id), |
| UINT_TO_POINTER(id), 1, ase_attr_cb, ase); |
| k_work_init(&ase->work, ase_process); |
| } |
| |
| static struct bt_ascs_ase *ase_new(struct bt_ascs *ascs, uint8_t id) |
| { |
| struct bt_ascs_ase *ase; |
| int i; |
| |
| if (id) { |
| if (id > ASE_COUNT) { |
| return NULL; |
| } |
| i = id; |
| ase = &ascs->ases[i - 1]; |
| goto done; |
| } |
| |
| for (i = 0; i < ASE_COUNT; i++) { |
| ase = &ascs->ases[i]; |
| |
| if (!ase->ep.status.id) { |
| i++; |
| goto done; |
| } |
| } |
| |
| return NULL; |
| |
| done: |
| ase_init(ase, i); |
| ase->ascs = ascs; |
| |
| return ase; |
| } |
| |
| static struct bt_ascs_ase *ase_find(struct bt_ascs *ascs, uint8_t id) |
| { |
| struct bt_ascs_ase *ase; |
| |
| if (!id || id > ASE_COUNT) { |
| return NULL; |
| } |
| |
| ase = &ascs->ases[id - 1]; |
| if (ase->ep.status.id == id) { |
| return ase; |
| } |
| |
| return NULL; |
| } |
| |
| static struct bt_ascs_ase *ase_get(struct bt_ascs *ascs, uint8_t id) |
| { |
| struct bt_ascs_ase *ase; |
| |
| ase = ase_find(ascs, id); |
| if (ase) { |
| return ase; |
| } |
| |
| return ase_new(ascs, id); |
| } |
| |
| static ssize_t ascs_ase_read(struct bt_conn *conn, |
| const struct bt_gatt_attr *attr, void *buf, |
| uint16_t len, uint16_t offset) |
| { |
| struct bt_ascs *ascs; |
| struct bt_ascs_ase *ase; |
| |
| BT_DBG("conn %p attr %p buf %p len %u offset %u", conn, attr, buf, len, |
| offset); |
| |
| ascs = ascs_get(conn); |
| if (!ascs) { |
| BT_ERR("Unable to get ASCS session"); |
| return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY); |
| } |
| |
| ase = ase_get(ascs, POINTER_TO_UINT(attr->user_data)); |
| if (!ase) { |
| BT_ERR("Unable to get ASE"); |
| return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY); |
| } |
| |
| ascs_ep_get_status(&ase->ep, &ase_buf); |
| |
| return bt_gatt_attr_read(conn, attr, buf, len, offset, ase_buf.data, |
| ase_buf.len); |
| } |
| |
| static void ascs_cp_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) |
| { |
| BT_DBG("attr %p value 0x%04x", attr, value); |
| } |
| |
| static bool ascs_codec_config_store(struct bt_data *data, void *user_data) |
| { |
| struct bt_codec *codec = user_data; |
| struct bt_codec_data *cdata; |
| |
| if (codec->data_count >= ARRAY_SIZE(codec->data)) { |
| BT_ERR("No slot available for Codec Config"); |
| return false; |
| } |
| |
| cdata = &codec->data[codec->data_count]; |
| |
| if (data->data_len > sizeof(cdata->value)) { |
| BT_ERR("Not enough space for Codec Config: %u > %zu", |
| data->data_len, sizeof(cdata->value)); |
| return false; |
| } |
| |
| BT_DBG("#%u type 0x%02x len %u", codec->data_count, data->type, |
| data->data_len); |
| |
| cdata->data.type = data->type; |
| cdata->data.data_len = data->data_len; |
| |
| /* Deep copy data contents */ |
| cdata->data.data = cdata->value; |
| (void)memcpy(cdata->value, data->data, data->data_len); |
| |
| BT_HEXDUMP_DBG(cdata->value, data->data_len, "data"); |
| |
| codec->data_count++; |
| |
| return true; |
| } |
| |
| static int ascs_ep_set_codec(struct bt_audio_ep *ep, uint8_t id, uint16_t cid, |
| uint16_t vid, struct net_buf_simple *buf, |
| uint8_t len, struct bt_codec *codec) |
| { |
| struct net_buf_simple ad; |
| |
| if (ep == NULL && codec == NULL) { |
| return -EINVAL; |
| } |
| |
| BT_DBG("ep %p codec id 0x%02x cid 0x%04x vid 0x%04x len %u", ep, id, |
| cid, vid, len); |
| |
| if (codec == NULL) { |
| codec = &ep->codec; |
| } |
| |
| codec->id = id; |
| codec->cid = cid; |
| codec->vid = vid; |
| codec->data_count = 0; |
| |
| if (len == 0) { |
| return 0; |
| } |
| |
| net_buf_simple_init_with_data(&ad, net_buf_simple_pull_mem(buf, len), |
| len); |
| |
| /* Parse LTV entries */ |
| bt_data_parse(&ad, ascs_codec_config_store, codec); |
| |
| /* Check if all entries could be parsed */ |
| if (ad.len) { |
| BT_ERR("Unable to parse Codec Config: len %u", ad.len); |
| (void)memset(codec, 0, sizeof(*codec)); |
| |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| static int ase_config(struct bt_ascs *ascs, struct bt_ascs_ase *ase, |
| const struct bt_ascs_config *cfg, |
| struct net_buf_simple *buf) |
| { |
| struct bt_audio_stream *stream; |
| struct bt_codec codec; |
| int err; |
| |
| BT_DBG("ase %p latency 0x%02x phy 0x%02x codec 0x%02x " |
| "cid 0x%04x vid 0x%04x codec config len 0x%02x", ase, |
| cfg->latency, cfg->phy, cfg->codec.id, cfg->codec.cid, |
| cfg->codec.vid, cfg->cc_len); |
| |
| if (cfg->latency < BT_ASCS_CONFIG_LATENCY_LOW || |
| cfg->latency > BT_ASCS_CONFIG_LATENCY_HIGH) { |
| BT_ERR("Invalid latency: 0x%02x", cfg->latency); |
| ascs_cp_rsp_add(ASE_ID(ase), BT_ASCS_CONFIG_OP, |
| BT_ASCS_RSP_CONF_INVALID, |
| BT_ASCS_REASON_LATENCY); |
| return 0; |
| } |
| |
| if (cfg->phy < BT_ASCS_CONFIG_PHY_LE_1M || |
| cfg->phy > BT_ASCS_CONFIG_PHY_LE_CODED) { |
| BT_ERR("Invalid PHY: 0x%02x", cfg->phy); |
| ascs_cp_rsp_add(ASE_ID(ase), BT_ASCS_CONFIG_OP, |
| BT_ASCS_RSP_CONF_INVALID, BT_ASCS_REASON_PHY); |
| return 0; |
| } |
| |
| switch (ase->ep.status.state) { |
| /* Valid only if ASE_State field = 0x00 (Idle) */ |
| case BT_AUDIO_EP_STATE_IDLE: |
| /* or 0x01 (Codec Configured) */ |
| case BT_AUDIO_EP_STATE_CODEC_CONFIGURED: |
| /* or 0x02 (QoS Configured) */ |
| case BT_AUDIO_EP_STATE_QOS_CONFIGURED: |
| break; |
| default: |
| BT_ERR("Invalid state: %s", |
| bt_audio_ep_state_str(ase->ep.status.state)); |
| ascs_cp_rsp_add(ASE_ID(ase), BT_ASCS_CONFIG_OP, |
| BT_ASCS_RSP_INVALID_ASE_STATE, 0x00); |
| return 0; |
| } |
| |
| /* Store current codec configuration to be able to restore it |
| * in case of error. |
| */ |
| (void)memcpy(&codec, &ase->ep.codec, sizeof(codec)); |
| |
| if (ascs_ep_set_codec(&ase->ep, cfg->codec.id, |
| sys_le16_to_cpu(cfg->codec.cid), |
| sys_le16_to_cpu(cfg->codec.vid), |
| buf, cfg->cc_len, &ase->ep.codec)) { |
| (void)memcpy(&ase->ep.codec, &codec, sizeof(codec)); |
| ascs_cp_rsp_add(ASE_ID(ase), BT_ASCS_CONFIG_OP, |
| BT_ASCS_RSP_CONF_INVALID, |
| BT_ASCS_REASON_CODEC_DATA); |
| return 0; |
| } |
| |
| if (ase->ep.stream != NULL) { |
| if (unicast_server_cb != NULL && |
| unicast_server_cb->reconfig != NULL) { |
| err = unicast_server_cb->reconfig(ase->ep.stream, |
| ase->ep.dir, |
| &ase->ep.codec, |
| &ase->ep.qos_pref); |
| } else { |
| err = -ENOTSUP; |
| } |
| |
| if (err != 0) { |
| uint8_t reason = BT_ASCS_REASON_CODEC_DATA; |
| |
| BT_ERR("Reconfig failed: %d", err); |
| |
| (void)memcpy(&ase->ep.codec, &codec, sizeof(codec)); |
| ascs_cp_rsp_add_errno(ASE_ID(ase), |
| BT_ASCS_CONFIG_OP, |
| err, reason); |
| return 0; |
| } |
| |
| stream = ase->ep.stream; |
| } else { |
| stream = NULL; |
| if (unicast_server_cb != NULL && |
| unicast_server_cb->config != NULL) { |
| err = unicast_server_cb->config(ascs->conn, &ase->ep, |
| ase->ep.dir, |
| &ase->ep.codec, &stream, |
| &ase->ep.qos_pref); |
| } else { |
| err = -ENOTSUP; |
| } |
| |
| if (err != 0 || stream == NULL) { |
| BT_ERR("Config failed, err: %d, stream %p", |
| err, stream); |
| |
| (void)memcpy(&ase->ep.codec, &codec, sizeof(codec)); |
| ascs_cp_rsp_add(ASE_ID(ase), BT_ASCS_CONFIG_OP, |
| BT_ASCS_RSP_CONF_REJECTED, |
| BT_ASCS_REASON_CODEC_DATA); |
| |
| return err; |
| } |
| |
| ase_stream_add(ascs, ase, stream); |
| } |
| |
| ascs_cp_rsp_success(ASE_ID(ase), BT_ASCS_CONFIG_OP); |
| |
| /* TODO: bt_audio_stream_attach duplicates some of the |
| * ase_stream_add. Should be cleaned up. |
| */ |
| bt_audio_stream_attach(ascs->conn, stream, &ase->ep, |
| &ase->ep.codec); |
| |
| ascs_ep_set_state(&ase->ep, BT_AUDIO_EP_STATE_CODEC_CONFIGURED); |
| |
| return 0; |
| } |
| |
| static ssize_t ascs_config(struct bt_ascs *ascs, struct net_buf_simple *buf) |
| { |
| const struct bt_ascs_config_op *req; |
| const struct bt_ascs_config *cfg; |
| int i; |
| |
| if (buf->len < sizeof(*req)) { |
| BT_ERR("Malformed ASE Config"); |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); |
| } |
| |
| req = net_buf_simple_pull_mem(buf, sizeof(*req)); |
| |
| BT_DBG("num_ases %u", req->num_ases); |
| |
| if (buf->len < req->num_ases * sizeof(*cfg)) { |
| BT_ERR("Malformed ASE Config: len %u < %zu", buf->len, |
| req->num_ases * sizeof(*cfg)); |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); |
| } |
| |
| for (i = 0; i < req->num_ases; i++) { |
| struct bt_ascs_ase *ase; |
| int err; |
| |
| if (buf->len < sizeof(*cfg)) { |
| BT_ERR("Malformed ASE Config: len %u < %zu", buf->len, |
| sizeof(*cfg)); |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); |
| } |
| |
| cfg = net_buf_simple_pull_mem(buf, sizeof(*cfg)); |
| |
| if (buf->len < cfg->cc_len) { |
| BT_ERR("Malformed ASE Codec Config len %u != %u", |
| buf->len, cfg->cc_len); |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); |
| } |
| |
| BT_DBG("ase 0x%02x cc_len %u", cfg->ase, cfg->cc_len); |
| |
| if (cfg->ase) { |
| ase = ase_get(ascs, cfg->ase); |
| } else { |
| ase = ase_new(ascs, 0); |
| } |
| |
| if (!ase) { |
| ascs_cp_rsp_add(cfg->ase, BT_ASCS_CONFIG_OP, |
| BT_ASCS_RSP_INVALID_ASE, 0x00); |
| BT_ERR("Unable to find ASE"); |
| continue; |
| } |
| |
| err = ase_config(ascs, ase, cfg, buf); |
| if (err != 0) { |
| BT_ERR("Malformed ASE Config"); |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); |
| } |
| } |
| |
| return buf->size; |
| } |
| |
| static int ase_stream_qos(struct bt_audio_stream *stream, struct bt_codec_qos *qos) |
| { |
| BT_DBG("stream %p qos %p", stream, qos); |
| |
| if (stream == NULL || stream->ep == NULL || qos == NULL) { |
| return -EINVAL; |
| } |
| |
| switch (stream->ep->status.state) { |
| /* Valid only if ASE_State field = 0x01 (Codec Configured) */ |
| case BT_AUDIO_EP_STATE_CODEC_CONFIGURED: |
| /* or 0x02 (QoS Configured) */ |
| case BT_AUDIO_EP_STATE_QOS_CONFIGURED: |
| break; |
| default: |
| BT_ERR("Invalid state: %s", |
| bt_audio_ep_state_str(stream->ep->status.state)); |
| return -EBADMSG; |
| } |
| |
| if (!bt_audio_valid_qos(qos)) { |
| return -EINVAL; |
| } |
| |
| if (!bt_audio_valid_stream_qos(stream, qos)) { |
| return -EINVAL; |
| } |
| |
| if (unicast_server_cb != NULL && unicast_server_cb->qos != NULL) { |
| int err; |
| |
| err = unicast_server_cb->qos(stream, qos); |
| if (err != 0) { |
| return err; |
| } |
| } |
| |
| stream->qos = qos; |
| |
| ascs_ep_set_state(stream->ep, BT_AUDIO_EP_STATE_QOS_CONFIGURED); |
| bt_audio_stream_iso_listen(stream); |
| |
| return 0; |
| } |
| |
| static void ase_qos(struct bt_ascs_ase *ase, const struct bt_ascs_qos *qos) |
| { |
| struct bt_audio_stream *stream = ase->ep.stream; |
| struct bt_codec_qos *cqos = &ase->ep.qos; |
| int err; |
| |
| cqos->interval = sys_get_le24(qos->interval); |
| cqos->framing = qos->framing; |
| cqos->phy = qos->phy; |
| cqos->sdu = sys_le16_to_cpu(qos->sdu); |
| cqos->rtn = qos->rtn; |
| cqos->latency = sys_le16_to_cpu(qos->latency); |
| cqos->pd = sys_get_le24(qos->pd); |
| |
| BT_DBG("ase %p cig 0x%02x cis 0x%02x interval %u framing 0x%02x " |
| "phy 0x%02x sdu %u rtn %u latency %u pd %u", ase, qos->cig, |
| qos->cis, cqos->interval, cqos->framing, cqos->phy, cqos->sdu, |
| cqos->rtn, cqos->latency, cqos->pd); |
| |
| err = ase_stream_qos(stream, cqos); |
| if (err) { |
| uint8_t reason = BT_ASCS_REASON_NONE; |
| |
| BT_ERR("QoS failed: err %d", err); |
| |
| if (err == -ENOTSUP) { |
| if (cqos->interval == 0) { |
| reason = BT_ASCS_REASON_INTERVAL; |
| } else if (cqos->framing == 0xff) { |
| reason = BT_ASCS_REASON_FRAMING; |
| } else if (cqos->phy == 0) { |
| reason = BT_ASCS_REASON_PHY; |
| } else if (cqos->sdu == 0xffff) { |
| reason = BT_ASCS_REASON_SDU; |
| } else if (cqos->latency == 0) { |
| reason = BT_ASCS_REASON_LATENCY; |
| } else if (cqos->pd == 0) { |
| reason = BT_ASCS_REASON_PD; |
| } |
| } |
| |
| memset(cqos, 0, sizeof(*cqos)); |
| |
| ascs_cp_rsp_add_errno(ASE_ID(ase), BT_ASCS_QOS_OP, |
| err, reason); |
| return; |
| } |
| |
| ase->ep.cig_id = qos->cig; |
| ase->ep.cis_id = qos->cis; |
| |
| ascs_cp_rsp_success(ASE_ID(ase), BT_ASCS_QOS_OP); |
| } |
| |
| static ssize_t ascs_qos(struct bt_ascs *ascs, struct net_buf_simple *buf) |
| { |
| const struct bt_ascs_qos_op *req; |
| const struct bt_ascs_qos *qos; |
| int i; |
| |
| if (buf->len < sizeof(*req)) { |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); |
| } |
| |
| req = net_buf_simple_pull_mem(buf, sizeof(*req)); |
| |
| BT_DBG("num_ases %u", req->num_ases); |
| |
| if (buf->len < req->num_ases * sizeof(*qos)) { |
| BT_ERR("Malformed ASE QoS: len %u < %zu", buf->len, |
| req->num_ases * sizeof(*qos)); |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); |
| } |
| |
| for (i = 0; i < req->num_ases; i++) { |
| struct bt_ascs_ase *ase; |
| |
| qos = net_buf_simple_pull_mem(buf, sizeof(*qos)); |
| |
| BT_DBG("ase 0x%02x", qos->ase); |
| |
| ase = ase_find(ascs, qos->ase); |
| if (!ase) { |
| ascs_cp_rsp_add(qos->ase, BT_ASCS_QOS_OP, |
| BT_ASCS_RSP_INVALID_ASE, 0x00); |
| BT_ERR("Unable to find ASE"); |
| continue; |
| } |
| |
| ase_qos(ase, qos); |
| } |
| |
| return buf->size; |
| } |
| |
| static bool ascs_codec_store_metadata(struct bt_data *data, void *user_data) |
| { |
| struct bt_codec *codec = user_data; |
| struct bt_codec_data *meta; |
| |
| if (codec->meta_count >= ARRAY_SIZE(codec->meta)) { |
| BT_ERR("No slot available for Codec Config Metadata"); |
| return false; |
| } |
| |
| meta = &codec->meta[codec->meta_count]; |
| |
| if (data->data_len > sizeof(meta->value)) { |
| BT_ERR("Not enough space for Codec Config Metadata: %u > %zu", |
| data->data_len, sizeof(meta->value)); |
| return false; |
| } |
| |
| BT_DBG("#%u type 0x%02x len %u", codec->meta_count, data->type, |
| data->data_len); |
| |
| meta->data.type = data->type; |
| meta->data.data_len = data->data_len; |
| |
| /* Deep copy data contents */ |
| meta->data.data = meta->value; |
| (void)memcpy(meta->value, data->data, data->data_len); |
| |
| BT_HEXDUMP_DBG(meta->value, data->data_len, "data"); |
| |
| codec->meta_count++; |
| |
| return true; |
| } |
| |
| static int ascs_ep_set_metadata(struct bt_audio_ep *ep, |
| struct net_buf_simple *buf, uint8_t len, |
| struct bt_codec *codec) |
| { |
| struct net_buf_simple meta; |
| int err; |
| |
| if (ep == NULL && codec == NULL) { |
| return -EINVAL; |
| } |
| |
| BT_DBG("ep %p len %u codec %p", ep, len, codec); |
| |
| if (len == 0) { |
| return 0; |
| } |
| |
| if (codec == NULL) { |
| codec = &ep->codec; |
| } |
| |
| /* Reset current metadata */ |
| codec->meta_count = 0; |
| (void)memset(codec->meta, 0, sizeof(codec->meta)); |
| |
| net_buf_simple_init_with_data(&meta, net_buf_simple_pull_mem(buf, len), |
| len); |
| |
| /* Parse LTV entries */ |
| bt_data_parse(&meta, ascs_codec_store_metadata, codec); |
| |
| /* Check if all entries could be parsed */ |
| if (meta.len != 0) { |
| BT_ERR("Unable to parse Metadata: len %u", meta.len); |
| err = -EINVAL; |
| |
| if (meta.len > 2) { |
| /* Value of the Metadata Type field in error */ |
| err = meta.data[2]; |
| } |
| |
| codec->meta_count = 0; |
| (void)memset(codec->meta, 0, sizeof(codec->meta)); |
| return err; |
| } |
| |
| return 0; |
| } |
| |
| static int ase_metadata(struct bt_ascs_ase *ase, uint8_t op, |
| struct bt_ascs_metadata *meta, |
| struct net_buf_simple *buf) |
| { |
| struct bt_audio_stream *stream; |
| struct bt_audio_ep *ep; |
| uint8_t state; |
| int err; |
| |
| BT_DBG("ase %p meta->len %u", ase, meta->len); |
| |
| ep = &ase->ep; |
| state = ep->status.state; |
| |
| switch (state) { |
| /* Valid for an ASE only if ASE_State field = 0x03 (Enabling) */ |
| case BT_AUDIO_EP_STATE_ENABLING: |
| /* or 0x04 (Streaming) */ |
| case BT_AUDIO_EP_STATE_STREAMING: |
| break; |
| default: |
| BT_ERR("Invalid state: %s", bt_audio_ep_state_str(state)); |
| err = -EBADMSG; |
| ascs_cp_rsp_add_errno(ASE_ID(ase), op, EBADMSG, |
| buf->len ? *buf->data : 0x00); |
| return err; |
| } |
| |
| if (!meta->len) { |
| goto done; |
| } |
| |
| /* TODO: We should ask the upper layer for accept before we store it */ |
| err = ascs_ep_set_metadata(ep, buf, meta->len, &ep->codec); |
| if (err) { |
| if (err < 0) { |
| ascs_cp_rsp_add_errno(ASE_ID(ase), op, err, 0x00); |
| } else { |
| ascs_cp_rsp_add(ASE_ID(ase), op, |
| BT_ASCS_RSP_METADATA_INVALID, err); |
| } |
| return 0; |
| } |
| |
| stream = ep->stream; |
| if (unicast_server_cb != NULL && unicast_server_cb->metadata != NULL) { |
| err = unicast_server_cb->metadata(stream, ep->codec.meta, |
| ep->codec.meta_count); |
| } else { |
| err = -ENOTSUP; |
| } |
| |
| if (err) { |
| BT_ERR("Metadata failed: %d", err); |
| ascs_cp_rsp_add_errno(ASE_ID(ase), op, err, |
| buf->len ? *buf->data : 0x00); |
| return -EFAULT; |
| } |
| |
| /* Set the state to the same state to trigger the notifications */ |
| ascs_ep_set_state(ep, ep->status.state); |
| done: |
| ascs_cp_rsp_success(ASE_ID(ase), op); |
| |
| return 0; |
| } |
| |
| static int ase_enable(struct bt_ascs_ase *ase, struct bt_ascs_metadata *meta, |
| struct net_buf_simple *buf) |
| { |
| struct bt_audio_stream *stream; |
| struct bt_audio_ep *ep; |
| int err; |
| |
| BT_DBG("ase %p buf->len %u", ase, buf->len); |
| |
| ep = &ase->ep; |
| |
| /* Valid for an ASE only if ASE_State field = 0x02 (QoS Configured) */ |
| if (ep->status.state != BT_AUDIO_EP_STATE_QOS_CONFIGURED) { |
| err = -EBADMSG; |
| BT_ERR("Invalid state: %s", |
| bt_audio_ep_state_str(ep->status.state)); |
| ascs_cp_rsp_add_errno(ASE_ID(ase), BT_ASCS_ENABLE_OP, err, |
| BT_ASCS_REASON_NONE); |
| return err; |
| } |
| |
| err = ascs_ep_set_metadata(ep, buf, meta->len, &ep->codec); |
| if (err) { |
| if (err < 0) { |
| ascs_cp_rsp_add_errno(ASE_ID(ase), BT_ASCS_ENABLE_OP, |
| err, 0x00); |
| } else { |
| ascs_cp_rsp_add(ASE_ID(ase), BT_ASCS_ENABLE_OP, |
| BT_ASCS_RSP_METADATA_INVALID, err); |
| } |
| return 0; |
| } |
| |
| stream = ep->stream; |
| if (unicast_server_cb != NULL && unicast_server_cb->enable != NULL) { |
| err = unicast_server_cb->enable(stream, ep->codec.meta, |
| ep->codec.meta_count); |
| } else { |
| err = -ENOTSUP; |
| } |
| |
| if (err) { |
| BT_ERR("Enable rejected: %d", err); |
| ascs_cp_rsp_add_errno(ASE_ID(ase), BT_ASCS_ENABLE_OP, err, |
| BT_ASCS_REASON_NONE); |
| return -EFAULT; |
| } |
| |
| ascs_ep_set_state(ep, BT_AUDIO_EP_STATE_ENABLING); |
| |
| |
| if (ep->dir == BT_AUDIO_SINK) { |
| /* SINK ASEs can autonomously go into the streaming state if |
| * the CIS is connected |
| */ |
| /* TODO: Verify that CIS is connected and set ep state to |
| * BT_AUDIO_EP_STATE_STREAMING |
| */ |
| } |
| |
| ascs_cp_rsp_success(ASE_ID(ase), BT_ASCS_ENABLE_OP); |
| |
| return 0; |
| } |
| |
| static ssize_t ascs_enable(struct bt_ascs *ascs, struct net_buf_simple *buf) |
| { |
| const struct bt_ascs_enable_op *req; |
| struct bt_ascs_metadata *meta; |
| int i; |
| |
| if (buf->len < sizeof(*req)) { |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); |
| } |
| |
| req = net_buf_simple_pull_mem(buf, sizeof(*req)); |
| |
| BT_DBG("num_ases %u", req->num_ases); |
| |
| if (buf->len < req->num_ases * sizeof(*meta)) { |
| BT_ERR("Malformed ASE Metadata: len %u < %zu", buf->len, |
| req->num_ases * sizeof(*meta)); |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); |
| } |
| |
| for (i = 0; i < req->num_ases; i++) { |
| struct bt_ascs_ase *ase; |
| |
| meta = net_buf_simple_pull_mem(buf, sizeof(*meta)); |
| |
| BT_DBG("ase 0x%02x meta->len %u", meta->ase, meta->len); |
| |
| if (buf->len < meta->len) { |
| BT_ERR("Malformed ASE Enable Metadata len %u != %u", |
| buf->len, meta->len); |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); |
| } |
| |
| ase = ase_find(ascs, meta->ase); |
| if (!ase) { |
| ascs_cp_rsp_add(meta->ase, BT_ASCS_ENABLE_OP, |
| BT_ASCS_RSP_INVALID_ASE, 0x00); |
| BT_ERR("Unable to find ASE"); |
| continue; |
| } |
| |
| ase_enable(ase, meta, buf); |
| } |
| |
| return buf->size; |
| } |
| |
| static void ase_start(struct bt_ascs_ase *ase) |
| { |
| struct bt_audio_stream *stream; |
| struct bt_audio_ep *ep; |
| int err; |
| |
| BT_DBG("ase %p", ase); |
| |
| ep = &ase->ep; |
| |
| /* Valid for an ASE only if ASE_State field = 0x02 (QoS Configured) */ |
| if (ep->status.state != BT_AUDIO_EP_STATE_ENABLING) { |
| err = -EBADMSG; |
| BT_ERR("Invalid state: %s", |
| bt_audio_ep_state_str(ep->status.state)); |
| ascs_cp_rsp_add_errno(ASE_ID(ase), BT_ASCS_START_OP, err, |
| BT_ASCS_REASON_NONE); |
| return; |
| } |
| |
| /* If the ASE_ID written by the client represents a Sink ASE, the |
| * server shall not accept the Receiver Start Ready operation for that |
| * ASE. The server shall send a notification of the ASE Control Point |
| * characteristic to the client, and the server shall set the |
| * Response_Code value for that ASE to 0x05 (Invalid ASE direction). |
| */ |
| if (ep->dir == BT_AUDIO_SINK) { |
| BT_ERR("Start failed: invalid operation for Sink"); |
| ascs_cp_rsp_add(ASE_ID(ase), BT_ASCS_START_OP, |
| BT_ASCS_RSP_INVALID_DIR, BT_ASCS_REASON_NONE); |
| return; |
| } |
| |
| stream = ep->stream; |
| if (unicast_server_cb != NULL && unicast_server_cb->start != NULL) { |
| err = unicast_server_cb->start(stream); |
| } else { |
| err = -ENOTSUP; |
| } |
| |
| if (err) { |
| BT_ERR("Start failed: %d", err); |
| ascs_cp_rsp_add(ASE_ID(ase), BT_ASCS_START_OP, err, |
| BT_ASCS_REASON_NONE); |
| return; |
| } |
| |
| ascs_ep_set_state(ep, BT_AUDIO_EP_STATE_STREAMING); |
| |
| ascs_cp_rsp_success(ASE_ID(ase), BT_ASCS_START_OP); |
| } |
| |
| static ssize_t ascs_start(struct bt_ascs *ascs, struct net_buf_simple *buf) |
| { |
| const struct bt_ascs_start_op *req; |
| int i; |
| |
| if (buf->len < sizeof(*req)) { |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); |
| } |
| |
| req = net_buf_simple_pull_mem(buf, sizeof(*req)); |
| |
| BT_DBG("num_ases %u", req->num_ases); |
| |
| if (buf->len < req->num_ases) { |
| BT_ERR("Malformed ASE Start: len %u < %u", buf->len, |
| req->num_ases); |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); |
| } |
| |
| for (i = 0; i < req->num_ases; i++) { |
| struct bt_ascs_ase *ase; |
| uint8_t id; |
| |
| id = net_buf_simple_pull_u8(buf); |
| |
| BT_DBG("ase 0x%02x", id); |
| |
| ase = ase_find(ascs, id); |
| if (!ase) { |
| ascs_cp_rsp_add(id, BT_ASCS_START_OP, |
| BT_ASCS_RSP_INVALID_ASE, 0x00); |
| BT_ERR("Unable to find ASE"); |
| continue; |
| } |
| |
| ase_start(ase); |
| } |
| |
| return buf->size; |
| } |
| |
| static ssize_t ascs_disable(struct bt_ascs *ascs, struct net_buf_simple *buf) |
| { |
| const struct bt_ascs_disable_op *req; |
| int i; |
| |
| if (buf->len < sizeof(*req)) { |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); |
| } |
| |
| req = net_buf_simple_pull_mem(buf, sizeof(*req)); |
| |
| BT_DBG("num_ases %u", req->num_ases); |
| |
| if (buf->len < req->num_ases) { |
| BT_ERR("Malformed ASE Disable: len %u < %u", buf->len, |
| req->num_ases); |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); |
| } |
| |
| for (i = 0; i < req->num_ases; i++) { |
| struct bt_ascs_ase *ase; |
| uint8_t id; |
| |
| id = net_buf_simple_pull_u8(buf); |
| |
| BT_DBG("ase 0x%02x", id); |
| |
| ase = ase_find(ascs, id); |
| if (!ase) { |
| ascs_cp_rsp_add(id, BT_ASCS_DISABLE_OP, |
| BT_ASCS_RSP_INVALID_ASE_STATE, 0); |
| BT_ERR("Unable to find ASE"); |
| continue; |
| } |
| |
| ase_disable(ase); |
| } |
| |
| return buf->size; |
| } |
| |
| static void ase_stop(struct bt_ascs_ase *ase) |
| { |
| struct bt_audio_stream *stream; |
| struct bt_audio_ep *ep; |
| int err; |
| |
| BT_DBG("ase %p", ase); |
| |
| ep = &ase->ep; |
| |
| /* If the ASE_ID written by the client represents a Sink ASE, the |
| * server shall not accept the Receiver Stop Ready operation for that |
| * ASE. The server shall send a notification of the ASE Control Point |
| * characteristic to the client, and the server shall set the |
| * Response_Code value for that ASE to 0x05 (Invalid ASE direction). |
| */ |
| if (ase->ep.dir == BT_AUDIO_SINK) { |
| BT_ERR("Stop failed: invalid operation for Sink"); |
| ascs_cp_rsp_add(ASE_ID(ase), BT_ASCS_STOP_OP, |
| BT_ASCS_RSP_INVALID_DIR, BT_ASCS_REASON_NONE); |
| return; |
| } |
| |
| if (ep->status.state != BT_AUDIO_EP_STATE_DISABLING) { |
| BT_ERR("Invalid state: %s", |
| bt_audio_ep_state_str(ep->status.state)); |
| ascs_cp_rsp_add_errno(ASE_ID(ase), BT_ASCS_STOP_OP, EBADMSG, |
| BT_ASCS_REASON_NONE); |
| return; |
| } |
| |
| stream = ep->stream; |
| if (unicast_server_cb != NULL && unicast_server_cb->stop != NULL) { |
| err = unicast_server_cb->stop(stream); |
| } else { |
| err = -ENOTSUP; |
| } |
| |
| if (err) { |
| BT_ERR("Stop failed: %d", err); |
| ascs_cp_rsp_add_errno(ASE_ID(ase), BT_ASCS_STOP_OP, err, |
| BT_ASCS_REASON_NONE); |
| return; |
| } |
| |
| /* If the Receiver Stop Ready operation has completed successfully the |
| * Unicast Client or the Unicast Server may terminate a CIS established |
| * for that ASE by following the Connected Isochronous Stream Terminate |
| * procedure defined in Volume 3, Part C, Section 9.3.15. |
| */ |
| err = bt_audio_stream_disconnect(stream); |
| if (err != 0) { |
| BT_ERR("Could not disconnect the CIS: %d", err); |
| return; |
| } |
| |
| ascs_ep_set_state(ep, BT_AUDIO_EP_STATE_QOS_CONFIGURED); |
| err = bt_audio_stream_iso_listen(stream); |
| if (err != 0) { |
| BT_ERR("Could not make stream listen: %d", err); |
| return; |
| } |
| |
| ascs_cp_rsp_success(ASE_ID(ase), BT_ASCS_STOP_OP); |
| } |
| |
| static ssize_t ascs_stop(struct bt_ascs *ascs, struct net_buf_simple *buf) |
| { |
| const struct bt_ascs_start_op *req; |
| int i; |
| |
| if (buf->len < sizeof(*req)) { |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); |
| } |
| |
| req = net_buf_simple_pull_mem(buf, sizeof(*req)); |
| |
| BT_DBG("num_ases %u", req->num_ases); |
| |
| if (buf->len < req->num_ases) { |
| BT_ERR("Malformed ASE Start: len %u < %u", buf->len, |
| req->num_ases); |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); |
| } |
| |
| for (i = 0; i < req->num_ases; i++) { |
| struct bt_ascs_ase *ase; |
| uint8_t id; |
| |
| id = net_buf_simple_pull_u8(buf); |
| |
| BT_DBG("ase 0x%02x", id); |
| |
| ase = ase_find(ascs, id); |
| if (!ase) { |
| ascs_cp_rsp_add(id, BT_ASCS_STOP_OP, |
| BT_ASCS_RSP_INVALID_ASE, 0x00); |
| BT_ERR("Unable to find ASE"); |
| continue; |
| } |
| |
| ase_stop(ase); |
| } |
| |
| return buf->size; |
| } |
| |
| static ssize_t ascs_metadata(struct bt_ascs *ascs, struct net_buf_simple *buf) |
| { |
| const struct bt_ascs_metadata_op *req; |
| struct bt_ascs_metadata *meta; |
| int i; |
| |
| if (buf->len < sizeof(*req)) { |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); |
| } |
| |
| req = net_buf_simple_pull_mem(buf, sizeof(*req)); |
| |
| BT_DBG("num_ases %u", req->num_ases); |
| |
| if (buf->len < req->num_ases * sizeof(*meta)) { |
| BT_ERR("Malformed ASE Metadata: len %u < %zu", buf->len, |
| req->num_ases * sizeof(*meta)); |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); |
| } |
| |
| for (i = 0; i < req->num_ases; i++) { |
| struct bt_ascs_ase *ase; |
| |
| meta = net_buf_simple_pull_mem(buf, sizeof(*meta)); |
| |
| if (buf->len < meta->len) { |
| BT_ERR("Malformed ASE Metadata: len %u < %u", buf->len, |
| meta->len); |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); |
| } |
| |
| BT_DBG("ase 0x%02x meta->len %u", meta->ase, meta->len); |
| |
| ase = ase_find(ascs, meta->ase); |
| if (!ase) { |
| ascs_cp_rsp_add(meta->ase, BT_ASCS_METADATA_OP, |
| BT_ASCS_RSP_INVALID_ASE, 0x00); |
| BT_ERR("Unable to find ASE"); |
| continue; |
| } |
| |
| ase_metadata(ase, BT_ASCS_METADATA_OP, meta, buf); |
| } |
| |
| return buf->size; |
| } |
| |
| static ssize_t ascs_release(struct bt_ascs *ascs, struct net_buf_simple *buf) |
| { |
| const struct bt_ascs_release_op *req; |
| int i; |
| |
| if (buf->len < sizeof(*req)) { |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); |
| } |
| |
| req = net_buf_simple_pull_mem(buf, sizeof(*req)); |
| |
| BT_DBG("num_ases %u", req->num_ases); |
| |
| if (buf->len < req->num_ases) { |
| BT_ERR("Malformed ASE Release: len %u < %u", buf->len, |
| req->num_ases); |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); |
| } |
| |
| for (i = 0; i < req->num_ases; i++) { |
| uint8_t id; |
| struct bt_ascs_ase *ase; |
| |
| id = net_buf_simple_pull_u8(buf); |
| |
| BT_DBG("ase 0x%02x", id); |
| |
| ase = ase_find(ascs, id); |
| if (!ase) { |
| ascs_cp_rsp_add(id, BT_ASCS_RELEASE_OP, |
| BT_ASCS_RSP_INVALID_ASE, 0); |
| BT_ERR("Unable to find ASE"); |
| continue; |
| } |
| |
| ase_release(ase, false); |
| } |
| |
| return buf->size; |
| } |
| |
| static ssize_t ascs_cp_write(struct bt_conn *conn, |
| const struct bt_gatt_attr *attr, const void *data, |
| uint16_t len, uint16_t offset, uint8_t flags) |
| { |
| struct bt_ascs *ascs; |
| const struct bt_ascs_ase_cp *req; |
| struct net_buf_simple buf; |
| ssize_t ret; |
| |
| if (offset) { |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); |
| } |
| |
| if (len < sizeof(*req)) { |
| return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); |
| } |
| |
| net_buf_simple_init_with_data(&buf, (void *) data, len); |
| |
| req = net_buf_simple_pull_mem(&buf, sizeof(*req)); |
| |
| BT_DBG("conn %p attr %p buf %p len %u op %s (0x%02x)", conn, |
| attr, data, len, bt_ascs_op_str(req->op), req->op); |
| |
| /* Reset/empty response buffer before using it again */ |
| net_buf_simple_reset(&rsp_buf); |
| |
| ascs = ascs_get(conn); |
| if (!ascs) { |
| BT_ERR("Unable to get ASCS session"); |
| len = BT_GATT_ERR(BT_ATT_ERR_UNLIKELY); |
| ascs_cp_rsp_add(0, req->op, BT_ASCS_RSP_UNSPECIFIED, 0x00); |
| goto respond; |
| } |
| |
| switch (req->op) { |
| case BT_ASCS_CONFIG_OP: |
| ret = ascs_config(ascs, &buf); |
| break; |
| case BT_ASCS_QOS_OP: |
| ret = ascs_qos(ascs, &buf); |
| break; |
| case BT_ASCS_ENABLE_OP: |
| ret = ascs_enable(ascs, &buf); |
| break; |
| case BT_ASCS_START_OP: |
| ret = ascs_start(ascs, &buf); |
| break; |
| case BT_ASCS_DISABLE_OP: |
| ret = ascs_disable(ascs, &buf); |
| break; |
| case BT_ASCS_STOP_OP: |
| ret = ascs_stop(ascs, &buf); |
| break; |
| case BT_ASCS_METADATA_OP: |
| ret = ascs_metadata(ascs, &buf); |
| break; |
| case BT_ASCS_RELEASE_OP: |
| ret = ascs_release(ascs, &buf); |
| break; |
| default: |
| ascs_cp_rsp_add(0x00, req->op, BT_ASCS_RSP_NOT_SUPPORTED, 0); |
| BT_DBG("Unknown opcode"); |
| len = BT_GATT_ERR(BT_ATT_ERR_NOT_SUPPORTED); |
| } |
| |
| if (ret == BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN)) { |
| ascs_cp_rsp_add(0, req->op, BT_ASCS_RSP_TRUNCATED, |
| BT_ASCS_REASON_NONE); |
| } |
| |
| respond: |
| ascs_cp_notify(ascs); |
| |
| return len; |
| } |
| |
| BT_GATT_SERVICE_DEFINE(ascs_svc, |
| BT_GATT_PRIMARY_SERVICE(BT_UUID_ASCS), |
| #if CONFIG_BT_ASCS_ASE_SNK_COUNT > 0 |
| BT_GATT_CHARACTERISTIC(BT_UUID_ASCS_ASE_SNK, |
| BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, |
| BT_GATT_PERM_READ_ENCRYPT, |
| ascs_ase_read, NULL, UINT_TO_POINTER(1)), |
| BT_GATT_CCC(ascs_ase_cfg_changed, |
| BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), |
| #endif |
| #if CONFIG_BT_ASCS_ASE_SNK_COUNT > 1 |
| BT_GATT_CHARACTERISTIC(BT_UUID_ASCS_ASE_SNK, |
| BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, |
| BT_GATT_PERM_READ_ENCRYPT, |
| ascs_ase_read, NULL, UINT_TO_POINTER(2)), |
| BT_GATT_CCC(ascs_ase_cfg_changed, |
| BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), |
| #endif |
| #if CONFIG_BT_ASCS_ASE_SRC_COUNT > 0 |
| BT_GATT_CHARACTERISTIC(BT_UUID_ASCS_ASE_SRC, |
| BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, |
| BT_GATT_PERM_READ_ENCRYPT, |
| ascs_ase_read, NULL, |
| UINT_TO_POINTER(CONFIG_BT_ASCS_ASE_SNK_COUNT + 1)), |
| BT_GATT_CCC(ascs_ase_cfg_changed, |
| BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), |
| #endif |
| #if CONFIG_BT_ASCS_ASE_SRC_COUNT > 1 |
| BT_GATT_CHARACTERISTIC(BT_UUID_ASCS_ASE_SRC, |
| BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, |
| BT_GATT_PERM_READ_ENCRYPT, |
| ascs_ase_read, NULL, |
| UINT_TO_POINTER(CONFIG_BT_ASCS_ASE_SNK_COUNT + 2)), |
| BT_GATT_CCC(ascs_ase_cfg_changed, |
| BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT), |
| #endif |
| BT_GATT_CHARACTERISTIC(BT_UUID_ASCS_ASE_CP, |
| BT_GATT_CHRC_WRITE | |
| BT_GATT_CHRC_WRITE_WITHOUT_RESP | |
| BT_GATT_CHRC_NOTIFY, |
| BT_GATT_PERM_WRITE_ENCRYPT, |
| NULL, ascs_cp_write, NULL), |
| BT_GATT_CCC(ascs_cp_cfg_changed, |
| BT_GATT_PERM_READ | BT_GATT_PERM_WRITE_ENCRYPT) |
| ); |
| |
| #endif /* BT_AUDIO_UNICAST_SERVER */ |