Bluetooth: samples: Add HAP Hearing Aid sample application

This adds sample application that can be used as reference
implementation and for HAP qualification purposes.

Signed-off-by: Mariusz Skamra <mariusz.skamra@codecoup.pl>
This commit is contained in:
Mariusz Skamra 2022-07-14 16:14:20 +02:00 committed by Fabio Baltieri
parent cedc99b8ed
commit acb9da2ba0
16 changed files with 1482 additions and 0 deletions

View file

@ -0,0 +1,26 @@
# SPDX-License-Identifier: Apache-2.0
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(hap_ha)
target_sources(app PRIVATE
src/bap_unicast_sr.c
src/has_server.c
src/main.c
src/vcp_vol_renderer.c
)
target_sources_ifdef(CONFIG_BT_HAS_HEARING_AID_BINAURAL app PRIVATE
src/csip_set_member.c
)
target_sources_ifdef(CONFIG_BT_ASCS_ASE_SRC app PRIVATE
src/micp_mic_dev.c
)
target_sources_ifdef(CONFIG_BT_TBS_CLIENT app PRIVATE
src/ccp_call_ctrl.c
)
zephyr_library_include_directories(${ZEPHYR_BASE}/samples/bluetooth)

View file

@ -0,0 +1,22 @@
#
# Copyright (c) 2022 Codecoup
#
# SPDX-License-Identifier: Apache-2.0
#
mainmenu "Bluetooth: Hearing Aid"
menu "Zephyr"
source "Kconfig.zephyr"
endmenu
menu "Hearing Aid"
config HAP_HA_SET_RANK
int "Device rank in set"
depends on BT_CSIS
range 1 2
help
Rank of this device in set.
endmenu

View file

@ -0,0 +1,24 @@
.. _bluetooth_hap_ha:
Bluetooth: HAP Hearing Aid (HA)
###############################
Overview
********
Application demonstrating the LE Audio Hearing Aid sample functionality.
Starts advertising and awaits connection from a Hearing Aid Unicast Client (HAUC)
or Hearing Aid Remote Controller (HARC).
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/hap_ha` in the Zephyr tree.
See :ref:`bluetooth samples section <bluetooth-samples>` for details.

View file

@ -0,0 +1,6 @@
CONFIG_BT_HAS_HEARING_AID_BANDED=y
# If the HA supports the Volume Balance feature (see Section 3.1) and the HA
# is a Banded Hearing Aid, the HA shall instantiate two instances of VOCS.
CONFIG_BT_VOCS_MAX_INSTANCE_COUNT=2
CONFIG_BT_VCS_VOCS_INSTANCE_COUNT=2

View file

@ -0,0 +1,3 @@
CONFIG_BT_CSIS=y
CONFIG_BT_HAS_HEARING_AID_BINAURAL=y
CONFIG_BT_CAP_ACCEPTOR_SET_MEMBER=y

View file

@ -0,0 +1,7 @@
CONFIG_LOG_MODE_IMMEDIATE=y
CONFIG_BT_TINYCRYPT_ECC=y
# For LE-audio at 10ms intervals we need the tick counter to occur more frequently
# than every 10 ms as each PDU for some reason takes 2 ticks to process.
CONFIG_SYS_CLOCK_TICKS_PER_SEC=1000
CONFIG_NATIVE_POSIX_SLOWDOWN_TO_REAL_TIME=y

View file

@ -0,0 +1,58 @@
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_PRIVACY=y
CONFIG_BT_SMP=y
CONFIG_BT_SMP_SC_PAIR_ONLY=y
CONFIG_BT_SMP_MIN_ENC_KEY_SIZE=16
CONFIG_BT_EXT_ADV=y
CONFIG_BT_DEVICE_NAME="Hearing Aid sample"
# Appearance: Generic Hearing aid (0x0A40)
CONFIG_BT_DEVICE_APPEARANCE=2624
CONFIG_BT_AUDIO=y
CONFIG_BT_AUDIO_UNICAST_SERVER=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
# Sink Contexts Supported: Unspecified, Conversational, Media, Live
CONFIG_BT_PACS_SNK_CONTEXT=0x0047
# Source Contexts Supported: Unspecified, Conversational
CONFIG_BT_PACS_SRC_CONTEXT=0x0003
CONFIG_BT_PAC_SNK_LOC=y
CONFIG_BT_PAC_SRC_LOC=y
CONFIG_BT_VCS=y
CONFIG_BT_MICP_MIC_DEV=y
CONFIG_BT_HAS=y
CONFIG_BT_HAS_HEARING_AID_MONAURAL=y
CONFIG_BT_HAS_PRESET_COUNT=4
CONFIG_BT_HAS_PRESET_NAME_DYNAMIC=y
CONFIG_BT_CAP_ACCEPTOR=y
CONFIG_BT_AICS_MAX_INSTANCE_COUNT=2
# The HA may expose an instance of MICS and an instance of AICS to control
# the capture of ambient sound.
CONFIG_BT_VCS_AICS_INSTANCE_COUNT=1
# An HA may instantiate one or more instances of Audio Input Control Service
# (AICS) to expose control of the gain of its inputs to a Volume Controller.
CONFIG_BT_MICP_MIC_DEV_AICS_INSTANCE_COUNT=1
# If the HA supports the Volume Balance feature (see Section 3.1) and the HA
# is part of a Binaural Hearing Aid Set, the HA shall instantiate one instance
# of Volume Offset Control Service (VOCS).
CONFIG_BT_VOCS_MAX_INSTANCE_COUNT=1
CONFIG_BT_VCS_VOCS_INSTANCE_COUNT=1
CONFIG_BT_BAS=y
CONFIG_BT_IAS=y
CONFIG_BT_TBS_CLIENT=y
CONFIG_BT_TBS_CLIENT_CCID=y
CONFIG_BT_TBS_CLIENT_STATUS_FLAGS=y
CONFIG_BT_DEBUG_LOG=y

View file

@ -0,0 +1,23 @@
sample:
description: Bluetooth HAP Hearing Aid sample
name: Bluetooth HAP Hearing Aid sample
tests:
sample.bluetooth.hap_ha.monaural:
harness: bluetooth
platform_allow: native_posix
tags: bluetooth
build_only: true
sample.bluetooth.hap_ha.banded:
harness: bluetooth
platform_allow: native_posix
tags: bluetooth
extra_args: OVERLAY_CONFIG="banded.conf"
build_only: true
sample.bluetooth.hap_ha.binaural:
harness: bluetooth
platform_allow: native_posix
tags: bluetooth
extra_args: OVERLAY_CONFIG="binaural.conf"
build_only: true
extra_configs:
- CONFIG_HAP_HA_SET_RANK=2

View file

@ -0,0 +1,437 @@
/** @file
* @brief Bluetooth Basic Audio Profile (BAP) Unicast Server role.
*
* Copyright (c) 2021-2022 Nordic Semiconductor ASA
* Copyright (c) 2022 Codecoup
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/zephyr.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/sys/printk.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/audio/audio.h>
#include <zephyr/bluetooth/audio/capabilities.h>
NET_BUF_POOL_FIXED_DEFINE(tx_pool, CONFIG_BT_ASCS_ASE_SRC_COUNT,
BT_ISO_SDU_BUF_SIZE(CONFIG_BT_ISO_TX_MTU),
8, NULL);
static struct bt_codec lc3_codec =
BT_CODEC_LC3(BT_CODEC_LC3_FREQ_16KHZ | BT_CODEC_LC3_FREQ_24KHZ,
BT_CODEC_LC3_DURATION_10,
BT_CODEC_LC3_CHAN_COUNT_SUPPORT(1), 40u, 60u, 1u,
(BT_AUDIO_CONTEXT_TYPE_CONVERSATIONAL | BT_AUDIO_CONTEXT_TYPE_MEDIA));
static struct bt_conn *default_conn;
static struct k_work_delayable audio_send_work;
static struct bt_audio_stream streams[CONFIG_BT_ASCS_ASE_SNK_COUNT + CONFIG_BT_ASCS_ASE_SRC_COUNT];
static struct bt_audio_source {
struct bt_audio_stream *stream;
uint32_t seq_num;
} source_streams[CONFIG_BT_ASCS_ASE_SRC_COUNT];
static size_t configured_source_stream_count;
static uint32_t get_and_incr_seq_num(const struct bt_audio_stream *stream)
{
for (size_t i = 0U; i < configured_source_stream_count; i++) {
if (stream == source_streams[i].stream) {
return source_streams[i].seq_num++;
}
}
printk("Could not find endpoint from stream %p\n", stream);
return 0;
}
static 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");
}
if (codec->id == BT_CODEC_LC3_ID) {
/* LC3 uses the generic LTV format - other codecs might do as well */
uint32_t chan_allocation;
printk(" Frequency: %d Hz\n", bt_codec_cfg_get_freq(codec));
printk(" Frame Duration: %d us\n", bt_codec_cfg_get_frame_duration_us(codec));
if (bt_codec_cfg_get_chan_allocation_val(codec, &chan_allocation) == 0) {
printk(" Channel allocation: 0x%x\n", chan_allocation);
}
printk(" Octets per frame: %d (negative means value not pressent)\n",
bt_codec_cfg_get_octets_per_frame(codec));
printk(" Frames per SDU: %d\n",
bt_codec_cfg_get_frame_blocks_per_sdu(codec, true));
}
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: interval %u framing 0x%02x phy 0x%02x sdu %u "
"rtn %u latency %u pd %u\n",
qos->interval, qos->framing, qos->phy, qos->sdu,
qos->rtn, qos->latency, qos->pd);
}
/**
* @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 (size_t i = 0U; i < ARRAY_SIZE(buf_data); i++) {
buf_data[i] = (uint8_t)i;
}
data_initialized = true;
}
/* We configured the sink streams to be first in `streams`, so that
* we can use `stream[i]` to select sink streams (i.e. streams with
* data going to the server)
*/
for (size_t i = 0; i < configured_source_stream_count; i++) {
struct bt_audio_stream *stream = source_streams[i].stream;
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(stream, buf,
get_and_incr_seq_num(stream),
BT_ISO_TIMESTAMP_NONE);
if (ret < 0) {
printk("Failed to send audio data on streams[%zu] (%p): (%d)\n",
i, stream, ret);
net_buf_unref(buf);
} else {
printk("Sending mock data with len %zu on streams[%zu] (%p)\n",
len_to_send, i, stream);
}
}
k_work_schedule(&audio_send_work, K_MSEC(1000U));
len_to_send++;
if (len_to_send > ARRAY_SIZE(buf_data)) {
len_to_send = 1;
}
}
static struct bt_audio_stream *lc3_config(struct bt_conn *conn, struct bt_audio_ep *ep,
enum bt_audio_dir dir, struct bt_audio_capability *cap,
struct bt_codec *codec)
{
printk("ASE Codec Config: conn %p ep %p dir %u, cap %p\n",
conn, ep, dir, 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);
if (dir == BT_AUDIO_DIR_SOURCE) {
source_streams[configured_source_stream_count++].stream = 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, struct bt_codec_data *meta,
size_t meta_count)
{
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);
if (IS_ENABLED(CONFIG_BT_ASCS_ASE_SRC)) {
for (size_t i = 0U; i < configured_source_stream_count; i++) {
if (source_streams[i].stream == stream) {
source_streams[i].seq_num = 0U;
break;
}
}
if (configured_source_stream_count > 0 &&
!k_work_delayable_is_pending(&audio_send_work)) {
/* Start send timer */
k_work_schedule(&audio_send_work, K_MSEC(0));
}
}
return 0;
}
static bool valid_metadata_type(uint8_t type, uint8_t len)
{
switch (type) {
case BT_AUDIO_METADATA_TYPE_PREF_CONTEXT:
case BT_AUDIO_METADATA_TYPE_STREAM_CONTEXT:
if (len != 2) {
return false;
}
return true;
case BT_AUDIO_METADATA_TYPE_STREAM_LANG:
if (len != 3) {
return false;
}
return true;
case BT_AUDIO_METADATA_TYPE_PARENTAL_RATING:
if (len != 1) {
return false;
}
return true;
case BT_AUDIO_METADATA_TYPE_EXTENDED: /* 1 - 255 octets */
case BT_AUDIO_METADATA_TYPE_VENDOR: /* 1 - 255 octets */
if (len < 1) {
return false;
}
return true;
case BT_AUDIO_METADATA_TYPE_CCID_LIST: /* 2 - 254 octets */
if (len < 2) {
return false;
}
return true;
case BT_AUDIO_METADATA_TYPE_PROGRAM_INFO: /* 0 - 255 octets */
case BT_AUDIO_METADATA_TYPE_PROGRAM_INFO_URI: /* 0 - 255 octets */
return true;
default:
return false;
}
}
static int lc3_metadata(struct bt_audio_stream *stream, struct bt_codec_data *meta,
size_t meta_count)
{
printk("Metadata: stream %p meta_count %u\n", stream, meta_count);
for (size_t i = 0; i < meta_count; i++) {
if (!valid_metadata_type(meta->data.type, meta->data.data_len)) {
printk("Invalid metadata type %u or length %u\n",
meta->data.type, meta->data.data_len);
return -EINVAL;
}
}
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_recv(struct bt_audio_stream *stream, const struct bt_iso_recv_info *info,
struct net_buf *buf)
{
printk("Incoming audio on stream %p len %u\n", stream, buf->len);
}
static struct bt_audio_stream_ops stream_ops = {
.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 = bt_conn_ref(conn);
}
static void disconnected(struct bt_conn *conn, uint8_t reason)
{
char addr[BT_ADDR_LE_STR_LEN];
struct k_work_sync sync;
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;
if (IS_ENABLED(CONFIG_BT_ASCS_ASE_SRC)) {
/* reset data */
(void)memset(source_streams, 0, sizeof(source_streams));
configured_source_stream_count = 0U;
k_work_cancel_delayable_sync(&audio_send_work, &sync);
}
}
BT_CONN_CB_DEFINE(conn_callbacks) = {
.connected = connected,
.disconnected = disconnected,
};
static struct bt_audio_capability caps[] = {
{
.dir = BT_AUDIO_DIR_SINK,
.pref = BT_AUDIO_CAPABILITY_PREF(
BT_AUDIO_CAPABILITY_UNFRAMED_SUPPORTED,
BT_GAP_LE_PHY_2M, 0x02, 10, 20000, 40000,
20000, 40000),
.codec = &lc3_codec,
.ops = &lc3_ops,
},
#if defined(CONFIG_BT_ASCS_ASE_SRC)
{
.dir = BT_AUDIO_DIR_SOURCE,
.pref = BT_AUDIO_CAPABILITY_PREF(
BT_AUDIO_CAPABILITY_UNFRAMED_SUPPORTED,
BT_GAP_LE_PHY_2M, 0x02, 10, 20000, 40000,
20000, 40000),
.codec = &lc3_codec,
.ops = &lc3_ops,
}
#endif /* CONFIG_BT_ASCS_ASE_SRC */
};
int bap_unicast_sr_init(void)
{
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);
}
if (IS_ENABLED(CONFIG_BT_ASCS_ASE_SRC)) {
k_work_init_delayable(&audio_send_work, audio_timer_timeout);
}
return 0;
}

View file

@ -0,0 +1,160 @@
/** @file
* @brief Bluetooth Call Control Profile (CCP) Call Controller role.
*
* Copyright (c) 2020 Nordic Semiconductor ASA
* Copyright (c) 2022 Codecoup
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/zephyr.h>
#include <zephyr/sys/printk.h>
#include <zephyr/sys/util.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/audio/tbs.h>
enum {
CCP_FLAG_PROFILE_CONNECTED,
CCP_FLAG_GTBS_DISCOVER,
CCP_FLAG_CCID_READ,
CCP_FLAG_STATUS_FLAGS_READ,
CCP_FLAG_CALL_STATE_READ,
CCP_FLAG_NUM,
};
static ATOMIC_DEFINE(flags, CCP_FLAG_NUM)[CONFIG_BT_MAX_CONN];
static int process_profile_connection(struct bt_conn *conn)
{
atomic_t *flags_for_conn = flags[bt_conn_index(conn)];
int err = 0;
if (!atomic_test_and_set_bit(flags_for_conn, CCP_FLAG_GTBS_DISCOVER)) {
err = bt_tbs_client_discover(conn, true);
if (err != 0) {
printk("bt_tbs_client_discover (err %d)\n", err);
}
} else if (!atomic_test_and_set_bit(flags_for_conn, CCP_FLAG_CCID_READ)) {
err = bt_tbs_client_read_ccid(conn, BT_TBS_GTBS_INDEX);
if (err != 0) {
printk("bt_tbs_client_read_ccid (err %d)\n", err);
}
} else if (!atomic_test_and_set_bit(flags_for_conn, CCP_FLAG_STATUS_FLAGS_READ)) {
err = bt_tbs_client_read_status_flags(conn, BT_TBS_GTBS_INDEX);
if (err != 0) {
printk("bt_tbs_client_read_status_flags (err %d)\n", err);
}
} else if (!atomic_test_and_set_bit(flags_for_conn, CCP_FLAG_CALL_STATE_READ)) {
err = bt_tbs_client_read_call_state(conn, BT_TBS_GTBS_INDEX);
if (err != 0) {
printk("bt_tbs_client_read_call_state (err %d)\n", err);
}
} else if (!atomic_test_and_set_bit(flags_for_conn, CCP_FLAG_PROFILE_CONNECTED)) {
printk("CCP Profile connected\n");
}
return err;
}
static void connected(struct bt_conn *conn, uint8_t err)
{
if (err) {
printk("Connection failed (err %d)\n", err);
return;
}
atomic_set(flags[bt_conn_index(conn)], 0);
if (process_profile_connection(conn) != 0) {
printk("Profile connection failed");
}
}
BT_CONN_CB_DEFINE(conn_callbacks) = {
.connected = connected,
};
static void discover_cb(struct bt_conn *conn, int err, uint8_t tbs_count, bool gtbs_found)
{
if (!gtbs_found) {
printk("Failed to discover GTBS\n");
return;
}
if (err) {
printk("%s (err %d)\n", __func__, err);
return;
}
if (!atomic_test_bit(flags[bt_conn_index(conn)], CCP_FLAG_PROFILE_CONNECTED)) {
process_profile_connection(conn);
}
}
static void ccid_cb(struct bt_conn *conn, int err, uint8_t inst_index, uint32_t value)
{
if (inst_index != BT_TBS_GTBS_INDEX) {
printk("Unexpected %s for instance %u\n", __func__, inst_index);
return;
}
if (err) {
printk("%s (err %d)\n", __func__, err);
return;
}
if (!atomic_test_bit(flags[bt_conn_index(conn)], CCP_FLAG_PROFILE_CONNECTED)) {
process_profile_connection(conn);
}
}
static void status_flags_cb(struct bt_conn *conn, int err, uint8_t inst_index, uint32_t value)
{
if (inst_index != BT_TBS_GTBS_INDEX) {
printk("Unexpected %s for instance %u\n", __func__, inst_index);
return;
}
if (err) {
printk("%s (err %d)\n", __func__, err);
return;
}
if (!atomic_test_bit(flags[bt_conn_index(conn)], CCP_FLAG_PROFILE_CONNECTED)) {
process_profile_connection(conn);
}
}
static void call_state_cb(struct bt_conn *conn, int err, uint8_t inst_index, uint8_t call_count,
const struct bt_tbs_client_call_state *call_states)
{
if (inst_index != BT_TBS_GTBS_INDEX) {
printk("Unexpected %s for instance %u\n", __func__, inst_index);
return;
}
if (err) {
printk("%s (err %d)\n", __func__, err);
return;
}
if (!atomic_test_bit(flags[bt_conn_index(conn)], CCP_FLAG_PROFILE_CONNECTED)) {
process_profile_connection(conn);
}
}
struct bt_tbs_client_cb tbs_client_cb = {
.discover = discover_cb,
.ccid = ccid_cb,
.status_flags = status_flags_cb,
.call_state = call_state_cb,
};
int ccp_call_ctrl_init(void)
{
bt_tbs_client_register_cb(&tbs_client_cb);
return 0;
}

View file

@ -0,0 +1,48 @@
/** @file
* @brief Bluetooth Coordinated Set Identifier Profile (CSIP) Set Member role.
*
* Copyright (c) 2022 Codecoup
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/zephyr.h>
#include <zephyr/sys/printk.h>
#include <zephyr/bluetooth/audio/cap.h>
#include <zephyr/bluetooth/audio/csis.h>
#define CSIS_SIRK_DEBUG { 0xcd, 0xcc, 0x72, 0xdd, 0x86, 0x8c, 0xcd, 0xce, \
0x22, 0xfd, 0xa1, 0x21, 0x09, 0x7d, 0x7d, 0x45 }
static struct bt_csis *csis;
static void csis_lock_changed_cb(struct bt_conn *conn, struct bt_csis *csis, bool locked)
{
printk("Client %p %s the lock\n", conn, locked ? "locked" : "released");
}
static uint8_t sirk_read_req_cb(struct bt_conn *conn, struct bt_csis *csis)
{
return BT_CSIS_READ_SIRK_REQ_RSP_ACCEPT;
}
static struct bt_csis_cb csis_cb = {
.lock_changed = csis_lock_changed_cb,
.sirk_read_req = sirk_read_req_cb,
};
int csip_set_member_init(void (*rsi_changed)(const uint8_t *rsi))
{
struct bt_csis_register_param param = {
.set_size = 2,
.rank = CONFIG_HAP_HA_SET_RANK,
.lockable = false,
.set_sirk = CSIS_SIRK_DEBUG,
.cb = &csis_cb,
};
csis_cb.rsi_changed = rsi_changed;
return bt_cap_acceptor_register(&param, &csis);
}

View file

@ -0,0 +1,51 @@
/*
* Copyright (c) 2022 Codecoup
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/types.h>
/**
* @brief Initialize the BAP Unicast Server role
*
* @return 0 if success, errno on failure.
*/
int bap_unicast_sr_init(void);
/**
* @brief Initialize the CSIP Set Member role
*
* @return 0 if success, errno on failure.
*/
int csip_set_member_init(void (*rsi_changed)(const uint8_t *rsi));
/**
* @brief Initialize the VCP Volume Renderer role
*
* @return 0 if success, errno on failure.
*/
int vcp_vol_renderer_init(void);
/**
* @brief Initialize the MICP Microphone Device role
*
* @return 0 if success, errno on failure.
*/
int micp_mic_dev_init(void);
/**
* @brief Initialize the CCP Call Control Client role
*
* @return 0 if success, errno on failure.
*/
int ccp_call_ctrl_init(void);
/**
* @brief Initialize the HAS Server
*
* This will register hearing aid sample presets.
*
* @return 0 if success, errno on failure.
*/
int has_server_init(void);

View file

@ -0,0 +1,76 @@
/** @file
* @brief Bluetooth Hearing Access Service (HAS) Server role.
*
* Copyright (c) 2022 Codecoup
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/zephyr.h>
#include <zephyr/sys/printk.h>
#include <zephyr/sys/util.h>
#include <zephyr/bluetooth/audio/has.h>
#define PRESET_INDEX_UNIVERSAL 1
#define PRESET_INDEX_OUTDOOR 5
#define PRESET_INDEX_NOISY_ENV 8
#define PRESET_INDEX_OFFICE 22
static int select_cb(uint8_t index, bool sync)
{
printk("%s %u sync %d", __func__, index, sync);
return 0;
}
static void name_changed_cb(uint8_t index, const char *name)
{
printk("%s %u name %s", __func__, index, name);
}
static const struct bt_has_preset_ops ops = {
.select = select_cb,
.name_changed = name_changed_cb,
};
int has_server_init(void)
{
int err;
struct bt_has_preset_register_param param[] = {
{
.index = PRESET_INDEX_UNIVERSAL,
.properties = BT_HAS_PROP_AVAILABLE | BT_HAS_PROP_WRITABLE,
.name = "Universal",
.ops = &ops,
},
{
.index = PRESET_INDEX_OUTDOOR,
.properties = BT_HAS_PROP_AVAILABLE | BT_HAS_PROP_WRITABLE,
.name = "Outdoor",
.ops = &ops,
},
{
.index = PRESET_INDEX_NOISY_ENV,
.properties = BT_HAS_PROP_AVAILABLE | BT_HAS_PROP_WRITABLE,
.name = "Noisy environment",
.ops = &ops,
},
{
.index = PRESET_INDEX_OFFICE,
.properties = BT_HAS_PROP_AVAILABLE | BT_HAS_PROP_WRITABLE,
.name = "Office",
.ops = &ops,
},
};
for (size_t i = 0; i < ARRAY_SIZE(param); i++) {
err = bt_has_preset_register(&param[i]);
if (err != 0) {
return err;
}
}
return 0;
}

View file

@ -0,0 +1,234 @@
/*
* Copyright (c) 2022 Codecoup
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/zephyr.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/sys/printk.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/audio/audio.h>
#include <zephyr/bluetooth/audio/capabilities.h>
#include <zephyr/bluetooth/audio/csis.h>
#include <zephyr/bluetooth/services/ias.h>
#include "hap_ha.h"
#define MANDATORY_SINK_CONTEXT (BT_AUDIO_CONTEXT_TYPE_UNSPECIFIED | \
BT_AUDIO_CONTEXT_TYPE_CONVERSATIONAL | \
BT_AUDIO_CONTEXT_TYPE_MEDIA | \
BT_AUDIO_CONTEXT_TYPE_LIVE)
#define AVAILABLE_SINK_CONTEXT CONFIG_BT_PACS_SNK_CONTEXT
#define AVAILABLE_SOURCE_CONTEXT CONFIG_BT_PACS_SRC_CONTEXT
BUILD_ASSERT((CONFIG_BT_PACS_SNK_CONTEXT & MANDATORY_SINK_CONTEXT) == MANDATORY_SINK_CONTEXT,
"Need to support mandatory Supported_Sink_Contexts");
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 */
};
static uint8_t csis_rsi_addata[BT_CSIS_RSI_SIZE];
/* 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)),
#if defined(CONFIG_BT_CSIS)
BT_DATA(BT_DATA_CSIS_RSI, csis_rsi_addata, ARRAY_SIZE(csis_rsi_addata)),
#endif /* CONFIG_BT_CSIS */
BT_DATA(BT_DATA_SVC_DATA16, unicast_server_addata, ARRAY_SIZE(unicast_server_addata)),
};
static struct k_work_delayable adv_work;
static struct bt_le_ext_adv *adv;
static void disconnected(struct bt_conn *conn, uint8_t reason)
{
/* Restart advertising after disconnection */
k_work_schedule(&adv_work, K_SECONDS(1));
}
BT_CONN_CB_DEFINE(conn_callbacks) = {
.disconnected = disconnected,
};
static void rsi_changed_cb(const uint8_t *rsi)
{
char rsi_str[13];
snprintk(rsi_str, 163, "%02x%02x%02x%02x%02x%02x",
rsi[0], rsi[1], rsi[2], rsi[3], rsi[4], rsi[5]);
printk("PRSI: 0x%s\n", rsi_str);
memcpy(csis_rsi_addata, rsi, sizeof(csis_rsi_addata));
if (adv != NULL) {
int err;
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);
}
}
}
#if defined(CONFIG_BT_PRIVACY) && defined(CONFIG_BT_CSIS)
static bool adv_rpa_expired_cb(struct bt_le_ext_adv *adv)
{
printk("%s", __func__);
/* TODO: Generate new RSI and update the advertisement data */
return true;
}
#endif /* CONFIG_BT_PRIVACY && CONFIG_BT_CSIS */
static const struct bt_le_ext_adv_cb adv_cb = {
#if defined(CONFIG_BT_PRIVACY) && defined(CONFIG_BT_CSIS)
.rpa_expired = adv_rpa_expired_cb,
#endif /* CONFIG_BT_PRIVACY && CONFIG_BT_CSIS */
};
static void adv_work_handler(struct k_work *work)
{
int err;
if (adv == NULL) {
/* Create a non-connectable non-scannable advertising set */
err = bt_le_ext_adv_create(BT_LE_EXT_ADV_CONN_NAME, &adv_cb, &adv);
if (err) {
printk("Failed to create advertising set (err %d)\n", err);
}
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);
}
__ASSERT_NO_MSG(err == 0);
}
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);
} else {
printk("Advertising successfully started\n");
}
}
#if defined(CONFIG_BT_IAS)
static void alert_stop(void)
{
printk("Alert stopped\n");
}
static void alert_start(void)
{
printk("Mild alert started\n");
}
static void alert_high_start(void)
{
printk("High alert started\n");
}
BT_IAS_CB_DEFINE(ias_callbacks) = {
.no_alert = alert_stop,
.mild_alert = alert_start,
.high_alert = alert_high_start,
};
#endif /* CONFIG_BT_IAS */
void main(void)
{
int err;
err = bt_enable(NULL);
if (err != 0) {
printk("Bluetooth init failed (err %d)\n", err);
return;
}
printk("Bluetooth initialized\n");
err = has_server_init();
if (err != 0) {
printk("HAS Server init failed (err %d)\n", err);
return;
}
err = bap_unicast_sr_init();
if (err != 0) {
printk("BAP Unicast Server init failed (err %d)\n", err);
return;
}
if (IS_ENABLED(CONFIG_BT_HAS_HEARING_AID_BINAURAL)) {
err = csip_set_member_init(rsi_changed_cb);
if (err != 0) {
printk("CSIP Set Member init failed (err %d)\n", err);
return;
}
}
err = vcp_vol_renderer_init();
if (err != 0) {
printk("VCP Volume Renderer init failed (err %d)\n", err);
return;
}
if (IS_ENABLED(CONFIG_BT_ASCS_ASE_SRC)) {
err = micp_mic_dev_init();
if (err != 0) {
printk("MICP Microphone Device init failed (err %d)\n", err);
return;
}
}
if (IS_ENABLED(CONFIG_BT_TBS_CLIENT)) {
err = ccp_call_ctrl_init();
if (err != 0) {
printk("MICP Microphone Device init failed (err %d)\n", err);
return;
}
}
if (IS_ENABLED(CONFIG_BT_HAS_HEARING_AID_BANDED)) {
/* HAP_d1.0r00; 3.7 BAP Unicast Server role requirements
* A Banded Hearing Aid in the HA role shall set the
* Front Left and the Front Right bits to a value of 0b1
* in the Sink Audio Locations characteristic value.
*/
bt_audio_capability_set_location(BT_AUDIO_DIR_SINK,
(BT_AUDIO_LOCATION_FRONT_LEFT |
BT_AUDIO_LOCATION_FRONT_RIGHT));
} else {
bt_audio_capability_set_location(BT_AUDIO_DIR_SINK,
BT_AUDIO_LOCATION_FRONT_LEFT);
}
bt_audio_capability_set_available_contexts(BT_AUDIO_DIR_SINK,
AVAILABLE_SINK_CONTEXT);
if (IS_ENABLED(CONFIG_BT_ASCS_ASE_SRC)) {
bt_audio_capability_set_location(BT_AUDIO_DIR_SOURCE,
BT_AUDIO_LOCATION_FRONT_LEFT);
bt_audio_capability_set_available_contexts(BT_AUDIO_DIR_SOURCE,
AVAILABLE_SOURCE_CONTEXT);
}
k_work_init_delayable(&adv_work, adv_work_handler);
k_work_schedule(&adv_work, K_NO_WAIT);
}

View file

@ -0,0 +1,128 @@
/** @file
* @brief Bluetooth Microphone Input Control Profile (MICP) Microphone Device role.
*
* Copyright (c) 2020 Bose Corporation
* Copyright (c) 2020-2022 Nordic Semiconductor ASA
* Copyright (c) 2022 Codecoup
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/zephyr.h>
#include <zephyr/sys/printk.h>
#include <stdlib.h>
#include <stdio.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/audio/micp.h>
static void micp_mic_dev_mute_cb(uint8_t mute)
{
printk("Mute value %u\n", mute);
}
static struct bt_micp_mic_dev_cb micp_mic_dev_cbs = {
.mute = micp_mic_dev_mute_cb,
};
#if defined(CONFIG_BT_MICP_MIC_DEV_AICS)
static struct bt_micp_included micp_included;
static void micp_mic_dev_aics_state_cb(struct bt_aics *inst, int err, int8_t gain, uint8_t mute,
uint8_t mode)
{
if (err != 0) {
printk("AICS state get failed (%d) for inst %p\n", err, inst);
} else {
printk("AICS inst %p state gain %d, mute %u, mode %u\n",
inst, gain, mute, mode);
}
}
static void micp_mic_dev_aics_gain_setting_cb(struct bt_aics *inst, int err, uint8_t units,
int8_t minimum, int8_t maximum)
{
if (err != 0) {
printk("AICS gain settings get failed (%d) for inst %p\n", err, inst);
} else {
printk("AICS inst %p gain settings units %u, min %d, max %d\n",
inst, units, minimum, maximum);
}
}
static void micp_mic_dev_aics_input_type_cb(struct bt_aics *inst, int err, uint8_t input_type)
{
if (err != 0) {
printk("AICS input type get failed (%d) for inst %p\n", err, inst);
} else {
printk("AICS inst %p input type %u\n", inst, input_type);
}
}
static void micp_mic_dev_aics_status_cb(struct bt_aics *inst, int err, bool active)
{
if (err != 0) {
printk("AICS status get failed (%d) for inst %p\n", err, inst);
} else {
printk("AICS inst %p status %s\n", inst, active ? "active" : "inactive");
}
}
static void micp_mic_dev_aics_description_cb(struct bt_aics *inst, int err, char *description)
{
if (err != 0) {
printk("AICS description get failed (%d) for inst %p\n", err, inst);
} else {
printk("AICS inst %p description %s\n", inst, description);
}
}
static struct bt_aics_cb aics_cb = {
.state = micp_mic_dev_aics_state_cb,
.gain_setting = micp_mic_dev_aics_gain_setting_cb,
.type = micp_mic_dev_aics_input_type_cb,
.status = micp_mic_dev_aics_status_cb,
.description = micp_mic_dev_aics_description_cb,
};
#endif /* CONFIG_BT_MICP_MIC_DEV_AICS */
int micp_mic_dev_init(void)
{
int err;
struct bt_micp_mic_dev_register_param micp_param;
(void)memset(&micp_param, 0, sizeof(micp_param));
#if defined(CONFIG_BT_MICP_MIC_DEV_AICS)
char input_desc[CONFIG_BT_MICP_MIC_DEV_AICS_INSTANCE_COUNT][16];
for (int i = 0; i < ARRAY_SIZE(micp_param.aics_param); i++) {
micp_param.aics_param[i].desc_writable = true;
snprintf(input_desc[i], sizeof(input_desc[i]), "Input %d", i + 1);
micp_param.aics_param[i].description = input_desc[i];
micp_param.aics_param[i].type = BT_AICS_INPUT_TYPE_UNSPECIFIED;
micp_param.aics_param[i].status = true;
micp_param.aics_param[i].gain_mode = BT_AICS_MODE_MANUAL;
micp_param.aics_param[i].units = 1;
micp_param.aics_param[i].min_gain = -100;
micp_param.aics_param[i].max_gain = 100;
micp_param.aics_param[i].cb = &aics_cb;
}
#endif /* CONFIG_BT_MICP_MIC_DEV_AICS */
micp_param.cb = &micp_mic_dev_cbs;
err = bt_micp_mic_dev_register(&micp_param);
if (err != 0) {
return err;
}
#if defined(CONFIG_BT_MICP_MIC_DEV_AICS)
err = bt_micp_mic_dev_included_get(&micp_included);
if (err != 0) {
return err;
}
#endif /* CONFIG_BT_MICP_MIC_DEV_AICS */
return 0;
}

View file

@ -0,0 +1,179 @@
/** @file
* @brief Bluetooth Volume Control Profile (VCP) Volume Renderer role.
*
* Copyright (c) 2020 Bose Corporation
* Copyright (c) 2020-2021 Nordic Semiconductor ASA
* Copyright (c) 2022 Codecoup
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/zephyr.h>
#include <zephyr/sys/printk.h>
#include <stdlib.h>
#include <stdio.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/audio/vcs.h>
static struct bt_vcs *vcs;
static struct bt_vcs_included vcs_included;
static void vcs_state_cb(struct bt_vcs *vcs, int err, uint8_t volume, uint8_t mute)
{
if (err) {
printk("VCS state get failed (%d)\n", err);
} else {
printk("VCS volume %u, mute %u\n", volume, mute);
}
}
static void vcs_flags_cb(struct bt_vcs *vcs, int err, uint8_t flags)
{
if (err) {
printk("VCS flags get failed (%d)\n", err);
} else {
printk("VCS flags 0x%02X\n", flags);
}
}
static void aics_state_cb(struct bt_aics *inst, int err, int8_t gain, uint8_t mute, uint8_t mode)
{
if (err) {
printk("AICS state get failed (%d) for inst %p\n", err, inst);
} else {
printk("AICS inst %p state gain %d, mute %u, mode %u\n",
inst, gain, mute, mode);
}
}
static void aics_gain_setting_cb(struct bt_aics *inst, int err, uint8_t units, int8_t minimum,
int8_t maximum)
{
if (err) {
printk("AICS gain settings get failed (%d) for inst %p\n", err, inst);
} else {
printk("AICS inst %p gain settings units %u, min %d, max %d\n",
inst, units, minimum, maximum);
}
}
static void aics_input_type_cb(struct bt_aics *inst, int err, uint8_t input_type)
{
if (err) {
printk("AICS input type get failed (%d) for inst %p\n", err, inst);
} else {
printk("AICS inst %p input type %u\n", inst, input_type);
}
}
static void aics_status_cb(struct bt_aics *inst, int err, bool active)
{
if (err) {
printk("AICS status get failed (%d) for inst %p\n", err, inst);
} else {
printk("AICS inst %p status %s\n", inst, active ? "active" : "inactive");
}
}
static void aics_description_cb(struct bt_aics *inst, int err, char *description)
{
if (err) {
printk("AICS description get failed (%d) for inst %p\n", err, inst);
} else {
printk("AICS inst %p description %s\n", inst, description);
}
}
static void vocs_state_cb(struct bt_vocs *inst, int err, int16_t offset)
{
if (err) {
printk("VOCS state get failed (%d) for inst %p\n", err, inst);
} else {
printk("VOCS inst %p offset %d\n", inst, offset);
}
}
static void vocs_location_cb(struct bt_vocs *inst, int err, uint32_t location)
{
if (err) {
printk("VOCS location get failed (%d) for inst %p\n", err, inst);
} else {
printk("VOCS inst %p location %u\n", inst, location);
}
}
static void vocs_description_cb(struct bt_vocs *inst, int err, char *description)
{
if (err) {
printk("VOCS description get failed (%d) for inst %p\n", err, inst);
} else {
printk("VOCS inst %p description %s\n", inst, description);
}
}
static struct bt_vcs_cb vcs_cbs = {
.state = vcs_state_cb,
.flags = vcs_flags_cb,
};
static struct bt_aics_cb aics_cbs = {
.state = aics_state_cb,
.gain_setting = aics_gain_setting_cb,
.type = aics_input_type_cb,
.status = aics_status_cb,
.description = aics_description_cb
};
static struct bt_vocs_cb vocs_cbs = {
.state = vocs_state_cb,
.location = vocs_location_cb,
.description = vocs_description_cb
};
int vcp_vol_renderer_init(void)
{
int err;
struct bt_vcs_register_param vcs_param;
char input_desc[CONFIG_BT_VCS_AICS_INSTANCE_COUNT][16];
char output_desc[CONFIG_BT_VCS_VOCS_INSTANCE_COUNT][16];
memset(&vcs_param, 0, sizeof(vcs_param));
for (int i = 0; i < ARRAY_SIZE(vcs_param.vocs_param); i++) {
vcs_param.vocs_param[i].location_writable = true;
vcs_param.vocs_param[i].desc_writable = true;
snprintf(output_desc[i], sizeof(output_desc[i]), "Output %d", i + 1);
vcs_param.vocs_param[i].output_desc = output_desc[i];
vcs_param.vocs_param[i].cb = &vocs_cbs;
}
for (int i = 0; i < ARRAY_SIZE(vcs_param.aics_param); i++) {
vcs_param.aics_param[i].desc_writable = true;
snprintf(input_desc[i], sizeof(input_desc[i]), "Input %d", i + 1);
vcs_param.aics_param[i].description = input_desc[i];
vcs_param.aics_param[i].type = BT_AICS_INPUT_TYPE_UNSPECIFIED;
vcs_param.aics_param[i].status = true;
vcs_param.aics_param[i].gain_mode = BT_AICS_MODE_MANUAL;
vcs_param.aics_param[i].units = 1;
vcs_param.aics_param[i].min_gain = -100;
vcs_param.aics_param[i].max_gain = 100;
vcs_param.aics_param[i].cb = &aics_cbs;
}
vcs_param.step = 1;
vcs_param.mute = BT_VCS_STATE_UNMUTED;
vcs_param.volume = 100;
vcs_param.cb = &vcs_cbs;
err = bt_vcs_register(&vcs_param, &vcs);
if (err) {
return err;
}
err = bt_vcs_included_get(vcs, &vcs_included);
if (err != 0) {
return err;
}
return 0;
}