Bluetooth: Samples: Add unicast audio client and server samples
Adds Basic Audio Profile (BAP) unicast server and client samples. These are the barebones versions of what is needed to scan/advertise for audio and setup a stream using the mandatory LC3 preset defined by the BAP spec. Signed-off-by: Emil Gydesen <emil.gydesen@nordicsemi.no>
This commit is contained in:
parent
5b25c33554
commit
f24d4bcf75
12 changed files with 1054 additions and 0 deletions
11
samples/bluetooth/unicast_audio_client/CMakeLists.txt
Normal file
11
samples/bluetooth/unicast_audio_client/CMakeLists.txt
Normal file
|
@ -0,0 +1,11 @@
|
|||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
cmake_minimum_required(VERSION 3.20.0)
|
||||
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
||||
project(unicast_audio_client)
|
||||
|
||||
target_sources(app PRIVATE
|
||||
src/main.c
|
||||
)
|
||||
|
||||
zephyr_library_include_directories(${ZEPHYR_BASE}/samples/bluetooth)
|
24
samples/bluetooth/unicast_audio_client/README.rst
Normal file
24
samples/bluetooth/unicast_audio_client/README.rst
Normal file
|
@ -0,0 +1,24 @@
|
|||
.. _bluetooth_unicast_audio_client:
|
||||
|
||||
Bluetooth: Unicast Audio CLient
|
||||
###############################
|
||||
|
||||
Overview
|
||||
********
|
||||
|
||||
Application demonstrating the LE Audio unicast client functionality. Scans for and
|
||||
connects to a LE Audio unicast server and establishes an audio stream.
|
||||
|
||||
|
||||
Requirements
|
||||
************
|
||||
|
||||
* BlueZ running on the host, or
|
||||
* A board with Bluetooth Low Energy 5.2 support
|
||||
|
||||
Building and Running
|
||||
********************
|
||||
This sample can be found under
|
||||
:zephyr_file:`samples/bluetooth/audio_unicast_client` in the Zephyr tree.
|
||||
|
||||
See :ref:`bluetooth samples section <bluetooth-samples>` for details.
|
8
samples/bluetooth/unicast_audio_client/prj.conf
Normal file
8
samples/bluetooth/unicast_audio_client/prj.conf
Normal file
|
@ -0,0 +1,8 @@
|
|||
CONFIG_BT=y
|
||||
CONFIG_BT_DEBUG_LOG=y
|
||||
CONFIG_BT_CENTRAL=y
|
||||
CONFIG_BT_AUDIO=y
|
||||
CONFIG_BT_AUDIO_UNICAST_CLIENT=y
|
||||
|
||||
CONFIG_BT_EXT_ADV=y
|
||||
CONFIG_BT_CTLR_ADV_EXT=y
|
8
samples/bluetooth/unicast_audio_client/sample.yaml
Normal file
8
samples/bluetooth/unicast_audio_client/sample.yaml
Normal file
|
@ -0,0 +1,8 @@
|
|||
sample:
|
||||
description: Bluetooth Low Energy Audio Unicast Client sample
|
||||
name: Bluetooth Low Energy Audio Unicast Client sample
|
||||
tests:
|
||||
sample.bluetooth.audio_unicast_client:
|
||||
harness: bluetooth
|
||||
platform_allow: qemu_cortex_m3 qemu_x86
|
||||
tags: bluetooth
|
639
samples/bluetooth/unicast_audio_client/src/main.c
Normal file
639
samples/bluetooth/unicast_audio_client/src/main.c
Normal file
|
@ -0,0 +1,639 @@
|
|||
/*
|
||||
* Copyright (c) 2021-2022 Nordic Semiconductor ASA
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <zephyr/types.h>
|
||||
#include <stddef.h>
|
||||
#include <errno.h>
|
||||
#include <zephyr.h>
|
||||
#include <sys/printk.h>
|
||||
|
||||
#include <bluetooth/bluetooth.h>
|
||||
#include <bluetooth/conn.h>
|
||||
#include <bluetooth/audio/audio.h>
|
||||
#include <sys/byteorder.h>
|
||||
|
||||
static void start_scan(void);
|
||||
|
||||
static struct bt_conn *default_conn;
|
||||
static struct k_work_delayable audio_send_work;
|
||||
static struct bt_audio_stream audio_stream;
|
||||
static struct bt_audio_unicast_group *unicast_group;
|
||||
static struct bt_codec *remote_codecs[CONFIG_BT_AUDIO_UNICAST_CLIENT_PAC_COUNT];
|
||||
static struct bt_audio_ep *sinks[CONFIG_BT_AUDIO_UNICAST_CLIENT_ASE_SNK_COUNT];
|
||||
NET_BUF_POOL_FIXED_DEFINE(tx_pool, 1,
|
||||
CONFIG_BT_ISO_TX_MTU + BT_ISO_CHAN_SEND_RESERVE,
|
||||
8, NULL);
|
||||
|
||||
/* Mandatory support preset by both client and server */
|
||||
static struct bt_audio_lc3_preset preset_16_2_1 = BT_AUDIO_LC3_UNICAST_PRESET_16_2_1;
|
||||
|
||||
static K_SEM_DEFINE(sem_connected, 0, 1);
|
||||
static K_SEM_DEFINE(sem_mtu_exchanged, 0, 1);
|
||||
static K_SEM_DEFINE(sem_sink_discovered, 0, 1);
|
||||
static K_SEM_DEFINE(sem_stream_configured, 0, 1);
|
||||
static K_SEM_DEFINE(sem_stream_qos, 0, 1);
|
||||
static K_SEM_DEFINE(sem_stream_enabled, 0, 1);
|
||||
static K_SEM_DEFINE(sem_stream_started, 0, 1);
|
||||
|
||||
void print_hex(const uint8_t *ptr, size_t len)
|
||||
{
|
||||
while (len-- != 0) {
|
||||
printk("%02x", *ptr++);
|
||||
}
|
||||
}
|
||||
|
||||
static void print_codec(const struct bt_codec *codec)
|
||||
{
|
||||
printk("codec 0x%02x cid 0x%04x vid 0x%04x count %u\n",
|
||||
codec->id, codec->cid, codec->vid, codec->data_count);
|
||||
|
||||
for (size_t i = 0; i < codec->data_count; i++) {
|
||||
printk("data #%zu: type 0x%02x len %u\n",
|
||||
i, codec->data[i].data.type,
|
||||
codec->data[i].data.data_len);
|
||||
print_hex(codec->data[i].data.data,
|
||||
codec->data[i].data.data_len -
|
||||
sizeof(codec->data[i].data.type));
|
||||
printk("\n");
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < codec->meta_count; i++) {
|
||||
printk("meta #%zu: type 0x%02x len %u\n",
|
||||
i, codec->meta[i].data.type,
|
||||
codec->meta[i].data.data_len);
|
||||
print_hex(codec->meta[i].data.data,
|
||||
codec->meta[i].data.data_len -
|
||||
sizeof(codec->meta[i].data.type));
|
||||
printk("\n");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Send audio data on timeout
|
||||
*
|
||||
* This will send an increasing amount of audio data, starting from 1 octet.
|
||||
* The data is just mock data, and does not actually represent any audio.
|
||||
*
|
||||
* First iteration : 0x00
|
||||
* Second iteration: 0x00 0x01
|
||||
* Third iteration : 0x00 0x01 0x02
|
||||
*
|
||||
* And so on, until it wraps around the configured MTU (CONFIG_BT_ISO_TX_MTU)
|
||||
*
|
||||
* @param work Pointer to the work structure
|
||||
*/
|
||||
static void audio_timer_timeout(struct k_work *work)
|
||||
{
|
||||
int ret;
|
||||
static uint8_t buf_data[CONFIG_BT_ISO_TX_MTU];
|
||||
static bool data_initialized;
|
||||
struct net_buf *buf;
|
||||
static size_t len_to_send = 1;
|
||||
|
||||
if (!data_initialized) {
|
||||
/* TODO: Actually encode some audio data */
|
||||
for (int i = 0; i < ARRAY_SIZE(buf_data); i++) {
|
||||
buf_data[i] = (uint8_t)i;
|
||||
}
|
||||
|
||||
data_initialized = true;
|
||||
}
|
||||
|
||||
buf = net_buf_alloc(&tx_pool, K_FOREVER);
|
||||
net_buf_reserve(buf, BT_ISO_CHAN_SEND_RESERVE);
|
||||
|
||||
net_buf_add_mem(buf, buf_data, len_to_send);
|
||||
|
||||
ret = bt_audio_stream_send(&audio_stream, buf);
|
||||
if (ret < 0) {
|
||||
printk("Failed to send audio data (%d)\n", ret);
|
||||
net_buf_unref(buf);
|
||||
} else {
|
||||
printk("Sending mock data with len %zu\n", len_to_send);
|
||||
}
|
||||
|
||||
k_work_schedule(&audio_send_work, K_MSEC(1000));
|
||||
|
||||
len_to_send++;
|
||||
if (len_to_send > ARRAY_SIZE(buf_data)) {
|
||||
len_to_send = 1;
|
||||
}
|
||||
}
|
||||
|
||||
static bool check_audio_support_and_connect(struct bt_data *data,
|
||||
void *user_data)
|
||||
{
|
||||
bt_addr_le_t *addr = user_data;
|
||||
int i;
|
||||
|
||||
printk("[AD]: %u data_len %u\n", data->type, data->data_len);
|
||||
|
||||
switch (data->type) {
|
||||
case BT_DATA_UUID16_SOME:
|
||||
case BT_DATA_UUID16_ALL:
|
||||
if (data->data_len % sizeof(uint16_t) != 0U) {
|
||||
printk("AD malformed\n");
|
||||
return true; /* Continue */
|
||||
}
|
||||
|
||||
for (i = 0; i < data->data_len; i += sizeof(uint16_t)) {
|
||||
struct bt_uuid *uuid;
|
||||
uint16_t uuid_val;
|
||||
int err;
|
||||
|
||||
memcpy(&uuid_val, &data->data[i], sizeof(uuid_val));
|
||||
uuid = BT_UUID_DECLARE_16(sys_le16_to_cpu(uuid_val));
|
||||
if (bt_uuid_cmp(uuid, BT_UUID_ASCS) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
err = bt_le_scan_stop();
|
||||
if (err != 0) {
|
||||
printk("Failed to stop scan: %d\n", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
printk("Audio server found; connecting\n");
|
||||
|
||||
err = bt_conn_le_create(addr, BT_CONN_LE_CREATE_CONN,
|
||||
BT_LE_CONN_PARAM_DEFAULT,
|
||||
&default_conn);
|
||||
if (err != 0) {
|
||||
printk("Create conn to failed (%u)\n", err);
|
||||
start_scan();
|
||||
}
|
||||
|
||||
return false; /* Stop parsing */
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void device_found(const bt_addr_le_t *addr, int8_t rssi, uint8_t type,
|
||||
struct net_buf_simple *ad)
|
||||
{
|
||||
char addr_str[BT_ADDR_LE_STR_LEN];
|
||||
|
||||
if (default_conn != NULL) {
|
||||
/* Already connected */
|
||||
return;
|
||||
}
|
||||
|
||||
/* We're only interested in connectable events */
|
||||
if (type != BT_GAP_ADV_TYPE_ADV_IND && type != BT_GAP_ADV_TYPE_ADV_DIRECT_IND) {
|
||||
return;
|
||||
}
|
||||
|
||||
(void)bt_addr_le_to_str(addr, addr_str, sizeof(addr_str));
|
||||
printk("Device found: %s (RSSI %d)\n", addr_str, rssi);
|
||||
|
||||
/* connect only to devices in close proximity */
|
||||
if (rssi < -70) {
|
||||
return;
|
||||
}
|
||||
|
||||
bt_data_parse(ad, check_audio_support_and_connect, (void *)addr);
|
||||
}
|
||||
|
||||
static void start_scan(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
/* This demo doesn't require active scan */
|
||||
err = bt_le_scan_start(BT_LE_SCAN_PASSIVE, device_found);
|
||||
if (err != 0) {
|
||||
printk("Scanning failed to start (err %d)\n", err);
|
||||
return;
|
||||
}
|
||||
|
||||
printk("Scanning successfully started\n");
|
||||
}
|
||||
|
||||
static void stream_configured(struct bt_audio_stream *stream,
|
||||
const struct bt_codec_qos_pref *pref)
|
||||
{
|
||||
printk("Audio Stream %p configured\n", stream);
|
||||
|
||||
k_sem_give(&sem_stream_configured);
|
||||
}
|
||||
|
||||
static void stream_qos_set(struct bt_audio_stream *stream)
|
||||
{
|
||||
printk("Audio Stream %p QoS set\n", stream);
|
||||
|
||||
k_sem_give(&sem_stream_qos);
|
||||
}
|
||||
|
||||
static void stream_enabled(struct bt_audio_stream *stream)
|
||||
{
|
||||
printk("Audio Stream %p enabled\n", stream);
|
||||
|
||||
k_sem_give(&sem_stream_enabled);
|
||||
}
|
||||
|
||||
static void stream_started(struct bt_audio_stream *stream)
|
||||
{
|
||||
printk("Audio Stream %p started\n", stream);
|
||||
|
||||
/* Start send timer */
|
||||
k_work_schedule(&audio_send_work, K_MSEC(0));
|
||||
}
|
||||
|
||||
static void stream_metadata_updated(struct bt_audio_stream *stream)
|
||||
{
|
||||
printk("Audio Stream %p metadata updated\n", stream);
|
||||
}
|
||||
|
||||
static void stream_disabled(struct bt_audio_stream *stream)
|
||||
{
|
||||
printk("Audio Stream %p disabled\n", stream);
|
||||
}
|
||||
|
||||
static void stream_stopped(struct bt_audio_stream *stream)
|
||||
{
|
||||
printk("Audio Stream %p stopped\n", stream);
|
||||
}
|
||||
|
||||
static void stream_released(struct bt_audio_stream *stream)
|
||||
{
|
||||
printk("Audio Stream %p released\n", stream);
|
||||
}
|
||||
|
||||
static void stream_connected(struct bt_audio_stream *stream)
|
||||
{
|
||||
printk("Audio Stream %p connected, start sending\n", stream);
|
||||
}
|
||||
|
||||
static void stream_disconnected(struct bt_audio_stream *stream, uint8_t reason)
|
||||
{
|
||||
printk("Audio Stream %p disconnected (reason 0x%02x)\n",
|
||||
stream, reason);
|
||||
k_work_cancel_delayable(&audio_send_work);
|
||||
}
|
||||
|
||||
static struct bt_audio_stream_ops stream_ops = {
|
||||
.configured = stream_configured,
|
||||
.qos_set = stream_qos_set,
|
||||
.enabled = stream_enabled,
|
||||
.started = stream_started,
|
||||
.metadata_updated = stream_metadata_updated,
|
||||
.disabled = stream_disabled,
|
||||
.stopped = stream_stopped,
|
||||
.released = stream_released,
|
||||
.connected = stream_connected,
|
||||
.disconnected = stream_disconnected,
|
||||
};
|
||||
|
||||
static void add_remote_sink(struct bt_audio_ep *ep, uint8_t index)
|
||||
{
|
||||
printk("Sink #%u: ep %p\n", index, ep);
|
||||
|
||||
sinks[index] = ep;
|
||||
}
|
||||
|
||||
static void add_remote_codec(struct bt_codec *codec, int index,
|
||||
uint8_t type)
|
||||
{
|
||||
printk("#%u: codec %p type 0x%02x\n", index, codec, type);
|
||||
|
||||
print_codec(codec);
|
||||
|
||||
if (type != BT_AUDIO_SINK && type != BT_AUDIO_SOURCE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (index < CONFIG_BT_AUDIO_UNICAST_CLIENT_PAC_COUNT) {
|
||||
remote_codecs[index] = codec;
|
||||
}
|
||||
}
|
||||
|
||||
static void discover_sink_cb(struct bt_conn *conn,
|
||||
struct bt_codec *codec,
|
||||
struct bt_audio_ep *ep,
|
||||
struct bt_audio_discover_params *params)
|
||||
{
|
||||
if (params->err != 0) {
|
||||
printk("Discovery failed: %d\n", params->err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (codec != NULL) {
|
||||
add_remote_codec(codec, params->num_caps, params->type);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ep != NULL) {
|
||||
if (params->type == BT_AUDIO_SINK) {
|
||||
add_remote_sink(ep, params->num_eps);
|
||||
} else {
|
||||
printk("Invalid param type: %u\n", params->type);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
printk("Discover complete: err %d\n", params->err);
|
||||
|
||||
(void)memset(params, 0, sizeof(*params));
|
||||
|
||||
k_sem_give(&sem_sink_discovered);
|
||||
}
|
||||
|
||||
static void gatt_mtu_cb(struct bt_conn *conn, uint8_t err,
|
||||
struct bt_gatt_exchange_params *params)
|
||||
{
|
||||
if (err != 0) {
|
||||
printk("Failed to exchange MTU (%u)\n", err);
|
||||
return;
|
||||
}
|
||||
|
||||
k_sem_give(&sem_mtu_exchanged);
|
||||
}
|
||||
|
||||
static void connected(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));
|
||||
|
||||
if (err != 0) {
|
||||
printk("Failed to connect to %s (%u)\n", addr, err);
|
||||
|
||||
bt_conn_unref(default_conn);
|
||||
default_conn = NULL;
|
||||
|
||||
start_scan();
|
||||
return;
|
||||
}
|
||||
|
||||
if (conn != default_conn) {
|
||||
return;
|
||||
}
|
||||
|
||||
printk("Connected: %s\n", addr);
|
||||
k_sem_give(&sem_connected);
|
||||
}
|
||||
|
||||
static void disconnected(struct bt_conn *conn, uint8_t reason)
|
||||
{
|
||||
char addr[BT_ADDR_LE_STR_LEN];
|
||||
|
||||
if (conn != default_conn) {
|
||||
return;
|
||||
}
|
||||
|
||||
(void)bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
|
||||
|
||||
printk("Disconnected: %s (reason 0x%02x)\n", addr, reason);
|
||||
|
||||
bt_conn_unref(default_conn);
|
||||
default_conn = NULL;
|
||||
|
||||
start_scan();
|
||||
}
|
||||
|
||||
BT_CONN_CB_DEFINE(conn_callbacks) = {
|
||||
.connected = connected,
|
||||
.disconnected = disconnected,
|
||||
};
|
||||
|
||||
static int init(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = bt_enable(NULL);
|
||||
if (err != 0) {
|
||||
printk("Bluetooth enable failed (err %d)\n", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
audio_stream.ops = &stream_ops;
|
||||
|
||||
k_work_init_delayable(&audio_send_work, audio_timer_timeout);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int scan_and_connect(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
start_scan();
|
||||
|
||||
err = k_sem_take(&sem_connected, K_FOREVER);
|
||||
if (err != 0) {
|
||||
printk("failed to take sem_connected (err %d)\n", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int exchange_mtu(void)
|
||||
{
|
||||
struct bt_gatt_exchange_params mtu_params = {
|
||||
.func = gatt_mtu_cb
|
||||
};
|
||||
int err;
|
||||
|
||||
err = bt_gatt_exchange_mtu(default_conn, &mtu_params);
|
||||
if (err != 0) {
|
||||
printk("Failed to exchange MTU %d\n", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = k_sem_take(&sem_mtu_exchanged, K_FOREVER);
|
||||
if (err != 0) {
|
||||
printk("failed to take sem_mtu_exchanged (err %d)\n", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int discover_sink(void)
|
||||
{
|
||||
static struct bt_audio_discover_params params;
|
||||
int err;
|
||||
|
||||
params.func = discover_sink_cb;
|
||||
params.type = BT_AUDIO_SINK;
|
||||
|
||||
err = bt_audio_discover(default_conn, ¶ms);
|
||||
if (err != 0) {
|
||||
printk("Failed to discover sink: %d\n", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = k_sem_take(&sem_sink_discovered, K_FOREVER);
|
||||
if (err != 0) {
|
||||
printk("failed to take sem_sink_discovered (err %d)\n", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int configure_stream(struct bt_audio_stream *stream)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = bt_audio_stream_config(default_conn, stream, sinks[0],
|
||||
&preset_16_2_1.codec);
|
||||
if (err != 0) {
|
||||
printk("Could not configure stream\n");
|
||||
return err;
|
||||
}
|
||||
|
||||
err = k_sem_take(&sem_stream_configured, K_FOREVER);
|
||||
if (err != 0) {
|
||||
printk("failed to take sem_stream_configured (err %d)\n", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int create_group(struct bt_audio_stream *stream)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = bt_audio_unicast_group_create(stream, 1, &unicast_group);
|
||||
if (err != 0) {
|
||||
printk("Could not create unicast group (err %d)\n", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int set_stream_qos(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = bt_audio_stream_qos(default_conn, unicast_group,
|
||||
&preset_16_2_1.qos);
|
||||
if (err != 0) {
|
||||
printk("Unable to setup QoS: %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = k_sem_take(&sem_stream_qos, K_FOREVER);
|
||||
if (err != 0) {
|
||||
printk("failed to take sem_stream_qos (err %d)\n", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int enable_stream(struct bt_audio_stream *stream)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = bt_audio_stream_enable(stream, preset_16_2_1.codec.meta_count,
|
||||
preset_16_2_1.codec.meta);
|
||||
if (err != 0) {
|
||||
printk("Unable to enable stream: %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = k_sem_take(&sem_stream_enabled, K_FOREVER);
|
||||
if (err != 0) {
|
||||
printk("failed to take sem_stream_enabled (err %d)\n", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int start_stream(struct bt_audio_stream *stream)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = bt_audio_stream_start(stream);
|
||||
if (err != 0) {
|
||||
printk("Unable to start stream: %d\n", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = k_sem_take(&sem_stream_started, K_FOREVER);
|
||||
if (err != 0) {
|
||||
printk("failed to take sem_stream_started (err %d)\n", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void main(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
printk("Initializing\n");
|
||||
err = init();
|
||||
if (err != 0) {
|
||||
return;
|
||||
}
|
||||
printk("Initialized\n");
|
||||
|
||||
printk("Waiting for connection\n");
|
||||
err = scan_and_connect();
|
||||
if (err != 0) {
|
||||
return;
|
||||
}
|
||||
printk("Connected\n");
|
||||
|
||||
printk("Initiating MTU exchange\n");
|
||||
err = exchange_mtu();
|
||||
if (err != 0) {
|
||||
return;
|
||||
}
|
||||
printk("MTU exchanged\n");
|
||||
|
||||
printk("Discovering sink\n");
|
||||
err = discover_sink();
|
||||
if (err != 0) {
|
||||
return;
|
||||
}
|
||||
printk("Sink discovered\n");
|
||||
|
||||
printk("Configuring stream\n");
|
||||
err = configure_stream(&audio_stream);
|
||||
if (err != 0) {
|
||||
return;
|
||||
}
|
||||
printk("Stream configured\n");
|
||||
|
||||
printk("Creating unicast group\n");
|
||||
err = create_group(&audio_stream);
|
||||
if (err != 0) {
|
||||
return;
|
||||
}
|
||||
printk("Unicast group created\n");
|
||||
|
||||
printk("Setting stream QoS\n");
|
||||
err = set_stream_qos();
|
||||
if (err != 0) {
|
||||
return;
|
||||
}
|
||||
printk("Stream QoS Set\n");
|
||||
|
||||
printk("Enabling stream\n");
|
||||
err = enable_stream(&audio_stream);
|
||||
if (err != 0) {
|
||||
return;
|
||||
}
|
||||
printk("Stream enabled\n");
|
||||
|
||||
printk("Starting stream\n");
|
||||
err = start_stream(&audio_stream);
|
||||
if (err != 0) {
|
||||
return;
|
||||
}
|
||||
printk("Stream started\n");
|
||||
}
|
11
samples/bluetooth/unicast_audio_server/CMakeLists.txt
Normal file
11
samples/bluetooth/unicast_audio_server/CMakeLists.txt
Normal file
|
@ -0,0 +1,11 @@
|
|||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
cmake_minimum_required(VERSION 3.20.0)
|
||||
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
||||
project(unicast_audio_server)
|
||||
|
||||
target_sources(app PRIVATE
|
||||
src/main.c
|
||||
)
|
||||
|
||||
zephyr_library_include_directories(${ZEPHYR_BASE}/samples/bluetooth)
|
23
samples/bluetooth/unicast_audio_server/README.rst
Normal file
23
samples/bluetooth/unicast_audio_server/README.rst
Normal file
|
@ -0,0 +1,23 @@
|
|||
.. _bluetooth_unicast_audio_server:
|
||||
|
||||
Bluetooth: Unicast Audio Server
|
||||
###############################
|
||||
|
||||
Overview
|
||||
********
|
||||
|
||||
Application demonstrating the LE Audio unicast server functionality.
|
||||
Starts advertising and awaits connection from a LE Audio unicast client.
|
||||
|
||||
Requirements
|
||||
************
|
||||
|
||||
* BlueZ running on the host, or
|
||||
* A board with Bluetooth Low Energy 5.2 support
|
||||
|
||||
Building and Running
|
||||
********************
|
||||
This sample can be found under
|
||||
:zephyr_file:`samples/bluetooth/audio_unicast_server` in the Zephyr tree.
|
||||
|
||||
See :ref:`bluetooth samples section <bluetooth-samples>` for details.
|
|
@ -0,0 +1,2 @@
|
|||
CONFIG_BT_CTLR_ADV_EXT=y
|
||||
CONFIG_BT_CTLR_ADV_PERIODIC=y
|
|
@ -0,0 +1,2 @@
|
|||
CONFIG_BT_CTLR_ADV_EXT=y
|
||||
CONFIG_BT_CTLR_ADV_PERIODIC=y
|
8
samples/bluetooth/unicast_audio_server/prj.conf
Normal file
8
samples/bluetooth/unicast_audio_server/prj.conf
Normal file
|
@ -0,0 +1,8 @@
|
|||
CONFIG_BT=y
|
||||
CONFIG_BT_DEBUG_LOG=y
|
||||
CONFIG_BT_PERIPHERAL=y
|
||||
CONFIG_BT_AUDIO=y
|
||||
CONFIG_BT_AUDIO_UNICAST_SERVER=y
|
||||
|
||||
CONFIG_BT_EXT_ADV=y
|
||||
CONFIG_BT_DEVICE_NAME="Unicast Audio Server"
|
8
samples/bluetooth/unicast_audio_server/sample.yaml
Normal file
8
samples/bluetooth/unicast_audio_server/sample.yaml
Normal file
|
@ -0,0 +1,8 @@
|
|||
sample:
|
||||
description: Bluetooth Low Energy Audio Unicast Server sample
|
||||
name: Bluetooth Low Energy Audio Unicast Server sample
|
||||
tests:
|
||||
sample.bluetooth.audio_unicast_server:
|
||||
harness: bluetooth
|
||||
platform_allow: qemu_cortex_m3 qemu_x86
|
||||
tags: bluetooth
|
310
samples/bluetooth/unicast_audio_server/src/main.c
Normal file
310
samples/bluetooth/unicast_audio_server/src/main.c
Normal file
|
@ -0,0 +1,310 @@
|
|||
/*
|
||||
* Copyright (c) 2021-2022 Nordic Semiconductor ASA
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <zephyr/types.h>
|
||||
#include <stddef.h>
|
||||
#include <errno.h>
|
||||
#include <zephyr.h>
|
||||
#include <sys/printk.h>
|
||||
|
||||
#include <bluetooth/bluetooth.h>
|
||||
#include <bluetooth/conn.h>
|
||||
#include <bluetooth/audio/audio.h>
|
||||
#include <bluetooth/audio/capabilities.h>
|
||||
#include <sys/byteorder.h>
|
||||
|
||||
#define MAX_PAC 1
|
||||
|
||||
#define AVAILABLE_SINK_CONTEXT (BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED | \
|
||||
BT_AUDIO_CONTEXT_TYPE_CONVERSATIONAL | \
|
||||
BT_AUDIO_CONTEXT_TYPE_MEDIA | \
|
||||
BT_AUDIO_CONTEXT_TYPE_GAME | \
|
||||
BT_AUDIO_CONTEXT_TYPE_INSTRUCTIONAL)
|
||||
|
||||
#define AVAILABLE_SOURCE_CONTEXT (BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED | \
|
||||
BT_AUDIO_CONTEXT_TYPE_CONVERSATIONAL | \
|
||||
BT_AUDIO_CONTEXT_TYPE_MEDIA | \
|
||||
BT_AUDIO_CONTEXT_TYPE_GAME)
|
||||
|
||||
/* Mandatory support preset by both client and server */
|
||||
static struct bt_audio_lc3_preset preset_16_2_1 = BT_AUDIO_LC3_UNICAST_PRESET_16_2_1;
|
||||
|
||||
NET_BUF_POOL_FIXED_DEFINE(tx_pool, 1, CONFIG_BT_ISO_TX_MTU, 8, NULL);
|
||||
static struct bt_conn *default_conn;
|
||||
static struct bt_audio_stream streams[MAX_PAC];
|
||||
|
||||
|
||||
static uint8_t unicast_server_addata[] = {
|
||||
BT_UUID_16_ENCODE(BT_UUID_ASCS_VAL), /* ASCS UUID */
|
||||
BT_AUDIO_UNICAST_ANNOUNCEMENT_TARGETED, /* Target Announcement */
|
||||
(((AVAILABLE_SINK_CONTEXT) >> 0) & 0xFF),
|
||||
(((AVAILABLE_SINK_CONTEXT) >> 8) & 0xFF),
|
||||
(((AVAILABLE_SOURCE_CONTEXT) >> 0) & 0xFF),
|
||||
(((AVAILABLE_SOURCE_CONTEXT) >> 8) & 0xFF),
|
||||
0x00, /* Metadata length */
|
||||
};
|
||||
|
||||
/* TODO: Expand with BAP data */
|
||||
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_ALL, BT_UUID_16_ENCODE(BT_UUID_ASCS_VAL)),
|
||||
BT_DATA(BT_DATA_SVC_DATA16, unicast_server_addata, ARRAY_SIZE(unicast_server_addata)),
|
||||
};
|
||||
|
||||
void print_hex(const uint8_t *ptr, size_t len)
|
||||
{
|
||||
while (len-- != 0) {
|
||||
printk("%02x", *ptr++);
|
||||
}
|
||||
}
|
||||
|
||||
static void print_codec(const struct bt_codec *codec)
|
||||
{
|
||||
printk("codec 0x%02x cid 0x%04x vid 0x%04x count %u\n",
|
||||
codec->id, codec->cid, codec->vid, codec->data_count);
|
||||
|
||||
for (size_t i = 0; i < codec->data_count; i++) {
|
||||
printk("data #%zu: type 0x%02x len %u\n",
|
||||
i, codec->data[i].data.type,
|
||||
codec->data[i].data.data_len);
|
||||
print_hex(codec->data[i].data.data,
|
||||
codec->data[i].data.data_len -
|
||||
sizeof(codec->data[i].data.type));
|
||||
printk("\n");
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < codec->meta_count; i++) {
|
||||
printk("meta #%zu: type 0x%02x len %u\n",
|
||||
i, codec->meta[i].data.type,
|
||||
codec->meta[i].data.data_len);
|
||||
print_hex(codec->meta[i].data.data,
|
||||
codec->meta[i].data.data_len -
|
||||
sizeof(codec->meta[i].data.type));
|
||||
printk("\n");
|
||||
}
|
||||
}
|
||||
|
||||
static void print_qos(struct bt_codec_qos *qos)
|
||||
{
|
||||
printk("QoS: dir 0x%02x interval %u framing 0x%02x phy 0x%02x sdu %u "
|
||||
"rtn %u latency %u pd %u\n",
|
||||
qos->dir, qos->interval, qos->framing, qos->phy, qos->sdu,
|
||||
qos->rtn, qos->latency, qos->pd);
|
||||
}
|
||||
|
||||
static struct bt_audio_stream *lc3_config(struct bt_conn *conn,
|
||||
struct bt_audio_ep *ep,
|
||||
struct bt_audio_capability *cap,
|
||||
struct bt_codec *codec)
|
||||
{
|
||||
printk("ASE Codec Config: conn %p ep %p cap %p\n", conn, ep, cap);
|
||||
|
||||
print_codec(codec);
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(streams); i++) {
|
||||
struct bt_audio_stream *stream = &streams[i];
|
||||
|
||||
if (!stream->conn) {
|
||||
printk("ASE Codec Config stream %p\n", stream);
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
|
||||
printk("No streams available\n");
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int lc3_reconfig(struct bt_audio_stream *stream,
|
||||
struct bt_audio_capability *cap,
|
||||
struct bt_codec *codec)
|
||||
{
|
||||
printk("ASE Codec Reconfig: stream %p cap %p\n", stream, cap);
|
||||
|
||||
print_codec(codec);
|
||||
|
||||
/* We only support one QoS at the moment, reject changes */
|
||||
return -ENOEXEC;
|
||||
}
|
||||
|
||||
static int lc3_qos(struct bt_audio_stream *stream, struct bt_codec_qos *qos)
|
||||
{
|
||||
printk("QoS: stream %p qos %p\n", stream, qos);
|
||||
|
||||
print_qos(qos);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int lc3_enable(struct bt_audio_stream *stream, uint8_t meta_count,
|
||||
struct bt_codec_data *meta)
|
||||
{
|
||||
printk("Enable: stream %p meta_count %u\n", stream, meta_count);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int lc3_start(struct bt_audio_stream *stream)
|
||||
{
|
||||
printk("Start: stream %p\n", stream);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int lc3_metadata(struct bt_audio_stream *stream, uint8_t meta_count,
|
||||
struct bt_codec_data *meta)
|
||||
{
|
||||
printk("Metadata: stream %p meta_count %u\n", stream, meta_count);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int lc3_disable(struct bt_audio_stream *stream)
|
||||
{
|
||||
printk("Disable: stream %p\n", stream);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int lc3_stop(struct bt_audio_stream *stream)
|
||||
{
|
||||
printk("Stop: stream %p\n", stream);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int lc3_release(struct bt_audio_stream *stream)
|
||||
{
|
||||
printk("Release: stream %p\n", stream);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static struct bt_audio_capability_ops lc3_ops = {
|
||||
.config = lc3_config,
|
||||
.reconfig = lc3_reconfig,
|
||||
.qos = lc3_qos,
|
||||
.enable = lc3_enable,
|
||||
.start = lc3_start,
|
||||
.metadata = lc3_metadata,
|
||||
.disable = lc3_disable,
|
||||
.stop = lc3_stop,
|
||||
.release = lc3_release,
|
||||
};
|
||||
|
||||
static void stream_connected(struct bt_audio_stream *stream)
|
||||
{
|
||||
printk("Audio Stream %p connected\n", stream);
|
||||
}
|
||||
|
||||
static void stream_disconnected(struct bt_audio_stream *stream, uint8_t reason)
|
||||
{
|
||||
printk("Audio Stream %p disconnected (reason 0x%02x)\n", stream, reason);
|
||||
}
|
||||
|
||||
static void stream_recv(struct bt_audio_stream *stream, struct net_buf *buf)
|
||||
{
|
||||
printk("Incoming audio on stream %p len %u\n", stream, buf->len);
|
||||
}
|
||||
|
||||
static struct bt_audio_stream_ops stream_ops = {
|
||||
.connected = stream_connected,
|
||||
.disconnected = stream_disconnected,
|
||||
.recv = stream_recv
|
||||
};
|
||||
|
||||
static void connected(struct bt_conn *conn, uint8_t err)
|
||||
{
|
||||
char addr[BT_ADDR_LE_STR_LEN];
|
||||
|
||||
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
|
||||
|
||||
if (err != 0) {
|
||||
printk("Failed to connect to %s (%u)\n", addr, err);
|
||||
|
||||
default_conn = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
printk("Connected: %s\n", addr);
|
||||
default_conn = conn;
|
||||
}
|
||||
|
||||
static void disconnected(struct bt_conn *conn, uint8_t reason)
|
||||
{
|
||||
char addr[BT_ADDR_LE_STR_LEN];
|
||||
|
||||
if (conn != default_conn) {
|
||||
return;
|
||||
}
|
||||
|
||||
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
|
||||
|
||||
printk("Disconnected: %s (reason 0x%02x)\n", addr, reason);
|
||||
|
||||
bt_conn_unref(default_conn);
|
||||
default_conn = NULL;
|
||||
}
|
||||
|
||||
BT_CONN_CB_DEFINE(conn_callbacks) = {
|
||||
.connected = connected,
|
||||
.disconnected = disconnected,
|
||||
};
|
||||
|
||||
static struct bt_audio_capability caps[] = {
|
||||
{
|
||||
.type = BT_AUDIO_SINK,
|
||||
.pref = BT_AUDIO_CAPABILITY_PREF(
|
||||
BT_AUDIO_CAPABILITY_UNFRAMED_SUPPORTED,
|
||||
BT_GAP_LE_PHY_2M, 0x02, 10, 40000, 40000,
|
||||
40000, 40000),
|
||||
.codec = &preset_16_2_1.codec,
|
||||
.ops = &lc3_ops,
|
||||
}
|
||||
};
|
||||
|
||||
void main(void)
|
||||
{
|
||||
struct bt_le_ext_adv *adv;
|
||||
int err;
|
||||
|
||||
err = bt_enable(NULL);
|
||||
if (err != 0) {
|
||||
printk("Bluetooth init failed (err %d)\n", err);
|
||||
return;
|
||||
}
|
||||
|
||||
printk("Bluetooth initialized\n");
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(caps); i++) {
|
||||
bt_audio_capability_register(&caps[i]);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(streams); i++) {
|
||||
bt_audio_stream_cb_register(&streams[i], &stream_ops);
|
||||
}
|
||||
|
||||
/* Create a non-connectable non-scannable advertising set */
|
||||
err = bt_le_ext_adv_create(BT_LE_EXT_ADV_CONN_NAME, NULL, &adv);
|
||||
if (err) {
|
||||
printk("Failed to create advertising set (err %d)\n", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = bt_le_ext_adv_set_data(adv, ad, ARRAY_SIZE(ad), NULL, 0);
|
||||
if (err) {
|
||||
printk("Failed to set advertising data (err %d)\n", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = bt_le_ext_adv_start(adv, BT_LE_EXT_ADV_START_DEFAULT);
|
||||
if (err) {
|
||||
printk("Failed to start advertising set (err %d)\n", err);
|
||||
return;
|
||||
}
|
||||
|
||||
printk("Advertising successfully started\n");
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue