From daf7d8066a1bc5fe9e1b431a89ad339d8b698e59 Mon Sep 17 00:00:00 2001 From: Emil Gydesen Date: Wed, 1 May 2024 11:36:23 +0200 Subject: [PATCH] Samples: Bluetooth: Add CAP acceptor unicast sample Adds a CAP acceptor unicast sample that simply uses the CAP acceptor implementation to setup streams with a CAP initiator. To keep it simple no audio or encoding support has been added. Signed-off-by: Emil Gydesen --- samples/bluetooth/cap_acceptor/CMakeLists.txt | 12 + .../bluetooth/cap_acceptor/Kconfig.sysbuild | 15 + samples/bluetooth/cap_acceptor/README.rst | 74 ++++ .../cap_acceptor/overlay-bt_ll_sw_split.conf | 13 + samples/bluetooth/cap_acceptor/prj.conf | 36 ++ samples/bluetooth/cap_acceptor/sample.yaml | 30 ++ .../bluetooth/cap_acceptor/src/cap_acceptor.h | 56 +++ .../cap_acceptor/src/cap_acceptor_unicast.c | 356 ++++++++++++++++++ samples/bluetooth/cap_acceptor/src/main.c | 348 +++++++++++++++++ samples/bluetooth/cap_acceptor/sysbuild.cmake | 24 ++ 10 files changed, 964 insertions(+) create mode 100644 samples/bluetooth/cap_acceptor/CMakeLists.txt create mode 100644 samples/bluetooth/cap_acceptor/Kconfig.sysbuild create mode 100644 samples/bluetooth/cap_acceptor/README.rst create mode 100644 samples/bluetooth/cap_acceptor/overlay-bt_ll_sw_split.conf create mode 100644 samples/bluetooth/cap_acceptor/prj.conf create mode 100644 samples/bluetooth/cap_acceptor/sample.yaml create mode 100644 samples/bluetooth/cap_acceptor/src/cap_acceptor.h create mode 100644 samples/bluetooth/cap_acceptor/src/cap_acceptor_unicast.c create mode 100644 samples/bluetooth/cap_acceptor/src/main.c create mode 100644 samples/bluetooth/cap_acceptor/sysbuild.cmake diff --git a/samples/bluetooth/cap_acceptor/CMakeLists.txt b/samples/bluetooth/cap_acceptor/CMakeLists.txt new file mode 100644 index 00000000000..f8d344cc981 --- /dev/null +++ b/samples/bluetooth/cap_acceptor/CMakeLists.txt @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(cap_acceptor) + +target_sources(app PRIVATE + src/main.c + src/cap_acceptor_unicast.c +) + +zephyr_library_include_directories(${ZEPHYR_BASE}/samples/bluetooth) diff --git a/samples/bluetooth/cap_acceptor/Kconfig.sysbuild b/samples/bluetooth/cap_acceptor/Kconfig.sysbuild new file mode 100644 index 00000000000..f37b265ecbc --- /dev/null +++ b/samples/bluetooth/cap_acceptor/Kconfig.sysbuild @@ -0,0 +1,15 @@ +# Copyright 2023 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +source "share/sysbuild/Kconfig" + +config NET_CORE_BOARD + string + default "nrf5340dk/nrf5340/cpunet" if "$(BOARD)" = "nrf5340dk" + default "nrf5340_audio_dk/nrf5340/cpunet" if "$(BOARD)" = "nrf5340_audio_dk" + default "nrf5340bsim/nrf5340/cpunet" if $(BOARD_TARGET_STRING) = "NRF5340BSIM_NRF5340_CPUAPP" + +config NET_CORE_IMAGE_HCI_IPC + bool "HCI IPC image on network core" + default y + depends on NET_CORE_BOARD != "" diff --git a/samples/bluetooth/cap_acceptor/README.rst b/samples/bluetooth/cap_acceptor/README.rst new file mode 100644 index 00000000000..dce148cfd01 --- /dev/null +++ b/samples/bluetooth/cap_acceptor/README.rst @@ -0,0 +1,74 @@ +.. zephyr:code-sample:: bluetooth_cap_acceptor + :name: Bluetooth: Common Audio Profile Acceptor + :relevant-api: bt_cap bt_bap bluetooth + + CAP Acceptor sample that advertises audio availability to CAP Initiators. + +Overview +******** + +Application demonstrating the CAP Acceptor functionality. +Starts by advertising for a CAP Initiator to connect and set up available streams. + +This sample can be found under :zephyr_file:`samples/bluetooth/cap_acceptor` in the Zephyr tree. + +Check the :ref:`bluetooth samples section ` for general information. + +Requirements +************ + +* BlueZ running on the host, or +* A board with Bluetooth Low Energy 5.2 support + +Building and Running +******************** + +When building targeting an nrf52 series board with the Zephyr Bluetooth Controller, +use ``-DOVERLAY_CONFIG=overlay-bt_ll_sw_split.conf`` to enable the required ISO +feature support. + +Building for an nrf5340dk +------------------------- + +You can build both the application core image and an appropriate controller image for the network +core with: + +.. zephyr-app-commands:: + :zephyr-app: samples/bluetooth/cap_acceptor/ + :board: nrf5340dk/nrf5340/cpuapp + :goals: build + :west-args: --sysbuild + +If you prefer to only build the application core image, you can do so by doing instead: + +.. zephyr-app-commands:: + :zephyr-app: samples/bluetooth/cap_acceptor/ + :board: nrf5340dk/nrf5340/cpuapp + :goals: build + +In that case you can pair this application core image with the +:ref:`hci_ipc sample ` +:zephyr_file:`samples/bluetooth/hci_ipc/nrf5340_cpunet_iso-bt_ll_sw_split.conf` configuration. + +Building for a simulated nrf5340bsim +------------------------------------ + +Similarly to how you would for real HW, you can do: + +.. zephyr-app-commands:: + :zephyr-app: samples/bluetooth/cap_acceptor/ + :board: nrf5340bsim/nrf5340/cpuapp + :goals: build + :west-args: --sysbuild + +Note this will produce a Linux executable in `./build/zephyr/zephyr.exe`. +For more information, check :ref:`this board documentation `. + +Building for a simulated nrf52_bsim +----------------------------------- + +.. zephyr-app-commands:: + :zephyr-app: samples/bluetooth/cap_acceptor/ + :board: nrf52_bsim + :goals: build + :gen-args: -DOVERLAY_CONFIG=overlay-bt_ll_sw_split.conf diff --git a/samples/bluetooth/cap_acceptor/overlay-bt_ll_sw_split.conf b/samples/bluetooth/cap_acceptor/overlay-bt_ll_sw_split.conf new file mode 100644 index 00000000000..e4bcdbad7c5 --- /dev/null +++ b/samples/bluetooth/cap_acceptor/overlay-bt_ll_sw_split.conf @@ -0,0 +1,13 @@ +# Zephyr Bluetooth Controller +CONFIG_BT_LL_SW_SPLIT=y + +# Zephyr Controller tested maximum advertising data that can be set in a single HCI command +CONFIG_BT_CTLR_ADV_DATA_LEN_MAX=191 + +# Enable support for central ISO in Zephyr Bluetooth Controller +CONFIG_BT_CTLR_PERIPHERAL_ISO=y + +# Support the highest SDU size required by any BAP LC3 presets (155) + 8 bytes of HCI ISO Data +# packet overhead (the Packet_Sequence_Number, ISO_SDU_Length, Packet_Status_Flag fields; and +# the optional Time_Stamp field, if supplied) +CONFIG_BT_CTLR_ISO_TX_BUFFER_SIZE=163 diff --git a/samples/bluetooth/cap_acceptor/prj.conf b/samples/bluetooth/cap_acceptor/prj.conf new file mode 100644 index 00000000000..4a67de1e582 --- /dev/null +++ b/samples/bluetooth/cap_acceptor/prj.conf @@ -0,0 +1,36 @@ +CONFIG_BT=y +CONFIG_LOG=y +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_ISO_PERIPHERAL=y +CONFIG_BT_GATT_DYNAMIC_DB=y +CONFIG_BT_GATT_CLIENT=y +CONFIG_BT_EXT_ADV=y +CONFIG_BT_DEVICE_NAME="CAP Acceptor" + +CONFIG_BT_AUDIO=y + +CONFIG_BT_SMP=y +CONFIG_BT_KEYS_OVERWRITE_OLDEST=y + +# CAP +CONFIG_BT_CAP_ACCEPTOR=y + +# BAP support +CONFIG_BT_BAP_UNICAST_SERVER=y + +# Mandatory to support at least 1 for ASCS +CONFIG_BT_ATT_PREPARE_COUNT=1 + +# Support an ISO channel per ASE +CONFIG_BT_ASCS=y +CONFIG_BT_ASCS_ASE_SNK_COUNT=1 +CONFIG_BT_ASCS_ASE_SRC_COUNT=1 + +# Support an ISO channel per ASE +CONFIG_BT_ISO_MAX_CHAN=2 + +# PACS +CONFIG_BT_PAC_SNK=y +CONFIG_BT_PAC_SNK_LOC=y +CONFIG_BT_PAC_SRC=y +CONFIG_BT_PAC_SRC_LOC=y diff --git a/samples/bluetooth/cap_acceptor/sample.yaml b/samples/bluetooth/cap_acceptor/sample.yaml new file mode 100644 index 00000000000..6be99aab183 --- /dev/null +++ b/samples/bluetooth/cap_acceptor/sample.yaml @@ -0,0 +1,30 @@ +sample: + description: Bluetooth Low Energy Common Audio Profile Acceptor sample + name: Bluetooth Low Energy Common Audio Profile Acceptor sample +tests: + sample.bluetooth.cap_acceptor: + harness: bluetooth + platform_allow: + - qemu_cortex_m3 + - qemu_x86 + - nrf5340dk/nrf5340/cpuapp + - nrf5340bsim/nrf5340/cpuapp + integration_platforms: + - qemu_x86 + - nrf5340dk/nrf5340/cpuapp + tags: bluetooth + sysbuild: true + sample.bluetooth.cap_acceptor.bt_ll_sw_split: + harness: bluetooth + platform_allow: + - nrf52_bsim + - nrf52833dk/nrf52833 + - nrf52840dk/nrf52840 + - nrf52840dongle/nrf52840 + integration_platforms: + - nrf52_bsim + - nrf52833dk/nrf52833 + - nrf52840dk/nrf52840 + - nrf52840dongle/nrf52840 + extra_args: OVERLAY_CONFIG=overlay-bt_ll_sw_split.conf + tags: bluetooth diff --git a/samples/bluetooth/cap_acceptor/src/cap_acceptor.h b/samples/bluetooth/cap_acceptor/src/cap_acceptor.h new file mode 100644 index 00000000000..20c8da5f1d1 --- /dev/null +++ b/samples/bluetooth/cap_acceptor/src/cap_acceptor.h @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#define SINK_CONTEXT BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED +#define SOURCE_CONTEXT BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED + +/** Struct to contain information for a specific peer (CAP) device */ +struct peer_config { + /** Stream for the source endpoint */ + struct bt_cap_stream source_stream; + /** Stream for the sink endpoint */ + struct bt_cap_stream sink_stream; + /** Semaphore to help wait for a release operation if the source stream is not idle */ + struct k_sem source_stream_sem; + /** Semaphore to help wait for a release operation if the sink stream is not idle */ + struct k_sem sink_stream_sem; + /** ACL connection object for the peer device */ + struct bt_conn *conn; +}; + +/** + * @brief Initialize the unicast part of the CAP Acceptor + * + * @param peer Pointer to the specific peer to initialize the CAP Acceptor for + * + * @retval 0 if success + * @retval -ENOEXEC if callbacks failed to be registered + */ +int init_cap_acceptor_unicast(struct peer_config *peer); + +/** + * @brief Request to allocate a CAP stream + * + * @param dir Audio direction of the stream to allocate + * + * @retval Pointer to the allocated CAP stream + * @retval NULL if no more CAP streams for the @p dir could be allocated + */ +struct bt_cap_stream *stream_alloc(enum bt_audio_dir dir); + +/** + * Notify about a released stream + + * This is used to handle some state checks in the main.c file. + * + * @param cap_stream Pointer to the stream that was released + */ +void stream_released(const struct bt_cap_stream *cap_stream); diff --git a/samples/bluetooth/cap_acceptor/src/cap_acceptor_unicast.c b/samples/bluetooth/cap_acceptor/src/cap_acceptor_unicast.c new file mode 100644 index 00000000000..87358f79b3e --- /dev/null +++ b/samples/bluetooth/cap_acceptor/src/cap_acceptor_unicast.c @@ -0,0 +1,356 @@ +/** @file + * @brief Bluetooth Common Audio Profile (CAP) Acceptor unicast. + * + * Copyright (c) 2021-2024 Nordic Semiconductor ASA + * Copyright (c) 2023 NXP + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cap_acceptor.h" + +LOG_MODULE_REGISTER(cap_acceptor_unicast, LOG_LEVEL_INF); + +#define PREF_PHY BT_GAP_LE_PHY_2M +#define MIN_PD 20000U +#define MAX_PD 40000U +#define UNFRAMED_SUPPORTED true +#define LATENCY 20U +#define RTN 2U + +static const struct bt_audio_codec_qos_pref qos_pref = BT_AUDIO_CODEC_QOS_PREF( + UNFRAMED_SUPPORTED, PREF_PHY, RTN, LATENCY, MIN_PD, MAX_PD, MIN_PD, MAX_PD); + +static bool log_codec_cfg_cb(struct bt_data *data, void *user_data) +{ + const char *str = (const char *)user_data; + + LOG_DBG("\t%s: type 0x%02x value_len %u", str, data->type, data->data_len); + LOG_HEXDUMP_DBG(data->data, data->data_len, "\t\tdata"); + + return true; +} + +static void log_codec_cfg(const struct bt_audio_codec_cfg *codec_cfg) +{ + LOG_INF("codec_cfg 0x%02x cid 0x%04x vid 0x%04x count %u", codec_cfg->id, codec_cfg->cid, + codec_cfg->vid, codec_cfg->data_len); + + if (codec_cfg->id == BT_HCI_CODING_FORMAT_LC3) { + enum bt_audio_location chan_allocation; + int ret; + + /* LC3 uses the generic LTV format - other codecs might do as well */ + + bt_audio_data_parse(codec_cfg->data, codec_cfg->data_len, log_codec_cfg_cb, "data"); + + ret = bt_audio_codec_cfg_get_freq(codec_cfg); + if (ret > 0) { + LOG_INF("\tFrequency: %d Hz", bt_audio_codec_cfg_freq_to_freq_hz(ret)); + } + + ret = bt_audio_codec_cfg_get_frame_dur(codec_cfg); + if (ret > 0) { + LOG_INF("\tFrame Duration: %d us", + bt_audio_codec_cfg_frame_dur_to_frame_dur_us(ret)); + } + + if (bt_audio_codec_cfg_get_chan_allocation(codec_cfg, &chan_allocation) == 0) { + LOG_INF("\tChannel allocation: 0x%08X", chan_allocation); + } + + ret = bt_audio_codec_cfg_get_octets_per_frame(codec_cfg); + if (ret > 0) { + LOG_INF("\tOctets per frame: %d", ret); + } + + LOG_INF("\tFrames per SDU: %d", + bt_audio_codec_cfg_get_frame_blocks_per_sdu(codec_cfg, true)); + } else { + LOG_HEXDUMP_DBG(codec_cfg->data, codec_cfg->data_len, "data"); + } + + bt_audio_data_parse(codec_cfg->meta, codec_cfg->meta_len, log_codec_cfg_cb, "meta"); +} + +static void log_qos(const struct bt_audio_codec_qos *qos) +{ + LOG_INF("QoS: interval %u framing 0x%02x phy 0x%02x sdu %u rtn %u latency %u pd %u", + qos->interval, qos->framing, qos->phy, qos->sdu, qos->rtn, qos->latency, qos->pd); +} + +static int unicast_server_config_cb(struct bt_conn *conn, const struct bt_bap_ep *ep, + enum bt_audio_dir dir, + const struct bt_audio_codec_cfg *codec_cfg, + struct bt_bap_stream **bap_stream, + struct bt_audio_codec_qos_pref *const pref, + struct bt_bap_ascs_rsp *rsp) +{ + struct bt_cap_stream *cap_stream; + + LOG_INF("ASE Codec Config: conn %p ep %p dir %u", (void *)conn, (void *)ep, dir); + + log_codec_cfg(codec_cfg); + + cap_stream = stream_alloc(dir); + if (cap_stream == NULL) { + LOG_WRN("No streams available"); + *rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_NO_MEM, BT_BAP_ASCS_REASON_NONE); + + return -ENOMEM; + } + + *bap_stream = &cap_stream->bap_stream; + + LOG_INF("ASE Codec Config bap_stream %p", *bap_stream); + + *pref = qos_pref; + + return 0; +} + +static int unicast_server_reconfig_cb(struct bt_bap_stream *bap_stream, enum bt_audio_dir dir, + const struct bt_audio_codec_cfg *codec_cfg, + struct bt_audio_codec_qos_pref *const pref, + struct bt_bap_ascs_rsp *rsp) +{ + LOG_INF("ASE Codec Reconfig: bap_stream %p", bap_stream); + log_codec_cfg(codec_cfg); + *pref = qos_pref; + + return 0; +} + +static int unicast_server_qos_cb(struct bt_bap_stream *bap_stream, + const struct bt_audio_codec_qos *qos, struct bt_bap_ascs_rsp *rsp) +{ + LOG_INF("QoS: bap_stream %p qos %p", bap_stream, qos); + + log_qos(qos); + + return 0; +} + +static int unicast_server_enable_cb(struct bt_bap_stream *bap_stream, const uint8_t meta[], + size_t meta_len, struct bt_bap_ascs_rsp *rsp) +{ + LOG_INF("Enable: bap_stream %p meta_len %zu", bap_stream, meta_len); + + return 0; +} + +static int unicast_server_start_cb(struct bt_bap_stream *bap_stream, struct bt_bap_ascs_rsp *rsp) +{ + LOG_INF("Start: bap_stream %p", bap_stream); + + return 0; +} + +struct data_func_param { + struct bt_bap_ascs_rsp *rsp; + bool stream_context_present; +}; + +static bool data_func_cb(struct bt_data *data, void *user_data) +{ + struct data_func_param *func_param = (struct data_func_param *)user_data; + + if (data->type == BT_AUDIO_METADATA_TYPE_STREAM_CONTEXT) { + func_param->stream_context_present = true; + } + + return true; +} + +static int unicast_server_metadata_cb(struct bt_bap_stream *bap_stream, const uint8_t meta[], + size_t meta_len, struct bt_bap_ascs_rsp *rsp) +{ + struct data_func_param func_param = { + .rsp = rsp, + .stream_context_present = false, + }; + int err; + + LOG_INF("Metadata: bap_stream %p meta_len %zu", bap_stream, meta_len); + + err = bt_audio_data_parse(meta, meta_len, data_func_cb, &func_param); + if (err != 0) { + return err; + } + + if (!func_param.stream_context_present) { + LOG_ERR("Stream audio context not present"); + *rsp = BT_BAP_ASCS_RSP(BT_BAP_ASCS_RSP_CODE_METADATA_REJECTED, + BT_BAP_ASCS_REASON_NONE); + + return -EINVAL; + } + + return 0; +} + +static int unicast_server_disable_cb(struct bt_bap_stream *bap_stream, struct bt_bap_ascs_rsp *rsp) +{ + LOG_INF("Disable: bap_stream %p", bap_stream); + + return 0; +} + +static int unicast_server_stop_cb(struct bt_bap_stream *bap_stream, struct bt_bap_ascs_rsp *rsp) +{ + LOG_INF("Stop: bap_stream %p", bap_stream); + + return 0; +} + +static int unicast_server_release_cb(struct bt_bap_stream *bap_stream, struct bt_bap_ascs_rsp *rsp) +{ + LOG_INF("Release: bap_stream %p", bap_stream); + + return 0; +} + +static const struct bt_bap_unicast_server_cb unicast_server_cb = { + .config = unicast_server_config_cb, + .reconfig = unicast_server_reconfig_cb, + .qos = unicast_server_qos_cb, + .enable = unicast_server_enable_cb, + .start = unicast_server_start_cb, + .metadata = unicast_server_metadata_cb, + .disable = unicast_server_disable_cb, + .stop = unicast_server_stop_cb, + .release = unicast_server_release_cb, +}; + +static void unicast_stream_configured_cb(struct bt_bap_stream *bap_stream, + const struct bt_audio_codec_qos_pref *pref) +{ + LOG_INF("Configured bap_stream %p", bap_stream); + + /* TODO: The preference should be used/taken into account when + * setting the QoS + */ + + LOG_INF("Local preferences: unframed %s, phy %u, rtn %u, latency %u, pd_min %u, pd_max " + "%u, pref_pd_min %u, pref_pd_max %u", + pref->unframed_supported ? "supported" : "not supported", pref->phy, pref->rtn, + pref->latency, pref->pd_min, pref->pd_max, pref->pref_pd_min, pref->pref_pd_max); +} + +static void unicast_stream_qos_set_cb(struct bt_bap_stream *bap_stream) +{ + LOG_INF("QoS set bap_stream %p", bap_stream); +} + +static void unicast_stream_enabled_cb(struct bt_bap_stream *bap_stream) +{ + struct bt_bap_ep_info ep_info; + int err; + + LOG_INF("Enabled bap_stream %p", bap_stream); + + err = bt_bap_ep_get_info(bap_stream->ep, &ep_info); + if (err != 0) { + LOG_ERR("Failed to get ep info: %d", err); + + return; + } + + if (ep_info.dir == BT_AUDIO_DIR_SINK) { + /* Automatically do the receiver start ready operation */ + err = bt_bap_stream_start(bap_stream); + if (err != 0) { + LOG_ERR("Failed to start: %d", err); + + return; + } + } +} + +static void unicast_stream_started_cb(struct bt_bap_stream *bap_stream) +{ + LOG_INF("Started bap_stream %p", bap_stream); +} + +static void unicast_stream_metadata_updated_cb(struct bt_bap_stream *bap_stream) +{ + LOG_INF("Metadata updated bap_stream %p", bap_stream); +} + +static void unicast_stream_disabled_cb(struct bt_bap_stream *bap_stream) +{ + LOG_INF("Disabled bap_stream %p", bap_stream); +} + +static void unicast_stream_stopped_cb(struct bt_bap_stream *bap_stream, uint8_t reason) +{ + LOG_INF("Stopped bap_stream %p with reason 0x%02X", bap_stream, reason); +} + +static void unicast_stream_released_cb(struct bt_bap_stream *bap_stream) +{ + struct bt_cap_stream *cap_stream = + CONTAINER_OF(bap_stream, struct bt_cap_stream, bap_stream); + + LOG_INF("Released bap_stream %p", bap_stream); + + stream_released(cap_stream); +} + +static void unicast_stream_recv_cb(struct bt_bap_stream *bap_stream, + const struct bt_iso_recv_info *info, struct net_buf *buf) +{ + /* TODO: Add test code */ +} + +int init_cap_acceptor_unicast(struct peer_config *peer) +{ + static struct bt_bap_stream_ops unicast_stream_ops = { + .configured = unicast_stream_configured_cb, + .qos_set = unicast_stream_qos_set_cb, + .enabled = unicast_stream_enabled_cb, + .started = unicast_stream_started_cb, + .metadata_updated = unicast_stream_metadata_updated_cb, + .disabled = unicast_stream_disabled_cb, + .stopped = unicast_stream_stopped_cb, + .released = unicast_stream_released_cb, + .recv = unicast_stream_recv_cb, + }; + static bool cbs_registered; + + if (!cbs_registered) { + int err; + + err = bt_bap_unicast_server_register_cb(&unicast_server_cb); + if (err != 0) { + LOG_ERR("Failed to register BAP unicast server callbacks: %d", err); + + return -ENOEXEC; + } + } + + bt_cap_stream_ops_register(&peer->source_stream, &unicast_stream_ops); + bt_cap_stream_ops_register(&peer->sink_stream, &unicast_stream_ops); + + return 0; +} diff --git a/samples/bluetooth/cap_acceptor/src/main.c b/samples/bluetooth/cap_acceptor/src/main.c new file mode 100644 index 00000000000..fe53bcb7eaa --- /dev/null +++ b/samples/bluetooth/cap_acceptor/src/main.c @@ -0,0 +1,348 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cap_acceptor.h" + +LOG_MODULE_REGISTER(cap_acceptor, LOG_LEVEL_INF); + +#define SUPPORTED_DURATION (BT_AUDIO_CODEC_CAP_DURATION_7_5 | BT_AUDIO_CODEC_CAP_DURATION_10) +#define MAX_CHAN_PER_STREAM BT_AUDIO_CODEC_CAP_CHAN_COUNT_SUPPORT(2) +#define SUPPORTED_FREQ BT_AUDIO_CODEC_CAP_FREQ_ANY +#define SEM_TIMEOUT K_SECONDS(5) +#define MAX_SDU 155U +#define MIN_SDU 30U +#define FRAMES_PER_SDU 2 + +static const struct bt_data ad[] = { + BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), + BT_DATA_BYTES(BT_DATA_UUID16_SOME, BT_UUID_16_ENCODE(BT_UUID_ASCS_VAL), + BT_UUID_16_ENCODE(BT_UUID_CAS_VAL)), + BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME, sizeof(CONFIG_BT_DEVICE_NAME) - 1), + BT_DATA_BYTES(BT_DATA_SVC_DATA16, + BT_UUID_16_ENCODE(BT_UUID_CAS_VAL), + BT_AUDIO_UNICAST_ANNOUNCEMENT_TARGETED), + IF_ENABLED(CONFIG_BT_BAP_UNICAST_SERVER, + (BT_DATA_BYTES(BT_DATA_SVC_DATA16, + BT_UUID_16_ENCODE(BT_UUID_ASCS_VAL), + BT_AUDIO_UNICAST_ANNOUNCEMENT_TARGETED, + BT_BYTES_LIST_LE16(SINK_CONTEXT), + BT_BYTES_LIST_LE16(SOURCE_CONTEXT), + 0x00, /* Metadata length */), + )) +}; + +static struct bt_le_ext_adv *adv; +static struct peer_config peer; + +static K_SEM_DEFINE(sem_state_change, 0, 1); + +static void connected_cb(struct bt_conn *conn, uint8_t err) +{ + char addr[BT_ADDR_LE_STR_LEN]; + + (void)bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + LOG_INF("Connected: %s", addr); + + peer.conn = bt_conn_ref(conn); + k_sem_give(&sem_state_change); +} + +static void disconnected_cb(struct bt_conn *conn, uint8_t reason) +{ + char addr[BT_ADDR_LE_STR_LEN]; + + if (conn != peer.conn) { + return; + } + + (void)bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + LOG_INF("Disconnected: %s (reason 0x%02x)", addr, reason); + + bt_conn_unref(peer.conn); + peer.conn = NULL; + k_sem_give(&sem_state_change); +} + +BT_CONN_CB_DEFINE(conn_callbacks) = { + .connected = connected_cb, + .disconnected = disconnected_cb, +}; + +static int advertise(void) +{ + int err; + + err = bt_le_ext_adv_create(BT_LE_EXT_ADV_CONN, NULL, &adv); + if (err) { + LOG_ERR("Failed to create advertising set: %d", err); + + return err; + } + + err = bt_le_ext_adv_set_data(adv, ad, ARRAY_SIZE(ad), NULL, 0); + if (err) { + LOG_ERR("Failed to set advertising data: %d", err); + + return err; + } + + err = bt_le_ext_adv_start(adv, BT_LE_EXT_ADV_START_DEFAULT); + if (err) { + LOG_ERR("Failed to start advertising set: %d", err); + + return err; + } + + LOG_INF("Advertising successfully started"); + + /* Wait for connection*/ + err = k_sem_take(&sem_state_change, K_FOREVER); + if (err != 0) { + LOG_ERR("Failed to take sem_state_change: err %d", err); + + return err; + } + + return 0; +} + +struct bt_cap_stream *stream_alloc(enum bt_audio_dir dir) +{ + if (dir == BT_AUDIO_DIR_SINK && peer.sink_stream.bap_stream.ep == NULL) { + return &peer.sink_stream; + } else if (dir == BT_AUDIO_DIR_SOURCE && peer.source_stream.bap_stream.ep == NULL) { + return &peer.source_stream; + } + + return NULL; +} + +void stream_released(const struct bt_cap_stream *cap_stream) +{ + if (cap_stream == &peer.source_stream) { + k_sem_give(&peer.source_stream_sem); + } else if (cap_stream == &peer.sink_stream) { + k_sem_give(&peer.sink_stream_sem); + } +} + +static int reset_cap_acceptor(void) +{ + int err; + + LOG_INF("Resetting"); + + if (peer.conn != NULL) { + err = bt_conn_disconnect(peer.conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); + if (err != 0) { + return err; + } + + err = k_sem_take(&sem_state_change, K_FOREVER); + if (err != 0) { + LOG_ERR("Timeout on disconnect: %d", err); + return err; + } + } + + if (adv != NULL) { + err = bt_le_ext_adv_stop(adv); + if (err != 0) { + LOG_ERR("Failed to stop advertiser: %d", err); + return err; + } + + err = bt_le_ext_adv_delete(adv); + if (err != 0) { + LOG_ERR("Failed to delete advertiser: %d", err); + return err; + } + + adv = NULL; + } + + if (peer.source_stream.bap_stream.ep != NULL) { + err = k_sem_take(&peer.source_stream_sem, SEM_TIMEOUT); + if (err != 0) { + LOG_ERR("Timeout on source_stream_sem: %d", err); + return err; + } + } + + if (peer.sink_stream.bap_stream.ep != NULL) { + err = k_sem_take(&peer.sink_stream_sem, SEM_TIMEOUT); + if (err != 0) { + LOG_ERR("Timeout on sink_stream_sem: %d", err); + return err; + } + } + + k_sem_reset(&sem_state_change); + + return 0; +} + +/** Register the PAC records for PACS */ +static int register_pac(enum bt_audio_dir dir, enum bt_audio_context context, + struct bt_pacs_cap *cap) +{ + int err; + + err = bt_pacs_cap_register(dir, cap); + if (err != 0) { + LOG_ERR("Failed to register capabilities: %d", err); + + return err; + } + + err = bt_pacs_set_location(dir, BT_AUDIO_LOCATION_MONO_AUDIO); + if (err != 0) { + LOG_ERR("Failed to set location: %d", err); + + return err; + } + + err = bt_pacs_set_supported_contexts(dir, context); + if (err != 0 && err != -EALREADY) { + LOG_ERR("Failed to set supported contexts: %d", err); + + return err; + } + + err = bt_pacs_set_available_contexts(dir, context); + if (err != 0 && err != -EALREADY) { + LOG_ERR("Failed to set available contexts: %d", err); + + return err; + } + + return 0; +} + +static int init_cap_acceptor(void) +{ + static const struct bt_audio_codec_cap lc3_codec_cap = BT_AUDIO_CODEC_CAP_LC3( + SUPPORTED_FREQ, SUPPORTED_DURATION, MAX_CHAN_PER_STREAM, MIN_SDU, MAX_SDU, + FRAMES_PER_SDU, (SINK_CONTEXT | SOURCE_CONTEXT)); + int err; + + err = bt_enable(NULL); + if (err != 0) { + LOG_ERR("Bluetooth enable failed: %d", err); + + return 0; + } + + LOG_INF("Bluetooth initialized"); + + if (IS_ENABLED(CONFIG_BT_PAC_SNK)) { + static struct bt_pacs_cap sink_cap = { + .codec_cap = &lc3_codec_cap, + }; + int err; + + err = register_pac(BT_AUDIO_DIR_SINK, SINK_CONTEXT, &sink_cap); + if (err != 0) { + LOG_ERR("Failed to register sink capabilities: %d", err); + + return -ENOEXEC; + } + } + + if (IS_ENABLED(CONFIG_BT_PAC_SRC)) { + static struct bt_pacs_cap source_cap = { + .codec_cap = &lc3_codec_cap, + }; + int err; + + err = register_pac(BT_AUDIO_DIR_SOURCE, SOURCE_CONTEXT, &source_cap); + if (err != 0) { + LOG_ERR("Failed to register sink capabilities: %d", err); + + return -ENOEXEC; + } + } + + return 0; +} + +int main(void) +{ + int err; + + err = init_cap_acceptor(); + if (err != 0) { + return 0; + } + + if (IS_ENABLED(CONFIG_BT_BAP_UNICAST_SERVER)) { + err = init_cap_acceptor_unicast(&peer); + if (err != 0) { + return 0; + } + } + + LOG_INF("CAP Acceptor initialized"); + + while (true) { + err = reset_cap_acceptor(); + if (err != 0) { + LOG_ERR("Failed to reset"); + + break; + } + + /* Start advertising as a CAP Acceptor, which includes setting the required + * advertising data based on the roles we support. The Common Audio Service data is + * always advertised, as CAP Initiators and CAP Commanders will use this to identify + * our device as a CAP Acceptor. + */ + err = advertise(); + if (err != 0) { + continue; + } + + /* After advertising we expect CAP Initiators to connect to us and setup streams, + * and eventually disconnect again. As a CAP Acceptor we just need to react to their + * requests and not do anything else. + */ + + /* Reset if disconnected */ + err = k_sem_take(&sem_state_change, K_FOREVER); + if (err != 0) { + LOG_ERR("Failed to take sem_state_change: err %d", err); + + break; + } + } + + /* TODO: Add CAP acceptor broadcast support */ + + return 0; +} diff --git a/samples/bluetooth/cap_acceptor/sysbuild.cmake b/samples/bluetooth/cap_acceptor/sysbuild.cmake new file mode 100644 index 00000000000..2523aac8ea7 --- /dev/null +++ b/samples/bluetooth/cap_acceptor/sysbuild.cmake @@ -0,0 +1,24 @@ +# Copyright (c) 2023 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +if(SB_CONFIG_NET_CORE_IMAGE_HCI_IPC) + # For builds in the nrf5340, we build the netcore image with the controller + + set(NET_APP hci_ipc) + set(NET_APP_SRC_DIR ${ZEPHYR_BASE}/samples/bluetooth/${NET_APP}) + + ExternalZephyrProject_Add( + APPLICATION ${NET_APP} + SOURCE_DIR ${NET_APP_SRC_DIR} + BOARD ${SB_CONFIG_NET_CORE_BOARD} + ) + + set(${NET_APP}_CONF_FILE + ${NET_APP_SRC_DIR}/nrf5340_cpunet_iso-bt_ll_sw_split.conf + CACHE INTERNAL "" + ) + + native_simulator_set_child_images(${DEFAULT_IMAGE} ${NET_APP}) +endif() + +native_simulator_set_final_executable(${DEFAULT_IMAGE})