Bluetooth: CAP: Commander set volume support

Add support for setting volume on one or more CAP
acceptors using the CAP Commander API.

Signed-off-by: Emil Gydesen <emil.gydesen@nordicsemi.no>
This commit is contained in:
Emil Gydesen 2023-11-15 10:24:54 +01:00 committed by Anas Nashif
commit b09ce2fade
7 changed files with 397 additions and 15 deletions

View file

@ -317,7 +317,7 @@ int bt_cap_initiator_unicast_audio_stop(struct bt_bap_unicast_group *unicast_gro
* This will stop the current procedure from continuing and making it possible to run a new
* Common Audio Profile procedure.
*
* It is recommended to do this if any existing procedure take longer time than expected, which
* It is recommended to do this if any existing procedure takes longer time than expected, which
* could indicate a missing response from the Common Audio Profile Acceptor.
*
* This does not send any requests to any Common Audio Profile Acceptors involved with the current
@ -655,6 +655,20 @@ struct bt_cap_commander_cb {
*/
void (*discovery_complete)(struct bt_conn *conn, int err,
const struct bt_csip_set_coordinator_csis_inst *csis_inst);
#if defined(CONFIG_BT_VCP_VOL_CTLR)
/**
* @brief Callback for bt_cap_commander_change_volume().
*
* @param conn Pointer to the connection where the error
* occurred. NULL if @p err is 0 or if cancelled by
* bt_cap_commander_cancel()
* @param err 0 on success, BT_GATT_ERR() with a
* specific ATT (BT_ATT_ERR_*) error code or -ECANCELED if cancelled
* by bt_cap_commander_cancel().
*/
void (*volume_changed)(struct bt_conn *conn, int err);
#endif /* CONFIG_BT_VCP_VOL_CTLR */
};
/**
@ -698,6 +712,30 @@ int bt_cap_commander_unregister_cb(const struct bt_cap_commander_cb *cb);
*/
int bt_cap_commander_discover(struct bt_conn *conn);
/** @brief Cancel any current Common Audio Profile commander procedure
*
* This will stop the current procedure from continuing and making it possible to run a new
* Common Audio Profile procedure.
*
* It is recommended to do this if any existing procedure takes longer time than expected, which
* could indicate a missing response from the Common Audio Profile Acceptor.
*
* This does not send any requests to any Common Audio Profile Acceptors involved with the current
* procedure, and thus notifications from the Common Audio Profile Acceptors may arrive after this
* has been called. It is thus recommended to either only use this if a procedure has stalled, or
* wait a short while before starting any new Common Audio Profile procedure after this has been
* called to avoid getting notifications from the cancelled procedure. The wait time depends on
* the connection interval, the number of devices in the previous procedure and the behavior of the
* Common Audio Profile Acceptors.
*
* The respective callbacks of the procedure will be called as part of this with the connection
* pointer set to NULL and the err value set to -ECANCELED.
*
* @retval 0 on success
* @retval -EALREADY if no procedure is active
*/
int bt_cap_commander_cancel(void);
struct bt_cap_commander_broadcast_reception_start_member_param {
/** Coordinated or ad-hoc set member. */
union bt_cap_set_member member;

View file

@ -8,7 +8,7 @@
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/bluetooth/audio/audio.h>
#include <zephyr/bluetooth/audio/cap.h>
#include <zephyr/bluetooth/audio/tbs.h>
#include <zephyr/bluetooth/audio/vcp.h>
#include "cap_internal.h"
#include "ccid_internal.h"
#include "csip_internal.h"
@ -86,10 +86,252 @@ int bt_cap_commander_broadcast_reception_stop(
{
return -ENOSYS;
}
static void cap_commander_unicast_audio_proc_complete(void)
{
struct bt_cap_common_proc *active_proc = bt_cap_common_get_active_proc();
enum bt_cap_common_proc_type proc_type;
struct bt_conn *failed_conn;
int err;
failed_conn = active_proc->failed_conn;
err = active_proc->err;
proc_type = active_proc->proc_type;
bt_cap_common_clear_active_proc();
if (cap_cb == NULL) {
return;
}
switch (proc_type) {
#if defined(CONFIG_BT_VCP_VOL_CTLR)
case BT_CAP_COMMON_PROC_TYPE_VOLUME_CHANGE:
if (cap_cb->volume_changed != NULL) {
cap_cb->volume_changed(failed_conn, err);
}
break;
#endif /* CONFIG_BT_VCP_VOL_CTLR */
case BT_CAP_COMMON_PROC_TYPE_NONE:
default:
__ASSERT(false, "Invalid proc_type: %u", proc_type);
}
}
int bt_cap_commander_cancel(void)
{
if (!bt_cap_common_proc_is_active() && !bt_cap_common_proc_is_aborted()) {
LOG_DBG("No CAP procedure is in progress");
return -EALREADY;
}
bt_cap_common_abort_proc(NULL, -ECANCELED);
cap_commander_unicast_audio_proc_complete();
return 0;
}
#if defined(CONFIG_BT_VCP_VOL_CTLR)
static bool valid_change_volume_param(const struct bt_cap_commander_change_volume_param *param)
{
CHECKIF(param == NULL) {
LOG_DBG("param is NULL");
return false;
}
CHECKIF(param->count == 0) {
LOG_DBG("Invalid param->count: %u", param->count);
return false;
}
CHECKIF(param->members == NULL) {
LOG_DBG("param->members is NULL");
return false;
}
CHECKIF(param->count > CONFIG_BT_MAX_CONN) {
LOG_DBG("param->count (%zu) is larger than CONFIG_BT_MAX_CONN (%d)", param->count,
CONFIG_BT_MAX_CONN);
return false;
}
for (size_t i = 0U; i < param->count; i++) {
const union bt_cap_set_member *member = &param->members[i];
struct bt_cap_common_client *client = NULL;
if (param->type == BT_CAP_SET_TYPE_AD_HOC) {
CHECKIF(member->member == NULL) {
LOG_DBG("param->members[%zu].member is NULL", i);
return false;
}
client = bt_cap_common_get_client_by_acl(member->member);
if (client == NULL || !client->cas_found) {
LOG_DBG("CAS was not found for param->members[%zu]", i);
return false;
}
} else if (param->type == BT_CAP_SET_TYPE_CSIP) {
CHECKIF(member->csip == NULL) {
LOG_DBG("param->members[%zu].csip is NULL", i);
return false;
}
client = bt_cap_common_get_client_by_csis(member->csip);
if (client == NULL) {
LOG_DBG("CSIS was not found for param->members[%zu]", i);
return false;
}
}
if (client == NULL || !client->cas_found) {
LOG_DBG("CAS was not found for param->members[%zu]", i);
return false;
}
if (bt_vcp_vol_ctlr_get_by_conn(client->conn) == NULL) {
LOG_DBG("Volume control not available for param->members[%zu]", i);
return false;
}
for (size_t j = 0U; j < i; j++) {
const union bt_cap_set_member *other = &param->members[j];
if (other == member) {
LOG_DBG("param->members[%zu] (%p) is duplicated by "
"param->members[%zu] (%p)",
j, other, i, member);
return false;
}
}
}
return true;
}
static void cap_commander_vcp_vol_set_cb(struct bt_vcp_vol_ctlr *vol_ctlr, int err)
{
struct bt_cap_common_proc *active_proc = bt_cap_common_get_active_proc();
struct bt_conn *conn;
int vcp_err;
LOG_DBG("vol_ctlr %p", (void *)vol_ctlr);
vcp_err = bt_vcp_vol_ctlr_conn_get(vol_ctlr, &conn);
if (vcp_err != 0) {
LOG_ERR("Failed to get conn by vol_ctrl: %d", vcp_err);
return;
}
LOG_DBG("conn %p", (void *)conn);
if (!bt_cap_common_conn_in_active_proc(conn)) {
/* State change happened outside of a procedure; ignore */
return;
}
if (err != 0) {
LOG_DBG("Failed to set volume: %d", err);
bt_cap_common_abort_proc(conn, err);
} else {
active_proc->proc_done_cnt++;
LOG_DBG("Conn %p volume updated (%zu/%zu streams done)", (void *)conn,
active_proc->proc_done_cnt, active_proc->proc_cnt);
}
if (bt_cap_common_proc_is_aborted()) {
LOG_DBG("Proc is aborted");
if (bt_cap_common_proc_all_handled()) {
LOG_DBG("All handled");
cap_commander_unicast_audio_proc_complete();
}
return;
}
if (!bt_cap_common_proc_is_done()) {
const struct bt_cap_commander_proc_param *proc_param;
proc_param = &active_proc->proc_param.commander[active_proc->proc_done_cnt];
conn = proc_param->conn;
err = bt_vcp_vol_ctlr_set_vol(bt_vcp_vol_ctlr_get_by_conn(conn),
proc_param->change_volume.volume);
if (err != 0) {
LOG_DBG("Failed to set volume for conn %p: %d", (void *)conn, err);
bt_cap_common_abort_proc(conn, err);
cap_commander_unicast_audio_proc_complete();
} else {
active_proc->proc_initiated_cnt++;
}
} else {
cap_commander_unicast_audio_proc_complete();
}
}
int bt_cap_commander_change_volume(const struct bt_cap_commander_change_volume_param *param)
{
return -ENOSYS;
const struct bt_cap_commander_proc_param *proc_param;
static struct bt_vcp_vol_ctlr_cb vol_ctlr_cb = {
.vol_set = cap_commander_vcp_vol_set_cb,
};
struct bt_cap_common_proc *active_proc;
static bool cb_registered;
struct bt_conn *conn;
int err;
if (bt_cap_common_proc_is_active()) {
LOG_DBG("A CAP procedure is already in progress");
return -EBUSY;
}
if (!valid_change_volume_param(param)) {
return -EINVAL;
}
bt_cap_common_start_proc(BT_CAP_COMMON_PROC_TYPE_VOLUME_CHANGE, param->count);
if (!cb_registered) {
/* Ensure that ops are registered before any procedures are started */
err = bt_vcp_vol_ctlr_cb_register(&vol_ctlr_cb);
if (err != 0) {
LOG_DBG("Failed to register VCP callbacks: %d", err);
return -ENOEXEC;
}
cb_registered = true;
}
active_proc = bt_cap_common_get_active_proc();
for (size_t i = 0U; i < param->count; i++) {
struct bt_conn *member_conn =
bt_cap_common_get_member_conn(param->type, &param->members[i]);
if (member_conn == NULL) {
LOG_DBG("Invalid param->members[%zu]", i);
return -EINVAL;
}
/* Store the necessary parameters as we cannot assume that the supplied parameters
* are kept valid
*/
active_proc->proc_param.commander[i].conn = member_conn;
active_proc->proc_param.commander[i].change_volume.volume = param->volume;
}
proc_param = &active_proc->proc_param.commander[0];
conn = proc_param->conn;
err = bt_vcp_vol_ctlr_set_vol(bt_vcp_vol_ctlr_get_by_conn(conn),
proc_param->change_volume.volume);
if (err != 0) {
LOG_DBG("Failed to set volume for conn %p: %d", (void *)conn, err);
return -ENOEXEC;
}
active_proc->proc_initiated_cnt++;
return 0;
}
int bt_cap_commander_change_volume_offset(
@ -103,6 +345,7 @@ int bt_cap_commander_change_volume_mute_state(
{
return -ENOSYS;
}
#endif /* CONFIG_BT_VCP_VOL_CTLR */
int bt_cap_commander_change_microphone_mute_state(
const struct bt_cap_commander_change_microphone_mute_state_param *param)

View file

@ -51,6 +51,24 @@ bool bt_cap_common_subproc_is_type(enum bt_cap_common_subproc_type subproc_type)
}
#endif /* CONFIG_BT_CAP_INITIATOR_UNICAST */
struct bt_conn *bt_cap_common_get_member_conn(enum bt_cap_set_type type,
union bt_cap_set_member *member)
{
if (type == BT_CAP_SET_TYPE_CSIP) {
struct bt_cap_common_client *client;
/* We have verified that `client` won't be NULL in
* `valid_change_volume_param`.
*/
client = bt_cap_common_get_client_by_csis(member->csip);
if (client != NULL) {
return client->conn;
}
}
return member->member;
}
bool bt_cap_common_proc_is_active(void)
{
return atomic_test_bit(active_proc.proc_state_flags, BT_CAP_COMMON_PROC_STATE_ACTIVE);
@ -61,7 +79,7 @@ bool bt_cap_common_proc_is_aborted(void)
return atomic_test_bit(active_proc.proc_state_flags, BT_CAP_COMMON_PROC_STATE_ABORTED);
}
bool bt_cap_common_proc_all_streams_handled(void)
bool bt_cap_common_proc_all_handled(void)
{
return active_proc.proc_done_cnt == active_proc.proc_initiated_cnt;
}
@ -97,21 +115,40 @@ static bool active_proc_is_initiator(void)
}
#endif /* CONFIG_BT_CAP_INITIATOR_UNICAST */
#if defined(CONFIG_BT_CAP_COMMANDER)
static bool active_proc_is_commander(void)
{
switch (active_proc.proc_type) {
case BT_CAP_COMMON_PROC_TYPE_VOLUME_CHANGE:
return true;
default:
return false;
}
}
#endif /* CONFIG_BT_CAP_INITIATOR_UNICAST */
bool bt_cap_common_conn_in_active_proc(const struct bt_conn *conn)
{
if (!bt_cap_common_proc_is_active()) {
return false;
}
for (size_t i = 0U; i < active_proc.proc_initiated_cnt; i++) {
#if defined(CONFIG_BT_CAP_INITIATOR_UNICAST)
if (active_proc_is_initiator()) {
for (size_t i = 0U; i < active_proc.proc_initiated_cnt; i++) {
if (active_proc_is_initiator()) {
if (active_proc.proc_param.initiator[i].stream->bap_stream.conn == conn) {
return true;
}
}
}
#endif /* CONFIG_BT_CAP_INITIATOR_UNICAST */
#if defined(CONFIG_BT_CAP_COMMANDER)
if (active_proc_is_commander()) {
if (active_proc.proc_param.commander[i].conn == conn) {
return true;
}
}
#endif /* CONFIG_BT_CAP_INITIATOR_UNICAST */
}
return false;
}

View file

@ -624,7 +624,7 @@ void bt_cap_initiator_codec_configured(struct bt_cap_stream *cap_stream)
}
if (bt_cap_common_proc_is_aborted()) {
if (bt_cap_common_proc_all_streams_handled()) {
if (bt_cap_common_proc_all_handled()) {
cap_initiator_unicast_audio_proc_complete();
}
@ -757,7 +757,7 @@ void bt_cap_initiator_qos_configured(struct bt_cap_stream *cap_stream)
}
if (bt_cap_common_proc_is_aborted()) {
if (bt_cap_common_proc_all_streams_handled()) {
if (bt_cap_common_proc_all_handled()) {
cap_initiator_unicast_audio_proc_complete();
}
@ -813,7 +813,7 @@ void bt_cap_initiator_enabled(struct bt_cap_stream *cap_stream)
}
if (bt_cap_common_proc_is_aborted()) {
if (bt_cap_common_proc_all_streams_handled()) {
if (bt_cap_common_proc_all_handled()) {
cap_initiator_unicast_audio_proc_complete();
}
@ -1058,7 +1058,7 @@ void bt_cap_initiator_metadata_updated(struct bt_cap_stream *cap_stream)
}
if (bt_cap_common_proc_is_aborted()) {
if (bt_cap_common_proc_all_streams_handled()) {
if (bt_cap_common_proc_all_handled()) {
cap_initiator_unicast_audio_proc_complete();
}
@ -1202,7 +1202,7 @@ void bt_cap_initiator_released(struct bt_cap_stream *cap_stream)
}
if (bt_cap_common_proc_is_aborted()) {
if (bt_cap_common_proc_all_streams_handled()) {
if (bt_cap_common_proc_all_handled()) {
cap_initiator_unicast_audio_proc_complete();
}

View file

@ -36,6 +36,7 @@ enum bt_cap_common_proc_type {
BT_CAP_COMMON_PROC_TYPE_START,
BT_CAP_COMMON_PROC_TYPE_UPDATE,
BT_CAP_COMMON_PROC_TYPE_STOP,
BT_CAP_COMMON_PROC_TYPE_VOLUME_CHANGE,
};
enum bt_cap_common_subproc_type {
@ -65,13 +66,28 @@ struct bt_cap_initiator_proc_param {
};
};
struct bt_cap_commander_proc_param {
struct bt_conn *conn;
union {
#if defined(CONFIG_BT_VCP_VOL_CTLR)
struct {
uint8_t volume;
} change_volume;
#endif /* CONFIG_BT_VCP_VOL_CTLR */
/* TODO Add other procedures */
};
};
struct bt_cap_common_proc_param {
union {
#if defined(CONFIG_BT_CAP_INITIATOR_UNICAST)
struct bt_cap_initiator_proc_param
initiator[CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT];
#endif /* CONFIG_BT_CAP_INITIATOR_UNICAST */
/* TODO: Add commander_proc_param struct */
#endif /* CONFIG_BT_CAP_INITIATOR_UNICAST */
#if defined(CONFIG_BT_CAP_COMMANDER)
struct bt_cap_commander_proc_param commander[CONFIG_BT_MAX_CONN];
#endif /* CONFIG_BT_CAP_COMMANDER */
};
};
@ -106,9 +122,11 @@ void bt_cap_common_clear_active_proc(void);
void bt_cap_common_start_proc(enum bt_cap_common_proc_type proc_type, size_t proc_cnt);
void bt_cap_common_set_subproc(enum bt_cap_common_subproc_type subproc_type);
bool bt_cap_common_subproc_is_type(enum bt_cap_common_subproc_type subproc_type);
struct bt_conn *bt_cap_common_get_member_conn(enum bt_cap_set_type type,
union bt_cap_set_member *member);
bool bt_cap_common_proc_is_active(void);
bool bt_cap_common_proc_is_aborted(void);
bool bt_cap_common_proc_all_streams_handled(void);
bool bt_cap_common_proc_all_handled(void);
bool bt_cap_common_proc_is_done(void);
void bt_cap_common_abort_proc(struct bt_conn *conn, int err);
bool bt_cap_common_conn_in_active_proc(const struct bt_conn *conn);

View file

@ -12,6 +12,7 @@ add_library(uut STATIC
${ZEPHYR_BASE}/subsys/logging/log_minimal.c
${ZEPHYR_BASE}/subsys/net/buf_simple.c
csip.c
vcp.c
)
add_subdirectory(${ZEPHYR_BASE}/tests/bluetooth/audio/mocks mocks)

View file

@ -0,0 +1,45 @@
/* csip.c - CAP Commander specific VCP mocks */
/*
* Copyright (c) 2023 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/bluetooth/audio/vcp.h>
static struct bt_vcp_vol_ctlr_cb *vcp_cb;
static struct bt_vcp_vol_ctlr {
struct bt_conn *conn;
} vol_ctlrs[CONFIG_BT_MAX_CONN];
struct bt_vcp_vol_ctlr *bt_vcp_vol_ctlr_get_by_conn(const struct bt_conn *conn)
{
for (size_t i = 0; i < ARRAY_SIZE(vol_ctlrs); i++) {
if (vol_ctlrs[i].conn == conn) {
return &vol_ctlrs[i];
}
}
return NULL;
}
int bt_vcp_vol_ctlr_conn_get(const struct bt_vcp_vol_ctlr *vol_ctlr, struct bt_conn **conn)
{
*conn = vol_ctlr->conn;
return 0;
}
int bt_vcp_vol_ctlr_set_vol(struct bt_vcp_vol_ctlr *vol_ctlr, uint8_t volume)
{
if (vcp_cb->vol_set != NULL) {
vcp_cb->vol_set(vol_ctlr, 0);
}
}
int bt_vcp_vol_ctlr_cb_register(struct bt_vcp_vol_ctlr_cb *cb)
{
vcp_cb = cb;
}