Move the network buffer header file from zephyr/net/buf.h to zephyr/net_buf.h as the implementation now lives outside of the networking subsystem. Add (deprecated) zephyr/net/buf.h header to maintain compatibility with old file path. Signed-off-by: Henrik Brix Andersen <henrik@brixandersen.dk>
3144 lines
82 KiB
C
3144 lines
82 KiB
C
/* @file
|
|
* @brief Bluetooth ASCS
|
|
*/
|
|
/*
|
|
* Copyright (c) 2020 Intel Corporation
|
|
* Copyright (c) 2022-2023 Nordic Semiconductor ASA
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
#include <errno.h>
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
#include <sys/types.h>
|
|
|
|
#include <zephyr/autoconf.h>
|
|
#include <zephyr/bluetooth/att.h>
|
|
#include <zephyr/bluetooth/audio/audio.h>
|
|
#include <zephyr/bluetooth/audio/bap.h>
|
|
#include <zephyr/bluetooth/audio/pacs.h>
|
|
#include <zephyr/bluetooth/bluetooth.h>
|
|
#include <zephyr/bluetooth/conn.h>
|
|
#include <zephyr/bluetooth/gatt.h>
|
|
#include <zephyr/bluetooth/hci_types.h>
|
|
#include <zephyr/bluetooth/iso.h>
|
|
#include <zephyr/bluetooth/uuid.h>
|
|
#include <zephyr/kernel.h>
|
|
#include <zephyr/logging/log.h>
|
|
#include <zephyr/net_buf.h>
|
|
#include <zephyr/sys/__assert.h>
|
|
#include <zephyr/sys/byteorder.h>
|
|
#include <zephyr/sys/check.h>
|
|
#include <zephyr/sys/util.h>
|
|
#include <zephyr/sys/util_macro.h>
|
|
#include <zephyr/toolchain.h>
|
|
|
|
LOG_MODULE_REGISTER(bt_ascs, CONFIG_BT_ASCS_LOG_LEVEL);
|
|
|
|
#include "common/bt_str.h"
|
|
#include "common/assert.h"
|
|
|
|
#include "../host/att_internal.h"
|
|
|
|
#include "ascs_internal.h"
|
|
#include "audio_internal.h"
|
|
#include "bap_endpoint.h"
|
|
#include "bap_iso.h"
|
|
#include "bap_stream.h"
|
|
#include "bap_unicast_server.h"
|
|
#include "pacs_internal.h"
|
|
#include "cap_internal.h"
|
|
|
|
#define ASE_BUF_SEM_TIMEOUT K_MSEC(CONFIG_BT_ASCS_ASE_BUF_TIMEOUT)
|
|
|
|
#define MAX_ASES_SESSIONS CONFIG_BT_MAX_CONN * \
|
|
(CONFIG_BT_ASCS_ASE_SNK_COUNT + \
|
|
CONFIG_BT_ASCS_ASE_SRC_COUNT)
|
|
|
|
#define NTF_HEADER_SIZE (3) /* opcode (1) + handle (2) */
|
|
|
|
BUILD_ASSERT(CONFIG_BT_ASCS_MAX_ACTIVE_ASES <= MAX(MAX_ASES_SESSIONS,
|
|
CONFIG_BT_ISO_MAX_CHAN),
|
|
"Max active ASEs are set to more than actual number of ASEs or ISOs");
|
|
|
|
#if defined(CONFIG_BT_BAP_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)
|
|
#define BT_BAP_ASCS_RSP_NULL ((struct bt_bap_ascs_rsp[]) { BT_BAP_ASCS_RSP(0, 0) })
|
|
|
|
static struct bt_ascs_ase {
|
|
struct bt_conn *conn;
|
|
struct bt_bap_ep ep;
|
|
const struct bt_gatt_attr *attr;
|
|
struct k_work_delayable disconnect_work;
|
|
struct k_work_delayable state_transition_work;
|
|
enum bt_bap_ep_state state_pending;
|
|
bool unexpected_iso_link_loss;
|
|
} ase_pool[CONFIG_BT_ASCS_MAX_ACTIVE_ASES];
|
|
|
|
/* Minimum state size when in the codec configured state */
|
|
#define MIN_CONFIG_STATE_SIZE (1 + 1 + 1 + 1 + 1 + 2 + 3 + 3 + 3 + 3 + 5 + 1)
|
|
/* Minimum state size when in the QoS configured state */
|
|
#define MIN_QOS_STATE_SIZE (1 + 1 + 1 + 1 + 3 + 1 + 1 + 2 + 1 + 2 + 3 + 1 + 1 + 1)
|
|
|
|
/* Calculate the size requirement of the ASE BUF, based on the maximum possible
|
|
* size of the Codec Configured state or the QoS Configured state, as either
|
|
* of them can be the largest state
|
|
*/
|
|
#define ASE_BUF_SIZE \
|
|
MIN(BT_ATT_MAX_ATTRIBUTE_LEN, \
|
|
MAX(MIN_CONFIG_STATE_SIZE + CONFIG_BT_AUDIO_CODEC_CFG_MAX_DATA_SIZE, \
|
|
MIN_QOS_STATE_SIZE + CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE))
|
|
|
|
/* Verify that the prepare count is large enough to cover the maximum value we support a client
|
|
* writing
|
|
*/
|
|
BUILD_ASSERT(
|
|
(BT_ATT_BUF_SIZE - NTF_HEADER_SIZE) >= ASE_BUF_SIZE ||
|
|
DIV_ROUND_UP(ASE_BUF_SIZE, (BT_ATT_BUF_SIZE - NTF_HEADER_SIZE)) <=
|
|
CONFIG_BT_ATT_PREPARE_COUNT,
|
|
"CONFIG_BT_ATT_PREPARE_COUNT not large enough to cover the maximum supported ASCS value");
|
|
|
|
/* It is mandatory to support long writes in ASCS unconditionally, and thus
|
|
* CONFIG_BT_ATT_PREPARE_COUNT must be at least 1 to support the feature
|
|
*/
|
|
BUILD_ASSERT(CONFIG_BT_ATT_PREPARE_COUNT > 0, "CONFIG_BT_ATT_PREPARE_COUNT shall be at least 1");
|
|
|
|
static const struct bt_bap_unicast_server_cb *unicast_server_cb;
|
|
|
|
static K_SEM_DEFINE(ase_buf_sem, 1, 1);
|
|
NET_BUF_SIMPLE_DEFINE_STATIC(ase_buf, ASE_BUF_SIZE);
|
|
|
|
static int control_point_notify(struct bt_conn *conn, const void *data, uint16_t len);
|
|
static int ascs_ep_get_status(struct bt_bap_ep *ep, struct net_buf_simple *buf);
|
|
|
|
static void ascs_app_rsp_warn_valid(const struct bt_bap_ascs_rsp *rsp)
|
|
{
|
|
/* Validate application error code */
|
|
switch (rsp->code) {
|
|
case BT_BAP_ASCS_RSP_CODE_SUCCESS:
|
|
case BT_BAP_ASCS_RSP_CODE_CAP_UNSUPPORTED:
|
|
case BT_BAP_ASCS_RSP_CODE_NO_MEM:
|
|
case BT_BAP_ASCS_RSP_CODE_UNSPECIFIED:
|
|
case BT_BAP_ASCS_RSP_CODE_CONF_UNSUPPORTED:
|
|
case BT_BAP_ASCS_RSP_CODE_CONF_REJECTED:
|
|
case BT_BAP_ASCS_RSP_CODE_METADATA_UNSUPPORTED:
|
|
case BT_BAP_ASCS_RSP_CODE_METADATA_REJECTED:
|
|
break;
|
|
default:
|
|
LOG_WRN("Invalid application error code: %u", rsp->code);
|
|
return;
|
|
}
|
|
|
|
/* Validate application error code and reason combinations */
|
|
switch (rsp->code) {
|
|
case BT_BAP_ASCS_RSP_CODE_SUCCESS:
|
|
case BT_BAP_ASCS_RSP_CODE_CAP_UNSUPPORTED:
|
|
case BT_BAP_ASCS_RSP_CODE_NO_MEM:
|
|
case BT_BAP_ASCS_RSP_CODE_UNSPECIFIED:
|
|
if (rsp->reason != BT_BAP_ASCS_REASON_NONE) {
|
|
LOG_WRN("Invalid reason %u for code %u", rsp->reason, rsp->code);
|
|
}
|
|
break;
|
|
case BT_BAP_ASCS_RSP_CODE_CONF_UNSUPPORTED:
|
|
case BT_BAP_ASCS_RSP_CODE_CONF_REJECTED:
|
|
if (!IN_RANGE(rsp->reason, BT_BAP_ASCS_REASON_NONE, BT_BAP_ASCS_REASON_CIS)) {
|
|
LOG_WRN("Invalid reason %u for code %u", rsp->reason, rsp->code);
|
|
}
|
|
break;
|
|
case BT_BAP_ASCS_RSP_CODE_METADATA_UNSUPPORTED:
|
|
case BT_BAP_ASCS_RSP_CODE_METADATA_REJECTED:
|
|
if (!BT_AUDIO_METADATA_TYPE_IS_KNOWN(rsp->metadata_type)) {
|
|
LOG_WRN("Invalid metadata type %u for code %u", rsp->metadata_type,
|
|
rsp->code);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static bool is_valid_ase_id(uint8_t ase_id)
|
|
{
|
|
return IN_RANGE(ase_id, 1, ASE_COUNT);
|
|
}
|
|
|
|
static enum bt_bap_ep_state ascs_ep_get_state(struct bt_bap_ep *ep)
|
|
{
|
|
return ep->status.state;
|
|
}
|
|
|
|
static void ase_free(struct bt_ascs_ase *ase)
|
|
{
|
|
__ASSERT(ase && ase->conn, "Non-existing ASE");
|
|
|
|
LOG_DBG("conn %p ase %p id 0x%02x", (void *)ase->conn, ase, ase->ep.status.id);
|
|
|
|
if (ase->ep.iso != NULL) {
|
|
bt_bap_iso_unbind_ep(ase->ep.iso, &ase->ep);
|
|
}
|
|
|
|
bt_conn_unref(ase->conn);
|
|
ase->conn = NULL;
|
|
|
|
(void)k_work_cancel_delayable(&ase->disconnect_work);
|
|
(void)k_work_cancel_delayable(&ase->state_transition_work);
|
|
}
|
|
|
|
static uint16_t get_max_ntf_size(struct bt_conn *conn)
|
|
{
|
|
const uint16_t mtu = conn == NULL ? 0 : bt_gatt_get_mtu(conn);
|
|
|
|
if (mtu > NTF_HEADER_SIZE) {
|
|
return mtu - NTF_HEADER_SIZE;
|
|
}
|
|
|
|
return 0U;
|
|
}
|
|
|
|
static int ase_state_notify(struct bt_ascs_ase *ase)
|
|
{
|
|
struct bt_conn *conn = ase->conn;
|
|
struct bt_conn_info conn_info;
|
|
uint16_t max_ntf_size;
|
|
uint16_t ntf_size;
|
|
int err;
|
|
|
|
__ASSERT_NO_MSG(conn != NULL);
|
|
|
|
err = bt_conn_get_info(conn, &conn_info);
|
|
__ASSERT_NO_MSG(err == 0);
|
|
|
|
if (conn_info.state != BT_CONN_STATE_CONNECTED ||
|
|
!bt_gatt_is_subscribed(conn, ase->attr, BT_GATT_CCC_NOTIFY)) {
|
|
return 0;
|
|
}
|
|
|
|
err = k_sem_take(&ase_buf_sem, ASE_BUF_SEM_TIMEOUT);
|
|
if (err != 0) {
|
|
LOG_WRN("Failed to take ase_buf_sem: %d", err);
|
|
|
|
return err;
|
|
}
|
|
|
|
ascs_ep_get_status(&ase->ep, &ase_buf);
|
|
|
|
max_ntf_size = get_max_ntf_size(conn);
|
|
|
|
ntf_size = MIN(max_ntf_size, ase_buf.len);
|
|
if (ntf_size < ase_buf.len) {
|
|
LOG_DBG("Sending truncated notification (%u / %u)",
|
|
ntf_size, ase_buf.len);
|
|
}
|
|
|
|
err = bt_gatt_notify(conn, ase->attr, ase_buf.data, ntf_size);
|
|
|
|
k_sem_give(&ase_buf_sem);
|
|
|
|
return err;
|
|
}
|
|
|
|
static void ascs_disconnect_stream_work_handler(struct k_work *work)
|
|
{
|
|
struct k_work_delayable *d_work = k_work_delayable_from_work(work);
|
|
struct bt_ascs_ase *ase = CONTAINER_OF(d_work, struct bt_ascs_ase,
|
|
disconnect_work);
|
|
struct bt_bap_ep *ep = &ase->ep;
|
|
struct bt_bap_stream *stream = ep->stream;
|
|
struct bt_bap_stream *pair_stream;
|
|
|
|
__ASSERT(ep != NULL && ep->iso && stream != NULL,
|
|
"Invalid endpoint %p, iso %p or stream %p",
|
|
ep, ep == NULL ? NULL : ep->iso, stream);
|
|
|
|
if (ep->dir == BT_AUDIO_DIR_SINK) {
|
|
pair_stream = ep->iso->tx.stream;
|
|
} else {
|
|
pair_stream = ep->iso->rx.stream;
|
|
}
|
|
|
|
LOG_DBG("ase %p ep %p stream %p pair_stream %p",
|
|
ase, ep, stream, pair_stream);
|
|
|
|
if (pair_stream != NULL) {
|
|
struct bt_ascs_ase *pair_ase;
|
|
|
|
__ASSERT(pair_stream->ep != NULL, "Invalid pair_stream %p",
|
|
pair_stream);
|
|
|
|
if (pair_stream->ep->status.state == BT_BAP_EP_STATE_STREAMING) {
|
|
/* Should not disconnect ISO if the stream is paired
|
|
* with another one in the streaming state
|
|
*/
|
|
|
|
return;
|
|
}
|
|
|
|
pair_ase = CONTAINER_OF(pair_stream->ep, struct bt_ascs_ase,
|
|
ep);
|
|
|
|
/* Cancel pair ASE disconnect work if pending */
|
|
(void)k_work_cancel_delayable(&pair_ase->disconnect_work);
|
|
}
|
|
|
|
if (stream != NULL &&
|
|
ep->iso != NULL &&
|
|
(ep->iso->chan.state == BT_ISO_STATE_CONNECTED ||
|
|
ep->iso->chan.state == BT_ISO_STATE_CONNECTING)) {
|
|
const int err = bt_bap_stream_disconnect(stream);
|
|
|
|
if (err != 0) {
|
|
LOG_ERR("Failed to disconnect CIS %p: %d",
|
|
stream, err);
|
|
}
|
|
}
|
|
}
|
|
|
|
static int ascs_disconnect_stream(struct bt_bap_stream *stream)
|
|
{
|
|
struct bt_ascs_ase *ase = CONTAINER_OF(stream->ep, struct bt_ascs_ase,
|
|
ep);
|
|
|
|
LOG_DBG("%p", stream);
|
|
|
|
return k_work_reschedule(&ase->disconnect_work,
|
|
K_MSEC(CONFIG_BT_ASCS_ISO_DISCONNECT_DELAY));
|
|
}
|
|
|
|
static void ase_enter_state_idle(struct bt_ascs_ase *ase)
|
|
{
|
|
struct bt_bap_stream *stream = ase->ep.stream;
|
|
struct bt_bap_stream_ops *ops;
|
|
|
|
__ASSERT_NO_MSG(stream != NULL);
|
|
|
|
ase->ep.receiver_ready = false;
|
|
|
|
if (stream->conn != NULL) {
|
|
bt_conn_unref(stream->conn);
|
|
stream->conn = NULL;
|
|
}
|
|
|
|
ops = stream->ops;
|
|
if (ops != NULL && ops->released != NULL) {
|
|
ops->released(stream);
|
|
}
|
|
|
|
ase_free(ase);
|
|
}
|
|
|
|
static void ase_enter_state_codec_configured(struct bt_ascs_ase *ase)
|
|
{
|
|
struct bt_bap_stream *stream = ase->ep.stream;
|
|
struct bt_bap_stream_ops *ops;
|
|
|
|
__ASSERT_NO_MSG(stream != NULL);
|
|
|
|
ase->ep.receiver_ready = false;
|
|
|
|
ops = stream->ops;
|
|
if (ops != NULL && ops->configured != NULL) {
|
|
ops->configured(stream, &ase->ep.qos_pref);
|
|
}
|
|
}
|
|
|
|
static void ase_enter_state_qos_configured(struct bt_ascs_ase *ase)
|
|
{
|
|
struct bt_bap_stream *stream = ase->ep.stream;
|
|
struct bt_bap_stream_ops *ops;
|
|
|
|
__ASSERT_NO_MSG(stream != NULL);
|
|
|
|
ase->ep.receiver_ready = false;
|
|
|
|
ops = stream->ops;
|
|
if (ops != NULL && ops->qos_set != NULL) {
|
|
ops->qos_set(stream);
|
|
}
|
|
}
|
|
|
|
static void ase_enter_state_enabling(struct bt_ascs_ase *ase)
|
|
{
|
|
struct bt_bap_stream *stream = ase->ep.stream;
|
|
struct bt_bap_stream_ops *ops;
|
|
|
|
__ASSERT_NO_MSG(stream != NULL);
|
|
|
|
ops = stream->ops;
|
|
if (ops != NULL && ops->enabled != NULL) {
|
|
ops->enabled(stream);
|
|
}
|
|
|
|
/* SINK ASEs can autonomously go into the streaming state if the CIS is connected */
|
|
if (ase->ep.dir == BT_AUDIO_DIR_SINK && ase->ep.receiver_ready && ase->ep.iso != NULL &&
|
|
ase->ep.iso->chan.state == BT_ISO_STATE_CONNECTED) {
|
|
ascs_ep_set_state(&ase->ep, BT_BAP_EP_STATE_STREAMING);
|
|
}
|
|
}
|
|
|
|
static void ase_enter_state_streaming(struct bt_ascs_ase *ase)
|
|
{
|
|
struct bt_bap_stream *stream = ase->ep.stream;
|
|
struct bt_bap_stream_ops *ops;
|
|
|
|
__ASSERT_NO_MSG(stream != NULL);
|
|
|
|
ops = stream->ops;
|
|
if (ops != NULL && ops->started != NULL) {
|
|
ops->started(stream);
|
|
}
|
|
}
|
|
|
|
static void ase_metadata_updated(struct bt_ascs_ase *ase)
|
|
{
|
|
struct bt_bap_stream *stream = ase->ep.stream;
|
|
struct bt_bap_stream_ops *ops;
|
|
|
|
__ASSERT_NO_MSG(stream != NULL);
|
|
|
|
ops = stream->ops;
|
|
if (ops != NULL && ops->metadata_updated != NULL) {
|
|
ops->metadata_updated(stream);
|
|
}
|
|
}
|
|
|
|
static void ase_exit_state_streaming(struct bt_ascs_ase *ase)
|
|
{
|
|
struct bt_bap_stream *stream = ase->ep.stream;
|
|
struct bt_bap_stream_ops *ops;
|
|
const enum bt_bap_ep_state next_state = ascs_ep_get_state(&ase->ep);
|
|
uint8_t reason = ase->ep.reason;
|
|
|
|
__ASSERT_NO_MSG(stream != NULL);
|
|
|
|
if (reason == BT_HCI_ERR_SUCCESS) {
|
|
/* Default to BT_HCI_ERR_UNSPECIFIED if no other reason is set */
|
|
reason = BT_HCI_ERR_UNSPECIFIED;
|
|
}
|
|
|
|
ops = stream->ops;
|
|
|
|
/*
|
|
* On link-loss we go from streaming state to QOS configured state,
|
|
* and it makes sense to do the disabled callback before entering the
|
|
* QOS configured state
|
|
*/
|
|
if (next_state == BT_BAP_EP_STATE_QOS_CONFIGURED) {
|
|
if (ops != NULL && ops->disabled != NULL) {
|
|
ops->disabled(stream);
|
|
} else {
|
|
LOG_WRN("No callback for disabled set");
|
|
}
|
|
}
|
|
|
|
if (ops != NULL && ops->stopped != NULL) {
|
|
ops->stopped(stream, reason);
|
|
} else {
|
|
LOG_WRN("No callback for stopped set");
|
|
}
|
|
}
|
|
|
|
static void ase_exit_state_enabling(struct bt_ascs_ase *ase)
|
|
{
|
|
struct bt_bap_stream *stream = ase->ep.stream;
|
|
struct bt_bap_stream_ops *ops;
|
|
const enum bt_bap_ep_state next_state = ascs_ep_get_state(&ase->ep);
|
|
|
|
ops = stream->ops;
|
|
|
|
/*
|
|
* When the EP direction is BT_AUDIO_DIR_SOURCE the state machine goes from
|
|
* enabled to disabled where the disabled callback will be called,
|
|
* for BT_AUDIO_DIR_SINK we go from enabled to qos_configured,
|
|
* and logically we have to do the disabled callback first
|
|
*/
|
|
if (next_state == BT_BAP_EP_STATE_QOS_CONFIGURED && ase->ep.dir == BT_AUDIO_DIR_SINK) {
|
|
if (ops != NULL && ops->disabled != NULL) {
|
|
ops->disabled(stream);
|
|
} else {
|
|
LOG_WRN("No callback for disabled set");
|
|
}
|
|
}
|
|
}
|
|
|
|
static void ase_enter_state_disabling(struct bt_ascs_ase *ase)
|
|
{
|
|
struct bt_bap_stream *stream = ase->ep.stream;
|
|
struct bt_bap_stream_ops *ops;
|
|
|
|
__ASSERT_NO_MSG(stream != NULL);
|
|
|
|
ase->ep.receiver_ready = false;
|
|
|
|
ops = stream->ops;
|
|
if (ops != NULL && ops->disabled != NULL) {
|
|
ops->disabled(stream);
|
|
}
|
|
}
|
|
|
|
static void ase_enter_state_releasing(struct bt_ascs_ase *ase)
|
|
{
|
|
struct bt_bap_stream *stream = ase->ep.stream;
|
|
|
|
__ASSERT_NO_MSG(stream != NULL);
|
|
|
|
ase->ep.receiver_ready = false;
|
|
|
|
/* Either the client or the server may disconnect the CISes when entering the releasing
|
|
* state.
|
|
*/
|
|
if (bt_bap_stream_can_disconnect(stream)) {
|
|
int err;
|
|
|
|
err = ascs_disconnect_stream(stream);
|
|
if (err < 0) {
|
|
LOG_ERR("Failed to disconnect stream %p: %d", stream, err);
|
|
}
|
|
} else {
|
|
ascs_ep_set_state(&ase->ep, BT_BAP_EP_STATE_IDLE);
|
|
}
|
|
}
|
|
|
|
static void state_transition_work_handler(struct k_work *work)
|
|
{
|
|
struct k_work_delayable *d_work = k_work_delayable_from_work(work);
|
|
struct bt_ascs_ase *ase = CONTAINER_OF(d_work, struct bt_ascs_ase, state_transition_work);
|
|
const enum bt_bap_ep_state old_state = ascs_ep_get_state(&ase->ep);
|
|
const enum bt_bap_ep_state new_state = ase->state_pending;
|
|
int err;
|
|
|
|
ase->ep.status.state = new_state;
|
|
|
|
/* Notify ASE state */
|
|
if (ase->conn != NULL) {
|
|
err = ase_state_notify(ase);
|
|
if (err == -ENOMEM) {
|
|
struct bt_conn_info info;
|
|
uint32_t retry_delay_ms;
|
|
|
|
/* Revert back to old state */
|
|
ase->ep.status.state = old_state;
|
|
|
|
err = bt_conn_get_info(ase->conn, &info);
|
|
__ASSERT_NO_MSG(err == 0);
|
|
|
|
retry_delay_ms = BT_CONN_INTERVAL_TO_MS(info.le.interval);
|
|
|
|
/* Reschedule the state transition */
|
|
err = k_work_reschedule(d_work, K_MSEC(retry_delay_ms));
|
|
if (err >= 0) {
|
|
LOG_WRN("Out of buffers for ase state notification. "
|
|
"Will retry in %dms", retry_delay_ms);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (err < 0) {
|
|
LOG_ERR("Failed to notify ASE state (err %d)", err);
|
|
}
|
|
}
|
|
|
|
LOG_DBG("ase %p ep %p id 0x%02x %s -> %s", ase, &ase->ep, ase->ep.status.id,
|
|
bt_bap_ep_state_str(old_state), bt_bap_ep_state_str(new_state));
|
|
|
|
if (old_state == new_state) {
|
|
switch (new_state) {
|
|
case BT_BAP_EP_STATE_ENABLING:
|
|
case BT_BAP_EP_STATE_STREAMING:
|
|
ase_metadata_updated(ase);
|
|
return;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Actions needed for exiting the old state */
|
|
switch (old_state) {
|
|
case BT_BAP_EP_STATE_STREAMING:
|
|
ase_exit_state_streaming(ase);
|
|
break;
|
|
case BT_BAP_EP_STATE_ENABLING:
|
|
ase_exit_state_enabling(ase);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
/* Actions needed for entering the new state */
|
|
switch (new_state) {
|
|
case BT_BAP_EP_STATE_IDLE:
|
|
ase_enter_state_idle(ase);
|
|
break;
|
|
case BT_BAP_EP_STATE_CODEC_CONFIGURED:
|
|
ase_enter_state_codec_configured(ase);
|
|
break;
|
|
case BT_BAP_EP_STATE_QOS_CONFIGURED:
|
|
ase_enter_state_qos_configured(ase);
|
|
break;
|
|
case BT_BAP_EP_STATE_ENABLING:
|
|
ase_enter_state_enabling(ase);
|
|
break;
|
|
case BT_BAP_EP_STATE_STREAMING:
|
|
ase_enter_state_streaming(ase);
|
|
break;
|
|
case BT_BAP_EP_STATE_DISABLING:
|
|
ase_enter_state_disabling(ase);
|
|
break;
|
|
case BT_BAP_EP_STATE_RELEASING:
|
|
ase_enter_state_releasing(ase);
|
|
break;
|
|
default:
|
|
__ASSERT_PRINT("Invalid state %d", new_state);
|
|
}
|
|
}
|
|
|
|
int ascs_ep_set_state(struct bt_bap_ep *ep, uint8_t state)
|
|
{
|
|
struct bt_ascs_ase *ase = CONTAINER_OF(ep, struct bt_ascs_ase, ep);
|
|
const enum bt_bap_ep_state old_state = ascs_ep_get_state(&ase->ep);
|
|
bool valid_state_transition = false;
|
|
int err;
|
|
|
|
switch (state) {
|
|
case BT_BAP_EP_STATE_IDLE:
|
|
valid_state_transition = true;
|
|
break;
|
|
case BT_BAP_EP_STATE_CODEC_CONFIGURED:
|
|
switch (old_state) {
|
|
case BT_BAP_EP_STATE_IDLE:
|
|
case BT_BAP_EP_STATE_CODEC_CONFIGURED:
|
|
case BT_BAP_EP_STATE_QOS_CONFIGURED:
|
|
case BT_BAP_EP_STATE_RELEASING:
|
|
valid_state_transition = true;
|
|
break;
|
|
default:
|
|
break;
|
|
} break;
|
|
case BT_BAP_EP_STATE_QOS_CONFIGURED:
|
|
switch (old_state) {
|
|
case BT_BAP_EP_STATE_CODEC_CONFIGURED:
|
|
case BT_BAP_EP_STATE_QOS_CONFIGURED:
|
|
valid_state_transition = true;
|
|
break;
|
|
case BT_BAP_EP_STATE_DISABLING:
|
|
valid_state_transition = ase->ep.dir == BT_AUDIO_DIR_SOURCE;
|
|
break;
|
|
case BT_BAP_EP_STATE_ENABLING:
|
|
case BT_BAP_EP_STATE_STREAMING:
|
|
/* Source ASE transition Streaming->QoS configured is valid on case of CIS
|
|
* link-loss.
|
|
*/
|
|
valid_state_transition = ase->ep.dir == BT_AUDIO_DIR_SINK ||
|
|
ase->unexpected_iso_link_loss;
|
|
break;
|
|
default:
|
|
break;
|
|
} break;
|
|
case BT_BAP_EP_STATE_ENABLING:
|
|
switch (old_state) {
|
|
case BT_BAP_EP_STATE_QOS_CONFIGURED:
|
|
case BT_BAP_EP_STATE_ENABLING:
|
|
valid_state_transition = true;
|
|
break;
|
|
default:
|
|
break;
|
|
} break;
|
|
case BT_BAP_EP_STATE_STREAMING:
|
|
switch (old_state) {
|
|
case BT_BAP_EP_STATE_ENABLING:
|
|
case BT_BAP_EP_STATE_STREAMING:
|
|
valid_state_transition = true;
|
|
break;
|
|
default:
|
|
break;
|
|
} break;
|
|
case BT_BAP_EP_STATE_DISABLING:
|
|
switch (old_state) {
|
|
case BT_BAP_EP_STATE_ENABLING:
|
|
case BT_BAP_EP_STATE_STREAMING:
|
|
valid_state_transition = ase->ep.dir == BT_AUDIO_DIR_SOURCE;
|
|
break;
|
|
default:
|
|
break;
|
|
} break;
|
|
case BT_BAP_EP_STATE_RELEASING:
|
|
switch (old_state) {
|
|
case BT_BAP_EP_STATE_CODEC_CONFIGURED:
|
|
case BT_BAP_EP_STATE_QOS_CONFIGURED:
|
|
case BT_BAP_EP_STATE_ENABLING:
|
|
case BT_BAP_EP_STATE_STREAMING:
|
|
valid_state_transition = true;
|
|
break;
|
|
case BT_BAP_EP_STATE_DISABLING:
|
|
valid_state_transition = ase->ep.dir == BT_AUDIO_DIR_SOURCE;
|
|
break;
|
|
default:
|
|
break;
|
|
} break;
|
|
}
|
|
|
|
if (!valid_state_transition) {
|
|
BT_ASSERT_MSG(false, "Invalid state transition: %s -> %s",
|
|
bt_bap_ep_state_str(old_state), bt_bap_ep_state_str(state));
|
|
|
|
return -EBADMSG;
|
|
}
|
|
|
|
ase->state_pending = state;
|
|
|
|
err = k_work_schedule(&ase->state_transition_work, K_NO_WAIT);
|
|
if (err < 0) {
|
|
LOG_ERR("Failed to schedule state transition work err %d", err);
|
|
return err;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void ascs_ep_get_status_config(struct bt_bap_ep *ep, struct net_buf_simple *buf)
|
|
{
|
|
struct bt_ascs_ase_status_config *cfg;
|
|
struct bt_audio_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_cfg.id;
|
|
cfg->codec.cid = sys_cpu_to_le16(ep->codec_cfg.cid);
|
|
cfg->codec.vid = sys_cpu_to_le16(ep->codec_cfg.vid);
|
|
|
|
LOG_DBG("dir %s unframed_supported 0x%02x phy 0x%02x rtn %u "
|
|
"latency %u pd_min %u pd_max %u pref_pd_min %u pref_pd_max %u codec id 0x%02x",
|
|
bt_audio_dir_str(ep->dir), pref->unframed_supported, pref->phy, pref->rtn,
|
|
pref->latency, pref->pd_min, pref->pd_max, pref->pref_pd_min, pref->pref_pd_max,
|
|
ep->stream->codec_cfg->id);
|
|
|
|
cfg->cc_len = ep->codec_cfg.data_len;
|
|
net_buf_simple_add_mem(buf, ep->codec_cfg.data, ep->codec_cfg.data_len);
|
|
}
|
|
|
|
static void ascs_ep_get_status_qos(struct bt_bap_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 %s codec id 0x%02x interval %u framing 0x%02x phy 0x%02x "
|
|
"rtn %u latency %u pd %u",
|
|
bt_audio_dir_str(ep->dir), ep->stream->codec_cfg->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_bap_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 = ep->codec_cfg.meta_len;
|
|
net_buf_simple_add_mem(buf, ep->codec_cfg.meta, ep->codec_cfg.meta_len);
|
|
|
|
LOG_DBG("dir %s cig 0x%02x cis 0x%02x",
|
|
bt_audio_dir_str(ep->dir), ep->cig_id, ep->cis_id);
|
|
}
|
|
|
|
static ssize_t ascs_ase_read_status_idle(struct bt_conn *conn, const struct bt_gatt_attr *attr,
|
|
void *buf, uint16_t len, uint16_t offset)
|
|
{
|
|
struct bt_ascs_ase_status status = {
|
|
.id = POINTER_TO_UINT(BT_AUDIO_CHRC_USER_DATA(attr)),
|
|
.state = BT_BAP_EP_STATE_IDLE,
|
|
};
|
|
|
|
LOG_DBG("conn %p id 0x%02x", (void *)conn, status.id);
|
|
|
|
return bt_gatt_attr_read(conn, attr, buf, len, offset, &status, sizeof(status));
|
|
}
|
|
|
|
static int ascs_ep_get_status(struct bt_bap_ep *ep, struct net_buf_simple *buf)
|
|
{
|
|
if (!ep || !buf) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
LOG_DBG("ep %p id 0x%02x state %s", ep, ep->status.id,
|
|
bt_bap_ep_state_str(ep->status.state));
|
|
|
|
/* Reset if buffer before using */
|
|
net_buf_simple_reset(buf);
|
|
|
|
(void)net_buf_simple_add_mem(buf, &ep->status, sizeof(ep->status));
|
|
|
|
switch (ep->status.state) {
|
|
case BT_BAP_EP_STATE_IDLE:
|
|
/* Fallthrough */
|
|
case BT_BAP_EP_STATE_RELEASING:
|
|
break;
|
|
case BT_BAP_EP_STATE_CODEC_CONFIGURED:
|
|
ascs_ep_get_status_config(ep, buf);
|
|
break;
|
|
case BT_BAP_EP_STATE_QOS_CONFIGURED:
|
|
ascs_ep_get_status_qos(ep, buf);
|
|
break;
|
|
case BT_BAP_EP_STATE_ENABLING:
|
|
/* Fallthrough */
|
|
case BT_BAP_EP_STATE_STREAMING:
|
|
/* Fallthrough */
|
|
case BT_BAP_EP_STATE_DISABLING:
|
|
ascs_ep_get_status_enable(ep, buf);
|
|
break;
|
|
default:
|
|
LOG_ERR("Invalid Endpoint state");
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int ascs_iso_accept(const struct bt_iso_accept_info *info, struct bt_iso_chan **iso_chan)
|
|
{
|
|
LOG_DBG("conn %p", (void *)info->acl);
|
|
|
|
for (size_t i = 0; i < ARRAY_SIZE(ase_pool); i++) {
|
|
struct bt_ascs_ase *ase = &ase_pool[i];
|
|
enum bt_bap_ep_state state;
|
|
struct bt_iso_chan *chan;
|
|
|
|
if (ase->conn != info->acl ||
|
|
ase->ep.cig_id != info->cig_id ||
|
|
ase->ep.cis_id != info->cis_id) {
|
|
continue;
|
|
}
|
|
|
|
state = ascs_ep_get_state(&ase->ep);
|
|
if (state != BT_BAP_EP_STATE_ENABLING && state != BT_BAP_EP_STATE_QOS_CONFIGURED) {
|
|
LOG_WRN("ase %p cannot accept ISO connection", ase);
|
|
break;
|
|
}
|
|
|
|
__ASSERT(ase->ep.iso != NULL, "ep %p not bound with ISO", &ase->ep);
|
|
|
|
chan = &ase->ep.iso->chan;
|
|
if (chan->iso != NULL) {
|
|
LOG_WRN("ase %p chan %p already connected", ase, chan);
|
|
return -EALREADY;
|
|
}
|
|
|
|
*iso_chan = chan;
|
|
|
|
LOG_DBG("iso_chan %p", *iso_chan);
|
|
|
|
return 0;
|
|
}
|
|
|
|
return -EACCES;
|
|
}
|
|
|
|
#if defined(CONFIG_BT_AUDIO_RX)
|
|
static void ascs_iso_recv(struct bt_iso_chan *chan,
|
|
const struct bt_iso_recv_info *info,
|
|
struct net_buf *buf)
|
|
{
|
|
struct bt_bap_iso *iso = CONTAINER_OF(chan, struct bt_bap_iso, chan);
|
|
const struct bt_bap_stream_ops *ops;
|
|
struct bt_bap_stream *stream;
|
|
struct bt_bap_ep *ep;
|
|
|
|
ep = iso->rx.ep;
|
|
if (ep == NULL) {
|
|
/* In the case that the CIS has been setup as bidirectional, and
|
|
* only one of the directions have an ASE configured yet,
|
|
* we should only care about valid ISO packets when doing this
|
|
* check. The reason is that some controllers send HCI ISO data
|
|
* packets to the host, even if no SDU was sent on the remote
|
|
* side. This basically means that empty PDUs are sent to the
|
|
* host as HCI ISO data packets, which we should just ignore
|
|
*/
|
|
if ((info->flags & BT_ISO_FLAGS_VALID) != 0) {
|
|
LOG_DBG("Valid ISO packet of len %zu received for iso %p not bound with ep",
|
|
net_buf_frags_len(buf), chan);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (ep->status.state != BT_BAP_EP_STATE_STREAMING) {
|
|
if (IS_ENABLED(CONFIG_BT_BAP_DEBUG_STREAM_DATA)) {
|
|
LOG_DBG("ep %p is not in the streaming state: %s", ep,
|
|
bt_bap_ep_state_str(ep->status.state));
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
stream = ep->stream;
|
|
if (stream == NULL) {
|
|
LOG_ERR("No stream for ep %p", ep);
|
|
return;
|
|
}
|
|
|
|
ops = stream->ops;
|
|
|
|
if (IS_ENABLED(CONFIG_BT_BAP_DEBUG_STREAM_DATA)) {
|
|
LOG_DBG("stream %p ep %p len %zu", stream, stream->ep, net_buf_frags_len(buf));
|
|
}
|
|
|
|
if (ops != NULL && ops->recv != NULL) {
|
|
ops->recv(stream, info, buf);
|
|
} else {
|
|
LOG_WRN("No callback for recv set");
|
|
}
|
|
}
|
|
#endif /* CONFIG_BT_AUDIO_RX */
|
|
|
|
#if defined(CONFIG_BT_AUDIO_TX)
|
|
static void ascs_iso_sent(struct bt_iso_chan *chan)
|
|
{
|
|
struct bt_bap_iso *iso = CONTAINER_OF(chan, struct bt_bap_iso, chan);
|
|
const struct bt_bap_stream_ops *ops;
|
|
struct bt_bap_stream *stream;
|
|
struct bt_bap_ep *ep;
|
|
|
|
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_BAP_DEBUG_STREAM_DATA)) {
|
|
LOG_DBG("stream %p ep %p", stream, stream->ep);
|
|
}
|
|
|
|
if (ops != NULL && ops->sent != NULL) {
|
|
ops->sent(stream);
|
|
}
|
|
}
|
|
#endif /* CONFIG_BT_AUDIO_TX */
|
|
|
|
static void ascs_update_sdu_size(struct bt_bap_ep *ep)
|
|
{
|
|
struct bt_iso_chan_io_qos *io_qos;
|
|
struct bt_audio_codec_qos *codec_qos = &ep->qos;
|
|
|
|
if (ep->dir == BT_AUDIO_DIR_SINK) {
|
|
io_qos = ep->iso->chan.qos->rx;
|
|
} else if (ep->dir == BT_AUDIO_DIR_SOURCE) {
|
|
io_qos = ep->iso->chan.qos->tx;
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
io_qos->sdu = codec_qos->sdu;
|
|
io_qos->rtn = codec_qos->rtn;
|
|
}
|
|
|
|
static void ascs_ep_iso_connected(struct bt_bap_ep *ep)
|
|
{
|
|
struct bt_ascs_ase *ase = CONTAINER_OF(ep, struct bt_ascs_ase, ep);
|
|
const struct bt_bap_stream_ops *stream_ops;
|
|
struct bt_bap_stream *stream;
|
|
|
|
if (ep->status.state != BT_BAP_EP_STATE_ENABLING) {
|
|
LOG_DBG("ep %p not in enabling state: %s", ep,
|
|
bt_bap_ep_state_str(ep->status.state));
|
|
return;
|
|
}
|
|
|
|
stream = ep->stream;
|
|
if (stream == NULL) {
|
|
LOG_ERR("No stream for ep %p", ep);
|
|
return;
|
|
}
|
|
|
|
/* Reset reason */
|
|
ep->reason = BT_HCI_ERR_SUCCESS;
|
|
ase->unexpected_iso_link_loss = false;
|
|
|
|
/* Some values are not provided by the HCI events when the CIS is established for the
|
|
* peripheral, so we update them here based on the parameters provided by the BAP Unicast
|
|
* Client
|
|
*/
|
|
ascs_update_sdu_size(ep);
|
|
|
|
LOG_DBG("stream %p ep %p dir %s", stream, ep, bt_audio_dir_str(ep->dir));
|
|
|
|
#if defined(CONFIG_BT_BAP_DEBUG_STREAM_SEQ_NUM)
|
|
/* reset sequence number */
|
|
stream->_prev_seq_num = 0U;
|
|
#endif /* CONFIG_BT_BAP_DEBUG_STREAM_SEQ_NUM */
|
|
|
|
stream_ops = stream->ops;
|
|
if (stream_ops != NULL && stream_ops->connected != NULL) {
|
|
stream_ops->connected(stream);
|
|
}
|
|
|
|
if (ep->dir == BT_AUDIO_DIR_SINK && ep->receiver_ready) {
|
|
/* Source ASEs shall be ISO connected first, and then receive
|
|
* the receiver start ready command to enter the streaming
|
|
* state
|
|
*/
|
|
ascs_ep_set_state(ep, BT_BAP_EP_STATE_STREAMING);
|
|
}
|
|
}
|
|
|
|
static void ascs_iso_connected(struct bt_iso_chan *chan)
|
|
{
|
|
struct bt_bap_iso *iso = CONTAINER_OF(chan, struct bt_bap_iso, chan);
|
|
|
|
if (iso->rx.ep == NULL && iso->tx.ep == NULL) {
|
|
LOG_ERR("iso %p not bound with ep", chan);
|
|
return;
|
|
}
|
|
|
|
if (iso->rx.ep != NULL) {
|
|
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_bap_ep *ep, uint8_t reason)
|
|
{
|
|
struct bt_ascs_ase *ase = CONTAINER_OF(ep, struct bt_ascs_ase, ep);
|
|
const struct bt_bap_stream_ops *stream_ops;
|
|
struct bt_bap_stream *stream;
|
|
|
|
stream = ep->stream;
|
|
if (stream == NULL) {
|
|
LOG_ERR("No stream for ep %p", ep);
|
|
return;
|
|
}
|
|
|
|
LOG_DBG("stream %p ep %p state %s reason 0x%02x", stream, stream->ep,
|
|
bt_bap_ep_state_str(ep->status.state), reason);
|
|
|
|
stream_ops = stream->ops;
|
|
if (stream_ops != NULL && stream_ops->disconnected != NULL) {
|
|
stream_ops->disconnected(stream, reason);
|
|
}
|
|
|
|
/* Cancel ASE disconnect work if pending */
|
|
(void)k_work_cancel_delayable(&ase->disconnect_work);
|
|
ep->reason = reason;
|
|
|
|
if (ep->status.state == BT_BAP_EP_STATE_RELEASING) {
|
|
ascs_ep_set_state(ep, BT_BAP_EP_STATE_IDLE);
|
|
} else if (ep->status.state == BT_BAP_EP_STATE_STREAMING ||
|
|
ep->status.state == BT_BAP_EP_STATE_DISABLING) {
|
|
/* ASCS_v1.0 3.2 ASE state machine transitions
|
|
*
|
|
* If the server detects link loss of a CIS for an ASE in the Streaming
|
|
* state or the Disabling state, the server shall immediately transition
|
|
* that ASE to the QoS Configured state.
|
|
*/
|
|
ase->unexpected_iso_link_loss = true;
|
|
|
|
ascs_ep_set_state(ep, BT_BAP_EP_STATE_QOS_CONFIGURED);
|
|
}
|
|
}
|
|
|
|
static void ascs_iso_disconnected(struct bt_iso_chan *chan, uint8_t reason)
|
|
{
|
|
struct bt_bap_iso *iso = CONTAINER_OF(chan, struct bt_bap_iso, chan);
|
|
|
|
if (iso->rx.ep == NULL && iso->tx.ep == NULL) {
|
|
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 = {
|
|
#if defined(CONFIG_BT_AUDIO_RX)
|
|
.recv = ascs_iso_recv,
|
|
#endif /* CONFIG_BT_AUDIO_RX */
|
|
#if defined(CONFIG_BT_AUDIO_TX)
|
|
.sent = ascs_iso_sent,
|
|
#endif /* CONFIG_BT_AUDIO_TX */
|
|
.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);
|
|
}
|
|
|
|
#define CP_RSP_BUF_SIZE \
|
|
(sizeof(struct bt_ascs_cp_rsp) + (ASE_COUNT * sizeof(struct bt_ascs_cp_ase_rsp)))
|
|
|
|
/* Ensure that the cp_rsp_buf can fit in any notification
|
|
* (sizeof buffer - header for notification)
|
|
*/
|
|
BUILD_ASSERT(BT_ATT_BUF_SIZE - NTF_HEADER_SIZE >= CP_RSP_BUF_SIZE,
|
|
"BT_ATT_BUF_SIZE not large enough to hold responses for all ASEs");
|
|
NET_BUF_SIMPLE_DEFINE_STATIC(cp_rsp_buf, CP_RSP_BUF_SIZE);
|
|
|
|
static void ascs_cp_rsp_init(uint8_t op)
|
|
{
|
|
struct bt_ascs_cp_rsp *rsp;
|
|
|
|
net_buf_simple_reset(&cp_rsp_buf);
|
|
|
|
rsp = net_buf_simple_add(&cp_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 code, uint8_t reason)
|
|
{
|
|
struct bt_ascs_cp_rsp *rsp = (void *)cp_rsp_buf.__buf;
|
|
struct bt_ascs_cp_ase_rsp *ase_rsp;
|
|
|
|
LOG_DBG("id 0x%02x code %s (0x%02x) reason %s (0x%02x)", id,
|
|
bt_ascs_rsp_str(code), code, bt_ascs_reason_str(reason), reason);
|
|
|
|
if (rsp->num_ase == BT_ASCS_UNSUPP_OR_LENGTH_ERR_NUM_ASE) {
|
|
return;
|
|
}
|
|
|
|
switch (code) {
|
|
/* If the Response_Code value is 0x01 or 0x02, Number_of_ASEs shall be
|
|
* set to 0xFF.
|
|
*/
|
|
case BT_BAP_ASCS_RSP_CODE_NOT_SUPPORTED:
|
|
case BT_BAP_ASCS_RSP_CODE_INVALID_LENGTH:
|
|
rsp->num_ase = BT_ASCS_UNSUPP_OR_LENGTH_ERR_NUM_ASE;
|
|
break;
|
|
default:
|
|
rsp->num_ase++;
|
|
break;
|
|
}
|
|
|
|
ase_rsp = net_buf_simple_add(&cp_rsp_buf, sizeof(*ase_rsp));
|
|
ase_rsp->id = id;
|
|
ase_rsp->code = code;
|
|
ase_rsp->reason = reason;
|
|
}
|
|
|
|
static void ascs_cp_rsp_success(uint8_t id)
|
|
{
|
|
ascs_cp_rsp_add(id, BT_BAP_ASCS_RSP_CODE_SUCCESS, BT_BAP_ASCS_REASON_NONE);
|
|
}
|
|
|
|
static int ase_release(struct bt_ascs_ase *ase, uint8_t reason, struct bt_bap_ascs_rsp *rsp)
|
|
{
|
|
enum bt_bap_ep_state state = ascs_ep_get_state(&ase->ep);
|
|
int err;
|
|
|
|
if (state == BT_BAP_EP_STATE_IDLE || state == BT_BAP_EP_STATE_RELEASING) {
|
|
LOG_WRN("Invalid operation in state: %s", bt_bap_ep_state_str(state));
|
|
*rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_INVALID_ASE_STATE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
return -EBADMSG;
|
|
}
|
|
|
|
if (unicast_server_cb == NULL || unicast_server_cb->release == NULL) {
|
|
*rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_UNSPECIFIED,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
return -ENOTSUP;
|
|
}
|
|
|
|
err = unicast_server_cb->release(ase->ep.stream, rsp);
|
|
if (err) {
|
|
if (rsp->code == BT_BAP_ASCS_RSP_CODE_SUCCESS) {
|
|
*rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_UNSPECIFIED,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
}
|
|
|
|
LOG_ERR("Release failed: err %d, code %u, reason %u", err, rsp->code, rsp->reason);
|
|
return err;
|
|
}
|
|
|
|
/* Set reason in case this exits the streaming state */
|
|
ase->ep.reason = reason;
|
|
|
|
ascs_ep_set_state(&ase->ep, BT_BAP_EP_STATE_RELEASING);
|
|
|
|
*rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_SUCCESS, BT_BAP_ASCS_REASON_NONE);
|
|
return 0;
|
|
}
|
|
|
|
int bt_ascs_release_ase(struct bt_bap_ep *ep)
|
|
{
|
|
struct bt_ascs_ase *ase = CONTAINER_OF(ep, struct bt_ascs_ase, ep);
|
|
const enum bt_bap_ep_state state = ascs_ep_get_state(&ase->ep);
|
|
|
|
if (state == BT_BAP_EP_STATE_IDLE) {
|
|
ase_free(ase);
|
|
return 0;
|
|
}
|
|
|
|
return ase_release(ase, BT_HCI_ERR_LOCALHOST_TERM_CONN, BT_BAP_ASCS_RSP_NULL);
|
|
}
|
|
|
|
static int ase_disable(struct bt_ascs_ase *ase, uint8_t reason, struct bt_bap_ascs_rsp *rsp)
|
|
{
|
|
struct bt_bap_stream *stream;
|
|
struct bt_bap_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_BAP_EP_STATE_ENABLING:
|
|
/* or 0x04 (Streaming) */
|
|
case BT_BAP_EP_STATE_STREAMING:
|
|
break;
|
|
default:
|
|
LOG_WRN("Invalid operation in state: %s", bt_bap_ep_state_str(ep->status.state));
|
|
*rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_INVALID_ASE_STATE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
return -EBADMSG;
|
|
}
|
|
|
|
stream = ep->stream;
|
|
|
|
if (unicast_server_cb == NULL || unicast_server_cb->disable == NULL) {
|
|
*rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_UNSPECIFIED, BT_BAP_ASCS_REASON_NONE);
|
|
return -ENOTSUP;
|
|
}
|
|
|
|
err = unicast_server_cb->disable(stream, rsp);
|
|
if (err) {
|
|
if (rsp->code == BT_BAP_ASCS_RSP_CODE_SUCCESS) {
|
|
*rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_UNSPECIFIED,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
}
|
|
|
|
LOG_ERR("Disable failed: err %d, code %u, reason %u", err, rsp->code, rsp->reason);
|
|
return err;
|
|
}
|
|
|
|
/* Set reason in case this exits the streaming state */
|
|
ep->reason = reason;
|
|
|
|
/* 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_BAP_EP_STATE_DISABLING);
|
|
} else {
|
|
ascs_ep_set_state(ep, BT_BAP_EP_STATE_QOS_CONFIGURED);
|
|
}
|
|
|
|
*rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_SUCCESS, BT_BAP_ASCS_REASON_NONE);
|
|
return 0;
|
|
}
|
|
|
|
int bt_ascs_disable_ase(struct bt_bap_ep *ep)
|
|
{
|
|
struct bt_ascs_ase *ase = CONTAINER_OF(ep, struct bt_ascs_ase, ep);
|
|
|
|
return ase_disable(ase, BT_HCI_ERR_LOCALHOST_TERM_CONN, BT_BAP_ASCS_RSP_NULL);
|
|
}
|
|
|
|
static void disconnected(struct bt_conn *conn, uint8_t reason)
|
|
{
|
|
for (size_t i = 0; i < ARRAY_SIZE(ase_pool); i++) {
|
|
struct bt_ascs_ase *ase = &ase_pool[i];
|
|
|
|
if (ase->conn != conn) {
|
|
continue;
|
|
}
|
|
|
|
if (ase->ep.status.state != BT_BAP_EP_STATE_IDLE) {
|
|
/* We must set the state to idle when the ACL is disconnected immediately,
|
|
* as when the ACL disconnect callbacks have been called, the application
|
|
* should expect there to be only a single reference to the bt_conn pointer
|
|
* from the stack.
|
|
* We trigger the work handler directly rather than e.g. calling
|
|
* ase_enter_state_idle to trigger "regular" state change behavior (such) as
|
|
* calling stream->stopped when leaving the streaming state.
|
|
*/
|
|
ase->ep.reason = reason;
|
|
ase->state_pending = BT_BAP_EP_STATE_IDLE;
|
|
state_transition_work_handler(&ase->state_transition_work.work);
|
|
/* At this point, `ase` object have been free'd */
|
|
}
|
|
}
|
|
}
|
|
|
|
BT_CONN_CB_DEFINE(conn_cb) = {
|
|
.disconnected = disconnected,
|
|
};
|
|
|
|
struct bap_iso_find_params {
|
|
struct bt_conn *acl;
|
|
uint8_t cig_id;
|
|
uint8_t cis_id;
|
|
};
|
|
|
|
static bool bap_iso_find_func(struct bt_bap_iso *iso, void *user_data)
|
|
{
|
|
struct bap_iso_find_params *params = user_data;
|
|
const struct bt_bap_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_bap_iso *bap_iso_get_or_new(struct bt_conn *conn, uint8_t cig_id, uint8_t cis_id)
|
|
{
|
|
struct bt_bap_iso *iso;
|
|
struct bap_iso_find_params params = {
|
|
.acl = bt_conn_ref(conn),
|
|
.cig_id = cig_id,
|
|
.cis_id = cis_id,
|
|
};
|
|
|
|
iso = bt_bap_iso_find(bap_iso_find_func, ¶ms);
|
|
bt_conn_unref(conn);
|
|
|
|
if (iso) {
|
|
return iso;
|
|
}
|
|
|
|
iso = bt_bap_iso_new();
|
|
if (!iso) {
|
|
return NULL;
|
|
}
|
|
|
|
bt_bap_iso_init(iso, &ascs_iso_ops);
|
|
|
|
return iso;
|
|
}
|
|
|
|
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->attr = attr;
|
|
|
|
return BT_GATT_ITER_STOP;
|
|
}
|
|
|
|
return BT_GATT_ITER_CONTINUE;
|
|
}
|
|
|
|
void ascs_ep_init(struct bt_bap_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);
|
|
ep->reason = BT_HCI_ERR_SUCCESS;
|
|
}
|
|
|
|
static void ase_init(struct bt_ascs_ase *ase, struct bt_conn *conn, uint8_t id)
|
|
{
|
|
memset(ase, 0, sizeof(*ase));
|
|
|
|
ascs_ep_init(&ase->ep, id);
|
|
|
|
ase->conn = bt_conn_ref(conn);
|
|
|
|
/* Lookup ASE characteristic */
|
|
bt_gatt_foreach_attr_type(0x0001, 0xffff, ASE_UUID(id), NULL, 0, ase_attr_cb, ase);
|
|
|
|
__ASSERT(ase->attr, "ASE characteristic not found\n");
|
|
|
|
k_work_init_delayable(&ase->disconnect_work, ascs_disconnect_stream_work_handler);
|
|
k_work_init_delayable(&ase->state_transition_work, state_transition_work_handler);
|
|
}
|
|
|
|
static struct bt_ascs_ase *ase_new(struct bt_conn *conn, uint8_t id)
|
|
{
|
|
struct bt_ascs_ase *ase = NULL;
|
|
|
|
__ASSERT(id > 0 && id <= ASE_COUNT, "invalid ASE_ID 0x%02x", id);
|
|
|
|
for (size_t i = 0; i < ARRAY_SIZE(ase_pool); i++) {
|
|
if (ase_pool[i].conn == NULL) {
|
|
ase = &ase_pool[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ase == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
ase_init(ase, conn, id);
|
|
|
|
LOG_DBG("conn %p new ase %p id 0x%02x", (void *)conn, ase, id);
|
|
|
|
return ase;
|
|
}
|
|
|
|
static struct bt_ascs_ase *ase_find(struct bt_conn *conn, uint8_t id)
|
|
{
|
|
for (size_t i = 0; i < ARRAY_SIZE(ase_pool); i++) {
|
|
struct bt_ascs_ase *ase = &ase_pool[i];
|
|
|
|
if (ase->conn == conn && ase->ep.status.id == id) {
|
|
return ase;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static ssize_t ascs_ase_read(struct bt_conn *conn,
|
|
const struct bt_gatt_attr *attr, void *buf,
|
|
uint16_t len, uint16_t offset)
|
|
{
|
|
uint8_t ase_id = POINTER_TO_UINT(BT_AUDIO_CHRC_USER_DATA(attr));
|
|
struct bt_ascs_ase *ase = NULL;
|
|
ssize_t ret_val;
|
|
int err;
|
|
|
|
LOG_DBG("conn %p attr %p buf %p len %u offset %u", (void *)conn, attr, buf, len, offset);
|
|
|
|
/* The callback can be used locally to read the ASE_ID in which case conn won't be set. */
|
|
if (conn != NULL) {
|
|
ase = ase_find(conn, ase_id);
|
|
}
|
|
|
|
/* If NULL, we haven't assigned an ASE, this also means that we are currently in IDLE */
|
|
if (ase == NULL) {
|
|
return ascs_ase_read_status_idle(conn, attr, buf, len, offset);
|
|
}
|
|
|
|
err = k_sem_take(&ase_buf_sem, ASE_BUF_SEM_TIMEOUT);
|
|
if (err != 0) {
|
|
LOG_DBG("Failed to take ase_buf_sem: %d", err);
|
|
|
|
return BT_GATT_ERR(BT_ATT_ERR_INSUFFICIENT_RESOURCES);
|
|
}
|
|
|
|
ascs_ep_get_status(&ase->ep, &ase_buf);
|
|
|
|
ret_val = bt_gatt_attr_read(conn, attr, buf, len, offset, ase_buf.data, ase_buf.len);
|
|
|
|
k_sem_give(&ase_buf_sem);
|
|
|
|
return ret_val;
|
|
}
|
|
|
|
static void ascs_cp_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value)
|
|
{
|
|
LOG_DBG("attr %p value 0x%04x", attr, value);
|
|
}
|
|
|
|
struct codec_cap_lookup_id_data {
|
|
uint8_t id;
|
|
uint16_t cid;
|
|
uint16_t vid;
|
|
const struct bt_audio_codec_cap *codec_cap;
|
|
};
|
|
|
|
static bool codec_lookup_id(const struct bt_pacs_cap *cap, void *user_data)
|
|
{
|
|
struct codec_cap_lookup_id_data *data = user_data;
|
|
|
|
if (cap->codec_cap->id == data->id && cap->codec_cap->cid == data->cid &&
|
|
cap->codec_cap->vid == data->vid) {
|
|
data->codec_cap = cap->codec_cap;
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static int ascs_ep_set_codec(struct bt_bap_ep *ep, uint8_t id, uint16_t cid, uint16_t vid,
|
|
uint8_t *cc, uint8_t len, struct bt_bap_ascs_rsp *rsp)
|
|
{
|
|
struct bt_audio_codec_cfg *codec_cfg;
|
|
struct codec_cap_lookup_id_data lookup_data = {
|
|
.id = id,
|
|
.cid = cid,
|
|
.vid = vid,
|
|
};
|
|
|
|
if (ep == NULL) {
|
|
*rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_CONF_INVALID,
|
|
BT_BAP_ASCS_REASON_CODEC_DATA);
|
|
return -EINVAL;
|
|
}
|
|
|
|
codec_cfg = &ep->codec_cfg;
|
|
|
|
LOG_DBG("ep %p dir %s codec id 0x%02x cid 0x%04x vid 0x%04x len %u",
|
|
ep, bt_audio_dir_str(ep->dir), id, cid, vid, len);
|
|
|
|
bt_pacs_cap_foreach(ep->dir, codec_lookup_id, &lookup_data);
|
|
|
|
if (lookup_data.codec_cap == NULL) {
|
|
LOG_DBG("Codec with id %u for dir %s is not supported by our capabilities",
|
|
id, bt_audio_dir_str(ep->dir));
|
|
|
|
*rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_CONF_INVALID,
|
|
BT_BAP_ASCS_REASON_CODEC);
|
|
return -ENOENT;
|
|
}
|
|
|
|
codec_cfg->id = id;
|
|
codec_cfg->cid = cid;
|
|
codec_cfg->vid = vid;
|
|
codec_cfg->data_len = len;
|
|
memcpy(codec_cfg->data, cc, len);
|
|
codec_cfg->path_id = lookup_data.codec_cap->path_id;
|
|
|
|
*rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_SUCCESS, BT_BAP_ASCS_REASON_NONE);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int ase_config(struct bt_ascs_ase *ase, const struct bt_ascs_config *cfg)
|
|
{
|
|
struct bt_bap_stream *stream;
|
|
struct bt_audio_codec_cfg codec_cfg;
|
|
struct bt_bap_ascs_rsp rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_SUCCESS,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
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_BAP_ASCS_RSP_CODE_CONF_INVALID,
|
|
BT_BAP_ASCS_REASON_LATENCY);
|
|
return -EINVAL;
|
|
}
|
|
|
|
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_BAP_ASCS_RSP_CODE_CONF_INVALID,
|
|
BT_BAP_ASCS_REASON_PHY);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (cfg->cc_len > CONFIG_BT_AUDIO_CODEC_CFG_MAX_DATA_SIZE) {
|
|
LOG_DBG("Can not store %u codec configuration data", cfg->cc_len);
|
|
|
|
return -ENOMEM;
|
|
}
|
|
|
|
switch (ase->ep.status.state) {
|
|
/* Valid only if ASE_State field = 0x00 (Idle) */
|
|
case BT_BAP_EP_STATE_IDLE:
|
|
/* or 0x01 (Codec Configured) */
|
|
case BT_BAP_EP_STATE_CODEC_CONFIGURED:
|
|
/* or 0x02 (QoS Configured) */
|
|
case BT_BAP_EP_STATE_QOS_CONFIGURED:
|
|
break;
|
|
default:
|
|
LOG_WRN("Invalid operation in state: %s",
|
|
bt_bap_ep_state_str(ase->ep.status.state));
|
|
ascs_cp_rsp_add(ASE_ID(ase), BT_BAP_ASCS_RSP_CODE_INVALID_ASE_STATE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* Store current codec configuration to be able to restore it
|
|
* in case of error.
|
|
*/
|
|
(void)memcpy(&codec_cfg, &ase->ep.codec_cfg, sizeof(codec_cfg));
|
|
|
|
err = ascs_ep_set_codec(&ase->ep, cfg->codec.id, sys_le16_to_cpu(cfg->codec.cid),
|
|
sys_le16_to_cpu(cfg->codec.vid), (uint8_t *)cfg->cc, cfg->cc_len,
|
|
&rsp);
|
|
if (err) {
|
|
ascs_app_rsp_warn_valid(&rsp);
|
|
(void)memcpy(&ase->ep.codec_cfg, &codec_cfg, sizeof(codec_cfg));
|
|
ascs_cp_rsp_add(ASE_ID(ase), rsp.code, rsp.reason);
|
|
return err;
|
|
}
|
|
|
|
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_cfg, &ase->ep.qos_pref,
|
|
&rsp);
|
|
} else {
|
|
err = -ENOTSUP;
|
|
rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_UNSPECIFIED,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
}
|
|
|
|
if (err) {
|
|
ascs_app_rsp_warn_valid(&rsp);
|
|
|
|
if (rsp.code == BT_BAP_ASCS_RSP_CODE_SUCCESS) {
|
|
rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_UNSPECIFIED,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
}
|
|
|
|
LOG_ERR("Reconfig failed: err %d, code %u, reason %u",
|
|
err, rsp.code, rsp.reason);
|
|
|
|
(void)memcpy(&ase->ep.codec_cfg, &codec_cfg, sizeof(codec_cfg));
|
|
ascs_cp_rsp_add(ASE_ID(ase), rsp.code, rsp.reason);
|
|
|
|
return err;
|
|
}
|
|
|
|
stream = ase->ep.stream;
|
|
} else {
|
|
stream = NULL;
|
|
if (unicast_server_cb != NULL &&
|
|
unicast_server_cb->config != NULL) {
|
|
err = unicast_server_cb->config(ase->conn, &ase->ep, ase->ep.dir,
|
|
&ase->ep.codec_cfg, &stream,
|
|
&ase->ep.qos_pref, &rsp);
|
|
} else {
|
|
err = -ENOTSUP;
|
|
rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_UNSPECIFIED,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
}
|
|
|
|
if (err || stream == NULL) {
|
|
ascs_app_rsp_warn_valid(&rsp);
|
|
|
|
if (rsp.code == BT_BAP_ASCS_RSP_CODE_SUCCESS) {
|
|
rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_UNSPECIFIED,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
}
|
|
|
|
LOG_ERR("Config failed: err %d, stream %p, code %u, reason %u",
|
|
err, stream, rsp.code, rsp.reason);
|
|
|
|
(void)memcpy(&ase->ep.codec_cfg, &codec_cfg, sizeof(codec_cfg));
|
|
ascs_cp_rsp_add(ASE_ID(ase), rsp.code, rsp.reason);
|
|
|
|
return err ? err : -ENOMEM;
|
|
}
|
|
|
|
bt_bap_stream_init(stream);
|
|
}
|
|
|
|
ascs_cp_rsp_success(ASE_ID(ase));
|
|
|
|
bt_bap_stream_attach(ase->conn, stream, &ase->ep, &ase->ep.codec_cfg);
|
|
|
|
ascs_ep_set_state(&ase->ep, BT_BAP_EP_STATE_CODEC_CONFIGURED);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static struct bt_bap_ep *ep_lookup_stream(struct bt_conn *conn, struct bt_bap_stream *stream)
|
|
{
|
|
for (size_t i = 0; i < ARRAY_SIZE(ase_pool); i++) {
|
|
struct bt_ascs_ase *ase = &ase_pool[i];
|
|
|
|
if (ase->conn == conn && ase->ep.stream == stream) {
|
|
return &ase->ep;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
int bt_ascs_config_ase(struct bt_conn *conn, struct bt_bap_stream *stream,
|
|
struct bt_audio_codec_cfg *codec_cfg,
|
|
const struct bt_audio_codec_qos_pref *qos_pref)
|
|
{
|
|
int err;
|
|
struct bt_ascs_ase *ase = NULL;
|
|
struct bt_bap_ep *ep;
|
|
struct codec_cap_lookup_id_data lookup_data;
|
|
|
|
CHECKIF(conn == NULL || stream == NULL || codec_cfg == NULL || qos_pref == NULL) {
|
|
LOG_DBG("NULL value(s) supplied)");
|
|
return -EINVAL;
|
|
}
|
|
|
|
ep = ep_lookup_stream(conn, stream);
|
|
if (ep != NULL) {
|
|
LOG_DBG("Stream already configured for conn %p", (void *)stream->conn);
|
|
return -EALREADY;
|
|
}
|
|
|
|
/* Get a free ASE or NULL if all ASE instances are already in use */
|
|
for (int i = 1; i <= ASE_COUNT; i++) {
|
|
if (ase_find(conn, i) == NULL) {
|
|
ase = ase_new(conn, i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ase == NULL) {
|
|
LOG_WRN("No free ASE found.");
|
|
return -ENOTSUP;
|
|
}
|
|
|
|
ep = &ase->ep;
|
|
|
|
if (ep == NULL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
lookup_data.id = codec_cfg->id;
|
|
lookup_data.cid = codec_cfg->cid;
|
|
lookup_data.vid = codec_cfg->vid;
|
|
|
|
bt_pacs_cap_foreach(ep->dir, codec_lookup_id, &lookup_data);
|
|
|
|
if (lookup_data.codec_cap == NULL) {
|
|
LOG_DBG("Codec with id %u for dir %s is not supported by our capabilities",
|
|
codec_cfg->id, bt_audio_dir_str(ep->dir));
|
|
return -ENOENT;
|
|
}
|
|
|
|
(void)memcpy(&ep->codec_cfg, codec_cfg, sizeof(ep->codec_cfg));
|
|
|
|
ep->qos_pref = *qos_pref;
|
|
|
|
bt_bap_stream_attach(conn, stream, ep, &ep->codec_cfg);
|
|
|
|
err = ascs_ep_set_state(ep, BT_BAP_EP_STATE_CODEC_CONFIGURED);
|
|
if (err != 0) {
|
|
bt_bap_stream_detach(stream);
|
|
ase_free(ase);
|
|
return err;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static uint16_t get_max_ase_rsp_for_conn(struct bt_conn *conn)
|
|
{
|
|
const uint16_t max_ntf_size = get_max_ntf_size(conn);
|
|
const size_t rsp_hdr_size = sizeof(struct bt_ascs_cp_rsp);
|
|
|
|
if (max_ntf_size > rsp_hdr_size) {
|
|
return (max_ntf_size - rsp_hdr_size) / sizeof(struct bt_ascs_cp_ase_rsp);
|
|
}
|
|
|
|
return 0U;
|
|
}
|
|
|
|
static bool is_valid_num_ases(struct bt_conn *conn, uint8_t num_ases)
|
|
{
|
|
const uint16_t max_ase_rsp = get_max_ase_rsp_for_conn(conn);
|
|
|
|
if (num_ases < 1U) {
|
|
LOG_WRN("Number_of_ASEs parameter value is less than 1");
|
|
return false;
|
|
} else if (num_ases > ASE_COUNT) {
|
|
/* If the request is for more ASEs than we have, we just reject the request */
|
|
LOG_DBG("Number_of_ASEs parameter value (%u) is greater than %d", num_ases,
|
|
ASE_COUNT);
|
|
return false;
|
|
} else if (num_ases > max_ase_rsp) {
|
|
/* If the request is for more ASEs than we can respond to, we reject the request */
|
|
LOG_DBG("Number_of_ASEs parameter value (%u) is greater than what we can respond "
|
|
"to (%u) based on the MTU",
|
|
num_ases, max_ase_rsp);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool is_valid_config_len(struct bt_conn *conn, struct net_buf_simple *buf)
|
|
{
|
|
const struct bt_ascs_config_op *op;
|
|
struct net_buf_simple_state state;
|
|
|
|
net_buf_simple_save(buf, &state);
|
|
|
|
if (buf->len < sizeof(*op)) {
|
|
LOG_WRN("Invalid length %u < %zu", buf->len, sizeof(*op));
|
|
return false;
|
|
}
|
|
|
|
op = net_buf_simple_pull_mem(buf, sizeof(*op));
|
|
if (!is_valid_num_ases(conn, op->num_ases)) {
|
|
return false;
|
|
}
|
|
|
|
for (uint8_t i = 0U; i < op->num_ases; i++) {
|
|
const struct bt_ascs_config *config;
|
|
|
|
if (buf->len < sizeof(*config)) {
|
|
LOG_WRN("Malformed params array");
|
|
return false;
|
|
}
|
|
|
|
config = net_buf_simple_pull_mem(buf, sizeof(*config));
|
|
if (buf->len < config->cc_len) {
|
|
LOG_WRN("Malformed codec specific config");
|
|
return false;
|
|
}
|
|
|
|
(void)net_buf_simple_pull_mem(buf, config->cc_len);
|
|
}
|
|
|
|
if (buf->len > 0) {
|
|
LOG_WRN("Unexpected data");
|
|
return false;
|
|
}
|
|
|
|
net_buf_simple_restore(buf, &state);
|
|
|
|
return true;
|
|
}
|
|
|
|
static ssize_t ascs_config(struct bt_conn *conn, struct net_buf_simple *buf)
|
|
{
|
|
const struct bt_ascs_config_op *req;
|
|
const struct bt_ascs_config *cfg;
|
|
|
|
if (!is_valid_config_len(conn, buf)) {
|
|
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);
|
|
|
|
for (uint8_t i = 0; i < req->num_ases; i++) {
|
|
struct bt_ascs_ase *ase;
|
|
int err;
|
|
|
|
cfg = net_buf_simple_pull_mem(buf, sizeof(*cfg));
|
|
(void)net_buf_simple_pull(buf, cfg->cc_len);
|
|
|
|
LOG_DBG("ase 0x%02x cc_len %u", cfg->ase, cfg->cc_len);
|
|
|
|
if (!cfg->ase || cfg->ase > ASE_COUNT) {
|
|
LOG_WRN("Invalid ASE ID: %u", cfg->ase);
|
|
ascs_cp_rsp_add(cfg->ase, BT_BAP_ASCS_RSP_CODE_INVALID_ASE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
continue;
|
|
}
|
|
|
|
ase = ase_find(conn, cfg->ase);
|
|
if (ase != NULL) {
|
|
ase_config(ase, cfg);
|
|
continue;
|
|
}
|
|
|
|
ase = ase_new(conn, cfg->ase);
|
|
if (!ase) {
|
|
ascs_cp_rsp_add(cfg->ase, BT_BAP_ASCS_RSP_CODE_NO_MEM,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
LOG_WRN("No free ASE found for config ASE ID 0x%02x", cfg->ase);
|
|
continue;
|
|
}
|
|
|
|
err = ase_config(ase, cfg);
|
|
if (err != 0) {
|
|
ase_free(ase);
|
|
}
|
|
}
|
|
|
|
return buf->size;
|
|
}
|
|
|
|
void bt_ascs_foreach_ep(struct bt_conn *conn, bt_bap_ep_func_t func, void *user_data)
|
|
{
|
|
for (size_t i = 0; i < ARRAY_SIZE(ase_pool); i++) {
|
|
struct bt_ascs_ase *ase = &ase_pool[i];
|
|
|
|
if (ase->conn == conn) {
|
|
func(&ase->ep, user_data);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void ase_qos(struct bt_ascs_ase *ase, uint8_t cig_id, uint8_t cis_id,
|
|
struct bt_audio_codec_qos *qos, struct bt_bap_ascs_rsp *rsp)
|
|
{
|
|
struct bt_bap_ep *ep = &ase->ep;
|
|
struct bt_bap_stream *stream;
|
|
|
|
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, cig_id, cis_id, qos->interval, qos->framing, qos->phy,
|
|
qos->sdu, qos->rtn, qos->latency, qos->pd);
|
|
|
|
switch (ep->status.state) {
|
|
/* Valid only if ASE_State field = 0x01 (Codec Configured) */
|
|
case BT_BAP_EP_STATE_CODEC_CONFIGURED:
|
|
/* or 0x02 (QoS Configured) */
|
|
case BT_BAP_EP_STATE_QOS_CONFIGURED:
|
|
break;
|
|
default:
|
|
LOG_WRN("Invalid operation in state: %s", bt_bap_ep_state_str(ep->status.state));
|
|
*rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_INVALID_ASE_STATE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
return;
|
|
}
|
|
|
|
stream = ep->stream;
|
|
if (stream == NULL) {
|
|
LOG_ERR("NULL stream");
|
|
*rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_UNSPECIFIED, BT_BAP_ASCS_REASON_NONE);
|
|
return;
|
|
}
|
|
|
|
if (stream->ep == NULL) {
|
|
LOG_ERR("NULL stream->ep");
|
|
*rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_UNSPECIFIED, BT_BAP_ASCS_REASON_NONE);
|
|
return;
|
|
}
|
|
|
|
rsp->reason = bt_audio_verify_qos(qos);
|
|
if (rsp->reason != BT_BAP_ASCS_REASON_NONE) {
|
|
rsp->code = BT_BAP_ASCS_RSP_CODE_CONF_INVALID;
|
|
return;
|
|
}
|
|
|
|
rsp->reason = bt_bap_stream_verify_qos(stream, qos);
|
|
if (rsp->reason != BT_BAP_ASCS_REASON_NONE) {
|
|
rsp->code = BT_BAP_ASCS_RSP_CODE_CONF_INVALID;
|
|
return;
|
|
}
|
|
|
|
if (unicast_server_cb != NULL && unicast_server_cb->qos != NULL) {
|
|
int err = unicast_server_cb->qos(stream, qos, rsp);
|
|
|
|
if (err) {
|
|
if (rsp->code == BT_BAP_ASCS_RSP_CODE_SUCCESS) {
|
|
*rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_UNSPECIFIED,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
}
|
|
|
|
LOG_DBG("Application returned error: err %d status %u reason %u",
|
|
err, rsp->code, rsp->reason);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* QoS->QoS transition. Unbind ISO if CIG/CIS changed. */
|
|
if (ep->iso != NULL && (ep->cig_id != cig_id || ep->cis_id != cis_id)) {
|
|
bt_bap_iso_unbind_ep(ep->iso, ep);
|
|
}
|
|
|
|
if (ep->iso == NULL) {
|
|
struct bt_bap_iso *iso;
|
|
|
|
iso = bap_iso_get_or_new(ase->conn, cig_id, cis_id);
|
|
if (iso == NULL) {
|
|
LOG_ERR("Could not allocate bap_iso");
|
|
*rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_NO_MEM,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
return;
|
|
}
|
|
|
|
if (bt_bap_iso_get_ep(false, iso, ep->dir) != NULL) {
|
|
LOG_ERR("iso %p already in use in dir %s",
|
|
&iso->chan, bt_audio_dir_str(ep->dir));
|
|
bt_bap_iso_unref(iso);
|
|
*rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_CONF_INVALID,
|
|
BT_BAP_ASCS_REASON_CIS);
|
|
return;
|
|
}
|
|
|
|
bt_bap_iso_bind_ep(iso, ep);
|
|
bt_bap_iso_unref(iso);
|
|
}
|
|
|
|
/* Store the QoS once accepted */
|
|
ep->qos = *qos;
|
|
stream->qos = &ep->qos;
|
|
|
|
/* We setup the data path here, as this is the earliest where
|
|
* we have the ISO <-> EP coupling completed (due to setting
|
|
* the CIS ID in the QoS procedure).
|
|
*/
|
|
bt_bap_iso_configure_data_path(ep, stream->codec_cfg);
|
|
|
|
ep->cig_id = cig_id;
|
|
ep->cis_id = cis_id;
|
|
|
|
ascs_ep_set_state(ep, BT_BAP_EP_STATE_QOS_CONFIGURED);
|
|
|
|
*rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_SUCCESS, BT_BAP_ASCS_REASON_NONE);
|
|
}
|
|
|
|
static bool is_valid_qos_len(struct bt_conn *conn, struct net_buf_simple *buf)
|
|
{
|
|
const struct bt_ascs_qos_op *op;
|
|
struct net_buf_simple_state state;
|
|
size_t params_size;
|
|
|
|
net_buf_simple_save(buf, &state);
|
|
|
|
if (buf->len < sizeof(*op)) {
|
|
LOG_WRN("Invalid length %u < %zu", buf->len, sizeof(*op));
|
|
return false;
|
|
}
|
|
|
|
op = net_buf_simple_pull_mem(buf, sizeof(*op));
|
|
if (!is_valid_num_ases(conn, op->num_ases)) {
|
|
return false;
|
|
}
|
|
|
|
params_size = sizeof(struct bt_ascs_qos) * op->num_ases;
|
|
if (buf->len < params_size) {
|
|
LOG_WRN("Malformed params array");
|
|
return false;
|
|
}
|
|
|
|
(void)net_buf_simple_pull_mem(buf, params_size);
|
|
|
|
if (buf->len > 0) {
|
|
LOG_WRN("Unexpected data");
|
|
return false;
|
|
}
|
|
|
|
net_buf_simple_restore(buf, &state);
|
|
|
|
return true;
|
|
}
|
|
|
|
static ssize_t ascs_qos(struct bt_conn *conn, struct net_buf_simple *buf)
|
|
{
|
|
const struct bt_ascs_qos_op *req;
|
|
|
|
if (!is_valid_qos_len(conn, buf)) {
|
|
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);
|
|
|
|
for (uint8_t i = 0; i < req->num_ases; i++) {
|
|
struct bt_bap_ascs_rsp rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_UNSPECIFIED,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
struct bt_audio_codec_qos cqos;
|
|
const struct bt_ascs_qos *qos;
|
|
struct bt_ascs_ase *ase;
|
|
|
|
qos = net_buf_simple_pull_mem(buf, sizeof(*qos));
|
|
|
|
LOG_DBG("ase 0x%02x", qos->ase);
|
|
|
|
if (!is_valid_ase_id(qos->ase)) {
|
|
ascs_cp_rsp_add(qos->ase, BT_BAP_ASCS_RSP_CODE_INVALID_ASE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
LOG_WRN("Unknown ase 0x%02x", qos->ase);
|
|
continue;
|
|
}
|
|
|
|
ase = ase_find(conn, qos->ase);
|
|
if (!ase) {
|
|
LOG_DBG("Invalid operation for idle ASE");
|
|
ascs_cp_rsp_add(qos->ase, BT_BAP_ASCS_RSP_CODE_INVALID_ASE_STATE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
continue;
|
|
}
|
|
|
|
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);
|
|
|
|
ase_qos(ase, qos->cig, qos->cis, &cqos, &rsp);
|
|
ascs_cp_rsp_add(qos->ase, rsp.code, rsp.reason);
|
|
}
|
|
|
|
return buf->size;
|
|
}
|
|
|
|
struct ascs_parse_result {
|
|
int err;
|
|
struct bt_conn *conn;
|
|
struct bt_bap_ascs_rsp *rsp;
|
|
const struct bt_bap_ep *ep;
|
|
};
|
|
|
|
static bool is_context_available(struct bt_conn *conn, enum bt_audio_dir dir, uint16_t context)
|
|
{
|
|
return (context & bt_pacs_get_available_contexts_for_conn(conn, dir)) == context;
|
|
}
|
|
|
|
static bool ascs_parse_metadata(struct bt_data *data, void *user_data)
|
|
{
|
|
struct ascs_parse_result *result = user_data;
|
|
const struct bt_bap_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;
|
|
|
|
LOG_DBG("type 0x%02x len %u", data_type, data_len);
|
|
|
|
if (!BT_AUDIO_METADATA_TYPE_IS_KNOWN(data_type)) {
|
|
LOG_WRN("Unknown metadata type 0x%02x", data_type);
|
|
return true;
|
|
}
|
|
|
|
switch (data_type) {
|
|
/* TODO: Consider rejecting BT_AUDIO_METADATA_TYPE_PREF_CONTEXT type */
|
|
case BT_AUDIO_METADATA_TYPE_PREF_CONTEXT:
|
|
case BT_AUDIO_METADATA_TYPE_STREAM_CONTEXT: {
|
|
uint16_t context;
|
|
|
|
if (data_len != sizeof(context)) {
|
|
*result->rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_METADATA_INVALID,
|
|
data_type);
|
|
result->err = -EBADMSG;
|
|
return false;
|
|
}
|
|
|
|
context = sys_get_le16(data_value);
|
|
if (context == BT_AUDIO_CONTEXT_TYPE_PROHIBITED) {
|
|
*result->rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_METADATA_INVALID,
|
|
data_type);
|
|
result->err = -EINVAL;
|
|
return false;
|
|
}
|
|
|
|
/* The CAP acceptor shall not accept metadata with unsupported stream context. */
|
|
if (IS_ENABLED(CONFIG_BT_CAP_ACCEPTOR) &&
|
|
data_type == BT_AUDIO_METADATA_TYPE_STREAM_CONTEXT) {
|
|
if (!is_context_available(result->conn, ep->dir, context)) {
|
|
LOG_WRN("Context 0x%04x is unavailable", context);
|
|
*result->rsp = BT_BAP_ASCS_RSP(
|
|
BT_BAP_ASCS_RSP_CODE_METADATA_REJECTED, data_type);
|
|
result->err = -EACCES;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case BT_AUDIO_METADATA_TYPE_LANG:
|
|
if (data_len != BT_AUDIO_LANG_SIZE) {
|
|
*result->rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_METADATA_INVALID,
|
|
data_type);
|
|
result->err = -EBADMSG;
|
|
return false;
|
|
}
|
|
|
|
break;
|
|
case BT_AUDIO_METADATA_TYPE_CCID_LIST: {
|
|
/* Verify that the CCID is a known CCID on the writing device */
|
|
if (IS_ENABLED(CONFIG_BT_CAP_ACCEPTOR)) {
|
|
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?
|
|
*/
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case BT_AUDIO_METADATA_TYPE_PARENTAL_RATING:
|
|
if (data_len != 1) {
|
|
*result->rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_METADATA_INVALID,
|
|
data_type);
|
|
result->err = -EBADMSG;
|
|
return false;
|
|
}
|
|
|
|
break;
|
|
case BT_AUDIO_METADATA_TYPE_AUDIO_STATE: {
|
|
uint8_t state;
|
|
|
|
if (data_len != sizeof(state)) {
|
|
*result->rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_METADATA_INVALID,
|
|
data_type);
|
|
result->err = -EBADMSG;
|
|
return false;
|
|
}
|
|
|
|
break;
|
|
}
|
|
/* TODO: Consider rejecting BT_AUDIO_METADATA_TYPE_BROADCAST_IMMEDIATE type */
|
|
case BT_AUDIO_METADATA_TYPE_BROADCAST_IMMEDIATE:
|
|
if (data_len != 0) {
|
|
*result->rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_METADATA_INVALID,
|
|
data_type);
|
|
result->err = -EBADMSG;
|
|
return false;
|
|
}
|
|
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static int ascs_verify_metadata(struct bt_bap_ep *ep, const struct bt_ascs_metadata *meta,
|
|
struct bt_bap_ascs_rsp *rsp)
|
|
{
|
|
struct bt_ascs_ase *ase = CONTAINER_OF(ep, struct bt_ascs_ase, ep);
|
|
struct ascs_parse_result result = {
|
|
.conn = ase->conn,
|
|
.rsp = rsp,
|
|
.err = 0,
|
|
.ep = ep,
|
|
};
|
|
int err;
|
|
|
|
if (meta->len > CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE) {
|
|
LOG_WRN("Not enough space for Codec Config Metadata: %u > %d", meta->len,
|
|
CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE);
|
|
*rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_NO_MEM, BT_BAP_ASCS_REASON_NONE);
|
|
|
|
return -ENOMEM;
|
|
}
|
|
|
|
/* Parse LTV entries */
|
|
err = bt_audio_data_parse(meta->data, meta->len, ascs_parse_metadata, &result);
|
|
if (err != 0 && err != -ECANCELED) {
|
|
/* ECANCELED is called if the callback stops the parsing prematurely, in which case
|
|
* result.err will be set
|
|
*/
|
|
LOG_DBG("Failed to parse metadata: %d", err);
|
|
*rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_METADATA_INVALID,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
|
|
return err;
|
|
}
|
|
|
|
return result.err;
|
|
}
|
|
|
|
static void ase_metadata(struct bt_ascs_ase *ase, struct bt_ascs_metadata *meta)
|
|
{
|
|
struct bt_bap_stream *stream;
|
|
struct bt_bap_ep *ep;
|
|
struct bt_bap_ascs_rsp rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_SUCCESS,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
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_BAP_EP_STATE_ENABLING:
|
|
/* or 0x04 (Streaming) */
|
|
case BT_BAP_EP_STATE_STREAMING:
|
|
break;
|
|
default:
|
|
LOG_WRN("Invalid operation in state: %s", bt_bap_ep_state_str(state));
|
|
ascs_cp_rsp_add(ASE_ID(ase), BT_BAP_ASCS_RSP_CODE_INVALID_ASE_STATE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
return;
|
|
}
|
|
|
|
stream = ep->stream;
|
|
|
|
err = ascs_verify_metadata(ep, meta, &rsp);
|
|
if (err != 0) {
|
|
LOG_DBG("Invalid metadata from client: %d", err);
|
|
|
|
/* rsp will be set by ascs_verify_metadata*/
|
|
} else if (unicast_server_cb != NULL && unicast_server_cb->metadata != NULL) {
|
|
err = unicast_server_cb->metadata(stream, meta->data, meta->len, &rsp);
|
|
} else {
|
|
err = -ENOTSUP;
|
|
rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_UNSPECIFIED,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
}
|
|
|
|
if (err) {
|
|
ascs_app_rsp_warn_valid(&rsp);
|
|
|
|
if (rsp.code == BT_BAP_ASCS_RSP_CODE_SUCCESS) {
|
|
rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_UNSPECIFIED,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
}
|
|
|
|
LOG_ERR("Metadata failed: err %d, code %u, reason %u", err, rsp.code, rsp.reason);
|
|
ascs_cp_rsp_add(ASE_ID(ase), rsp.code, rsp.reason);
|
|
return;
|
|
}
|
|
|
|
ep->codec_cfg.meta_len = meta->len;
|
|
(void)memcpy(ep->codec_cfg.meta, meta->data, meta->len);
|
|
|
|
/* Set the state to the same state to trigger the notifications */
|
|
ascs_ep_set_state(ep, ep->status.state);
|
|
ascs_cp_rsp_success(ASE_ID(ase));
|
|
}
|
|
|
|
static int ase_enable(struct bt_ascs_ase *ase, struct bt_ascs_metadata *meta)
|
|
{
|
|
struct bt_bap_stream *stream;
|
|
struct bt_bap_ep *ep;
|
|
struct bt_bap_ascs_rsp rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_SUCCESS,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
int err;
|
|
|
|
LOG_DBG("ase %p meta->len %u", ase, meta->len);
|
|
|
|
ep = &ase->ep;
|
|
|
|
/* Valid for an ASE only if ASE_State field = 0x02 (QoS Configured) */
|
|
if (ep->status.state != BT_BAP_EP_STATE_QOS_CONFIGURED) {
|
|
err = -EBADMSG;
|
|
LOG_WRN("Invalid operation in state: %s", bt_bap_ep_state_str(ep->status.state));
|
|
ascs_cp_rsp_add(ASE_ID(ase), BT_BAP_ASCS_RSP_CODE_INVALID_ASE_STATE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
return err;
|
|
}
|
|
|
|
stream = ep->stream;
|
|
|
|
err = ascs_verify_metadata(ep, meta, &rsp);
|
|
if (err != 0) {
|
|
LOG_DBG("Invalid metadata from client: %d", err);
|
|
|
|
/* rsp will be set by ascs_verify_metadata*/
|
|
} else if (unicast_server_cb != NULL && unicast_server_cb->enable != NULL) {
|
|
err = unicast_server_cb->enable(stream, meta->data, meta->len, &rsp);
|
|
} else {
|
|
err = -ENOTSUP;
|
|
rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_UNSPECIFIED,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
}
|
|
|
|
if (err) {
|
|
ascs_app_rsp_warn_valid(&rsp);
|
|
|
|
if (rsp.code == BT_BAP_ASCS_RSP_CODE_SUCCESS) {
|
|
rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_UNSPECIFIED,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
}
|
|
|
|
LOG_ERR("Enable rejected: err %d, code %u, reason %u", err, rsp.code, rsp.reason);
|
|
ascs_cp_rsp_add(ASE_ID(ase), rsp.code, rsp.reason);
|
|
|
|
return -EFAULT;
|
|
}
|
|
|
|
ep->codec_cfg.meta_len = meta->len;
|
|
(void)memcpy(ep->codec_cfg.meta, meta->data, meta->len);
|
|
|
|
ascs_ep_set_state(ep, BT_BAP_EP_STATE_ENABLING);
|
|
|
|
ascs_cp_rsp_success(ASE_ID(ase));
|
|
|
|
return 0;
|
|
}
|
|
|
|
static bool is_valid_enable_len(struct bt_conn *conn, struct net_buf_simple *buf)
|
|
{
|
|
const struct bt_ascs_enable_op *op;
|
|
struct net_buf_simple_state state;
|
|
|
|
net_buf_simple_save(buf, &state);
|
|
|
|
if (buf->len < sizeof(*op)) {
|
|
LOG_WRN("Invalid length %u < %zu", buf->len, sizeof(*op));
|
|
return false;
|
|
}
|
|
|
|
op = net_buf_simple_pull_mem(buf, sizeof(*op));
|
|
if (!is_valid_num_ases(conn, op->num_ases)) {
|
|
return false;
|
|
}
|
|
|
|
for (uint8_t i = 0U; i < op->num_ases; i++) {
|
|
const struct bt_ascs_metadata *metadata;
|
|
|
|
if (buf->len < sizeof(*metadata)) {
|
|
LOG_WRN("Malformed params array");
|
|
return false;
|
|
}
|
|
|
|
metadata = net_buf_simple_pull_mem(buf, sizeof(*metadata));
|
|
if (buf->len < metadata->len) {
|
|
LOG_WRN("Malformed metadata");
|
|
return false;
|
|
}
|
|
|
|
(void)net_buf_simple_pull_mem(buf, metadata->len);
|
|
}
|
|
|
|
if (buf->len > 0) {
|
|
LOG_WRN("Unexpected data");
|
|
return false;
|
|
}
|
|
|
|
net_buf_simple_restore(buf, &state);
|
|
|
|
return true;
|
|
}
|
|
|
|
static ssize_t ascs_enable(struct bt_conn *conn, struct net_buf_simple *buf)
|
|
{
|
|
const struct bt_ascs_enable_op *req;
|
|
struct bt_ascs_metadata *meta;
|
|
int i;
|
|
|
|
if (!is_valid_enable_len(conn, buf)) {
|
|
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);
|
|
|
|
for (i = 0; i < req->num_ases; i++) {
|
|
struct bt_ascs_ase *ase;
|
|
|
|
meta = net_buf_simple_pull_mem(buf, sizeof(*meta));
|
|
(void)net_buf_simple_pull(buf, meta->len);
|
|
|
|
if (!is_valid_ase_id(meta->ase)) {
|
|
ascs_cp_rsp_add(meta->ase, BT_BAP_ASCS_RSP_CODE_INVALID_ASE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
LOG_WRN("Unknown ase 0x%02x", meta->ase);
|
|
continue;
|
|
}
|
|
|
|
ase = ase_find(conn, meta->ase);
|
|
if (!ase) {
|
|
LOG_DBG("Invalid operation for idle ase 0x%02x", meta->ase);
|
|
ascs_cp_rsp_add(meta->ase, BT_BAP_ASCS_RSP_CODE_INVALID_ASE_STATE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
continue;
|
|
}
|
|
|
|
ase_enable(ase, meta);
|
|
}
|
|
|
|
return buf->size;
|
|
}
|
|
|
|
static void ase_start(struct bt_ascs_ase *ase)
|
|
{
|
|
struct bt_bap_ep *ep;
|
|
struct bt_bap_ascs_rsp rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_SUCCESS,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
int err;
|
|
|
|
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_BAP_EP_STATE_ENABLING) {
|
|
LOG_WRN("Invalid operation in state: %s", bt_bap_ep_state_str(ep->status.state));
|
|
ascs_cp_rsp_add(ASE_ID(ase), BT_BAP_ASCS_RSP_CODE_INVALID_ASE_STATE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
return;
|
|
}
|
|
|
|
if (ep->iso->chan.state != BT_ISO_STATE_CONNECTED) {
|
|
/* An ASE may not go into the streaming state unless the CIS
|
|
* is connected
|
|
*/
|
|
LOG_WRN("Start failed: CIS not connected: %u",
|
|
ep->iso->chan.state);
|
|
ascs_cp_rsp_add(ASE_ID(ase), BT_BAP_ASCS_RSP_CODE_INVALID_ASE_STATE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
return;
|
|
}
|
|
|
|
if (unicast_server_cb != NULL && unicast_server_cb->start != NULL) {
|
|
err = unicast_server_cb->start(ep->stream, &rsp);
|
|
} else {
|
|
err = -ENOTSUP;
|
|
rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_UNSPECIFIED,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
}
|
|
|
|
if (err) {
|
|
ascs_app_rsp_warn_valid(&rsp);
|
|
|
|
if (rsp.code == BT_BAP_ASCS_RSP_CODE_SUCCESS) {
|
|
rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_UNSPECIFIED,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
}
|
|
|
|
LOG_ERR("Start failed: err %d, code %u, reason %u", err, rsp.code, rsp.reason);
|
|
ascs_cp_rsp_add(ASE_ID(ase), rsp.code, rsp.reason);
|
|
|
|
return;
|
|
}
|
|
|
|
ep->receiver_ready = true;
|
|
|
|
ascs_ep_set_state(ep, BT_BAP_EP_STATE_STREAMING);
|
|
|
|
ascs_cp_rsp_success(ASE_ID(ase));
|
|
}
|
|
|
|
static bool is_valid_start_len(struct bt_conn *conn, struct net_buf_simple *buf)
|
|
{
|
|
const struct bt_ascs_start_op *op;
|
|
struct net_buf_simple_state state;
|
|
|
|
net_buf_simple_save(buf, &state);
|
|
|
|
if (buf->len < sizeof(*op)) {
|
|
LOG_WRN("Invalid length %u < %zu", buf->len, sizeof(*op));
|
|
return false;
|
|
}
|
|
|
|
op = net_buf_simple_pull_mem(buf, sizeof(*op));
|
|
if (!is_valid_num_ases(conn, op->num_ases)) {
|
|
return false;
|
|
}
|
|
|
|
if (buf->len != op->num_ases) {
|
|
LOG_WRN("Number_of_ASEs mismatch");
|
|
return false;
|
|
}
|
|
|
|
net_buf_simple_restore(buf, &state);
|
|
|
|
return true;
|
|
}
|
|
|
|
static ssize_t ascs_start(struct bt_conn *conn, struct net_buf_simple *buf)
|
|
{
|
|
const struct bt_ascs_start_op *req;
|
|
int i;
|
|
|
|
if (!is_valid_start_len(conn, buf)) {
|
|
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);
|
|
|
|
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);
|
|
|
|
if (!is_valid_ase_id(id)) {
|
|
ascs_cp_rsp_add(id, BT_BAP_ASCS_RSP_CODE_INVALID_ASE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
LOG_WRN("Unknown ase 0x%02x", id);
|
|
continue;
|
|
}
|
|
|
|
/* 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 (ASE_DIR(id) == BT_AUDIO_DIR_SINK) {
|
|
LOG_WRN("Start failed: invalid operation for Sink");
|
|
ascs_cp_rsp_add(id, BT_BAP_ASCS_RSP_CODE_INVALID_DIR,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
continue;
|
|
}
|
|
|
|
ase = ase_find(conn, id);
|
|
if (!ase) {
|
|
LOG_DBG("Invalid operation for idle ASE");
|
|
ascs_cp_rsp_add(id, BT_BAP_ASCS_RSP_CODE_INVALID_ASE_STATE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
continue;
|
|
}
|
|
|
|
ase_start(ase);
|
|
}
|
|
|
|
return buf->size;
|
|
}
|
|
|
|
static bool is_valid_disable_len(struct bt_conn *conn, struct net_buf_simple *buf)
|
|
{
|
|
const struct bt_ascs_disable_op *op;
|
|
struct net_buf_simple_state state;
|
|
|
|
net_buf_simple_save(buf, &state);
|
|
|
|
if (buf->len < sizeof(*op)) {
|
|
LOG_WRN("Invalid length %u < %zu", buf->len, sizeof(*op));
|
|
return false;
|
|
}
|
|
|
|
op = net_buf_simple_pull_mem(buf, sizeof(*op));
|
|
if (!is_valid_num_ases(conn, op->num_ases)) {
|
|
return false;
|
|
}
|
|
|
|
if (buf->len != op->num_ases) {
|
|
LOG_WRN("Number_of_ASEs mismatch");
|
|
return false;
|
|
}
|
|
|
|
net_buf_simple_restore(buf, &state);
|
|
|
|
return true;
|
|
}
|
|
|
|
static ssize_t ascs_disable(struct bt_conn *conn, struct net_buf_simple *buf)
|
|
{
|
|
const struct bt_ascs_disable_op *req;
|
|
|
|
if (!is_valid_disable_len(conn, buf)) {
|
|
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);
|
|
|
|
for (uint8_t i = 0; i < req->num_ases; i++) {
|
|
struct bt_bap_ascs_rsp rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_UNSPECIFIED,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
struct bt_ascs_ase *ase;
|
|
uint8_t id;
|
|
|
|
id = net_buf_simple_pull_u8(buf);
|
|
|
|
LOG_DBG("ase 0x%02x", id);
|
|
|
|
if (!is_valid_ase_id(id)) {
|
|
ascs_cp_rsp_add(id, BT_BAP_ASCS_RSP_CODE_INVALID_ASE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
LOG_WRN("Unknown ase 0x%02x", id);
|
|
continue;
|
|
}
|
|
|
|
ase = ase_find(conn, id);
|
|
if (!ase) {
|
|
LOG_DBG("Invalid operation for idle ASE");
|
|
ascs_cp_rsp_add(id, BT_BAP_ASCS_RSP_CODE_INVALID_ASE_STATE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
continue;
|
|
}
|
|
|
|
ase_disable(ase, BT_HCI_ERR_REMOTE_USER_TERM_CONN, &rsp);
|
|
ascs_cp_rsp_add(id, rsp.code, rsp.reason);
|
|
}
|
|
|
|
return buf->size;
|
|
}
|
|
|
|
static void ase_stop(struct bt_ascs_ase *ase)
|
|
{
|
|
struct bt_bap_stream *stream;
|
|
struct bt_bap_ep *ep;
|
|
struct bt_bap_ascs_rsp rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_SUCCESS,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
int err;
|
|
|
|
LOG_DBG("ase %p", ase);
|
|
|
|
ep = &ase->ep;
|
|
|
|
if (ep->status.state != BT_BAP_EP_STATE_DISABLING) {
|
|
LOG_WRN("Invalid operation in state: %s", bt_bap_ep_state_str(ep->status.state));
|
|
ascs_cp_rsp_add(ASE_ID(ase), BT_BAP_ASCS_RSP_CODE_INVALID_ASE_STATE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
return;
|
|
}
|
|
|
|
stream = ep->stream;
|
|
if (unicast_server_cb != NULL && unicast_server_cb->stop != NULL) {
|
|
err = unicast_server_cb->stop(stream, &rsp);
|
|
} else {
|
|
err = -ENOTSUP;
|
|
rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_UNSPECIFIED,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
}
|
|
|
|
if (err) {
|
|
ascs_app_rsp_warn_valid(&rsp);
|
|
|
|
if (rsp.code == BT_BAP_ASCS_RSP_CODE_SUCCESS) {
|
|
rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_UNSPECIFIED,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
}
|
|
|
|
LOG_ERR("Stop failed: err %d, code %u, reason %u", err, rsp.code, rsp.reason);
|
|
ascs_cp_rsp_add(ASE_ID(ase), rsp.code, rsp.reason);
|
|
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.
|
|
*/
|
|
if (bt_bap_stream_can_disconnect(stream)) {
|
|
err = ascs_disconnect_stream(stream);
|
|
if (err < 0) {
|
|
LOG_ERR("Failed to disconnect stream %p: %d", stream, err);
|
|
}
|
|
}
|
|
|
|
ascs_ep_set_state(ep, BT_BAP_EP_STATE_QOS_CONFIGURED);
|
|
|
|
ascs_cp_rsp_success(ASE_ID(ase));
|
|
}
|
|
|
|
static bool is_valid_stop_len(struct bt_conn *conn, struct net_buf_simple *buf)
|
|
{
|
|
const struct bt_ascs_stop_op *op;
|
|
struct net_buf_simple_state state;
|
|
|
|
net_buf_simple_save(buf, &state);
|
|
|
|
if (buf->len < sizeof(*op)) {
|
|
LOG_WRN("Invalid length %u < %zu", buf->len, sizeof(*op));
|
|
return false;
|
|
}
|
|
|
|
op = net_buf_simple_pull_mem(buf, sizeof(*op));
|
|
if (op->num_ases < 1U) {
|
|
LOG_WRN("Number_of_ASEs parameter value is less than 1");
|
|
return false;
|
|
}
|
|
|
|
if (buf->len != op->num_ases) {
|
|
LOG_WRN("Number_of_ASEs mismatch");
|
|
return false;
|
|
}
|
|
|
|
net_buf_simple_restore(buf, &state);
|
|
|
|
return true;
|
|
}
|
|
|
|
static ssize_t ascs_stop(struct bt_conn *conn, struct net_buf_simple *buf)
|
|
{
|
|
const struct bt_ascs_start_op *req;
|
|
int i;
|
|
|
|
if (!is_valid_stop_len(conn, buf)) {
|
|
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);
|
|
|
|
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);
|
|
|
|
if (!is_valid_ase_id(id)) {
|
|
ascs_cp_rsp_add(id, BT_BAP_ASCS_RSP_CODE_INVALID_ASE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
LOG_WRN("Unknown ase 0x%02x", id);
|
|
continue;
|
|
}
|
|
|
|
/* 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_DIR(id) == BT_AUDIO_DIR_SINK) {
|
|
LOG_WRN("Stop failed: invalid operation for Sink");
|
|
ascs_cp_rsp_add(id, BT_BAP_ASCS_RSP_CODE_INVALID_DIR,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
continue;
|
|
}
|
|
|
|
ase = ase_find(conn, id);
|
|
if (!ase) {
|
|
LOG_DBG("Invalid operation for idle ASE");
|
|
ascs_cp_rsp_add(id, BT_BAP_ASCS_RSP_CODE_INVALID_ASE_STATE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
continue;
|
|
}
|
|
|
|
ase_stop(ase);
|
|
}
|
|
|
|
return buf->size;
|
|
}
|
|
|
|
static bool is_valid_metadata_len(struct bt_conn *conn, struct net_buf_simple *buf)
|
|
{
|
|
const struct bt_ascs_metadata_op *op;
|
|
struct net_buf_simple_state state;
|
|
|
|
net_buf_simple_save(buf, &state);
|
|
|
|
if (buf->len < sizeof(*op)) {
|
|
LOG_WRN("Invalid length %u < %zu", buf->len, sizeof(*op));
|
|
return false;
|
|
}
|
|
|
|
op = net_buf_simple_pull_mem(buf, sizeof(*op));
|
|
if (!is_valid_num_ases(conn, op->num_ases)) {
|
|
return false;
|
|
}
|
|
|
|
for (uint8_t i = 0U; i < op->num_ases; i++) {
|
|
const struct bt_ascs_metadata *metadata;
|
|
|
|
if (buf->len < sizeof(*metadata)) {
|
|
LOG_WRN("Malformed params array");
|
|
return false;
|
|
}
|
|
|
|
metadata = net_buf_simple_pull_mem(buf, sizeof(*metadata));
|
|
if (buf->len < metadata->len) {
|
|
LOG_WRN("Malformed metadata");
|
|
return false;
|
|
}
|
|
|
|
(void)net_buf_simple_pull_mem(buf, metadata->len);
|
|
}
|
|
|
|
if (buf->len > 0) {
|
|
LOG_WRN("Unexpected data");
|
|
return false;
|
|
}
|
|
|
|
net_buf_simple_restore(buf, &state);
|
|
|
|
return true;
|
|
}
|
|
|
|
static ssize_t ascs_metadata(struct bt_conn *conn, struct net_buf_simple *buf)
|
|
{
|
|
const struct bt_ascs_metadata_op *req;
|
|
struct bt_ascs_metadata *meta;
|
|
int i;
|
|
|
|
if (!is_valid_metadata_len(conn, buf)) {
|
|
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);
|
|
|
|
for (i = 0; i < req->num_ases; i++) {
|
|
struct bt_ascs_ase *ase;
|
|
|
|
meta = net_buf_simple_pull_mem(buf, sizeof(*meta));
|
|
(void)net_buf_simple_pull(buf, meta->len);
|
|
|
|
if (meta->len > CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE) {
|
|
LOG_DBG("Cannot store %u octets of metadata", meta->len);
|
|
|
|
ascs_cp_rsp_add(meta->ase, BT_BAP_ASCS_RSP_CODE_NO_MEM,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
continue;
|
|
}
|
|
|
|
if (!is_valid_ase_id(meta->ase)) {
|
|
ascs_cp_rsp_add(meta->ase, BT_BAP_ASCS_RSP_CODE_INVALID_ASE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
LOG_WRN("Unknown ase 0x%02x", meta->ase);
|
|
continue;
|
|
}
|
|
|
|
ase = ase_find(conn, meta->ase);
|
|
if (!ase) {
|
|
LOG_DBG("Invalid operation for idle ase 0x%02x", meta->ase);
|
|
ascs_cp_rsp_add(meta->ase, BT_BAP_ASCS_RSP_CODE_INVALID_ASE_STATE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
continue;
|
|
}
|
|
|
|
ase_metadata(ase, meta);
|
|
}
|
|
|
|
return buf->size;
|
|
}
|
|
|
|
static bool is_valid_release_len(struct bt_conn *conn, struct net_buf_simple *buf)
|
|
{
|
|
const struct bt_ascs_release_op *op;
|
|
struct net_buf_simple_state state;
|
|
|
|
net_buf_simple_save(buf, &state);
|
|
|
|
if (buf->len < sizeof(*op)) {
|
|
LOG_WRN("Invalid length %u < %zu", buf->len, sizeof(*op));
|
|
return false;
|
|
}
|
|
|
|
op = net_buf_simple_pull_mem(buf, sizeof(*op));
|
|
if (!is_valid_num_ases(conn, op->num_ases)) {
|
|
return false;
|
|
}
|
|
|
|
if (buf->len != op->num_ases) {
|
|
LOG_WRN("Number_of_ASEs mismatch");
|
|
return false;
|
|
}
|
|
|
|
net_buf_simple_restore(buf, &state);
|
|
|
|
return true;
|
|
}
|
|
|
|
static ssize_t ascs_release(struct bt_conn *conn, struct net_buf_simple *buf)
|
|
{
|
|
const struct bt_ascs_release_op *req;
|
|
int i;
|
|
|
|
if (!is_valid_release_len(conn, buf)) {
|
|
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);
|
|
|
|
for (i = 0; i < req->num_ases; i++) {
|
|
struct bt_bap_ascs_rsp rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_UNSPECIFIED,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
struct bt_ascs_ase *ase;
|
|
uint8_t id;
|
|
|
|
id = net_buf_simple_pull_u8(buf);
|
|
|
|
LOG_DBG("ase 0x%02x", id);
|
|
|
|
if (!is_valid_ase_id(id)) {
|
|
ascs_cp_rsp_add(id, BT_BAP_ASCS_RSP_CODE_INVALID_ASE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
LOG_WRN("Unknown ase 0x%02x", id);
|
|
continue;
|
|
}
|
|
|
|
ase = ase_find(conn, id);
|
|
if (!ase) {
|
|
LOG_DBG("Invalid operation for idle ASE");
|
|
ascs_cp_rsp_add(id, BT_BAP_ASCS_RSP_CODE_INVALID_ASE_STATE,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
continue;
|
|
}
|
|
|
|
ase_release(ase, BT_HCI_ERR_REMOTE_USER_TERM_CONN, &rsp);
|
|
ascs_cp_rsp_add(id, rsp.code, rsp.reason);
|
|
}
|
|
|
|
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)
|
|
{
|
|
const struct bt_ascs_ase_cp *req;
|
|
struct net_buf_simple buf;
|
|
ssize_t ret;
|
|
|
|
if (flags & BT_GATT_WRITE_FLAG_PREPARE) {
|
|
/* Return 0 to allow long writes */
|
|
return 0;
|
|
}
|
|
|
|
if (offset != 0 && (flags & BT_GATT_WRITE_FLAG_EXECUTE) == 0) {
|
|
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)",
|
|
(void *)conn, attr, data, len, bt_ascs_op_str(req->op), req->op);
|
|
|
|
ascs_cp_rsp_init(req->op);
|
|
|
|
switch (req->op) {
|
|
case BT_ASCS_CONFIG_OP:
|
|
ret = ascs_config(conn, &buf);
|
|
break;
|
|
case BT_ASCS_QOS_OP:
|
|
ret = ascs_qos(conn, &buf);
|
|
break;
|
|
case BT_ASCS_ENABLE_OP:
|
|
ret = ascs_enable(conn, &buf);
|
|
break;
|
|
case BT_ASCS_START_OP:
|
|
ret = ascs_start(conn, &buf);
|
|
break;
|
|
case BT_ASCS_DISABLE_OP:
|
|
ret = ascs_disable(conn, &buf);
|
|
break;
|
|
case BT_ASCS_STOP_OP:
|
|
ret = ascs_stop(conn, &buf);
|
|
break;
|
|
case BT_ASCS_METADATA_OP:
|
|
ret = ascs_metadata(conn, &buf);
|
|
break;
|
|
case BT_ASCS_RELEASE_OP:
|
|
ret = ascs_release(conn, &buf);
|
|
break;
|
|
default:
|
|
ascs_cp_rsp_add(BT_ASCS_ASE_ID_NONE, BT_BAP_ASCS_RSP_CODE_NOT_SUPPORTED,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
LOG_DBG("Unknown opcode");
|
|
goto respond;
|
|
}
|
|
|
|
if (ret == BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN)) {
|
|
ascs_cp_rsp_add(BT_ASCS_ASE_ID_NONE, BT_BAP_ASCS_RSP_CODE_INVALID_LENGTH,
|
|
BT_BAP_ASCS_REASON_NONE);
|
|
}
|
|
|
|
respond:
|
|
control_point_notify(conn, cp_rsp_buf.data, cp_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 | BT_GATT_PERM_PREPARE_WRITE,
|
|
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);
|
|
}
|
|
|
|
static struct bt_iso_server iso_server = {
|
|
.sec_level = BT_SECURITY_L2,
|
|
.accept = ascs_iso_accept,
|
|
};
|
|
|
|
int bt_ascs_init(const struct bt_bap_unicast_server_cb *cb)
|
|
{
|
|
int err;
|
|
|
|
if (unicast_server_cb != NULL) {
|
|
return -EALREADY;
|
|
}
|
|
|
|
err = bt_iso_server_register(&iso_server);
|
|
if (err) {
|
|
LOG_ERR("Failed to register ISO server %d", err);
|
|
return err;
|
|
}
|
|
|
|
unicast_server_cb = cb;
|
|
|
|
return 0;
|
|
}
|
|
|
|
void bt_ascs_cleanup(void)
|
|
{
|
|
for (size_t i = 0; i < ARRAY_SIZE(ase_pool); i++) {
|
|
struct bt_ascs_ase *ase = &ase_pool[i];
|
|
|
|
if (ase->conn != NULL) {
|
|
bt_ascs_release_ase(&ase->ep);
|
|
}
|
|
}
|
|
|
|
if (unicast_server_cb != NULL) {
|
|
bt_iso_server_unregister(&iso_server);
|
|
unicast_server_cb = NULL;
|
|
}
|
|
}
|
|
#endif /* BT_BAP_UNICAST_SERVER */
|