mirror of
https://github.com/espressif/esp-idf.git
synced 2026-04-27 19:13:21 +00:00
feat(ble_audio): Support ISO & LE Audio functionalities (Preview)
This commit is contained in:
@@ -162,6 +162,24 @@ examples/bluetooth/blufi:
|
||||
- esp_console
|
||||
- esp_wifi
|
||||
|
||||
examples/bluetooth/esp_ble_audio:
|
||||
<<: *bt_default_depends
|
||||
enable:
|
||||
- if: SOC_BLE_AUDIO_SUPPORTED == 1 and IDF_TARGET == "esp32H4"
|
||||
temporary: true
|
||||
reason: BLE Audio examples only supported on ESP32-H4
|
||||
depends_filepatterns:
|
||||
- examples/bluetooth/esp_ble_audio/common_components/**/*
|
||||
|
||||
examples/bluetooth/esp_ble_iso:
|
||||
<<: *bt_default_depends
|
||||
enable:
|
||||
- if: SOC_BLE_ISO_SUPPORTED == 1 and IDF_TARGET == "esp32H4"
|
||||
temporary: true
|
||||
reason: BLE ISO examples only supported on ESP32-H4
|
||||
depends_filepatterns:
|
||||
- examples/bluetooth/esp_ble_iso/common_components/**/*
|
||||
|
||||
examples/bluetooth/esp_ble_mesh:
|
||||
<<: *bt_default_depends
|
||||
disable:
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# The following lines of boilerplate have to be in your project's CMakeLists
|
||||
# in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
|
||||
idf_build_set_property(MINIMAL_BUILD ON)
|
||||
project(bap_broadcast_sink)
|
||||
@@ -0,0 +1,72 @@
|
||||
| Supported Targets | ESP32-H4 |
|
||||
| ----------------- | -------- |
|
||||
|
||||
# BAP Broadcast Sink Example
|
||||
|
||||
(See the README.md file in the upper level `examples` directory for more information about examples.)
|
||||
|
||||
This example demonstrates the **Basic Audio Profile (BAP) Broadcast Sink** functionality. It scans for BAP Broadcast Sources (advertising with the Broadcast Audio service data), establishes periodic advertising synchronization with the first matching source, creates a BAP Broadcast Sink, and synchronizes to the BIG to receive broadcast audio streams. It listens until the source stops or sync is lost. The scan filters by the hardcoded source name (`BAP Broadcast Source`). Optionally, you can run in a mode where a Broadcast Assistant (e.g. a phone app) controls when to sync via the Scan Delegator.
|
||||
|
||||
The implementation uses the NimBLE host stack with ISO and BAP support, ESP-BLE-ISO, and ESP-BLE-AUDIO APIs (PACS, BAP broadcast sink, scan delegator). It is intended for chips that support BLE 5.2 ISO and LE Audio (e.g. ESP32-H4). Sink capabilities are registered with LC3 (16 kHz and 24 kHz, 10 ms frame duration, 1 channel). For encrypted broadcasts, the broadcast code is hardcoded as `1234` in the source, or can be provided by a Broadcast Assistant.
|
||||
|
||||
## Requirements
|
||||
|
||||
* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4)
|
||||
* A BAP Broadcast Source (e.g. another device or sample running as broadcast source) that is advertising and sending broadcast audio
|
||||
|
||||
## How to Use Example
|
||||
|
||||
Before project configuration and build, set the correct chip target:
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32h4
|
||||
```
|
||||
|
||||
### Configure the Project
|
||||
|
||||
Open the project configuration menu:
|
||||
|
||||
```bash
|
||||
idf.py menuconfig
|
||||
```
|
||||
|
||||
In the **Example: Broadcast Sink** menu:
|
||||
|
||||
* **Whether to wait for a Broadcast Assistant**: If enabled, the device advertises connectable and waits for a Broadcast Assistant to connect and send PA sync, broadcast code, and BIS sync requests via the Scan Delegator; the sink then syncs according to those requests.
|
||||
|
||||
### Build and Flash
|
||||
|
||||
Run the following to build, flash and monitor:
|
||||
|
||||
```bash
|
||||
idf.py -p PORT flash monitor
|
||||
```
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF.
|
||||
|
||||
## Example Flow
|
||||
|
||||
1. **Initialization**: NVS, Bluetooth stack, and LE Audio common layer (`esp_ble_audio_common_init`). Register PACS with sink capabilities (LC3), register BAP scan delegator callbacks and broadcast sink callbacks, then start the audio stack. Set device name if needed.
|
||||
2. **Scanning**: Start extended scanning. On scan report, look for Broadcast Audio service data (UUID + broadcast ID). Filter by the hardcoded target device name (`BAP Broadcast Source`). When a matching advertiser with periodic advertising is found and not already syncing, create periodic advertising synchronization.
|
||||
3. **PA sync**: When periodic sync is established, create a BAP Broadcast Sink (`esp_ble_audio_bap_broadcast_sink_create`) for that sync handle and broadcast ID.
|
||||
4. **BASE and syncable**: When the BASE is received, the sink gets subgroup count and BIS index bitfield. When BIGInfo indicates the sink is syncable, call `esp_ble_audio_bap_broadcast_sink_sync` with the chosen BIS bitfield and broadcast code (from menuconfig or from the scan delegator). Streams are bound to the registered stream array.
|
||||
5. **Streams**: As each BIS stream starts, the stream started callback runs; when all requested streams have started, the sink is considered running. Incoming audio data is delivered in the stream receive callback; the example counts valid, error, and lost packets per stream and logs periodically. When streams stop or PA sync is lost, the sink is cleaned up.
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
I (xxx) BAP_BSNK: Broadcast source PA synced, creating Broadcast Sink
|
||||
I (xxx) BAP_BSNK: Received BASE with 1 subgroups from broadcast sink 0x...
|
||||
I (xxx) BAP_BSNK: Broadcast sink (0x...) is syncable, BIG not encrypted
|
||||
I (xxx) BAP_BSNK: Stream 0x... started (1/1)
|
||||
I (xxx) BAP_BSNK: Received 1000(1000/0/0) ISO data packets (stream 0x...)
|
||||
...
|
||||
```
|
||||
|
||||
If PA sync is lost:
|
||||
|
||||
```
|
||||
I (xxx) BAP_BSNK: PA sync lost, reason ...
|
||||
```
|
||||
@@ -0,0 +1,4 @@
|
||||
set(srcs "main.c")
|
||||
|
||||
idf_component_register(SRCS "${srcs}"
|
||||
REQUIRES bt nvs_flash)
|
||||
@@ -0,0 +1,14 @@
|
||||
# SPDX-FileCopyrightText: 2022 Nordic Semiconductor ASA
|
||||
# SPDX-FileContributor: 2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
menu "Example: Broadcast Sink"
|
||||
|
||||
config EXAMPLE_SCAN_OFFLOAD
|
||||
bool "Whether to wait for a Broadcast Assistant"
|
||||
select BT_NIMBLE_PERIODIC_ADV_SYNC_TRANSFER
|
||||
help
|
||||
If set to true, the example will start advertising connectable
|
||||
for Broadcast Assistants.
|
||||
|
||||
endmenu
|
||||
@@ -0,0 +1,5 @@
|
||||
dependencies:
|
||||
example_init:
|
||||
path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_init
|
||||
example_utils:
|
||||
path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_utils
|
||||
@@ -0,0 +1,637 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021-2022 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_system.h"
|
||||
|
||||
#include "host/ble_hs.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
|
||||
#include "esp_ble_audio_lc3_defs.h"
|
||||
#include "esp_ble_audio_bap_api.h"
|
||||
#include "esp_ble_audio_pacs_api.h"
|
||||
|
||||
#include "ble_audio_example_init.h"
|
||||
#include "ble_audio_example_utils.h"
|
||||
|
||||
#define TAG "BAP_BSNK"
|
||||
|
||||
#define TARGET_DEVICE_NAME "BAP Broadcast Source"
|
||||
#define TARGET_DEVICE_NAME_LEN (sizeof(TARGET_DEVICE_NAME) - 1)
|
||||
|
||||
#define TARGET_BROADCAST_CODE "1234"
|
||||
|
||||
#define SCAN_INTERVAL 160 /* 100ms */
|
||||
#define SCAN_WINDOW 160 /* 100ms */
|
||||
|
||||
#define PA_SYNC_SKIP 0
|
||||
#define PA_SYNC_TIMEOUT 1000 /* 1000 * 10ms = 10s */
|
||||
#define PA_SYNC_HANDLE_INIT UINT16_MAX
|
||||
|
||||
#define CONN_HANDLE_INIT UINT16_MAX
|
||||
|
||||
#define SINK_CONTEXT (ESP_BLE_AUDIO_CONTEXT_TYPE_CONVERSATIONAL | \
|
||||
ESP_BLE_AUDIO_CONTEXT_TYPE_MEDIA)
|
||||
|
||||
static struct broadcast_sink_stream {
|
||||
esp_ble_audio_bap_stream_t stream;
|
||||
example_audio_rx_metrics_t rx_metrics;
|
||||
} streams[CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT];
|
||||
|
||||
static esp_ble_audio_bap_stream_t *streams_p[ARRAY_SIZE(streams)];
|
||||
|
||||
static const esp_ble_audio_bap_scan_delegator_recv_state_t *req_recv_state;
|
||||
static esp_ble_audio_bap_broadcast_sink_t *broadcast_sink;
|
||||
static uint16_t sync_handle = PA_SYNC_HANDLE_INIT;
|
||||
static bool pa_syncing;
|
||||
|
||||
static uint16_t conn_handle = CONN_HANDLE_INIT;
|
||||
static volatile bool stream_started;
|
||||
static volatile bool base_received;
|
||||
static uint32_t bis_index_bitfield;
|
||||
|
||||
static const uint32_t bis_index_mask = BIT_MASK(ARRAY_SIZE(streams) + 1);
|
||||
static uint8_t sink_broadcast_code[ESP_BLE_ISO_BROADCAST_CODE_SIZE];
|
||||
static uint32_t broadcaster_broadcast_id;
|
||||
static uint32_t requested_bis_sync;
|
||||
|
||||
static volatile uint8_t stream_count;
|
||||
static volatile uint8_t stream_count_started;
|
||||
static volatile uint8_t stream_count_stopped;
|
||||
|
||||
static uint8_t codec_data[] =
|
||||
ESP_BLE_AUDIO_CODEC_CAP_LC3_DATA(
|
||||
ESP_BLE_AUDIO_CODEC_CAP_FREQ_16KHZ | \
|
||||
ESP_BLE_AUDIO_CODEC_CAP_FREQ_24KHZ, /* Sampling frequency 16kHz/24kHz */
|
||||
ESP_BLE_AUDIO_CODEC_CAP_DURATION_10, /* Frame duration 10ms */
|
||||
ESP_BLE_AUDIO_CODEC_CAP_CHAN_COUNT_SUPPORT(1), /* Supported channels 1 */
|
||||
40, /* Minimum 40 octets per frame */
|
||||
60, /* Maximum 60 octets per frame */
|
||||
1); /* Maximum 1 codec frame per SDU */
|
||||
|
||||
static uint8_t codec_meta[] =
|
||||
ESP_BLE_AUDIO_CODEC_CAP_LC3_META(SINK_CONTEXT);
|
||||
|
||||
static const esp_ble_audio_codec_cap_t codec_cap =
|
||||
ESP_BLE_AUDIO_CODEC_CAP_LC3(codec_data, codec_meta);
|
||||
|
||||
static esp_ble_audio_pacs_cap_t cap = {
|
||||
.codec_cap = &codec_cap,
|
||||
};
|
||||
|
||||
static void ext_scan_start(void)
|
||||
{
|
||||
struct ble_gap_disc_params params = {0};
|
||||
uint8_t own_addr_type;
|
||||
int err;
|
||||
|
||||
err = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
params.passive = 1;
|
||||
params.itvl = SCAN_INTERVAL;
|
||||
params.window = SCAN_WINDOW;
|
||||
|
||||
err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms,
|
||||
example_audio_gap_event_cb, NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start scanning, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Extended scan started");
|
||||
}
|
||||
|
||||
static int pa_sync_create(const bt_addr_le_t *addr, uint8_t adv_sid)
|
||||
{
|
||||
struct ble_gap_periodic_sync_params params = {0};
|
||||
ble_addr_t sync_addr = {0};
|
||||
|
||||
sync_addr.type = addr->type;
|
||||
memcpy(sync_addr.val, addr->a.val, sizeof(sync_addr.val));
|
||||
params.skip = PA_SYNC_SKIP;
|
||||
params.sync_timeout = PA_SYNC_TIMEOUT;
|
||||
|
||||
return ble_gap_periodic_adv_sync_create(&sync_addr, adv_sid, ¶ms,
|
||||
example_audio_gap_event_cb, NULL);
|
||||
}
|
||||
|
||||
static int pa_sync_terminate(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = ble_gap_periodic_adv_sync_terminate(sync_handle);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to terminate PA sync, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "PA sync terminated");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void recv_state_updated_cb(esp_ble_conn_t *conn,
|
||||
const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state)
|
||||
{
|
||||
ESP_LOGI(TAG, "Receive state updated, pa_sync 0x%02x encrypt 0x%02x",
|
||||
recv_state->pa_sync_state, recv_state->encrypt_state);
|
||||
|
||||
for (uint8_t i = 0; i < recv_state->num_subgroups; i++) {
|
||||
ESP_LOGI(TAG, "subgroup %d bis_sync 0x%08lx", i, recv_state->subgroups[i].bis_sync);
|
||||
}
|
||||
|
||||
if (recv_state->pa_sync_state == ESP_BLE_AUDIO_BAP_PA_STATE_SYNCED) {
|
||||
req_recv_state = recv_state;
|
||||
}
|
||||
}
|
||||
|
||||
static int pa_sync_req_cb(esp_ble_conn_t *conn,
|
||||
const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state,
|
||||
bool past_available, uint16_t pa_interval)
|
||||
{
|
||||
ESP_LOGI(TAG, "Received request to sync to PA (PAST %savailable): %u",
|
||||
past_available ? "" : "not ",
|
||||
recv_state->pa_sync_state);
|
||||
|
||||
req_recv_state = recv_state;
|
||||
|
||||
if (recv_state->pa_sync_state == ESP_BLE_AUDIO_BAP_PA_STATE_SYNCED ||
|
||||
recv_state->pa_sync_state == ESP_BLE_AUDIO_BAP_PA_STATE_INFO_REQ ||
|
||||
sync_handle != PA_SYNC_HANDLE_INIT) {
|
||||
/* Already syncing */
|
||||
ESP_LOGW(TAG, "Rejecting PA sync request");
|
||||
return -EALREADY;
|
||||
}
|
||||
|
||||
if (past_available) {
|
||||
ESP_LOGW(TAG, "Currently not support PAST");
|
||||
return -ENOTSUP;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int pa_sync_term_req_cb(esp_ble_conn_t *conn,
|
||||
const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state)
|
||||
{
|
||||
int err;
|
||||
|
||||
ESP_LOGI(TAG, "Received request to terminate PA sync");
|
||||
|
||||
for (uint8_t i = 0; i < recv_state->num_subgroups; i++) {
|
||||
ESP_LOGI(TAG, "subgroup %d bis_sync 0x%08lx", i, recv_state->subgroups[i].bis_sync);
|
||||
}
|
||||
|
||||
req_recv_state = recv_state;
|
||||
|
||||
err = pa_sync_terminate();
|
||||
if (err) {
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
sync_handle = PA_SYNC_HANDLE_INIT;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void broadcast_code_cb(esp_ble_conn_t *conn,
|
||||
const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state,
|
||||
const uint8_t broadcast_code[ESP_BLE_ISO_BROADCAST_CODE_SIZE])
|
||||
{
|
||||
ESP_LOGI(TAG, "Broadcast code received for %p", recv_state);
|
||||
|
||||
req_recv_state = recv_state;
|
||||
|
||||
memcpy(sink_broadcast_code, broadcast_code, ESP_BLE_ISO_BROADCAST_CODE_SIZE);
|
||||
}
|
||||
|
||||
static int bis_sync_req_cb(esp_ble_conn_t *conn,
|
||||
const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state,
|
||||
const uint32_t bis_sync_req[CONFIG_BT_BAP_BASS_MAX_SUBGROUPS])
|
||||
{
|
||||
/* Bit field indicating from which subgroup(s) BIS sync is requested */
|
||||
uint32_t requested_subgroup_sync = 0;
|
||||
esp_err_t err;
|
||||
|
||||
requested_bis_sync = 0;
|
||||
|
||||
assert(bis_sync_req);
|
||||
|
||||
for (uint8_t subgroup = 0; subgroup < recv_state->num_subgroups &&
|
||||
subgroup < CONFIG_BT_BAP_BASS_MAX_SUBGROUPS; subgroup++) {
|
||||
if (bis_sync_req[subgroup]) {
|
||||
if (requested_bis_sync == 0) {
|
||||
requested_bis_sync = bis_sync_req[subgroup];
|
||||
} else {
|
||||
if (requested_bis_sync != ESP_BLE_AUDIO_BAP_BIS_SYNC_NO_PREF &&
|
||||
bis_sync_req[subgroup] != ESP_BLE_AUDIO_BAP_BIS_SYNC_NO_PREF) {
|
||||
/* Spec a little bit unclear. Here we choose to say that BIS sync
|
||||
* request from more than 1 subgroup is not possible unless sync
|
||||
* value is 0 or ESP_BLE_AUDIO_BAP_BIS_SYNC_NO_PREF.
|
||||
*/
|
||||
ESP_LOGE(TAG, "Unsupported BIS sync request from more than 1 subgroup");
|
||||
return -EINVAL;
|
||||
}
|
||||
}
|
||||
|
||||
requested_subgroup_sync |= BIT(subgroup);
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "BIS sync req for %p: BIS indexes 0x%08x (subgroup indexes 0x%08x), "
|
||||
"broadcast id: 0x%06x, (%s)", recv_state, requested_bis_sync,
|
||||
requested_subgroup_sync, recv_state->broadcast_id,
|
||||
stream_started ? "Stream started" : "Stream not started");
|
||||
|
||||
if (stream_started && requested_bis_sync == 0) {
|
||||
/* The stream stopped callback will be called as part of this, and
|
||||
* we do not need to wait for any events from the controller. Thus,
|
||||
* when this returns, the `stream_started` is back to false.
|
||||
*/
|
||||
err = esp_ble_audio_bap_broadcast_sink_stop(broadcast_sink);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop broadcast sink, err %d", err);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to delete broadcast sink, err %d", err);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
broadcast_sink = NULL;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static esp_ble_audio_bap_scan_delegator_cb_t scan_delegator_cbs = {
|
||||
.recv_state_updated = recv_state_updated_cb,
|
||||
.pa_sync_req = pa_sync_req_cb,
|
||||
.pa_sync_term_req = pa_sync_term_req_cb,
|
||||
.broadcast_code = broadcast_code_cb,
|
||||
.bis_sync_req = bis_sync_req_cb,
|
||||
};
|
||||
|
||||
static void base_recv_cb(esp_ble_audio_bap_broadcast_sink_t *sink,
|
||||
const esp_ble_audio_bap_base_t *base,
|
||||
size_t base_size)
|
||||
{
|
||||
uint32_t base_bis_index_bitfield = 0;
|
||||
uint8_t base_subgroup_count;
|
||||
esp_err_t err;
|
||||
|
||||
if (base_received) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_base_get_subgroup_count(base, &base_subgroup_count);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get subgroup count");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Received BASE with %d subgroups from broadcast sink %p",
|
||||
base_subgroup_count, sink);
|
||||
|
||||
err = esp_ble_audio_bap_base_get_bis_indexes(base, &base_bis_index_bitfield);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get BIS indexes, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
bis_index_bitfield = (base_bis_index_bitfield & bis_index_mask);
|
||||
|
||||
ESP_LOGI(TAG, "bis_index_bitfield = 0x%08lx", bis_index_bitfield);
|
||||
|
||||
if (conn_handle == CONN_HANDLE_INIT) {
|
||||
/* No broadcast assistant requesting anything */
|
||||
requested_bis_sync = ESP_BLE_AUDIO_BAP_BIS_SYNC_NO_PREF;
|
||||
}
|
||||
|
||||
base_received = true;
|
||||
}
|
||||
|
||||
static void syncable_cb(esp_ble_audio_bap_broadcast_sink_t *sink,
|
||||
const esp_ble_iso_biginfo_t *biginfo)
|
||||
{
|
||||
uint32_t sync_bitfield;
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "Broadcast sink (%p) is syncable, BIG %s",
|
||||
sink, biginfo->encryption ? "encrypted" : "not encrypted");
|
||||
|
||||
sync_bitfield = (bis_index_bitfield & requested_bis_sync);
|
||||
if (sync_bitfield == 0) {
|
||||
ESP_LOGW(TAG, "No matching BIS indexes, skipping sync");
|
||||
return;
|
||||
}
|
||||
|
||||
stream_count = 0;
|
||||
stream_count_started = 0;
|
||||
stream_count_stopped = 0;
|
||||
|
||||
for (size_t i = 0; i < ESP_BLE_ISO_MAX_GROUP_ISO_COUNT; i++) {
|
||||
if (sync_bitfield & BIT(i)) {
|
||||
stream_count++;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Syncing to broadcast with bitfield:");
|
||||
ESP_LOGI(TAG, "0x%08x = 0x%08x (bis_index) & 0x%08x (req_bis_sync), stream_count %u",
|
||||
sync_bitfield, bis_index_bitfield, requested_bis_sync, stream_count);
|
||||
|
||||
if (biginfo->encryption) {
|
||||
memset(sink_broadcast_code, 0, ESP_BLE_ISO_BROADCAST_CODE_SIZE);
|
||||
memcpy(sink_broadcast_code, TARGET_BROADCAST_CODE,
|
||||
MIN(ESP_BLE_ISO_BROADCAST_CODE_SIZE, strlen(TARGET_BROADCAST_CODE)));
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_sink_sync(broadcast_sink, sync_bitfield,
|
||||
streams_p, sink_broadcast_code);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to sync to broadcast source, err %d", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static esp_ble_audio_bap_broadcast_sink_cb_t broadcast_sink_cbs = {
|
||||
.base_recv = base_recv_cb,
|
||||
.syncable = syncable_cb,
|
||||
};
|
||||
|
||||
static void stream_started_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
struct broadcast_sink_stream *sink_stream = CONTAINER_OF(stream,
|
||||
struct broadcast_sink_stream,
|
||||
stream);
|
||||
|
||||
ESP_LOGI(TAG, "Stream %p started (%u/%u)", stream, stream_count_started, stream_count);
|
||||
|
||||
example_audio_rx_metrics_reset(&sink_stream->rx_metrics);
|
||||
|
||||
if (++stream_count_started == stream_count) {
|
||||
stream_started = true;
|
||||
}
|
||||
}
|
||||
|
||||
static void stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "Stream %p stopped with reason 0x%02x (%u/%u)",
|
||||
stream, reason, stream_count_stopped, stream_count);
|
||||
|
||||
if (++stream_count_stopped == stream_count) {
|
||||
stream_started = false;
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to delete broadcast sink, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
broadcast_sink = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void stream_recv_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
const esp_ble_iso_recv_info_t *info,
|
||||
const uint8_t *data, uint16_t len)
|
||||
{
|
||||
struct broadcast_sink_stream *sink_stream = CONTAINER_OF(stream,
|
||||
struct broadcast_sink_stream,
|
||||
stream);
|
||||
|
||||
sink_stream->rx_metrics.last_sdu_len = len;
|
||||
example_audio_rx_metrics_on_recv(info, &sink_stream->rx_metrics,
|
||||
TAG, "stream", stream);
|
||||
}
|
||||
|
||||
static esp_ble_audio_bap_stream_ops_t stream_ops = {
|
||||
.started = stream_started_cb,
|
||||
.stopped = stream_stopped_cb,
|
||||
.recv = stream_recv_cb,
|
||||
};
|
||||
|
||||
struct scan_recv_data {
|
||||
bool target_matched;
|
||||
bool broadcast_id_found;
|
||||
uint32_t broadcast_id;
|
||||
};
|
||||
|
||||
static bool data_cb(uint8_t type, const uint8_t *data,
|
||||
uint8_t data_len, void *user_data)
|
||||
{
|
||||
struct scan_recv_data *sr = user_data;
|
||||
uint16_t uuid;
|
||||
|
||||
switch (type) {
|
||||
case EXAMPLE_AD_TYPE_NAME_SHORTENED:
|
||||
case EXAMPLE_AD_TYPE_NAME_COMPLETE:
|
||||
case EXAMPLE_AD_TYPE_BROADCAST_NAME:
|
||||
sr->target_matched = (data_len == TARGET_DEVICE_NAME_LEN) &&
|
||||
!memcmp(data, TARGET_DEVICE_NAME, TARGET_DEVICE_NAME_LEN);
|
||||
if (!sr->target_matched) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
case EXAMPLE_AD_TYPE_SERVICE_DATA16:
|
||||
if (data_len < ESP_BLE_AUDIO_UUID_SIZE_16 + ESP_BLE_AUDIO_BROADCAST_ID_SIZE) {
|
||||
return true;
|
||||
}
|
||||
uuid = sys_get_le16(data);
|
||||
if (uuid != ESP_BLE_AUDIO_UUID_BROADCAST_AUDIO_VAL) {
|
||||
return true;
|
||||
}
|
||||
sr->broadcast_id = sys_get_le24(data + ESP_BLE_AUDIO_UUID_SIZE_16);
|
||||
sr->broadcast_id_found = true;
|
||||
return true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
struct scan_recv_data sr = {0};
|
||||
bt_addr_le_t addr;
|
||||
int err;
|
||||
|
||||
/* Periodic advertising interval. 0 if no periodic advertising. */
|
||||
if (event->ext_scan_recv.per_adv_itvl == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
esp_ble_audio_data_parse(event->ext_scan_recv.data,
|
||||
event->ext_scan_recv.data_len,
|
||||
data_cb, &sr);
|
||||
|
||||
if (!sr.target_matched || !sr.broadcast_id_found) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pa_syncing == false && req_recv_state == NULL) {
|
||||
broadcaster_broadcast_id = sr.broadcast_id;
|
||||
|
||||
addr.type = event->ext_scan_recv.addr.type;
|
||||
memcpy(addr.a.val, event->ext_scan_recv.addr.val, sizeof(addr.a.val));
|
||||
|
||||
err = pa_sync_create(&addr, event->ext_scan_recv.sid);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to create PA sync, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
pa_syncing = true;
|
||||
}
|
||||
}
|
||||
|
||||
static void pa_sync(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
pa_syncing = false;
|
||||
|
||||
if (event->pa_sync.status) {
|
||||
ESP_LOGE(TAG, "PA sync failed, status %d", event->pa_sync.status);
|
||||
return;
|
||||
}
|
||||
|
||||
sync_handle = event->pa_sync.sync_handle;
|
||||
|
||||
ESP_LOGI(TAG, "Broadcast source PA synced, creating Broadcast Sink");
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_sink_create(event->pa_sync.sync_handle,
|
||||
broadcaster_broadcast_id,
|
||||
&broadcast_sink);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to create broadcast sink, err %d", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void pa_sync_lost(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
ESP_LOGI(TAG, "PA sync lost: sync_handle 0x%04x reason 0x%02x",
|
||||
event->pa_sync_lost.sync_handle, event->pa_sync_lost.reason);
|
||||
|
||||
if (sync_handle == event->pa_sync_lost.sync_handle) {
|
||||
sync_handle = PA_SYNC_HANDLE_INIT;
|
||||
pa_syncing = false;
|
||||
base_received = false;
|
||||
stream_started = false;
|
||||
stream_count = 0;
|
||||
stream_count_started = 0;
|
||||
stream_count_stopped = 0;
|
||||
|
||||
if (broadcast_sink != NULL) {
|
||||
esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink);
|
||||
broadcast_sink = NULL;
|
||||
}
|
||||
|
||||
ext_scan_start();
|
||||
}
|
||||
}
|
||||
|
||||
static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
switch (event->type) {
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_EXT_SCAN_RECV:
|
||||
ext_scan_recv(event);
|
||||
break;
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC:
|
||||
pa_sync(event);
|
||||
break;
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC_LOST:
|
||||
pa_sync_lost(event);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
const esp_ble_audio_pacs_register_param_t pacs_param = {
|
||||
.snk_pac = true,
|
||||
.snk_loc = true,
|
||||
};
|
||||
esp_ble_audio_init_info_t info = {
|
||||
.gap_cb = iso_gap_app_cb,
|
||||
};
|
||||
esp_err_t err;
|
||||
|
||||
/* Initialize NVS — it is used to store PHY calibration data */
|
||||
err = nvs_flash_init();
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
err = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(err);
|
||||
|
||||
err = bluetooth_init();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize BLE, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_common_init(&info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize audio, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_pacs_register(&pacs_param);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register pacs, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(streams); i++) {
|
||||
streams[i].stream.ops = &stream_ops;
|
||||
streams_p[i] = &streams[i].stream;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_pacs_cap_register(ESP_BLE_AUDIO_DIR_SINK, &cap);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register pacs capabilities, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_scan_delegator_register(&scan_delegator_cbs);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register scan delegator, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_sink_register_cb(&broadcast_sink_cbs);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register broadcast sink callbacks, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_common_start(NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start audio, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ext_scan_start();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
# This file was generated using idf.py save-defconfig. It can be edited manually.
|
||||
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
|
||||
#
|
||||
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_BLUEDROID_ENABLED=n
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_EXT_ADV=y
|
||||
CONFIG_BT_NIMBLE_MAX_CCCDS=20
|
||||
CONFIG_BT_NIMBLE_ISO=y
|
||||
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
|
||||
|
||||
CONFIG_BT_ISO_MAX_CHAN=2
|
||||
|
||||
CONFIG_BT_BAP_BROADCAST_SINK=y
|
||||
CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT=2
|
||||
CONFIG_BT_BAP_SCAN_DELEGATOR=y
|
||||
CONFIG_BT_PAC_SNK_NOTIFIABLE=y
|
||||
CONFIG_BT_PAC_SNK_LOC_WRITEABLE=y
|
||||
CONFIG_BT_PAC_SNK_LOC_NOTIFIABLE=y
|
||||
CONFIG_BT_PAC_SRC_NOTIFIABLE=y
|
||||
CONFIG_BT_PAC_SRC_LOC_WRITEABLE=y
|
||||
CONFIG_BT_PAC_SRC_LOC_NOTIFIABLE=y
|
||||
CONFIG_BT_PACS_SUPPORTED_CONTEXT_NOTIFIABLE=y
|
||||
CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=30
|
||||
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
@@ -0,0 +1,6 @@
|
||||
# Override some defaults so BT stack is enabled
|
||||
# by default in this example
|
||||
|
||||
CONFIG_IDF_TARGET="esp32h4"
|
||||
|
||||
CONFIG_BT_LE_ISO_SUPPORT=y
|
||||
@@ -0,0 +1,8 @@
|
||||
# The following lines of boilerplate have to be in your project's CMakeLists
|
||||
# in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
|
||||
idf_build_set_property(MINIMAL_BUILD ON)
|
||||
project(bap_broadcast_source)
|
||||
@@ -0,0 +1,61 @@
|
||||
| Supported Targets | ESP32-H4 |
|
||||
| ----------------- | -------- |
|
||||
|
||||
# BAP Broadcast Source Example
|
||||
|
||||
(See the README.md file in the upper level `examples` directory for more information about examples.)
|
||||
|
||||
This example demonstrates the **Basic Audio Profile (BAP) Broadcast Source** functionality. It starts extended advertising with the Broadcast Audio service data (UUID and Broadcast ID) and device name, periodic advertising with the Broadcast Audio Source Endpoint (BASE), then starts the BAP Broadcast Source so that BIGInfo and (mock) audio data are sent over the BIG. Sinks such as the [broadcast_sink](../broadcast_sink) example can discover this source, establish periodic sync, and synchronize to the BIG to receive the streams.
|
||||
|
||||
The implementation uses the NimBLE host stack with ISO and BAP support, ESP-BLE-ISO, and ESP-BLE-AUDIO APIs (BAP broadcast source create/start, stream send, LC3 presets). It is intended for chips that support BLE 5.2 ISO and LE Audio (e.g. ESP32-H4). The source is configured with the LC3 16_2_1 broadcast preset, a hardcoded broadcast ID (`0x123456`), and a hardcoded broadcast code (`1234`) for encrypted broadcasts. These values can be changed by editing the source code constants.
|
||||
|
||||
## Requirements
|
||||
|
||||
* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4)
|
||||
* Optionally, a device running the [broadcast_sink](../broadcast_sink) example to receive and play the broadcast streams
|
||||
|
||||
## How to Use Example
|
||||
|
||||
Before project configuration and build, set the correct chip target:
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32h4
|
||||
```
|
||||
|
||||
### Build and Flash
|
||||
|
||||
Run the following to build, flash and monitor:
|
||||
|
||||
```bash
|
||||
idf.py -p PORT flash monitor
|
||||
```
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF.
|
||||
|
||||
## Example Flow
|
||||
|
||||
1. **Initialization**: NVS, Bluetooth stack, and LE Audio common layer (`esp_ble_audio_common_init`). No PACS or GAP callback needed for the source role.
|
||||
2. **Broadcast source setup**: Register broadcast source callbacks (started/stopped). Configure subgroups and streams using the selected LC3 preset (codec config, QoS, channel allocation). Create the BAP Broadcast Source (`esp_ble_audio_bap_broadcast_source_create`) with optional encryption and sequential packing. Register stream callbacks (started, stopped, sent) for each stream.
|
||||
3. **Extended and periodic advertising**: Set extended advertising data: Broadcast Audio UUID + Broadcast ID (static or random) + complete device name. Set periodic advertising data: Broadcast Audio UUID + encoded BASE from the broadcast source. Start periodic advertising then extended advertising.
|
||||
4. **Start BIG**: Add the advertising set for BIG and start the BAP Broadcast Source (`esp_ble_audio_bap_broadcast_source_start`) with the same adv handle. BIGInfo is sent in the periodic advertising; BIS streams are created.
|
||||
5. **Stream and send**: When each BIS stream starts, the stream started callback allocates an SDU buffer and starts a periodic TX scheduler based on `k_work_delayable`. The scheduler posts work items to the ISO task at the stream QoS interval to send mock audio data; sent and drift are reported in the stream sent callback. When a stream stops, resources are freed and the scheduler is stopped. Note that the scheduler timer resolution is in milliseconds, which may not match the exact SDU interval for all configurations.
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
I (xxx) BAP_BSRC: Creating broadcast source with 1 subgroups & 2 streams per subgroup
|
||||
I (xxx) BAP_BSRC: Extended adv instance 0 started
|
||||
I (xxx) BAP_BSRC: Broadcast source 0x... started
|
||||
I (xxx) BAP_BSRC: Stream 0x... started
|
||||
I (xxx) BAP_BSRC: Transmitted 1000 ISO data packets (stream 0x...)
|
||||
...
|
||||
```
|
||||
|
||||
If the broadcast source stops (e.g. stream stopped):
|
||||
|
||||
```
|
||||
I (xxx) BAP_BSRC: Stream 0x... stopped, reason 0x...
|
||||
I (xxx) BAP_BSRC: Broadcast source 0x... stopped, reason 0x...
|
||||
```
|
||||
@@ -0,0 +1,4 @@
|
||||
set(srcs "main.c")
|
||||
|
||||
idf_component_register(SRCS "${srcs}"
|
||||
REQUIRES bt nvs_flash)
|
||||
@@ -0,0 +1,5 @@
|
||||
dependencies:
|
||||
example_init:
|
||||
path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_init
|
||||
example_utils:
|
||||
path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_utils
|
||||
@@ -0,0 +1,539 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021-2022 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_random.h"
|
||||
#include "esp_timer.h"
|
||||
|
||||
#include "host/ble_hs.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
|
||||
#include "esp_ble_audio_lc3_defs.h"
|
||||
#include "esp_ble_audio_bap_api.h"
|
||||
#include "esp_ble_audio_pacs_api.h"
|
||||
#include "esp_ble_audio_bap_lc3_preset_defs.h"
|
||||
|
||||
#include "ble_audio_example_init.h"
|
||||
#include "ble_audio_example_utils.h"
|
||||
|
||||
#define TAG "BAP_BSRC"
|
||||
|
||||
ESP_BLE_AUDIO_BAP_LC3_BROADCAST_PRESET_16_2_1_DEFINE(preset_active,
|
||||
ESP_BLE_AUDIO_LOCATION_FRONT_LEFT |
|
||||
ESP_BLE_AUDIO_LOCATION_FRONT_RIGHT,
|
||||
ESP_BLE_AUDIO_CONTEXT_TYPE_UNSPECIFIED);
|
||||
|
||||
#define LOCAL_DEVICE_NAME "BAP Broadcast Source"
|
||||
#define LOCAL_DEVICE_NAME_LEN (sizeof(LOCAL_DEVICE_NAME) - 1)
|
||||
|
||||
#define LOCAL_BROADCAST_CODE "1234" /* Maximum length is 16 */
|
||||
#define LOCAL_BROADCAST_ID 0x123456
|
||||
|
||||
#define ADV_HANDLE 0x00
|
||||
#define ADV_SID 0
|
||||
#define ADV_TX_POWER 127
|
||||
#define ADV_ADDRESS BLE_OWN_ADDR_PUBLIC
|
||||
#define ADV_PRIMARY_PHY BLE_HCI_LE_PHY_1M
|
||||
#define ADV_SECONDARY_PHY BLE_HCI_LE_PHY_2M
|
||||
#define ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(200)
|
||||
|
||||
#define PER_ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(100)
|
||||
|
||||
#define STREAM_COUNT CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT
|
||||
#define SUBGROUP_COUNT CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT
|
||||
|
||||
static struct broadcast_source_stream {
|
||||
esp_ble_audio_bap_stream_t stream;
|
||||
uint16_t seq_num;
|
||||
uint8_t *data;
|
||||
example_audio_tx_scheduler_t scheduler;
|
||||
} streams[STREAM_COUNT];
|
||||
|
||||
static esp_ble_audio_bap_broadcast_source_t *broadcast_source;
|
||||
|
||||
static void broadcast_source_tx(struct broadcast_source_stream *source_stream);
|
||||
|
||||
static void stream_started_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
struct broadcast_source_stream *source_stream = CONTAINER_OF(stream,
|
||||
struct broadcast_source_stream,
|
||||
stream);
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "Stream %p started", stream);
|
||||
|
||||
if (source_stream->stream.qos == NULL || source_stream->stream.qos->sdu == 0) {
|
||||
ESP_LOGE(TAG, "Invalid stream qos");
|
||||
return;
|
||||
}
|
||||
|
||||
if (source_stream->data == NULL) {
|
||||
source_stream->data = calloc(1, source_stream->stream.qos->sdu);
|
||||
if (source_stream->data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to alloc tx buffer, sdu %u", source_stream->stream.qos->sdu);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
source_stream->seq_num = 0;
|
||||
example_audio_tx_scheduler_reset(&source_stream->scheduler);
|
||||
|
||||
/* Note: esp timer is not accurate enough */
|
||||
err = example_audio_tx_scheduler_start(&source_stream->scheduler, preset_active.qos.interval);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start tx scheduler, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
broadcast_source_tx(source_stream);
|
||||
}
|
||||
|
||||
static void stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
|
||||
{
|
||||
struct broadcast_source_stream *source_stream = CONTAINER_OF(stream,
|
||||
struct broadcast_source_stream,
|
||||
stream);
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "Stream %p stopped, reason 0x%02x", stream, reason);
|
||||
|
||||
err = example_audio_tx_scheduler_stop(&source_stream->scheduler);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err);
|
||||
}
|
||||
}
|
||||
|
||||
static void stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
|
||||
{
|
||||
struct broadcast_source_stream *source_stream = CONTAINER_OF(stream,
|
||||
struct broadcast_source_stream,
|
||||
stream);
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "Stream %p disconnected, reason 0x%02x", stream, reason);
|
||||
|
||||
err = example_audio_tx_scheduler_stop(&source_stream->scheduler);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err);
|
||||
}
|
||||
}
|
||||
|
||||
static void stream_sent_cb(esp_ble_audio_bap_stream_t *stream, void *user_data)
|
||||
{
|
||||
struct broadcast_source_stream *source_stream = CONTAINER_OF(stream,
|
||||
struct broadcast_source_stream,
|
||||
stream);
|
||||
|
||||
example_audio_tx_scheduler_on_sent(&source_stream->scheduler, user_data, TAG, "stream", stream);
|
||||
}
|
||||
|
||||
static esp_ble_audio_bap_stream_ops_t stream_ops = {
|
||||
.started = stream_started_cb,
|
||||
.stopped = stream_stopped_cb,
|
||||
.sent = stream_sent_cb,
|
||||
.disconnected = stream_disconnected_cb,
|
||||
};
|
||||
|
||||
static bool stream_is_streaming(const esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
esp_ble_audio_bap_ep_info_t ep_info = {0};
|
||||
|
||||
if (stream->ep == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (ep_info.state == ESP_BLE_AUDIO_BAP_EP_STATE_STREAMING);
|
||||
}
|
||||
|
||||
static void broadcast_source_tx(struct broadcast_source_stream *source_stream)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
if (!stream_is_streaming(&source_stream->stream)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (source_stream->stream.qos == NULL || source_stream->stream.qos->sdu == 0) {
|
||||
ESP_LOGE(TAG, "Invalid stream qos");
|
||||
return;
|
||||
}
|
||||
|
||||
if (source_stream->data == NULL) {
|
||||
ESP_LOGE(TAG, "Tx buffer unavailable, sdu %u", source_stream->stream.qos->sdu);
|
||||
return;
|
||||
}
|
||||
|
||||
memset(source_stream->data, (uint8_t)source_stream->seq_num, source_stream->stream.qos->sdu);
|
||||
|
||||
err = esp_ble_audio_bap_stream_send(&source_stream->stream,
|
||||
source_stream->data,
|
||||
source_stream->stream.qos->sdu,
|
||||
source_stream->seq_num);
|
||||
if (err) {
|
||||
ESP_LOGD(TAG, "Failed to broadcast data on stream %p, err %d",
|
||||
&source_stream->stream, err);
|
||||
return;
|
||||
}
|
||||
|
||||
source_stream->seq_num++;
|
||||
}
|
||||
|
||||
static void tx_scheduler_cb(void *arg)
|
||||
{
|
||||
struct broadcast_source_stream *source_stream = arg;
|
||||
|
||||
if (source_stream == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
broadcast_source_tx(source_stream);
|
||||
}
|
||||
|
||||
static void source_started_cb(esp_ble_audio_bap_broadcast_source_t *source)
|
||||
{
|
||||
ESP_LOGI(TAG, "Broadcast source %p started", source);
|
||||
}
|
||||
|
||||
static void source_stopped_cb(esp_ble_audio_bap_broadcast_source_t *source, uint8_t reason)
|
||||
{
|
||||
ESP_LOGI(TAG, "Broadcast source %p stopped, reason 0x%02x", source, reason);
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(streams); i++) {
|
||||
if (streams[i].data != NULL) {
|
||||
free(streams[i].data);
|
||||
streams[i].data = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static esp_err_t broadcast_source_setup(void)
|
||||
{
|
||||
esp_ble_audio_bap_broadcast_source_subgroup_param_t subgroup_param[SUBGROUP_COUNT];
|
||||
esp_ble_audio_bap_broadcast_source_stream_param_t stream_params[STREAM_COUNT];
|
||||
esp_ble_audio_bap_broadcast_source_param_t create_param = {0};
|
||||
size_t streams_per_subgroup;
|
||||
uint8_t left[] = {
|
||||
ESP_BLE_AUDIO_CODEC_DATA(ESP_BLE_AUDIO_CODEC_CFG_CHAN_ALLOC,
|
||||
EXAMPLE_BYTES_LIST_LE32(ESP_BLE_AUDIO_LOCATION_FRONT_LEFT))
|
||||
};
|
||||
uint8_t right[] = {
|
||||
ESP_BLE_AUDIO_CODEC_DATA(ESP_BLE_AUDIO_CODEC_CFG_CHAN_ALLOC,
|
||||
EXAMPLE_BYTES_LIST_LE32(ESP_BLE_AUDIO_LOCATION_FRONT_RIGHT))
|
||||
};
|
||||
static esp_ble_audio_bap_broadcast_source_cb_t broadcast_source_cb = {
|
||||
.started = source_started_cb,
|
||||
.stopped = source_stopped_cb,
|
||||
};
|
||||
esp_err_t err;
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_source_register_cb(&broadcast_source_cb);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register broadcast source callbacks, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
streams_per_subgroup = ARRAY_SIZE(stream_params) / ARRAY_SIZE(subgroup_param);
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(subgroup_param); i++) {
|
||||
subgroup_param[i].params_count = streams_per_subgroup;
|
||||
subgroup_param[i].params = stream_params + i * streams_per_subgroup;
|
||||
subgroup_param[i].codec_cfg = &preset_active.codec_cfg;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(stream_params); i++) {
|
||||
stream_params[i].stream = &streams[i].stream;
|
||||
stream_params[i].data = (i == 0 ? left : right);
|
||||
stream_params[i].data_len = (i == 0 ? sizeof(left) : sizeof(right));
|
||||
|
||||
esp_ble_audio_bap_stream_cb_register(stream_params[i].stream, &stream_ops);
|
||||
}
|
||||
|
||||
create_param.params_count = ARRAY_SIZE(subgroup_param);
|
||||
create_param.params = subgroup_param;
|
||||
create_param.qos = &preset_active.qos;
|
||||
create_param.encryption = (strlen(LOCAL_BROADCAST_CODE) > 0);
|
||||
create_param.packing = ESP_BLE_ISO_PACKING_SEQUENTIAL;
|
||||
|
||||
if (create_param.encryption) {
|
||||
memcpy(create_param.broadcast_code, LOCAL_BROADCAST_CODE, strlen(LOCAL_BROADCAST_CODE));
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Creating broadcast source with %u subgroups & %u streams per subgroup",
|
||||
ARRAY_SIZE(subgroup_param), streams_per_subgroup);
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_source_create(&create_param, &broadcast_source);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to create broadcast source, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint8_t *ext_adv_data_get(uint8_t *data_len)
|
||||
{
|
||||
uint32_t broadcast_id;
|
||||
uint8_t *data;
|
||||
|
||||
broadcast_id = LOCAL_BROADCAST_ID;
|
||||
|
||||
/* - Broadcast Audio Announcement Service UUID (2 octets) and
|
||||
* Broadcast ID (3 octets)
|
||||
* - Complete Device Name
|
||||
*/
|
||||
*data_len = 7 + 2 + LOCAL_DEVICE_NAME_LEN;
|
||||
|
||||
data = calloc(1, *data_len);
|
||||
if (data == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
data[0] = 0x06; /* 1 + 2 + 3 */
|
||||
data[1] = EXAMPLE_AD_TYPE_SERVICE_DATA16;
|
||||
data[2] = (ESP_BLE_AUDIO_UUID_BROADCAST_AUDIO_VAL & 0xFF);
|
||||
data[3] = ((ESP_BLE_AUDIO_UUID_BROADCAST_AUDIO_VAL >> 8) & 0xFF);
|
||||
data[4] = (broadcast_id & 0xFF);
|
||||
data[5] = ((broadcast_id >> 8) & 0xFF);
|
||||
data[6] = ((broadcast_id >> 16) & 0xFF);
|
||||
|
||||
data[7] = LOCAL_DEVICE_NAME_LEN + 1;
|
||||
data[8] = EXAMPLE_AD_TYPE_NAME_COMPLETE;
|
||||
memcpy(data + 9, LOCAL_DEVICE_NAME, LOCAL_DEVICE_NAME_LEN);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static uint8_t *per_adv_data_get(uint8_t *data_len)
|
||||
{
|
||||
NET_BUF_SIMPLE_DEFINE(base_buf, 128);
|
||||
uint8_t *data;
|
||||
esp_err_t err;
|
||||
|
||||
/* Broadcast Audio Announcement Service UUID (2 octets) and
|
||||
* Broadcast Audio Source Endpoint (BASE)
|
||||
*/
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_source_get_base(broadcast_source, &base_buf);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get encoded BASE, err %d", err);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
*data_len = 2 + base_buf.len;
|
||||
|
||||
data = calloc(1, *data_len);
|
||||
if (data == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* base_buf.len has included the UUID length (2 octets) */
|
||||
data[0] = 1 + base_buf.len;
|
||||
data[1] = EXAMPLE_AD_TYPE_SERVICE_DATA16;
|
||||
memcpy(data + 2, base_buf.data, base_buf.len);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static int ext_adv_start(void)
|
||||
{
|
||||
struct ble_gap_periodic_adv_params per_params = {0};
|
||||
struct ble_gap_ext_adv_params ext_params = {0};
|
||||
struct os_mbuf *data = NULL;
|
||||
uint8_t *ext_data = NULL;
|
||||
uint8_t *per_data = NULL;
|
||||
uint8_t data_len = 0;
|
||||
int err;
|
||||
|
||||
ext_params.connectable = 0;
|
||||
ext_params.scannable = 0;
|
||||
ext_params.legacy_pdu = 0;
|
||||
ext_params.own_addr_type = ADV_ADDRESS;
|
||||
ext_params.primary_phy = ADV_PRIMARY_PHY;
|
||||
ext_params.secondary_phy = ADV_SECONDARY_PHY;
|
||||
ext_params.tx_power = ADV_TX_POWER;
|
||||
ext_params.sid = ADV_SID;
|
||||
ext_params.itvl_min = ADV_INTERVAL;
|
||||
ext_params.itvl_max = ADV_INTERVAL;
|
||||
|
||||
err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL,
|
||||
example_audio_gap_event_cb, NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
ext_data = ext_adv_data_get(&data_len);
|
||||
if (ext_data == NULL) {
|
||||
err = -ENOMEM;
|
||||
goto end;
|
||||
}
|
||||
|
||||
data = os_msys_get_pkthdr(data_len, 0);
|
||||
if (data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to get ext adv mbuf");
|
||||
err = -ENOMEM;
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = os_mbuf_append(data, ext_data, data_len);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err);
|
||||
os_mbuf_free_chain(data);
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = ble_gap_ext_adv_set_data(ADV_HANDLE, data);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
per_params.include_tx_power = 0;
|
||||
per_params.itvl_min = PER_ADV_INTERVAL;
|
||||
per_params.itvl_max = PER_ADV_INTERVAL;
|
||||
|
||||
err = ble_gap_periodic_adv_configure(ADV_HANDLE, &per_params);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to configure per adv params, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
per_data = per_adv_data_get(&data_len);
|
||||
if (per_data == NULL) {
|
||||
err = -ENOMEM;
|
||||
goto end;
|
||||
}
|
||||
|
||||
data = os_msys_get_pkthdr(data_len, 0);
|
||||
if (data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to get per adv mbuf");
|
||||
err = -ENOMEM;
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = os_mbuf_append(data, per_data, data_len);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to append per adv data, err %d", err);
|
||||
os_mbuf_free_chain(data);
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = ble_gap_periodic_adv_set_data(ADV_HANDLE, data);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set per adv data, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = ble_gap_periodic_adv_start(ADV_HANDLE);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start per advertising, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Extended adv instance %u started", ADV_HANDLE);
|
||||
|
||||
end:
|
||||
if (ext_data) {
|
||||
free(ext_data);
|
||||
}
|
||||
if (per_data) {
|
||||
free(per_data);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
static void broadcast_start(void)
|
||||
{
|
||||
esp_ble_audio_bap_broadcast_adv_info_t info = {
|
||||
.adv_handle = ADV_HANDLE,
|
||||
};
|
||||
int err;
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_adv_add(&info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to add adv for broadcast source, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_source_start(broadcast_source, ADV_HANDLE);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start broadcast source, err %d", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
/* Initialize NVS — it is used to store PHY calibration data */
|
||||
err = nvs_flash_init();
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
err = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(err);
|
||||
|
||||
err = bluetooth_init();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize BLE, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_common_init(NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize audio, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = broadcast_source_setup();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_common_start(NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start audio, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(streams); i++) {
|
||||
err = example_audio_tx_scheduler_init(&streams[i].scheduler,
|
||||
tx_scheduler_cb,
|
||||
&streams[i]);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize tx scheduler[%u], err %d", i, err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
err = ext_adv_start();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
broadcast_start();
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
# This file was generated using idf.py save-defconfig. It can be edited manually.
|
||||
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
|
||||
#
|
||||
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_BLUEDROID_ENABLED=n
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_EXT_ADV=y
|
||||
CONFIG_BT_NIMBLE_ISO=y
|
||||
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
|
||||
|
||||
CONFIG_BT_ISO_MAX_CHAN=2
|
||||
|
||||
CONFIG_BT_BAP_BROADCAST_SOURCE=y
|
||||
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
@@ -0,0 +1,6 @@
|
||||
# Override some defaults so BT stack is enabled
|
||||
# by default in this example
|
||||
|
||||
CONFIG_IDF_TARGET="esp32h4"
|
||||
|
||||
CONFIG_BT_LE_ISO_SUPPORT=y
|
||||
@@ -0,0 +1,8 @@
|
||||
# The following lines of boilerplate have to be in your project's CMakeLists
|
||||
# in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
|
||||
idf_build_set_property(MINIMAL_BUILD ON)
|
||||
project(bap_unicast_client)
|
||||
@@ -0,0 +1,64 @@
|
||||
| Supported Targets | ESP32-H4 |
|
||||
| ----------------- | -------- |
|
||||
|
||||
# BAP Unicast Client Example
|
||||
|
||||
(See the README.md file in the upper level `examples` directory for more information about examples.)
|
||||
|
||||
This example demonstrates the **Basic Audio Profile (BAP) Unicast Client** functionality. It scans for a BAP Unicast Server (advertising with the ASCS service data), connects to it, performs GATT service discovery and MTU exchange, then discovers sink and source ASEs, configures streams with an LC3 unicast preset, sets QoS, enables and connects the streams, and establishes unicast audio. For source (TX) streams the client starts the stream and can send audio data; sink streams are started by the server. Run it together with the [unicast_server](../unicast_server) example on another device as the peer.
|
||||
|
||||
The implementation uses the NimBLE host stack with ISO and BAP support, ESP-BLE-ISO, and ESP-BLE-AUDIO APIs (BAP unicast client discover, config, QoS, enable, connect, start; stream send). It is intended for chips that support BLE 5.2 ISO and LE Audio (e.g. ESP32-H4). The client uses the LC3 16_2_1 unicast preset and initiates pairing after the ACL connection.
|
||||
|
||||
## Requirements
|
||||
|
||||
* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4)
|
||||
* Another device running the [unicast_server](../unicast_server) example, which advertises with ASCS and acts as BAP Unicast Server
|
||||
|
||||
## How to Use Example
|
||||
|
||||
Before project configuration and build, set the correct chip target:
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32h4
|
||||
```
|
||||
|
||||
### Build and Flash
|
||||
|
||||
Run the following to build, flash and monitor:
|
||||
|
||||
```bash
|
||||
idf.py -p PORT flash monitor
|
||||
```
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF.
|
||||
|
||||
## Example Flow
|
||||
|
||||
1. **Initialization**: NVS, Bluetooth stack, and LE Audio common layer (`esp_ble_audio_common_init`) with GAP and GATT callbacks. Register stream ops for sink and source streams, register BAP unicast client callbacks, then start the audio stack.
|
||||
2. **Scanning**: Start passive extended scanning. On scan report, parse advertising for connectable packets containing the ASCS (Audio Stream Control Service) service data; when found, initiate an ACL connection to that device.
|
||||
3. **Connection and security**: On ACL connection, initiate pairing. After MTU exchange and GATT service discovery complete, start BAP discovery (sinks first, then sources).
|
||||
4. **Discovery**: Discover sink ASEs then source ASEs on the server. For each discovered endpoint, the endpoint callback records the EP; when source discovery completes, start configuring streams.
|
||||
5. **Configure, QoS, enable**: For each sink and source stream, configure with the LC3 16_2_1 codec config. When all streams are configured, create a unicast group, set QoS for the group, then enable each stream with metadata. When all streams are enabled, connect each stream.
|
||||
6. **Connect and start**: When each stream is connected, connect the next; when all are connected, start the source (TX) streams. Sink streams are started by the server. When a TX stream is started, it is registered for sending; the example can then send audio on that stream.
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
I (xxx) BAP_UCL: Unicast server found, type ...
|
||||
I (xxx) BAP_UCL: connection established, status 0
|
||||
I (xxx) BAP_UCL: Sink #0: ep 0x...
|
||||
I (xxx) BAP_UCL: Source #0: ep 0x...
|
||||
I (xxx) BAP_UCL: Discover sinks complete
|
||||
I (xxx) BAP_UCL: Discover sources complete
|
||||
I (xxx) BAP_UCL: Sink stream[0] configured
|
||||
I (xxx) BAP_UCL: Source stream[0] configured
|
||||
I (xxx) BAP_UCL: Stream 0x... QoS set
|
||||
I (xxx) BAP_UCL: Stream 0x... enabled
|
||||
I (xxx) BAP_UCL: Stream 0x... connected
|
||||
I (xxx) BAP_UCL: Stream 0x... started
|
||||
...
|
||||
```
|
||||
|
||||
If connection or discovery fails, relevant error messages are logged.
|
||||
@@ -0,0 +1,5 @@
|
||||
set(srcs "stream_tx.c"
|
||||
"main.c")
|
||||
|
||||
idf_component_register(SRCS "${srcs}"
|
||||
REQUIRES bt nvs_flash)
|
||||
@@ -0,0 +1,5 @@
|
||||
dependencies:
|
||||
example_init:
|
||||
path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_init
|
||||
example_utils:
|
||||
path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_utils
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "stream_tx.h"
|
||||
|
||||
static struct tx_stream tx_streams[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT];
|
||||
|
||||
static bool stream_is_streaming(const esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
esp_ble_audio_bap_ep_info_t ep_info = {0};
|
||||
int err;
|
||||
|
||||
/* No-op if stream is not configured */
|
||||
if (stream->ep == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info);
|
||||
if (err) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (ep_info.state == ESP_BLE_AUDIO_BAP_EP_STATE_STREAMING);
|
||||
}
|
||||
|
||||
static void stream_tx_send(struct tx_stream *tx_stream)
|
||||
{
|
||||
int err;
|
||||
|
||||
if (tx_stream == NULL || tx_stream->stream == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (stream_is_streaming(tx_stream->stream) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tx_stream->stream->qos == NULL || tx_stream->stream->qos->sdu == 0) {
|
||||
ESP_LOGE(TAG, "Invalid stream qos");
|
||||
return;
|
||||
}
|
||||
|
||||
if (tx_stream->data == NULL) {
|
||||
ESP_LOGE(TAG, "TX buffer unavailable, SDU %u (stream %p)",
|
||||
tx_stream->stream->qos->sdu, tx_stream->stream);
|
||||
return;
|
||||
}
|
||||
|
||||
memset(tx_stream->data, (uint8_t)tx_stream->seq_num,
|
||||
tx_stream->stream->qos->sdu);
|
||||
|
||||
err = esp_ble_audio_bap_stream_send(tx_stream->stream,
|
||||
tx_stream->data,
|
||||
tx_stream->stream->qos->sdu,
|
||||
tx_stream->seq_num);
|
||||
if (err) {
|
||||
ESP_LOGD(TAG, "Failed to transmit data on stream %p, err %d",
|
||||
tx_stream->stream, err);
|
||||
return;
|
||||
}
|
||||
|
||||
tx_stream->seq_num++;
|
||||
}
|
||||
|
||||
void stream_tx_sent(esp_ble_audio_bap_stream_t *stream, void *user_data)
|
||||
{
|
||||
for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) {
|
||||
if (tx_streams[i].stream == stream) {
|
||||
example_audio_tx_scheduler_on_sent(&tx_streams[i].scheduler, user_data,
|
||||
TAG, "stream", stream);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void tx_scheduler_cb(void *arg)
|
||||
{
|
||||
struct tx_stream *tx_stream = arg;
|
||||
|
||||
stream_tx_send(tx_stream);
|
||||
}
|
||||
|
||||
int stream_tx_register(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
int err;
|
||||
|
||||
if (stream == NULL) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) {
|
||||
if (tx_streams[i].stream == NULL) {
|
||||
ESP_LOGI(TAG, "Registered stream %p for TX", stream);
|
||||
|
||||
if (stream->qos == NULL || stream->qos->sdu == 0) {
|
||||
ESP_LOGE(TAG, "Invalid stream qos");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
if (tx_streams[i].data == NULL) {
|
||||
tx_streams[i].data = calloc(1, stream->qos->sdu);
|
||||
if (tx_streams[i].data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to alloc TX buffer, SDU %u (stream %p)",
|
||||
stream->qos->sdu, stream);
|
||||
return -ENOMEM;
|
||||
}
|
||||
}
|
||||
|
||||
tx_streams[i].stream = stream;
|
||||
tx_streams[i].seq_num = 0;
|
||||
example_audio_tx_scheduler_reset(&tx_streams[i].scheduler);
|
||||
|
||||
err = example_audio_tx_scheduler_start(&tx_streams[i].scheduler,
|
||||
stream->qos->interval);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start tx scheduler, err %d", err);
|
||||
tx_streams[i].stream = NULL;
|
||||
return err;
|
||||
}
|
||||
|
||||
stream_tx_send(&tx_streams[i]);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGE(TAG, "No free TX stream slot");
|
||||
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
int stream_tx_unregister(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
int err;
|
||||
|
||||
if (stream == NULL) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) {
|
||||
if (tx_streams[i].stream == stream) {
|
||||
ESP_LOGI(TAG, "Unregistered stream %p for TX", stream);
|
||||
|
||||
err = example_audio_tx_scheduler_stop(&tx_streams[i].scheduler);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
tx_streams[i].stream = NULL;
|
||||
if (tx_streams[i].data != NULL) {
|
||||
free(tx_streams[i].data);
|
||||
tx_streams[i].data = NULL;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return -ENODATA;
|
||||
}
|
||||
|
||||
void stream_tx_init(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) {
|
||||
err = example_audio_tx_scheduler_init(&tx_streams[i].scheduler,
|
||||
tx_scheduler_cb,
|
||||
&tx_streams[i]);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to init tx scheduler[%u], err %d", i, err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#include "esp_ble_audio_lc3_defs.h"
|
||||
#include "esp_ble_audio_bap_api.h"
|
||||
#include "esp_ble_audio_pacs_api.h"
|
||||
#include "esp_ble_audio_bap_lc3_preset_defs.h"
|
||||
#include "esp_ble_audio_common_api.h"
|
||||
|
||||
#include "ble_audio_example_init.h"
|
||||
#include "ble_audio_example_utils.h"
|
||||
|
||||
#define TAG "BAP_UCL"
|
||||
|
||||
struct tx_stream {
|
||||
esp_ble_audio_bap_stream_t *stream;
|
||||
uint16_t seq_num;
|
||||
uint8_t *data;
|
||||
example_audio_tx_scheduler_t scheduler;
|
||||
};
|
||||
|
||||
void stream_tx_sent(esp_ble_audio_bap_stream_t *stream, void *user_data);
|
||||
|
||||
int stream_tx_register(esp_ble_audio_bap_stream_t *stream);
|
||||
|
||||
int stream_tx_unregister(esp_ble_audio_bap_stream_t *stream);
|
||||
|
||||
void stream_tx_init(void);
|
||||
@@ -0,0 +1,19 @@
|
||||
# This file was generated using idf.py save-defconfig. It can be edited manually.
|
||||
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
|
||||
#
|
||||
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_BLUEDROID_ENABLED=n
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_EXT_ADV=y
|
||||
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
|
||||
CONFIG_BT_NIMBLE_MAX_CCCDS=20
|
||||
CONFIG_BT_NIMBLE_ISO=y
|
||||
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
|
||||
|
||||
CONFIG_BT_ISO_MAX_CHAN=2
|
||||
|
||||
CONFIG_BT_BAP_UNICAST_CLIENT=y
|
||||
CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT=2
|
||||
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
@@ -0,0 +1,6 @@
|
||||
# Override some defaults so BT stack is enabled
|
||||
# by default in this example
|
||||
|
||||
CONFIG_IDF_TARGET="esp32h4"
|
||||
|
||||
CONFIG_BT_LE_ISO_SUPPORT=y
|
||||
@@ -0,0 +1,8 @@
|
||||
# The following lines of boilerplate have to be in your project's CMakeLists
|
||||
# in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
|
||||
idf_build_set_property(MINIMAL_BUILD ON)
|
||||
project(bap_unicast_server)
|
||||
@@ -0,0 +1,61 @@
|
||||
| Supported Targets | ESP32-H4 |
|
||||
| ----------------- | -------- |
|
||||
|
||||
# BAP Unicast Server Example
|
||||
|
||||
(See the README.md file in the upper level `examples` directory for more information about examples.)
|
||||
|
||||
This example demonstrates the **Basic Audio Profile (BAP) Unicast Server** functionality. It starts connectable extended advertising with the ASCS (Audio Stream Control Service) UUID and service data (targeted announcement, sink and source audio contexts), then waits for a BAP Unicast Client to connect. After connection, the server responds to client requests: codec configuration, QoS, enable, and start. The server starts sink (RX) streams when the client enables them; source (TX) streams are started by the client. The server receives audio on sink streams and can send on source streams. Run it together with the [unicast_client](../unicast_client) example on another device as the client.
|
||||
|
||||
The implementation uses the NimBLE host stack with ISO and BAP support, ESP-BLE-ISO, and ESP-BLE-AUDIO APIs (PACS, BAP unicast server register and callbacks, stream start, stream recv). It is intended for chips that support BLE 5.2 ISO and LE Audio (e.g. ESP32-H4). PACS advertises sink and source capabilities with LC3 (any frequency, 10 ms frame, up to 2 channels, various contexts). The number of sink and source ASEs is set via `CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT` and `CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT`.
|
||||
|
||||
## Requirements
|
||||
|
||||
* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4)
|
||||
* Another device running the [unicast_client](../unicast_client) example, which scans for ASCS and connects to this server
|
||||
|
||||
## How to Use Example
|
||||
|
||||
Before project configuration and build, set the correct chip target:
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32h4
|
||||
```
|
||||
|
||||
### Build and Flash
|
||||
|
||||
Run the following to build, flash and monitor:
|
||||
|
||||
```bash
|
||||
idf.py -p PORT flash monitor
|
||||
```
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF.
|
||||
|
||||
## Example Flow
|
||||
|
||||
1. **Initialization**: NVS, Bluetooth stack, and LE Audio common layer (`esp_ble_audio_common_init`) with GAP and GATT callbacks. Register PACS (sink and source PAC and location), register the BAP unicast server (sink and source ASE counts), register unicast server callbacks (config, reconfig, QoS, enable, start, metadata, disable, stop, release), register PACS capabilities (LC3 sink and source), register stream callbacks for sink and source streams, set PACS location and supported/available contexts, then start the audio stack and set the device name.
|
||||
2. **Advertising**: Start connectable extended advertising with flags, ASCS UUID, ASCS service data (targeted, sink/source contexts), and device name `bap_unicast_server`.
|
||||
3. **Client connection**: When a client connects, ACL and optional security change are handled. On MTU exchange the server may start GATT service discovery (as needed for the stack).
|
||||
4. **ASCS operations**: The client discovers ASEs, then sends codec config, QoS, enable, and start. The server allocates a stream per config (sink or source), returns QoS preference, validates enable (codec params), and on start resets stream counters. For sink streams, when the client enables the stream the server calls `esp_ble_audio_bap_stream_start`; for source streams the client starts them.
|
||||
5. **Streaming**: When a sink stream is started, received ISO SDUs are delivered in the stream recv callback; the example counts valid, error, and lost packets and logs periodically. Source streams can send audio when started by the client.
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
I (xxx) BAP_USR: Extended adv instance 0 started
|
||||
I (xxx) BAP_USR: connection established, status 0
|
||||
I (xxx) BAP_USR: ASE Codec Config: conn 0x... ep 0x... dir ...
|
||||
I (xxx) BAP_USR: Stream 0x... enabled
|
||||
I (xxx) BAP_USR: Stream 0x... started
|
||||
I (xxx) BAP_USR: Received 1000(1000/0/0) ISO data packets (stream 0x...)
|
||||
...
|
||||
```
|
||||
|
||||
If the client disconnects:
|
||||
|
||||
```
|
||||
I (xxx) BAP_USR: connection disconnected, reason 0x...
|
||||
```
|
||||
@@ -0,0 +1,4 @@
|
||||
set(srcs "main.c")
|
||||
|
||||
idf_component_register(SRCS "${srcs}"
|
||||
REQUIRES bt nvs_flash)
|
||||
@@ -0,0 +1,5 @@
|
||||
dependencies:
|
||||
example_init:
|
||||
path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_init
|
||||
example_utils:
|
||||
path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_utils
|
||||
@@ -0,0 +1,781 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021-2022 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_timer.h"
|
||||
|
||||
#include "host/ble_hs.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
|
||||
#include "esp_ble_audio_lc3_defs.h"
|
||||
#include "esp_ble_audio_bap_api.h"
|
||||
#include "esp_ble_audio_pacs_api.h"
|
||||
|
||||
#include "ble_audio_example_init.h"
|
||||
#include "ble_audio_example_utils.h"
|
||||
|
||||
#define TAG "BAP_USR"
|
||||
|
||||
#define LOCAL_DEVICE_NAME "BAP Unicast Server"
|
||||
#define LOCAL_DEVICE_NAME_LEN (sizeof(LOCAL_DEVICE_NAME) - 1)
|
||||
|
||||
#define ADV_HANDLE 0x00
|
||||
#define ADV_SID 0
|
||||
#define ADV_TX_POWER 127
|
||||
#define ADV_ADDRESS BLE_OWN_ADDR_PUBLIC
|
||||
#define ADV_PRIMARY_PHY BLE_HCI_LE_PHY_1M
|
||||
#define ADV_SECONDARY_PHY BLE_HCI_LE_PHY_2M
|
||||
#define ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(200)
|
||||
|
||||
#define SINK_LOCATION (ESP_BLE_AUDIO_LOCATION_FRONT_LEFT | \
|
||||
ESP_BLE_AUDIO_LOCATION_FRONT_RIGHT)
|
||||
|
||||
#define SOURCE_LOCATION (ESP_BLE_AUDIO_LOCATION_FRONT_LEFT | \
|
||||
ESP_BLE_AUDIO_LOCATION_FRONT_RIGHT)
|
||||
|
||||
#define SINK_CONTEXT (ESP_BLE_AUDIO_CONTEXT_TYPE_UNSPECIFIED | \
|
||||
ESP_BLE_AUDIO_CONTEXT_TYPE_CONVERSATIONAL | \
|
||||
ESP_BLE_AUDIO_CONTEXT_TYPE_MEDIA)
|
||||
|
||||
#define SOURCE_CONTEXT (ESP_BLE_AUDIO_CONTEXT_TYPE_UNSPECIFIED | \
|
||||
ESP_BLE_AUDIO_CONTEXT_TYPE_CONVERSATIONAL | \
|
||||
ESP_BLE_AUDIO_CONTEXT_TYPE_MEDIA)
|
||||
|
||||
static uint8_t codec_data[] =
|
||||
ESP_BLE_AUDIO_CODEC_CAP_LC3_DATA(
|
||||
ESP_BLE_AUDIO_CODEC_CAP_FREQ_ANY, /* Sampling frequency Any */
|
||||
ESP_BLE_AUDIO_CODEC_CAP_DURATION_10, /* Frame duration 10ms */
|
||||
ESP_BLE_AUDIO_CODEC_CAP_CHAN_COUNT_SUPPORT(2), /* Supported channels 2 */
|
||||
40, /* Minimum 40 octets per frame */
|
||||
120, /* Maximum 120 octets per frame */
|
||||
2); /* Maximum 2 codec frames per SDU */
|
||||
|
||||
static uint8_t codec_meta[] =
|
||||
ESP_BLE_AUDIO_CODEC_CAP_LC3_META(SINK_CONTEXT | SOURCE_CONTEXT);
|
||||
|
||||
static const esp_ble_audio_codec_cap_t codec_cap =
|
||||
ESP_BLE_AUDIO_CODEC_CAP_LC3(codec_data, codec_meta);
|
||||
|
||||
static esp_ble_audio_pacs_cap_t cap_sink = {
|
||||
.codec_cap = &codec_cap,
|
||||
};
|
||||
|
||||
static esp_ble_audio_pacs_cap_t cap_source = {
|
||||
.codec_cap = &codec_cap,
|
||||
};
|
||||
|
||||
static esp_ble_audio_bap_unicast_server_register_param_t param = {
|
||||
CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT,
|
||||
CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT
|
||||
};
|
||||
|
||||
static struct unicast_server_sink_stream {
|
||||
esp_ble_audio_bap_stream_t stream;
|
||||
|
||||
/* RX */
|
||||
example_audio_rx_metrics_t rx_metrics;
|
||||
} sink_streams[CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT];
|
||||
|
||||
static struct unicast_server_stream {
|
||||
esp_ble_audio_bap_stream_t stream;
|
||||
|
||||
/* TX */
|
||||
uint16_t seq_num;
|
||||
uint16_t max_sdu;
|
||||
uint32_t send_count;
|
||||
} source_streams[CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT];
|
||||
|
||||
static size_t configured_source_stream_count;
|
||||
|
||||
static const esp_ble_audio_bap_qos_cfg_pref_t qos_pref =
|
||||
ESP_BLE_AUDIO_BAP_QOS_CFG_PREF(true, /* Unframed PDUs supported */
|
||||
ESP_BLE_ISO_PHY_2M, /* Preferred Target PHY */
|
||||
2, /* Preferred Retransmission number */
|
||||
10, /* Preferred Maximum Transport Latency (msec) */
|
||||
20000, /* Minimum Presentation Delay (usec) */
|
||||
40000, /* Maximum Presentation Delay (usec) */
|
||||
20000, /* Preferred Minimum Presentation Delay (usec) */
|
||||
40000); /* Preferred Maximum Presentation Delay (usec) */
|
||||
|
||||
static uint8_t ext_adv_data[3 + 4 + 10 + 2 + LOCAL_DEVICE_NAME_LEN];
|
||||
|
||||
static esp_ble_audio_dir_t stream_dir(const esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
for (size_t i = 0; i < ARRAY_SIZE(source_streams); i++) {
|
||||
if (stream == &source_streams[i].stream) {
|
||||
return ESP_BLE_AUDIO_DIR_SOURCE;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(sink_streams); i++) {
|
||||
if (stream == &sink_streams[i].stream) {
|
||||
return ESP_BLE_AUDIO_DIR_SINK;
|
||||
}
|
||||
}
|
||||
|
||||
assert(0 && "Invalid stream");
|
||||
}
|
||||
|
||||
static esp_ble_audio_bap_stream_t *stream_alloc(esp_ble_audio_dir_t dir)
|
||||
{
|
||||
if (dir == ESP_BLE_AUDIO_DIR_SOURCE) {
|
||||
for (size_t i = 0; i < ARRAY_SIZE(source_streams); i++) {
|
||||
esp_ble_audio_bap_stream_t *stream = &source_streams[i].stream;
|
||||
|
||||
if (stream->conn == NULL) {
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (size_t i = 0; i < ARRAY_SIZE(sink_streams); i++) {
|
||||
esp_ble_audio_bap_stream_t *stream = &sink_streams[i].stream;
|
||||
|
||||
if (stream->conn == NULL) {
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int config_cb(esp_ble_conn_t *conn,
|
||||
const esp_ble_audio_bap_ep_t *ep,
|
||||
esp_ble_audio_dir_t dir,
|
||||
const esp_ble_audio_codec_cfg_t *codec_cfg,
|
||||
esp_ble_audio_bap_stream_t **stream,
|
||||
esp_ble_audio_bap_qos_cfg_pref_t *const pref,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
ESP_LOGI(TAG, "Config: conn %p ep %p dir %u", conn, ep, dir);
|
||||
|
||||
example_print_codec_cfg(codec_cfg);
|
||||
|
||||
*stream = stream_alloc(dir);
|
||||
if (*stream == NULL) {
|
||||
ESP_LOGI(TAG, "No streams available");
|
||||
|
||||
*rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_NO_MEM,
|
||||
ESP_BLE_AUDIO_BAP_ASCS_REASON_NONE);
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Config stream %p", *stream);
|
||||
|
||||
if (dir == ESP_BLE_AUDIO_DIR_SOURCE) {
|
||||
configured_source_stream_count++;
|
||||
}
|
||||
|
||||
*pref = qos_pref;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int reconfig_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_dir_t dir,
|
||||
const esp_ble_audio_codec_cfg_t *codec_cfg,
|
||||
esp_ble_audio_bap_qos_cfg_pref_t *const pref,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
ESP_LOGI(TAG, "Reconfig: stream %p dir %u", stream, dir);
|
||||
|
||||
example_print_codec_cfg(codec_cfg);
|
||||
|
||||
*rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_CONF_UNSUPPORTED,
|
||||
ESP_BLE_AUDIO_BAP_ASCS_REASON_NONE);
|
||||
|
||||
/* We only support one QoS at the moment, reject changes */
|
||||
return -ENOEXEC;
|
||||
}
|
||||
|
||||
static int qos_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
const esp_ble_audio_bap_qos_cfg_t *qos,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
ESP_LOGI(TAG, "QoS: stream %p qos %p", stream, qos);
|
||||
|
||||
example_print_qos(qos);
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(source_streams); i++) {
|
||||
if (stream == &source_streams[i].stream) {
|
||||
source_streams[i].max_sdu = qos->sdu;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int enable_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
const uint8_t meta[], size_t meta_len,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
esp_ble_audio_codec_cfg_frame_dur_t frame_dur;
|
||||
esp_ble_audio_codec_cfg_freq_t freq;
|
||||
uint32_t frame_dur_us;
|
||||
uint8_t frame_blocks;
|
||||
uint32_t freq_hz;
|
||||
esp_err_t err;
|
||||
|
||||
err = esp_ble_audio_codec_cfg_get_freq(stream->codec_cfg, &freq);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Error: Codec frequency not set");
|
||||
|
||||
*rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_CONF_INVALID,
|
||||
ESP_BLE_AUDIO_BAP_ASCS_REASON_CODEC_DATA);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_codec_cfg_freq_to_freq_hz(freq, &freq_hz);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get frequency hz");
|
||||
|
||||
*rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_CONF_INVALID,
|
||||
ESP_BLE_AUDIO_BAP_ASCS_REASON_CODEC_DATA);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_codec_cfg_get_frame_dur(stream->codec_cfg, &frame_dur);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Error: Frame duration not set");
|
||||
|
||||
*rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_CONF_INVALID,
|
||||
ESP_BLE_AUDIO_BAP_ASCS_REASON_CODEC_DATA);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_codec_cfg_frame_dur_to_frame_dur_us(frame_dur, &frame_dur_us);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get frame duration us");
|
||||
|
||||
*rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_CONF_INVALID,
|
||||
ESP_BLE_AUDIO_BAP_ASCS_REASON_CODEC_DATA);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_codec_cfg_get_frame_blocks_per_sdu(stream->codec_cfg, &frame_blocks, true);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Error: Frame blocks per SDU not set");
|
||||
|
||||
*rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_CONF_INVALID,
|
||||
ESP_BLE_AUDIO_BAP_ASCS_REASON_CODEC_DATA);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Enable: stream %p meta_len %u codec %u %u %u",
|
||||
stream, meta_len, freq_hz, frame_dur_us, frame_blocks);
|
||||
|
||||
*rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_SUCCESS,
|
||||
ESP_BLE_AUDIO_BAP_ASCS_REASON_CODEC_DATA);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int start_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
ESP_LOGI(TAG, "Start: stream %p", stream);
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(source_streams); i++) {
|
||||
if (stream == &source_streams[i].stream) {
|
||||
source_streams[i].seq_num = 0;
|
||||
source_streams[i].send_count = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool data_cb(uint8_t type, const uint8_t *data,
|
||||
uint8_t data_len, void *user_data)
|
||||
{
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp = user_data;
|
||||
|
||||
if (ESP_BLE_AUDIO_METADATA_TYPE_IS_KNOWN(type) == false) {
|
||||
ESP_LOGW(TAG, "Invalid metadata type %u or length %u", type, data_len);
|
||||
|
||||
*rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_METADATA_REJECTED,
|
||||
type);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static int metadata_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
const uint8_t meta[], size_t meta_len,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "Metadata: stream %p meta_len %u", stream, meta_len);
|
||||
|
||||
err = esp_ble_audio_data_parse(meta, meta_len, data_cb, rsp);
|
||||
if (err) {
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int disable_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
ESP_LOGI(TAG, "Disable: stream %p", stream);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int stop_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
ESP_LOGI(TAG, "Stop: stream %p", stream);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int release_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
ESP_LOGI(TAG, "Release: stream %p", stream);
|
||||
|
||||
if (stream_dir(stream) == ESP_BLE_AUDIO_DIR_SOURCE &&
|
||||
configured_source_stream_count > 0) {
|
||||
configured_source_stream_count--;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const esp_ble_audio_bap_unicast_server_cb_t unicast_server_cb = {
|
||||
.config = config_cb,
|
||||
.reconfig = reconfig_cb,
|
||||
.qos = qos_cb,
|
||||
.enable = enable_cb,
|
||||
.start = start_cb,
|
||||
.metadata = metadata_cb,
|
||||
.disable = disable_cb,
|
||||
.stop = stop_cb,
|
||||
.release = release_cb,
|
||||
};
|
||||
|
||||
static void stream_enabled_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "Stream %p enabled", stream);
|
||||
|
||||
/* The unicast server is responsible for starting sink ASEs
|
||||
* after the client has enabled them.
|
||||
*/
|
||||
if (stream_dir(stream) == ESP_BLE_AUDIO_DIR_SINK) {
|
||||
err = esp_ble_audio_bap_stream_start(stream);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to start stream %p, err %d", stream, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void stream_started_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
ESP_LOGI(TAG, "Stream %p started", stream);
|
||||
|
||||
if (stream_dir(stream) == ESP_BLE_AUDIO_DIR_SOURCE) {
|
||||
struct unicast_server_stream *source_stream =
|
||||
CONTAINER_OF(stream, struct unicast_server_stream, stream);
|
||||
|
||||
/* Reset TX counters */
|
||||
source_stream->seq_num = 0;
|
||||
source_stream->send_count = 0;
|
||||
} else if (stream_dir(stream) == ESP_BLE_AUDIO_DIR_SINK) {
|
||||
struct unicast_server_sink_stream *sink_stream =
|
||||
CONTAINER_OF(stream, struct unicast_server_sink_stream, stream);
|
||||
|
||||
/* Reset RX counters */
|
||||
example_audio_rx_metrics_reset(&sink_stream->rx_metrics);
|
||||
}
|
||||
}
|
||||
|
||||
static void stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
|
||||
{
|
||||
ESP_LOGI(TAG, "Stream %p stopped, reason 0x%02x", stream, reason);
|
||||
}
|
||||
|
||||
static void stream_recv_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
const esp_ble_iso_recv_info_t *info,
|
||||
const uint8_t *data, uint16_t len)
|
||||
{
|
||||
if (stream_dir(stream) == ESP_BLE_AUDIO_DIR_SINK) {
|
||||
struct unicast_server_sink_stream *sink_stream =
|
||||
CONTAINER_OF(stream, struct unicast_server_sink_stream, stream);
|
||||
|
||||
sink_stream->rx_metrics.last_sdu_len = len;
|
||||
example_audio_rx_metrics_on_recv(info, &sink_stream->rx_metrics,
|
||||
TAG, "stream", stream);
|
||||
}
|
||||
}
|
||||
|
||||
static esp_ble_audio_bap_stream_ops_t stream_ops = {
|
||||
.enabled = stream_enabled_cb,
|
||||
.started = stream_started_cb,
|
||||
.stopped = stream_stopped_cb,
|
||||
.recv = stream_recv_cb,
|
||||
};
|
||||
|
||||
static int set_pacs_location(void)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
if (IS_ENABLED(CONFIG_BT_PAC_SNK_LOC)) {
|
||||
err = esp_ble_audio_pacs_set_location(ESP_BLE_AUDIO_DIR_SINK, SINK_LOCATION);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to set sink location (err %d)", err);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (IS_ENABLED(CONFIG_BT_PAC_SRC_LOC)) {
|
||||
err = esp_ble_audio_pacs_set_location(ESP_BLE_AUDIO_DIR_SOURCE, SOURCE_LOCATION);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to set source location (err %d)", err);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Location successfully set");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int set_pacs_supported_contexts(void)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
if (IS_ENABLED(CONFIG_BT_PAC_SNK)) {
|
||||
err = esp_ble_audio_pacs_set_supported_contexts(ESP_BLE_AUDIO_DIR_SINK, SINK_CONTEXT);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to set sink supported contexts (err %d)", err);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (IS_ENABLED(CONFIG_BT_PAC_SRC)) {
|
||||
err = esp_ble_audio_pacs_set_supported_contexts(ESP_BLE_AUDIO_DIR_SOURCE, SOURCE_CONTEXT);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to set source supported contexts (err %d)", err);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Supported contexts successfully set");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int set_pacs_available_contexts(void)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
if (IS_ENABLED(CONFIG_BT_PAC_SNK)) {
|
||||
err = esp_ble_audio_pacs_set_available_contexts(ESP_BLE_AUDIO_DIR_SINK, SINK_CONTEXT);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to set sink available contexts (err %d)", err);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (IS_ENABLED(CONFIG_BT_PAC_SRC)) {
|
||||
err = esp_ble_audio_pacs_set_available_contexts(ESP_BLE_AUDIO_DIR_SOURCE, SOURCE_CONTEXT);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to set source available contexts (err %d)", err);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Available contexts successfully set");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void build_adv_data(void)
|
||||
{
|
||||
size_t idx = 0;
|
||||
|
||||
/* Flags */
|
||||
ext_adv_data[idx++] = 0x02;
|
||||
ext_adv_data[idx++] = EXAMPLE_AD_TYPE_FLAGS;
|
||||
ext_adv_data[idx++] = EXAMPLE_AD_FLAGS_GENERAL | EXAMPLE_AD_FLAGS_NO_BREDR;
|
||||
|
||||
/* Incomplete List of 16-bit Service UUIDs */
|
||||
ext_adv_data[idx++] = 0x03;
|
||||
ext_adv_data[idx++] = EXAMPLE_AD_TYPE_UUID16_SOME;
|
||||
ext_adv_data[idx++] = (ESP_BLE_AUDIO_UUID_ASCS_VAL & 0xFF);
|
||||
ext_adv_data[idx++] = ((ESP_BLE_AUDIO_UUID_ASCS_VAL >> 8) & 0xFF);
|
||||
|
||||
/* Service Data - 16-bit UUID */
|
||||
ext_adv_data[idx++] = 0x09;
|
||||
ext_adv_data[idx++] = EXAMPLE_AD_TYPE_SERVICE_DATA16;
|
||||
ext_adv_data[idx++] = (ESP_BLE_AUDIO_UUID_ASCS_VAL & 0xFF);
|
||||
ext_adv_data[idx++] = ((ESP_BLE_AUDIO_UUID_ASCS_VAL >> 8) & 0xFF);
|
||||
ext_adv_data[idx++] = ESP_BLE_AUDIO_UNICAST_ANNOUNCEMENT_TARGETED;
|
||||
ext_adv_data[idx++] = (SINK_CONTEXT & 0xFF);
|
||||
ext_adv_data[idx++] = ((SINK_CONTEXT >> 8) & 0xFF);
|
||||
ext_adv_data[idx++] = (SOURCE_CONTEXT & 0xFF);
|
||||
ext_adv_data[idx++] = ((SOURCE_CONTEXT >> 8) & 0xFF);
|
||||
ext_adv_data[idx++] = 0x00; /* Metadata length */
|
||||
|
||||
/* Complete Device Name */
|
||||
ext_adv_data[idx++] = LOCAL_DEVICE_NAME_LEN + 1;
|
||||
ext_adv_data[idx++] = EXAMPLE_AD_TYPE_NAME_COMPLETE;
|
||||
memcpy(&ext_adv_data[idx], LOCAL_DEVICE_NAME, LOCAL_DEVICE_NAME_LEN);
|
||||
}
|
||||
|
||||
static void ext_adv_start(void)
|
||||
{
|
||||
struct ble_gap_ext_adv_params ext_params = {0};
|
||||
struct os_mbuf *data = NULL;
|
||||
int err;
|
||||
|
||||
build_adv_data();
|
||||
|
||||
ext_params.connectable = 1;
|
||||
ext_params.scannable = 0;
|
||||
ext_params.legacy_pdu = 0;
|
||||
ext_params.own_addr_type = ADV_ADDRESS;
|
||||
ext_params.primary_phy = ADV_PRIMARY_PHY;
|
||||
ext_params.secondary_phy = ADV_SECONDARY_PHY;
|
||||
ext_params.tx_power = ADV_TX_POWER;
|
||||
ext_params.sid = ADV_SID;
|
||||
ext_params.itvl_min = ADV_INTERVAL;
|
||||
ext_params.itvl_max = ADV_INTERVAL;
|
||||
|
||||
err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL,
|
||||
example_audio_gap_event_cb, NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
data = os_msys_get_pkthdr(sizeof(ext_adv_data), 0);
|
||||
if (data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to get ext adv mbuf");
|
||||
return;
|
||||
}
|
||||
|
||||
err = os_mbuf_append(data, ext_adv_data, sizeof(ext_adv_data));
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err);
|
||||
os_mbuf_free_chain(data);
|
||||
return;
|
||||
}
|
||||
|
||||
err = ble_gap_ext_adv_set_data(ADV_HANDLE, data);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Extended adv instance %u started", ADV_HANDLE);
|
||||
}
|
||||
|
||||
static void acl_connect(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
if (event->acl_connect.status) {
|
||||
ESP_LOGE(TAG, "connection failed, status %d", event->acl_connect.status);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Conn established:");
|
||||
ESP_LOGI(TAG, "conn_handle 0x%04x status 0x%02x role %u peer %02x:%02x:%02x:%02x:%02x:%02x",
|
||||
event->acl_connect.conn_handle, event->acl_connect.status,
|
||||
event->acl_connect.role, event->acl_connect.dst.val[5],
|
||||
event->acl_connect.dst.val[4], event->acl_connect.dst.val[3],
|
||||
event->acl_connect.dst.val[2], event->acl_connect.dst.val[1],
|
||||
event->acl_connect.dst.val[0]);
|
||||
}
|
||||
|
||||
static void acl_disconnect(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
ESP_LOGI(TAG, "Conn terminated: conn_handle 0x%04x reason 0x%02x",
|
||||
event->acl_disconnect.conn_handle, event->acl_disconnect.reason);
|
||||
|
||||
ext_adv_start();
|
||||
}
|
||||
|
||||
static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
switch (event->type) {
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_ACL_CONNECT:
|
||||
acl_connect(event);
|
||||
break;
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_ACL_DISCONNECT:
|
||||
acl_disconnect(event);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event)
|
||||
{
|
||||
int err;
|
||||
|
||||
ESP_LOGI(TAG, "gatt mtu change, conn_handle %u, mtu %u",
|
||||
event->gatt_mtu_change.conn_handle, event->gatt_mtu_change.mtu);
|
||||
|
||||
if (event->gatt_mtu_change.mtu < ESP_BLE_AUDIO_ATT_MTU_MIN) {
|
||||
ESP_LOGW(TAG, "Invalid new mtu %u, shall be at least %u",
|
||||
event->gatt_mtu_change.mtu, ESP_BLE_AUDIO_ATT_MTU_MIN);
|
||||
return;
|
||||
}
|
||||
|
||||
/* This function only needs to be invoked when:
|
||||
* - The device is a Peripheral (Link Layer role);
|
||||
* - The device works as a GATT client.
|
||||
*/
|
||||
err = esp_ble_audio_gattc_disc_start(event->gatt_mtu_change.conn_handle);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start svc disc, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Start discovering gatt services");
|
||||
}
|
||||
|
||||
static void gattc_disc_cmpl(esp_ble_audio_gatt_app_event_t *event)
|
||||
{
|
||||
ESP_LOGI(TAG, "gattc disc cmpl, status %u, conn_handle %u",
|
||||
event->gattc_disc_cmpl.status, event->gattc_disc_cmpl.conn_handle);
|
||||
}
|
||||
|
||||
static void iso_gatt_app_cb(esp_ble_audio_gatt_app_event_t *event)
|
||||
{
|
||||
switch (event->type) {
|
||||
case ESP_BLE_AUDIO_GATT_EVENT_GATT_MTU_CHANGE:
|
||||
gatt_mtu_change(event);
|
||||
break;
|
||||
case ESP_BLE_AUDIO_GATT_EVENT_GATTC_DISC_CMPL:
|
||||
gattc_disc_cmpl(event);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
const esp_ble_audio_pacs_register_param_t pacs_param = {
|
||||
.snk_pac = true,
|
||||
.snk_loc = true,
|
||||
.src_pac = true,
|
||||
.src_loc = true,
|
||||
};
|
||||
esp_ble_audio_init_info_t info = {
|
||||
.gap_cb = iso_gap_app_cb,
|
||||
.gatt_cb = iso_gatt_app_cb,
|
||||
};
|
||||
esp_err_t err;
|
||||
|
||||
/* Initialize NVS — it is used to store PHY calibration data */
|
||||
err = nvs_flash_init();
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
err = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(err);
|
||||
|
||||
err = bluetooth_init();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize BLE, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_common_init(&info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize audio, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_pacs_register(&pacs_param);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register pacs, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_unicast_server_register(¶m);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register unicast server, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_unicast_server_register_cb(&unicast_server_cb);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register unicast server callbacks, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_pacs_cap_register(ESP_BLE_AUDIO_DIR_SINK, &cap_sink);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register pacs capabilities, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_pacs_cap_register(ESP_BLE_AUDIO_DIR_SOURCE, &cap_source);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register pacs capabilities, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(sink_streams); i++) {
|
||||
esp_ble_audio_bap_stream_cb_register(&sink_streams[i].stream, &stream_ops);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(source_streams); i++) {
|
||||
esp_ble_audio_bap_stream_cb_register(&source_streams[i].stream, &stream_ops);
|
||||
}
|
||||
|
||||
err = set_pacs_location();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = set_pacs_supported_contexts();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = set_pacs_available_contexts();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_common_start(NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start audio, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set device name, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ext_adv_start();
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# This file was generated using idf.py save-defconfig. It can be edited manually.
|
||||
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
|
||||
#
|
||||
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_BLUEDROID_ENABLED=n
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_EXT_ADV=y
|
||||
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
|
||||
CONFIG_BT_NIMBLE_MAX_CCCDS=20
|
||||
CONFIG_BT_NIMBLE_ISO=y
|
||||
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
|
||||
|
||||
CONFIG_BT_ISO_MAX_CHAN=2
|
||||
|
||||
CONFIG_BT_ASCS_MAX_ACTIVE_ASES=4
|
||||
CONFIG_BT_BAP_UNICAST_SERVER=y
|
||||
CONFIG_BT_PAC_SNK_NOTIFIABLE=y
|
||||
CONFIG_BT_PAC_SNK_LOC_WRITEABLE=y
|
||||
CONFIG_BT_PAC_SNK_LOC_NOTIFIABLE=y
|
||||
CONFIG_BT_PAC_SRC_NOTIFIABLE=y
|
||||
CONFIG_BT_PAC_SRC_LOC_WRITEABLE=y
|
||||
CONFIG_BT_PAC_SRC_LOC_NOTIFIABLE=y
|
||||
CONFIG_BT_PACS_SUPPORTED_CONTEXT_NOTIFIABLE=y
|
||||
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
@@ -0,0 +1,6 @@
|
||||
# Override some defaults so BT stack is enabled
|
||||
# by default in this example
|
||||
|
||||
CONFIG_IDF_TARGET="esp32h4"
|
||||
|
||||
CONFIG_BT_LE_ISO_SUPPORT=y
|
||||
@@ -0,0 +1,8 @@
|
||||
# The following lines of boilerplate have to be in your project's CMakeLists
|
||||
# in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
|
||||
idf_build_set_property(MINIMAL_BUILD ON)
|
||||
project(cap_acceptor)
|
||||
@@ -0,0 +1,76 @@
|
||||
| Supported Targets | ESP32-H4 |
|
||||
| ----------------- | -------- |
|
||||
|
||||
# CAP Acceptor Example
|
||||
|
||||
(See the README.md file in the upper level `examples` directory for more information about examples.)
|
||||
|
||||
This example demonstrates the **Common Audio Profile (CAP) Acceptor** functionality. It advertises so that a CAP Initiator can connect and set up available audio streams. The acceptor operates in one of two mutually exclusive modes selected at build time: as a **BAP Unicast Server** (unicast mode, for use with a CAP Initiator) or as a **BAP Broadcast Sink** (broadcast mode). In broadcast mode, it can be configured to scan for broadcast sources by itself, or to wait for a Broadcast Assistant to connect and direct the sync. Run it together with the [initiator](../initiator) example on another device as the CAP Initiator.
|
||||
|
||||
The implementation uses the NimBLE host stack with ISO and LE Audio support, ESP-BLE-AUDIO (PACS, CAP acceptor, BAP unicast server, BAP broadcast sink, BAP scan delegator). Advertising includes the CAS (Common Audio Service) UUID and service data; when unicast is enabled it also advertises ASCS with targeted announcement and sink/source contexts; when broadcast is enabled it can include BASS (Broadcast Assistant) service data. PACS advertises LC3 sink and source capabilities (e.g. 7.5 ms / 10 ms frame, stereo, multiple contexts). The example supports optional Kconfig: unicast, broadcast, self-scan for broadcast sources, device name, and (when self-scan) target broadcast device name and broadcast code.
|
||||
|
||||
## Requirements
|
||||
|
||||
* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4)
|
||||
* Optionally, another device running the [initiator](../initiator) example as CAP Initiator (for unicast and/or as Broadcast Assistant)
|
||||
|
||||
## How to Use Example
|
||||
|
||||
Before project configuration and build, set the correct chip target:
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32h4
|
||||
```
|
||||
|
||||
### Configure the Project
|
||||
|
||||
Open the configuration menu to select the mode:
|
||||
|
||||
```bash
|
||||
idf.py menuconfig
|
||||
```
|
||||
|
||||
Under **Example: CAP Acceptor**, select one of:
|
||||
|
||||
* **Unicast** — act as a BAP Unicast Server; start connectable advertising for a CAP Initiator
|
||||
* **Broadcast** — act as a BAP Broadcast Sink; receive broadcast audio. When this mode is selected, you can additionally enable:
|
||||
* **Scan for Broadcast Sources without Broadcast Assistant** — start scanning for broadcast sources independently, without waiting for a Broadcast Assistant to connect
|
||||
|
||||
### Build and Flash
|
||||
|
||||
Run the following to build, flash and monitor:
|
||||
|
||||
```bash
|
||||
idf.py -p PORT flash monitor
|
||||
```
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF.
|
||||
|
||||
## Example Flow
|
||||
|
||||
1. **Initialization**: NVS, Bluetooth stack, and LE Audio common layer (`esp_ble_audio_common_init`) with GAP and GATT callbacks. Register PACS (sink and source PAC and location), register sink and source capabilities (LC3), set PACS location and supported/available contexts (`init_cap_acceptor`). In unicast mode, initialize the CAP acceptor unicast part (BAP unicast server, stream alloc/release). In broadcast mode, initialize the CAP acceptor broadcast part (BAP broadcast sink, scan delegator). Start the audio stack and set the device name.
|
||||
2. **Scan (optional)**: In broadcast mode with self-scan enabled, start scanning for broadcast sources before or in addition to advertising.
|
||||
3. **Advertising**: Start connectable extended advertising with flags, CAS service data, and additionally ASCS service data in unicast mode or BASS service data in broadcast mode, plus device name (`cap_acceptor`).
|
||||
4. **Initiator connection (unicast mode)**: When a CAP Initiator connects, the peer connection handle is stored. On MTU exchange the acceptor may start GATT service discovery. The initiator discovers ASEs and sets up unicast streams; the acceptor allocates streams and reports stream released when appropriate.
|
||||
5. **Broadcast (broadcast mode)**: On PA sync events the acceptor syncs to the broadcast stream; on PA sync lost it handles disconnection. With self-scan, scan results are processed to sync to the chosen broadcast source.
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
I (xxx) CAP_ACC: Extended adv instance 0 started
|
||||
I (xxx) CAP_ACC: connection established, status 0
|
||||
I (xxx) CAP_ACC: gatt mtu change, conn_handle 1, mtu ...
|
||||
...
|
||||
I (xxx) CAP_ACC: PA synced, handle 0x... status 0x00
|
||||
...
|
||||
I (xxx) CAP_ACC: Sink stream released
|
||||
```
|
||||
|
||||
If the connection is lost:
|
||||
|
||||
```
|
||||
I (xxx) CAP_ACC: connection disconnected, reason 0x...
|
||||
I (xxx) CAP_ACC: PA sync lost, handle 0x... reason ...
|
||||
```
|
||||
@@ -0,0 +1,12 @@
|
||||
set(srcs "main.c")
|
||||
|
||||
if(CONFIG_EXAMPLE_UNICAST)
|
||||
list(APPEND srcs "cap_acceptor_unicast.c")
|
||||
endif()
|
||||
|
||||
if(CONFIG_EXAMPLE_BROADCAST)
|
||||
list(APPEND srcs "cap_acceptor_broadcast.c")
|
||||
endif()
|
||||
|
||||
idf_component_register(SRCS "${srcs}"
|
||||
REQUIRES bt nvs_flash)
|
||||
@@ -0,0 +1,35 @@
|
||||
# SPDX-FileCopyrightText: 2022 Nordic Semiconductor ASA
|
||||
# SPDX-FileContributor: 2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
menu "Example: CAP Acceptor"
|
||||
|
||||
choice EXAMPLE_CAP_ACCEPTOR_MODE
|
||||
prompt "CAP Acceptor mode"
|
||||
default EXAMPLE_UNICAST
|
||||
help
|
||||
Select exactly one CAP acceptor mode.
|
||||
|
||||
config EXAMPLE_UNICAST
|
||||
bool "Unicast"
|
||||
help
|
||||
If selected, the sample will start advertising connectable
|
||||
for Broadcast Assistants.
|
||||
|
||||
config EXAMPLE_BROADCAST
|
||||
bool "Broadcast"
|
||||
help
|
||||
If selected, the sample will start advertising syncable
|
||||
audio streams.
|
||||
|
||||
endchoice
|
||||
|
||||
if EXAMPLE_BROADCAST
|
||||
config EXAMPLE_SCAN_SELF
|
||||
bool "Scan for Broadcast Sources without Broadcast Assistant"
|
||||
help
|
||||
If set to true, the sample will start scanning for Broadcast
|
||||
Sources without waiting for a Broadcast Assistant to connect.
|
||||
endif
|
||||
|
||||
endmenu
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#include "host/ble_hs.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
|
||||
#include "esp_ble_audio_lc3_defs.h"
|
||||
#include "esp_ble_audio_cap_api.h"
|
||||
#include "esp_ble_audio_pacs_api.h"
|
||||
|
||||
#include "ble_audio_example_init.h"
|
||||
#include "ble_audio_example_utils.h"
|
||||
|
||||
#define TAG "CAP_ACC"
|
||||
|
||||
#define CONN_HANDLE_INIT 0xFFFF
|
||||
|
||||
#define SINK_CONTEXT (ESP_BLE_AUDIO_CONTEXT_TYPE_UNSPECIFIED | \
|
||||
ESP_BLE_AUDIO_CONTEXT_TYPE_CONVERSATIONAL | \
|
||||
ESP_BLE_AUDIO_CONTEXT_TYPE_MEDIA | \
|
||||
ESP_BLE_AUDIO_CONTEXT_TYPE_GAME | \
|
||||
ESP_BLE_AUDIO_CONTEXT_TYPE_INSTRUCTIONAL)
|
||||
#define SOURCE_CONTEXT (ESP_BLE_AUDIO_CONTEXT_TYPE_UNSPECIFIED | \
|
||||
ESP_BLE_AUDIO_CONTEXT_TYPE_CONVERSATIONAL | \
|
||||
ESP_BLE_AUDIO_CONTEXT_TYPE_MEDIA | \
|
||||
ESP_BLE_AUDIO_CONTEXT_TYPE_GAME)
|
||||
|
||||
struct peer_config {
|
||||
esp_ble_audio_cap_stream_t source_stream;
|
||||
esp_ble_audio_cap_stream_t sink_stream;
|
||||
uint16_t conn_handle;
|
||||
};
|
||||
|
||||
int cap_acceptor_unicast_init(struct peer_config *peer);
|
||||
|
||||
int cap_acceptor_broadcast_init(void);
|
||||
|
||||
esp_ble_audio_cap_stream_t *stream_alloc(esp_ble_audio_dir_t dir);
|
||||
|
||||
void stream_released(const esp_ble_audio_cap_stream_t *cap_stream);
|
||||
|
||||
#if CONFIG_EXAMPLE_BROADCAST
|
||||
#if CONFIG_EXAMPLE_SCAN_SELF
|
||||
int check_start_scan(void);
|
||||
|
||||
void broadcast_scan_recv(esp_ble_audio_gap_app_event_t *event);
|
||||
#endif /* CONFIG_EXAMPLE_SCAN_SELF */
|
||||
|
||||
void broadcast_pa_synced(esp_ble_audio_gap_app_event_t *event);
|
||||
|
||||
void broadcast_pa_lost(esp_ble_audio_gap_app_event_t *event);
|
||||
#endif /* CONFIG_EXAMPLE_BROADCAST */
|
||||
@@ -0,0 +1,707 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "cap_acceptor.h"
|
||||
|
||||
#if CONFIG_EXAMPLE_SCAN_SELF
|
||||
#define TARGET_DEVICE_NAME "CAP Broadcast Source"
|
||||
#define TARGET_DEVICE_NAME_LEN (sizeof(TARGET_DEVICE_NAME) - 1)
|
||||
#define TARGET_BROADCAST_CODE "1234"
|
||||
|
||||
#define SCAN_INTERVAL 160 /* 100ms */
|
||||
#define SCAN_WINDOW 160 /* 100ms */
|
||||
#endif /* CONFIG_EXAMPLE_SCAN_SELF */
|
||||
|
||||
#define PA_SYNC_SKIP 0
|
||||
#define PA_SYNC_TIMEOUT 1000 /* 1000 * 10ms = 10s */
|
||||
#define PA_SYNC_HANDLE_INIT UINT16_MAX
|
||||
|
||||
enum broadcast_flag {
|
||||
FLAG_BROADCAST_SYNC_REQUESTED,
|
||||
FLAG_BROADCAST_CODE_REQUIRED,
|
||||
FLAG_BROADCAST_CODE_RECEIVED,
|
||||
FLAG_BROADCAST_SYNCABLE,
|
||||
FLAG_BROADCAST_SYNCING,
|
||||
FLAG_BROADCAST_SYNCED,
|
||||
FLAG_BASE_RECEIVED,
|
||||
FLAG_PA_SYNCING,
|
||||
FLAG_PA_SYNCED,
|
||||
FLAG_SCANNING,
|
||||
FLAG_NUM,
|
||||
};
|
||||
ATOMIC_DEFINE(flags, FLAG_NUM);
|
||||
|
||||
static struct broadcast_sink {
|
||||
const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state;
|
||||
uint8_t broadcast_code[ESP_BLE_ISO_BROADCAST_CODE_SIZE];
|
||||
esp_ble_audio_bap_broadcast_sink_t *sink;
|
||||
esp_ble_audio_cap_stream_t cap_stream;
|
||||
uint8_t received_base[UINT8_MAX];
|
||||
uint32_t requested_bis_sync;
|
||||
uint32_t broadcast_id;
|
||||
uint16_t sync_handle;
|
||||
} broadcast_sink = {
|
||||
.sync_handle = PA_SYNC_HANDLE_INIT,
|
||||
};
|
||||
|
||||
static example_audio_rx_metrics_t rx_metrics;
|
||||
|
||||
static int pa_sync_create(const bt_addr_le_t *addr, uint8_t adv_sid)
|
||||
{
|
||||
struct ble_gap_periodic_sync_params params = {0};
|
||||
ble_addr_t sync_addr = {0};
|
||||
|
||||
sync_addr.type = addr->type;
|
||||
memcpy(sync_addr.val, addr->a.val, sizeof(sync_addr.val));
|
||||
params.skip = PA_SYNC_SKIP;
|
||||
params.sync_timeout = PA_SYNC_TIMEOUT;
|
||||
|
||||
return ble_gap_periodic_adv_sync_create(&sync_addr, adv_sid, ¶ms,
|
||||
example_audio_gap_event_cb, NULL);
|
||||
}
|
||||
|
||||
static int pa_sync_terminate(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = ble_gap_periodic_adv_sync_terminate(broadcast_sink.sync_handle);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to terminate PA sync, err %d", err);
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
#if CONFIG_EXAMPLE_SCAN_SELF
|
||||
static int ext_scan_start(void)
|
||||
{
|
||||
struct ble_gap_disc_params params = {0};
|
||||
uint8_t own_addr_type;
|
||||
int err;
|
||||
|
||||
err = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
params.passive = 1;
|
||||
params.itvl = SCAN_INTERVAL;
|
||||
params.window = SCAN_WINDOW;
|
||||
|
||||
err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms,
|
||||
example_audio_gap_event_cb, NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start scanning, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Extended scan started");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int ext_scan_stop(void)
|
||||
{
|
||||
return ble_gap_disc_cancel();
|
||||
}
|
||||
|
||||
int check_start_scan(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
if (atomic_test_bit(flags, FLAG_SCANNING)) {
|
||||
ESP_LOGW(TAG, "FLAG_SCANNING");
|
||||
return -EALREADY;
|
||||
}
|
||||
|
||||
if (atomic_test_bit(flags, FLAG_PA_SYNCED)) {
|
||||
ESP_LOGW(TAG, "FLAG_PA_SYNCED");
|
||||
return -EALREADY;
|
||||
}
|
||||
|
||||
if (atomic_test_bit(flags, FLAG_BROADCAST_SYNCED)) {
|
||||
ESP_LOGW(TAG, "FLAG_BROADCAST_SYNCED");
|
||||
return -EALREADY;
|
||||
}
|
||||
|
||||
err = ext_scan_start();
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
|
||||
atomic_set_bit(flags, FLAG_SCANNING);
|
||||
|
||||
return 0;
|
||||
}
|
||||
#endif /* CONFIG_EXAMPLE_SCAN_SELF */
|
||||
|
||||
static void broadcast_stream_started_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
ESP_LOGI(TAG, "Broadcast stream %p started", stream);
|
||||
|
||||
example_audio_rx_metrics_reset(&rx_metrics);
|
||||
|
||||
atomic_clear_bit(flags, FLAG_BROADCAST_SYNCING);
|
||||
atomic_set_bit(flags, FLAG_BROADCAST_SYNCED);
|
||||
}
|
||||
|
||||
static void broadcast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
|
||||
{
|
||||
ESP_LOGI(TAG, "Broadcast stream %p stopped, reason 0x%02x", stream, reason);
|
||||
|
||||
atomic_clear_bit(flags, FLAG_BROADCAST_SYNCING);
|
||||
atomic_clear_bit(flags, FLAG_BROADCAST_SYNCED);
|
||||
|
||||
#if CONFIG_EXAMPLE_SCAN_SELF
|
||||
check_start_scan();
|
||||
#endif /* CONFIG_EXAMPLE_SCAN_SELF */
|
||||
}
|
||||
|
||||
static void broadcast_stream_recv_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
const esp_ble_iso_recv_info_t *info,
|
||||
const uint8_t *data, uint16_t len)
|
||||
{
|
||||
|
||||
rx_metrics.last_sdu_len = len;
|
||||
example_audio_rx_metrics_on_recv(info, &rx_metrics, TAG, "stream", stream);
|
||||
}
|
||||
|
||||
static int create_broadcast_sink(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
if (broadcast_sink.sink) {
|
||||
return -EALREADY;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Creating broadcast sink for broadcast ID 0x%06X",
|
||||
broadcast_sink.broadcast_id);
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_sink_create(broadcast_sink.sync_handle,
|
||||
broadcast_sink.broadcast_id,
|
||||
&broadcast_sink.sink);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to create broadcast sink, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void check_sync_broadcast(void)
|
||||
{
|
||||
esp_ble_audio_bap_stream_t *sync_stream = &broadcast_sink.cap_stream.bap_stream;
|
||||
uint32_t sync_bitfield;
|
||||
int err;
|
||||
|
||||
if (atomic_test_bit(flags, FLAG_BASE_RECEIVED) == false) {
|
||||
ESP_LOGI(TAG, "FLAG_BASE_RECEIVED");
|
||||
return;
|
||||
}
|
||||
|
||||
if (atomic_test_bit(flags, FLAG_BROADCAST_SYNCABLE) == false) {
|
||||
ESP_LOGI(TAG, "FLAG_BROADCAST_SYNCABLE");
|
||||
return;
|
||||
}
|
||||
|
||||
if (atomic_test_bit(flags, FLAG_BROADCAST_CODE_REQUIRED) &&
|
||||
atomic_test_bit(flags, FLAG_BROADCAST_CODE_RECEIVED) == false) {
|
||||
ESP_LOGI(TAG, "FLAG_BROADCAST_CODE_REQUIRED");
|
||||
return;
|
||||
}
|
||||
|
||||
if (atomic_test_bit(flags, FLAG_BROADCAST_SYNC_REQUESTED) == false) {
|
||||
ESP_LOGI(TAG, "FLAG_BROADCAST_SYNC_REQUESTED");
|
||||
return;
|
||||
}
|
||||
|
||||
if (atomic_test_bit(flags, FLAG_PA_SYNCED) == false) {
|
||||
ESP_LOGI(TAG, "FLAG_PA_SYNCED");
|
||||
return;
|
||||
}
|
||||
|
||||
if (atomic_test_bit(flags, FLAG_BROADCAST_SYNCED) ||
|
||||
atomic_test_bit(flags, FLAG_BROADCAST_SYNCING)) {
|
||||
ESP_LOGI(TAG, "FLAG_BROADCAST_SYNCED");
|
||||
return;
|
||||
}
|
||||
|
||||
if (broadcast_sink.requested_bis_sync == ESP_BLE_AUDIO_BAP_BIS_SYNC_NO_PREF) {
|
||||
uint32_t base_bis;
|
||||
|
||||
/* Get the first BIS index from the BASE */
|
||||
err = esp_ble_audio_bap_base_get_bis_indexes(
|
||||
(esp_ble_audio_bap_base_t *)broadcast_sink.received_base, &base_bis);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get BIS indexes from BASE, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
sync_bitfield = 0;
|
||||
|
||||
for (uint8_t i = ESP_BLE_ISO_BIS_INDEX_MIN; i <= ESP_BLE_ISO_BIS_INDEX_MAX; i++) {
|
||||
if (base_bis & ESP_BLE_ISO_BIS_INDEX_BIT(i)) {
|
||||
sync_bitfield = ESP_BLE_ISO_BIS_INDEX_BIT(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (sync_bitfield == 0) {
|
||||
ESP_LOGE(TAG, "No valid BIS index found in BASE");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
sync_bitfield = broadcast_sink.requested_bis_sync;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Syncing to broadcast with bitfield 0x%08X", sync_bitfield);
|
||||
|
||||
/* Sync the BIG */
|
||||
err = esp_ble_audio_bap_broadcast_sink_sync(broadcast_sink.sink,
|
||||
sync_bitfield, &sync_stream,
|
||||
broadcast_sink.broadcast_code);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to sync the broadcast sink, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
atomic_set_bit(flags, FLAG_BROADCAST_SYNCING);
|
||||
}
|
||||
|
||||
static void base_recv_cb(esp_ble_audio_bap_broadcast_sink_t *sink,
|
||||
const esp_ble_audio_bap_base_t *base,
|
||||
size_t base_size)
|
||||
{
|
||||
if (base_size > sizeof(broadcast_sink.received_base)) {
|
||||
ESP_LOGE(TAG, "Too large BASE (%u > %u)",
|
||||
base_size, sizeof(broadcast_sink.received_base));
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(broadcast_sink.received_base, base, base_size);
|
||||
|
||||
if (!atomic_test_and_set_bit(flags, FLAG_BASE_RECEIVED)) {
|
||||
ESP_LOGI(TAG, "BASE received");
|
||||
|
||||
check_sync_broadcast();
|
||||
}
|
||||
}
|
||||
|
||||
static void syncable_cb(esp_ble_audio_bap_broadcast_sink_t *sink,
|
||||
const esp_ble_iso_biginfo_t *biginfo)
|
||||
{
|
||||
if (biginfo->encryption == false) {
|
||||
atomic_clear_bit(flags, FLAG_BROADCAST_CODE_REQUIRED);
|
||||
} else {
|
||||
atomic_set_bit(flags, FLAG_BROADCAST_CODE_REQUIRED);
|
||||
|
||||
#if CONFIG_EXAMPLE_SCAN_SELF
|
||||
/* If self-scanning is enabled, local broadcast code will be used */
|
||||
memset(broadcast_sink.broadcast_code, 0, ESP_BLE_ISO_BROADCAST_CODE_SIZE);
|
||||
memcpy(broadcast_sink.broadcast_code, TARGET_BROADCAST_CODE,
|
||||
MIN(ESP_BLE_ISO_BROADCAST_CODE_SIZE, strlen(TARGET_BROADCAST_CODE)));
|
||||
|
||||
atomic_set_bit(flags, FLAG_BROADCAST_CODE_RECEIVED);
|
||||
#endif /* CONFIG_EXAMPLE_SCAN_SELF */
|
||||
}
|
||||
|
||||
if (!atomic_test_and_set_bit(flags, FLAG_BROADCAST_SYNCABLE)) {
|
||||
ESP_LOGI(TAG, "BIGInfo received");
|
||||
|
||||
check_sync_broadcast();
|
||||
}
|
||||
}
|
||||
|
||||
static int pa_sync_without_past(const bt_addr_le_t *addr, uint8_t adv_sid)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = pa_sync_create(addr, adv_sid);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to create PA sync without past, err %d", err);
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
static void recv_state_updated_cb(esp_ble_conn_t *conn,
|
||||
const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state)
|
||||
{
|
||||
ESP_LOGI(TAG, "Receive state updated, pa_sync 0x%02x encrypt 0x%02x",
|
||||
recv_state->pa_sync_state, recv_state->encrypt_state);
|
||||
|
||||
for (uint8_t i = 0; i < recv_state->num_subgroups; i++) {
|
||||
ESP_LOGI(TAG, "subgroup %d bis_sync: 0x%08x", i, recv_state->subgroups[i].bis_sync);
|
||||
}
|
||||
|
||||
if (recv_state->pa_sync_state == ESP_BLE_AUDIO_BAP_PA_STATE_SYNCED) {
|
||||
broadcast_sink.recv_state = recv_state;
|
||||
}
|
||||
}
|
||||
|
||||
static int pa_sync_req_cb(esp_ble_conn_t *conn,
|
||||
const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state,
|
||||
bool past_available, uint16_t pa_interval)
|
||||
{
|
||||
int err;
|
||||
|
||||
ESP_LOGI(TAG, "Received request to sync to PA (PAST %savailble): %u",
|
||||
past_available ? "" : "not ", recv_state->pa_sync_state);
|
||||
|
||||
broadcast_sink.recv_state = recv_state;
|
||||
|
||||
if (recv_state->pa_sync_state == ESP_BLE_AUDIO_BAP_PA_STATE_SYNCED ||
|
||||
recv_state->pa_sync_state == ESP_BLE_AUDIO_BAP_PA_STATE_INFO_REQ ||
|
||||
broadcast_sink.sync_handle != PA_SYNC_HANDLE_INIT) {
|
||||
/* Already syncing */
|
||||
ESP_LOGW(TAG, "Rejecting PA sync request");
|
||||
return -EALREADY;
|
||||
}
|
||||
|
||||
if (past_available) {
|
||||
ESP_LOGW(TAG, "Currently not support PAST");
|
||||
return -ENOTSUP;
|
||||
} else {
|
||||
err = pa_sync_without_past(&recv_state->addr, recv_state->adv_sid);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Syncing without PAST");
|
||||
}
|
||||
|
||||
broadcast_sink.broadcast_id = recv_state->broadcast_id;
|
||||
atomic_set_bit(flags, FLAG_PA_SYNCING);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int pa_sync_term_req_cb(esp_ble_conn_t *conn,
|
||||
const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state)
|
||||
{
|
||||
int err;
|
||||
|
||||
ESP_LOGI(TAG, "Received request to terminate PA sync");
|
||||
|
||||
broadcast_sink.recv_state = recv_state;
|
||||
|
||||
err = pa_sync_terminate();
|
||||
if (err) {
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
broadcast_sink.sync_handle = PA_SYNC_HANDLE_INIT;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void broadcast_code_cb(esp_ble_conn_t *conn,
|
||||
const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state,
|
||||
const uint8_t broadcast_code[ESP_BLE_ISO_BROADCAST_CODE_SIZE])
|
||||
{
|
||||
ESP_LOGI(TAG, "Broadcast code received for %p", recv_state);
|
||||
|
||||
broadcast_sink.recv_state = recv_state;
|
||||
|
||||
memcpy(broadcast_sink.broadcast_code, broadcast_code,
|
||||
ESP_BLE_ISO_BROADCAST_CODE_SIZE);
|
||||
|
||||
atomic_set_bit(flags, FLAG_BROADCAST_CODE_RECEIVED);
|
||||
}
|
||||
|
||||
static uint32_t get_req_bis_sync(const uint32_t bis_sync_req[CONFIG_BT_BAP_BASS_MAX_SUBGROUPS])
|
||||
{
|
||||
uint32_t bis_sync = 0;
|
||||
|
||||
for (size_t i = 0; i < CONFIG_BT_BAP_BASS_MAX_SUBGROUPS; i++) {
|
||||
bis_sync |= bis_sync_req[i];
|
||||
}
|
||||
|
||||
return bis_sync;
|
||||
}
|
||||
|
||||
static int bis_sync_req_cb(esp_ble_conn_t *conn,
|
||||
const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state,
|
||||
const uint32_t bis_sync_req[CONFIG_BT_BAP_BASS_MAX_SUBGROUPS])
|
||||
{
|
||||
const uint32_t new_bis_sync_req = get_req_bis_sync(bis_sync_req);
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "BIS sync request received for %p: 0x%08lx", recv_state, bis_sync_req[0]);
|
||||
|
||||
if (new_bis_sync_req != ESP_BLE_AUDIO_BAP_BIS_SYNC_NO_PREF &&
|
||||
__builtin_popcount(new_bis_sync_req) > 1) {
|
||||
ESP_LOGW(TAG, "Rejecting BIS sync request for 0x%08lx as we do not support that",
|
||||
new_bis_sync_req);
|
||||
return -ENOTSUP;
|
||||
}
|
||||
|
||||
if (broadcast_sink.requested_bis_sync == new_bis_sync_req) {
|
||||
return 0; /* no op */
|
||||
}
|
||||
|
||||
if (atomic_test_bit(flags, FLAG_BROADCAST_SYNCED)) {
|
||||
/* If the BIS sync request is received while we are already
|
||||
* synced, it means that the requested BIS sync has changed.
|
||||
*/
|
||||
|
||||
/* The stream stopped callback will be called as part of this,
|
||||
* and we do not need to wait for any events from the controller.
|
||||
* Thus, when this returns, the broadcast sink is stopped.
|
||||
*/
|
||||
err = esp_ble_audio_bap_broadcast_sink_stop(broadcast_sink.sink);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop Broadcast Sink, err %d", err);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink.sink);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to delete Broadcast Sink, err %d", err);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
broadcast_sink.sink = NULL;
|
||||
|
||||
atomic_clear_bit(flags, FLAG_BROADCAST_SYNCED);
|
||||
}
|
||||
|
||||
broadcast_sink.requested_bis_sync = new_bis_sync_req;
|
||||
|
||||
if (broadcast_sink.requested_bis_sync != 0) {
|
||||
atomic_set_bit(flags, FLAG_BROADCAST_SYNC_REQUESTED);
|
||||
|
||||
check_sync_broadcast();
|
||||
} else {
|
||||
atomic_clear_bit(flags, FLAG_BROADCAST_SYNC_REQUESTED);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if CONFIG_EXAMPLE_SCAN_SELF
|
||||
struct scan_recv_data {
|
||||
bool target_matched;
|
||||
bool broadcast_id_found;
|
||||
uint32_t broadcast_id;
|
||||
};
|
||||
|
||||
static bool data_cb(uint8_t type, const uint8_t *data,
|
||||
uint8_t data_len, void *user_data)
|
||||
{
|
||||
struct scan_recv_data *sr = user_data;
|
||||
uint16_t uuid;
|
||||
|
||||
switch (type) {
|
||||
case EXAMPLE_AD_TYPE_NAME_SHORTENED:
|
||||
case EXAMPLE_AD_TYPE_NAME_COMPLETE:
|
||||
case EXAMPLE_AD_TYPE_BROADCAST_NAME:
|
||||
sr->target_matched = (data_len == TARGET_DEVICE_NAME_LEN) &&
|
||||
!memcmp(data, TARGET_DEVICE_NAME, TARGET_DEVICE_NAME_LEN);
|
||||
if (!sr->target_matched) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
case EXAMPLE_AD_TYPE_SERVICE_DATA16:
|
||||
if (data_len < ESP_BLE_AUDIO_UUID_SIZE_16 + ESP_BLE_AUDIO_BROADCAST_ID_SIZE) {
|
||||
return true;
|
||||
}
|
||||
uuid = sys_get_le16(data);
|
||||
if (uuid != ESP_BLE_AUDIO_UUID_BROADCAST_AUDIO_VAL) {
|
||||
return true;
|
||||
}
|
||||
sr->broadcast_id = sys_get_le24(data + ESP_BLE_AUDIO_UUID_SIZE_16);
|
||||
sr->broadcast_id_found = true;
|
||||
return true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void broadcast_scan_recv(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
struct scan_recv_data sr = {0};
|
||||
bt_addr_le_t addr;
|
||||
int err;
|
||||
|
||||
/* Periodic advertising interval. 0 if no periodic advertising. */
|
||||
if (event->ext_scan_recv.per_adv_itvl == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
esp_ble_audio_data_parse(event->ext_scan_recv.data,
|
||||
event->ext_scan_recv.data_len,
|
||||
data_cb, &sr);
|
||||
|
||||
if (!sr.target_matched || !sr.broadcast_id_found) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (atomic_test_bit(flags, FLAG_PA_SYNCING) == false &&
|
||||
broadcast_sink.recv_state == NULL) {
|
||||
/* Since we are scanning ourselves, we consider this as
|
||||
* broadcast sync has been requested.
|
||||
*/
|
||||
broadcast_sink.requested_bis_sync = ESP_BLE_AUDIO_BAP_BIS_SYNC_NO_PREF;
|
||||
broadcast_sink.broadcast_id = sr.broadcast_id;
|
||||
|
||||
addr.type = event->ext_scan_recv.addr.type;
|
||||
memcpy(addr.a.val, event->ext_scan_recv.addr.val, sizeof(addr.a.val));
|
||||
|
||||
err = pa_sync_create(&addr, event->ext_scan_recv.sid);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to create PA sync, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Syncing without PAST from scan");
|
||||
|
||||
atomic_set_bit(flags, FLAG_PA_SYNCING);
|
||||
atomic_set_bit(flags, FLAG_BROADCAST_SYNC_REQUESTED);
|
||||
}
|
||||
}
|
||||
#endif /* CONFIG_EXAMPLE_SCAN_SELF */
|
||||
|
||||
void broadcast_pa_synced(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
bt_addr_le_t addr = {0};
|
||||
int err;
|
||||
|
||||
addr.type = event->pa_sync.addr.type;
|
||||
memcpy(addr.a.val, event->pa_sync.addr.val, sizeof(addr.a.val));
|
||||
|
||||
if (broadcast_sink.sync_handle == PA_SYNC_HANDLE_INIT ||
|
||||
(broadcast_sink.recv_state &&
|
||||
broadcast_sink.recv_state->addr.type == addr.type &&
|
||||
memcmp(broadcast_sink.recv_state->addr.a.val, addr.a.val, sizeof(addr.a.val)) == 0 &&
|
||||
broadcast_sink.recv_state->adv_sid == event->pa_sync.sid)) {
|
||||
ESP_LOGI(TAG, "PA sync %u synced for broadcast sink", event->pa_sync.sync_handle);
|
||||
|
||||
if (broadcast_sink.sync_handle == PA_SYNC_HANDLE_INIT) {
|
||||
broadcast_sink.sync_handle = event->pa_sync.sync_handle;
|
||||
}
|
||||
|
||||
atomic_set_bit(flags, FLAG_PA_SYNCED);
|
||||
atomic_clear_bit(flags, FLAG_PA_SYNCING);
|
||||
|
||||
#if CONFIG_EXAMPLE_SCAN_SELF
|
||||
err = ext_scan_stop();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop scanning, err %d", err);
|
||||
/* Continue anyway - scan stop failure should not block sink creation */
|
||||
} else {
|
||||
atomic_clear_bit(flags, FLAG_SCANNING);
|
||||
}
|
||||
#endif /* CONFIG_EXAMPLE_SCAN_SELF */
|
||||
|
||||
err = create_broadcast_sink();
|
||||
if (err && err != -EALREADY) {
|
||||
ESP_LOGE(TAG, "Failed to create broadcast sink, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
check_sync_broadcast();
|
||||
}
|
||||
}
|
||||
|
||||
void broadcast_pa_lost(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
if (event->pa_sync_lost.sync_handle != broadcast_sink.sync_handle) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (broadcast_sink.recv_state) {
|
||||
err = esp_ble_audio_bap_scan_delegator_set_pa_state(broadcast_sink.recv_state->src_id,
|
||||
ESP_BLE_AUDIO_BAP_PA_STATE_NOT_SYNCED);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set PA state to ESP_BLE_AUDIO_BAP_PA_STATE_NOT_SYNCED, err %d", err);
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_scan_delegator_rem_src(broadcast_sink.recv_state->src_id);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to remove receive state source, err %d", err);
|
||||
}
|
||||
|
||||
if (broadcast_sink.sink) {
|
||||
err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink.sink);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to delete broadcast sink, err %d", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
broadcast_sink.recv_state = NULL;
|
||||
broadcast_sink.sink = NULL;
|
||||
broadcast_sink.requested_bis_sync = 0;
|
||||
broadcast_sink.sync_handle = PA_SYNC_HANDLE_INIT;
|
||||
|
||||
atomic_clear_bit(flags, FLAG_BROADCAST_SYNCABLE);
|
||||
atomic_clear_bit(flags, FLAG_BROADCAST_SYNCING);
|
||||
atomic_clear_bit(flags, FLAG_BROADCAST_SYNCED);
|
||||
atomic_clear_bit(flags, FLAG_PA_SYNCED);
|
||||
atomic_clear_bit(flags, FLAG_PA_SYNCING);
|
||||
atomic_clear_bit(flags, FLAG_BASE_RECEIVED);
|
||||
atomic_clear_bit(flags, FLAG_BROADCAST_CODE_REQUIRED);
|
||||
atomic_clear_bit(flags, FLAG_BROADCAST_CODE_RECEIVED);
|
||||
atomic_clear_bit(flags, FLAG_BROADCAST_SYNC_REQUESTED);
|
||||
|
||||
#if CONFIG_EXAMPLE_SCAN_SELF
|
||||
check_start_scan();
|
||||
#endif /* CONFIG_EXAMPLE_SCAN_SELF */
|
||||
}
|
||||
|
||||
int cap_acceptor_broadcast_init(void)
|
||||
{
|
||||
static bool cbs_registered;
|
||||
int err;
|
||||
|
||||
if (cbs_registered == false) {
|
||||
static esp_ble_audio_bap_scan_delegator_cb_t scan_delegator_cbs = {
|
||||
.recv_state_updated = recv_state_updated_cb,
|
||||
.pa_sync_req = pa_sync_req_cb,
|
||||
.pa_sync_term_req = pa_sync_term_req_cb,
|
||||
.broadcast_code = broadcast_code_cb,
|
||||
.bis_sync_req = bis_sync_req_cb,
|
||||
};
|
||||
static esp_ble_audio_bap_broadcast_sink_cb_t broadcast_sink_cbs = {
|
||||
.base_recv = base_recv_cb,
|
||||
.syncable = syncable_cb,
|
||||
};
|
||||
static esp_ble_audio_bap_stream_ops_t broadcast_stream_ops = {
|
||||
.started = broadcast_stream_started_cb,
|
||||
.stopped = broadcast_stream_stopped_cb,
|
||||
.recv = broadcast_stream_recv_cb,
|
||||
};
|
||||
|
||||
err = esp_ble_audio_bap_scan_delegator_register(&scan_delegator_cbs);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register scan delegator callbacks, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_sink_register_cb(&broadcast_sink_cbs);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register broadcast sink callbacks, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_ble_audio_cap_stream_ops_register(&broadcast_sink.cap_stream,
|
||||
&broadcast_stream_ops);
|
||||
|
||||
cbs_registered = true;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021-2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileCopyrightText: 2023 NXP
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "cap_acceptor.h"
|
||||
|
||||
static const esp_ble_audio_bap_qos_cfg_pref_t qos_pref =
|
||||
ESP_BLE_AUDIO_BAP_QOS_CFG_PREF(true, /* Unframed PDUs supported */
|
||||
ESP_BLE_ISO_PHY_2M, /* Preferred Target PHY */
|
||||
2, /* Preferred Retransmission number */
|
||||
20, /* Preferred Maximum Transport Latency (msec) */
|
||||
20000, /* Minimum Presentation Delay (usec) */
|
||||
40000, /* Maximum Presentation Delay (usec) */
|
||||
20000, /* Preferred Minimum Presentation Delay (usec) */
|
||||
40000); /* Preferred Maximum Presentation Delay (usec) */
|
||||
|
||||
static example_audio_rx_metrics_t rx_metrics;
|
||||
|
||||
static example_audio_tx_scheduler_t tx_scheduler;
|
||||
static uint16_t tx_seq_num;
|
||||
static uint8_t *iso_data;
|
||||
|
||||
static void unicast_server_tx(void);
|
||||
|
||||
static void tx_scheduler_cb(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
unicast_server_tx();
|
||||
}
|
||||
|
||||
static int config_cb(esp_ble_conn_t *conn,
|
||||
const esp_ble_audio_bap_ep_t *ep,
|
||||
esp_ble_audio_dir_t dir,
|
||||
const esp_ble_audio_codec_cfg_t *codec_cfg,
|
||||
esp_ble_audio_bap_stream_t **stream,
|
||||
esp_ble_audio_bap_qos_cfg_pref_t *const pref,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
esp_ble_audio_cap_stream_t *cap_stream;
|
||||
|
||||
ESP_LOGI(TAG, "Config: conn %p ep %p dir %u", conn, ep, dir);
|
||||
|
||||
example_print_codec_cfg(codec_cfg);
|
||||
|
||||
cap_stream = stream_alloc(dir);
|
||||
if (cap_stream == NULL) {
|
||||
ESP_LOGE(TAG, "No streams available");
|
||||
|
||||
*rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_NO_MEM,
|
||||
ESP_BLE_AUDIO_BAP_ASCS_REASON_NONE);
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
*stream = &cap_stream->bap_stream;
|
||||
|
||||
ESP_LOGI(TAG, "Config stream %p", *stream);
|
||||
|
||||
*pref = qos_pref;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int reconfig_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_dir_t dir,
|
||||
const esp_ble_audio_codec_cfg_t *codec_cfg,
|
||||
esp_ble_audio_bap_qos_cfg_pref_t *const pref,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
ESP_LOGI(TAG, "Reconfig: stream %p dir %u", stream, dir);
|
||||
|
||||
example_print_codec_cfg(codec_cfg);
|
||||
|
||||
*pref = qos_pref;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int qos_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
const esp_ble_audio_bap_qos_cfg_t *qos,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
ESP_LOGI(TAG, "QoS: stream %p qos %p", stream, qos);
|
||||
|
||||
example_print_qos(qos);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int enable_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
const uint8_t meta[], size_t meta_len,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
ESP_LOGI(TAG, "Enable: stream %p meta_len %u", stream, meta_len);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int start_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
ESP_LOGI(TAG, "Start: stream %p", stream);
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct data_func_param {
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp;
|
||||
bool stream_context_present;
|
||||
};
|
||||
|
||||
static bool data_func_cb(uint8_t type, const uint8_t *data,
|
||||
uint8_t data_len, void *user_data)
|
||||
{
|
||||
struct data_func_param *func_param = (struct data_func_param *)user_data;
|
||||
|
||||
if (type == ESP_BLE_AUDIO_METADATA_TYPE_STREAM_CONTEXT) {
|
||||
func_param->stream_context_present = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static int metadata_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
const uint8_t meta[], size_t meta_len,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
struct data_func_param func_param = {
|
||||
.rsp = rsp,
|
||||
.stream_context_present = false,
|
||||
};
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "Metadata: stream %p meta_len %u", stream, meta_len);
|
||||
|
||||
err = esp_ble_audio_data_parse(meta, meta_len, data_func_cb, &func_param);
|
||||
if (err) {
|
||||
*rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_METADATA_REJECTED,
|
||||
ESP_BLE_AUDIO_BAP_ASCS_REASON_NONE);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
if (func_param.stream_context_present == false) {
|
||||
ESP_LOGE(TAG, "Stream audio context not present");
|
||||
|
||||
*rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_METADATA_REJECTED,
|
||||
ESP_BLE_AUDIO_BAP_ASCS_REASON_NONE);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int disable_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
ESP_LOGI(TAG, "Disable: stream %p", stream);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int stop_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
ESP_LOGI(TAG, "Stop: stream %p", stream);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int release_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
ESP_LOGI(TAG, "Release: stream %p", stream);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const esp_ble_audio_bap_unicast_server_cb_t unicast_server_cb = {
|
||||
.config = config_cb,
|
||||
.reconfig = reconfig_cb,
|
||||
.qos = qos_cb,
|
||||
.enable = enable_cb,
|
||||
.start = start_cb,
|
||||
.metadata = metadata_cb,
|
||||
.disable = disable_cb,
|
||||
.stop = stop_cb,
|
||||
.release = release_cb,
|
||||
};
|
||||
|
||||
static void unicast_stream_configured_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
const esp_ble_audio_bap_qos_cfg_pref_t *pref)
|
||||
{
|
||||
ESP_LOGI(TAG, "Unicast stream %p configured", stream);
|
||||
|
||||
example_print_qos_pref(pref);
|
||||
}
|
||||
|
||||
static void unicast_stream_qos_set_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
ESP_LOGI(TAG, "Unicast stream %p QoS set", stream);
|
||||
}
|
||||
|
||||
static void unicast_stream_enabled_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
esp_ble_audio_bap_ep_info_t ep_info = {0};
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "Unicast stream %p enabled", stream);
|
||||
|
||||
err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get ep info, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "id 0x%02x dir 0x%02x can_send %u can_recv %u",
|
||||
ep_info.id, ep_info.dir, ep_info.can_send, ep_info.can_recv);
|
||||
|
||||
if (ep_info.dir == ESP_BLE_AUDIO_DIR_SINK) {
|
||||
/* Automatically do the receiver start ready operation */
|
||||
err = esp_ble_audio_bap_stream_start(stream);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start stream, err %d", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
esp_ble_audio_bap_ep_info_t ep_info = {0};
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "Unicast stream %p started", stream);
|
||||
|
||||
example_audio_rx_metrics_reset(&rx_metrics);
|
||||
|
||||
err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get ep info, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "id 0x%02x dir 0x%02x can_send %u can_recv %u",
|
||||
ep_info.id, ep_info.dir, ep_info.can_send, ep_info.can_recv);
|
||||
|
||||
if (ep_info.dir == ESP_BLE_AUDIO_DIR_SOURCE) {
|
||||
if (stream->qos == NULL || stream->qos->sdu == 0) {
|
||||
ESP_LOGE(TAG, "Invalid stream qos");
|
||||
return;
|
||||
}
|
||||
|
||||
if (iso_data == NULL) {
|
||||
iso_data = calloc(1, stream->qos->sdu);
|
||||
if (iso_data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to alloc TX buffer, SDU %u", stream->qos->sdu);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
tx_seq_num = 0;
|
||||
example_audio_tx_scheduler_reset(&tx_scheduler);
|
||||
|
||||
err = example_audio_tx_scheduler_start(&tx_scheduler, stream->qos->interval);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start tx scheduler, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
unicast_server_tx();
|
||||
}
|
||||
}
|
||||
|
||||
static void unicast_stream_metadata_updated_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
ESP_LOGI(TAG, "Unicast stream %p metadata updated", stream);
|
||||
}
|
||||
|
||||
static void unicast_stream_disabled_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
ESP_LOGI(TAG, "Unicast stream %p disabled", stream);
|
||||
}
|
||||
|
||||
static void unicast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
|
||||
{
|
||||
esp_ble_audio_bap_ep_info_t ep_info = {0};
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "Unicast stream %p stopped, reason 0x%02x", stream, reason);
|
||||
|
||||
err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get ep info, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "id 0x%02x dir 0x%02x can_send %u can_recv %u",
|
||||
ep_info.id, ep_info.dir, ep_info.can_send, ep_info.can_recv);
|
||||
|
||||
if (ep_info.dir == ESP_BLE_AUDIO_DIR_SOURCE) {
|
||||
err = example_audio_tx_scheduler_stop(&tx_scheduler);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err);
|
||||
}
|
||||
|
||||
if (iso_data != NULL) {
|
||||
free(iso_data);
|
||||
iso_data = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void unicast_stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
|
||||
{
|
||||
esp_ble_audio_bap_ep_info_t ep_info = {0};
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "Unicast stream %p disconnected, reason 0x%02x", stream, reason);
|
||||
|
||||
err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get ep info, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ep_info.dir == ESP_BLE_AUDIO_DIR_SOURCE) {
|
||||
err = example_audio_tx_scheduler_stop(&tx_scheduler);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err);
|
||||
}
|
||||
|
||||
if (iso_data != NULL) {
|
||||
free(iso_data);
|
||||
iso_data = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void unicast_stream_released_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
esp_ble_audio_cap_stream_t *cap_stream = CONTAINER_OF(stream,
|
||||
esp_ble_audio_cap_stream_t,
|
||||
bap_stream);
|
||||
|
||||
ESP_LOGI(TAG, "Unicast stream %p released", stream);
|
||||
|
||||
stream_released(cap_stream);
|
||||
}
|
||||
|
||||
static void unicast_stream_recv_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
const esp_ble_iso_recv_info_t *info,
|
||||
const uint8_t *data, uint16_t len)
|
||||
{
|
||||
rx_metrics.last_sdu_len = len;
|
||||
example_audio_rx_metrics_on_recv(info, &rx_metrics, TAG, "stream", stream);
|
||||
}
|
||||
|
||||
static void unicast_stream_sent_cb(esp_ble_audio_bap_stream_t *stream, void *user_data)
|
||||
{
|
||||
example_audio_tx_scheduler_on_sent(&tx_scheduler, user_data, TAG, "stream", stream);
|
||||
}
|
||||
|
||||
static esp_ble_audio_bap_stream_ops_t 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,
|
||||
.sent = unicast_stream_sent_cb,
|
||||
.disconnected = unicast_stream_disconnected_cb,
|
||||
};
|
||||
|
||||
static void unicast_server_tx(void)
|
||||
{
|
||||
esp_ble_audio_bap_ep_info_t ep_info = {0};
|
||||
esp_ble_audio_cap_stream_t *cap_stream;
|
||||
esp_ble_audio_bap_stream_t *bap_stream;
|
||||
esp_err_t err;
|
||||
|
||||
cap_stream = stream_alloc(ESP_BLE_AUDIO_DIR_SOURCE);
|
||||
assert(cap_stream);
|
||||
bap_stream = &cap_stream->bap_stream;
|
||||
|
||||
if (bap_stream->ep == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_ep_get_info(bap_stream->ep, &ep_info);
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ep_info.state != ESP_BLE_AUDIO_BAP_EP_STATE_STREAMING) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bap_stream->qos == NULL || bap_stream->qos->sdu == 0) {
|
||||
ESP_LOGE(TAG, "Invalid stream qos");
|
||||
return;
|
||||
}
|
||||
|
||||
if (iso_data == NULL) {
|
||||
ESP_LOGE(TAG, "TX buffer unavailable, SDU %u", bap_stream->qos->sdu);
|
||||
return;
|
||||
}
|
||||
|
||||
memset(iso_data, (uint8_t)tx_seq_num, bap_stream->qos->sdu);
|
||||
|
||||
err = esp_ble_audio_cap_stream_send(cap_stream, iso_data,
|
||||
bap_stream->qos->sdu,
|
||||
tx_seq_num);
|
||||
if (err) {
|
||||
ESP_LOGD(TAG, "Failed to transmit data on streams, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
tx_seq_num++;
|
||||
}
|
||||
|
||||
int cap_acceptor_unicast_init(struct peer_config *peer)
|
||||
{
|
||||
static bool cbs_registered;
|
||||
esp_err_t err;
|
||||
|
||||
if (cbs_registered == false) {
|
||||
esp_ble_audio_bap_unicast_server_register_param_t param = {
|
||||
CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT,
|
||||
CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT
|
||||
};
|
||||
|
||||
err = esp_ble_audio_bap_unicast_server_register(¶m);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register BAP unicast server, err %d", err);
|
||||
return -1;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_unicast_server_register_cb(&unicast_server_cb);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register BAP unicast server callbacks, err %d", err);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cbs_registered = true;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_cap_stream_ops_register(&peer->source_stream, &unicast_stream_ops);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register source stream ops, err %d", err);
|
||||
return -1;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_cap_stream_ops_register(&peer->sink_stream, &unicast_stream_ops);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register sink stream ops, err %d", err);
|
||||
return -1;
|
||||
}
|
||||
|
||||
err = example_audio_tx_scheduler_init(&tx_scheduler,
|
||||
tx_scheduler_cb,
|
||||
NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize tx scheduler, err %d", err);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
dependencies:
|
||||
example_init:
|
||||
path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_init
|
||||
example_utils:
|
||||
path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_utils
|
||||
@@ -0,0 +1,429 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021-2022 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_timer.h"
|
||||
|
||||
#include "cap_acceptor.h"
|
||||
|
||||
#define ADV_HANDLE 0x00
|
||||
#define ADV_SID 0
|
||||
#define ADV_TX_POWER 127
|
||||
#define ADV_ADDRESS BLE_OWN_ADDR_PUBLIC
|
||||
#define ADV_PRIMARY_PHY BLE_HCI_LE_PHY_1M
|
||||
#define ADV_SECONDARY_PHY BLE_HCI_LE_PHY_2M
|
||||
#define ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(200)
|
||||
|
||||
static uint8_t codec_data[] =
|
||||
ESP_BLE_AUDIO_CODEC_CAP_LC3_DATA(
|
||||
ESP_BLE_AUDIO_CODEC_CAP_FREQ_ANY, /* Sampling frequency Any */
|
||||
ESP_BLE_AUDIO_CODEC_CAP_DURATION_7_5 | \
|
||||
ESP_BLE_AUDIO_CODEC_CAP_DURATION_10, /* Frame duration 7.5ms/10ms */
|
||||
ESP_BLE_AUDIO_CODEC_CAP_CHAN_COUNT_SUPPORT(2), /* Supported channels 2 */
|
||||
30, /* Minimum 30 octets per frame */
|
||||
155, /* Maximum 155 octets per frame */
|
||||
2); /* Maximum 2 codec frames per SDU */
|
||||
|
||||
static uint8_t codec_meta[] =
|
||||
ESP_BLE_AUDIO_CODEC_CAP_LC3_META(SINK_CONTEXT | SOURCE_CONTEXT);
|
||||
|
||||
static const esp_ble_audio_codec_cap_t codec_cap =
|
||||
ESP_BLE_AUDIO_CODEC_CAP_LC3(codec_data, codec_meta);
|
||||
|
||||
static esp_ble_audio_pacs_cap_t sink_cap = {
|
||||
.codec_cap = &codec_cap,
|
||||
};
|
||||
|
||||
static esp_ble_audio_pacs_cap_t source_cap = {
|
||||
.codec_cap = &codec_cap,
|
||||
};
|
||||
|
||||
static struct peer_config peer = {
|
||||
.conn_handle = CONN_HANDLE_INIT,
|
||||
};
|
||||
|
||||
static uint8_t ext_adv_data[] = {
|
||||
/* Flags */
|
||||
0x02, EXAMPLE_AD_TYPE_FLAGS, (EXAMPLE_AD_FLAGS_GENERAL | EXAMPLE_AD_FLAGS_NO_BREDR),
|
||||
/* Incomplete List of 16-bit Service UUIDs */
|
||||
0x05, EXAMPLE_AD_TYPE_UUID16_SOME, (ESP_BLE_AUDIO_UUID_ASCS_VAL & 0xFF),
|
||||
((ESP_BLE_AUDIO_UUID_ASCS_VAL >> 8) & 0xFF),
|
||||
(ESP_BLE_AUDIO_UUID_CAS_VAL & 0xFF),
|
||||
((ESP_BLE_AUDIO_UUID_CAS_VAL >> 8) & 0xFF),
|
||||
/* Service Data - 16-bit UUID */
|
||||
0x04, EXAMPLE_AD_TYPE_SERVICE_DATA16, (ESP_BLE_AUDIO_UUID_CAS_VAL & 0xFF),
|
||||
((ESP_BLE_AUDIO_UUID_CAS_VAL >> 8) & 0xFF),
|
||||
ESP_BLE_AUDIO_UNICAST_ANNOUNCEMENT_TARGETED,
|
||||
#if CONFIG_EXAMPLE_UNICAST
|
||||
/* Service Data - 16-bit UUID */
|
||||
0x09, EXAMPLE_AD_TYPE_SERVICE_DATA16, (ESP_BLE_AUDIO_UUID_ASCS_VAL & 0xFF),
|
||||
((ESP_BLE_AUDIO_UUID_ASCS_VAL >> 8) & 0xFF),
|
||||
ESP_BLE_AUDIO_UNICAST_ANNOUNCEMENT_TARGETED,
|
||||
(SINK_CONTEXT & 0xFF),
|
||||
((SINK_CONTEXT >> 8) & 0xFF),
|
||||
(SOURCE_CONTEXT & 0xFF),
|
||||
((SOURCE_CONTEXT >> 8) & 0xFF),
|
||||
0x00, /* Metadata length */
|
||||
#endif /* CONFIG_EXAMPLE_UNICAST */
|
||||
#if CONFIG_EXAMPLE_BROADCAST
|
||||
/* Service Data - 16-bit UUID */
|
||||
0x03, EXAMPLE_AD_TYPE_SERVICE_DATA16, (ESP_BLE_AUDIO_UUID_BASS_VAL & 0xFF),
|
||||
((ESP_BLE_AUDIO_UUID_BASS_VAL >> 8) & 0xFF),
|
||||
#endif /* CONFIG_EXAMPLE_BROADCAST */
|
||||
/* Complete Device Name */
|
||||
0x0d, EXAMPLE_AD_TYPE_NAME_COMPLETE, 'c', 'a', 'p', '_', 'a', 'c', 'c', 'e', 'p', 't', 'o', 'r',
|
||||
};
|
||||
|
||||
static void ext_adv_start(void)
|
||||
{
|
||||
struct ble_gap_ext_adv_params ext_params = {0};
|
||||
struct os_mbuf *data = NULL;
|
||||
int err;
|
||||
|
||||
ext_params.connectable = 1;
|
||||
ext_params.scannable = 0;
|
||||
ext_params.legacy_pdu = 0;
|
||||
ext_params.own_addr_type = ADV_ADDRESS;
|
||||
ext_params.primary_phy = ADV_PRIMARY_PHY;
|
||||
ext_params.secondary_phy = ADV_SECONDARY_PHY;
|
||||
ext_params.tx_power = ADV_TX_POWER;
|
||||
ext_params.sid = ADV_SID;
|
||||
ext_params.itvl_min = ADV_INTERVAL;
|
||||
ext_params.itvl_max = ADV_INTERVAL;
|
||||
|
||||
err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL,
|
||||
example_audio_gap_event_cb, NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
data = os_msys_get_pkthdr(sizeof(ext_adv_data), 0);
|
||||
if (data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to get ext adv mbuf");
|
||||
return;
|
||||
}
|
||||
|
||||
err = os_mbuf_append(data, ext_adv_data, sizeof(ext_adv_data));
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err);
|
||||
os_mbuf_free_chain(data);
|
||||
return;
|
||||
}
|
||||
|
||||
err = ble_gap_ext_adv_set_data(ADV_HANDLE, data);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Extended adv instance %u started", ADV_HANDLE);
|
||||
}
|
||||
|
||||
esp_ble_audio_cap_stream_t *stream_alloc(esp_ble_audio_dir_t dir)
|
||||
{
|
||||
if (dir == ESP_BLE_AUDIO_DIR_SINK) {
|
||||
return &peer.sink_stream;
|
||||
}
|
||||
|
||||
if (dir == ESP_BLE_AUDIO_DIR_SOURCE) {
|
||||
return &peer.source_stream;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void stream_released(const esp_ble_audio_cap_stream_t *cap_stream)
|
||||
{
|
||||
if (cap_stream == &peer.source_stream) {
|
||||
ESP_LOGI(TAG, "Source stream released");
|
||||
} else if (cap_stream == &peer.sink_stream) {
|
||||
ESP_LOGI(TAG, "Sink stream released");
|
||||
}
|
||||
}
|
||||
|
||||
static int register_pac(esp_ble_audio_dir_t dir,
|
||||
esp_ble_audio_context_t context,
|
||||
esp_ble_audio_pacs_cap_t *cap)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
err = esp_ble_audio_pacs_cap_register(dir, cap);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register pacs capabilities, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* Note:
|
||||
* If using ESP_BLE_AUDIO_LOCATION_MONO_AUDIO, Samsung S24 will not perform
|
||||
* the BAP unicast related procedures.
|
||||
*/
|
||||
err = esp_ble_audio_pacs_set_location(dir, (ESP_BLE_AUDIO_LOCATION_FRONT_LEFT |
|
||||
ESP_BLE_AUDIO_LOCATION_FRONT_RIGHT));
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set pacs location, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_pacs_set_supported_contexts(dir, context);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set supported contexts, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_pacs_set_available_contexts(dir, context);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set available contexts, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int init_cap_acceptor(void)
|
||||
{
|
||||
const esp_ble_audio_pacs_register_param_t pacs_param = {
|
||||
.snk_pac = true,
|
||||
.snk_loc = true,
|
||||
.src_pac = true,
|
||||
.src_loc = true,
|
||||
};
|
||||
int err;
|
||||
|
||||
err = esp_ble_audio_pacs_register(&pacs_param);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register pacs, err %d", err);
|
||||
return -1;
|
||||
}
|
||||
|
||||
err = register_pac(ESP_BLE_AUDIO_DIR_SINK, SINK_CONTEXT, &sink_cap);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register sink capabilities, err %d", err);
|
||||
return -1;
|
||||
}
|
||||
|
||||
err = register_pac(ESP_BLE_AUDIO_DIR_SOURCE, SOURCE_CONTEXT, &source_cap);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register source capabilities, err %d", err);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if CONFIG_EXAMPLE_BROADCAST
|
||||
#if CONFIG_EXAMPLE_SCAN_SELF
|
||||
static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
broadcast_scan_recv(event);
|
||||
}
|
||||
#endif /* CONFIG_EXAMPLE_SCAN_SELF */
|
||||
|
||||
static void pa_sync(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
if (event->pa_sync.status) {
|
||||
ESP_LOGE(TAG, "PA sync failed, status %d", event->pa_sync.status);
|
||||
return;
|
||||
}
|
||||
|
||||
broadcast_pa_synced(event);
|
||||
}
|
||||
|
||||
static void pa_sync_lost(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
ESP_LOGI(TAG, "PA sync lost: sync_handle 0x%04x reason 0x%02x",
|
||||
event->pa_sync_lost.sync_handle, event->pa_sync_lost.reason);
|
||||
|
||||
broadcast_pa_lost(event);
|
||||
}
|
||||
#endif /* CONFIG_EXAMPLE_BROADCAST */
|
||||
|
||||
static void acl_connect(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
if (event->acl_connect.status) {
|
||||
ESP_LOGE(TAG, "connection failed, status %d", event->acl_connect.status);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Conn established:");
|
||||
ESP_LOGI(TAG, "conn_handle 0x%04x status 0x%02x role %u peer %02x:%02x:%02x:%02x:%02x:%02x",
|
||||
event->acl_connect.conn_handle, event->acl_connect.status,
|
||||
event->acl_connect.role, event->acl_connect.dst.val[5],
|
||||
event->acl_connect.dst.val[4], event->acl_connect.dst.val[3],
|
||||
event->acl_connect.dst.val[2], event->acl_connect.dst.val[1],
|
||||
event->acl_connect.dst.val[0]);
|
||||
|
||||
peer.conn_handle = event->acl_connect.conn_handle;
|
||||
}
|
||||
|
||||
static void acl_disconnect(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
ESP_LOGI(TAG, "Conn terminated: conn_handle 0x%04x reason 0x%02x",
|
||||
event->acl_disconnect.conn_handle, event->acl_disconnect.reason);
|
||||
|
||||
peer.conn_handle = CONN_HANDLE_INIT;
|
||||
|
||||
ext_adv_start();
|
||||
}
|
||||
|
||||
static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
switch (event->type) {
|
||||
#if CONFIG_EXAMPLE_BROADCAST
|
||||
#if CONFIG_EXAMPLE_SCAN_SELF
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_EXT_SCAN_RECV:
|
||||
ext_scan_recv(event);
|
||||
break;
|
||||
#endif /* CONFIG_EXAMPLE_SCAN_SELF */
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC:
|
||||
pa_sync(event);
|
||||
break;
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC_LOST:
|
||||
pa_sync_lost(event);
|
||||
break;
|
||||
#endif /* CONFIG_EXAMPLE_BROADCAST */
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_ACL_CONNECT:
|
||||
acl_connect(event);
|
||||
break;
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_ACL_DISCONNECT:
|
||||
acl_disconnect(event);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event)
|
||||
{
|
||||
int err;
|
||||
|
||||
ESP_LOGI(TAG, "gatt mtu change, conn_handle %u, mtu %u",
|
||||
event->gatt_mtu_change.conn_handle, event->gatt_mtu_change.mtu);
|
||||
|
||||
if (event->gatt_mtu_change.mtu < ESP_BLE_AUDIO_ATT_MTU_MIN) {
|
||||
ESP_LOGW(TAG, "Invalid new mtu %u, shall be at least %u",
|
||||
event->gatt_mtu_change.mtu, ESP_BLE_AUDIO_ATT_MTU_MIN);
|
||||
return;
|
||||
}
|
||||
|
||||
/* This function only needs to be invoked when:
|
||||
* - The device is a Peripheral (Link Layer role);
|
||||
* - The device works as a GATT client.
|
||||
*/
|
||||
err = esp_ble_audio_gattc_disc_start(event->gatt_mtu_change.conn_handle);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start svc disc, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Start discovering gatt services");
|
||||
}
|
||||
|
||||
static void gattc_disc_cmpl(esp_ble_audio_gatt_app_event_t *event)
|
||||
{
|
||||
ESP_LOGI(TAG, "gattc disc cmpl, status %u, conn_handle %u",
|
||||
event->gattc_disc_cmpl.status, event->gattc_disc_cmpl.conn_handle);
|
||||
}
|
||||
|
||||
static void iso_gatt_app_cb(esp_ble_audio_gatt_app_event_t *event)
|
||||
{
|
||||
switch (event->type) {
|
||||
case ESP_BLE_AUDIO_GATT_EVENT_GATT_MTU_CHANGE:
|
||||
gatt_mtu_change(event);
|
||||
break;
|
||||
case ESP_BLE_AUDIO_GATT_EVENT_GATTC_DISC_CMPL:
|
||||
gattc_disc_cmpl(event);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
esp_ble_audio_init_info_t info = {
|
||||
.gap_cb = iso_gap_app_cb,
|
||||
.gatt_cb = iso_gatt_app_cb,
|
||||
};
|
||||
esp_err_t err;
|
||||
|
||||
/* Initialize NVS — it is used to store PHY calibration data */
|
||||
err = nvs_flash_init();
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
err = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(err);
|
||||
|
||||
err = bluetooth_init();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize BLE, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_common_init(&info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize audio, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = init_cap_acceptor();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if CONFIG_EXAMPLE_UNICAST
|
||||
err = cap_acceptor_unicast_init(&peer);
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
#endif /* CONFIG_EXAMPLE_UNICAST */
|
||||
|
||||
#if CONFIG_EXAMPLE_BROADCAST
|
||||
err = cap_acceptor_broadcast_init();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
#endif /* CONFIG_EXAMPLE_BROADCAST */
|
||||
|
||||
err = esp_ble_audio_common_start(NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start audio, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = ble_svc_gap_device_name_set("CAP Acceptor");
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set device name, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
#if CONFIG_EXAMPLE_SCAN_SELF
|
||||
err = check_start_scan();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
#endif /* CONFIG_EXAMPLE_SCAN_SELF */
|
||||
|
||||
/* Advertising will be used by Unicast Server and
|
||||
* Broadcast Sink when self-scanning is disabled.
|
||||
*/
|
||||
ext_adv_start();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
# This file was generated using idf.py save-defconfig. It can be edited manually.
|
||||
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
|
||||
#
|
||||
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_BLUEDROID_ENABLED=n
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_EXT_ADV=y
|
||||
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
|
||||
CONFIG_BT_NIMBLE_MAX_CCCDS=20
|
||||
CONFIG_BT_NIMBLE_ISO=y
|
||||
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
|
||||
|
||||
CONFIG_BT_ISO_MAX_CHAN=2
|
||||
|
||||
CONFIG_BT_CAP_ACCEPTOR=y
|
||||
CONFIG_BT_BAP_UNICAST_SERVER=y
|
||||
CONFIG_BT_BAP_BROADCAST_SINK=y
|
||||
CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT=2
|
||||
CONFIG_BT_BAP_SCAN_DELEGATOR=y
|
||||
CONFIG_BT_PAC_SNK_NOTIFIABLE=y
|
||||
CONFIG_BT_PAC_SNK_LOC_WRITEABLE=y
|
||||
CONFIG_BT_PAC_SNK_LOC_NOTIFIABLE=y
|
||||
CONFIG_BT_PAC_SRC_NOTIFIABLE=y
|
||||
CONFIG_BT_PAC_SRC_LOC_WRITEABLE=y
|
||||
CONFIG_BT_PAC_SRC_LOC_NOTIFIABLE=y
|
||||
CONFIG_BT_PACS_SUPPORTED_CONTEXT_NOTIFIABLE=y
|
||||
CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=30
|
||||
|
||||
CONFIG_EXAMPLE_UNICAST=y
|
||||
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
@@ -0,0 +1,6 @@
|
||||
# Override some defaults so BT stack is enabled
|
||||
# by default in this example
|
||||
|
||||
CONFIG_IDF_TARGET="esp32h4"
|
||||
|
||||
CONFIG_BT_LE_ISO_SUPPORT=y
|
||||
@@ -0,0 +1,8 @@
|
||||
# The following lines of boilerplate have to be in your project's CMakeLists
|
||||
# in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
|
||||
idf_build_set_property(MINIMAL_BUILD ON)
|
||||
project(cap_initiator)
|
||||
@@ -0,0 +1,73 @@
|
||||
| Supported Targets | ESP32-H4 |
|
||||
| ----------------- | -------- |
|
||||
|
||||
# CAP Initiator Example
|
||||
|
||||
(See the README.md file in the upper level `examples` directory for more information about examples.)
|
||||
|
||||
This example demonstrates the **Common Audio Profile (CAP) Initiator** functionality. It operates in one of two mutually exclusive modes selected at build time: **unicast** or **broadcast**. In unicast mode, the initiator scans for connectable advertising that includes the CAS/ASCS service data, connects to the acceptor, performs service discovery and MTU exchange, discovers sink and source ASEs, configures streams with an LC3 16_2_1 unicast preset, sets QoS, enables and connects the streams, then starts the streams and can send or receive audio. In broadcast mode, the initiator starts extended and periodic advertising with the Broadcast Audio Announcement service data and BASE, so that Broadcast Sinks (e.g. the [acceptor](../acceptor) in broadcast mode) can sync. Run it together with the [acceptor](../acceptor) example on another device as the CAP Acceptor.
|
||||
|
||||
The implementation uses the NimBLE host stack with ISO and BAP support, ESP-BLE-AUDIO (CAP initiator, BAP unicast client, BAP broadcast source). Unicast uses the LC3 16_2_1 preset and initiates pairing after connection. Broadcast uses an LC3 16_2_1 broadcast preset with a hardcoded broadcast ID. The device name is hardcoded as `CAP Initiator`.
|
||||
|
||||
## Requirements
|
||||
|
||||
* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4)
|
||||
* For unicast or as Broadcast Assistant: another device running the [acceptor](../acceptor) example (advertising as CAP Acceptor)
|
||||
|
||||
## How to Use Example
|
||||
|
||||
Before project configuration and build, set the correct chip target:
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32h4
|
||||
```
|
||||
|
||||
### Configure the Project
|
||||
|
||||
Open the configuration menu to select the mode:
|
||||
|
||||
```bash
|
||||
idf.py menuconfig
|
||||
```
|
||||
|
||||
Under **Example: CAP Initiator**, select one of:
|
||||
|
||||
* **Unicast** — scan for a CAP Acceptor and set up unicast audio streams after connection
|
||||
* **Broadcast** — act as a Broadcast Source; start extended and periodic advertising so that Broadcast Sinks can sync
|
||||
|
||||
### Build and Flash
|
||||
|
||||
Run the following to build, flash and monitor:
|
||||
|
||||
```bash
|
||||
idf.py -p PORT flash monitor
|
||||
```
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF.
|
||||
|
||||
## Example Flow
|
||||
|
||||
1. **Initialization**: NVS, Bluetooth stack, and LE Audio common layer (`esp_ble_audio_common_init`) with GAP and GATT callbacks (unicast mode). In unicast mode, initialize the CAP initiator unicast part (BAP unicast client, CAP unicast group, stream callbacks). In broadcast mode, initialize the CAP initiator broadcast part (broadcast source, stream ops). Register the TX module for sending audio. Start the audio stack and set the device name (`CAP Initiator`).
|
||||
2. **Unicast mode**: Start extended scanning. On scan result with connectable advertising and CAS/ASCS service data, connect to the acceptor. On connection, start pairing. On MTU exchange, start GATT service discovery. After discovery, discover ASEs, configure codec (LC3 16_2_1), set QoS, enable and connect streams, then start streams; the initiator registers the TX stream for sending and can receive on sink streams.
|
||||
3. **Broadcast mode**: Start extended and periodic advertising with Broadcast Audio Announcement UUID, hardcoded broadcast ID, and BASE. When the broadcast stream is started, register it for TX and send audio. Sinks (e.g. acceptor in broadcast mode) can sync to this broadcast.
|
||||
4. **Stream lifecycle**: Stream started/stopped callbacks register/unregister streams with the TX module; sent callbacks log periodic packet counts.
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
I (xxx) CAP_INI: connection established, status 0
|
||||
I (xxx) CAP_INI: Configured stream 0x...
|
||||
I (xxx) CAP_INI: Started stream 0x...
|
||||
...
|
||||
I (xxx) CAP_INI: Started broadcast stream 0x...
|
||||
I (xxx) CAP_INI: Sent 1000 HCI ISO data packets
|
||||
...
|
||||
```
|
||||
|
||||
If the connection is lost:
|
||||
|
||||
```
|
||||
I (xxx) CAP_INI: connection disconnected, reason 0x...
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
set(srcs "cap_initiator_tx.c"
|
||||
"main.c")
|
||||
|
||||
if(CONFIG_EXAMPLE_UNICAST)
|
||||
list(APPEND srcs "cap_initiator_unicast.c")
|
||||
endif()
|
||||
|
||||
if(CONFIG_EXAMPLE_BROADCAST)
|
||||
list(APPEND srcs "cap_initiator_broadcast.c")
|
||||
endif()
|
||||
|
||||
idf_component_register(SRCS "${srcs}"
|
||||
REQUIRES bt nvs_flash)
|
||||
@@ -0,0 +1,27 @@
|
||||
# SPDX-FileCopyrightText: 2022 Nordic Semiconductor ASA
|
||||
# SPDX-FileContributor: 2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
menu "Example: CAP Initiator"
|
||||
|
||||
choice EXAMPLE_CAP_INITIATOR_MODE
|
||||
prompt "CAP Initiator mode"
|
||||
default EXAMPLE_UNICAST
|
||||
help
|
||||
Select exactly one CAP initiator mode.
|
||||
|
||||
config EXAMPLE_UNICAST
|
||||
bool "Unicast"
|
||||
help
|
||||
If selected, the sample will start advertising connectable
|
||||
for Broadcast Assistants.
|
||||
|
||||
config EXAMPLE_BROADCAST
|
||||
bool "Broadcast"
|
||||
help
|
||||
If selected, the sample will start advertising syncable
|
||||
audio streams.
|
||||
|
||||
endchoice
|
||||
|
||||
endmenu
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#include "host/ble_hs.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
|
||||
#include "esp_ble_audio_lc3_defs.h"
|
||||
#include "esp_ble_audio_bap_api.h"
|
||||
#include "esp_ble_audio_cap_api.h"
|
||||
#include "esp_ble_audio_pacs_api.h"
|
||||
#include "esp_ble_audio_bap_lc3_preset_defs.h"
|
||||
|
||||
#include "ble_audio_example_init.h"
|
||||
#include "ble_audio_example_utils.h"
|
||||
|
||||
#define TAG "CAP_INI"
|
||||
|
||||
#define CONN_HANDLE_INIT 0xFFFF
|
||||
|
||||
struct tx_stream {
|
||||
esp_ble_audio_cap_stream_t *stream;
|
||||
uint16_t seq_num;
|
||||
uint8_t *data;
|
||||
example_audio_tx_scheduler_t scheduler;
|
||||
};
|
||||
|
||||
void cap_initiator_unicast_gap_cb(esp_ble_audio_gap_app_event_t *event);
|
||||
|
||||
void cap_initiator_unicast_gatt_cb(esp_ble_audio_gatt_app_event_t *event);
|
||||
|
||||
int cap_initiator_unicast_start(void);
|
||||
|
||||
int cap_initiator_unicast_init(void);
|
||||
|
||||
int cap_initiator_broadcast_start(void);
|
||||
|
||||
int cap_initiator_broadcast_init(void);
|
||||
|
||||
void cap_initiator_tx_stream_sent(esp_ble_audio_bap_stream_t *stream, void *user_data);
|
||||
|
||||
int cap_initiator_tx_register_stream(esp_ble_audio_cap_stream_t *cap_stream);
|
||||
|
||||
int cap_initiator_tx_unregister_stream(esp_ble_audio_cap_stream_t *cap_stream);
|
||||
|
||||
void cap_initiator_tx_init(void);
|
||||
@@ -0,0 +1,325 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "cap_initiator.h"
|
||||
|
||||
#define ADV_HANDLE 0x00
|
||||
#define ADV_SID 0
|
||||
#define ADV_TX_POWER 127
|
||||
#define ADV_ADDRESS BLE_OWN_ADDR_PUBLIC
|
||||
#define ADV_PRIMARY_PHY BLE_HCI_LE_PHY_1M
|
||||
#define ADV_SECONDARY_PHY BLE_HCI_LE_PHY_2M
|
||||
#define ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(200)
|
||||
|
||||
#define PER_ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(100)
|
||||
|
||||
#define LOCAL_DEVICE_NAME "CAP Broadcast Source"
|
||||
#define LOCAL_BROADCAST_ID 0x123456
|
||||
|
||||
ESP_BLE_AUDIO_BAP_LC3_BROADCAST_PRESET_16_2_1_DEFINE(broadcast_preset_16_2_1,
|
||||
ESP_BLE_AUDIO_LOCATION_MONO_AUDIO,
|
||||
ESP_BLE_AUDIO_CONTEXT_TYPE_UNSPECIFIED);
|
||||
|
||||
static esp_ble_audio_cap_broadcast_source_t *broadcast_source;
|
||||
static esp_ble_audio_cap_stream_t broadcast_stream;
|
||||
|
||||
static void broadcast_stream_started_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
esp_ble_audio_cap_stream_t *cap_stream;
|
||||
int err;
|
||||
|
||||
ESP_LOGI(TAG, "Broadcast stream %p started", stream);
|
||||
|
||||
cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream);
|
||||
|
||||
err = cap_initiator_tx_register_stream(cap_stream);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register stream %p for TX, err %d", stream, err);
|
||||
}
|
||||
}
|
||||
|
||||
static void broadcast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
|
||||
{
|
||||
esp_ble_audio_cap_stream_t *cap_stream;
|
||||
|
||||
ESP_LOGI(TAG, "Broadcast stream %p stopped, reason 0x%02x", stream, reason);
|
||||
|
||||
cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream);
|
||||
|
||||
(void)cap_initiator_tx_unregister_stream(cap_stream);
|
||||
}
|
||||
|
||||
static void broadcast_stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
|
||||
{
|
||||
esp_ble_audio_cap_stream_t *cap_stream;
|
||||
|
||||
ESP_LOGI(TAG, "Broadcast stream %p disconnected, reason 0x%02x", stream, reason);
|
||||
|
||||
cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream);
|
||||
|
||||
(void)cap_initiator_tx_unregister_stream(cap_stream);
|
||||
}
|
||||
|
||||
static void broadcast_stream_sent_cb(esp_ble_audio_bap_stream_t *stream, void *user_data)
|
||||
{
|
||||
cap_initiator_tx_stream_sent(stream, user_data);
|
||||
}
|
||||
|
||||
static esp_ble_audio_bap_stream_ops_t broadcast_stream_ops = {
|
||||
.started = broadcast_stream_started_cb,
|
||||
.stopped = broadcast_stream_stopped_cb,
|
||||
.sent = broadcast_stream_sent_cb,
|
||||
.disconnected = broadcast_stream_disconnected_cb,
|
||||
};
|
||||
|
||||
static uint8_t *ext_adv_data_get(uint8_t *data_len)
|
||||
{
|
||||
uint32_t broadcast_id;
|
||||
uint8_t *data;
|
||||
|
||||
broadcast_id = LOCAL_BROADCAST_ID;
|
||||
|
||||
/* - Broadcast Audio Announcement Service UUID (2 octets)
|
||||
* - Broadcast ID (3 octets)
|
||||
* - Complete Device Name
|
||||
*/
|
||||
*data_len = 7 + 2 + strlen(LOCAL_DEVICE_NAME);
|
||||
|
||||
data = calloc(1, *data_len);
|
||||
if (data == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
data[0] = 0x06; /* 1 + 2 + 3 */
|
||||
data[1] = EXAMPLE_AD_TYPE_SERVICE_DATA16;
|
||||
data[2] = (ESP_BLE_AUDIO_UUID_BROADCAST_AUDIO_VAL & 0xFF);
|
||||
data[3] = ((ESP_BLE_AUDIO_UUID_BROADCAST_AUDIO_VAL >> 8) & 0xFF);
|
||||
data[4] = (broadcast_id & 0xFF);
|
||||
data[5] = ((broadcast_id >> 8) & 0xFF);
|
||||
data[6] = ((broadcast_id >> 16) & 0xFF);
|
||||
|
||||
data[7] = strlen(LOCAL_DEVICE_NAME) + 1;
|
||||
data[8] = EXAMPLE_AD_TYPE_NAME_COMPLETE;
|
||||
memcpy(data + 9, LOCAL_DEVICE_NAME, strlen(LOCAL_DEVICE_NAME));
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static uint8_t *per_adv_data_get(uint8_t *data_len)
|
||||
{
|
||||
NET_BUF_SIMPLE_DEFINE(base_buf, 128);
|
||||
uint8_t *data;
|
||||
esp_err_t err;
|
||||
|
||||
/* Broadcast Audio Announcement Service UUID (2 octets) and
|
||||
* Broadcast Audio Source Endpoint (BASE)
|
||||
*/
|
||||
|
||||
err = esp_ble_audio_cap_initiator_broadcast_get_base(broadcast_source, &base_buf);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get encoded BASE, err %d", err);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
*data_len = 2 + base_buf.len;
|
||||
|
||||
data = calloc(1, *data_len);
|
||||
if (data == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* base_buf.len has included the UUID length (2 octets) */
|
||||
data[0] = 1 + base_buf.len;
|
||||
data[1] = EXAMPLE_AD_TYPE_SERVICE_DATA16;
|
||||
memcpy(data + 2, base_buf.data, base_buf.len);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static int ext_adv_start(void)
|
||||
{
|
||||
struct ble_gap_periodic_adv_params per_params = {0};
|
||||
struct ble_gap_ext_adv_params ext_params = {0};
|
||||
struct os_mbuf *data = NULL;
|
||||
uint8_t *ext_data = NULL;
|
||||
uint8_t *per_data = NULL;
|
||||
uint8_t data_len = 0;
|
||||
int err;
|
||||
|
||||
ext_params.connectable = 0;
|
||||
ext_params.scannable = 0;
|
||||
ext_params.legacy_pdu = 0;
|
||||
ext_params.own_addr_type = ADV_ADDRESS;
|
||||
ext_params.primary_phy = ADV_PRIMARY_PHY;
|
||||
ext_params.secondary_phy = ADV_SECONDARY_PHY;
|
||||
ext_params.tx_power = ADV_TX_POWER;
|
||||
ext_params.sid = ADV_SID;
|
||||
ext_params.itvl_min = ADV_INTERVAL;
|
||||
ext_params.itvl_max = ADV_INTERVAL;
|
||||
|
||||
err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL,
|
||||
example_audio_gap_event_cb, NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
ext_data = ext_adv_data_get(&data_len);
|
||||
if (ext_data == NULL) {
|
||||
err = -ENOMEM;
|
||||
goto end;
|
||||
}
|
||||
|
||||
data = os_msys_get_pkthdr(data_len, 0);
|
||||
if (data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to get ext adv mbuf");
|
||||
err = -ENOMEM;
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = os_mbuf_append(data, ext_data, data_len);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err);
|
||||
os_mbuf_free_chain(data);
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = ble_gap_ext_adv_set_data(ADV_HANDLE, data);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
per_params.include_tx_power = 0;
|
||||
per_params.itvl_min = PER_ADV_INTERVAL;
|
||||
per_params.itvl_max = PER_ADV_INTERVAL;
|
||||
|
||||
err = ble_gap_periodic_adv_configure(ADV_HANDLE, &per_params);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to configure per adv params, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
per_data = per_adv_data_get(&data_len);
|
||||
if (per_data == NULL) {
|
||||
err = -ENOMEM;
|
||||
goto end;
|
||||
}
|
||||
|
||||
data = os_msys_get_pkthdr(data_len, 0);
|
||||
if (data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to get per adv mbuf");
|
||||
err = -ENOMEM;
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = os_mbuf_append(data, per_data, data_len);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to append per adv data, err %d", err);
|
||||
os_mbuf_free_chain(data);
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = ble_gap_periodic_adv_set_data(ADV_HANDLE, data);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set per adv data, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = ble_gap_periodic_adv_start(ADV_HANDLE);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start per advertising, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Extended adv instance %u started", ADV_HANDLE);
|
||||
|
||||
end:
|
||||
if (ext_data) {
|
||||
free(ext_data);
|
||||
}
|
||||
if (per_data) {
|
||||
free(per_data);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
int cap_initiator_broadcast_start(void)
|
||||
{
|
||||
esp_ble_audio_cap_initiator_broadcast_stream_param_t stream_params = {
|
||||
.stream = &broadcast_stream,
|
||||
};
|
||||
esp_ble_audio_cap_initiator_broadcast_subgroup_param_t subgroup_param = {
|
||||
.codec_cfg = &broadcast_preset_16_2_1.codec_cfg,
|
||||
.stream_params = &stream_params,
|
||||
.stream_count = 1,
|
||||
};
|
||||
const esp_ble_audio_cap_initiator_broadcast_create_param_t create_param = {
|
||||
.qos = &broadcast_preset_16_2_1.qos,
|
||||
.subgroup_params = &subgroup_param,
|
||||
.subgroup_count = 1,
|
||||
};
|
||||
esp_ble_audio_bap_broadcast_adv_info_t info = {
|
||||
.adv_handle = ADV_HANDLE,
|
||||
};
|
||||
int err;
|
||||
|
||||
ESP_LOGI(TAG, "Creating broadcast source");
|
||||
|
||||
err = esp_ble_audio_cap_initiator_broadcast_audio_create(&create_param, &broadcast_source);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to create broadcast source, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = ext_adv_start();
|
||||
if (err) {
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_adv_add(&info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to add adv for broadcast source, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_cap_initiator_broadcast_audio_start(broadcast_source, ADV_HANDLE);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start broadcast source, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
end:
|
||||
err = esp_ble_audio_cap_initiator_broadcast_audio_delete(broadcast_source);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to delete broadcast source, err %d", err);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
int cap_initiator_broadcast_init(void)
|
||||
{
|
||||
esp_ble_audio_cap_stream_ops_register(&broadcast_stream, &broadcast_stream_ops);
|
||||
|
||||
ESP_LOGI(TAG, "CAP initiator broadcast initialized");
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "cap_initiator.h"
|
||||
|
||||
static struct tx_stream tx_streams[IS_ENABLED(CONFIG_EXAMPLE_UNICAST) + IS_ENABLED(CONFIG_EXAMPLE_BROADCAST)];
|
||||
|
||||
static bool cap_stream_is_streaming(const esp_ble_audio_cap_stream_t *cap_stream)
|
||||
{
|
||||
esp_ble_audio_bap_ep_info_t ep_info = {0};
|
||||
int err;
|
||||
|
||||
if (cap_stream == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cap_stream->bap_stream.ep == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_ep_get_info(cap_stream->bap_stream.ep, &ep_info);
|
||||
if (err) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (ep_info.state == ESP_BLE_AUDIO_BAP_EP_STATE_STREAMING);
|
||||
}
|
||||
|
||||
static void cap_initiator_tx_send(struct tx_stream *tx_stream)
|
||||
{
|
||||
int err;
|
||||
|
||||
if (tx_stream == NULL || tx_stream->stream == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cap_stream_is_streaming(tx_stream->stream) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tx_stream->stream->bap_stream.qos == NULL ||
|
||||
tx_stream->stream->bap_stream.qos->sdu == 0) {
|
||||
ESP_LOGE(TAG, "Invalid stream qos");
|
||||
return;
|
||||
}
|
||||
|
||||
if (tx_stream->data == NULL) {
|
||||
ESP_LOGE(TAG, "TX buffer unavailable, SDU %u (stream %p)",
|
||||
tx_stream->stream->bap_stream.qos->sdu, &tx_stream->stream->bap_stream);
|
||||
return;
|
||||
}
|
||||
|
||||
memset(tx_stream->data, (uint8_t)tx_stream->seq_num, tx_stream->stream->bap_stream.qos->sdu);
|
||||
|
||||
err = esp_ble_audio_cap_stream_send(tx_stream->stream, tx_stream->data,
|
||||
tx_stream->stream->bap_stream.qos->sdu,
|
||||
tx_stream->seq_num);
|
||||
if (err) {
|
||||
ESP_LOGD(TAG, "Failed to transmit data on stream %p, err %d",
|
||||
&tx_stream->stream->bap_stream, err);
|
||||
return;
|
||||
}
|
||||
|
||||
tx_stream->seq_num++;
|
||||
}
|
||||
|
||||
void cap_initiator_tx_stream_sent(esp_ble_audio_bap_stream_t *stream, void *user_data)
|
||||
{
|
||||
for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) {
|
||||
if (tx_streams[i].stream && &tx_streams[i].stream->bap_stream == stream) {
|
||||
example_audio_tx_scheduler_on_sent(&tx_streams[i].scheduler, user_data, TAG, "stream", stream);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void tx_scheduler_cb(void *arg)
|
||||
{
|
||||
struct tx_stream *tx_stream = arg;
|
||||
|
||||
cap_initiator_tx_send(tx_stream);
|
||||
}
|
||||
|
||||
int cap_initiator_tx_register_stream(esp_ble_audio_cap_stream_t *cap_stream)
|
||||
{
|
||||
int err;
|
||||
|
||||
if (cap_stream == NULL) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) {
|
||||
if (tx_streams[i].stream == NULL) {
|
||||
if (cap_stream->bap_stream.qos == NULL || cap_stream->bap_stream.qos->sdu == 0) {
|
||||
ESP_LOGE(TAG, "Invalid stream qos");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
if (tx_streams[i].data == NULL) {
|
||||
tx_streams[i].data = calloc(1, cap_stream->bap_stream.qos->sdu);
|
||||
if (tx_streams[i].data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to alloc TX buffer, SDU %u (stream %p)",
|
||||
cap_stream->bap_stream.qos->sdu, &cap_stream->bap_stream);
|
||||
return -ENOMEM;
|
||||
}
|
||||
}
|
||||
|
||||
tx_streams[i].stream = cap_stream;
|
||||
tx_streams[i].seq_num = 0;
|
||||
example_audio_tx_scheduler_reset(&tx_streams[i].scheduler);
|
||||
|
||||
err = example_audio_tx_scheduler_start(&tx_streams[i].scheduler, cap_stream->bap_stream.qos->interval);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start tx scheduler, err %d", err);
|
||||
tx_streams[i].stream = NULL;
|
||||
return err;
|
||||
}
|
||||
|
||||
cap_initiator_tx_send(&tx_streams[i]);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGE(TAG, "No free TX stream slot");
|
||||
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
int cap_initiator_tx_unregister_stream(esp_ble_audio_cap_stream_t *cap_stream)
|
||||
{
|
||||
int err;
|
||||
|
||||
if (cap_stream == NULL) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) {
|
||||
if (tx_streams[i].stream == cap_stream) {
|
||||
err = example_audio_tx_scheduler_stop(&tx_streams[i].scheduler);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
tx_streams[i].stream = NULL;
|
||||
if (tx_streams[i].data != NULL) {
|
||||
free(tx_streams[i].data);
|
||||
tx_streams[i].data = NULL;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return -ENODATA;
|
||||
}
|
||||
|
||||
void cap_initiator_tx_init(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
memset(tx_streams, 0, sizeof(tx_streams));
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) {
|
||||
err = example_audio_tx_scheduler_init(&tx_streams[i].scheduler,
|
||||
tx_scheduler_cb,
|
||||
&tx_streams[i]);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize tx scheduler[%u], err %d", i, err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,728 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "cap_initiator.h"
|
||||
|
||||
#define SCAN_INTERVAL 160 /* 100ms */
|
||||
#define SCAN_WINDOW 160 /* 100ms */
|
||||
|
||||
#define INIT_SCAN_INTERVAL 16 /* 10ms */
|
||||
#define INIT_SCAN_WINDOW 16 /* 10ms */
|
||||
#define CONN_INTERVAL 64 /* 64 * 1.25 = 80ms */
|
||||
#define CONN_LATENCY 0
|
||||
#define CONN_TIMEOUT 500 /* 500 * 10ms = 5s */
|
||||
#define CONN_MAX_CE_LEN 0xFFFF
|
||||
#define CONN_MIN_CE_LEN 0xFFFF
|
||||
#define CONN_DURATION 10000 /* 10s */
|
||||
|
||||
ESP_BLE_AUDIO_BAP_LC3_UNICAST_PRESET_16_2_1_DEFINE(unicast_preset_16_2_1,
|
||||
ESP_BLE_AUDIO_LOCATION_MONO_AUDIO,
|
||||
ESP_BLE_AUDIO_CONTEXT_TYPE_UNSPECIFIED);
|
||||
static esp_ble_audio_cap_unicast_group_t *unicast_group;
|
||||
|
||||
static struct peer_config {
|
||||
esp_ble_audio_cap_stream_t source_stream;
|
||||
esp_ble_audio_cap_stream_t sink_stream;
|
||||
esp_ble_audio_bap_ep_t *source_ep;
|
||||
esp_ble_audio_bap_ep_t *sink_ep;
|
||||
|
||||
esp_ble_conn_t *conn;
|
||||
uint16_t conn_handle;
|
||||
bool disc_completed;
|
||||
bool disc_cancelled;
|
||||
bool mtu_exchanged;
|
||||
} peer = {
|
||||
.conn_handle = CONN_HANDLE_INIT,
|
||||
};
|
||||
|
||||
static example_audio_rx_metrics_t rx_metrics;
|
||||
|
||||
static bool is_tx_stream(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
esp_ble_audio_bap_ep_info_t ep_info = {0};
|
||||
esp_err_t err;
|
||||
|
||||
err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get ep info, err %d", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
return (ep_info.dir == ESP_BLE_AUDIO_DIR_SINK);
|
||||
}
|
||||
|
||||
static void unicast_stream_configured_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
const esp_ble_audio_bap_qos_cfg_pref_t *pref)
|
||||
{
|
||||
ESP_LOGI(TAG, "Unicast stream %p configured", stream);
|
||||
|
||||
example_print_qos_pref(pref);
|
||||
}
|
||||
|
||||
static void unicast_stream_qos_set_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
ESP_LOGI(TAG, "Unicast stream %p QoS set", stream);
|
||||
}
|
||||
|
||||
static void unicast_stream_enabled_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
ESP_LOGI(TAG, "Unicast stream %p enabled", stream);
|
||||
}
|
||||
|
||||
static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
esp_ble_audio_cap_stream_t *cap_stream;
|
||||
int err;
|
||||
|
||||
ESP_LOGI(TAG, "Unicast stream %p started", stream);
|
||||
|
||||
example_audio_rx_metrics_reset(&rx_metrics);
|
||||
|
||||
if (is_tx_stream(stream)) {
|
||||
cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream);
|
||||
|
||||
err = cap_initiator_tx_register_stream(cap_stream);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register stream %p for TX, err %d", stream, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void unicast_stream_metadata_updated_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
ESP_LOGI(TAG, "Unicast stream %p metadata updated", stream);
|
||||
}
|
||||
|
||||
static void unicast_stream_disabled_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
ESP_LOGI(TAG, "Unicast stream %p disabled", stream);
|
||||
}
|
||||
|
||||
static void unicast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
|
||||
{
|
||||
esp_ble_audio_cap_stream_t *cap_stream;
|
||||
|
||||
ESP_LOGI(TAG, "Unicast stream %p stopped, reason 0x%02x", stream, reason);
|
||||
|
||||
if (is_tx_stream(stream)) {
|
||||
cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream);
|
||||
|
||||
(void)cap_initiator_tx_unregister_stream(cap_stream);
|
||||
}
|
||||
}
|
||||
|
||||
static void unicast_stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
|
||||
{
|
||||
esp_ble_audio_cap_stream_t *cap_stream;
|
||||
|
||||
ESP_LOGI(TAG, "Unicast stream %p disconnected, reason 0x%02x", stream, reason);
|
||||
|
||||
if (is_tx_stream(stream)) {
|
||||
cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream);
|
||||
|
||||
(void)cap_initiator_tx_unregister_stream(cap_stream);
|
||||
}
|
||||
}
|
||||
|
||||
static void unicast_stream_released_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
if (stream == &peer.source_stream.bap_stream) {
|
||||
ESP_LOGI(TAG, "Unicast source stream %p released", stream);
|
||||
} else if (stream == &peer.sink_stream.bap_stream) {
|
||||
ESP_LOGI(TAG, "Unicast sink stream %p released", stream);
|
||||
}
|
||||
}
|
||||
|
||||
static void unicast_stream_recv_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
const esp_ble_iso_recv_info_t *info,
|
||||
const uint8_t *data, uint16_t len)
|
||||
{
|
||||
rx_metrics.last_sdu_len = len;
|
||||
example_audio_rx_metrics_on_recv(info, &rx_metrics, TAG, "stream", stream);
|
||||
}
|
||||
|
||||
static void unicast_stream_sent_cb(esp_ble_audio_bap_stream_t *stream, void *user_data)
|
||||
{
|
||||
cap_initiator_tx_stream_sent(stream, user_data);
|
||||
}
|
||||
|
||||
static esp_ble_audio_bap_stream_ops_t 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,
|
||||
.sent = unicast_stream_sent_cb,
|
||||
.disconnected = unicast_stream_disconnected_cb,
|
||||
};
|
||||
|
||||
static int discover_cas(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = esp_ble_audio_cap_initiator_unicast_discover(peer.conn_handle);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to discover CAS, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Discovering CAS");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int discover_sinks(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
esp_ble_audio_cap_stream_ops_register(&peer.sink_stream, &unicast_stream_ops);
|
||||
|
||||
err = esp_ble_audio_bap_unicast_client_discover(peer.conn_handle, ESP_BLE_AUDIO_DIR_SINK);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to discover sinks, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Discovering sinks");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int discover_sources(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
esp_ble_audio_cap_stream_ops_register(&peer.source_stream, &unicast_stream_ops);
|
||||
|
||||
err = esp_ble_audio_bap_unicast_client_discover(peer.conn_handle, ESP_BLE_AUDIO_DIR_SOURCE);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to discover sources, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Discovering source ASEs");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int unicast_group_create(void)
|
||||
{
|
||||
esp_ble_audio_cap_unicast_group_stream_param_t source_stream_param = {
|
||||
.qos_cfg = &unicast_preset_16_2_1.qos,
|
||||
.stream = &peer.source_stream,
|
||||
};
|
||||
esp_ble_audio_cap_unicast_group_stream_param_t sink_stream_param = {
|
||||
.qos_cfg = &unicast_preset_16_2_1.qos,
|
||||
.stream = &peer.sink_stream,
|
||||
};
|
||||
esp_ble_audio_cap_unicast_group_stream_pair_param_t pair_params = {0};
|
||||
esp_ble_audio_cap_unicast_group_param_t group_param = {0};
|
||||
int err;
|
||||
|
||||
if (peer.source_ep) {
|
||||
pair_params.rx_param = &source_stream_param;
|
||||
}
|
||||
|
||||
if (peer.sink_ep) {
|
||||
pair_params.tx_param = &sink_stream_param;
|
||||
}
|
||||
|
||||
group_param.params_count = 1;
|
||||
group_param.params = &pair_params;
|
||||
|
||||
err = esp_ble_audio_cap_unicast_group_create(&group_param, &unicast_group);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to create unicast group, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Created unicast group");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int unicast_group_delete(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
if (unicast_group == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_cap_unicast_group_delete(unicast_group);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to delete unicast group, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
unicast_group = NULL;
|
||||
|
||||
ESP_LOGI(TAG, "Deleted unicast group");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int unicast_audio_start(void)
|
||||
{
|
||||
esp_ble_audio_cap_unicast_audio_start_stream_param_t stream_param[2] = {0};
|
||||
esp_ble_audio_cap_unicast_audio_start_param_t param = {0};
|
||||
int err;
|
||||
|
||||
if (peer.sink_ep) {
|
||||
stream_param[param.count].member.member = peer.conn;
|
||||
stream_param[param.count].stream = &peer.sink_stream;
|
||||
stream_param[param.count].ep = peer.sink_ep;
|
||||
stream_param[param.count].codec_cfg = &unicast_preset_16_2_1.codec_cfg;
|
||||
param.count++;
|
||||
}
|
||||
|
||||
if (peer.source_ep) {
|
||||
stream_param[param.count].member.member = peer.conn;
|
||||
stream_param[param.count].stream = &peer.source_stream;
|
||||
stream_param[param.count].ep = peer.source_ep;
|
||||
stream_param[param.count].codec_cfg = &unicast_preset_16_2_1.codec_cfg;
|
||||
param.count++;
|
||||
}
|
||||
|
||||
if (param.count == 0) {
|
||||
ESP_LOGW(TAG, "No endpoints available, skip starting unicast audio");
|
||||
return 0;
|
||||
}
|
||||
|
||||
param.type = ESP_BLE_AUDIO_CAP_SET_TYPE_AD_HOC;
|
||||
param.stream_params = stream_param;
|
||||
|
||||
err = esp_ble_audio_cap_initiator_unicast_audio_start(¶m);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start unicast audio, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Starting unicast streams");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void discover_cb(esp_ble_conn_t *conn, int err, esp_ble_audio_dir_t dir)
|
||||
{
|
||||
if (conn->handle != peer.conn_handle) {
|
||||
return;
|
||||
}
|
||||
|
||||
peer.conn = conn;
|
||||
|
||||
if (dir == ESP_BLE_AUDIO_DIR_SINK) {
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Discovery sinks failed, err %d", err);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Discover sinks complete");
|
||||
|
||||
discover_sources();
|
||||
}
|
||||
} else if (dir == ESP_BLE_AUDIO_DIR_SOURCE) {
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Discovery sources failed, err %d", err);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Discover sources complete");
|
||||
|
||||
err = unicast_group_create();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = unicast_audio_start();
|
||||
if (err) {
|
||||
unicast_group_delete();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void pac_record_cb(esp_ble_conn_t *conn,
|
||||
esp_ble_audio_dir_t dir,
|
||||
const esp_ble_audio_codec_cap_t *codec_cap)
|
||||
{
|
||||
example_print_codec_cap(codec_cap);
|
||||
}
|
||||
|
||||
static void endpoint_cb(esp_ble_conn_t *conn,
|
||||
esp_ble_audio_dir_t dir,
|
||||
esp_ble_audio_bap_ep_t *ep)
|
||||
{
|
||||
if (dir == ESP_BLE_AUDIO_DIR_SOURCE) {
|
||||
if (peer.source_ep == NULL) {
|
||||
ESP_LOGI(TAG, "Source ep: %p", ep);
|
||||
peer.source_ep = ep;
|
||||
}
|
||||
} else if (dir == ESP_BLE_AUDIO_DIR_SINK) {
|
||||
if (peer.sink_ep == NULL) {
|
||||
ESP_LOGI(TAG, "Sink ep: %p", ep);
|
||||
peer.sink_ep = ep;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static esp_ble_audio_bap_unicast_client_cb_t unicast_client_cbs = {
|
||||
.discover = discover_cb,
|
||||
.pac_record = pac_record_cb,
|
||||
.endpoint = endpoint_cb,
|
||||
};
|
||||
|
||||
static void unicast_discovery_complete_cb(esp_ble_conn_t *conn, int err,
|
||||
const esp_ble_audio_csip_set_coordinator_set_member_t *member,
|
||||
const esp_ble_audio_csip_set_coordinator_csis_inst_t *csis_inst)
|
||||
{
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Unicast discovery completed, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (IS_ENABLED(CONFIG_BT_CAP_ACCEPTOR_SET_MEMBER)) {
|
||||
if (csis_inst == NULL) {
|
||||
ESP_LOGW(TAG, "Failed to discover CAS CSIS");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Found CAS with CSIS %p", csis_inst);
|
||||
|
||||
/* TODO: Do set member discovery */
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Found CAS");
|
||||
}
|
||||
|
||||
(void)discover_sinks();
|
||||
}
|
||||
|
||||
static void unicast_start_complete_cb(int err, esp_ble_conn_t *conn)
|
||||
{
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Unicast start completed, err %d", err);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Unicast start completed");
|
||||
}
|
||||
}
|
||||
|
||||
static esp_ble_audio_cap_initiator_cb_t cap_cb = {
|
||||
.unicast_discovery_complete = unicast_discovery_complete_cb,
|
||||
.unicast_start_complete = unicast_start_complete_cb,
|
||||
};
|
||||
|
||||
static int conn_create(ble_addr_t *dst)
|
||||
{
|
||||
struct ble_gap_conn_params params = {0};
|
||||
uint8_t own_addr_type = 0;
|
||||
int err;
|
||||
|
||||
err = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to determine address type, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = ble_gap_disc_cancel();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop scanning, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
peer.disc_cancelled = true;
|
||||
|
||||
params.scan_itvl = INIT_SCAN_INTERVAL;
|
||||
params.scan_window = INIT_SCAN_WINDOW;
|
||||
params.itvl_min = CONN_INTERVAL;
|
||||
params.itvl_max = CONN_INTERVAL;
|
||||
params.latency = CONN_LATENCY;
|
||||
params.supervision_timeout = CONN_TIMEOUT;
|
||||
params.max_ce_len = CONN_MAX_CE_LEN;
|
||||
params.min_ce_len = CONN_MIN_CE_LEN;
|
||||
|
||||
return ble_gap_connect(own_addr_type, dst, CONN_DURATION, ¶ms,
|
||||
example_audio_gap_event_cb, NULL);
|
||||
}
|
||||
|
||||
static int pairing_start(uint16_t conn_handle)
|
||||
{
|
||||
return ble_gap_security_initiate(conn_handle);
|
||||
}
|
||||
|
||||
static int exchange_mtu(uint16_t conn_handle)
|
||||
{
|
||||
return ble_gattc_exchange_mtu(conn_handle, NULL, NULL);
|
||||
}
|
||||
|
||||
static bool check_and_connect(uint8_t type, const uint8_t *data,
|
||||
uint8_t data_len, void *user_data)
|
||||
{
|
||||
esp_ble_audio_gap_app_event_t *event;
|
||||
ble_addr_t dst = {0};
|
||||
uint16_t uuid_val;
|
||||
int err;
|
||||
|
||||
event = user_data;
|
||||
assert(event);
|
||||
|
||||
if (type != EXAMPLE_AD_TYPE_SERVICE_DATA16) {
|
||||
return true; /* Continue parsing to next AD data type */
|
||||
}
|
||||
|
||||
if (data_len < sizeof(uuid_val)) {
|
||||
ESP_LOGW(TAG, "Invalid ad size %u (cas uuid)", data_len);
|
||||
return true; /* Continue parsing to next AD data type */
|
||||
}
|
||||
|
||||
uuid_val = sys_get_le16(data);
|
||||
|
||||
if (uuid_val != ESP_BLE_AUDIO_UUID_CAS_VAL) {
|
||||
/* We are looking for the TMAS service data */
|
||||
return true; /* Continue parsing to next AD data type */
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Found CAS in peer adv data!");
|
||||
|
||||
dst.type = event->ext_scan_recv.addr.type;
|
||||
memcpy(dst.val, event->ext_scan_recv.addr.val, sizeof(dst.val));
|
||||
|
||||
err = conn_create(&dst);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to create conn, err %d", err);
|
||||
|
||||
if (peer.disc_cancelled) {
|
||||
peer.disc_cancelled = false;
|
||||
cap_initiator_unicast_start();
|
||||
}
|
||||
}
|
||||
|
||||
return false; /* Stop parsing */
|
||||
}
|
||||
|
||||
static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
if (peer.conn_handle != CONN_HANDLE_INIT) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Check if the advertising is connectable and if TMAS is supported */
|
||||
if (event->ext_scan_recv.event_type & EXAMPLE_ADV_PROP_CONNECTABLE) {
|
||||
esp_ble_audio_data_parse(event->ext_scan_recv.data,
|
||||
event->ext_scan_recv.data_len,
|
||||
check_and_connect, (void *)event);
|
||||
}
|
||||
}
|
||||
|
||||
static void acl_connect(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
int err;
|
||||
|
||||
if (event->acl_connect.status) {
|
||||
ESP_LOGE(TAG, "connection failed, status %d", event->acl_connect.status);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Conn established:");
|
||||
ESP_LOGI(TAG, "conn_handle 0x%04x status 0x%02x role %u peer %02x:%02x:%02x:%02x:%02x:%02x",
|
||||
event->acl_connect.conn_handle, event->acl_connect.status,
|
||||
event->acl_connect.role, event->acl_connect.dst.val[5],
|
||||
event->acl_connect.dst.val[4], event->acl_connect.dst.val[3],
|
||||
event->acl_connect.dst.val[2], event->acl_connect.dst.val[1],
|
||||
event->acl_connect.dst.val[0]);
|
||||
|
||||
err = pairing_start(event->acl_connect.conn_handle);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initiate security, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
peer.conn_handle = event->acl_connect.conn_handle;
|
||||
}
|
||||
|
||||
static void acl_disconnect(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
ESP_LOGI(TAG, "Conn terminated: conn_handle 0x%04x reason 0x%02x",
|
||||
event->acl_disconnect.conn_handle, event->acl_disconnect.reason);
|
||||
|
||||
peer.conn_handle = CONN_HANDLE_INIT;
|
||||
peer.conn = NULL;
|
||||
peer.source_ep = NULL;
|
||||
peer.sink_ep = NULL;
|
||||
peer.disc_completed = false;
|
||||
peer.disc_cancelled = false;
|
||||
peer.mtu_exchanged = false;
|
||||
|
||||
unicast_group_delete();
|
||||
|
||||
cap_initiator_unicast_start();
|
||||
}
|
||||
|
||||
static void security_change(esp_ble_iso_gap_app_event_t *event)
|
||||
{
|
||||
int err;
|
||||
|
||||
if (event->security_change.status) {
|
||||
ESP_LOGE(TAG, "security change failed, status %d", event->security_change.status);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Security change:");
|
||||
ESP_LOGI(TAG, "conn_handle 0x%04x status 0x%02x role %u sec_level %u bonded %u "
|
||||
"peer %02x:%02x:%02x:%02x:%02x:%02x",
|
||||
event->security_change.conn_handle, event->security_change.status,
|
||||
event->security_change.role, event->security_change.sec_level,
|
||||
event->security_change.bonded, event->security_change.dst.val[5],
|
||||
event->security_change.dst.val[4], event->security_change.dst.val[3],
|
||||
event->security_change.dst.val[2], event->security_change.dst.val[1],
|
||||
event->security_change.dst.val[0]);
|
||||
|
||||
err = exchange_mtu(event->security_change.conn_handle);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to exchange MTU, err %d", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void cap_initiator_unicast_gap_cb(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
switch (event->type) {
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_EXT_SCAN_RECV:
|
||||
ext_scan_recv(event);
|
||||
break;
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_ACL_CONNECT:
|
||||
acl_connect(event);
|
||||
break;
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_ACL_DISCONNECT:
|
||||
acl_disconnect(event);
|
||||
break;
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_SECURITY_CHANGE:
|
||||
security_change(event);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event)
|
||||
{
|
||||
int err;
|
||||
|
||||
ESP_LOGI(TAG, "gatt mtu change, conn_handle %u, mtu %u",
|
||||
event->gatt_mtu_change.conn_handle, event->gatt_mtu_change.mtu);
|
||||
|
||||
if (event->gatt_mtu_change.mtu < ESP_BLE_AUDIO_ATT_MTU_MIN) {
|
||||
ESP_LOGW(TAG, "Invalid new mtu %u, shall be at least %u",
|
||||
event->gatt_mtu_change.mtu, ESP_BLE_AUDIO_ATT_MTU_MIN);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_gattc_disc_start(event->gatt_mtu_change.conn_handle);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start svc disc, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Start discovering gatt services");
|
||||
|
||||
/* Note:
|
||||
* MTU exchanged event may arrived after discover completed event.
|
||||
*/
|
||||
peer.mtu_exchanged = true;
|
||||
|
||||
if (peer.disc_completed) {
|
||||
(void)discover_cas();
|
||||
}
|
||||
}
|
||||
|
||||
static void gattc_disc_cmpl(esp_ble_audio_gatt_app_event_t *event)
|
||||
{
|
||||
ESP_LOGI(TAG, "gattc disc cmpl, status %u, conn_handle %u",
|
||||
event->gattc_disc_cmpl.status, event->gattc_disc_cmpl.conn_handle);
|
||||
|
||||
if (event->gattc_disc_cmpl.status) {
|
||||
ESP_LOGE(TAG, "gattc disc failed, status %u", event->gattc_disc_cmpl.status);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Note:
|
||||
* Discover completed event may arrived before MTU exchanged event.
|
||||
*/
|
||||
peer.disc_completed = true;
|
||||
|
||||
if (peer.mtu_exchanged) {
|
||||
(void)discover_cas();
|
||||
}
|
||||
}
|
||||
|
||||
void cap_initiator_unicast_gatt_cb(esp_ble_audio_gatt_app_event_t *event)
|
||||
{
|
||||
switch (event->type) {
|
||||
case ESP_BLE_AUDIO_GATT_EVENT_GATT_MTU_CHANGE:
|
||||
gatt_mtu_change(event);
|
||||
break;
|
||||
case ESP_BLE_AUDIO_GATT_EVENT_GATTC_DISC_CMPL:
|
||||
gattc_disc_cmpl(event);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int cap_initiator_unicast_start(void)
|
||||
{
|
||||
struct ble_gap_disc_params params = {0};
|
||||
uint8_t own_addr_type;
|
||||
int err;
|
||||
|
||||
err = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
params.passive = 1;
|
||||
params.itvl = SCAN_INTERVAL;
|
||||
params.window = SCAN_WINDOW;
|
||||
|
||||
err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms,
|
||||
example_audio_gap_event_cb, NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start scanning, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Extended scan started");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cap_initiator_unicast_init(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = esp_ble_audio_cap_initiator_register_cb(&cap_cb);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register CAP callbacks, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_unicast_client_register_cb(&unicast_client_cbs);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register BAP unicast client callbacks, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "CAP initiator unicast initialized");
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
dependencies:
|
||||
example_init:
|
||||
path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_init
|
||||
example_utils:
|
||||
path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_utils
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_system.h"
|
||||
|
||||
#include "cap_initiator.h"
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
esp_ble_audio_init_info_t info = {
|
||||
#if CONFIG_EXAMPLE_UNICAST
|
||||
.gap_cb = cap_initiator_unicast_gap_cb,
|
||||
.gatt_cb = cap_initiator_unicast_gatt_cb,
|
||||
#endif /* CONFIG_EXAMPLE_UNICAST */
|
||||
};
|
||||
esp_err_t err;
|
||||
|
||||
/* Initialize NVS — it is used to store PHY calibration data */
|
||||
err = nvs_flash_init();
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
err = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(err);
|
||||
|
||||
err = bluetooth_init();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize BLE, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_common_init(&info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize audio, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
#if CONFIG_EXAMPLE_UNICAST
|
||||
err = cap_initiator_unicast_init();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
#endif /* CONFIG_EXAMPLE_UNICAST */
|
||||
|
||||
#if CONFIG_EXAMPLE_BROADCAST
|
||||
err = cap_initiator_broadcast_init();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
#endif /* CONFIG_EXAMPLE_BROADCAST */
|
||||
|
||||
cap_initiator_tx_init();
|
||||
|
||||
err = esp_ble_audio_common_start(NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start audio, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = ble_svc_gap_device_name_set("CAP Initiator");
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set device name, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
#if CONFIG_EXAMPLE_UNICAST
|
||||
err = cap_initiator_unicast_start();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
#endif /* CONFIG_EXAMPLE_UNICAST */
|
||||
|
||||
#if CONFIG_EXAMPLE_BROADCAST
|
||||
err = cap_initiator_broadcast_start();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
#endif /* CONFIG_EXAMPLE_BROADCAST */
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# This file was generated using idf.py save-defconfig. It can be edited manually.
|
||||
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
|
||||
#
|
||||
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_BLUEDROID_ENABLED=n
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_EXT_ADV=y
|
||||
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
|
||||
CONFIG_BT_NIMBLE_MAX_CCCDS=20
|
||||
CONFIG_BT_NIMBLE_ISO=y
|
||||
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
|
||||
|
||||
CONFIG_BT_ISO_MAX_CHAN=2
|
||||
|
||||
CONFIG_BT_CAP_INITIATOR=y
|
||||
CONFIG_BT_BAP_UNICAST_CLIENT=y
|
||||
CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT=2
|
||||
CONFIG_BT_BAP_BROADCAST_SOURCE=y
|
||||
CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT=2
|
||||
|
||||
CONFIG_EXAMPLE_UNICAST=y
|
||||
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
@@ -0,0 +1,6 @@
|
||||
# Override some defaults so BT stack is enabled
|
||||
# by default in this example
|
||||
|
||||
CONFIG_IDF_TARGET="esp32h4"
|
||||
|
||||
CONFIG_BT_LE_ISO_SUPPORT=y
|
||||
@@ -0,0 +1,3 @@
|
||||
idf_component_register(SRCS "ble_audio_example_init.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES bt)
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#include "nimble/nimble_port.h"
|
||||
#include "nimble/nimble_port_freertos.h"
|
||||
|
||||
#include "host/ble_hs.h"
|
||||
#include "host/ble_gap.h"
|
||||
#include "host/ble_gatt.h"
|
||||
#include "host/util/util.h"
|
||||
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
#include "services/gatt/ble_svc_gatt.h"
|
||||
|
||||
#define TAG "INIT"
|
||||
|
||||
static SemaphoreHandle_t example_audio_sem;
|
||||
|
||||
void ble_store_config_init(void);
|
||||
|
||||
static void example_audio_gatt_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg)
|
||||
{
|
||||
char buf[BLE_UUID_STR_LEN];
|
||||
|
||||
switch (ctxt->op) {
|
||||
case BLE_GATT_REGISTER_OP_SVC:
|
||||
ESP_LOGI(TAG, "Register svc %s, handle %u",
|
||||
ble_uuid_to_str(ctxt->svc.svc_def->uuid, buf),
|
||||
ctxt->svc.handle);
|
||||
break;
|
||||
|
||||
case BLE_GATT_REGISTER_OP_CHR:
|
||||
ESP_LOGI(TAG, "Register chr %s, handles %u/%u",
|
||||
ble_uuid_to_str(ctxt->chr.chr_def->uuid, buf),
|
||||
ctxt->chr.def_handle, ctxt->chr.val_handle);
|
||||
break;
|
||||
|
||||
case BLE_GATT_REGISTER_OP_DSC:
|
||||
ESP_LOGI(TAG, "Register dsc %s, handle %u",
|
||||
ble_uuid_to_str(ctxt->dsc.dsc_def->uuid, buf), ctxt->dsc.handle);
|
||||
break;
|
||||
|
||||
default:
|
||||
assert(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void example_audio_on_reset(int reason)
|
||||
{
|
||||
ESP_LOGI(TAG, "Resetting state; reason=%d", reason);
|
||||
}
|
||||
|
||||
static void example_audio_on_sync(void)
|
||||
{
|
||||
int rc;
|
||||
|
||||
rc = ble_hs_util_ensure_addr(0);
|
||||
assert(rc == 0);
|
||||
|
||||
xSemaphoreGive(example_audio_sem);
|
||||
}
|
||||
|
||||
static void example_audio_host_task(void *param)
|
||||
{
|
||||
ESP_LOGI(TAG, "BLE Host Task Started");
|
||||
|
||||
/* This function will return only when nimble_port_stop() is executed */
|
||||
nimble_port_run();
|
||||
|
||||
nimble_port_freertos_deinit();
|
||||
}
|
||||
|
||||
esp_err_t bluetooth_init(void)
|
||||
{
|
||||
esp_err_t ret;
|
||||
|
||||
example_audio_sem = xSemaphoreCreateBinary();
|
||||
if (example_audio_sem == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to create audio semaphore");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ret = nimble_port_init();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to init nimble %d ", ret);
|
||||
vSemaphoreDelete(example_audio_sem);
|
||||
example_audio_sem = NULL;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Initialize the NimBLE host configuration */
|
||||
ble_hs_cfg.gatts_register_cb = example_audio_gatt_register_cb;
|
||||
ble_hs_cfg.reset_cb = example_audio_on_reset;
|
||||
ble_hs_cfg.sync_cb = example_audio_on_sync;
|
||||
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
|
||||
#if 0
|
||||
ble_hs_cfg.sm_our_key_dist = 1;
|
||||
ble_hs_cfg.sm_their_key_dist = 1;
|
||||
ble_hs_cfg.sm_sc = 1;
|
||||
ble_hs_cfg.sm_mitm = 0;
|
||||
ble_hs_cfg.sm_bonding = 1;
|
||||
ble_hs_cfg.sm_io_cap = BLE_SM_IO_CAP_NO_IO;
|
||||
#endif
|
||||
|
||||
/* XXX Need to have template for store */
|
||||
ble_store_config_init();
|
||||
|
||||
nimble_port_freertos_init(example_audio_host_task);
|
||||
|
||||
xSemaphoreTake(example_audio_sem, portMAX_DELAY);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#ifndef BLE_AUDIO_EXAMPLE_INIT_H_
|
||||
#define BLE_AUDIO_EXAMPLE_INIT_H_
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
esp_err_t bluetooth_init(void);
|
||||
|
||||
#endif /* BLE_AUDIO_EXAMPLE_INIT_H_ */
|
||||
@@ -0,0 +1,3 @@
|
||||
idf_component_register(SRCS "ble_audio_example_utils.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES bt esp_timer freertos)
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021-2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileCopyrightText: 2024 Demant A/S
|
||||
* SPDX-FileCopyrightText: 2015-2016 Intel Corporation
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "ble_audio_example_utils.h"
|
||||
|
||||
#include "esp_ble_audio_common_api.h"
|
||||
|
||||
#define TAG "UTILS"
|
||||
|
||||
#define LOG_GREEN "\033[0;" "32" "m"
|
||||
#define LOG_RESET "\033[0m"
|
||||
|
||||
int example_audio_gap_event_cb(struct ble_gap_event *event, void *arg)
|
||||
{
|
||||
if (event->type == BLE_GAP_EVENT_EXT_DISC ||
|
||||
event->type == BLE_GAP_EVENT_PERIODIC_SYNC ||
|
||||
event->type == BLE_GAP_EVENT_PERIODIC_REPORT ||
|
||||
event->type == BLE_GAP_EVENT_PERIODIC_SYNC_LOST ||
|
||||
event->type == BLE_GAP_EVENT_CONNECT ||
|
||||
event->type == BLE_GAP_EVENT_DISCONNECT ||
|
||||
event->type == BLE_GAP_EVENT_ENC_CHANGE) {
|
||||
esp_ble_audio_gap_app_post_event(event->type, event);
|
||||
} else if (event->type == BLE_GAP_EVENT_MTU ||
|
||||
event->type == BLE_GAP_EVENT_NOTIFY_RX ||
|
||||
event->type == BLE_GAP_EVENT_NOTIFY_TX ||
|
||||
event->type == BLE_GAP_EVENT_SUBSCRIBE) {
|
||||
esp_ble_audio_gatt_app_post_event(event->type, event);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void print_hex(const uint8_t *ptr, size_t len)
|
||||
{
|
||||
while (len--) {
|
||||
printf(LOG_GREEN "%02x" LOG_RESET, *ptr++);
|
||||
}
|
||||
}
|
||||
|
||||
static bool print_cb(uint8_t type, const uint8_t *data,
|
||||
uint8_t data_len, void *user_data)
|
||||
{
|
||||
const char *str = (const char *)user_data;
|
||||
|
||||
printf(LOG_GREEN "I (%lu) %s: %s, type 0x%02x len %u data " LOG_RESET,
|
||||
esp_log_timestamp(), TAG, str, type, data_len);
|
||||
print_hex(data, data_len);
|
||||
printf("\n");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void example_print_codec_cfg(const esp_ble_audio_codec_cfg_t *codec_cfg)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "codec_cfg, id 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 == ESP_BLE_ISO_CODING_FORMAT_LC3) {
|
||||
esp_ble_audio_codec_cfg_frame_dur_t frame_dur;
|
||||
esp_ble_audio_location_t chan_allocation;
|
||||
esp_ble_audio_codec_cfg_freq_t freq;
|
||||
uint16_t octets_per_frame;
|
||||
uint32_t frame_dur_us;
|
||||
uint8_t frame_blocks;
|
||||
uint32_t freq_hz;
|
||||
|
||||
/* LC3 uses the generic LTV format - other codecs might do as well */
|
||||
|
||||
err = esp_ble_audio_data_parse(codec_cfg->data, codec_cfg->data_len, print_cb, "data");
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to parse codec_cfg data");
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_codec_cfg_get_freq(codec_cfg, &freq);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get frequency");
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_codec_cfg_freq_to_freq_hz(freq, &freq_hz);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get frequency hz");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Frequency: %lu Hz", freq_hz);
|
||||
|
||||
err = esp_ble_audio_codec_cfg_get_frame_dur(codec_cfg, &frame_dur);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get frame duration");
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_codec_cfg_frame_dur_to_frame_dur_us(frame_dur, &frame_dur_us);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get frame duration us");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Frame Duration: %lu us", frame_dur_us);
|
||||
|
||||
err = esp_ble_audio_codec_cfg_get_chan_allocation(codec_cfg, &chan_allocation, false);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get channel allocation");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Channel allocation: 0x%08lx", chan_allocation);
|
||||
|
||||
err = esp_ble_audio_codec_cfg_get_octets_per_frame(codec_cfg, &octets_per_frame);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get octets per frame");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Octets per frame: %u", octets_per_frame);
|
||||
|
||||
err = esp_ble_audio_codec_cfg_get_frame_blocks_per_sdu(codec_cfg, &frame_blocks, true);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get frame blocks per sdu");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Frames per SDU: %u", frame_blocks);
|
||||
} else {
|
||||
print_hex(codec_cfg->data, codec_cfg->data_len);
|
||||
}
|
||||
|
||||
err = esp_ble_audio_data_parse(codec_cfg->meta, codec_cfg->meta_len, print_cb, "meta");
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to parse codec_cfg meta");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void example_print_codec_cap(const esp_ble_audio_codec_cap_t *codec_cap)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "codec_cap, id 0x%02x cid 0x%04x vid 0x%04x count %u",
|
||||
codec_cap->id, codec_cap->cid,
|
||||
codec_cap->vid, codec_cap->data_len);
|
||||
|
||||
if (codec_cap->id == ESP_BLE_ISO_CODING_FORMAT_LC3) {
|
||||
err = esp_ble_audio_data_parse(codec_cap->data, codec_cap->data_len, print_cb, "data");
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to parse codec_cap data");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
printf(LOG_GREEN "I (%lu) %s: data: " LOG_RESET, esp_log_timestamp(), TAG);
|
||||
print_hex(codec_cap->data, codec_cap->data_len);
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
err = esp_ble_audio_data_parse(codec_cap->meta, codec_cap->meta_len, print_cb, "meta");
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to parse codec_cap meta");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void example_print_qos(const esp_ble_audio_bap_qos_cfg_t *qos)
|
||||
{
|
||||
ESP_LOGI(TAG, "QoS: interval %u framing 0x%02x phy 0x%02x", qos->interval, qos->framing, qos->phy);
|
||||
ESP_LOGI(TAG, " sdu %u rtn %u latency %u pd %u", qos->sdu, qos->rtn, qos->latency, qos->pd);
|
||||
}
|
||||
|
||||
void example_print_qos_pref(const esp_ble_audio_bap_qos_cfg_pref_t *pref)
|
||||
{
|
||||
ESP_LOGI(TAG, "QoS pref: unframed %s, phy %u, rtn %u, latency %u",
|
||||
pref->unframed_supported ? "supported" : "not supported",
|
||||
pref->phy, pref->rtn, pref->latency);
|
||||
ESP_LOGI(TAG, " pd_min %u, pd_max %u",
|
||||
pref->pd_min, pref->pd_max);
|
||||
ESP_LOGI(TAG, " pref_pd_min %u, pref_pd_max %u",
|
||||
pref->pref_pd_min, pref->pref_pd_max);
|
||||
}
|
||||
|
||||
bool example_is_substring(const char *substr, const char *str)
|
||||
{
|
||||
const size_t sub_str_len = strlen(substr);
|
||||
const size_t str_len = strlen(str);
|
||||
|
||||
if (sub_str_len > str_len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t pos = 0; pos < str_len; pos++) {
|
||||
if (pos + sub_str_len > str_len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strncasecmp(substr, &str[pos], sub_str_len) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void example_audio_tx_work_handler(struct k_work *work)
|
||||
{
|
||||
example_audio_tx_scheduler_t *scheduler = work->user_data;
|
||||
size_t count;
|
||||
|
||||
assert(scheduler);
|
||||
assert(scheduler->cb);
|
||||
|
||||
count = 1 + scheduler->drift;
|
||||
scheduler->drift = 0;
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
scheduler->cb(scheduler->arg);
|
||||
}
|
||||
}
|
||||
|
||||
int example_audio_tx_scheduler_init(example_audio_tx_scheduler_t *scheduler,
|
||||
example_audio_tx_send_cb_t cb,
|
||||
void *arg)
|
||||
{
|
||||
assert(scheduler);
|
||||
assert(cb);
|
||||
|
||||
memset(scheduler, 0, sizeof(*scheduler));
|
||||
scheduler->cb = cb;
|
||||
scheduler->arg = arg;
|
||||
|
||||
k_work_init_delayable(&scheduler->timer, example_audio_tx_work_handler);
|
||||
scheduler->timer.work.user_data = scheduler;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void example_audio_tx_scheduler_reset(example_audio_tx_scheduler_t *scheduler)
|
||||
{
|
||||
assert(scheduler);
|
||||
|
||||
scheduler->drift = 0;
|
||||
scheduler->count = 0;
|
||||
}
|
||||
|
||||
int example_audio_tx_scheduler_start(example_audio_tx_scheduler_t *scheduler,
|
||||
uint64_t period_us)
|
||||
{
|
||||
assert(scheduler);
|
||||
assert(period_us > 0);
|
||||
|
||||
return k_work_schedule_periodic(&scheduler->timer, (uint32_t)(period_us / 1000));
|
||||
}
|
||||
|
||||
int example_audio_tx_scheduler_stop(example_audio_tx_scheduler_t *scheduler)
|
||||
{
|
||||
assert(scheduler);
|
||||
|
||||
return k_work_cancel_delayable(&scheduler->timer);
|
||||
}
|
||||
|
||||
void example_audio_tx_scheduler_on_sent(example_audio_tx_scheduler_t *scheduler,
|
||||
const esp_ble_iso_tx_cb_info_t *info,
|
||||
const char *tag,
|
||||
const char *obj_name,
|
||||
const void *obj)
|
||||
{
|
||||
size_t drift_count = 0;
|
||||
|
||||
assert(scheduler);
|
||||
assert(info);
|
||||
|
||||
for (size_t i = 0; i < info->pkt_cnt; i++) {
|
||||
if (info->pkt[i].drift) {
|
||||
drift_count++;
|
||||
}
|
||||
}
|
||||
|
||||
scheduler->drift += drift_count;
|
||||
scheduler->count++;
|
||||
|
||||
if (scheduler->count % 1000 == 0) {
|
||||
ESP_LOGI(tag, "Transmitted %u ISO data packets (%s %p)",
|
||||
scheduler->count, (obj_name ? obj_name : ""), obj);
|
||||
}
|
||||
}
|
||||
|
||||
void example_audio_rx_metrics_reset(example_audio_rx_metrics_t *metrics)
|
||||
{
|
||||
assert(metrics);
|
||||
|
||||
metrics->recv_count = 0;
|
||||
metrics->valid_count = 0;
|
||||
metrics->error_count = 0;
|
||||
metrics->lost_count = 0;
|
||||
metrics->null_sdu_count = 0;
|
||||
metrics->last_sdu_len = 0;
|
||||
}
|
||||
|
||||
void example_audio_rx_metrics_on_recv(const esp_ble_iso_recv_info_t *info,
|
||||
example_audio_rx_metrics_t *metrics,
|
||||
const char *tag,
|
||||
const char *obj_name,
|
||||
const void *obj)
|
||||
{
|
||||
assert(info);
|
||||
assert(metrics);
|
||||
|
||||
if (info->flags & ESP_BLE_ISO_FLAGS_ERROR) {
|
||||
metrics->error_count++;
|
||||
}
|
||||
|
||||
if (info->flags & ESP_BLE_ISO_FLAGS_LOST) {
|
||||
metrics->lost_count++;
|
||||
}
|
||||
|
||||
if (info->flags & ESP_BLE_ISO_FLAGS_VALID) {
|
||||
metrics->valid_count++;
|
||||
if (metrics->last_sdu_len == 0) {
|
||||
metrics->null_sdu_count++;
|
||||
}
|
||||
}
|
||||
|
||||
metrics->recv_count++;
|
||||
|
||||
if (metrics->recv_count % 1000 == 0) {
|
||||
ESP_LOGI(tag, "Received %u ISO data packets (%s %p)",
|
||||
metrics->recv_count, (obj_name ? obj_name : ""), obj);
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021-2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileCopyrightText: 2024 Demant A/S
|
||||
* SPDX-FileCopyrightText: 2015-2016 Intel Corporation
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#ifndef BLE_AUDIO_EXAMPLE_UTILS_H_
|
||||
#define BLE_AUDIO_EXAMPLE_UTILS_H_
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#include "zephyr/kernel.h"
|
||||
|
||||
#include "host/ble_gap.h"
|
||||
#include "host/ble_hs_adv.h"
|
||||
|
||||
#include "esp_ble_audio_bap_api.h"
|
||||
|
||||
#define EXAMPLE_AD_TYPE_FLAGS BT_DATA_FLAGS
|
||||
#define EXAMPLE_AD_TYPE_UUID16_SOME BT_DATA_UUID16_SOME
|
||||
#define EXAMPLE_AD_TYPE_UUID16_ALL BT_DATA_UUID16_ALL
|
||||
#define EXAMPLE_AD_TYPE_NAME_SHORTENED BT_DATA_NAME_SHORTENED
|
||||
#define EXAMPLE_AD_TYPE_NAME_COMPLETE BT_DATA_NAME_COMPLETE
|
||||
#define EXAMPLE_AD_TYPE_SERVICE_DATA16 BT_DATA_SVC_DATA16
|
||||
#define EXAMPLE_AD_TYPE_GAP_APPEARANCE BT_DATA_GAP_APPEARANCE
|
||||
#define EXAMPLE_AD_TYPE_CSIS_RSI BT_DATA_CSIS_RSI
|
||||
#define EXAMPLE_AD_TYPE_BROADCAST_NAME BT_DATA_BROADCAST_NAME
|
||||
|
||||
#define EXAMPLE_AD_FLAGS_LIMITED BT_LE_AD_LIMITED
|
||||
#define EXAMPLE_AD_FLAGS_GENERAL BT_LE_AD_GENERAL
|
||||
#define EXAMPLE_AD_FLAGS_NO_BREDR BT_LE_AD_NO_BREDR
|
||||
|
||||
#define EXAMPLE_ADV_PROP_CONNECTABLE BT_GAP_ADV_PROP_CONNECTABLE
|
||||
#define EXAMPLE_ADV_PROP_SCANNABLE BT_GAP_ADV_PROP_SCANNABLE
|
||||
#define EXAMPLE_ADV_PROP_DIRECTED BT_GAP_ADV_PROP_DIRECTED
|
||||
#define EXAMPLE_ADV_PROP_SCAN_RESPONSE BT_GAP_ADV_PROP_SCAN_RESPONSE
|
||||
#define EXAMPLE_ADV_PROP_EXT_ADV BT_GAP_ADV_PROP_EXT_ADV
|
||||
|
||||
#define EXAMPLE_BYTES_LIST_LE16 BT_BYTES_LIST_LE16
|
||||
#define EXAMPLE_BYTES_LIST_LE24 BT_BYTES_LIST_LE24
|
||||
#define EXAMPLE_BYTES_LIST_LE32 BT_BYTES_LIST_LE32
|
||||
#define EXAMPLE_BYTES_LIST_LE40 BT_BYTES_LIST_LE40
|
||||
#define EXAMPLE_BYTES_LIST_LE48 BT_BYTES_LIST_LE48
|
||||
#define EXAMPLE_BYTES_LIST_LE64 BT_BYTES_LIST_LE64
|
||||
|
||||
int example_audio_gap_event_cb(struct ble_gap_event *event, void *arg);
|
||||
|
||||
void example_print_codec_cfg(const esp_ble_audio_codec_cfg_t *codec_cfg);
|
||||
|
||||
void example_print_codec_cap(const esp_ble_audio_codec_cap_t *codec_cap);
|
||||
|
||||
void example_print_qos(const esp_ble_audio_bap_qos_cfg_t *qos);
|
||||
|
||||
void example_print_qos_pref(const esp_ble_audio_bap_qos_cfg_pref_t *pref);
|
||||
|
||||
bool example_is_substring(const char *substr, const char *str);
|
||||
|
||||
/**
|
||||
* @brief TX scheduler for periodic audio data transmission.
|
||||
*
|
||||
* Uses k_work_delayable to schedule send callbacks in the ISO task context,
|
||||
* ensuring thread safety without mutexes since both the timer handler and
|
||||
* BLE audio callbacks execute in the same task.
|
||||
*/
|
||||
typedef void (*example_audio_tx_send_cb_t)(void *ctx);
|
||||
|
||||
typedef struct {
|
||||
struct k_work_delayable timer;
|
||||
size_t drift;
|
||||
uint32_t count;
|
||||
/* Called periodically in ISO task context to transmit data */
|
||||
example_audio_tx_send_cb_t cb;
|
||||
void *arg;
|
||||
} example_audio_tx_scheduler_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t recv_count;
|
||||
uint32_t valid_count;
|
||||
uint32_t error_count;
|
||||
uint32_t lost_count;
|
||||
uint32_t null_sdu_count;
|
||||
uint16_t last_sdu_len;
|
||||
} example_audio_rx_metrics_t;
|
||||
|
||||
int example_audio_tx_scheduler_init(example_audio_tx_scheduler_t *scheduler,
|
||||
example_audio_tx_send_cb_t cb,
|
||||
void *arg);
|
||||
|
||||
void example_audio_tx_scheduler_reset(example_audio_tx_scheduler_t *scheduler);
|
||||
|
||||
int example_audio_tx_scheduler_start(example_audio_tx_scheduler_t *scheduler,
|
||||
uint64_t period_us);
|
||||
|
||||
int example_audio_tx_scheduler_stop(example_audio_tx_scheduler_t *scheduler);
|
||||
|
||||
void example_audio_tx_scheduler_on_sent(example_audio_tx_scheduler_t *scheduler,
|
||||
const esp_ble_iso_tx_cb_info_t *info,
|
||||
const char *tag,
|
||||
const char *obj_name,
|
||||
const void *obj);
|
||||
|
||||
void example_audio_rx_metrics_reset(example_audio_rx_metrics_t *metrics);
|
||||
|
||||
void example_audio_rx_metrics_on_recv(const esp_ble_iso_recv_info_t *info,
|
||||
example_audio_rx_metrics_t *metrics,
|
||||
const char *tag,
|
||||
const char *obj_name,
|
||||
const void *obj);
|
||||
|
||||
#endif /* BLE_AUDIO_EXAMPLE_UTILS_H_ */
|
||||
@@ -0,0 +1,8 @@
|
||||
# The following lines of boilerplate have to be in your project's CMakeLists
|
||||
# in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
|
||||
idf_build_set_property(MINIMAL_BUILD ON)
|
||||
project(tmap_bmr)
|
||||
@@ -0,0 +1,62 @@
|
||||
| Supported Targets | ESP32-H4 |
|
||||
| ----------------- | -------- |
|
||||
|
||||
# TMAP Broadcast Media Receiver (BMR) Example
|
||||
|
||||
(See the README.md file in the upper level `examples` directory for more information about examples.)
|
||||
|
||||
This example implements the **Telephone and Media Audio Profile (TMAP) Broadcast Media Receiver (BMR)** role. The BMR scans for broadcast sources that advertise both the Broadcast Audio service (broadcast ID) and the TMAP role TMAS with **BMS (Broadcast Media Sender)**. When such a source is found, it establishes periodic advertising sync, creates a BAP Broadcast Sink, and synchronizes to the BIG to receive broadcast audio. It also registers as a **VCP Volume Renderer** (Volume Control Profile) for local volume and mute. The BMR can work with a TMAP BMS or any BAP Broadcast Source that includes the TMAP BMS role in advertising (e.g. a device running the [broadcast_source](../../bap/broadcast_source) or [CAP initiator](../../cap/initiator) in broadcast mode, if configured as BMS).
|
||||
|
||||
The implementation uses the NimBLE host stack with ISO and LE Audio support, ESP-BLE-AUDIO (TMAP BMR role, VCP volume renderer, PACS sink, BAP broadcast sink, BAP scan delegator). PACS advertises LC3 sink capability (e.g. 48 kHz, 10 ms, mono, media context). After startup the device starts scanning; on matching scan results it creates PA sync, then creates the broadcast sink and syncs to the BIS streams; received audio is counted in the stream recv callback.
|
||||
|
||||
## Requirements
|
||||
|
||||
* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4)
|
||||
* A broadcast source that advertises as TMAP BMS (Broadcast Media Sender) and sends broadcast audio (e.g. another board running a broadcast source example with TMAP BMS role)
|
||||
|
||||
## How to Use Example
|
||||
|
||||
Before project configuration and build, set the correct chip target:
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32h4
|
||||
```
|
||||
|
||||
### Build and Flash
|
||||
|
||||
Run the following to build, flash and monitor:
|
||||
|
||||
```bash
|
||||
idf.py -p PORT flash monitor
|
||||
```
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF.
|
||||
|
||||
## Example Flow
|
||||
|
||||
1. **Initialization**: NVS, Bluetooth stack, and LE Audio common layer (`esp_ble_audio_common_init`) with GAP callback. Register TMAP role BMR (`esp_ble_audio_tmap_register(ESP_BLE_AUDIO_TMAP_ROLE_BMR)`). Register VCP Volume Renderer (volume step, mute, volume, state/flags callbacks). Initialize BAP broadcast sink: register PACS (sink only), register broadcast sink callbacks (base_recv, syncable), register PACS sink capability (LC3), register scan delegator callbacks, set stream ops (started, stopped, recv). Start the audio stack.
|
||||
2. **Scanning**: Start extended scanning. On scan result, parse for Broadcast Audio UUID (broadcast ID) and TMAS with BMS role; if both found and not already syncing, create periodic advertising sync with the source.
|
||||
3. **PA sync**: When PA sync is established, stop scanning, create the BAP broadcast sink for the sync handle and broadcast ID. When BASE is received (base_recv), extract BIS indexes; when syncable callback is invoked, sync to the BIG with the chosen BIS and stream pointers.
|
||||
4. **Streaming**: When streams start, reset packet counters. Received ISO SDUs are delivered in the stream recv callback; the example counts valid, error, lost, and zero-length SDU and logs periodically.
|
||||
5. **VCP**: The Volume Renderer exposes volume and mute state; VCS state/flags callbacks log volume and mute changes when queried.
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
I (xxx) TMAP_BMR: Found TMAP BMS
|
||||
I (xxx) TMAP_BMR: PA synced, handle 0x... status 0x00
|
||||
I (xxx) TMAP_BMR: PA sync ... synced with broadcast ID 0x...
|
||||
I (xxx) TMAP_BMR: Broadcast source PA synced, waiting for BASE
|
||||
I (xxx) TMAP_BMR: BASE received, creating broadcast sink
|
||||
I (xxx) TMAP_BMR: Stream 0x... started
|
||||
I (xxx) TMAP_BMR: Received 1000(...) ISO data packets (stream 0x...)
|
||||
...
|
||||
```
|
||||
|
||||
If PA sync is lost:
|
||||
|
||||
```
|
||||
I (xxx) TMAP_BMR: PA sync lost, handle 0x... reason ...
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
set(srcs "bap_broadcast_sink.c"
|
||||
"vcp_vol_renderer.c"
|
||||
"main.c")
|
||||
|
||||
idf_component_register(SRCS "${srcs}"
|
||||
REQUIRES bt nvs_flash)
|
||||
@@ -0,0 +1,350 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2022-2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileCopyrightText: 2023 NXP
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "tmap_bmr.h"
|
||||
|
||||
#define SCAN_INTERVAL 160 /* 100ms */
|
||||
#define SCAN_WINDOW 160 /* 100ms */
|
||||
|
||||
#define PA_SYNC_SKIP 0
|
||||
#define PA_SYNC_TIMEOUT 1000 /* 1000 * 10ms = 10s */
|
||||
#define PA_SYNC_HANDLE_INIT UINT16_MAX
|
||||
|
||||
static uint16_t sync_handle = PA_SYNC_HANDLE_INIT;
|
||||
static bool pa_syncing;
|
||||
|
||||
static bool tmap_bms_found;
|
||||
|
||||
static esp_ble_audio_bap_broadcast_sink_t *broadcast_sink;
|
||||
static uint32_t bcast_id;
|
||||
|
||||
static esp_ble_audio_bap_stream_t streams[CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT];
|
||||
static esp_ble_audio_bap_stream_t *streams_p[ARRAY_SIZE(streams)];
|
||||
|
||||
static uint8_t codec_data[] =
|
||||
ESP_BLE_AUDIO_CODEC_CAP_LC3_DATA(
|
||||
ESP_BLE_AUDIO_CODEC_CAP_FREQ_48KHZ, /* Sampling frequency 48kHz */
|
||||
ESP_BLE_AUDIO_CODEC_CAP_DURATION_10, /* Frame duration 10ms */
|
||||
ESP_BLE_AUDIO_CODEC_CAP_CHAN_COUNT_SUPPORT(1), /* Supported channels 1 */
|
||||
40, /* Minimum 40 octets per frame */
|
||||
60, /* Maximum 60 octets per frame */
|
||||
1); /* Maximum 1 codec frame per SDU */
|
||||
|
||||
static uint8_t codec_meta[] =
|
||||
ESP_BLE_AUDIO_CODEC_CAP_LC3_META(ESP_BLE_AUDIO_CONTEXT_TYPE_MEDIA);
|
||||
|
||||
static const esp_ble_audio_codec_cap_t codec =
|
||||
ESP_BLE_AUDIO_CODEC_CAP_LC3(codec_data, codec_meta);
|
||||
|
||||
/* Create a mask for the maximum BIS we can sync to using the number of
|
||||
* streams we have.
|
||||
* We add an additional 1 since the bis indexes start from 1 and not 0.
|
||||
*/
|
||||
static const uint32_t bis_index_mask = BIT_MASK(ARRAY_SIZE(streams) + 1);
|
||||
static uint32_t bis_index_bitfield;
|
||||
|
||||
static example_audio_rx_metrics_t rx_metrics;
|
||||
|
||||
static void stream_started_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
ESP_LOGI(TAG, "Stream %p started", stream);
|
||||
|
||||
example_audio_rx_metrics_reset(&rx_metrics);
|
||||
}
|
||||
|
||||
static void stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
|
||||
{
|
||||
ESP_LOGI(TAG, "Stream %p stopped, reason 0x%02x", stream, reason);
|
||||
}
|
||||
|
||||
static void stream_recv_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
const esp_ble_iso_recv_info_t *info,
|
||||
const uint8_t *data, uint16_t len)
|
||||
{
|
||||
|
||||
rx_metrics.last_sdu_len = len;
|
||||
example_audio_rx_metrics_on_recv(info, &rx_metrics, TAG, "stream", stream);
|
||||
}
|
||||
|
||||
static esp_ble_audio_bap_stream_ops_t stream_ops = {
|
||||
.started = stream_started_cb,
|
||||
.stopped = stream_stopped_cb,
|
||||
.recv = stream_recv_cb
|
||||
};
|
||||
|
||||
static esp_ble_audio_pacs_cap_t cap = {
|
||||
.codec_cap = &codec,
|
||||
};
|
||||
|
||||
static void base_recv_cb(esp_ble_audio_bap_broadcast_sink_t *sink,
|
||||
const esp_ble_audio_bap_base_t *base,
|
||||
size_t base_size)
|
||||
{
|
||||
uint32_t base_bis_index_bitfield = 0;
|
||||
int err;
|
||||
|
||||
assert(base);
|
||||
|
||||
err = esp_ble_audio_bap_base_get_bis_indexes(base, &base_bis_index_bitfield);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get bis indexes from BASE, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
bis_index_bitfield = (base_bis_index_bitfield & bis_index_mask);
|
||||
}
|
||||
|
||||
static void syncable_cb(esp_ble_audio_bap_broadcast_sink_t *sink,
|
||||
const esp_ble_iso_biginfo_t *biginfo)
|
||||
{
|
||||
int err;
|
||||
|
||||
ESP_LOGI(TAG, "BASE received, creating broadcast sink");
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_sink_sync(broadcast_sink,
|
||||
bis_index_bitfield,
|
||||
streams_p, NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to sync to broadcast source, err %d", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static esp_ble_audio_bap_broadcast_sink_cb_t broadcast_sink_cbs = {
|
||||
.base_recv = base_recv_cb,
|
||||
.syncable = syncable_cb,
|
||||
};
|
||||
|
||||
static esp_ble_audio_bap_scan_delegator_cb_t scan_delegator_cbs;
|
||||
|
||||
static void sync_broadcast_pa(const bt_addr_le_t *addr,
|
||||
uint8_t adv_sid,
|
||||
uint32_t broadcast_id)
|
||||
{
|
||||
struct ble_gap_periodic_sync_params params = {0};
|
||||
ble_addr_t sync_addr = {0};
|
||||
int err;
|
||||
|
||||
sync_addr.type = addr->type;
|
||||
memcpy(sync_addr.val, addr->a.val, sizeof(sync_addr.val));
|
||||
params.skip = PA_SYNC_SKIP;
|
||||
params.sync_timeout = PA_SYNC_TIMEOUT;
|
||||
|
||||
err = ble_gap_periodic_adv_sync_create(&sync_addr, adv_sid, ¶ms,
|
||||
example_audio_gap_event_cb, NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to create PA sync, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
bcast_id = broadcast_id;
|
||||
pa_syncing = true; /* Mark PA sync as in progress */
|
||||
}
|
||||
|
||||
static bool scan_check_and_sync_broadcast(uint8_t type, const uint8_t *data,
|
||||
uint8_t data_len, void *user_data)
|
||||
{
|
||||
uint32_t *broadcast_id = user_data;
|
||||
uint16_t tmap_role;
|
||||
uint16_t uuid_val;
|
||||
|
||||
if (type != EXAMPLE_AD_TYPE_SERVICE_DATA16) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (data_len < sizeof(uuid_val)) {
|
||||
ESP_LOGW(TAG, "Invalid ad size %u (uuid)", data_len);
|
||||
return true;
|
||||
}
|
||||
|
||||
uuid_val = sys_get_le16(data);
|
||||
|
||||
if (uuid_val == ESP_BLE_AUDIO_UUID_BROADCAST_AUDIO_VAL) {
|
||||
if (data_len < sizeof(uuid_val) + 3) {
|
||||
ESP_LOGW(TAG, "Invalid ad size %u (Broadcast ID)", data_len);
|
||||
return true;
|
||||
}
|
||||
|
||||
*broadcast_id = sys_get_le24(data + sizeof(uuid_val));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (uuid_val != ESP_BLE_AUDIO_UUID_TMAS_VAL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (data_len < sizeof(uuid_val) + sizeof(tmap_role)) {
|
||||
ESP_LOGW(TAG, "Invalid ad size %u (tmap role)", data_len);
|
||||
return false; /* Stop parsing */
|
||||
}
|
||||
|
||||
tmap_role = sys_get_le16(data + sizeof(uuid_val));
|
||||
|
||||
if (tmap_role & ESP_BLE_AUDIO_TMAP_ROLE_BMS) {
|
||||
ESP_LOGI(TAG, "Found TMAP BMS");
|
||||
|
||||
tmap_bms_found = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; /* Stop parsing */
|
||||
}
|
||||
|
||||
void bap_broadcast_scan_recv(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
uint32_t broadcast_id;
|
||||
bt_addr_le_t addr;
|
||||
|
||||
if ((event->ext_scan_recv.event_type & EXAMPLE_ADV_PROP_CONNECTABLE) ||
|
||||
event->ext_scan_recv.per_adv_itvl == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
tmap_bms_found = false;
|
||||
|
||||
broadcast_id = ESP_BLE_AUDIO_BAP_INVALID_BROADCAST_ID;
|
||||
|
||||
esp_ble_audio_data_parse(event->ext_scan_recv.data,
|
||||
event->ext_scan_recv.data_len,
|
||||
scan_check_and_sync_broadcast,
|
||||
(void *)&broadcast_id);
|
||||
|
||||
if (broadcast_id != ESP_BLE_AUDIO_BAP_INVALID_BROADCAST_ID &&
|
||||
tmap_bms_found &&
|
||||
pa_syncing == false) {
|
||||
addr.type = event->ext_scan_recv.addr.type;
|
||||
memcpy(addr.a.val, event->ext_scan_recv.addr.val, sizeof(addr.a.val));
|
||||
|
||||
sync_broadcast_pa(&addr, event->ext_scan_recv.sid, broadcast_id);
|
||||
}
|
||||
}
|
||||
|
||||
void bap_broadcast_pa_sync(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
int err;
|
||||
|
||||
pa_syncing = false; /* Mark PA sync as completed */
|
||||
|
||||
if (event->pa_sync.status) {
|
||||
ESP_LOGE(TAG, "PA sync failed, status %d", event->pa_sync.status);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "PA sync %u synced with broadcast ID 0x%06x",
|
||||
event->pa_sync.sync_handle, bcast_id);
|
||||
|
||||
err = ble_gap_disc_cancel();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop scanning, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Broadcast source PA synced, waiting for BASE");
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_sink_create(event->pa_sync.sync_handle,
|
||||
bcast_id, &broadcast_sink);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to create broadcast sink, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
sync_handle = event->pa_sync.sync_handle;
|
||||
}
|
||||
|
||||
void bap_broadcast_pa_lost(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
if (sync_handle == event->pa_sync_lost.sync_handle) {
|
||||
ESP_LOGI(TAG, "PA sync %u lost with reason %u",
|
||||
sync_handle, event->pa_sync_lost.reason);
|
||||
|
||||
sync_handle = PA_SYNC_HANDLE_INIT;
|
||||
|
||||
if (broadcast_sink != NULL) {
|
||||
esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink);
|
||||
broadcast_sink = NULL;
|
||||
}
|
||||
|
||||
bap_broadcast_sink_scan();
|
||||
}
|
||||
}
|
||||
|
||||
int bap_broadcast_sink_scan(void)
|
||||
{
|
||||
struct ble_gap_disc_params params = {0};
|
||||
uint8_t own_addr_type;
|
||||
int err;
|
||||
|
||||
err = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
params.passive = 1;
|
||||
params.itvl = SCAN_INTERVAL;
|
||||
params.window = SCAN_WINDOW;
|
||||
|
||||
err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms,
|
||||
example_audio_gap_event_cb, NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start scanning, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Extended scan started");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int bap_broadcast_sink_init(void)
|
||||
{
|
||||
const esp_ble_audio_pacs_register_param_t pacs_param = {
|
||||
.snk_pac = true,
|
||||
.snk_loc = true,
|
||||
};
|
||||
int err;
|
||||
|
||||
err = esp_ble_audio_pacs_register(&pacs_param);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register pacs, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_sink_register_cb(&broadcast_sink_cbs);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register broadcast sink cb, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_pacs_cap_register(ESP_BLE_AUDIO_DIR_SINK, &cap);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register pacs capabilities, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_scan_delegator_register(&scan_delegator_cbs);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register scan delegator, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(streams); i++) {
|
||||
streams[i].ops = &stream_ops;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(streams_p); i++) {
|
||||
streams_p[i] = &streams[i];
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "BAP broadcast sink initialized");
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
dependencies:
|
||||
example_init:
|
||||
path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_init
|
||||
example_utils:
|
||||
path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_utils
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 NXP
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_system.h"
|
||||
|
||||
#include "tmap_bmr.h"
|
||||
|
||||
static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
bap_broadcast_scan_recv(event);
|
||||
}
|
||||
|
||||
static void pa_sync(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
ESP_LOGI(TAG, "PA synced:");
|
||||
ESP_LOGI(TAG, "sync_handle 0x%04x status 0x%02x addr %02x:%02x:%02x:%02x:%02x:%02x "
|
||||
"sid %u adv_phy %u per_adv_itvl 0x%04x adv_ca %u",
|
||||
event->pa_sync.sync_handle, event->pa_sync.status,
|
||||
event->pa_sync.addr.val[5], event->pa_sync.addr.val[4],
|
||||
event->pa_sync.addr.val[3], event->pa_sync.addr.val[2],
|
||||
event->pa_sync.addr.val[1], event->pa_sync.addr.val[0],
|
||||
event->pa_sync.sid, event->pa_sync.adv_phy,
|
||||
event->pa_sync.per_adv_itvl, event->pa_sync.adv_ca);
|
||||
|
||||
bap_broadcast_pa_sync(event);
|
||||
}
|
||||
|
||||
static void pa_sync_lost(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
ESP_LOGI(TAG, "PA sync lost: sync_handle 0x%04x reason 0x%02x",
|
||||
event->pa_sync_lost.sync_handle, event->pa_sync_lost.reason);
|
||||
|
||||
bap_broadcast_pa_lost(event);
|
||||
}
|
||||
|
||||
static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
switch (event->type) {
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_EXT_SCAN_RECV:
|
||||
ext_scan_recv(event);
|
||||
break;
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC:
|
||||
pa_sync(event);
|
||||
break;
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC_LOST:
|
||||
pa_sync_lost(event);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
esp_ble_audio_init_info_t init_info = {
|
||||
.gap_cb = iso_gap_app_cb,
|
||||
.gatt_cb = NULL,
|
||||
};
|
||||
esp_err_t err;
|
||||
|
||||
/* Initialize NVS — it is used to store PHY calibration data */
|
||||
err = nvs_flash_init();
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
err = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(err);
|
||||
|
||||
err = bluetooth_init();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize BLE, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_common_init(&init_info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize audio, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_tmap_register(ESP_BLE_AUDIO_TMAP_ROLE_BMR);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register TMAP, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = vcp_vol_renderer_init();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = bap_broadcast_sink_init();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_common_start(NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start audio, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
bap_broadcast_sink_scan();
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 NXP
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#include "host/ble_hs.h"
|
||||
|
||||
#include "esp_ble_audio_bap_api.h"
|
||||
#include "esp_ble_audio_cap_api.h"
|
||||
#include "esp_ble_audio_pacs_api.h"
|
||||
#include "esp_ble_audio_tmap_api.h"
|
||||
#include "esp_ble_audio_bap_lc3_preset_defs.h"
|
||||
|
||||
#include "ble_audio_example_init.h"
|
||||
#include "ble_audio_example_utils.h"
|
||||
|
||||
#define TAG "TMAP_BMR"
|
||||
|
||||
int vcp_vol_renderer_init(void);
|
||||
|
||||
void bap_broadcast_scan_recv(esp_ble_audio_gap_app_event_t *event);
|
||||
|
||||
void bap_broadcast_pa_sync(esp_ble_audio_gap_app_event_t *event);
|
||||
|
||||
void bap_broadcast_pa_lost(esp_ble_audio_gap_app_event_t *event);
|
||||
|
||||
int bap_broadcast_sink_scan(void);
|
||||
|
||||
int bap_broadcast_sink_init(void);
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Bose Corporation
|
||||
* SPDX-FileCopyrightText: 2020-2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileCopyrightText: 2022 Codecoup
|
||||
* SPDX-FileCopyrightText: 2023 NXP
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_system.h"
|
||||
|
||||
#include "tmap_bmr.h"
|
||||
|
||||
static esp_ble_audio_vcp_included_t vcp_included;
|
||||
|
||||
static void vcs_state_cb(esp_ble_conn_t *conn, int err, uint8_t volume, uint8_t mute)
|
||||
{
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get VCS state, err %d", err);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "VCS volume %u, mute %u", volume, mute);
|
||||
}
|
||||
}
|
||||
|
||||
static void vcs_flags_cb(esp_ble_conn_t *conn, int err, uint8_t flags)
|
||||
{
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get VCS flags, err %d", err);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "VCS flags 0x%02x", flags);
|
||||
}
|
||||
}
|
||||
|
||||
static esp_ble_audio_vcp_vol_rend_cb_t vcp_cbs = {
|
||||
.state = vcs_state_cb,
|
||||
.flags = vcs_flags_cb,
|
||||
};
|
||||
|
||||
int vcp_vol_renderer_init(void)
|
||||
{
|
||||
esp_ble_audio_vcp_vol_rend_register_param_t vcp_register_param = {0};
|
||||
int err;
|
||||
|
||||
vcp_register_param.step = 1;
|
||||
vcp_register_param.mute = ESP_BLE_AUDIO_VCP_STATE_UNMUTED;
|
||||
vcp_register_param.volume = 10;
|
||||
vcp_register_param.cb = &vcp_cbs;
|
||||
|
||||
err = esp_ble_audio_vcp_vol_rend_register(&vcp_register_param);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register VCP renderer, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_vcp_vol_rend_included_get(&vcp_included);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get VCP renderer included service, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "VCP volume renderer initialized");
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# This file was generated using idf.py save-defconfig. It can be edited manually.
|
||||
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
|
||||
#
|
||||
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_BLUEDROID_ENABLED=n
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_EXT_ADV=y
|
||||
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
|
||||
CONFIG_BT_NIMBLE_MAX_CCCDS=30
|
||||
CONFIG_BT_NIMBLE_ISO=y
|
||||
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
|
||||
|
||||
CONFIG_BT_ISO_MAX_CHAN=2
|
||||
|
||||
CONFIG_BT_TMAP=y
|
||||
CONFIG_BT_CAP_ACCEPTOR=y
|
||||
CONFIG_BT_BAP_SCAN_DELEGATOR=y
|
||||
CONFIG_BT_BAP_BROADCAST_SINK=y
|
||||
CONFIG_BT_VCP_VOL_REND=y
|
||||
CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=30
|
||||
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
@@ -0,0 +1,6 @@
|
||||
# Override some defaults so BT stack is enabled
|
||||
# by default in this example
|
||||
|
||||
CONFIG_IDF_TARGET="esp32h4"
|
||||
|
||||
CONFIG_BT_LE_ISO_SUPPORT=y
|
||||
@@ -0,0 +1,8 @@
|
||||
# The following lines of boilerplate have to be in your project's CMakeLists
|
||||
# in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
|
||||
idf_build_set_property(MINIMAL_BUILD ON)
|
||||
project(tmap_bms)
|
||||
@@ -0,0 +1,58 @@
|
||||
| Supported Targets | ESP32-H4 |
|
||||
| ----------------- | -------- |
|
||||
|
||||
# TMAP Broadcast Media Sender (BMS) Example
|
||||
|
||||
(See the README.md file in the upper level `examples` directory for more information about examples.)
|
||||
|
||||
This example implements the **Telephone and Media Audio Profile (TMAP) Broadcast Media Sender (BMS)** role. The BMS acts as a BAP Broadcast Source: it starts non-connectable extended advertising and periodic advertising. Extended advertising includes the **TMAS (Telephone and Media Audio Service)** UUID with the **BMS** role and the Broadcast Audio Announcement service (broadcast ID and device name); periodic advertising carries the BASE (Broadcast Audio Source Endpoint). After creating the broadcast source and starting the BIG, it sends broadcast audio on the BIS at the configured interval. TMAP BMR receivers (e.g. the [bmr](../bmr) example) that scan for both TMAS BMS and Broadcast Audio will discover and sync to this source.
|
||||
|
||||
The implementation uses the NimBLE host stack with ISO and BAP support, ESP-BLE-AUDIO (TMAP BMS role, CAP initiator broadcast). It creates a single subgroup with one stream using the LC3 48_2_1 broadcast preset (48 kHz, stereo, media context). A periodic TX scheduler (based on `k_work_delayable`) drives ISO SDU transmission in the ISO task context; the stream sent callback logs packet counts and drift. All parameters (broadcast ID, broadcast code, device name) are hardcoded in the source and can be changed by editing the source code constants.
|
||||
|
||||
## Requirements
|
||||
|
||||
* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4)
|
||||
* Optionally, a TMAP BMR or BAP Broadcast Sink (e.g. the [bmr](../bmr) example) to receive the broadcast stream
|
||||
|
||||
## How to Use Example
|
||||
|
||||
Before project configuration and build, set the correct chip target:
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32h4
|
||||
```
|
||||
|
||||
### Build and Flash
|
||||
|
||||
Run the following to build, flash and monitor:
|
||||
|
||||
```bash
|
||||
idf.py -p PORT flash monitor
|
||||
```
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF.
|
||||
|
||||
## Example Flow
|
||||
|
||||
1. **Initialization**: NVS, Bluetooth stack, and LE Audio common layer (`esp_ble_audio_common_init`). Register TMAP role BMS (`esp_ble_audio_tmap_register(ESP_BLE_AUDIO_TMAP_ROLE_BMS)`). Initialize CAP initiator broadcast: register stream ops (started, stopped, sent), create broadcast audio TX scheduler.
|
||||
2. **Setup**: Create the broadcast source with one subgroup and one stream (LC3 48_2_1 preset), build extended adv data (TMAS + BMS role, Broadcast Audio UUID + broadcast ID, device name) and periodic adv data (BASE). Start periodic and extended advertising, add the ext adv to the BIG, then start the broadcast audio. The stream started callback starts the periodic TX scheduler and sends the first SDU.
|
||||
3. **Streaming**: The TX scheduler, based on `k_work_delayable`, fires at the stream QoS interval in the ISO task context and sends ISO SDUs; the sent callback counts packets and logs periodically. Drift and untransmitted packets are reported in the callback. Note that the scheduler timer resolution is in milliseconds, which may not match the exact SDU interval for all configurations.
|
||||
4. **Optional**: `cap_initiator_update` updates broadcast metadata; `cap_initiator_stop` stops and deletes the broadcast source.
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
I (xxx) TMAP_BMS: Creating broadcast source
|
||||
I (xxx) TMAP_BMS: Extended adv instance 0 started
|
||||
I (xxx) TMAP_BMS: Stream 0x... started
|
||||
I (xxx) TMAP_BMS: Transmitted 1000 ISO data packets (stream 0x...)
|
||||
...
|
||||
```
|
||||
|
||||
If the controller reports drift or untransmitted packets:
|
||||
|
||||
```
|
||||
W (xxx) TMAP_BMS: ISO data packets ... drifted and ... not txd
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
set(srcs "cap_initiator.c"
|
||||
"main.c")
|
||||
|
||||
idf_component_register(SRCS "${srcs}"
|
||||
REQUIRES bt nvs_flash)
|
||||
@@ -0,0 +1,527 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2022-2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileCopyrightText: 2023 NXP
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "esp_timer.h"
|
||||
#include "esp_random.h"
|
||||
|
||||
#include "tmap_bms.h"
|
||||
|
||||
#define ADV_HANDLE 0x00
|
||||
#define ADV_SID 0
|
||||
#define ADV_TX_POWER 127
|
||||
#define ADV_ADDRESS BLE_OWN_ADDR_PUBLIC
|
||||
#define ADV_PRIMARY_PHY BLE_HCI_LE_PHY_1M
|
||||
#define ADV_SECONDARY_PHY BLE_HCI_LE_PHY_2M
|
||||
#define ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(200)
|
||||
|
||||
#define PER_ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(100)
|
||||
|
||||
#define LOCAL_DEVICE_NAME "TMAP Broadcast Source"
|
||||
#define LOCAL_BROADCAST_ID 0x123456
|
||||
|
||||
static esp_ble_audio_cap_initiator_broadcast_subgroup_param_t subgroup_param;
|
||||
static esp_ble_audio_cap_initiator_broadcast_stream_param_t stream_params;
|
||||
static esp_ble_audio_cap_initiator_broadcast_create_param_t create_param;
|
||||
static esp_ble_audio_cap_broadcast_source_t *broadcast_source;
|
||||
static esp_ble_audio_cap_stream_t broadcast_stream;
|
||||
|
||||
static uint8_t bis_codec_data[] = {
|
||||
ESP_BLE_AUDIO_CODEC_DATA(ESP_BLE_AUDIO_CODEC_CFG_FREQ,
|
||||
EXAMPLE_BYTES_LIST_LE16(ESP_BLE_AUDIO_CODEC_CFG_FREQ_48KHZ))
|
||||
};
|
||||
|
||||
static uint8_t new_metadata[] = {
|
||||
ESP_BLE_AUDIO_CODEC_DATA(ESP_BLE_AUDIO_METADATA_TYPE_STREAM_CONTEXT,
|
||||
EXAMPLE_BYTES_LIST_LE16(ESP_BLE_AUDIO_CONTEXT_TYPE_MEDIA))
|
||||
};
|
||||
|
||||
ESP_BLE_AUDIO_BAP_LC3_BROADCAST_PRESET_48_2_1_DEFINE(broadcast_preset_48_2_1,
|
||||
ESP_BLE_AUDIO_LOCATION_FRONT_LEFT,
|
||||
ESP_BLE_AUDIO_CONTEXT_TYPE_MEDIA);
|
||||
|
||||
static example_audio_tx_scheduler_t tx_scheduler;
|
||||
static uint16_t tx_seq_num;
|
||||
static uint8_t *iso_data;
|
||||
|
||||
static void broadcast_audio_tx(void)
|
||||
{
|
||||
uint16_t sdu_len;
|
||||
int err;
|
||||
|
||||
if (iso_data == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (broadcast_stream.bap_stream.qos == NULL || broadcast_stream.bap_stream.qos->sdu == 0) {
|
||||
ESP_LOGE(TAG, "Invalid stream qos");
|
||||
return;
|
||||
}
|
||||
|
||||
sdu_len = broadcast_stream.bap_stream.qos->sdu;
|
||||
|
||||
memset(iso_data, (uint8_t)tx_seq_num, sdu_len);
|
||||
|
||||
err = esp_ble_audio_cap_stream_send(&broadcast_stream,
|
||||
iso_data, sdu_len,
|
||||
tx_seq_num);
|
||||
if (err) {
|
||||
ESP_LOGD(TAG, "Failed to transmit data on streams, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
tx_seq_num++;
|
||||
}
|
||||
|
||||
static void broadcast_started_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "Stream %p started", stream);
|
||||
|
||||
if (stream->qos == NULL || stream->qos->sdu == 0) {
|
||||
ESP_LOGE(TAG, "Invalid stream qos");
|
||||
return;
|
||||
}
|
||||
|
||||
if (iso_data == NULL) {
|
||||
iso_data = calloc(1, stream->qos->sdu);
|
||||
if (iso_data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to alloc TX buffer, SDU %u", stream->qos->sdu);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
tx_seq_num = 0;
|
||||
example_audio_tx_scheduler_reset(&tx_scheduler);
|
||||
|
||||
/* Note: esp timer is not accurate enough */
|
||||
err = example_audio_tx_scheduler_start(&tx_scheduler, stream->qos->interval);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start tx scheduler, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
broadcast_audio_tx();
|
||||
}
|
||||
|
||||
static void broadcast_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "Stream %p stopped, reason 0x%02x", stream, reason);
|
||||
|
||||
err = example_audio_tx_scheduler_stop(&tx_scheduler);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err);
|
||||
}
|
||||
|
||||
if (iso_data != NULL) {
|
||||
free(iso_data);
|
||||
iso_data = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void broadcast_disconnected_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "Stream %p disconnected, reason 0x%02x", stream, reason);
|
||||
|
||||
err = example_audio_tx_scheduler_stop(&tx_scheduler);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err);
|
||||
}
|
||||
|
||||
if (iso_data != NULL) {
|
||||
free(iso_data);
|
||||
iso_data = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void broadcast_sent_cb(esp_ble_audio_bap_stream_t *stream, void *user_data)
|
||||
{
|
||||
example_audio_tx_scheduler_on_sent(&tx_scheduler, user_data, TAG, "stream", stream);
|
||||
}
|
||||
|
||||
static esp_ble_audio_bap_stream_ops_t broadcast_stream_ops = {
|
||||
.started = broadcast_started_cb,
|
||||
.stopped = broadcast_stopped_cb,
|
||||
.sent = broadcast_sent_cb,
|
||||
.disconnected = broadcast_disconnected_cb,
|
||||
};
|
||||
|
||||
static uint8_t *ext_adv_data_get(uint8_t *data_len)
|
||||
{
|
||||
uint32_t broadcast_id;
|
||||
uint8_t *data;
|
||||
|
||||
broadcast_id = LOCAL_BROADCAST_ID;
|
||||
|
||||
/* - TMAP UUID (2 octets)
|
||||
* - TMAP Role (2 octets)
|
||||
* - Broadcast Audio Announcement Service UUID (2 octets)
|
||||
* - Broadcast ID (3 octets)
|
||||
* - Complete Device Name
|
||||
*/
|
||||
*data_len = 6 + 7 + 2 + strlen(LOCAL_DEVICE_NAME);
|
||||
|
||||
data = calloc(1, *data_len);
|
||||
if (data == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
data[0] = 0x05; /* 1 + 2 + 2 */
|
||||
data[1] = EXAMPLE_AD_TYPE_SERVICE_DATA16;
|
||||
data[2] = (ESP_BLE_AUDIO_UUID_TMAS_VAL & 0xFF);
|
||||
data[3] = ((ESP_BLE_AUDIO_UUID_TMAS_VAL >> 8) & 0xFF);
|
||||
data[4] = (ESP_BLE_AUDIO_TMAP_ROLE_BMS & 0xFF);
|
||||
data[5] = ((ESP_BLE_AUDIO_TMAP_ROLE_BMS >> 8) & 0xFF);
|
||||
|
||||
data[6] = 0x06; /* 1 + 2 + 3 */
|
||||
data[7] = EXAMPLE_AD_TYPE_SERVICE_DATA16;
|
||||
data[8] = (ESP_BLE_AUDIO_UUID_BROADCAST_AUDIO_VAL & 0xFF);
|
||||
data[9] = ((ESP_BLE_AUDIO_UUID_BROADCAST_AUDIO_VAL >> 8) & 0xFF);
|
||||
data[10] = (broadcast_id & 0xFF);
|
||||
data[11] = ((broadcast_id >> 8) & 0xFF);
|
||||
data[12] = ((broadcast_id >> 16) & 0xFF);
|
||||
|
||||
data[13] = strlen(LOCAL_DEVICE_NAME) + 1;
|
||||
data[14] = EXAMPLE_AD_TYPE_NAME_COMPLETE;
|
||||
memcpy(data + 15, LOCAL_DEVICE_NAME, strlen(LOCAL_DEVICE_NAME));
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static uint8_t *per_adv_data_get(uint8_t *data_len)
|
||||
{
|
||||
NET_BUF_SIMPLE_DEFINE(base_buf, 128);
|
||||
uint8_t *data;
|
||||
esp_err_t err;
|
||||
|
||||
/* Broadcast Audio Announcement Service UUID (2 octets) and
|
||||
* Broadcast Audio Source Endpoint (BASE)
|
||||
*/
|
||||
|
||||
err = esp_ble_audio_cap_initiator_broadcast_get_base(broadcast_source, &base_buf);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get encoded BASE, err %d", err);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
*data_len = 2 + base_buf.len;
|
||||
|
||||
data = calloc(1, *data_len);
|
||||
if (data == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* base_buf.len has included the UUID length (2 octets) */
|
||||
data[0] = 1 + base_buf.len;
|
||||
data[1] = EXAMPLE_AD_TYPE_SERVICE_DATA16;
|
||||
memcpy(data + 2, base_buf.data, base_buf.len);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static int ext_adv_start(void)
|
||||
{
|
||||
struct ble_gap_periodic_adv_params per_params = {0};
|
||||
struct ble_gap_ext_adv_params ext_params = {0};
|
||||
struct os_mbuf *data = NULL;
|
||||
uint8_t *ext_data = NULL;
|
||||
uint8_t *per_data = NULL;
|
||||
uint8_t data_len = 0;
|
||||
int err;
|
||||
|
||||
ext_params.connectable = 0;
|
||||
ext_params.scannable = 0;
|
||||
ext_params.legacy_pdu = 0;
|
||||
ext_params.own_addr_type = ADV_ADDRESS;
|
||||
ext_params.primary_phy = ADV_PRIMARY_PHY;
|
||||
ext_params.secondary_phy = ADV_SECONDARY_PHY;
|
||||
ext_params.tx_power = ADV_TX_POWER;
|
||||
ext_params.sid = ADV_SID;
|
||||
ext_params.itvl_min = ADV_INTERVAL;
|
||||
ext_params.itvl_max = ADV_INTERVAL;
|
||||
|
||||
err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL,
|
||||
example_audio_gap_event_cb, NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
ext_data = ext_adv_data_get(&data_len);
|
||||
if (ext_data == NULL) {
|
||||
err = -ENOMEM;
|
||||
goto end;
|
||||
}
|
||||
|
||||
data = os_msys_get_pkthdr(data_len, 0);
|
||||
if (data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to get ext adv mbuf");
|
||||
err = -ENOMEM;
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = os_mbuf_append(data, ext_data, data_len);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err);
|
||||
os_mbuf_free_chain(data);
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = ble_gap_ext_adv_set_data(ADV_HANDLE, data);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
per_params.include_tx_power = 0;
|
||||
per_params.itvl_min = PER_ADV_INTERVAL;
|
||||
per_params.itvl_max = PER_ADV_INTERVAL;
|
||||
|
||||
err = ble_gap_periodic_adv_configure(ADV_HANDLE, &per_params);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to configure per adv params, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
per_data = per_adv_data_get(&data_len);
|
||||
if (per_data == NULL) {
|
||||
err = -ENOMEM;
|
||||
goto end;
|
||||
}
|
||||
|
||||
data = os_msys_get_pkthdr(data_len, 0);
|
||||
if (data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to get per adv mbuf");
|
||||
err = -ENOMEM;
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = os_mbuf_append(data, per_data, data_len);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to append per adv data, err %d", err);
|
||||
os_mbuf_free_chain(data);
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = ble_gap_periodic_adv_set_data(ADV_HANDLE, data);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set per adv data, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = ble_gap_periodic_adv_start(ADV_HANDLE);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start per advertising, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Extended adv instance %u started", ADV_HANDLE);
|
||||
|
||||
end:
|
||||
if (ext_data) {
|
||||
free(ext_data);
|
||||
}
|
||||
if (per_data) {
|
||||
free(per_data);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
static int ext_adv_stop(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = ble_gap_periodic_adv_stop(ADV_HANDLE);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop per advertising, err %d", err);
|
||||
}
|
||||
|
||||
err = ble_gap_ext_adv_stop(ADV_HANDLE);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop ext advertising, err %d", err);
|
||||
}
|
||||
|
||||
if (err == 0) {
|
||||
ESP_LOGI(TAG, "Extended adv instance %u stopped", ADV_HANDLE);
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
int cap_initiator_setup(void)
|
||||
{
|
||||
esp_ble_audio_bap_broadcast_adv_info_t info = {
|
||||
.adv_handle = ADV_HANDLE,
|
||||
};
|
||||
bool adv_started = false;
|
||||
bool adv_added = false;
|
||||
int err;
|
||||
|
||||
stream_params.stream = &broadcast_stream;
|
||||
stream_params.data_len = ARRAY_SIZE(bis_codec_data);
|
||||
stream_params.data = bis_codec_data;
|
||||
|
||||
subgroup_param.stream_count = 1;
|
||||
subgroup_param.stream_params = &stream_params;
|
||||
subgroup_param.codec_cfg = &broadcast_preset_48_2_1.codec_cfg;
|
||||
|
||||
create_param.subgroup_count = 1;
|
||||
create_param.subgroup_params = &subgroup_param;
|
||||
create_param.qos = &broadcast_preset_48_2_1.qos;
|
||||
create_param.packing = ESP_BLE_ISO_PACKING_SEQUENTIAL;
|
||||
create_param.encryption = false;
|
||||
|
||||
ESP_LOGI(TAG, "Creating broadcast source");
|
||||
|
||||
err = esp_ble_audio_cap_initiator_broadcast_audio_create(&create_param, &broadcast_source);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to create broadcast source, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = ext_adv_start();
|
||||
if (err) {
|
||||
goto end;
|
||||
}
|
||||
|
||||
adv_started = true;
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_adv_add(&info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to add adv for broadcast source, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
adv_added = true;
|
||||
|
||||
err = esp_ble_audio_cap_initiator_broadcast_audio_start(broadcast_source, ADV_HANDLE);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start broadcast source, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "CAP initiator setup");
|
||||
|
||||
return 0;
|
||||
|
||||
end:
|
||||
if (adv_added) {
|
||||
err = esp_ble_audio_bap_broadcast_adv_delete(&info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to delete adv for broadcast source, err %d", err);
|
||||
}
|
||||
}
|
||||
|
||||
if (adv_started) {
|
||||
ext_adv_stop();
|
||||
}
|
||||
|
||||
esp_ble_audio_cap_initiator_broadcast_audio_delete(broadcast_source);
|
||||
broadcast_source = NULL;
|
||||
|
||||
if (iso_data != NULL) {
|
||||
free(iso_data);
|
||||
iso_data = NULL;
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
int cap_initiator_update(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = esp_ble_audio_cap_initiator_broadcast_audio_update(broadcast_source,
|
||||
new_metadata,
|
||||
ARRAY_SIZE(new_metadata));
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to update broadcast source metadata, err %d", err);
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
int cap_initiator_stop(void)
|
||||
{
|
||||
esp_ble_audio_bap_broadcast_adv_info_t info = {
|
||||
.adv_handle = ADV_HANDLE,
|
||||
};
|
||||
int err;
|
||||
|
||||
err = esp_ble_audio_cap_initiator_broadcast_audio_stop(broadcast_source);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop broadcast source, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_cap_initiator_broadcast_audio_delete(broadcast_source);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to delete broadcast source, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
broadcast_source = NULL;
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_adv_delete(&info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to delete adv for broadcast source, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = ext_adv_stop();
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
|
||||
if (iso_data != NULL) {
|
||||
free(iso_data);
|
||||
iso_data = NULL;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void tx_scheduler_cb(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
broadcast_audio_tx();
|
||||
}
|
||||
|
||||
int cap_initiator_init(void)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
esp_ble_audio_cap_stream_ops_register(&broadcast_stream, &broadcast_stream_ops);
|
||||
|
||||
err = example_audio_tx_scheduler_init(&tx_scheduler,
|
||||
tx_scheduler_cb,
|
||||
NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to init tx scheduler, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "CAP initiator initialized");
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
dependencies:
|
||||
example_init:
|
||||
path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_init
|
||||
example_utils:
|
||||
path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_utils
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 NXP
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_system.h"
|
||||
|
||||
#include "tmap_bms.h"
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
esp_ble_audio_init_info_t init_info = {
|
||||
.gap_cb = NULL,
|
||||
.gatt_cb = NULL,
|
||||
};
|
||||
esp_err_t err;
|
||||
|
||||
/* Initialize NVS — it is used to store PHY calibration data */
|
||||
err = nvs_flash_init();
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
err = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(err);
|
||||
|
||||
err = bluetooth_init();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize BLE, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_common_init(&init_info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize audio, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_tmap_register(ESP_BLE_AUDIO_TMAP_ROLE_BMS);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register TMAP, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = cap_initiator_init();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_common_start(NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start audio, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
cap_initiator_setup();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 NXP
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#include "host/ble_hs.h"
|
||||
|
||||
#include "esp_ble_audio_cap_api.h"
|
||||
#include "esp_ble_audio_tmap_api.h"
|
||||
#include "esp_ble_audio_bap_lc3_preset_defs.h"
|
||||
|
||||
#include "ble_audio_example_init.h"
|
||||
#include "ble_audio_example_utils.h"
|
||||
|
||||
#define TAG "TMAP_BMS"
|
||||
|
||||
int cap_initiator_setup(void);
|
||||
|
||||
int cap_initiator_update(void);
|
||||
|
||||
int cap_initiator_stop(void);
|
||||
|
||||
int cap_initiator_init(void);
|
||||
@@ -0,0 +1,20 @@
|
||||
# This file was generated using idf.py save-defconfig. It can be edited manually.
|
||||
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
|
||||
#
|
||||
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_BLUEDROID_ENABLED=n
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_EXT_ADV=y
|
||||
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
|
||||
CONFIG_BT_NIMBLE_MAX_CCCDS=30
|
||||
CONFIG_BT_NIMBLE_ISO=y
|
||||
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
|
||||
|
||||
CONFIG_BT_ISO_MAX_CHAN=2
|
||||
|
||||
CONFIG_BT_TMAP=y
|
||||
CONFIG_BT_CAP_INITIATOR=y
|
||||
CONFIG_BT_BAP_BROADCAST_SOURCE=y
|
||||
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
@@ -0,0 +1,6 @@
|
||||
# Override some defaults so BT stack is enabled
|
||||
# by default in this example
|
||||
|
||||
CONFIG_IDF_TARGET="esp32h4"
|
||||
|
||||
CONFIG_BT_LE_ISO_SUPPORT=y
|
||||
@@ -0,0 +1,8 @@
|
||||
# The following lines of boilerplate have to be in your project's CMakeLists
|
||||
# in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
|
||||
idf_build_set_property(MINIMAL_BUILD ON)
|
||||
project(tmap_central)
|
||||
@@ -0,0 +1,64 @@
|
||||
| Supported Targets | ESP32-H4 |
|
||||
| ----------------- | -------- |
|
||||
|
||||
# TMAP Central Example
|
||||
|
||||
(See the README.md file in the upper level `examples` directory for more information about examples.)
|
||||
|
||||
This example implements the **Telephone and Media Audio Profile (TMAP) Central** roles: **Call Gateway (CG)** and **Unicast Media Sender (UMS)**. The central scans for devices that advertise the TMAS (Telephone and Media Audio Service) with the **UMR (Unicast Media Receiver)** role, connects to the first such device, performs pairing, MTU exchange, and GATT service discovery, then runs TMAP discovery and VCP discovery. After TMAP discovery completes, it sets up unicast audio streams (CAP initiator): discovers ASEs, configures codec (e.g. LC3 48_2_1), sets QoS, enables and connects streams, then starts them and sends audio to the peer’s sink. It also acts as **VCP Volume Controller** (volume/mute on the peer), **MCP server** (Media Control Profile / media proxy), and **CCP server** (Call Control Profile with TBS – Telephone Bearer Service) for call originate/terminate. Run it with a device that advertises TMAP UMR (e.g. a TMAP peripheral or CAP acceptor with unicast).
|
||||
|
||||
The implementation uses the NimBLE host stack with ISO and LE Audio support, ESP-BLE-AUDIO (TMAP CG+UMS, CAP initiator unicast client, VCP volume controller, TBS, CSIP set coordinator, MCP/MCS/MCC, media proxy). Optional Kconfig: device name.
|
||||
|
||||
## Requirements
|
||||
|
||||
* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4)
|
||||
* A device advertising TMAP UMR (Unicast Media Receiver), e.g. a CAP Acceptor or TMAP peripheral with unicast
|
||||
|
||||
## How to Use Example
|
||||
|
||||
Before project configuration and build, set the correct chip target:
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32h4
|
||||
```
|
||||
|
||||
### Build and Flash
|
||||
|
||||
Run the following to build, flash and monitor:
|
||||
|
||||
```bash
|
||||
idf.py -p PORT flash monitor
|
||||
```
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF.
|
||||
|
||||
## Example Flow
|
||||
|
||||
1. **Initialization**: NVS, Bluetooth stack, and LE Audio common layer (`esp_ble_audio_common_init`) with GAP and GATT callbacks. Register TMAP roles CG and UMS (`esp_ble_audio_tmap_register(ESP_BLE_AUDIO_TMAP_ROLE_CG | ESP_BLE_AUDIO_TMAP_ROLE_UMS)`). Initialize CAP initiator (unicast client, stream callbacks, TX timer). Initialize VCP volume controller. Initialize MCP server (media proxy). Initialize CCP server (TBS bearer, originate/terminate callbacks). Start the audio stack and set the device name.
|
||||
2. **Scanning**: Start extended scanning. On scan result with connectable advertising, parse for TMAS with UMR role; when found, stop scan and connect to the peer.
|
||||
3. **Connection**: On ACL connect, store connection handle and initiate pairing. On MTU change, start GATT service discovery. On discovery complete (and when MTU has been exchanged), start TMAP discovery and VCP discovery.
|
||||
4. **TMAP discovery complete**: Call `cap_initiator_setup()` to create the unicast group, discover ASEs, configure codec, set QoS, enable and connect streams, then start streams.
|
||||
5. **Unicast streaming**: When a sink-direction stream (TX from central to peer) is started, a periodic TX scheduler based on `k_work_delayable` runs in the ISO task context and sends ISO SDUs at the stream QoS interval; the sent callback logs packet counts. Source streams (RX) can be used to receive audio from the peer. Note that the scheduler timer resolution is in milliseconds, which may not match the exact SDU interval for all configurations.
|
||||
6. **VCP**: After VCP discovery, the example can send mute/volume commands; state and flags callbacks log volume and mute.
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
I (xxx) TMAP_CEN: Found TMAS in peer adv data!
|
||||
I (xxx) TMAP_CEN: connection established, status 0
|
||||
I (xxx) TMAP_CEN: gatt mtu change, conn_handle 1, mtu ...
|
||||
I (xxx) TMAP_CEN: gattc disc cmpl, status 0, conn_handle 1
|
||||
I (xxx) TMAP_CEN: TMAP discovery done
|
||||
I (xxx) TMAP_CEN: Configured stream 0x...
|
||||
I (xxx) TMAP_CEN: Unicast stream 0x... started
|
||||
I (xxx) TMAP_CEN: Transmitted 1000 ISO data packets (stream 0x...)
|
||||
...
|
||||
```
|
||||
|
||||
If the connection is lost:
|
||||
|
||||
```
|
||||
I (xxx) TMAP_CEN: connection disconnected, reason 0x...
|
||||
```
|
||||
@@ -0,0 +1,8 @@
|
||||
set(srcs "cap_initiator.c"
|
||||
"ccp_server.c"
|
||||
"mcp_server.c"
|
||||
"vcp_vol_ctlr.c"
|
||||
"main.c")
|
||||
|
||||
idf_component_register(SRCS "${srcs}"
|
||||
REQUIRES bt nvs_flash)
|
||||
@@ -0,0 +1,602 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2022-2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileCopyrightText: 2023 NXP
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "esp_timer.h"
|
||||
|
||||
#include "esp_ble_audio_bap_lc3_preset_defs.h"
|
||||
|
||||
#include "tmap_central.h"
|
||||
|
||||
static esp_ble_audio_cap_stream_t unicast_sink_streams[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT];
|
||||
static esp_ble_audio_cap_stream_t unicast_source_streams[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT];
|
||||
static esp_ble_audio_bap_ep_t *unicast_sink_eps[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT];
|
||||
static esp_ble_audio_bap_ep_t *unicast_source_eps[CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SRC_COUNT];
|
||||
|
||||
static esp_ble_audio_cap_unicast_group_t *unicast_group;
|
||||
|
||||
static example_audio_tx_scheduler_t tx_scheduler;
|
||||
static uint16_t tx_seq_num;
|
||||
static uint8_t *iso_data;
|
||||
|
||||
static void unicast_audio_tx(void);
|
||||
|
||||
ESP_BLE_AUDIO_BAP_LC3_UNICAST_PRESET_48_2_1_DEFINE(unicast_preset_48_2_1,
|
||||
ESP_BLE_AUDIO_LOCATION_FRONT_LEFT,
|
||||
ESP_BLE_AUDIO_CONTEXT_TYPE_MEDIA);
|
||||
|
||||
static void unicast_stream_configured_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
const esp_ble_audio_bap_qos_cfg_pref_t *pref)
|
||||
{
|
||||
ESP_LOGI(TAG, "Unicast stream %p configured", stream);
|
||||
}
|
||||
|
||||
static void unicast_stream_qos_set_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
ESP_LOGI(TAG, "Unicast stream %p QoS set", stream);
|
||||
}
|
||||
|
||||
static void unicast_stream_enabled_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
ESP_LOGI(TAG, "Unicast stream %p enabled", stream);
|
||||
}
|
||||
|
||||
static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
esp_ble_audio_bap_ep_info_t ep_info = {0};
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "Unicast stream %p started", stream);
|
||||
|
||||
err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get ep info, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Note:
|
||||
* For a BAP Unicast Client, this is considered outgoing audio (TX).
|
||||
*/
|
||||
if (ep_info.dir == ESP_BLE_AUDIO_DIR_SINK) {
|
||||
if (stream->qos == NULL || stream->qos->sdu == 0) {
|
||||
ESP_LOGE(TAG, "Invalid stream qos");
|
||||
return;
|
||||
}
|
||||
|
||||
if (iso_data == NULL) {
|
||||
iso_data = calloc(1, stream->qos->sdu);
|
||||
if (iso_data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to alloc TX buffer, SDU %u", stream->qos->sdu);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
assert(stream->qos->interval);
|
||||
|
||||
ESP_LOGI(TAG, "Streaming, interval %u, length %u", stream->qos->interval, stream->qos->sdu);
|
||||
|
||||
tx_seq_num = 0;
|
||||
example_audio_tx_scheduler_reset(&tx_scheduler);
|
||||
|
||||
/* Note: esp timer is not accurate enough */
|
||||
err = example_audio_tx_scheduler_start(&tx_scheduler, stream->qos->interval);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start tx scheduler, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
unicast_audio_tx();
|
||||
}
|
||||
}
|
||||
|
||||
static void unicast_stream_metadata_updated_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
ESP_LOGI(TAG, "Unicast stream %p metadata updated", stream);
|
||||
}
|
||||
|
||||
static void unicast_stream_disabled_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
ESP_LOGI(TAG, "Unicast stream %p disabled", stream);
|
||||
}
|
||||
|
||||
static void unicast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
|
||||
{
|
||||
esp_ble_audio_bap_ep_info_t ep_info = {0};
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "Unicast stream %p stopped, reason 0x%02x", stream, reason);
|
||||
|
||||
err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get ep info, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ep_info.dir == ESP_BLE_AUDIO_DIR_SINK) {
|
||||
err = example_audio_tx_scheduler_stop(&tx_scheduler);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err);
|
||||
}
|
||||
|
||||
if (iso_data != NULL) {
|
||||
free(iso_data);
|
||||
iso_data = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void unicast_stream_released_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
ESP_LOGI(TAG, "Unicast stream %p released", stream);
|
||||
}
|
||||
|
||||
static void unicast_stream_sent_cb(esp_ble_audio_bap_stream_t *stream, void *user_data)
|
||||
{
|
||||
example_audio_tx_scheduler_on_sent(&tx_scheduler, user_data, TAG, "stream", stream);
|
||||
}
|
||||
|
||||
static void unicast_stream_connected_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
ESP_LOGI(TAG, "Unicast stream %p connected", stream);
|
||||
}
|
||||
|
||||
static void unicast_stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
|
||||
{
|
||||
esp_ble_audio_bap_ep_info_t ep_info = {0};
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "Unicast stream %p disconnected, reason 0x%02x", stream, reason);
|
||||
|
||||
err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get ep info, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ep_info.dir == ESP_BLE_AUDIO_DIR_SINK) {
|
||||
err = example_audio_tx_scheduler_stop(&tx_scheduler);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err);
|
||||
}
|
||||
|
||||
if (iso_data != NULL) {
|
||||
free(iso_data);
|
||||
iso_data = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static esp_ble_audio_bap_stream_ops_t 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,
|
||||
.sent = unicast_stream_sent_cb,
|
||||
.connected = unicast_stream_connected_cb,
|
||||
.disconnected = unicast_stream_disconnected_cb,
|
||||
};
|
||||
|
||||
static void unicast_discovery_complete_cb(esp_ble_conn_t *conn, int err,
|
||||
const esp_ble_audio_csip_set_coordinator_set_member_t *member,
|
||||
const esp_ble_audio_csip_set_coordinator_csis_inst_t *csis_inst)
|
||||
{
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Unicast discovery completed, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (default_conn_handle_get() != conn->handle) {
|
||||
ESP_LOGE(TAG, "%s, not connected", __func__);
|
||||
return;
|
||||
}
|
||||
|
||||
if (IS_ENABLED(CONFIG_BT_CAP_ACCEPTOR_SET_MEMBER)) {
|
||||
if (csis_inst == NULL) {
|
||||
ESP_LOGW(TAG, "Failed to discover CAS CSIS");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Found CAS with CSIS %p", csis_inst);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Found CAS");
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_unicast_client_discover(conn->handle, ESP_BLE_AUDIO_DIR_SINK);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to discover sink, err %d", err);
|
||||
}
|
||||
}
|
||||
|
||||
static void unicast_start_complete_cb(int err, esp_ble_conn_t *conn)
|
||||
{
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Unicast start completed, err %d", err);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Unicast start completed");
|
||||
}
|
||||
}
|
||||
|
||||
static void unicast_update_complete_cb(int err, esp_ble_conn_t *conn)
|
||||
{
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Unicast update completed, err %d", err);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Unicast update completed");
|
||||
}
|
||||
}
|
||||
|
||||
static void unicast_stop_complete_cb(int err, esp_ble_conn_t *conn)
|
||||
{
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Unicast stop completed, err %d", err);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Unicast stop completed");
|
||||
}
|
||||
}
|
||||
|
||||
static esp_ble_audio_cap_initiator_cb_t cap_cb = {
|
||||
.unicast_discovery_complete = unicast_discovery_complete_cb,
|
||||
.unicast_start_complete = unicast_start_complete_cb,
|
||||
.unicast_update_complete = unicast_update_complete_cb,
|
||||
.unicast_stop_complete = unicast_stop_complete_cb,
|
||||
};
|
||||
|
||||
static int unicast_group_create(void)
|
||||
{
|
||||
esp_ble_audio_cap_unicast_group_stream_param_t group_stream_params = {0};
|
||||
esp_ble_audio_cap_unicast_group_stream_pair_param_t pair_params = {0};
|
||||
esp_ble_audio_cap_unicast_group_param_t group_param = {0};
|
||||
int err;
|
||||
|
||||
if (unicast_group != NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
group_stream_params.qos_cfg = &unicast_preset_48_2_1.qos;
|
||||
group_stream_params.stream = &unicast_sink_streams[0];
|
||||
|
||||
pair_params.tx_param = &group_stream_params;
|
||||
pair_params.rx_param = NULL;
|
||||
|
||||
group_param.packing = ESP_BLE_ISO_PACKING_SEQUENTIAL;
|
||||
group_param.params_count = 1;
|
||||
group_param.params = &pair_params;
|
||||
|
||||
err = esp_ble_audio_cap_unicast_group_create(&group_param, &unicast_group);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to create unicast group, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Created unicast group");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int unicast_group_delete(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
if (unicast_group == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_cap_unicast_group_delete(unicast_group);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to delete unicast group, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
unicast_group = NULL;
|
||||
|
||||
ESP_LOGI(TAG, "Deleted unicast group");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int unicast_audio_start(esp_ble_conn_t *conn)
|
||||
{
|
||||
esp_ble_audio_cap_unicast_audio_start_stream_param_t stream_param = {0};
|
||||
esp_ble_audio_cap_unicast_audio_start_param_t param = {0};
|
||||
int err;
|
||||
|
||||
if (unicast_sink_eps[0] == NULL) {
|
||||
ESP_LOGE(TAG, "No sink endpoint available");
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
stream_param.member.member = conn;
|
||||
stream_param.stream = &unicast_sink_streams[0];
|
||||
stream_param.ep = unicast_sink_eps[0];
|
||||
stream_param.codec_cfg = &unicast_preset_48_2_1.codec_cfg;
|
||||
|
||||
param.type = ESP_BLE_AUDIO_CAP_SET_TYPE_AD_HOC;
|
||||
param.count = 1;
|
||||
param.stream_params = &stream_param;
|
||||
|
||||
err = esp_ble_audio_cap_initiator_unicast_audio_start(¶m);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start unicast audio, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Started unicast audio");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void location_cb(esp_ble_conn_t *conn,
|
||||
esp_ble_audio_dir_t dir,
|
||||
esp_ble_audio_location_t loc)
|
||||
{
|
||||
ESP_LOGI(TAG, "Location, dir %u loc 0x%08lx", dir, loc);
|
||||
}
|
||||
|
||||
static void available_contexts_cb(esp_ble_conn_t *conn,
|
||||
esp_ble_audio_context_t snk_ctx,
|
||||
esp_ble_audio_context_t src_ctx)
|
||||
{
|
||||
ESP_LOGI(TAG, "Available contexts, sink 0x%04x source 0x%04x", snk_ctx, src_ctx);
|
||||
}
|
||||
|
||||
static void config_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_bap_ascs_rsp_code_t rsp_code,
|
||||
esp_ble_audio_bap_ascs_reason_t reason)
|
||||
{
|
||||
ESP_LOGI(TAG, "Config, stream %p rsp_code %u reason %u", stream, rsp_code, reason);
|
||||
}
|
||||
|
||||
static void qos_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_bap_ascs_rsp_code_t rsp_code,
|
||||
esp_ble_audio_bap_ascs_reason_t reason)
|
||||
{
|
||||
ESP_LOGI(TAG, "Qos, stream %p rsp_code %u reason %u", stream, rsp_code, reason);
|
||||
}
|
||||
|
||||
static void enable_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_bap_ascs_rsp_code_t rsp_code,
|
||||
esp_ble_audio_bap_ascs_reason_t reason)
|
||||
{
|
||||
ESP_LOGI(TAG, "Enable, stream %p rsp_code %u reason %u", stream, rsp_code, reason);
|
||||
}
|
||||
|
||||
static void start_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_bap_ascs_rsp_code_t rsp_code,
|
||||
esp_ble_audio_bap_ascs_reason_t reason)
|
||||
{
|
||||
ESP_LOGI(TAG, "Start, stream %p rsp_code %u reason %u", stream, rsp_code, reason);
|
||||
}
|
||||
|
||||
static void stop_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_bap_ascs_rsp_code_t rsp_code,
|
||||
esp_ble_audio_bap_ascs_reason_t reason)
|
||||
{
|
||||
ESP_LOGI(TAG, "Stop, stream %p rsp_code %u reason %u", stream, rsp_code, reason);
|
||||
}
|
||||
|
||||
static void disable_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_bap_ascs_rsp_code_t rsp_code,
|
||||
esp_ble_audio_bap_ascs_reason_t reason)
|
||||
{
|
||||
ESP_LOGI(TAG, "Disable, stream %p rsp_code %u reason %u", stream, rsp_code, reason);
|
||||
}
|
||||
|
||||
static void metadata_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_bap_ascs_rsp_code_t rsp_code,
|
||||
esp_ble_audio_bap_ascs_reason_t reason)
|
||||
{
|
||||
ESP_LOGI(TAG, "Metadata, stream %p rsp_code %u reason %u", stream, rsp_code, reason);
|
||||
}
|
||||
|
||||
static void release_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_bap_ascs_rsp_code_t rsp_code,
|
||||
esp_ble_audio_bap_ascs_reason_t reason)
|
||||
{
|
||||
ESP_LOGI(TAG, "Release, stream %p rsp_code %u reason %u", stream, rsp_code, reason);
|
||||
}
|
||||
|
||||
static void discover_cb(esp_ble_conn_t *conn, int err, esp_ble_audio_dir_t dir)
|
||||
{
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Discovery failed, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (default_conn_handle_get() != conn->handle) {
|
||||
ESP_LOGE(TAG, "%s, not connected", __func__);
|
||||
return;
|
||||
}
|
||||
|
||||
if (dir == ESP_BLE_AUDIO_DIR_SINK) {
|
||||
ESP_LOGI(TAG, "Sink discover complete");
|
||||
|
||||
err = esp_ble_audio_bap_unicast_client_discover(conn->handle, ESP_BLE_AUDIO_DIR_SOURCE);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to discover source, err %d", err);
|
||||
return;
|
||||
}
|
||||
} else if (dir == ESP_BLE_AUDIO_DIR_SOURCE) {
|
||||
ESP_LOGI(TAG, "Source discover complete");
|
||||
|
||||
err = unicast_group_create();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = unicast_audio_start(conn);
|
||||
if (err) {
|
||||
unicast_group_delete();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void pac_record_cb(esp_ble_conn_t *conn, esp_ble_audio_dir_t dir,
|
||||
const esp_ble_audio_codec_cap_t *codec_cap)
|
||||
{
|
||||
example_print_codec_cap(codec_cap);
|
||||
}
|
||||
|
||||
static void endpoint_cb(esp_ble_conn_t *conn,
|
||||
esp_ble_audio_dir_t dir,
|
||||
esp_ble_audio_bap_ep_t *ep)
|
||||
{
|
||||
if (dir == ESP_BLE_AUDIO_DIR_SOURCE) {
|
||||
for (size_t i = 0; i < ARRAY_SIZE(unicast_source_eps); i++) {
|
||||
if (unicast_source_eps[i] == NULL) {
|
||||
ESP_LOGI(TAG, "Source #%zu: ep %p", i, ep);
|
||||
unicast_source_eps[i] = ep;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (dir == ESP_BLE_AUDIO_DIR_SINK) {
|
||||
for (size_t i = 0; i < ARRAY_SIZE(unicast_sink_eps); i++) {
|
||||
if (unicast_sink_eps[i] == NULL) {
|
||||
ESP_LOGI(TAG, "Sink #%zu: ep %p", i, ep);
|
||||
unicast_sink_eps[i] = ep;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Unhandled endpoint %p, dir %u", ep, dir);
|
||||
}
|
||||
|
||||
static esp_ble_audio_bap_unicast_client_cb_t unicast_client_cbs = {
|
||||
.location = location_cb,
|
||||
.available_contexts = available_contexts_cb,
|
||||
.config = config_cb,
|
||||
.qos = qos_cb,
|
||||
.enable = enable_cb,
|
||||
.start = start_cb,
|
||||
.stop = stop_cb,
|
||||
.disable = disable_cb,
|
||||
.metadata = metadata_cb,
|
||||
.release = release_cb,
|
||||
.pac_record = pac_record_cb,
|
||||
.endpoint = endpoint_cb,
|
||||
.discover = discover_cb,
|
||||
};
|
||||
|
||||
static void unicast_audio_tx(void)
|
||||
{
|
||||
esp_ble_audio_bap_ep_info_t ep_info = {0};
|
||||
esp_ble_audio_cap_stream_t *cap_stream;
|
||||
esp_ble_audio_bap_stream_t *bap_stream;
|
||||
esp_err_t err;
|
||||
|
||||
cap_stream = &unicast_sink_streams[0];
|
||||
bap_stream = &unicast_sink_streams[0].bap_stream;
|
||||
|
||||
if (bap_stream->ep == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_ep_get_info(bap_stream->ep, &ep_info);
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ep_info.state != ESP_BLE_AUDIO_BAP_EP_STATE_STREAMING) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bap_stream->qos == NULL || bap_stream->qos->sdu == 0) {
|
||||
ESP_LOGE(TAG, "Invalid stream qos");
|
||||
return;
|
||||
}
|
||||
|
||||
if (iso_data == NULL) {
|
||||
ESP_LOGE(TAG, "TX buffer unavailable, SDU %u", bap_stream->qos->sdu);
|
||||
return;
|
||||
}
|
||||
|
||||
memset(iso_data, (uint8_t)tx_seq_num, bap_stream->qos->sdu);
|
||||
|
||||
err = esp_ble_audio_cap_stream_send(cap_stream, iso_data,
|
||||
bap_stream->qos->sdu,
|
||||
tx_seq_num);
|
||||
if (err) {
|
||||
ESP_LOGD(TAG, "Failed to transmit data on streams, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
tx_seq_num++;
|
||||
}
|
||||
|
||||
static void tx_scheduler_cb(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
unicast_audio_tx();
|
||||
}
|
||||
|
||||
int cap_initiator_init(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = esp_ble_audio_cap_initiator_register_cb(&cap_cb);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register CAP callbacks (err %d)", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_unicast_client_register_cb(&unicast_client_cbs);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register BAP unicast client callbacks (err %d)", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(unicast_sink_streams); i++) {
|
||||
esp_ble_audio_cap_stream_ops_register(&unicast_sink_streams[i], &unicast_stream_ops);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(unicast_source_streams); i++) {
|
||||
esp_ble_audio_cap_stream_ops_register(&unicast_source_streams[i], &unicast_stream_ops);
|
||||
}
|
||||
|
||||
err = example_audio_tx_scheduler_init(&tx_scheduler,
|
||||
tx_scheduler_cb,
|
||||
NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to init tx scheduler, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "CAP initiator initialized");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cap_initiator_setup(void)
|
||||
{
|
||||
uint16_t conn_handle;
|
||||
int err;
|
||||
|
||||
conn_handle = default_conn_handle_get();
|
||||
if (conn_handle == CONN_HANDLE_INIT) {
|
||||
ESP_LOGE(TAG, "%s, not connected", __func__);
|
||||
return -ENOTCONN;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_cap_initiator_unicast_discover(conn_handle);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to discover CAS, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "CAP initiator setup");
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 NXP
|
||||
* SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "tmap_central.h"
|
||||
|
||||
static bool tbs_originate_call_cb(esp_ble_conn_t *conn,
|
||||
uint8_t call_index,
|
||||
const char *caller_id)
|
||||
{
|
||||
ESP_LOGI(TAG, "CCP: Placing call to remote with id %u to %s",
|
||||
call_index, caller_id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void tbs_terminate_call_cb(esp_ble_conn_t *conn,
|
||||
uint8_t call_index,
|
||||
uint8_t reason)
|
||||
{
|
||||
ESP_LOGI(TAG, "CCP: Call terminated for id %u with reason %u",
|
||||
call_index, reason);
|
||||
}
|
||||
|
||||
static esp_ble_audio_tbs_cb_t tbs_cbs = {
|
||||
.originate_call = tbs_originate_call_cb,
|
||||
.terminate_call = tbs_terminate_call_cb,
|
||||
};
|
||||
|
||||
int ccp_server_init(void)
|
||||
{
|
||||
const esp_ble_audio_tbs_register_param_t gtbs_param = {
|
||||
.provider_name = "Generic TBS",
|
||||
.uci = "un000",
|
||||
.uri_schemes_supported = "tel,wechat",
|
||||
.gtbs = true,
|
||||
.authorization_required = false,
|
||||
.technology = ESP_BLE_AUDIO_TBS_TECHNOLOGY_5G,
|
||||
.supported_features = CONFIG_BT_TBS_SUPPORTED_FEATURES,
|
||||
};
|
||||
uint8_t bearer_index;
|
||||
int err;
|
||||
|
||||
esp_ble_audio_tbs_register_cb(&tbs_cbs);
|
||||
|
||||
err = esp_ble_audio_tbs_register_bearer(>bs_param, &bearer_index);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register gtbs bearer (err %d)", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Registered gtbs bearer %u", bearer_index);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
dependencies:
|
||||
example_init:
|
||||
path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_init
|
||||
example_utils:
|
||||
path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_utils
|
||||
@@ -0,0 +1,446 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 NXP
|
||||
* SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_timer.h"
|
||||
|
||||
#include "host/ble_hs.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
|
||||
#include "tmap_central.h"
|
||||
|
||||
#include "ble_audio_example_init.h"
|
||||
#include "ble_audio_example_utils.h"
|
||||
|
||||
#define SCAN_INTERVAL 160 /* 100ms */
|
||||
#define SCAN_WINDOW 160 /* 100ms */
|
||||
|
||||
#define INIT_SCAN_INTERVAL 16 /* 10ms */
|
||||
#define INIT_SCAN_WINDOW 16 /* 10ms */
|
||||
#define CONN_INTERVAL 64 /* 64 * 1.25 = 80ms */
|
||||
#define CONN_LATENCY 0
|
||||
#define CONN_TIMEOUT 500 /* 500 * 10ms = 5s */
|
||||
#define CONN_MAX_CE_LEN 0xFFFF
|
||||
#define CONN_MIN_CE_LEN 0xFFFF
|
||||
#define CONN_DURATION 10000 /* 10s */
|
||||
|
||||
static uint16_t default_conn_handle = CONN_HANDLE_INIT;
|
||||
static bool disc_completed;
|
||||
static bool disc_cancelled;
|
||||
static bool mtu_exchanged;
|
||||
|
||||
uint16_t default_conn_handle_get(void)
|
||||
{
|
||||
return default_conn_handle;
|
||||
}
|
||||
|
||||
static void tmap_discovery_complete(esp_ble_audio_tmap_role_t role,
|
||||
esp_ble_conn_t *conn, int err)
|
||||
{
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "TMAP discovery completed, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (conn->handle != default_conn_handle) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "TMAP discovery done");
|
||||
|
||||
err = cap_initiator_setup();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static esp_ble_audio_tmap_cb_t tmap_callbacks = {
|
||||
.discovery_complete = tmap_discovery_complete,
|
||||
};
|
||||
|
||||
static void ext_scan_start(void)
|
||||
{
|
||||
struct ble_gap_disc_params params = {0};
|
||||
uint8_t own_addr_type;
|
||||
int err;
|
||||
|
||||
err = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
params.passive = 1;
|
||||
params.itvl = SCAN_INTERVAL;
|
||||
params.window = SCAN_WINDOW;
|
||||
|
||||
err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms,
|
||||
example_audio_gap_event_cb, NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start scanning, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Extended scan started");
|
||||
}
|
||||
|
||||
static int conn_create(ble_addr_t *dst)
|
||||
{
|
||||
struct ble_gap_conn_params params = {0};
|
||||
uint8_t own_addr_type = 0;
|
||||
int err;
|
||||
|
||||
err = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to determine address type, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = ble_gap_disc_cancel();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop scanning, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
disc_cancelled = true;
|
||||
|
||||
params.scan_itvl = INIT_SCAN_INTERVAL;
|
||||
params.scan_window = INIT_SCAN_WINDOW;
|
||||
params.itvl_min = CONN_INTERVAL;
|
||||
params.itvl_max = CONN_INTERVAL;
|
||||
params.latency = CONN_LATENCY;
|
||||
params.supervision_timeout = CONN_TIMEOUT;
|
||||
params.max_ce_len = CONN_MAX_CE_LEN;
|
||||
params.min_ce_len = CONN_MIN_CE_LEN;
|
||||
|
||||
return ble_gap_connect(own_addr_type, dst, CONN_DURATION, ¶ms,
|
||||
example_audio_gap_event_cb, NULL);
|
||||
}
|
||||
|
||||
static int pairing_start(uint16_t conn_handle)
|
||||
{
|
||||
return ble_gap_security_initiate(conn_handle);
|
||||
}
|
||||
|
||||
static int exchange_mtu(uint16_t conn_handle)
|
||||
{
|
||||
return ble_gattc_exchange_mtu(conn_handle, NULL, NULL);
|
||||
}
|
||||
|
||||
static bool check_and_connect(uint8_t type, const uint8_t *data,
|
||||
uint8_t data_len, void *user_data)
|
||||
{
|
||||
esp_ble_audio_gap_app_event_t *event;
|
||||
ble_addr_t dst = {0};
|
||||
uint16_t tmap_role;
|
||||
uint16_t uuid_val;
|
||||
int err;
|
||||
|
||||
event = user_data;
|
||||
assert(event);
|
||||
|
||||
if (type != EXAMPLE_AD_TYPE_SERVICE_DATA16) {
|
||||
return true; /* Continue parsing to next AD data type */
|
||||
}
|
||||
|
||||
if (data_len < sizeof(uuid_val)) {
|
||||
ESP_LOGW(TAG, "Invalid ad size %u (tmap uuid)", data_len);
|
||||
return true; /* Continue parsing to next AD data type */
|
||||
}
|
||||
|
||||
uuid_val = sys_get_le16(data);
|
||||
|
||||
if (uuid_val != ESP_BLE_AUDIO_UUID_TMAS_VAL) {
|
||||
/* We are looking for the TMAS service data */
|
||||
return true; /* Continue parsing to next AD data type */
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Found TMAS in peer adv data!");
|
||||
|
||||
if (data_len < sizeof(uuid_val) + sizeof(tmap_role)) {
|
||||
ESP_LOGW(TAG, "Invalid ad size %u (tmap role)", data_len);
|
||||
return false; /* Stop parsing */
|
||||
}
|
||||
|
||||
tmap_role = sys_get_le16(data + sizeof(uuid_val));
|
||||
|
||||
if ((tmap_role & ESP_BLE_AUDIO_TMAP_ROLE_UMR) == 0) {
|
||||
ESP_LOGW(TAG, "No TMAS UMR support!");
|
||||
return false; /* Stop parsing */
|
||||
}
|
||||
|
||||
dst.type = event->ext_scan_recv.addr.type;
|
||||
memcpy(dst.val, event->ext_scan_recv.addr.val, sizeof(dst.val));
|
||||
|
||||
err = conn_create(&dst);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to create conn, err %d", err);
|
||||
|
||||
if (disc_cancelled) {
|
||||
disc_cancelled = false;
|
||||
ext_scan_start();
|
||||
}
|
||||
}
|
||||
|
||||
return false; /* Stop parsing */
|
||||
}
|
||||
|
||||
static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
if (default_conn_handle != CONN_HANDLE_INIT) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Check if the advertising is connectable and if TMAS is supported */
|
||||
if (event->ext_scan_recv.event_type & EXAMPLE_ADV_PROP_CONNECTABLE) {
|
||||
esp_ble_audio_data_parse(event->ext_scan_recv.data,
|
||||
event->ext_scan_recv.data_len,
|
||||
check_and_connect, (void *)event);
|
||||
}
|
||||
}
|
||||
|
||||
static void acl_connect(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
int err;
|
||||
|
||||
if (event->acl_connect.status) {
|
||||
ESP_LOGE(TAG, "connection failed, status %d", event->acl_connect.status);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Conn established:");
|
||||
ESP_LOGI(TAG, "conn_handle 0x%04x status 0x%02x role %u peer %02x:%02x:%02x:%02x:%02x:%02x",
|
||||
event->acl_connect.conn_handle, event->acl_connect.status,
|
||||
event->acl_connect.role, event->acl_connect.dst.val[5],
|
||||
event->acl_connect.dst.val[4], event->acl_connect.dst.val[3],
|
||||
event->acl_connect.dst.val[2], event->acl_connect.dst.val[1],
|
||||
event->acl_connect.dst.val[0]);
|
||||
|
||||
err = pairing_start(event->acl_connect.conn_handle);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initiate security, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
default_conn_handle = event->acl_connect.conn_handle;
|
||||
}
|
||||
|
||||
static void acl_disconnect(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
ESP_LOGI(TAG, "Conn terminated: conn_handle 0x%04x reason 0x%02x",
|
||||
event->acl_disconnect.conn_handle, event->acl_disconnect.reason);
|
||||
|
||||
default_conn_handle = CONN_HANDLE_INIT;
|
||||
disc_completed = false;
|
||||
disc_cancelled = false;
|
||||
mtu_exchanged = false;
|
||||
|
||||
unicast_group_delete();
|
||||
|
||||
ext_scan_start();
|
||||
}
|
||||
|
||||
static void security_change(esp_ble_iso_gap_app_event_t *event)
|
||||
{
|
||||
int err;
|
||||
|
||||
if (event->security_change.status) {
|
||||
ESP_LOGE(TAG, "security change failed, status %d", event->security_change.status);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Security change:");
|
||||
ESP_LOGI(TAG, "conn_handle 0x%04x status 0x%02x role %u sec_level %u bonded %u "
|
||||
"peer %02x:%02x:%02x:%02x:%02x:%02x",
|
||||
event->security_change.conn_handle, event->security_change.status,
|
||||
event->security_change.role, event->security_change.sec_level,
|
||||
event->security_change.bonded, event->security_change.dst.val[5],
|
||||
event->security_change.dst.val[4], event->security_change.dst.val[3],
|
||||
event->security_change.dst.val[2], event->security_change.dst.val[1],
|
||||
event->security_change.dst.val[0]);
|
||||
|
||||
err = exchange_mtu(event->security_change.conn_handle);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to exchange MTU, err %d", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
switch (event->type) {
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_EXT_SCAN_RECV:
|
||||
ext_scan_recv(event);
|
||||
break;
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_ACL_CONNECT:
|
||||
acl_connect(event);
|
||||
break;
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_ACL_DISCONNECT:
|
||||
acl_disconnect(event);
|
||||
break;
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_SECURITY_CHANGE:
|
||||
security_change(event);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "gatt mtu change, conn_handle %u, mtu %u",
|
||||
event->gatt_mtu_change.conn_handle, event->gatt_mtu_change.mtu);
|
||||
|
||||
if (event->gatt_mtu_change.mtu < ESP_BLE_AUDIO_ATT_MTU_MIN) {
|
||||
ESP_LOGW(TAG, "Invalid new mtu %u, shall be at least %u",
|
||||
event->gatt_mtu_change.mtu, ESP_BLE_AUDIO_ATT_MTU_MIN);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_gattc_disc_start(event->gatt_mtu_change.conn_handle);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start svc disc, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Start discovering gatt services");
|
||||
|
||||
/* Note:
|
||||
* MTU exchanged event may arrived after discover completed event.
|
||||
*/
|
||||
mtu_exchanged = true;
|
||||
|
||||
if (disc_completed) {
|
||||
err = esp_ble_audio_tmap_discover(default_conn_handle, &tmap_callbacks);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to discover tmap, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
vcp_vol_ctlr_discover();
|
||||
}
|
||||
}
|
||||
|
||||
static void gattc_disc_cmpl(esp_ble_audio_gatt_app_event_t *event)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "gattc disc cmpl, status %u, conn_handle %u",
|
||||
event->gattc_disc_cmpl.status, event->gattc_disc_cmpl.conn_handle);
|
||||
|
||||
if (event->gattc_disc_cmpl.status) {
|
||||
ESP_LOGE(TAG, "gattc disc failed, status %u", event->gattc_disc_cmpl.status);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Note:
|
||||
* Discover completed event may arrived before MTU exchanged event.
|
||||
*/
|
||||
disc_completed = true;
|
||||
|
||||
if (mtu_exchanged) {
|
||||
err = esp_ble_audio_tmap_discover(default_conn_handle, &tmap_callbacks);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to discover tmap, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
vcp_vol_ctlr_discover();
|
||||
}
|
||||
}
|
||||
|
||||
static void iso_gatt_app_cb(esp_ble_audio_gatt_app_event_t *event)
|
||||
{
|
||||
switch (event->type) {
|
||||
case ESP_BLE_AUDIO_GATT_EVENT_GATT_MTU_CHANGE:
|
||||
gatt_mtu_change(event);
|
||||
break;
|
||||
case ESP_BLE_AUDIO_GATT_EVENT_GATTC_DISC_CMPL:
|
||||
gattc_disc_cmpl(event);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
esp_ble_audio_start_info_t start_info = {0};
|
||||
esp_ble_audio_init_info_t init_info = {
|
||||
.gap_cb = iso_gap_app_cb,
|
||||
.gatt_cb = iso_gatt_app_cb,
|
||||
};
|
||||
esp_err_t err;
|
||||
|
||||
/* Initialize NVS — it is used to store PHY calibration data */
|
||||
err = nvs_flash_init();
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
err = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(err);
|
||||
|
||||
err = bluetooth_init();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize BLE, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_common_init(&init_info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize audio, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_tmap_register(ESP_BLE_AUDIO_TMAP_ROLE_CG | ESP_BLE_AUDIO_TMAP_ROLE_UMS);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register tmap, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = cap_initiator_init();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = vcp_vol_ctlr_init();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = mcp_server_init();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = ccp_server_init();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_common_start(&start_info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start audio, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = ble_svc_gap_device_name_set("TMAP Central");
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set device name, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ext_scan_start();
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 NXP
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "tmap_central.h"
|
||||
|
||||
int mcp_server_init(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = esp_ble_audio_media_proxy_pl_init();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to init media proxy pl (err %d)", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "MCP server initialized");
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 NXP
|
||||
* SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#include "esp_ble_audio_lc3_defs.h"
|
||||
#include "esp_ble_audio_aics_api.h"
|
||||
#include "esp_ble_audio_cap_api.h"
|
||||
#include "esp_ble_audio_pacs_api.h"
|
||||
#include "esp_ble_audio_vcp_api.h"
|
||||
#include "esp_ble_audio_tbs_api.h"
|
||||
#include "esp_ble_audio_tmap_api.h"
|
||||
#include "esp_ble_audio_csip_api.h"
|
||||
#include "esp_ble_audio_mcs_defs.h"
|
||||
#include "esp_ble_audio_mcc_api.h"
|
||||
#include "esp_ble_audio_media_proxy_api.h"
|
||||
#include "esp_ble_audio_vocs_api.h"
|
||||
#include "esp_ble_audio_tbs_api.h"
|
||||
|
||||
#include "ble_audio_example_utils.h"
|
||||
|
||||
#define TAG "TMAP_CEN"
|
||||
|
||||
#define CONN_HANDLE_INIT 0xFFFF
|
||||
|
||||
uint16_t default_conn_handle_get(void);
|
||||
|
||||
int mcp_server_init(void);
|
||||
|
||||
int ccp_server_init(void);
|
||||
|
||||
int vcp_vol_ctlr_init(void);
|
||||
|
||||
int vcp_vol_ctlr_discover(void);
|
||||
|
||||
int cap_initiator_init(void);
|
||||
|
||||
int cap_initiator_setup(void);
|
||||
|
||||
int unicast_group_delete(void);
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 NXP
|
||||
* SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "tmap_central.h"
|
||||
|
||||
static esp_ble_audio_vcp_vol_ctlr_t *vcp_vol_ctlr;
|
||||
|
||||
static void vcs_discover_cb(esp_ble_audio_vcp_vol_ctlr_t *vol_ctlr, int err,
|
||||
uint8_t vocs_count, uint8_t aics_count)
|
||||
{
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "VCP discovery cb failed, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "VCP discovery done, %p %p", vol_ctlr, vcp_vol_ctlr);
|
||||
|
||||
err = esp_ble_audio_vcp_vol_ctlr_mute(vol_ctlr);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to send mute command, err %d", err);
|
||||
}
|
||||
}
|
||||
|
||||
static void vcs_write_cb(esp_ble_audio_vcp_vol_ctlr_t *vol_ctlr, int err)
|
||||
{
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "VCP write cb failed, err %d", err);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "VCP write cb done");
|
||||
}
|
||||
}
|
||||
|
||||
static void vcs_state_cb(esp_ble_audio_vcp_vol_ctlr_t *vol_ctlr,
|
||||
int err, uint8_t volume, uint8_t mute)
|
||||
{
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "VCP state cb failed, err %d", err);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "VCP state cb done, volume %u mute %u", volume, mute);
|
||||
}
|
||||
}
|
||||
|
||||
static void vcs_flags_cb(esp_ble_audio_vcp_vol_ctlr_t *vol_ctlr,
|
||||
int err, uint8_t flags)
|
||||
{
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "VCP flags cb failed, err %d", err);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "VCP flags cb done, flags 0x%02x", flags);
|
||||
}
|
||||
}
|
||||
|
||||
static esp_ble_audio_vcp_vol_ctlr_cb_t vcp_cbs = {
|
||||
.discover = vcs_discover_cb,
|
||||
.vol_down = vcs_write_cb,
|
||||
.vol_up = vcs_write_cb,
|
||||
.mute = vcs_write_cb,
|
||||
.unmute = vcs_write_cb,
|
||||
.vol_down_unmute = vcs_write_cb,
|
||||
.vol_up_unmute = vcs_write_cb,
|
||||
.vol_set = vcs_write_cb,
|
||||
.state = vcs_state_cb,
|
||||
.flags = vcs_flags_cb,
|
||||
};
|
||||
|
||||
int vcp_vol_ctlr_discover(void)
|
||||
{
|
||||
uint16_t conn_handle;
|
||||
int err;
|
||||
|
||||
conn_handle = default_conn_handle_get();
|
||||
if (conn_handle == CONN_HANDLE_INIT) {
|
||||
ESP_LOGE(TAG, "%s, not connected", __func__);
|
||||
return -ENOTCONN;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_vcp_vol_ctlr_discover(conn_handle, &vcp_vol_ctlr);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to discover vcp vol ctlr, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "VCP volume controller discovering");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int vcp_vol_ctlr_init(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = esp_ble_audio_vcp_vol_ctlr_cb_register(&vcp_cbs);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register vcp vol ctlr cb, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "VCP volume controller initialized");
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
# This file was generated using idf.py save-defconfig. It can be edited manually.
|
||||
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
|
||||
#
|
||||
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_BLUEDROID_ENABLED=n
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_EXT_ADV=y
|
||||
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
|
||||
CONFIG_BT_NIMBLE_MAX_CCCDS=40
|
||||
CONFIG_BT_NIMBLE_ISO=y
|
||||
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
|
||||
|
||||
CONFIG_BT_ISO_MAX_CHAN=2
|
||||
|
||||
CONFIG_BT_TMAP=y
|
||||
CONFIG_BT_CAP_INITIATOR=y
|
||||
CONFIG_BT_CAP_COMMANDER=y
|
||||
CONFIG_BT_CSIP_SET_COORDINATOR=y
|
||||
CONFIG_BT_BAP_UNICAST_CLIENT=y
|
||||
CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT=2
|
||||
CONFIG_BT_VCP_VOL_CTLR=y
|
||||
CONFIG_BT_MPL=y
|
||||
CONFIG_BT_MCS=y
|
||||
CONFIG_BT_MCTL=y
|
||||
CONFIG_BT_MCTL_LOCAL_PLAYER_CONTROL=y
|
||||
CONFIG_BT_MCTL_LOCAL_PLAYER_REMOTE_CONTROL=y
|
||||
CONFIG_BT_TBS=y
|
||||
CONFIG_BT_TBS_SUPPORTED_FEATURES=3
|
||||
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
@@ -0,0 +1,6 @@
|
||||
# Override some defaults so BT stack is enabled
|
||||
# by default in this example
|
||||
|
||||
CONFIG_IDF_TARGET="esp32h4"
|
||||
|
||||
CONFIG_BT_LE_ISO_SUPPORT=y
|
||||
@@ -0,0 +1,8 @@
|
||||
# The following lines of boilerplate have to be in your project's CMakeLists
|
||||
# in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
|
||||
idf_build_set_property(MINIMAL_BUILD ON)
|
||||
project(tmap_peripheral)
|
||||
@@ -0,0 +1,78 @@
|
||||
| Supported Targets | ESP32-H4 |
|
||||
| ----------------- | -------- |
|
||||
|
||||
# TMAP Peripheral Example
|
||||
|
||||
(See the README.md file in the upper level `examples` directory for more information about examples.)
|
||||
|
||||
This example implements the **Telephone and Media Audio Profile (TMAP) Peripheral** roles: **Call Terminal (CT)** and **Unicast Media Receiver (UMR)**. The peripheral advertises connectable extended advertising with ASCS, CAS, and **TMAS (Telephone and Media Audio Service)** with **UMR and CT** roles, plus targeted unicast announcement (sink/source contexts) and device name; optionally CSIS RSI when built as a set member (e.g. duo earbuds). A TMAP Central (e.g. the [central](../central) example) that scans for TMAS UMR will discover and connect to this device. After connection, the central performs TMAP discovery; the peripheral also runs TMAP discovery to detect peer roles (CG/UMS), then discovers TBS (Telephone Bearer Service) on the central for call control. The peripheral acts as **BAP Unicast Server** (responds to codec config, QoS, enable, start; receives/sends unicast audio), **VCP Volume Renderer** (local volume/mute), **TBS client** (originate/terminate calls via central), and **MCP controller** (Media Control). Run it together with the [central](../central) example on another device as the TMAP Central.
|
||||
|
||||
The implementation uses the NimBLE host stack with ISO and LE Audio support, ESP-BLE-AUDIO (TMAP CT+UMR, CAP acceptor, BAP unicast server, VCP volume renderer, TBS client, optional CSIP set member, MCP/MCC). Optional Kconfig: earbuds type (single / duo with set member), device rank, earbud location, device name.
|
||||
|
||||
## Requirements
|
||||
|
||||
* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4)
|
||||
* Another device running the [central](../central) example as TMAP Central (CG + UMS)
|
||||
|
||||
## How to Use Example
|
||||
|
||||
Before project configuration and build, set the correct chip target:
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32h4
|
||||
```
|
||||
|
||||
### Configure the Project
|
||||
|
||||
Open the configuration menu to set earbuds type, location, and device name:
|
||||
|
||||
```bash
|
||||
idf.py menuconfig
|
||||
```
|
||||
|
||||
Under **Example: TMAP Peripheral (CT & UMR)** you can set:
|
||||
|
||||
* **Earbuds type** — Single ear headset or Duo headset (duo enables CSIP set member and adds RSI to advertising)
|
||||
* **Device rank in set** — Rank of this device in set (when duo is selected)
|
||||
* **Earbud Location** — Left Ear or Right Ear
|
||||
|
||||
### Build and Flash
|
||||
|
||||
Run the following to build, flash and monitor:
|
||||
|
||||
```bash
|
||||
idf.py -p PORT flash monitor
|
||||
```
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF.
|
||||
|
||||
## Example Flow
|
||||
|
||||
1. **Initialization**: NVS, Bluetooth stack, and LE Audio common layer (`esp_ble_audio_common_init`) with GAP and GATT callbacks. Register TMAP roles CT and UMR (`esp_ble_audio_tmap_register(ESP_BLE_AUDIO_TMAP_ROLE_CT | ESP_BLE_AUDIO_TMAP_ROLE_UMR)`). Optionally initialize CSIP set member and generate RSI for advertising. Initialize VCP volume renderer. Initialize BAP unicast server (PACS, ASCS callbacks, stream ops). Initialize CCP call control (TBS client). Initialize MCP controller. Start the audio stack (with optional CSIS instance when set member) and set the device name.
|
||||
2. **Advertising**: Start connectable extended advertising with flags, appearance (earbud), ASCS/CAS/TMAS UUIDs, ASCS targeted + contexts, CAS targeted, TMAS with UMR|CT, device name, and optionally CSIS RSI.
|
||||
3. **Central connection**: When the central connects, store the connection handle. On MTU change, start GATT service discovery. On discovery complete, start TMAP discovery (`tmap_discover_tmas`).
|
||||
4. **TMAP discovery complete**: Store whether the peer is CG and/or UMS; start TBS discovery on the central (`ccp_discover_tbs`). The central will then run CAP initiator setup and configure unicast streams.
|
||||
5. **Unicast**: The central configures and starts streams; the peripheral handles ASCS config/QoS/enable/start, starts sink streams when enabled, and receives/sends audio on stream callbacks.
|
||||
6. **Call control**: The peripheral (TBS client) can originate or terminate calls via the central’s TBS (CCP server); discover and URI list read complete in callbacks.
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
I (xxx) TMAP_PER: Extended adv instance 0 started
|
||||
I (xxx) TMAP_PER: connection established, status 0
|
||||
I (xxx) TMAP_PER: gatt mtu change, conn_handle 1, mtu ...
|
||||
I (xxx) TMAP_PER: gattc disc cmpl, status 0, conn_handle 1
|
||||
I (xxx) TMAP_PER: TMAP discovery done
|
||||
I (xxx) TMAP_PER: CCP: Discovered GTBS
|
||||
...
|
||||
I (xxx) TMAP_PER: Stream 0x... started
|
||||
...
|
||||
```
|
||||
|
||||
If the connection is lost:
|
||||
|
||||
```
|
||||
I (xxx) TMAP_PER: connection disconnected, reason 0x...
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
set(srcs "bap_unicast_sr.c"
|
||||
"button.c"
|
||||
"ccp_call_ctrl.c"
|
||||
"mcp_ctlr.c"
|
||||
"tmap_ct_umr.c"
|
||||
"vcp_vol_renderer.c"
|
||||
"main.c")
|
||||
|
||||
if(CONFIG_EXAMPLE_TMAP_PER_DUO)
|
||||
list(APPEND srcs "csip_set_member.c")
|
||||
endif()
|
||||
|
||||
idf_component_register(SRCS "${srcs}"
|
||||
REQUIRES bt nvs_flash)
|
||||
@@ -0,0 +1,44 @@
|
||||
# SPDX-FileCopyrightText: 2022 Nordic Semiconductor ASA
|
||||
# SPDX-FileContributor: 2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
menu "Example: TMAP Peripheral (CT & UMR)"
|
||||
|
||||
config EXAMPLE_TMAP_PER_SET_RANK
|
||||
int "Device rank in set"
|
||||
depends on EXAMPLE_TMAP_PER_DUO
|
||||
range 1 2
|
||||
help
|
||||
Rank of this device in set.
|
||||
|
||||
choice EXAMPLE_TMAP_PER_TYPE_CHOICE
|
||||
|
||||
prompt "Earbuds type"
|
||||
help
|
||||
Select the Earbuds Type to compile.
|
||||
|
||||
config EXAMPLE_TMAP_PER_SINGLE
|
||||
bool "Single ear headset"
|
||||
|
||||
config EXAMPLE_TMAP_PER_DUO
|
||||
bool "Duo headset"
|
||||
select BT_CSIP_SET_MEMBER
|
||||
select BT_CAP_ACCEPTOR_SET_MEMBER
|
||||
|
||||
endchoice
|
||||
|
||||
choice EXAMPLE_TMAP_PER_LOCATION
|
||||
|
||||
prompt "Earbud Location"
|
||||
help
|
||||
Select the Earbud location.
|
||||
|
||||
config EXAMPLE_TMAP_PER_LEFT
|
||||
bool "Left Ear"
|
||||
|
||||
config EXAMPLE_TMAP_PER_RIGHT
|
||||
bool "Right Ear"
|
||||
|
||||
endchoice
|
||||
|
||||
endmenu
|
||||
@@ -0,0 +1,517 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021-2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileCopyrightText: 2022 Codecoup
|
||||
* SPDX-FileCopyrightText: 2023 NXP
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "tmap_peripheral.h"
|
||||
|
||||
static uint8_t codec_data[] =
|
||||
ESP_BLE_AUDIO_CODEC_CAP_LC3_DATA(
|
||||
ESP_BLE_AUDIO_CODEC_CAP_FREQ_16KHZ | \
|
||||
ESP_BLE_AUDIO_CODEC_CAP_FREQ_32KHZ | \
|
||||
ESP_BLE_AUDIO_CODEC_CAP_FREQ_48KHZ, /* Sampling frequency 16kHz/32kHz/48kHz */
|
||||
ESP_BLE_AUDIO_CODEC_CAP_DURATION_7_5 | \
|
||||
ESP_BLE_AUDIO_CODEC_CAP_DURATION_10, /* Frame duration 7.5ms/10ms */
|
||||
ESP_BLE_AUDIO_CODEC_CAP_CHAN_COUNT_SUPPORT(1), /* Supported channels 1 */
|
||||
30, /* Minimum 30 octets per frame */
|
||||
155, /* Maximum 155 octets per frame */
|
||||
1); /* Maximum 1 codec frame per SDU */
|
||||
|
||||
static uint8_t codec_meta[] =
|
||||
ESP_BLE_AUDIO_CODEC_CAP_LC3_META(SINK_CONTEXT | SOURCE_CONTEXT);
|
||||
|
||||
static const esp_ble_audio_codec_cap_t lc3_codec_cap =
|
||||
ESP_BLE_AUDIO_CODEC_CAP_LC3(codec_data, codec_meta);
|
||||
|
||||
static esp_ble_audio_pacs_cap_t cap = {
|
||||
.codec_cap = &lc3_codec_cap,
|
||||
};
|
||||
|
||||
static esp_ble_audio_bap_stream_t streams[CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT + \
|
||||
CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT];
|
||||
|
||||
static struct audio_sink {
|
||||
esp_ble_audio_bap_stream_t *stream;
|
||||
example_audio_rx_metrics_t rx_metrics;
|
||||
} sink_streams[CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT];
|
||||
|
||||
static size_t configured_sink_stream_count;
|
||||
|
||||
static struct audio_source {
|
||||
esp_ble_audio_bap_stream_t *stream;
|
||||
uint16_t seq_num;
|
||||
} source_streams[CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT];
|
||||
|
||||
static size_t configured_source_stream_count;
|
||||
|
||||
static const esp_ble_audio_bap_qos_cfg_pref_t qos_pref =
|
||||
ESP_BLE_AUDIO_BAP_QOS_CFG_PREF(true, /* Unframed PDUs supported */
|
||||
ESP_BLE_ISO_PHY_2M, /* Preferred Target PHY */
|
||||
2, /* Preferred Retransmission number */
|
||||
10, /* Preferred Maximum Transport Latency (msec) */
|
||||
20000, /* Minimum Presentation Delay (usec) */
|
||||
40000, /* Maximum Presentation Delay (usec) */
|
||||
20000, /* Preferred Minimum Presentation Delay (usec) */
|
||||
40000); /* Preferred Maximum Presentation Delay (usec) */
|
||||
|
||||
static esp_ble_audio_bap_stream_t *stream_alloc(void)
|
||||
{
|
||||
for (size_t i = 0; i < ARRAY_SIZE(streams); i++) {
|
||||
esp_ble_audio_bap_stream_t *stream = &streams[i];
|
||||
|
||||
if (stream->conn == NULL) {
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int config_cb(esp_ble_conn_t *conn,
|
||||
const esp_ble_audio_bap_ep_t *ep,
|
||||
esp_ble_audio_dir_t dir,
|
||||
const esp_ble_audio_codec_cfg_t *codec_cfg,
|
||||
esp_ble_audio_bap_stream_t **stream,
|
||||
esp_ble_audio_bap_qos_cfg_pref_t *const pref,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
bool stream_exist = false;
|
||||
|
||||
ESP_LOGI(TAG, "Config: conn %p ep %p dir %u", conn, ep, dir);
|
||||
|
||||
example_print_codec_cfg(codec_cfg);
|
||||
|
||||
*stream = stream_alloc();
|
||||
if (*stream == NULL) {
|
||||
ESP_LOGE(TAG, "No streams available");
|
||||
|
||||
*rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_NO_MEM,
|
||||
ESP_BLE_AUDIO_BAP_ASCS_REASON_NONE);
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Config stream %p", *stream);
|
||||
|
||||
if (dir == ESP_BLE_AUDIO_DIR_SINK) {
|
||||
for (size_t i = 0; i < ARRAY_SIZE(sink_streams); i++) {
|
||||
if (sink_streams[i].stream == *stream) {
|
||||
stream_exist = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (stream_exist == false) {
|
||||
if (configured_sink_stream_count >= ARRAY_SIZE(sink_streams)) {
|
||||
ESP_LOGE(TAG, "No more sink stream available");
|
||||
|
||||
*rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_NO_MEM,
|
||||
ESP_BLE_AUDIO_BAP_ASCS_REASON_NONE);
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
sink_streams[configured_sink_stream_count++].stream = *stream;
|
||||
}
|
||||
} else if (dir == ESP_BLE_AUDIO_DIR_SOURCE) {
|
||||
for (size_t i = 0; i < ARRAY_SIZE(source_streams); i++) {
|
||||
if (source_streams[i].stream == *stream) {
|
||||
stream_exist = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (stream_exist == false) {
|
||||
if (configured_source_stream_count >= ARRAY_SIZE(source_streams)) {
|
||||
ESP_LOGE(TAG, "No more source stream available");
|
||||
|
||||
*rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_NO_MEM,
|
||||
ESP_BLE_AUDIO_BAP_ASCS_REASON_NONE);
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
source_streams[configured_source_stream_count++].stream = *stream;
|
||||
}
|
||||
} else {
|
||||
assert(0);
|
||||
}
|
||||
|
||||
*pref = qos_pref;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int reconfig_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_dir_t dir,
|
||||
const esp_ble_audio_codec_cfg_t *codec_cfg,
|
||||
esp_ble_audio_bap_qos_cfg_pref_t *const pref,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
ESP_LOGI(TAG, "Reconfig: stream %p", stream);
|
||||
|
||||
example_print_codec_cfg(codec_cfg);
|
||||
|
||||
*pref = qos_pref;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int qos_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
const esp_ble_audio_bap_qos_cfg_t *qos,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
ESP_LOGI(TAG, "QoS: stream %p qos %p", stream, qos);
|
||||
|
||||
example_print_qos(qos);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int enable_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
const uint8_t meta[], size_t meta_len,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
ESP_LOGI(TAG, "Enable: stream %p meta_len %u", stream, meta_len);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int start_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
ESP_LOGI(TAG, "Start: stream %p", stream);
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct data_func_param {
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp;
|
||||
|
||||
bool stream_context_present;
|
||||
bool rejected;
|
||||
};
|
||||
|
||||
static bool data_func_cb(uint8_t type, const uint8_t *data,
|
||||
uint8_t data_len, void *user_data)
|
||||
{
|
||||
struct data_func_param *func_param = (struct data_func_param *)user_data;
|
||||
|
||||
if (!ESP_BLE_AUDIO_METADATA_TYPE_IS_KNOWN(type)) {
|
||||
ESP_LOGE(TAG, "Invalid metadata type %u or length %u", type, data_len);
|
||||
|
||||
*func_param->rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_METADATA_REJECTED,
|
||||
type);
|
||||
func_param->rejected = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (type == ESP_BLE_AUDIO_METADATA_TYPE_STREAM_CONTEXT) {
|
||||
func_param->stream_context_present = true;
|
||||
}
|
||||
|
||||
if (type == ESP_BLE_AUDIO_METADATA_TYPE_CCID_LIST) {
|
||||
for (uint8_t i = 0; i < data_len; i++) {
|
||||
const uint8_t ccid = data[i];
|
||||
|
||||
#if CONFIG_BT_TBS_CLIENT_CCID
|
||||
uint16_t conn_handle = default_conn_handle_get();
|
||||
|
||||
if (conn_handle == CONN_HANDLE_INIT) {
|
||||
ESP_LOGE(TAG, "%s, not connected", __func__);
|
||||
|
||||
*func_param->rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_METADATA_REJECTED,
|
||||
ESP_BLE_AUDIO_BAP_ASCS_REASON_NONE);
|
||||
func_param->rejected = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_ble_audio_tbs_client_get_by_ccid(conn_handle, ccid) == NULL) {
|
||||
ESP_LOGW(TAG, "CCID %u is unknown", ccid);
|
||||
|
||||
*func_param->rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_METADATA_REJECTED,
|
||||
ESP_BLE_AUDIO_BAP_ASCS_REASON_NONE);
|
||||
func_param->rejected = true;
|
||||
return false;
|
||||
}
|
||||
#else /* CONFIG_BT_TBS_CLIENT_CCID */
|
||||
ESP_LOGW(TAG, "CCID %u is unknown", ccid);
|
||||
|
||||
*func_param->rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_METADATA_REJECTED,
|
||||
ESP_BLE_AUDIO_BAP_ASCS_REASON_NONE);
|
||||
func_param->rejected = true;
|
||||
return false;
|
||||
#endif /* CONFIG_BT_TBS_CLIENT_CCID */
|
||||
}
|
||||
}
|
||||
|
||||
func_param->rejected = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static int metadata_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
const uint8_t meta[], size_t meta_len,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
struct data_func_param func_param = {
|
||||
.rsp = rsp,
|
||||
.stream_context_present = false,
|
||||
.rejected = false,
|
||||
};
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "Metadata: stream %p meta_len %u", stream, meta_len);
|
||||
|
||||
err = esp_ble_audio_data_parse(meta, meta_len, data_func_cb, &func_param);
|
||||
if (err) {
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
if (func_param.rejected) {
|
||||
/* The rsp is already set by data_func_cb */
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
if (func_param.stream_context_present == false) {
|
||||
ESP_LOGE(TAG, "Stream audio context not present on peer!");
|
||||
|
||||
*rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_METADATA_REJECTED,
|
||||
ESP_BLE_AUDIO_BAP_ASCS_REASON_NONE);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int disable_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
ESP_LOGI(TAG, "Disable: stream %p", stream);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int stop_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
ESP_LOGI(TAG, "Stop: stream %p", stream);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int release_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_bap_ascs_rsp_t *rsp)
|
||||
{
|
||||
ESP_LOGI(TAG, "Release: stream %p", stream);
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(sink_streams); i++) {
|
||||
if (sink_streams[i].stream == stream) {
|
||||
memset(&sink_streams[i], 0, sizeof(sink_streams[i]));
|
||||
if (configured_sink_stream_count > 0) {
|
||||
configured_sink_stream_count--;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(source_streams); i++) {
|
||||
if (source_streams[i].stream == stream) {
|
||||
memset(&source_streams[i], 0, sizeof(source_streams[i]));
|
||||
if (configured_source_stream_count > 0) {
|
||||
configured_source_stream_count--;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Stream %p not found", stream);
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
static const esp_ble_audio_bap_unicast_server_cb_t unicast_server_cb = {
|
||||
.config = config_cb,
|
||||
.reconfig = reconfig_cb,
|
||||
.qos = qos_cb,
|
||||
.enable = enable_cb,
|
||||
.start = start_cb,
|
||||
.metadata = metadata_cb,
|
||||
.disable = disable_cb,
|
||||
.stop = stop_cb,
|
||||
.release = release_cb,
|
||||
};
|
||||
|
||||
static void stream_enabled(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
esp_ble_audio_bap_ep_info_t ep_info = {0};
|
||||
int err;
|
||||
|
||||
ESP_LOGI(TAG, "Stream %p enabled", stream);
|
||||
|
||||
err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get ep info, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
/* The unicast server is responsible for starting the sink streams */
|
||||
if (ep_info.dir == ESP_BLE_AUDIO_DIR_SINK) {
|
||||
/* Automatically do the receiver start ready operation */
|
||||
err = esp_ble_audio_bap_stream_start(stream);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start stream %p, err %d", stream, err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void stream_started(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
ESP_LOGI(TAG, "Stream %p started", stream);
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(sink_streams); i++) {
|
||||
if (sink_streams[i].stream == stream) {
|
||||
example_audio_rx_metrics_reset(&sink_streams[i].rx_metrics);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void stream_stopped(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
|
||||
{
|
||||
ESP_LOGI(TAG, "Stream %p stopped, reason 0x%02x", stream, reason);
|
||||
}
|
||||
|
||||
static void stream_recv(esp_ble_audio_bap_stream_t *stream,
|
||||
const esp_ble_iso_recv_info_t *info,
|
||||
const uint8_t *data, uint16_t len)
|
||||
{
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(sink_streams); i++) {
|
||||
if (sink_streams[i].stream == stream) {
|
||||
struct audio_sink *sink_stream = CONTAINER_OF(&sink_streams[i].stream,
|
||||
struct audio_sink,
|
||||
stream);
|
||||
|
||||
sink_stream->rx_metrics.last_sdu_len = len;
|
||||
example_audio_rx_metrics_on_recv(info, &sink_stream->rx_metrics,
|
||||
TAG, "stream", stream);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static esp_ble_audio_bap_stream_ops_t stream_ops = {
|
||||
.enabled = stream_enabled,
|
||||
.started = stream_started,
|
||||
.stopped = stream_stopped,
|
||||
.recv = stream_recv,
|
||||
};
|
||||
|
||||
static esp_ble_audio_bap_unicast_server_register_param_t param = {
|
||||
CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT,
|
||||
CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT
|
||||
};
|
||||
|
||||
int bap_unicast_sr_init(void)
|
||||
{
|
||||
const esp_ble_audio_pacs_register_param_t pacs_param = {
|
||||
.snk_pac = true,
|
||||
.snk_loc = true,
|
||||
.src_pac = true,
|
||||
.src_loc = true,
|
||||
};
|
||||
esp_ble_audio_location_t location;
|
||||
int err;
|
||||
|
||||
location = 0;
|
||||
|
||||
err = esp_ble_audio_pacs_register(&pacs_param);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register pacs, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_unicast_server_register(¶m);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register Unicast Server params, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_unicast_server_register_cb(&unicast_server_cb);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register Unicast Server callbacks, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
#if CONFIG_EXAMPLE_TMAP_PER_LEFT
|
||||
location |= ESP_BLE_AUDIO_LOCATION_FRONT_LEFT;
|
||||
#endif /* CONFIG_EXAMPLE_TMAP_PER_LEFT */
|
||||
#if CONFIG_EXAMPLE_TMAP_PER_RIGHT
|
||||
location |= ESP_BLE_AUDIO_LOCATION_FRONT_RIGHT;
|
||||
#endif /* CONFIG_EXAMPLE_TMAP_PER_RIGHT */
|
||||
|
||||
#if CONFIG_BT_PAC_SNK
|
||||
/* Register CT required capabilities */
|
||||
err = esp_ble_audio_pacs_cap_register(ESP_BLE_AUDIO_DIR_SINK, &cap);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register pacs capabilities, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_pacs_set_location(ESP_BLE_AUDIO_DIR_SINK, location);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set pacs location, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_pacs_set_supported_contexts(ESP_BLE_AUDIO_DIR_SINK, SINK_CONTEXT);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set pacs supported contexts, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_pacs_set_available_contexts(ESP_BLE_AUDIO_DIR_SINK, SINK_CONTEXT);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set pacs available contexts, err %d", err);
|
||||
return err;
|
||||
}
|
||||
#endif /* CONFIG_BT_PAC_SNK */
|
||||
|
||||
#if CONFIG_BT_PAC_SRC
|
||||
/* Register CT required capabilities */
|
||||
err = esp_ble_audio_pacs_cap_register(ESP_BLE_AUDIO_DIR_SOURCE, &cap);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register pacs capabilities, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_pacs_set_location(ESP_BLE_AUDIO_DIR_SOURCE, location);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set pacs location, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_pacs_set_supported_contexts(ESP_BLE_AUDIO_DIR_SOURCE, SOURCE_CONTEXT);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set pacs supported contexts, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_pacs_set_available_contexts(ESP_BLE_AUDIO_DIR_SOURCE, SOURCE_CONTEXT);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set pacs available contexts, err %d", err);
|
||||
return err;
|
||||
}
|
||||
#endif /* CONFIG_BT_PAC_SRC */
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(streams); i++) {
|
||||
esp_ble_audio_bap_stream_cb_register(&streams[i], &stream_ops);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "BAP unicast server initialized");
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 NXP
|
||||
* SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include "tmap_peripheral.h"
|
||||
|
||||
void initiate_call(void)
|
||||
{
|
||||
if (tmap_peer_is_cg() == false) {
|
||||
ESP_LOGE(TAG, "Peer not support Call Gateway");
|
||||
return;
|
||||
}
|
||||
|
||||
ccp_originate_call();
|
||||
}
|
||||
|
||||
void terminate_call(void)
|
||||
{
|
||||
if (tmap_peer_is_cg() == false) {
|
||||
ESP_LOGE(TAG, "Peer not support Call Gateway");
|
||||
return;
|
||||
}
|
||||
|
||||
ccp_terminate_call();
|
||||
}
|
||||
|
||||
void play_media(void)
|
||||
{
|
||||
if (tmap_peer_is_ums() == false) {
|
||||
ESP_LOGE(TAG, "Peer not support Unicast Media Sender");
|
||||
return;
|
||||
}
|
||||
|
||||
mcp_send_cmd(ESP_BLE_AUDIO_MCS_OPC_PLAY);
|
||||
}
|
||||
|
||||
void pause_media(void)
|
||||
{
|
||||
if (tmap_peer_is_ums() == false) {
|
||||
ESP_LOGE(TAG, "Peer not support Unicast Media Sender");
|
||||
return;
|
||||
}
|
||||
|
||||
mcp_send_cmd(ESP_BLE_AUDIO_MCS_OPC_PAUSE);
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020-2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileCopyrightText: 2022 Codecoup
|
||||
* SPDX-FileCopyrightText: 2023 NXP
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "tmap_peripheral.h"
|
||||
|
||||
#define URI_SEPARATOR ":"
|
||||
#define CALLER_ID "friend"
|
||||
|
||||
static uint8_t new_call_index;
|
||||
static char remote_uri[CONFIG_BT_TBS_MAX_URI_LENGTH];
|
||||
|
||||
static void discover_cb(esp_ble_conn_t *conn,
|
||||
int err,
|
||||
uint8_t tbs_count,
|
||||
bool gtbs_found)
|
||||
{
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Discover GTBS failed, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Discovered GTBS");
|
||||
|
||||
if (gtbs_found == false) {
|
||||
ESP_LOGE(TAG, "No GTBS found");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Read Bearer URI Schemes Supported List Characteristic */
|
||||
err = esp_ble_audio_tbs_client_read_uri_list(default_conn_handle_get(),
|
||||
ESP_BLE_AUDIO_TBS_GTBS_INDEX);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to read URI list, err %d", err);
|
||||
}
|
||||
}
|
||||
|
||||
static void originate_call_cb(esp_ble_conn_t *conn,
|
||||
int err,
|
||||
uint8_t inst_index,
|
||||
uint8_t call_index)
|
||||
{
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Originate call failed, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Call %u originated", call_index);
|
||||
|
||||
if (inst_index != ESP_BLE_AUDIO_TBS_GTBS_INDEX) {
|
||||
ESP_LOGW(TAG, "Unexpected instance index %u", inst_index);
|
||||
return;
|
||||
}
|
||||
|
||||
new_call_index = call_index;
|
||||
}
|
||||
|
||||
static void terminate_call_cb(esp_ble_conn_t *conn,
|
||||
int err,
|
||||
uint8_t inst_index,
|
||||
uint8_t call_index)
|
||||
{
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Terminate call failed, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Call %u terminated", call_index);
|
||||
|
||||
if (inst_index != ESP_BLE_AUDIO_TBS_GTBS_INDEX) {
|
||||
ESP_LOGW(TAG, "Unexpected instance index %u", inst_index);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void read_uri_schemes_string_cb(esp_ble_conn_t *conn,
|
||||
int err,
|
||||
uint8_t inst_index,
|
||||
const char *value)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Read URI schemes string failed, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (inst_index != ESP_BLE_AUDIO_TBS_GTBS_INDEX) {
|
||||
ESP_LOGW(TAG, "Unexpected instance index %u", inst_index);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Save first remote URI
|
||||
*
|
||||
* First search for the first comma (separator), and use that to determine the end of the
|
||||
* first (or only) URI. Then use that length to copy the URI to `remote_uri` for later use.
|
||||
*/
|
||||
for (i = 0; i < strlen(value); i++) {
|
||||
if (value[i] == ',') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (i >= sizeof(remote_uri)) {
|
||||
ESP_LOGW(TAG, "Cannot store URI of length %u: %s", i, value);
|
||||
return;
|
||||
}
|
||||
|
||||
strncpy(remote_uri, value, i);
|
||||
remote_uri[i] = '\0';
|
||||
|
||||
ESP_LOGI(TAG, "Discovered remote URI %s", remote_uri);
|
||||
|
||||
/* CCP related GATT procedures done, start MCP discovery */
|
||||
mcp_discover_mcs();
|
||||
}
|
||||
|
||||
static esp_ble_audio_tbs_client_cb_t tbs_client_cb = {
|
||||
.discover = discover_cb,
|
||||
.uri_list = read_uri_schemes_string_cb,
|
||||
.originate_call = originate_call_cb,
|
||||
.terminate_call = terminate_call_cb,
|
||||
};
|
||||
|
||||
int ccp_call_ctrl_init(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = esp_ble_audio_tbs_client_register_cb(&tbs_client_cb);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register TBS client, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "CCP call controller initialized");
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
int ccp_discover_tbs(void)
|
||||
{
|
||||
uint16_t conn_handle;
|
||||
int err;
|
||||
|
||||
conn_handle = default_conn_handle_get();
|
||||
if (conn_handle == CONN_HANDLE_INIT) {
|
||||
ESP_LOGE(TAG, "%s, not connected", __func__);
|
||||
return -ENOTCONN;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_tbs_client_discover(conn_handle);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to discover TBS, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
int ccp_originate_call(void)
|
||||
{
|
||||
char uri[CONFIG_BT_TBS_MAX_URI_LENGTH] = {0};
|
||||
uint16_t conn_handle;
|
||||
int uri_len;
|
||||
int err;
|
||||
|
||||
conn_handle = default_conn_handle_get();
|
||||
if (conn_handle == CONN_HANDLE_INIT) {
|
||||
ESP_LOGE(TAG, "%s, not connected", __func__);
|
||||
return -ENOTCONN;
|
||||
}
|
||||
|
||||
uri_len = snprintf(uri, sizeof(uri), "%s%s%s", remote_uri, URI_SEPARATOR, CALLER_ID);
|
||||
if (uri_len < 0 || uri_len >= sizeof(uri)) {
|
||||
ESP_LOGE(TAG, "Invalid URI length %d", uri_len);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_tbs_client_originate_call(conn_handle,
|
||||
ESP_BLE_AUDIO_TBS_GTBS_INDEX,
|
||||
uri);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to originate TBS call, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ccp_terminate_call(void)
|
||||
{
|
||||
uint16_t conn_handle;
|
||||
int err;
|
||||
|
||||
conn_handle = default_conn_handle_get();
|
||||
if (conn_handle == CONN_HANDLE_INIT) {
|
||||
ESP_LOGE(TAG, "%s, not connected", __func__);
|
||||
return -ENOTCONN;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_tbs_client_terminate_call(conn_handle,
|
||||
ESP_BLE_AUDIO_TBS_GTBS_INDEX,
|
||||
new_call_index);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to terminate TBS call, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2022 Codecoup
|
||||
* SPDX-FileCopyrightText: 2023 NXP
|
||||
* SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "host/ble_hs.h"
|
||||
|
||||
#include "tmap_peripheral.h"
|
||||
|
||||
#define CSIP_SIRK_DEBUG { 0xcd, 0xcc, 0x72, 0xdd, 0x86, 0x8c, 0xcd, 0xce, \
|
||||
0x22, 0xfd, 0xa1, 0x21, 0x09, 0x7d, 0x7d, 0x45 }
|
||||
|
||||
static esp_ble_audio_csip_set_member_svc_inst_t *svc_inst;
|
||||
|
||||
static void csip_lock_changed_cb(esp_ble_conn_t *conn,
|
||||
esp_ble_audio_csip_set_member_svc_inst_t *inst,
|
||||
bool locked)
|
||||
{
|
||||
ESP_LOGI(TAG, "Client %p %s the lock", conn, locked ? "locked" : "released");
|
||||
}
|
||||
|
||||
static uint8_t sirk_read_req_cb(esp_ble_conn_t *conn,
|
||||
esp_ble_audio_csip_set_member_svc_inst_t *inst)
|
||||
{
|
||||
return ESP_BLE_AUDIO_CSIP_READ_SIRK_REQ_RSP_ACCEPT;
|
||||
}
|
||||
|
||||
static esp_ble_audio_csip_set_member_cb_t csip_cb = {
|
||||
.lock_changed = csip_lock_changed_cb,
|
||||
.sirk_read_req = sirk_read_req_cb,
|
||||
};
|
||||
|
||||
int csip_set_member_init(void)
|
||||
{
|
||||
esp_ble_audio_csip_set_member_register_param_t param = {
|
||||
.set_size = 2,
|
||||
.rank = CONFIG_EXAMPLE_TMAP_PER_SET_RANK,
|
||||
.lockable = false,
|
||||
.sirk = CSIP_SIRK_DEBUG,
|
||||
.cb = &csip_cb,
|
||||
};
|
||||
int err;
|
||||
|
||||
err = esp_ble_audio_cap_acceptor_register(¶m, &svc_inst);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register cap acceptor, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "CSIP set member initialized");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int csip_generate_rsi(uint8_t rsi[ESP_BLE_AUDIO_CSIP_RSI_SIZE])
|
||||
{
|
||||
char rsi_str[13];
|
||||
int err;
|
||||
|
||||
if (svc_inst == NULL) {
|
||||
ESP_LOGE(TAG, "CSIS service instant not exists");
|
||||
return -1;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_csip_set_member_generate_rsi(svc_inst, rsi);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to generate RSI, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
snprintf(rsi_str, ARRAY_SIZE(rsi_str), "%02x%02x%02x%02x%02x%02x",
|
||||
rsi[0], rsi[1], rsi[2], rsi[3], rsi[4], rsi[5]);
|
||||
|
||||
ESP_LOGI(TAG, "PRSI: 0x%s", rsi_str);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void *csip_svc_inst_get(void)
|
||||
{
|
||||
return svc_inst;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user