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 <emil.gydesen@nordicsemi.no>
This commit is contained in:
parent
a712315793
commit
daf7d8066a
12
samples/bluetooth/cap_acceptor/CMakeLists.txt
Normal file
12
samples/bluetooth/cap_acceptor/CMakeLists.txt
Normal file
@ -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)
|
||||
15
samples/bluetooth/cap_acceptor/Kconfig.sysbuild
Normal file
15
samples/bluetooth/cap_acceptor/Kconfig.sysbuild
Normal file
@ -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 != ""
|
||||
74
samples/bluetooth/cap_acceptor/README.rst
Normal file
74
samples/bluetooth/cap_acceptor/README.rst
Normal file
@ -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 <bluetooth-samples>` 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 <bluetooth-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 <nrf5340bsim>`.
|
||||
|
||||
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
|
||||
13
samples/bluetooth/cap_acceptor/overlay-bt_ll_sw_split.conf
Normal file
13
samples/bluetooth/cap_acceptor/overlay-bt_ll_sw_split.conf
Normal file
@ -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
|
||||
36
samples/bluetooth/cap_acceptor/prj.conf
Normal file
36
samples/bluetooth/cap_acceptor/prj.conf
Normal file
@ -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
|
||||
30
samples/bluetooth/cap_acceptor/sample.yaml
Normal file
30
samples/bluetooth/cap_acceptor/sample.yaml
Normal file
@ -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
|
||||
56
samples/bluetooth/cap_acceptor/src/cap_acceptor.h
Normal file
56
samples/bluetooth/cap_acceptor/src/cap_acceptor.h
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Nordic Semiconductor ASA
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <zephyr/bluetooth/audio/audio.h>
|
||||
#include <zephyr/bluetooth/audio/cap.h>
|
||||
#include <zephyr/bluetooth/conn.h>
|
||||
#include <zephyr/kernel.h>
|
||||
|
||||
#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);
|
||||
356
samples/bluetooth/cap_acceptor/src/cap_acceptor_unicast.c
Normal file
356
samples/bluetooth/cap_acceptor/src/cap_acceptor_unicast.c
Normal file
@ -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 <errno.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <zephyr/bluetooth/audio/audio.h>
|
||||
#include <zephyr/bluetooth/audio/bap.h>
|
||||
#include <zephyr/bluetooth/audio/cap.h>
|
||||
#include <zephyr/bluetooth/audio/lc3.h>
|
||||
#include <zephyr/bluetooth/bluetooth.h>
|
||||
#include <zephyr/bluetooth/conn.h>
|
||||
#include <zephyr/bluetooth/gap.h>
|
||||
#include <zephyr/bluetooth/hci_types.h>
|
||||
#include <zephyr/bluetooth/iso.h>
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <zephyr/logging/log_core.h>
|
||||
#include <zephyr/net/buf.h>
|
||||
#include <zephyr/sys/util.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
348
samples/bluetooth/cap_acceptor/src/main.c
Normal file
348
samples/bluetooth/cap_acceptor/src/main.c
Normal file
@ -0,0 +1,348 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Nordic Semiconductor ASA
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <autoconf.h>
|
||||
#include <errno.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <zephyr/bluetooth/addr.h>
|
||||
#include <zephyr/bluetooth/audio/audio.h>
|
||||
#include <zephyr/bluetooth/audio/cap.h>
|
||||
#include <zephyr/bluetooth/audio/lc3.h>
|
||||
#include <zephyr/bluetooth/audio/pacs.h>
|
||||
#include <zephyr/bluetooth/bluetooth.h>
|
||||
#include <zephyr/bluetooth/byteorder.h>
|
||||
#include <zephyr/bluetooth/conn.h>
|
||||
#include <zephyr/bluetooth/gap.h>
|
||||
#include <zephyr/bluetooth/hci_types.h>
|
||||
#include <zephyr/bluetooth/uuid.h>
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <zephyr/logging/log_core.h>
|
||||
#include <zephyr/sys/util.h>
|
||||
#include <zephyr/sys/util_macro.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
24
samples/bluetooth/cap_acceptor/sysbuild.cmake
Normal file
24
samples/bluetooth/cap_acceptor/sysbuild.cmake
Normal file
@ -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})
|
||||
Loading…
Reference in New Issue
Block a user