In case CIS failed to be established, leave the ASE in enabling state waiting for peer to retry. Signed-off-by: Mariusz Skamra <mariusz.skamra@codecoup.pl>
2373 lines
58 KiB
C
2373 lines
58 KiB
C
/* @file
|
|
* @brief Bluetooth ASCS
|
|
*/
|
|
/*
|
|
* Copyright (c) 2020 Intel Corporation
|
|
* Copyright (c) 2022 Nordic Semiconductor ASA
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#include <zephyr/kernel.h>
|
|
#include <zephyr/sys/byteorder.h>
|
|
#include <zephyr/sys/check.h>
|
|
|
|
#include <zephyr/device.h>
|
|
#include <zephyr/init.h>
|
|
|
|
#include <zephyr/bluetooth/bluetooth.h>
|
|
#include <zephyr/bluetooth/conn.h>
|
|
#include <zephyr/bluetooth/gatt.h>
|
|
#include "zephyr/bluetooth/iso.h"
|
|
#include <zephyr/bluetooth/audio/audio.h>
|
|
#include <zephyr/bluetooth/audio/pacs.h>
|
|
|
|
#include <zephyr/logging/log.h>
|
|
|
|
LOG_MODULE_REGISTER(bt_ascs, CONFIG_BT_ASCS_LOG_LEVEL);
|
|
|
|
#include "common/bt_str.h"
|
|
#include "common/assert.h"
|
|
|
|
#include "../host/hci_core.h"
|
|
#include "../host/conn_internal.h"
|
|
|
|
#include "audio_internal.h"
|
|
#include "audio_iso.h"
|
|
#include "endpoint.h"
|
|
#include "unicast_server.h"
|
|
#include "pacs_internal.h"
|
|
#include "cap_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_DIR_SOURCE : BT_AUDIO_DIR_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 bt_ascs {
|
|
struct bt_conn *conn;
|
|
struct bt_ascs_ase ases[ASE_COUNT];
|
|
};
|
|
|
|
static struct bt_ascs sessions[CONFIG_BT_MAX_CONN];
|
|
|
|
static int control_point_notify(struct bt_conn *conn, const void *data, uint16_t len);
|
|
|
|
static void ase_status_changed(struct bt_audio_ep *ep, uint8_t old_state,
|
|
uint8_t state)
|
|
{
|
|
k_work_submit(&ep->work);
|
|
}
|
|
|
|
void ascs_ep_set_state(struct bt_audio_ep *ep, uint8_t state)
|
|
{
|
|
struct bt_audio_stream *stream;
|
|
bool state_changed;
|
|
uint8_t old_state;
|
|
|
|
if (!ep) {
|
|
return;
|
|
}
|
|
|
|
/* TODO: Verify state changes */
|
|
|
|
old_state = ep->status.state;
|
|
ep->status.state = state;
|
|
state_changed = old_state != state;
|
|
|
|
LOG_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) {
|
|
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:
|
|
switch (old_state) {
|
|
case BT_AUDIO_EP_STATE_IDLE:
|
|
case BT_AUDIO_EP_STATE_CODEC_CONFIGURED:
|
|
case BT_AUDIO_EP_STATE_QOS_CONFIGURED:
|
|
case BT_AUDIO_EP_STATE_RELEASING:
|
|
break;
|
|
default:
|
|
BT_ASSERT_MSG(false,
|
|
"Invalid state transition: %s -> %s",
|
|
bt_audio_ep_state_str(old_state),
|
|
bt_audio_ep_state_str(ep->status.state));
|
|
return;
|
|
}
|
|
|
|
if (ops->configured != NULL) {
|
|
ops->configured(stream, &ep->qos_pref);
|
|
}
|
|
|
|
break;
|
|
case BT_AUDIO_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) {
|
|
case BT_AUDIO_EP_STATE_CODEC_CONFIGURED:
|
|
case BT_AUDIO_EP_STATE_QOS_CONFIGURED:
|
|
case BT_AUDIO_EP_STATE_DISABLING:
|
|
break;
|
|
default:
|
|
BT_ASSERT_MSG(false,
|
|
"Invalid state transition: %s -> %s",
|
|
bt_audio_ep_state_str(old_state),
|
|
bt_audio_ep_state_str(ep->status.state));
|
|
return;
|
|
}
|
|
} else {
|
|
switch (old_state) {
|
|
case BT_AUDIO_EP_STATE_CODEC_CONFIGURED:
|
|
case BT_AUDIO_EP_STATE_QOS_CONFIGURED:
|
|
case BT_AUDIO_EP_STATE_ENABLING:
|
|
case BT_AUDIO_EP_STATE_STREAMING:
|
|
break;
|
|
default:
|
|
BT_ASSERT_MSG(false,
|
|
"Invalid state transition: %s -> %s",
|
|
bt_audio_ep_state_str(old_state),
|
|
bt_audio_ep_state_str(ep->status.state));
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (ops->qos_set != NULL) {
|
|
ops->qos_set(stream);
|
|
}
|
|
|
|
break;
|
|
case BT_AUDIO_EP_STATE_ENABLING:
|
|
switch (old_state) {
|
|
case BT_AUDIO_EP_STATE_QOS_CONFIGURED:
|
|
case BT_AUDIO_EP_STATE_ENABLING:
|
|
break;
|
|
default:
|
|
BT_ASSERT_MSG(false,
|
|
"Invalid state transition: %s -> %s",
|
|
bt_audio_ep_state_str(old_state),
|
|
bt_audio_ep_state_str(ep->status.state));
|
|
return;
|
|
}
|
|
|
|
if (state_changed && ops->enabled != NULL) {
|
|
ops->enabled(stream);
|
|
} else if (!state_changed && ops->metadata_updated) {
|
|
ops->metadata_updated(stream);
|
|
}
|
|
|
|
break;
|
|
case BT_AUDIO_EP_STATE_STREAMING:
|
|
switch (old_state) {
|
|
case BT_AUDIO_EP_STATE_ENABLING:
|
|
case BT_AUDIO_EP_STATE_STREAMING:
|
|
break;
|
|
default:
|
|
BT_ASSERT_MSG(false,
|
|
"Invalid state transition: %s -> %s",
|
|
bt_audio_ep_state_str(old_state),
|
|
bt_audio_ep_state_str(ep->status.state));
|
|
return;
|
|
}
|
|
|
|
if (state_changed && ops->started != NULL) {
|
|
ops->started(stream);
|
|
} else if (!state_changed && ops->metadata_updated) {
|
|
ops->metadata_updated(stream);
|
|
}
|
|
|
|
break;
|
|
case BT_AUDIO_EP_STATE_DISABLING:
|
|
if (ep->dir == BT_AUDIO_DIR_SOURCE) {
|
|
switch (old_state) {
|
|
case BT_AUDIO_EP_STATE_ENABLING:
|
|
case BT_AUDIO_EP_STATE_STREAMING:
|
|
ep->receiver_ready = false;
|
|
break;
|
|
default:
|
|
BT_ASSERT_MSG(false,
|
|
"Invalid state transition: %s -> %s",
|
|
bt_audio_ep_state_str(old_state),
|
|
bt_audio_ep_state_str(ep->status.state));
|
|
return;
|
|
}
|
|
} else {
|
|
/* Sinks cannot go into the disabling state */
|
|
BT_ASSERT_MSG(false,
|
|
"Invalid state transition: %s -> %s",
|
|
bt_audio_ep_state_str(old_state),
|
|
bt_audio_ep_state_str(ep->status.state));
|
|
return;
|
|
}
|
|
|
|
if (ops->disabled != NULL) {
|
|
ops->disabled(stream);
|
|
}
|
|
|
|
break;
|
|
case BT_AUDIO_EP_STATE_RELEASING:
|
|
switch (old_state) {
|
|
case BT_AUDIO_EP_STATE_CODEC_CONFIGURED:
|
|
case BT_AUDIO_EP_STATE_QOS_CONFIGURED:
|
|
case BT_AUDIO_EP_STATE_ENABLING:
|
|
case BT_AUDIO_EP_STATE_STREAMING:
|
|
ep->receiver_ready = false;
|
|
break;
|
|
case BT_AUDIO_EP_STATE_DISABLING:
|
|
if (ep->dir == BT_AUDIO_DIR_SOURCE) {
|
|
break;
|
|
} /* else fall through for sink */
|
|
|
|
/* fall through */
|
|
default:
|
|
BT_ASSERT_MSG(false,
|
|
"Invalid state transition: %s -> %s",
|
|
bt_audio_ep_state_str(old_state),
|
|
bt_audio_ep_state_str(ep->status.state));
|
|
return;
|
|
}
|
|
|
|
break; /* no-op*/
|
|
default:
|
|
LOG_ERR("Invalid state: %u", state);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
LOG_DBG("#%u: %s type 0x%02x len %u", i, prefix, d->type, d->data_len);
|
|
LOG_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);
|
|
|
|
LOG_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);
|
|
|
|
LOG_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;
|
|
|
|
LOG_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;
|
|
}
|
|
|
|
LOG_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:
|
|
LOG_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_iso *iso = CONTAINER_OF(chan, struct bt_audio_iso, chan);
|
|
const struct bt_audio_stream_ops *ops;
|
|
struct bt_audio_stream *stream;
|
|
struct bt_audio_ep *ep;
|
|
|
|
ep = iso->rx.ep;
|
|
if (ep == NULL) {
|
|
LOG_ERR("iso %p not bound with ep", chan);
|
|
return;
|
|
}
|
|
|
|
if (ep->status.state != BT_AUDIO_EP_STATE_STREAMING) {
|
|
LOG_DBG("ep %p is not in the streaming state: %s",
|
|
ep, bt_audio_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_AUDIO_DEBUG_STREAM_DATA)) {
|
|
LOG_DBG("stream %p ep %p len %zu", stream, 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");
|
|
}
|
|
}
|
|
|
|
static void ascs_iso_sent(struct bt_iso_chan *chan)
|
|
{
|
|
struct bt_audio_iso *iso = CONTAINER_OF(chan, struct bt_audio_iso, chan);
|
|
const struct bt_audio_stream_ops *ops;
|
|
struct bt_audio_stream *stream;
|
|
struct bt_audio_ep *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;
|
|
}
|
|
|
|
ops = stream->ops;
|
|
|
|
if (IS_ENABLED(CONFIG_BT_AUDIO_DEBUG_STREAM_DATA)) {
|
|
LOG_DBG("stream %p ep %p", stream, stream->ep);
|
|
}
|
|
|
|
if (ops != NULL && ops->sent != NULL) {
|
|
ops->sent(stream);
|
|
}
|
|
}
|
|
|
|
static int ase_stream_start(struct bt_audio_stream *stream)
|
|
{
|
|
int err = 0;
|
|
|
|
if (unicast_server_cb != NULL && unicast_server_cb->start != NULL) {
|
|
err = unicast_server_cb->start(stream);
|
|
} else {
|
|
err = -ENOTSUP;
|
|
}
|
|
|
|
ascs_ep_set_state(stream->ep, BT_AUDIO_EP_STATE_STREAMING);
|
|
|
|
return err;
|
|
}
|
|
|
|
static void ascs_ep_iso_connected(struct bt_audio_ep *ep)
|
|
{
|
|
struct bt_audio_stream *stream;
|
|
int err;
|
|
|
|
if (ep->status.state != BT_AUDIO_EP_STATE_ENABLING) {
|
|
LOG_DBG("ep %p not in enabling state: %s",
|
|
ep, bt_audio_ep_state_str(ep->status.state));
|
|
return;
|
|
}
|
|
|
|
if (ep->dir == BT_AUDIO_DIR_SOURCE && !ep->receiver_ready) {
|
|
return;
|
|
}
|
|
|
|
stream = ep->stream;
|
|
if (stream == NULL) {
|
|
LOG_ERR("No stream for ep %p", ep);
|
|
return;
|
|
}
|
|
|
|
err = ase_stream_start(stream);
|
|
if (err) {
|
|
LOG_ERR("Could not start stream %d", err);
|
|
}
|
|
}
|
|
|
|
static void ascs_iso_connected(struct bt_iso_chan *chan)
|
|
{
|
|
struct bt_audio_iso *iso = CONTAINER_OF(chan, struct bt_audio_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) {
|
|
ascs_ep_iso_connected(iso->rx.ep);
|
|
}
|
|
|
|
if (iso->tx.ep != NULL) {
|
|
ascs_ep_iso_connected(iso->tx.ep);
|
|
}
|
|
}
|
|
|
|
static void ascs_ep_iso_disconnected(struct bt_audio_ep *ep, uint8_t reason)
|
|
{
|
|
const struct bt_audio_stream_ops *ops;
|
|
struct bt_audio_stream *stream;
|
|
int err;
|
|
|
|
stream = ep->stream;
|
|
if (stream == NULL) {
|
|
LOG_ERR("No stream for ep %p", ep);
|
|
return;
|
|
}
|
|
|
|
ops = stream->ops;
|
|
|
|
LOG_DBG("stream %p ep %p reason 0x%02x", stream, stream->ep, reason);
|
|
|
|
if (ep->status.state == BT_AUDIO_EP_STATE_ENABLING &&
|
|
reason == BT_HCI_ERR_CONN_FAIL_TO_ESTAB) {
|
|
LOG_DBG("Waiting for retry");
|
|
|
|
err = bt_audio_stream_iso_listen(stream);
|
|
if (err != 0) {
|
|
LOG_ERR("Could not make stream listen: %d", err);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (ops != NULL && ops->stopped != NULL) {
|
|
ops->stopped(stream);
|
|
} else {
|
|
LOG_WRN("No callback for stopped set");
|
|
}
|
|
|
|
if (ep->status.state == BT_AUDIO_EP_STATE_RELEASING) {
|
|
bt_audio_iso_unbind_ep(ep->iso, ep);
|
|
|
|
/* Trigger a call to ase_process to handle the cleanup */
|
|
k_work_submit(&ep->work);
|
|
} else {
|
|
/* The ASE state machine goes into different states from this operation
|
|
* based on whether it is a source or a sink ASE.
|
|
*/
|
|
if (ep->status.state == BT_AUDIO_EP_STATE_STREAMING ||
|
|
ep->status.state == BT_AUDIO_EP_STATE_ENABLING) {
|
|
if (ep->dir == BT_AUDIO_DIR_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) {
|
|
LOG_ERR("Could not make stream listen: %d", err);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void ascs_iso_disconnected(struct bt_iso_chan *chan, uint8_t reason)
|
|
{
|
|
struct bt_audio_iso *iso = CONTAINER_OF(chan, struct bt_audio_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) {
|
|
ascs_ep_iso_disconnected(iso->rx.ep, reason);
|
|
}
|
|
|
|
if (iso->tx.ep != NULL) {
|
|
ascs_ep_iso_disconnected(iso->tx.ep, reason);
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
LOG_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;
|
|
|
|
LOG_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)
|
|
{
|
|
LOG_DBG("id %u op %u err %d reason %u", id, op, err, 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);
|
|
case -EACCES:
|
|
switch (op) {
|
|
case BT_ASCS_METADATA_OP:
|
|
return ascs_cp_rsp_add(id, op,
|
|
BT_ASCS_RSP_METADATA_REJECTED,
|
|
reason);
|
|
default:
|
|
return ascs_cp_rsp_add(id, op, BT_ASCS_RSP_UNSPECIFIED,
|
|
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);
|
|
}
|
|
|
|
static void ase_release(struct bt_ascs_ase *ase)
|
|
{
|
|
int err;
|
|
|
|
LOG_DBG("ase %p state %s", ase, bt_audio_ep_state_str(ase->ep.status.state));
|
|
|
|
if (ase->ep.status.state == BT_AUDIO_EP_STATE_RELEASING) {
|
|
/* already releasing */
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
ascs_ep_set_state(&ase->ep, BT_AUDIO_EP_STATE_RELEASING);
|
|
|
|
ascs_cp_rsp_success(ASE_ID(ase), BT_ASCS_RELEASE_OP);
|
|
}
|
|
|
|
static void ase_disable(struct bt_ascs_ase *ase)
|
|
{
|
|
struct bt_audio_stream *stream;
|
|
struct bt_audio_ep *ep;
|
|
int err;
|
|
|
|
LOG_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:
|
|
LOG_WRN("Invalid operation in 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) {
|
|
LOG_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_DIR_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 disconnected(struct bt_conn *conn, uint8_t reason)
|
|
{
|
|
struct bt_ascs *session = &sessions[bt_conn_index(conn)];
|
|
|
|
if (session->conn == NULL) {
|
|
return;
|
|
}
|
|
|
|
for (size_t i = 0; i < ARRAY_SIZE(session->ases); i++) {
|
|
struct bt_ascs_ase *ase = &session->ases[i];
|
|
struct bt_audio_stream *stream = ase->ep.stream;
|
|
|
|
if (ase->ep.status.state != BT_AUDIO_EP_STATE_IDLE) {
|
|
/* ase_process will handle the final state transition into idle state */
|
|
ase_release(ase);
|
|
}
|
|
|
|
if (stream != NULL && stream->conn != NULL) {
|
|
bt_conn_unref(stream->conn);
|
|
stream->conn = NULL;
|
|
}
|
|
}
|
|
|
|
bt_conn_unref(session->conn);
|
|
session->conn = NULL;
|
|
}
|
|
|
|
BT_CONN_CB_DEFINE(conn_cb) = {
|
|
.disconnected = disconnected,
|
|
};
|
|
|
|
struct audio_iso_find_params {
|
|
struct bt_conn *acl;
|
|
uint8_t cig_id;
|
|
uint8_t cis_id;
|
|
};
|
|
|
|
static bool audio_iso_find_func(struct bt_audio_iso *iso, void *user_data)
|
|
{
|
|
struct audio_iso_find_params *params = user_data;
|
|
const struct bt_audio_ep *ep;
|
|
|
|
if (iso->rx.ep != NULL) {
|
|
ep = iso->rx.ep;
|
|
} else if (iso->tx.ep != NULL) {
|
|
ep = iso->tx.ep;
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
return ep->stream->conn == params->acl &&
|
|
ep->cig_id == params->cig_id &&
|
|
ep->cis_id == params->cis_id;
|
|
}
|
|
|
|
static struct bt_audio_iso *audio_iso_get_or_new(struct bt_ascs *ascs,
|
|
uint8_t cig_id,
|
|
uint8_t cis_id)
|
|
{
|
|
struct bt_audio_iso *iso;
|
|
struct audio_iso_find_params params = {
|
|
.acl = ascs->conn,
|
|
.cig_id = cig_id,
|
|
.cis_id = cis_id,
|
|
};
|
|
|
|
iso = bt_audio_iso_find(audio_iso_find_func, ¶ms);
|
|
if (iso) {
|
|
return iso;
|
|
}
|
|
|
|
iso = bt_audio_iso_new();
|
|
if (!iso) {
|
|
return NULL;
|
|
}
|
|
|
|
bt_audio_iso_init(iso, &ascs_iso_ops);
|
|
|
|
return iso;
|
|
}
|
|
|
|
static void ase_stream_add(struct bt_ascs *ascs, struct bt_ascs_ase *ase,
|
|
struct bt_audio_stream *stream)
|
|
{
|
|
LOG_DBG("ase %p stream %p", ase, stream);
|
|
ase->ep.stream = stream;
|
|
stream->conn = ascs->conn;
|
|
stream->ep = &ase->ep;
|
|
}
|
|
|
|
static struct bt_ascs *ascs_get(struct bt_conn *conn)
|
|
{
|
|
struct bt_ascs *session = &sessions[bt_conn_index(conn)];
|
|
|
|
if (session->conn == NULL) {
|
|
session->conn = bt_conn_ref(conn);
|
|
}
|
|
|
|
return session;
|
|
}
|
|
|
|
NET_BUF_SIMPLE_DEFINE_STATIC(ase_buf, CONFIG_BT_L2CAP_TX_MTU);
|
|
|
|
static void ase_process(struct k_work *work)
|
|
{
|
|
struct bt_audio_ep *ep = CONTAINER_OF(work, struct bt_audio_ep, work);
|
|
struct bt_ascs_ase *ase = CONTAINER_OF(ep, struct bt_ascs_ase, ep);
|
|
struct bt_audio_stream *stream = ep->stream;
|
|
const uint8_t ep_state = ep->status.state;
|
|
struct bt_conn *conn = ase->ascs->conn;
|
|
|
|
LOG_DBG("ase %p, ep %p, ep.stream %p", ase, ep, stream);
|
|
|
|
if (conn != NULL && conn->state == BT_CONN_CONNECTED) {
|
|
ascs_ep_get_status(ep, &ase_buf);
|
|
|
|
bt_gatt_notify(conn, ep->server.attr,
|
|
ase_buf.data, ase_buf.len);
|
|
}
|
|
|
|
/* Stream shall be NULL in the idle state, and non-NULL otherwise */
|
|
__ASSERT(ep_state == BT_AUDIO_EP_STATE_IDLE ?
|
|
stream == NULL : stream != NULL,
|
|
"stream is NULL");
|
|
|
|
if (ep_state == BT_AUDIO_EP_STATE_RELEASING) {
|
|
if (ep->iso == NULL ||
|
|
ep->iso->chan.state == BT_ISO_STATE_DISCONNECTED) {
|
|
if (ep->iso != NULL) {
|
|
bt_audio_iso_unbind_ep(ep->iso, ep);
|
|
}
|
|
bt_audio_stream_detach(stream);
|
|
ascs_ep_set_state(ep, BT_AUDIO_EP_STATE_IDLE);
|
|
} else {
|
|
/* Either the client or the server may disconnect the
|
|
* CISes when entering the releasing state.
|
|
*/
|
|
const int err = bt_audio_stream_disconnect(stream);
|
|
|
|
if (err != 0) {
|
|
LOG_ERR("Failed to disconnect stream %p: %d", stream, err);
|
|
}
|
|
}
|
|
} else if (ep_state == BT_AUDIO_EP_STATE_ENABLING) {
|
|
/* SINK ASEs can autonomously go into the streaming state if
|
|
* the CIS is connected
|
|
*/
|
|
if (ep->dir == BT_AUDIO_DIR_SINK &&
|
|
ep->iso != NULL &&
|
|
ep->iso->chan.state == BT_ISO_STATE_CONNECTED) {
|
|
ascs_ep_set_state(ep, BT_AUDIO_EP_STATE_STREAMING);
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
if (ase->ep.status.id == POINTER_TO_UINT(BT_AUDIO_CHRC_USER_DATA(attr))) {
|
|
ase->ep.server.attr = attr;
|
|
|
|
return BT_GATT_ITER_STOP;
|
|
}
|
|
|
|
return BT_GATT_ITER_CONTINUE;
|
|
}
|
|
|
|
void ascs_ep_init(struct bt_audio_ep *ep, uint8_t id)
|
|
{
|
|
LOG_DBG("ep %p id 0x%02x", ep, id);
|
|
|
|
(void)memset(ep, 0, sizeof(*ep));
|
|
ep->status.id = id;
|
|
ep->dir = ASE_DIR(id);
|
|
|
|
k_work_init(&ep->work, ase_process);
|
|
}
|
|
|
|
static void ase_init(struct bt_ascs_ase *ase, uint8_t id)
|
|
{
|
|
memset(ase, 0, sizeof(*ase));
|
|
ascs_ep_init(&ase->ep, id);
|
|
|
|
/* Lookup ASE characteristic */
|
|
bt_gatt_foreach_attr_type(0x0001, 0xffff, ASE_UUID(id), NULL, 0, ase_attr_cb, ase);
|
|
|
|
__ASSERT(ase->ep.server.attr, "ASE characteristic not found\n");
|
|
}
|
|
|
|
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 = ascs_get(conn);
|
|
struct bt_ascs_ase *ase;
|
|
|
|
LOG_DBG("conn %p attr %p buf %p len %u offset %u", conn, attr, buf, len, offset);
|
|
|
|
ase = ase_get(ascs, POINTER_TO_UINT(BT_AUDIO_CHRC_USER_DATA(attr)));
|
|
if (!ase) {
|
|
LOG_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)
|
|
{
|
|
LOG_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)) {
|
|
LOG_ERR("No slot available for Codec Config");
|
|
return false;
|
|
}
|
|
|
|
cdata = &codec->data[codec->data_count];
|
|
|
|
if (data->data_len > sizeof(cdata->value)) {
|
|
LOG_ERR("Not enough space for Codec Config: %u > %zu", data->data_len,
|
|
sizeof(cdata->value));
|
|
return false;
|
|
}
|
|
|
|
LOG_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);
|
|
|
|
LOG_HEXDUMP_DBG(cdata->value, data->data_len, "data");
|
|
|
|
codec->data_count++;
|
|
|
|
return true;
|
|
}
|
|
|
|
struct codec_lookup_id_data {
|
|
uint8_t id;
|
|
struct bt_codec *codec;
|
|
};
|
|
|
|
static bool codec_lookup_id(const struct bt_pacs_cap *cap, void *user_data)
|
|
{
|
|
struct codec_lookup_id_data *data = user_data;
|
|
|
|
if (cap->codec->id == data->id) {
|
|
data->codec = cap->codec;
|
|
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
struct codec_lookup_id_data lookup_data = {
|
|
.id = id,
|
|
};
|
|
|
|
if (ep == NULL && codec == NULL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
LOG_DBG("ep %p dir %u codec id 0x%02x cid 0x%04x vid 0x%04x len %u", ep, ep->dir, id, cid,
|
|
vid, len);
|
|
|
|
bt_pacs_cap_foreach(ep->dir, codec_lookup_id, &lookup_data);
|
|
|
|
if (lookup_data.codec == NULL) {
|
|
LOG_DBG("Codec with id %u for dir %u is not supported by our capabilities",
|
|
id, ep->dir);
|
|
|
|
return -ENOENT;
|
|
}
|
|
|
|
if (codec == NULL) {
|
|
codec = &ep->codec;
|
|
}
|
|
|
|
codec->id = id;
|
|
codec->cid = cid;
|
|
codec->vid = vid;
|
|
codec->data_count = 0;
|
|
codec->path_id = lookup_data.codec->path_id;
|
|
|
|
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) {
|
|
LOG_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;
|
|
|
|
LOG_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) {
|
|
LOG_WRN("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) {
|
|
LOG_WRN("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:
|
|
LOG_WRN("Invalid operation in 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;
|
|
|
|
LOG_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) {
|
|
LOG_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)) {
|
|
LOG_WRN("Malformed ASE Config");
|
|
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
|
|
}
|
|
|
|
req = net_buf_simple_pull_mem(buf, sizeof(*req));
|
|
|
|
LOG_DBG("num_ases %u", req->num_ases);
|
|
|
|
if (req->num_ases < 1) {
|
|
LOG_WRN("Number_of_ASEs parameter value is less than 1");
|
|
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
|
|
} else if (buf->len < req->num_ases * sizeof(*cfg)) {
|
|
LOG_WRN("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)) {
|
|
LOG_WRN("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) {
|
|
LOG_WRN("Malformed ASE Codec Config len %u != %u", buf->len, cfg->cc_len);
|
|
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
|
|
}
|
|
|
|
LOG_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);
|
|
LOG_WRN("Unknown ase 0x%02x", cfg->ase);
|
|
continue;
|
|
}
|
|
|
|
err = ase_config(ascs, ase, cfg, buf);
|
|
if (err != 0) {
|
|
LOG_WRN("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,
|
|
struct bt_ascs *ascs,
|
|
uint8_t cig_id,
|
|
uint8_t cis_id)
|
|
{
|
|
struct bt_audio_ep *ep;
|
|
|
|
LOG_DBG("stream %p ep %p qos %p", stream, stream->ep, qos);
|
|
|
|
if (stream == NULL || stream->ep == NULL || qos == NULL) {
|
|
LOG_DBG("Invalid input stream, ep or qos pointers");
|
|
return -EINVAL;
|
|
}
|
|
|
|
ep = stream->ep;
|
|
|
|
switch (ep->status.state) {
|
|
/* Valid only if ASE_State field = 0x01 (Codec Configured) */
|
|
case BT_AUDIO_EP_STATE_CODEC_CONFIGURED:
|
|
/* or 0x02 (QoS Configured) */
|
|
case BT_AUDIO_EP_STATE_QOS_CONFIGURED:
|
|
break;
|
|
default:
|
|
LOG_WRN("Invalid operation in state: %s", bt_audio_ep_state_str(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) {
|
|
LOG_DBG("Application returned error: %d", err);
|
|
return err;
|
|
}
|
|
}
|
|
|
|
/* QoS->QoS transition. Unbind ISO if CIG/CIS changed. */
|
|
if (ep->iso != NULL && (ep->cig_id != cig_id || ep->cis_id != cis_id)) {
|
|
bt_audio_iso_unbind_ep(ep->iso, ep);
|
|
}
|
|
|
|
if (ep->iso == NULL) {
|
|
struct bt_audio_iso *iso;
|
|
|
|
iso = audio_iso_get_or_new(ascs, cig_id, cis_id);
|
|
if (iso == NULL) {
|
|
LOG_ERR("Could not allocate audio_iso");
|
|
return -ENOMEM;
|
|
}
|
|
|
|
if (bt_audio_iso_get_ep(iso, ep->dir) != NULL) {
|
|
LOG_ERR("iso %p already in use in dir %u",
|
|
&iso->chan, ep->dir);
|
|
bt_audio_iso_unref(iso);
|
|
return -EALREADY;
|
|
}
|
|
|
|
bt_audio_iso_bind_ep(iso, ep);
|
|
bt_audio_iso_unref(iso);
|
|
}
|
|
|
|
stream->qos = qos;
|
|
|
|
ascs_ep_set_state(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_ep *ep = &ase->ep;
|
|
struct bt_audio_stream *stream = ep->stream;
|
|
struct bt_codec_qos *cqos = &ep->qos;
|
|
const uint8_t cig_id = qos->cig;
|
|
const uint8_t cis_id = qos->cis;
|
|
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);
|
|
|
|
LOG_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, ase->ascs, cig_id, cis_id);
|
|
if (err) {
|
|
uint8_t reason = BT_ASCS_REASON_NONE;
|
|
|
|
LOG_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;
|
|
}
|
|
} else if (err == -EADDRINUSE) {
|
|
reason = BT_ASCS_REASON_CIS;
|
|
/* FIXME: Ugly workaround to send Response_Code
|
|
* 0x09 = Invalid Configuration Parameter Value
|
|
*/
|
|
err = -EINVAL;
|
|
}
|
|
|
|
memset(cqos, 0, sizeof(*cqos));
|
|
|
|
ascs_cp_rsp_add_errno(ASE_ID(ase), BT_ASCS_QOS_OP,
|
|
err, reason);
|
|
return;
|
|
} 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).
|
|
*/
|
|
if (ep->dir == BT_AUDIO_DIR_SINK) {
|
|
bt_audio_codec_to_iso_path(&ep->iso->rx.path,
|
|
stream->codec);
|
|
} else {
|
|
bt_audio_codec_to_iso_path(&ep->iso->tx.path,
|
|
stream->codec);
|
|
}
|
|
}
|
|
|
|
ep->cig_id = cig_id;
|
|
ep->cis_id = cis_id;
|
|
|
|
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));
|
|
|
|
LOG_DBG("num_ases %u", req->num_ases);
|
|
|
|
if (req->num_ases < 1) {
|
|
LOG_WRN("Number_of_ASEs parameter value is less than 1");
|
|
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
|
|
} else if (buf->len < req->num_ases * sizeof(*qos)) {
|
|
LOG_WRN("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));
|
|
|
|
LOG_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);
|
|
LOG_WRN("Unknown ase 0x%02x", qos->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;
|
|
|
|
meta = &codec->meta[codec->meta_count];
|
|
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);
|
|
|
|
LOG_DBG("#%zu: data: %s", codec->meta_count, bt_hex(meta->value, data->data_len));
|
|
|
|
codec->meta_count++;
|
|
|
|
return true;
|
|
}
|
|
|
|
struct ascs_parse_result {
|
|
int err;
|
|
size_t count;
|
|
const struct bt_audio_ep *ep;
|
|
};
|
|
|
|
static bool ascs_parse_metadata(struct bt_data *data, void *user_data)
|
|
{
|
|
struct ascs_parse_result *result = user_data;
|
|
const struct bt_audio_ep *ep = result->ep;
|
|
const uint8_t data_len = data->data_len;
|
|
const uint8_t data_type = data->type;
|
|
const uint8_t *data_value = data->data;
|
|
|
|
result->count++;
|
|
|
|
LOG_DBG("#%u type 0x%02x len %u", result->count, data_type, data_len);
|
|
|
|
if (result->count > CONFIG_BT_CODEC_MAX_METADATA_COUNT) {
|
|
LOG_ERR("Not enough buffers for Codec Config Metadata: %zu > %zu", result->count,
|
|
CONFIG_BT_CODEC_MAX_METADATA_LEN);
|
|
result->err = -ENOMEM;
|
|
|
|
return false;
|
|
}
|
|
|
|
if (data_len > CONFIG_BT_CODEC_MAX_METADATA_LEN) {
|
|
LOG_ERR("Not enough space for Codec Config Metadata: %u > %zu", data->data_len,
|
|
CONFIG_BT_CODEC_MAX_METADATA_LEN);
|
|
result->err = -ENOMEM;
|
|
|
|
return false;
|
|
}
|
|
|
|
/* The CAP acceptor shall not accept metadata with
|
|
* unsupported stream context.
|
|
*/
|
|
if (IS_ENABLED(CONFIG_BT_CAP_ACCEPTOR)) {
|
|
if (data_type == BT_AUDIO_METADATA_TYPE_STREAM_CONTEXT) {
|
|
const uint16_t context = sys_get_le16(data_value);
|
|
|
|
if (!bt_pacs_context_available(ep->dir, context)) {
|
|
LOG_WRN("Context 0x%04x is unavailable", context);
|
|
|
|
result->err = -EACCES;
|
|
|
|
return false;
|
|
}
|
|
} else if (data_type == BT_AUDIO_METADATA_TYPE_CCID_LIST) {
|
|
/* Verify that the CCID is a known CCID on the
|
|
* writing device
|
|
*/
|
|
for (uint8_t i = 0; i < data_len; i++) {
|
|
const uint8_t ccid = data_value[i];
|
|
|
|
if (!bt_cap_acceptor_ccid_exist(ep->stream->conn,
|
|
ccid)) {
|
|
LOG_WRN("CCID %u is unknown", ccid);
|
|
|
|
/* TBD:
|
|
* Should we reject the Metadata?
|
|
*
|
|
* Should unknown CCIDs trigger a
|
|
* discovery procedure for TBS or MCS?
|
|
*
|
|
* Or should we just accept as is, and
|
|
* then let the application decide?
|
|
*/
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static int ascs_verify_metadata(const struct net_buf_simple *buf,
|
|
struct bt_audio_ep *ep)
|
|
{
|
|
struct ascs_parse_result result = {
|
|
.count = 0U,
|
|
.err = 0,
|
|
.ep = ep
|
|
};
|
|
struct net_buf_simple meta_ltv;
|
|
|
|
/* Clone the buf to avoid pulling data from the original buffer */
|
|
net_buf_simple_clone(buf, &meta_ltv);
|
|
|
|
/* Parse LTV entries */
|
|
bt_data_parse(&meta_ltv, ascs_parse_metadata, &result);
|
|
|
|
/* Check if all entries could be parsed */
|
|
if (meta_ltv.len != 0) {
|
|
LOG_ERR("Unable to parse Metadata: len %u", meta_ltv.len);
|
|
|
|
if (meta_ltv.len > 2) {
|
|
/* Value of the Metadata Type field in error */
|
|
return meta_ltv.data[2];
|
|
}
|
|
|
|
return -EINVAL;
|
|
}
|
|
|
|
return result.err;
|
|
}
|
|
|
|
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_ltv;
|
|
int err;
|
|
|
|
if (ep == NULL && codec == NULL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
LOG_DBG("ep %p len %u codec %p", ep, len, codec);
|
|
|
|
if (len == 0) {
|
|
(void)memset(codec->meta, 0, sizeof(codec->meta));
|
|
return 0;
|
|
}
|
|
|
|
if (codec == NULL) {
|
|
codec = &ep->codec;
|
|
}
|
|
|
|
/* Extract metadata LTV for this specific endpoint */
|
|
net_buf_simple_init_with_data(&meta_ltv,
|
|
net_buf_simple_pull_mem(buf, len),
|
|
len);
|
|
|
|
err = ascs_verify_metadata(&meta_ltv, ep);
|
|
if (err != 0) {
|
|
return err;
|
|
}
|
|
|
|
/* reset cached metadata */
|
|
ep->codec.meta_count = 0;
|
|
|
|
/* store data contents */
|
|
bt_data_parse(&meta_ltv, ascs_codec_store_metadata, codec);
|
|
|
|
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_codec_data metadata_backup[CONFIG_BT_CODEC_MAX_DATA_COUNT];
|
|
struct bt_audio_stream *stream;
|
|
struct bt_audio_ep *ep;
|
|
uint8_t state;
|
|
int err;
|
|
|
|
LOG_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:
|
|
LOG_WRN("Invalid operation in state: %s", bt_audio_ep_state_str(state));
|
|
err = -EBADMSG;
|
|
ascs_cp_rsp_add_errno(ASE_ID(ase), op, err,
|
|
buf->len ? *buf->data : 0x00);
|
|
return err;
|
|
}
|
|
|
|
if (!meta->len) {
|
|
goto done;
|
|
}
|
|
|
|
/* Backup existing metadata */
|
|
(void)memcpy(metadata_backup, ep->codec.meta, sizeof(metadata_backup));
|
|
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) {
|
|
/* Restore backup */
|
|
(void)memcpy(ep->codec.meta, metadata_backup,
|
|
sizeof(metadata_backup));
|
|
|
|
LOG_ERR("Metadata failed: %d", err);
|
|
ascs_cp_rsp_add_errno(ASE_ID(ase), op, err,
|
|
buf->len ? *buf->data : 0x00);
|
|
return err;
|
|
}
|
|
|
|
/* 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;
|
|
|
|
LOG_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;
|
|
LOG_WRN("Invalid operation in 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) {
|
|
LOG_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);
|
|
|
|
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));
|
|
|
|
LOG_DBG("num_ases %u", req->num_ases);
|
|
|
|
if (req->num_ases < 1) {
|
|
LOG_WRN("Number_of_ASEs parameter value is less than 1");
|
|
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
|
|
} else if (buf->len < req->num_ases * sizeof(*meta)) {
|
|
LOG_WRN("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));
|
|
|
|
LOG_DBG("ase 0x%02x meta->len %u", meta->ase, meta->len);
|
|
|
|
if (buf->len < meta->len) {
|
|
LOG_WRN("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);
|
|
LOG_WRN("Unknown ase 0x%02x", meta->ase);
|
|
continue;
|
|
}
|
|
|
|
ase_enable(ase, meta, buf);
|
|
}
|
|
|
|
return buf->size;
|
|
}
|
|
|
|
static void ase_start(struct bt_ascs_ase *ase)
|
|
{
|
|
struct bt_audio_ep *ep;
|
|
|
|
LOG_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) {
|
|
LOG_WRN("Invalid operation in state: %s", bt_audio_ep_state_str(ep->status.state));
|
|
ascs_cp_rsp_add_errno(ASE_ID(ase), BT_ASCS_START_OP, -EBADMSG,
|
|
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_DIR_SINK) {
|
|
LOG_WRN("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;
|
|
}
|
|
|
|
ep->receiver_ready = true;
|
|
|
|
if (ep->iso->chan.state == BT_ISO_STATE_CONNECTED) {
|
|
int err;
|
|
|
|
err = ase_stream_start(ep->stream);
|
|
if (err) {
|
|
LOG_ERR("Start failed: %d", err);
|
|
ascs_cp_rsp_add(ASE_ID(ase), BT_ASCS_START_OP, err,
|
|
BT_ASCS_REASON_NONE);
|
|
return;
|
|
}
|
|
}
|
|
|
|
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));
|
|
|
|
LOG_DBG("num_ases %u", req->num_ases);
|
|
|
|
if (req->num_ases < 1) {
|
|
LOG_WRN("Number_of_ASEs parameter value is less than 1");
|
|
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
|
|
} else if (buf->len < req->num_ases) {
|
|
LOG_WRN("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);
|
|
|
|
LOG_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);
|
|
LOG_WRN("Unknown ase 0x%02x", id);
|
|
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));
|
|
|
|
LOG_DBG("num_ases %u", req->num_ases);
|
|
|
|
if (req->num_ases < 1) {
|
|
LOG_WRN("Number_of_ASEs parameter value is less than 1");
|
|
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
|
|
} else if (buf->len < req->num_ases) {
|
|
LOG_WRN("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);
|
|
|
|
LOG_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, 0x00);
|
|
LOG_WRN("Unknown ase 0x%02x", id);
|
|
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;
|
|
|
|
LOG_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_DIR_SINK) {
|
|
LOG_WRN("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) {
|
|
LOG_WRN("Invalid operation in 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) {
|
|
LOG_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 != -ENOTCONN && err != 0) {
|
|
LOG_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) {
|
|
LOG_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));
|
|
|
|
LOG_DBG("num_ases %u", req->num_ases);
|
|
|
|
if (req->num_ases < 1) {
|
|
LOG_WRN("Number_of_ASEs parameter value is less than 1");
|
|
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
|
|
} else if (buf->len < req->num_ases) {
|
|
LOG_WRN("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);
|
|
|
|
LOG_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);
|
|
LOG_WRN("Unknown ase 0x%02x", id);
|
|
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));
|
|
|
|
LOG_DBG("num_ases %u", req->num_ases);
|
|
|
|
if (req->num_ases < 1) {
|
|
LOG_WRN("Number_of_ASEs parameter value is less than 1");
|
|
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
|
|
} else if (buf->len < req->num_ases * sizeof(*meta)) {
|
|
LOG_WRN("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) {
|
|
LOG_WRN("Malformed ASE Metadata: len %u < %u", buf->len, meta->len);
|
|
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
|
|
}
|
|
|
|
LOG_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);
|
|
LOG_WRN("Unknown ase 0x%02x", meta->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));
|
|
|
|
LOG_DBG("num_ases %u", req->num_ases);
|
|
|
|
if (req->num_ases < 1) {
|
|
LOG_WRN("Number_of_ASEs parameter value is less than 1");
|
|
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
|
|
} else if (buf->len < req->num_ases) {
|
|
LOG_WRN("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);
|
|
|
|
LOG_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);
|
|
LOG_WRN("Unknown ase 0x%02x", id);
|
|
continue;
|
|
}
|
|
|
|
if (ase->ep.status.state == BT_AUDIO_EP_STATE_IDLE ||
|
|
ase->ep.status.state == BT_AUDIO_EP_STATE_RELEASING) {
|
|
LOG_WRN("Invalid operation in state: %s",
|
|
bt_audio_ep_state_str(ase->ep.status.state));
|
|
ascs_cp_rsp_add(id, BT_ASCS_RELEASE_OP,
|
|
BT_ASCS_RSP_INVALID_ASE_STATE, BT_ASCS_REASON_NONE);
|
|
continue;
|
|
}
|
|
|
|
ase_release(ase);
|
|
}
|
|
|
|
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 = ascs_get(conn);
|
|
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));
|
|
|
|
LOG_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);
|
|
|
|
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);
|
|
LOG_DBG("Unknown opcode");
|
|
goto respond;
|
|
}
|
|
|
|
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:
|
|
control_point_notify(ascs->conn, rsp_buf.data, rsp_buf.len);
|
|
|
|
return len;
|
|
}
|
|
|
|
#define BT_ASCS_ASE_DEFINE(_uuid, _id) \
|
|
BT_AUDIO_CHRC(_uuid, \
|
|
BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, \
|
|
BT_GATT_PERM_READ_ENCRYPT, \
|
|
ascs_ase_read, NULL, UINT_TO_POINTER(_id)), \
|
|
BT_AUDIO_CCC(ascs_ase_cfg_changed)
|
|
#define BT_ASCS_ASE_SNK_DEFINE(_n, ...) BT_ASCS_ASE_DEFINE(BT_UUID_ASCS_ASE_SNK, (_n) + 1)
|
|
#define BT_ASCS_ASE_SRC_DEFINE(_n, ...) BT_ASCS_ASE_DEFINE(BT_UUID_ASCS_ASE_SRC, (_n) + 1 + \
|
|
CONFIG_BT_ASCS_ASE_SNK_COUNT)
|
|
|
|
BT_GATT_SERVICE_DEFINE(ascs_svc,
|
|
BT_GATT_PRIMARY_SERVICE(BT_UUID_ASCS),
|
|
BT_AUDIO_CHRC(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_AUDIO_CCC(ascs_cp_cfg_changed),
|
|
#if CONFIG_BT_ASCS_ASE_SNK_COUNT > 0
|
|
LISTIFY(CONFIG_BT_ASCS_ASE_SNK_COUNT, BT_ASCS_ASE_SNK_DEFINE, (,)),
|
|
#endif /* CONFIG_BT_ASCS_ASE_SNK_COUNT > 0 */
|
|
#if CONFIG_BT_ASCS_ASE_SRC_COUNT > 0
|
|
LISTIFY(CONFIG_BT_ASCS_ASE_SRC_COUNT, BT_ASCS_ASE_SRC_DEFINE, (,)),
|
|
#endif /* CONFIG_BT_ASCS_ASE_SRC_COUNT > 0 */
|
|
);
|
|
|
|
static int control_point_notify(struct bt_conn *conn, const void *data, uint16_t len)
|
|
{
|
|
return bt_gatt_notify_uuid(conn, BT_UUID_ASCS_ASE_CP, ascs_svc.attrs, data, len);
|
|
}
|
|
|
|
#endif /* BT_AUDIO_UNICAST_SERVER */
|