diff --git a/include/zephyr/bluetooth/audio/cap.h b/include/zephyr/bluetooth/audio/cap.h index 19aa1e4092b..a9370251d3d 100644 --- a/include/zephyr/bluetooth/audio/cap.h +++ b/include/zephyr/bluetooth/audio/cap.h @@ -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; diff --git a/subsys/bluetooth/audio/cap_commander.c b/subsys/bluetooth/audio/cap_commander.c index 611cf1fcacf..27346b822f7 100644 --- a/subsys/bluetooth/audio/cap_commander.c +++ b/subsys/bluetooth/audio/cap_commander.c @@ -8,7 +8,7 @@ #include #include #include -#include +#include #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 = ¶m->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 = ¶m->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, ¶m->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) diff --git a/subsys/bluetooth/audio/cap_common.c b/subsys/bluetooth/audio/cap_common.c index b341dcfc0d8..62ffbc7b874 100644 --- a/subsys/bluetooth/audio/cap_common.c +++ b/subsys/bluetooth/audio/cap_common.c @@ -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; } diff --git a/subsys/bluetooth/audio/cap_initiator.c b/subsys/bluetooth/audio/cap_initiator.c index 2b171548af2..79fb9faf44d 100644 --- a/subsys/bluetooth/audio/cap_initiator.c +++ b/subsys/bluetooth/audio/cap_initiator.c @@ -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(); } diff --git a/subsys/bluetooth/audio/cap_internal.h b/subsys/bluetooth/audio/cap_internal.h index 8823b3da2bb..4c833a1d8e5 100644 --- a/subsys/bluetooth/audio/cap_internal.h +++ b/subsys/bluetooth/audio/cap_internal.h @@ -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); diff --git a/tests/bluetooth/audio/cap_commander/uut/CMakeLists.txt b/tests/bluetooth/audio/cap_commander/uut/CMakeLists.txt index 8dc2539f682..8a0774a3182 100644 --- a/tests/bluetooth/audio/cap_commander/uut/CMakeLists.txt +++ b/tests/bluetooth/audio/cap_commander/uut/CMakeLists.txt @@ -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) diff --git a/tests/bluetooth/audio/cap_commander/uut/vcp.c b/tests/bluetooth/audio/cap_commander/uut/vcp.c new file mode 100644 index 00000000000..a031a8be0de --- /dev/null +++ b/tests/bluetooth/audio/cap_commander/uut/vcp.c @@ -0,0 +1,45 @@ +/* csip.c - CAP Commander specific VCP mocks */ + +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +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; +}