| /* |
| * Copyright (c) 2025 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 <zephyr/autoconf.h> |
| #include <zephyr/bluetooth/addr.h> |
| #include <zephyr/bluetooth/audio/audio.h> |
| #include <zephyr/bluetooth/audio/bap.h> |
| #include <zephyr/bluetooth/audio/cap.h> |
| #include <zephyr/bluetooth/audio/csip.h> |
| #include <zephyr/bluetooth/bluetooth.h> |
| #include <zephyr/bluetooth/gap.h> |
| #include <zephyr/bluetooth/hci_types.h> |
| #include <zephyr/bluetooth/iso.h> |
| #include <zephyr/logging/log.h> |
| #include <zephyr/sys/__assert.h> |
| #include <zephyr/sys/util.h> |
| #include <zephyr/toolchain.h> |
| |
| #include "bap_endpoint.h" |
| #include "cap_internal.h" |
| #include "csip_internal.h" |
| |
| LOG_MODULE_REGISTER(bt_cap_handover, CONFIG_BT_CAP_HANDOVER_LOG_LEVEL); |
| |
| static const struct bt_cap_handover_cb *cap_cb; |
| |
| bool bt_cap_handover_is_handover_broadcast_source( |
| const struct bt_cap_broadcast_source *cap_broadcast_source) |
| { |
| |
| const struct bt_cap_common_proc *active_proc = bt_cap_common_get_active_proc(); |
| const struct bt_cap_handover_proc_param *proc_param = &active_proc->proc_param.handover; |
| |
| if (!bt_cap_common_handover_is_active()) { |
| return false; |
| } |
| |
| if (proc_param->is_unicast_to_broadcast) { |
| return cap_broadcast_source == proc_param->unicast_to_broadcast.broadcast_source; |
| } |
| |
| return cap_broadcast_source == proc_param->broadcast_to_unicast.broadcast_source; |
| } |
| |
| struct cap_unicast_group_stream_lookup { |
| struct bt_cap_stream *active_sink_streams[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT]; |
| struct bt_cap_stream *streams[CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT]; |
| size_t active_sink_streams_cnt; |
| size_t cnt; |
| }; |
| |
| static bool unicast_group_foreach_stream_cb(struct bt_cap_stream *cap_stream, void *user_data) |
| { |
| struct cap_unicast_group_stream_lookup *data = user_data; |
| const struct bt_bap_stream *bap_stream = &cap_stream->bap_stream; |
| |
| __ASSERT_NO_MSG(data->cnt < ARRAY_SIZE(data->streams)); |
| __ASSERT_NO_MSG(data->active_sink_streams_cnt < ARRAY_SIZE(data->active_sink_streams)); |
| |
| if (bap_stream->ep != NULL) { |
| struct bt_bap_ep_info ep_info; |
| int err; |
| |
| err = bt_bap_ep_get_info(bap_stream->ep, &ep_info); |
| __ASSERT_NO_MSG(err == 0); |
| |
| /* Only consider sink streams for handover to broadcast */ |
| if (ep_info.state == BT_BAP_EP_STATE_STREAMING && |
| ep_info.dir == BT_AUDIO_DIR_SINK) { |
| data->active_sink_streams[data->active_sink_streams_cnt++] = cap_stream; |
| } |
| } |
| |
| data->streams[data->cnt++] = cap_stream; |
| |
| return false; |
| } |
| |
| void bt_cap_handover_complete(void) |
| { |
| struct bt_cap_common_proc *active_proc = bt_cap_common_get_active_proc(); |
| struct bt_cap_handover_proc_param *proc_param = &active_proc->proc_param.handover; |
| const bool is_unicast_to_broadcast = proc_param->is_unicast_to_broadcast; |
| struct bt_conn *failed_conn = active_proc->failed_conn; |
| struct bt_cap_broadcast_source *broadcast_source; |
| struct bt_cap_unicast_group *unicast_group; |
| const int err = active_proc->err; |
| |
| if (is_unicast_to_broadcast) { |
| broadcast_source = proc_param->unicast_to_broadcast.broadcast_source; |
| unicast_group = proc_param->unicast_to_broadcast.unicast_group; |
| } else { |
| broadcast_source = proc_param->broadcast_to_unicast.broadcast_source; |
| unicast_group = proc_param->broadcast_to_unicast.unicast_group; |
| } |
| |
| bt_cap_common_clear_active_proc(); |
| |
| if (cap_cb != NULL) { |
| if (is_unicast_to_broadcast) { |
| if (cap_cb->unicast_to_broadcast_complete != NULL) { |
| cap_cb->unicast_to_broadcast_complete( |
| err, failed_conn, unicast_group, broadcast_source); |
| } |
| } else { |
| if (cap_cb->broadcast_to_unicast_complete != NULL) { |
| cap_cb->broadcast_to_unicast_complete( |
| err, failed_conn, broadcast_source, unicast_group); |
| } |
| } |
| } |
| } |
| |
| void bt_cap_handover_unicast_proc_complete(void) |
| { |
| struct bt_cap_common_proc *active_proc = bt_cap_common_get_active_proc(); |
| const enum bt_cap_common_proc_type proc_type = active_proc->proc_type; |
| |
| if (proc_type == BT_CAP_COMMON_PROC_TYPE_STOP) { |
| if (active_proc->err != 0) { |
| bt_cap_handover_complete(); |
| } else { |
| /* continue */ |
| bt_cap_handover_unicast_to_broadcast_setup_broadcast(); |
| } |
| } else if (proc_type == BT_CAP_COMMON_PROC_TYPE_START) { |
| bt_cap_handover_complete(); |
| } else { |
| __ASSERT(false, "invalid proc_type %d", proc_type); |
| } |
| } |
| |
| void bt_cap_handover_broadcast_source_stopped(uint8_t reason) |
| { |
| struct bt_cap_common_proc *active_proc = bt_cap_common_get_active_proc(); |
| struct bt_cap_handover_proc_param *proc_param = &active_proc->proc_param.handover; |
| |
| if (proc_param->is_unicast_to_broadcast) { |
| LOG_DBG("Unexpected broadcast source stop with reason 0x%02x", reason); |
| |
| __maybe_unused const int err = bt_cap_initiator_broadcast_audio_delete( |
| proc_param->unicast_to_broadcast.broadcast_source); |
| |
| __ASSERT_NO_MSG(err == 0); |
| proc_param->unicast_to_broadcast.broadcast_source = NULL; |
| |
| active_proc->err = reason; |
| bt_cap_handover_complete(); |
| } else { |
| if (reason == BT_HCI_ERR_LOCALHOST_TERM_CONN) { |
| /* Successfully stopped the broadcast source */ |
| proc_param->broadcast_to_unicast.broadcast_stopped = true; |
| if (proc_param->broadcast_to_unicast.reception_stopped) { |
| bt_cap_handover_broadcast_audio_stopped(); |
| } |
| } else { |
| proc_param->unicast_to_broadcast.broadcast_source = NULL; |
| active_proc->err = reason; |
| bt_cap_handover_complete(); |
| } |
| } |
| } |
| |
| void bt_cap_handover_unicast_to_broadcast_reception_start(void) |
| { |
| struct bt_cap_commander_broadcast_reception_start_param param = {0}; |
| struct bt_cap_initiator_broadcast_create_param *create_param; |
| struct bt_cap_handover_proc_param *proc_param; |
| struct bt_cap_common_proc *active_proc; |
| struct bt_le_ext_adv_info adv_info; |
| int err; |
| |
| active_proc = bt_cap_common_get_active_proc(); |
| proc_param = &active_proc->proc_param.handover; |
| create_param = proc_param->unicast_to_broadcast.broadcast_create_param; |
| param.type = proc_param->unicast_to_broadcast.type; |
| param.param = proc_param->unicast_to_broadcast.reception_start_member_params; |
| |
| err = bt_le_ext_adv_get_info(proc_param->unicast_to_broadcast.ext_adv, &adv_info); |
| __ASSERT_NO_MSG(err != -EINVAL); |
| if (err != 0) { |
| /* May happen if the advertising set was deleted while in this procedure */ |
| LOG_DBG("Failed to get adv info: %d", err); |
| active_proc->err = err; |
| active_proc->failed_conn = NULL; |
| |
| bt_cap_handover_complete(); |
| return; |
| } |
| |
| if (adv_info.ext_adv_state != BT_LE_EXT_ADV_STATE_ENABLED) { |
| /* Start advertising to get the actual adv addr */ |
| err = bt_le_ext_adv_start(proc_param->unicast_to_broadcast.ext_adv, |
| BT_LE_EXT_ADV_START_DEFAULT); |
| if (err != 0) { |
| LOG_DBG("Failed to start advertising set (err %d)\n", err); |
| |
| active_proc->err = err; |
| active_proc->failed_conn = NULL; |
| |
| bt_cap_handover_complete(); |
| |
| return; |
| } |
| |
| /* Since bt_le_ext_adv_get_info returns the pointer to actual advertising address we |
| * do not need to call it again to get the address |
| */ |
| } |
| /* else if we are already in the paused or active state and do not need to anything */ |
| |
| ARRAY_FOR_EACH(proc_param->unicast_to_broadcast.reception_start_member_params, i) { |
| struct bt_cap_commander_broadcast_reception_start_member_param *member_param = |
| &proc_param->unicast_to_broadcast.reception_start_member_params[i]; |
| const struct bt_conn *member_conn = bt_cap_common_get_member_conn( |
| proc_param->unicast_to_broadcast.type, &member_param->member); |
| |
| /* The member_conns are populated from index and upwards, thus once we |
| * reach a NULL pointer in the array, there are no more acceptors to send the |
| * reception start request to. |
| */ |
| if (member_conn == NULL) { |
| break; |
| } |
| |
| member_param->addr = bt_addr_le_any; |
| member_param->adv_sid = adv_info.sid; |
| member_param->pa_interval = proc_param->unicast_to_broadcast.pa_interval; |
| member_param->broadcast_id = proc_param->unicast_to_broadcast.broadcast_id; |
| bt_addr_le_copy(&member_param->addr, adv_info.addr); |
| |
| member_param->num_subgroups = create_param->subgroup_count; |
| for (size_t j = 0U; j < member_param->num_subgroups; j++) { |
| const struct bt_cap_initiator_broadcast_subgroup_param *subgroup_param = |
| &create_param->subgroup_params[j]; |
| struct bt_bap_bass_subgroup *subgroup = &member_param->subgroups[j]; |
| |
| subgroup->metadata_len = subgroup_param->codec_cfg->meta_len; |
| (void)memcpy(subgroup->metadata, subgroup_param->codec_cfg->meta, |
| subgroup->metadata_len); |
| |
| /* The bis_sync value has been set up earlier in |
| * bt_cap_handover_unicast_to_broadcast while we still had the ACL |
| * references |
| */ |
| __ASSERT(subgroup->bis_sync != 0U, "BIS sync was not properly setup"); |
| } |
| |
| param.count++; |
| } |
| |
| err = cap_commander_broadcast_reception_start(¶m); |
| if (err != 0) { |
| LOG_DBG("Failed to start reception start: %d", err); |
| active_proc->err = err; |
| active_proc->failed_conn = NULL; |
| |
| bt_cap_handover_complete(); |
| } |
| } |
| |
| void bt_cap_handover_unicast_to_broadcast_setup_broadcast(void) |
| { |
| struct bt_cap_initiator_broadcast_create_param *broadcast_create_param; |
| struct bt_cap_broadcast_source **broadcast_source; |
| struct bt_cap_handover_proc_param *proc_param; |
| struct bt_cap_common_proc *active_proc; |
| struct bt_le_ext_adv *ext_adv; |
| int err; |
| |
| active_proc = bt_cap_common_get_active_proc(); |
| proc_param = &active_proc->proc_param.handover; |
| broadcast_create_param = proc_param->unicast_to_broadcast.broadcast_create_param; |
| |
| broadcast_source = &proc_param->unicast_to_broadcast.broadcast_source; |
| |
| err = bt_cap_unicast_group_delete(proc_param->unicast_to_broadcast.unicast_group); |
| __ASSERT_NO_MSG(err == 0); |
| proc_param->unicast_to_broadcast.unicast_group = NULL; |
| |
| err = bt_cap_initiator_broadcast_audio_create(broadcast_create_param, broadcast_source); |
| if (err != 0) { |
| LOG_DBG("Failed to create broadcast source: %d", err); |
| active_proc->err = err; |
| active_proc->failed_conn = NULL; |
| |
| bt_cap_handover_complete(); |
| |
| return; |
| } |
| |
| ext_adv = active_proc->proc_param.handover.unicast_to_broadcast.ext_adv; |
| err = bt_cap_initiator_broadcast_audio_start(*broadcast_source, ext_adv); |
| if (err != 0) { |
| LOG_DBG("Failed to start broadcast source: %d", err); |
| active_proc->err = err; |
| active_proc->failed_conn = NULL; |
| |
| err = bt_cap_initiator_broadcast_audio_delete(*broadcast_source); |
| __ASSERT_NO_MSG(err == 0); |
| |
| bt_cap_handover_complete(); |
| |
| return; |
| } |
| } |
| |
| static bool valid_unicast_to_broadcast_stream_metadata_param( |
| const struct bt_cap_initiator_broadcast_subgroup_param *subgroup_param, |
| const struct bt_cap_stream *stream, |
| const struct cap_unicast_group_stream_lookup *lookup_data) |
| { |
| const uint8_t *broadcast_ccid_list; |
| const uint8_t *unicast_ccid_list; |
| int broadcast_ret; |
| int unicast_ret; |
| |
| /* Compare existing unicast metadata with the subgroup param meteadata. It |
| * is mandatory that the CCID list and the context type remain the same |
| */ |
| if (stream->bap_stream.codec_cfg == subgroup_param->codec_cfg) { |
| return true; |
| } |
| |
| /* Verify CCID lists */ |
| unicast_ret = bt_audio_codec_cfg_meta_get_ccid_list(stream->bap_stream.codec_cfg, |
| &unicast_ccid_list); |
| |
| /* CCID list is not mandatory, so it is OK if it is missing, as long |
| * as it is missing for both unicast and broadcast |
| */ |
| |
| if (unicast_ret < 0 && unicast_ret != -ENODATA) { |
| return false; |
| } |
| |
| broadcast_ret = bt_audio_codec_cfg_meta_get_ccid_list(subgroup_param->codec_cfg, |
| &broadcast_ccid_list); |
| |
| if (unicast_ret != broadcast_ret) { |
| return false; |
| } |
| |
| /* we only need to compare if the list exists and is non-empty */ |
| if (unicast_ret > 0 && !util_memeq(unicast_ccid_list, broadcast_ccid_list, unicast_ret)) { |
| return false; |
| } |
| |
| /* Verify streaming contexts (mandatory to be in the metadata )*/ |
| unicast_ret = bt_audio_codec_cfg_meta_get_stream_context(stream->bap_stream.codec_cfg); |
| if (unicast_ret <= 0) { /* mandatory to have a streaming context */ |
| return false; |
| } |
| |
| broadcast_ret = bt_audio_codec_cfg_meta_get_stream_context(subgroup_param->codec_cfg); |
| if (unicast_ret != broadcast_ret) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| static bool |
| valid_unicast_to_broadcast_metadata(const struct bt_cap_handover_unicast_to_broadcast_param *param, |
| const struct cap_unicast_group_stream_lookup *lookup_data) |
| { |
| size_t unique_metadata_cnt = 0U; |
| |
| /* Count unique metadata from the unicast streams to verify the correct number of |
| * subgroups exist. Each unique set of metadata needs to be in its own group |
| */ |
| for (size_t i = 0U; i < lookup_data->active_sink_streams_cnt; i++) { |
| const struct bt_bap_stream *bap_stream_i = |
| &lookup_data->active_sink_streams[i]->bap_stream; |
| const struct bt_audio_codec_cfg *codec_cfg_i = bap_stream_i->codec_cfg; |
| bool unique_metadata = true; |
| |
| for (size_t j = 0U; j < i; j++) { |
| const struct bt_bap_stream *bap_stream_j = |
| &lookup_data->active_sink_streams[j]->bap_stream; |
| const struct bt_audio_codec_cfg *codec_cfg_j = bap_stream_j->codec_cfg; |
| |
| if (codec_cfg_i == codec_cfg_j || |
| util_eq(codec_cfg_i->meta, codec_cfg_i->meta_len, codec_cfg_j->meta, |
| codec_cfg_j->meta_len)) { |
| unique_metadata = false; |
| break; |
| } |
| } |
| |
| if (unique_metadata) { |
| unique_metadata_cnt++; |
| } |
| } |
| |
| if (unique_metadata_cnt > CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT) { |
| LOG_DBG("Cannot create broadcast source with %zu subgroups (max %d)", |
| unique_metadata_cnt, CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT); |
| |
| return false; |
| } |
| |
| if (unique_metadata_cnt > param->broadcast_create_param->subgroup_count) { |
| LOG_DBG("Mismatch between unique metadata from unicast (%zu) and number of " |
| "subgroups (%zu)", |
| unique_metadata_cnt, param->broadcast_create_param->subgroup_count); |
| |
| return false; |
| } |
| |
| return true; |
| } |
| |
| static bool valid_unicast_to_broadcast_create_param( |
| const struct bt_cap_handover_unicast_to_broadcast_param *param, |
| const struct cap_unicast_group_stream_lookup *lookup_data) |
| { |
| size_t total_broadcast_streams = 0U; |
| |
| for (size_t i = 0U; i < param->broadcast_create_param->subgroup_count; i++) { |
| const struct bt_cap_initiator_broadcast_subgroup_param *subgroup_param = |
| ¶m->broadcast_create_param->subgroup_params[i]; |
| |
| for (size_t j = 0U; j < subgroup_param->stream_count; j++) { |
| const struct bt_cap_stream *stream = |
| subgroup_param->stream_params[j].stream; |
| bool stream_is_handed_over = false; |
| |
| if (stream == NULL) { |
| LOG_DBG("subgroup_param[%zu].stream_params[%zu].stream is NULL", i, |
| j); |
| return false; |
| } |
| |
| if (stream->bap_stream.group != param->unicast_group->bap_unicast_group) { |
| LOG_DBG("Stream %p is not part of the unicast group %p", stream, |
| param->unicast_group->bap_unicast_group); |
| return false; |
| } |
| |
| for (size_t k = 0U; k < lookup_data->active_sink_streams_cnt; k++) { |
| if (stream == lookup_data->active_sink_streams[k]) { |
| stream_is_handed_over = true; |
| break; |
| } |
| } |
| |
| if (!stream_is_handed_over) { |
| LOG_DBG("Stream %p was not in unicast group %p", stream, |
| param->unicast_group); |
| return false; |
| } |
| |
| if (!valid_unicast_to_broadcast_stream_metadata_param( |
| subgroup_param, stream, lookup_data)) { |
| return false; |
| } |
| |
| total_broadcast_streams++; |
| } |
| } |
| |
| if (total_broadcast_streams != lookup_data->active_sink_streams_cnt) { |
| LOG_DBG("Not all unicast sink streams are being handed over to broadcast (%zu != " |
| "%zu)", |
| total_broadcast_streams, lookup_data->active_sink_streams_cnt); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| static bool |
| valid_unicast_to_broadcast_param(const struct bt_cap_handover_unicast_to_broadcast_param *param, |
| struct cap_unicast_group_stream_lookup *lookup_data) |
| { |
| struct bt_le_ext_adv_info adv_info; |
| int err; |
| |
| if (param == NULL) { |
| LOG_DBG("param is NULL"); |
| return false; |
| } |
| |
| if (param->unicast_group == NULL) { |
| LOG_DBG("param->unicast_group is NULL"); |
| return false; |
| } |
| |
| if (!bt_cap_initiator_broadcast_audio_start_valid_param(param->broadcast_create_param)) { |
| LOG_DBG("param->broadcast_create_param is invalid"); |
| return false; |
| } |
| |
| if (param->ext_adv == NULL) { |
| LOG_DBG("param->ext_adv is NULL"); |
| return false; |
| } |
| |
| err = bt_le_ext_adv_get_info(param->ext_adv, &adv_info); |
| __ASSERT_NO_MSG(err == 0); |
| |
| if (adv_info.per_adv_state == BT_LE_PER_ADV_STATE_NONE) { |
| LOG_DBG("Advertising set %p not configured for periodic advertising", |
| param->ext_adv); |
| return false; |
| } |
| |
| if (param->broadcast_id > BT_AUDIO_BROADCAST_ID_MAX) { |
| LOG_DBG("param->broadcast_id is invalid: 0x%08X", param->broadcast_id); |
| return false; |
| } |
| |
| if (!IN_RANGE(param->pa_interval, BT_GAP_PER_ADV_MIN_INTERVAL, |
| BT_GAP_PER_ADV_MAX_INTERVAL)) { |
| LOG_DBG("param->pa_interval is invalid: %u", param->pa_interval); |
| return false; |
| } |
| |
| err = bt_cap_unicast_group_foreach_stream(param->unicast_group, |
| unicast_group_foreach_stream_cb, lookup_data); |
| __ASSERT_NO_MSG(err == 0); |
| if (lookup_data->active_sink_streams_cnt == 0U) { |
| LOG_DBG("param->unicast_group does not contain any active streams"); |
| |
| return false; |
| } |
| |
| if (!valid_unicast_to_broadcast_create_param(param, lookup_data)) { |
| return false; |
| } |
| |
| if (!valid_unicast_to_broadcast_metadata(param, lookup_data)) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| int bt_cap_handover_unicast_to_broadcast( |
| const struct bt_cap_handover_unicast_to_broadcast_param *param) |
| { |
| struct cap_unicast_group_stream_lookup lookup_data = {0}; |
| struct bt_cap_unicast_audio_stop_param stop_param = {0}; |
| struct bt_cap_handover_proc_param *proc_param; |
| struct bt_cap_common_proc *active_proc; |
| uint8_t bis_index; |
| int err; |
| |
| if (!valid_unicast_to_broadcast_param(param, &lookup_data)) { |
| return -EINVAL; |
| } |
| |
| if (bt_cap_common_test_and_set_proc_active()) { |
| LOG_DBG("A CAP procedure is already in progress"); |
| |
| return -EBUSY; |
| } |
| |
| /* TBD: How do we prevent the unicast group from being modified or deleted while we are |
| * doing this procedure? |
| * TBD: How do we check for BASS? |
| */ |
| |
| if (lookup_data.cnt == 0U) { |
| LOG_DBG("param->unicast_group does not contain any streams"); |
| |
| bt_cap_common_clear_active_proc(); |
| |
| return -EINVAL; |
| } |
| |
| active_proc = bt_cap_common_get_active_proc(); |
| proc_param = &active_proc->proc_param.handover; |
| |
| /* Populate an array of unique connection pointers to determine which acceptors to add the |
| * broadcast source to |
| */ |
| for (size_t i = 0U; i < lookup_data.active_sink_streams_cnt; i++) { |
| const struct bt_cap_stream *stream = lookup_data.active_sink_streams[i]; |
| bool conn_added; |
| |
| /* Add stream's conn to conns if not already there */ |
| conn_added = false; |
| ARRAY_FOR_EACH(proc_param->unicast_to_broadcast.reception_start_member_params, j) { |
| struct bt_cap_commander_broadcast_reception_start_member_param |
| *member_param = &proc_param->unicast_to_broadcast |
| .reception_start_member_params[j]; |
| const struct bt_conn *member_conn = |
| bt_cap_common_get_member_conn(param->type, &member_param->member); |
| |
| if (member_conn == stream->bap_stream.conn) { |
| conn_added = true; |
| break; |
| } |
| |
| if (member_conn == NULL) { |
| if (param->type == BT_CAP_SET_TYPE_CSIP) { |
| struct bt_cap_common_client *client = |
| bt_cap_common_get_client_by_acl( |
| stream->bap_stream.conn); |
| |
| member_param->member.csip = |
| bt_csip_set_coordinator_csis_inst_by_handle( |
| stream->bap_stream.conn, |
| client->csis_start_handle); |
| } else { |
| member_param->member.member = stream->bap_stream.conn; |
| conn_added = true; |
| break; |
| } |
| } |
| } |
| |
| if (!conn_added) { |
| LOG_DBG("Stream %p connection could not be added. " |
| "Some streams contain an invalid conn pointer", |
| stream); |
| |
| bt_cap_common_clear_active_proc(); |
| |
| return -EINVAL; |
| } |
| } |
| |
| /* We need to set up the expected BIS index fields for the reception start now, as once the |
| * unicast group has been stopped, the reference to the ACL connection from the stream will |
| * be lost |
| */ |
| |
| bis_index = 1U; /* BIS indexes start from 1 */ |
| for (size_t i = 0U; i < param->broadcast_create_param->subgroup_count; i++) { |
| const struct bt_cap_initiator_broadcast_subgroup_param *subgroup_param = |
| ¶m->broadcast_create_param->subgroup_params[i]; |
| |
| for (size_t j = 0U; j < subgroup_param->stream_count; j++) { |
| const struct bt_cap_stream *stream = |
| subgroup_param->stream_params[j].stream; |
| |
| ARRAY_FOR_EACH_PTR( |
| proc_param->unicast_to_broadcast.reception_start_member_params, |
| member_param) { |
| const struct bt_conn *member_conn = bt_cap_common_get_member_conn( |
| param->type, &member_param->member); |
| |
| /* Once we reach a NULL connection pointer, we've handled all |
| * acceptors from the unicast group |
| */ |
| if (member_conn == NULL) { |
| break; |
| } |
| |
| if (stream->bap_stream.conn == member_conn) { |
| member_param->subgroups[i].bis_sync |= |
| BT_ISO_BIS_INDEX_BIT(bis_index); |
| } |
| } |
| |
| bis_index++; |
| } |
| } |
| |
| /* Store the broadcast parameters for later */ |
| proc_param->unicast_to_broadcast.broadcast_create_param = param->broadcast_create_param; |
| proc_param->unicast_to_broadcast.unicast_group = param->unicast_group; |
| proc_param->unicast_to_broadcast.ext_adv = param->ext_adv; |
| proc_param->unicast_to_broadcast.type = param->type; |
| proc_param->unicast_to_broadcast.pa_interval = param->pa_interval; |
| proc_param->unicast_to_broadcast.broadcast_id = param->broadcast_id; |
| |
| stop_param.type = param->type; |
| stop_param.release = true; |
| stop_param.streams = lookup_data.streams; |
| stop_param.count = lookup_data.cnt; |
| |
| bt_cap_common_set_handover_active(); |
| proc_param->is_unicast_to_broadcast = true; |
| |
| __ASSERT_NO_MSG(bt_cap_initiator_valid_unicast_audio_stop_param(&stop_param)); |
| |
| err = cap_initiator_unicast_audio_stop(&stop_param); |
| if (err != 0) { |
| LOG_DBG("Failed to stop unicast audio: %d", err); |
| |
| bt_cap_common_clear_active_proc(); |
| |
| return -ENOEXEC; |
| } |
| |
| return 0; |
| } |
| |
| struct cap_broadcast_source_stream_lookup { |
| struct bt_cap_stream *streams[CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT]; |
| size_t cnt; |
| }; |
| |
| static bool broadcast_source_foreach_stream_cb(struct bt_cap_stream *cap_stream, void *user_data) |
| { |
| struct cap_broadcast_source_stream_lookup *data = user_data; |
| const struct bt_bap_stream *bap_stream = &cap_stream->bap_stream; |
| |
| __ASSERT_NO_MSG(data->cnt < ARRAY_SIZE(data->streams)); |
| |
| if (!bt_cap_initiator_stream_is_in_state(bap_stream, BT_BAP_EP_STATE_STREAMING)) { |
| LOG_DBG("Stream %p is in invalid state %s", cap_stream, |
| bt_bap_ep_state_str(bt_cap_initiator_stream_get_state(bap_stream))); |
| return true; |
| } |
| |
| data->streams[data->cnt++] = cap_stream; |
| |
| return false; |
| } |
| |
| static bool valid_valid_broadcast_to_unicast_unicast_start_param( |
| const struct bt_cap_handover_broadcast_to_unicast_param *param, |
| const struct cap_broadcast_source_stream_lookup *lookup_data) |
| { |
| |
| for (size_t i = 0U; i < param->unicast_start_param->count; i++) { |
| const struct bt_cap_unicast_audio_start_stream_param *stream_param = |
| ¶m->unicast_start_param->stream_params[i]; |
| const struct bt_cap_stream *cap_stream = stream_param->stream; |
| const struct bt_bap_stream *bap_stream = &cap_stream->bap_stream; |
| bool stream_is_handed_over = false; |
| |
| if (!bt_cap_initiator_stream_is_in_state(bap_stream, BT_BAP_EP_STATE_STREAMING)) { |
| LOG_DBG("Stream %p is in invalid state %s", cap_stream, |
| bt_bap_ep_state_str(bt_cap_initiator_stream_get_state(bap_stream))); |
| return false; |
| } |
| |
| if (bap_stream->group != param->broadcast_source->bap_broadcast) { |
| LOG_DBG("Stream %p is not in the broadcast source", cap_stream); |
| return false; |
| } |
| |
| for (size_t j = 0U; j < lookup_data->cnt; j++) { |
| if (cap_stream == lookup_data->streams[j]) { |
| stream_is_handed_over = true; |
| break; |
| } |
| } |
| |
| if (!stream_is_handed_over) { |
| LOG_DBG("Stream %p was not in broadcast_source %p", cap_stream, |
| param->broadcast_source); |
| return false; |
| } |
| |
| /* If we are doing reception stop, verify that all unicast streams being started are |
| * also stopping a broadcast reception |
| */ |
| if (param->reception_stop_param != NULL) { |
| bool reception_stopped = false; |
| |
| for (size_t j = 0U; j < param->reception_stop_param->count; j++) { |
| const struct bt_conn *member_conn = bt_cap_common_get_member_conn( |
| param->reception_stop_param->type, &stream_param->member); |
| const struct bt_conn *other_conn = bt_cap_common_get_member_conn( |
| param->reception_stop_param->type, |
| ¶m->reception_stop_param->param[j].member); |
| |
| if (member_conn == other_conn) { |
| reception_stopped = true; |
| break; |
| } |
| } |
| |
| if (!reception_stopped) { |
| return false; |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| static bool |
| valid_broadcast_to_unicast_param(const struct bt_cap_handover_broadcast_to_unicast_param *param) |
| { |
| struct cap_broadcast_source_stream_lookup lookup_data = {0}; |
| int err; |
| |
| if (param == NULL) { |
| LOG_DBG("param is NULL"); |
| return false; |
| } |
| |
| if (param->broadcast_source == NULL) { |
| LOG_DBG("param->broadcast_source is NULL"); |
| return false; |
| } |
| |
| if (!bt_cap_initiator_valid_unicast_group_param(param->unicast_group_param)) { |
| LOG_DBG("param->unicast_group_param is invalid"); |
| return false; |
| } |
| |
| if (!bt_cap_initiator_valid_unicast_audio_start_param(param->unicast_start_param)) { |
| LOG_DBG("param->unicast_start_param is invalid"); |
| return false; |
| } |
| |
| if (param->reception_stop_param == NULL) { |
| if (param->broadcast_id > BT_AUDIO_BROADCAST_ID_MAX) { |
| LOG_DBG("param->broadcast_id is invalid: 0x%08X", param->broadcast_id); |
| return false; |
| } |
| |
| if (param->adv_sid > BT_GAP_SID_MAX) { |
| LOG_DBG("param->adv_sid is larger than %d", BT_GAP_SID_MAX); |
| return false; |
| } |
| |
| if (param->adv_type != BT_ADDR_LE_PUBLIC && param->adv_type != BT_ADDR_LE_RANDOM) { |
| LOG_DBG("Invalid address type %u", param->adv_type); |
| return false; |
| } |
| } else { |
| if (!bt_cap_commander_valid_broadcast_reception_stop_param( |
| param->reception_stop_param)) { |
| LOG_DBG("param->reception_stop_param is invalid"); |
| return false; |
| } |
| |
| if (param->unicast_start_param->type != param->reception_stop_param->type) { |
| LOG_DBG("Mismatching types for param->unicast_start_param (%d) and " |
| "param->reception_stop_param (%d)", |
| param->unicast_start_param->type, |
| param->reception_stop_param->type); |
| return false; |
| } |
| } |
| |
| err = bt_cap_initiator_broadcast_foreach_stream( |
| param->broadcast_source, broadcast_source_foreach_stream_cb, (void *)&lookup_data); |
| if (err == -ECANCELED) { |
| LOG_DBG("param->broadcast_source contain non-active streams"); |
| |
| return false; |
| } |
| __ASSERT_NO_MSG(err == 0); |
| |
| if (lookup_data.cnt == 0U) { |
| LOG_DBG("param->broadcast_source does not contain any streams"); |
| |
| return false; |
| } |
| |
| if (lookup_data.cnt != param->unicast_start_param->count) { |
| LOG_DBG("Invalid unicast_start_param->count %u, expected %u", |
| param->unicast_start_param->count, lookup_data.cnt); |
| |
| return false; |
| } |
| |
| if (!valid_valid_broadcast_to_unicast_unicast_start_param(param, &lookup_data)) { |
| LOG_DBG("param->unicast_start_param is invalid"); |
| |
| return false; |
| } |
| |
| return true; |
| } |
| |
| int bt_cap_handover_broadcast_reception_stopped(void) |
| { |
| struct bt_cap_handover_proc_param *proc_param; |
| struct bt_cap_common_proc *active_proc; |
| int err; |
| |
| active_proc = bt_cap_common_get_active_proc(); |
| proc_param = &active_proc->proc_param.handover; |
| |
| err = bt_cap_initiator_broadcast_audio_stop( |
| proc_param->broadcast_to_unicast.broadcast_source); |
| if (err != 0) { |
| LOG_DBG("Failed to stop broadcast source: %d", err); |
| active_proc->err = err; |
| } /* Else wait for broadcast stopped */ |
| |
| return err; |
| } |
| |
| static bool bt_cap_handover_broadcast_to_unicast_all_stopped(void) |
| { |
| struct bt_cap_common_proc *active_proc = bt_cap_common_get_active_proc(); |
| bool all_stopped = true; |
| |
| ARRAY_FOR_EACH( |
| active_proc->proc_param.handover.broadcast_to_unicast.pending_recv_state_conns, i) { |
| if (active_proc->proc_param.handover.broadcast_to_unicast |
| .pending_recv_state_conns[i] != NULL) { |
| all_stopped = false; |
| break; |
| } |
| } |
| |
| return all_stopped; |
| } |
| |
| void bt_cap_handover_receive_state_updated(const struct bt_conn *conn, |
| const struct bt_bap_scan_delegator_recv_state *state) |
| { |
| struct bt_cap_common_proc *active_proc = bt_cap_common_get_active_proc(); |
| struct bt_cap_handover_proc_param *proc_param = &active_proc->proc_param.handover; |
| |
| /* BAP 6.5.4 states that the Broadcast Assistant shall not initiate the Add |
| * Source operation if the operation would result in duplicate values for |
| * the combined Source_Address_Type, Source_Adv_SID, and Broadcast_ID fields |
| * of any Broadcast Receive State characteristic exposed by the Scan |
| * Delegator. |
| * |
| * We use that knowledge here to consider the triple {broadcast_id, sid, |
| * type} as being unique, which we will use to determine if a receive state |
| * notification with these values matches our broadcast source if we are not |
| * provided with a broadcast reception stop parameter |
| */ |
| |
| if (!proc_param->is_unicast_to_broadcast && |
| !proc_param->broadcast_to_unicast.reception_stopped && |
| proc_param->broadcast_to_unicast.broadcast_id == state->broadcast_id && |
| proc_param->broadcast_to_unicast.adv_sid == state->adv_sid && |
| proc_param->broadcast_to_unicast.adv_type == state->addr.type) { |
| |
| ARRAY_FOR_EACH(proc_param->broadcast_to_unicast.pending_recv_state_conns, i) { |
| if (proc_param->broadcast_to_unicast.pending_recv_state_conns[i] == conn) { |
| proc_param->broadcast_to_unicast.pending_recv_state_conns[i] = NULL; |
| break; |
| } |
| } |
| |
| if (bt_cap_handover_broadcast_to_unicast_all_stopped()) { |
| proc_param->broadcast_to_unicast.reception_stopped = true; |
| if (proc_param->broadcast_to_unicast.broadcast_stopped) { |
| /* Delete source and start unicast */ |
| bt_cap_handover_broadcast_audio_stopped(); |
| } |
| } |
| } |
| } |
| |
| void bt_cap_handover_broadcast_audio_stopped(void) |
| { |
| struct bt_cap_common_proc *active_proc = bt_cap_common_get_active_proc(); |
| struct bt_cap_handover_proc_param *proc_param = &active_proc->proc_param.handover; |
| int err; |
| |
| /* This will be the case when we do broadcast to unicast handover and |
| * it successfully stops the broadcast |
| */ |
| |
| err = bt_cap_initiator_broadcast_audio_delete( |
| proc_param->broadcast_to_unicast.broadcast_source); |
| if (err != 0) { |
| LOG_DBG("Failed to delete broadcast source: %d", err); |
| active_proc->err = err; |
| |
| bt_cap_handover_complete(); |
| |
| return; |
| } |
| proc_param->broadcast_to_unicast.broadcast_source = NULL; |
| |
| err = bt_cap_unicast_group_create(proc_param->broadcast_to_unicast.unicast_group_param, |
| &proc_param->broadcast_to_unicast.unicast_group); |
| if (err != 0) { |
| LOG_DBG("Failed to create unicast group: %d", err); |
| active_proc->err = err; |
| |
| bt_cap_handover_complete(); |
| |
| return; |
| } |
| |
| err = cap_initiator_unicast_audio_start( |
| proc_param->broadcast_to_unicast.unicast_start_param); |
| if (err != 0) { |
| LOG_DBG("Failed to start unicast audio: %d", err); |
| active_proc->err = err; |
| |
| bt_cap_handover_complete(); |
| |
| return; |
| } |
| } |
| |
| int bt_cap_handover_broadcast_to_unicast( |
| const struct bt_cap_handover_broadcast_to_unicast_param *param) |
| { |
| struct bt_cap_handover_proc_param *proc_param; |
| struct bt_cap_common_proc *active_proc; |
| int err; |
| |
| /** |
| * This will perform the following operations |
| * 1) Check parameters |
| * 2) Broadcast Reception Stop (optional) |
| * 3) Broadcast Source Stop |
| * 4) Broadcast Source Delete |
| * 5) Unicast Group create |
| * 6) Unicast Audio Start |
| */ |
| |
| if (!valid_broadcast_to_unicast_param(param)) { |
| return -EINVAL; |
| } |
| |
| if (bt_cap_common_test_and_set_proc_active()) { |
| LOG_DBG("A CAP procedure is already in progress"); |
| |
| return -EBUSY; |
| } |
| |
| active_proc = bt_cap_common_get_active_proc(); |
| proc_param = &active_proc->proc_param.handover; |
| proc_param->broadcast_to_unicast.unicast_group_param = param->unicast_group_param; |
| proc_param->broadcast_to_unicast.unicast_start_param = param->unicast_start_param; |
| proc_param->broadcast_to_unicast.broadcast_source = param->broadcast_source; |
| proc_param->broadcast_to_unicast.broadcast_id = param->broadcast_id; |
| proc_param->broadcast_to_unicast.adv_sid = param->adv_sid; |
| proc_param->broadcast_to_unicast.adv_type = param->adv_type; |
| |
| bt_cap_common_set_handover_active(); |
| proc_param->is_unicast_to_broadcast = false; |
| |
| if (param->reception_stop_param == NULL) { |
| /* Register the broadcast assistant callbacks to receive the BASS receive states */ |
| cap_commander_register_broadcast_assistant_callbacks(); |
| |
| err = bt_cap_handover_broadcast_reception_stopped(); |
| } else { |
| for (size_t i = 0U; i < param->reception_stop_param->count; i++) { |
| proc_param->broadcast_to_unicast.pending_recv_state_conns[i] = |
| bt_cap_common_get_member_conn( |
| param->reception_stop_param->type, |
| ¶m->reception_stop_param->param[i].member); |
| } |
| err = cap_commander_broadcast_reception_stop(param->reception_stop_param); |
| } |
| |
| if (err != 0) { |
| bt_cap_common_clear_active_proc(); |
| } |
| |
| return err; |
| } |
| |
| int bt_cap_handover_register_cb(const struct bt_cap_handover_cb *cb) |
| { |
| if (cb == NULL) { |
| LOG_DBG("cb is NULL"); |
| return -EINVAL; |
| } |
| |
| if (cap_cb != NULL) { |
| LOG_DBG("callbacks already registered"); |
| return -EALREADY; |
| } |
| |
| cap_cb = cb; |
| |
| return 0; |
| } |
| |
| int bt_cap_handover_unregister_cb(const struct bt_cap_handover_cb *cb) |
| { |
| if (cb == NULL) { |
| LOG_DBG("cb is NULL"); |
| return -EINVAL; |
| } |
| |
| if (cap_cb != cb) { |
| LOG_DBG("cb is not registered"); |
| return -EINVAL; |
| } |
| |
| cap_cb = NULL; |
| |
| return 0; |
| } |