Merge branch 'update/ble_iso_lea' into 'master'

fix(ble_audio): Miscellaneous fixes for ISO & LE Audio examples

See merge request espressif/esp-idf!47759
This commit is contained in:
Liu Linyan
2026-05-06 16:14:53 +08:00
108 changed files with 3051 additions and 1269 deletions
@@ -5,68 +5,91 @@
(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.
## Overview
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.
This example acts as a **BAP Broadcast Sink**. It scans for extended advertisements that carry the Broadcast Audio Announcement Service UUID and whose complete-name AD matches the hard-coded `"BAP Broadcast Source"` string; on a hit, it creates a periodic-advertising sync, builds a BAP broadcast sink for that PA handle and broadcast ID, decodes the BASE / BIGInfo from the PA channel, and then synchronizes to the BIG to receive BIS streams.
The build runs on top of the NimBLE host stack and the ESP BLE Audio component set. Sink-side APIs used: `esp_ble_audio_common_init` / `_start`, `esp_ble_audio_pacs_register` + `esp_ble_audio_pacs_cap_register` (sink PAC and sink location enabled), `esp_ble_audio_bap_scan_delegator_register` (so a Broadcast Assistant can drive PA-sync, broadcast-code, and BIS-sync requests via BASS), `esp_ble_audio_bap_broadcast_sink_register_cb`, `esp_ble_audio_bap_broadcast_sink_create` / `_sync` / `_stop` / `_delete`, and `esp_ble_audio_bap_base_get_subgroup_count` / `_get_bis_indexes`. PAC capabilities are LC3 with sample rates 16 kHz + 24 kHz, frame duration 10 ms, 1 channel, 40–60 octets/frame, 1 frame/SDU. The fallback broadcast code is `"1234"`; if a Broadcast Assistant has supplied one through BASS it is used instead.
After PA sync is established, `ble_gap_disc_cancel()` stops the extended scanner — BASE/BIGInfo arrive over the PA channel — and `pa_sync_lost()` re-arms the scanner.
## 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
* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31)
* A peer running the [broadcast_source](../broadcast_source) example (or another BAP Broadcast Source advertising the matching name)
## How to Use Example
## Configuration
Before project configuration and build, set the correct chip target:
```bash
idf.py set-target esp32h4
```
### Configure the Project
Open the project configuration menu:
Open menuconfig:
```bash
idf.py menuconfig
```
In the **Example: Broadcast Sink** menu:
No build-time options — runtime defaults are baked into source. The example always runs both: it scans for `"BAP Broadcast Source"` directly and registers a BASS scan delegator so a Broadcast Assistant could also drive sync (PAST is rejected — see `pa_sync_req_cb`).
* **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.
### Security & Pairing
### Build and Flash
The example inherits a Just-Works pairing model (LE Secure Connections, no MITM, no I/O capability) with bonding enabled from the shared init at `../common_components/example_init/ble_audio_example_init.c`; change pairing/IO-cap there if needed.
Run the following to build, flash and monitor:
## Build & Flash
```bash
idf.py set-target esp32h4 # or esp32s31
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.
(Exit serial monitor with `Ctrl-]`.)
## 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.
1. `app_main` initializes NVS, calls `bluetooth_init()`, and calls `esp_ble_audio_common_init(&info)` with `info.gap_cb = iso_gap_app_cb`.
2. PACS is registered (`snk_pac` + `snk_loc`), each stream's `ops` field is wired to `stream_ops`, and the LC3 sink capability is registered via `esp_ble_audio_pacs_cap_register(ESP_BLE_AUDIO_DIR_SINK, ...)`.
3. The scan delegator (`scan_delegator_cbs`: `recv_state_updated`, `pa_sync_req`, `pa_sync_term_req`, `broadcast_code`, `bis_sync_req`) and broadcast-sink callbacks (`base_recv`, `syncable`) are registered, then `esp_ble_audio_common_start(NULL)` runs.
4. `ext_scan_start()` runs passive extended discovery (`itvl=window=160`). For each `ESP_BLE_AUDIO_GAP_EVENT_EXT_SCAN_RECV`, `data_cb` matches the complete/short/broadcast name AD type against `"BAP Broadcast Source"` and records the Broadcast ID from the Broadcast Audio Service Data.
5. On match (and only when not already PA-syncing and no scan-delegator state is pinned), `pa_sync_create()` calls `ble_gap_periodic_adv_sync_create` with `skip=0`, `sync_timeout=10s`.
6. `ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC` clears `pa_syncing`, cancels discovery, stores `sync_handle`, and calls `esp_ble_audio_bap_broadcast_sink_create()`.
7. `base_recv_cb` extracts the subgroup count and BIS index bitfield (masked by `bis_index_mask`); when no Broadcast Assistant is connected, `requested_bis_sync` defaults to `ESP_BLE_AUDIO_BAP_BIS_SYNC_NO_PREF`.
8. `syncable_cb` AND-masks the BASE bitfield with the requested mask, copies `TARGET_BROADCAST_CODE` if the BIG is encrypted (unless BASS already supplied one), and calls `esp_ble_audio_bap_broadcast_sink_sync()` with the chosen mask and `streams_p`.
9. `stream_started_cb` resets per-stream RX metrics and increments `stream_count_started`; `stream_recv_cb` updates metrics via `example_audio_rx_metrics_on_recv()`. When all streams have stopped, `stream_stopped_cb` deletes the broadcast sink. `pa_sync_lost()` clears the cached `req_recv_state`, deletes any sink, and restarts the scanner.
## Example Output
## Expected Log
```
I (xxx) BAP_BSNK: Scanning for broadcast source...
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...)
...
I (xxx) BAP_BSNK: BASE received (... subgroup(s))
I (xxx) BAP_BSNK: bis_index_bitfield = 0x...
I (xxx) BAP_BSNK: Broadcast sink syncable, BIG encrypted
I (xxx) BAP_BSNK: Syncing to broadcast: BIS mask 0x... (... stream(s))
I (xxx) BAP_BSNK: [SNK #0] Stream started (.../...)
```
If PA sync is lost:
Per-stream RX metrics are then emitted under the `BAP_BSNK` tag, name `SNK #<idx>`. If a Broadcast Assistant connects via BASS, additional log lines appear:
```
I (xxx) BAP_BSNK: PA sync lost, reason ...
I (xxx) BAP_BSNK: Receive state updated, pa_sync 0x... encrypt 0x...
I (xxx) BAP_BSNK: Received request to sync to PA (PAST {not }available): ...
I (xxx) BAP_BSNK: Broadcast code received
I (xxx) BAP_BSNK: BIS sync req: broadcast_id 0x... BIS mask 0x... subgroup mask 0x... (...)
I (xxx) BAP_BSNK: Received request to terminate PA sync
```
On teardown / sync loss:
```
I (xxx) BAP_BSNK: [SNK #0] Stream stopped, reason 0x... (.../...)
I (xxx) BAP_BSNK: PA sync lost: sync_handle ... reason 0x...
I (xxx) BAP_BSNK: PA sync terminated
```
## Peer Pairing
Run [broadcast_source](../broadcast_source/) on a second board. Expected interaction sequence:
1. Source advertises extended PDU containing the Broadcast Audio Announcement Service UUID, Broadcast ID `0x123456`, and complete name `"BAP Broadcast Source"`; sink scans and matches by name + UUID.
2. Source's periodic advertising carries the encoded BASE; sink establishes PA sync and parses the BASE to recover subgroup count and BIS index bitfield.
3. Source's BIGInfo advertises the BIG as encrypted (broadcast code `"1234"`); sink reports `BIG encrypted`.
4. Source starts the BIG and the two BIS streams (`FRONT_LEFT`, `FRONT_RIGHT`); sink calls `esp_ble_audio_bap_broadcast_sink_sync()` with the chosen BIS bitfield and the matching broadcast code.
5. Source's TX scheduler keeps pushing SDUs at `preset_active.qos.interval`; sink stream `recv` callbacks deliver the data and update RX metrics.
6. Stopping the source (or losing PA sync) triggers `stream_stopped_cb` on the sink, which deletes the broadcast sink and resumes scanning.
@@ -1,5 +1,5 @@
# SPDX-FileCopyrightText: 2022 Nordic Semiconductor ASA
# SPDX-FileContributor: 2025 Espressif Systems (Shanghai) CO LTD
# SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
menu "Example: Broadcast Sink"
@@ -113,7 +113,7 @@ static void ext_scan_start(void)
return;
}
ESP_LOGI(TAG, "Extended scan started");
ESP_LOGI(TAG, "Scanning for broadcast source...");
}
static int pa_sync_create(const bt_addr_le_t *addr, uint8_t adv_sid)
@@ -213,7 +213,7 @@ 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);
ESP_LOGI(TAG, "Broadcast code received");
req_recv_state = recv_state;
@@ -253,10 +253,10 @@ static int bis_sync_req_cb(esp_ble_conn_t *conn,
}
}
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");
ESP_LOGI(TAG, "BIS sync req: broadcast_id 0x%06x BIS mask 0x%08x subgroup mask 0x%08x (%s)",
recv_state->broadcast_id, requested_bis_sync,
requested_subgroup_sync,
stream_started ? "streaming" : "not streaming");
if (stream_started && requested_bis_sync == 0) {
/* The stream stopped callback will be called as part of this, and
@@ -307,8 +307,7 @@ static void base_recv_cb(esp_ble_audio_bap_broadcast_sink_t *sink,
return;
}
ESP_LOGI(TAG, "Received BASE with %d subgroups from broadcast sink %p",
base_subgroup_count, sink);
ESP_LOGI(TAG, "BASE received (%d subgroup(s))", base_subgroup_count);
err = esp_ble_audio_bap_base_get_bis_indexes(base, &base_bis_index_bitfield);
if (err) {
@@ -334,8 +333,8 @@ static void syncable_cb(esp_ble_audio_bap_broadcast_sink_t *sink,
uint32_t sync_bitfield;
esp_err_t err;
ESP_LOGI(TAG, "Broadcast sink (%p) is syncable, BIG %s",
sink, biginfo->encryption ? "encrypted" : "not encrypted");
ESP_LOGI(TAG, "Broadcast sink syncable, BIG %s",
biginfo->encryption ? "encrypted" : "not encrypted");
sync_bitfield = (bis_index_bitfield & requested_bis_sync);
if (sync_bitfield == 0) {
@@ -353,9 +352,8 @@ static void syncable_cb(esp_ble_audio_bap_broadcast_sink_t *sink,
}
}
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);
ESP_LOGI(TAG, "Syncing to broadcast: BIS mask 0x%08x (%u stream(s))",
sync_bitfield, stream_count);
if (biginfo->encryption) {
memset(sink_broadcast_code, 0, ESP_BLE_ISO_BROADCAST_CODE_SIZE);
@@ -376,13 +374,24 @@ static esp_ble_audio_bap_broadcast_sink_cb_t broadcast_sink_cbs = {
.syncable = syncable_cb,
};
static int stream_index(const esp_ble_audio_bap_stream_t *stream)
{
for (size_t i = 0; i < ARRAY_SIZE(streams); i++) {
if (&streams[i].stream == stream) {
return (int)i;
}
}
return -1;
}
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);
ESP_LOGI(TAG, "[SNK #%d] Stream started (%u/%u)",
stream_index(stream), stream_count_started, stream_count);
example_audio_rx_metrics_reset(&sink_stream->rx_metrics);
@@ -395,8 +404,8 @@ 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);
ESP_LOGI(TAG, "[SNK #%d] Stream stopped, reason 0x%02x (%u/%u)",
stream_index(stream), reason, stream_count_stopped, stream_count);
if (++stream_count_stopped == stream_count) {
stream_started = false;
@@ -418,10 +427,11 @@ static void stream_recv_cb(esp_ble_audio_bap_stream_t *stream,
struct broadcast_sink_stream *sink_stream = CONTAINER_OF(stream,
struct broadcast_sink_stream,
stream);
char name[24];
snprintf(name, sizeof(name), "SNK #%d", stream_index(stream));
sink_stream->rx_metrics.last_sdu_len = len;
example_audio_rx_metrics_on_recv(info, &sink_stream->rx_metrics,
TAG, "stream", stream);
example_audio_rx_metrics_on_recv(info, &sink_stream->rx_metrics, TAG, name);
}
static esp_ble_audio_bap_stream_ops_t stream_ops = {
@@ -506,6 +516,7 @@ static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event)
static void pa_sync(esp_ble_audio_gap_app_event_t *event)
{
esp_err_t err;
int rc;
pa_syncing = false;
@@ -518,6 +529,15 @@ static void pa_sync(esp_ble_audio_gap_app_event_t *event)
ESP_LOGI(TAG, "Broadcast source PA synced, creating Broadcast Sink");
/* PA sync is established; the BASE / BIGInfo reports will arrive
* via the PA sync channel, so the extended scanner is no longer
* needed. Stop it now — pa_sync_lost() will restart it on loss.
*/
rc = ble_gap_disc_cancel();
if (rc) {
ESP_LOGW(TAG, "Failed to stop scanning, err %d", rc);
}
err = esp_ble_audio_bap_broadcast_sink_create(event->pa_sync.sync_handle,
broadcaster_broadcast_id,
&broadcast_sink);
@@ -529,7 +549,7 @@ static void pa_sync(esp_ble_audio_gap_app_event_t *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",
ESP_LOGI(TAG, "PA sync lost: sync_handle %u reason 0x%02x",
event->pa_sync_lost.sync_handle, event->pa_sync_lost.reason);
if (sync_handle == event->pa_sync_lost.sync_handle) {
@@ -540,6 +560,12 @@ static void pa_sync_lost(esp_ble_audio_gap_app_event_t *event)
stream_count = 0;
stream_count_started = 0;
stream_count_stopped = 0;
/* Clear the stale scan-delegator state pointer captured by
* recv_state_updated_cb() while we were synced. Without this,
* ext_scan_recv() would keep rejecting new source ads because
* its gate is `pa_syncing == false && req_recv_state == NULL`.
*/
req_recv_state = NULL;
if (broadcast_sink != NULL) {
esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink);
@@ -6,6 +6,8 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=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
@@ -5,57 +5,77 @@
(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.
## Overview
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.
This example acts as a **BAP Broadcast Source**. It creates a BAP broadcast source from the LC3 `16_2_1` broadcast preset, encodes the BASE into the periodic advertising payload, places the Broadcast Audio Announcement Service UUID and a fixed 24-bit Broadcast ID (`0x123456`) in the extended advertising payload, and then starts the BIG so that BIS streams transmit synthetic SDU data on a fixed cadence.
The build runs on top of the NimBLE host stack and the ESP BLE Audio component set. Source-side APIs used: `esp_ble_audio_common_init` / `esp_ble_audio_common_start` (common layer), `esp_ble_audio_bap_broadcast_source_create` / `_get_base` / `_start` (BAP), `esp_ble_audio_bap_broadcast_adv_add` (BAP/ISO glue), and the BAP stream callbacks (`started`, `stopped`, `sent`, `disconnected`). PACS, GAP scanner, and the scan delegator are **not** used on this side. Encryption is enabled because the broadcast code `"1234"` is non-empty; packing is `ESP_BLE_ISO_PACKING_SEQUENTIAL`. Channel allocation per stream is hard-coded as `FRONT_LEFT` for stream 0 and `FRONT_RIGHT` for stream 1.
The TX scheduler is built on `example_audio_tx_scheduler_*` helpers; the source comment notes that ESP timer resolution is not accurate enough for the SDU interval, so a `k_work_delayable`-based scheduler is used instead.
## 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
* A board with Bluetooth LE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31)
* A peer running the [broadcast_sink](../broadcast_sink) example to receive and decode the BIS streams
## How to Use Example
## Configuration
Before project configuration and build, set the correct chip target:
Open menuconfig:
```bash
idf.py set-target esp32h4
idf.py menuconfig
```
### Build and Flash
No build-time options — runtime defaults are baked into source. The device name (`"BAP Broadcast Source"`), broadcast ID (`0x123456`), broadcast code (`"1234"`), advertising/periodic intervals, stream and subgroup counts (via `CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT` / `_SUBGROUP_COUNT` from the BAP component), and LC3 preset (`16_2_1`) are compile-time constants in `main.c`.
Run the following to build, flash and monitor:
### Security & Pairing
The example inherits a Just-Works pairing model (LE Secure Connections, no MITM, no I/O capability) with bonding enabled from the shared init at `../common_components/example_init/ble_audio_example_init.c`; change pairing/IO-cap there if needed.
## Build & Flash
```bash
idf.py set-target esp32h4 # or esp32s31
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.
(Exit serial monitor with `Ctrl-]`.)
## 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.
1. `app_main` initializes NVS, calls `bluetooth_init()`, and calls `esp_ble_audio_common_init(NULL)` (no GAP callback for this role).
2. `broadcast_source_setup()` registers `source_started` / `source_stopped` callbacks, populates per-stream channel-allocation BIS metadata (left/right), registers `stream_ops` on each stream, and calls `esp_ble_audio_bap_broadcast_source_create()` with the encrypted preset.
3. `esp_ble_audio_common_start(NULL)` starts the audio stack; each stream's `example_audio_tx_scheduler_init()` is wired with `tx_scheduler_cb`.
4. `ext_adv_start()` configures non-connectable extended adv (1M primary / 2M secondary, 200 ms interval), writes Broadcast Audio Service Data + Broadcast ID + complete name, configures periodic adv (100 ms), writes the BASE returned by `esp_ble_audio_bap_broadcast_source_get_base()`, then starts periodic and extended advertising.
5. `broadcast_start()` calls `esp_ble_audio_bap_broadcast_adv_add()` and `esp_ble_audio_bap_broadcast_source_start()` on the same `ADV_HANDLE`, which kicks off BIGInfo + BIS creation.
6. When each BIS stream goes streaming, `stream_started_cb` allocates an SDU-sized buffer and calls `example_audio_tx_scheduler_start()` at `preset_active.qos.interval`; the scheduler invokes `broadcast_source_tx()`, which fills the buffer with the low byte of `seq_num` and calls `esp_ble_audio_bap_stream_send()`.
7. `stream_sent_cb` forwards completions to `example_audio_tx_scheduler_on_sent()` for drift accounting; `stream_stopped_cb` and `stream_disconnected_cb` stop the scheduler; `source_stopped_cb` frees all per-stream buffers.
## Example Output
## Expected Log
```
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...)
...
I (xxx) BAP_BSRC: Creating broadcast source: ... subgroup(s), ... stream(s)/subgroup
I (xxx) BAP_BSRC: Advertising started (handle 0)
I (xxx) BAP_BSRC: Broadcast source started
I (xxx) BAP_BSRC: [SRC #0] Stream started
I (xxx) BAP_BSRC: [SRC #1] Stream started
```
If the broadcast source stops (e.g. stream stopped):
Periodic TX accounting (emitted by `example_audio_tx_scheduler_on_sent` under the `BAP_BSRC` tag, name `SRC #<idx>`) follows. On teardown:
```
I (xxx) BAP_BSRC: Stream 0x... stopped, reason 0x...
I (xxx) BAP_BSRC: Broadcast source 0x... stopped, reason 0x...
I (xxx) BAP_BSRC: [SRC #0] Stream stopped, reason 0x...
I (xxx) BAP_BSRC: [SRC #0] ISO disconnected, reason 0x...
I (xxx) BAP_BSRC: Broadcast source stopped, reason 0x...
```
## Peer Pairing
Run [broadcast_sink](../broadcast_sink/) on a second board. Expected interaction sequence:
1. Source advertises extended PDU containing the Broadcast Audio Announcement Service UUID, Broadcast ID `0x123456`, and complete name `"BAP Broadcast Source"`; sink scans and matches by name + UUID.
2. Source's periodic advertising carries the encoded BASE; sink establishes PA sync and parses the BASE to recover subgroup count and BIS index bitfield.
3. Source's BIGInfo advertises the BIG as encrypted (broadcast code `"1234"`); sink reports `BIG encrypted`.
4. Source starts the BIG and the two BIS streams (`FRONT_LEFT`, `FRONT_RIGHT`); sink calls `esp_ble_audio_bap_broadcast_sink_sync()` with the chosen BIS bitfield and the matching broadcast code.
5. Source's TX scheduler keeps pushing SDUs at `preset_active.qos.interval`; sink stream `recv` callbacks deliver the data and update RX metrics.
6. Stopping the source (or losing PA sync) triggers `stream_stopped_cb` on the sink, which deletes the broadcast sink and resumes scanning.
@@ -66,24 +66,36 @@ static esp_ble_audio_bap_broadcast_source_t *broadcast_source;
static void broadcast_source_tx(struct broadcast_source_stream *source_stream);
static int stream_index(const esp_ble_audio_bap_stream_t *stream)
{
for (size_t i = 0; i < ARRAY_SIZE(streams); i++) {
if (&streams[i].stream == stream) {
return (int)i;
}
}
return -1;
}
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);
int idx = stream_index(stream);
esp_err_t err;
ESP_LOGI(TAG, "Stream %p started", stream);
ESP_LOGI(TAG, "[SRC #%d] Stream started", idx);
if (source_stream->stream.qos == NULL || source_stream->stream.qos->sdu == 0) {
ESP_LOGE(TAG, "Invalid stream qos");
ESP_LOGE(TAG, "[SRC #%d] Invalid QoS", idx);
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);
ESP_LOGE(TAG, "[SRC #%d] Failed to alloc TX buffer (sdu %u)",
idx, source_stream->stream.qos->sdu);
return;
}
}
@@ -94,7 +106,7 @@ static void stream_started_cb(esp_ble_audio_bap_stream_t *stream)
/* 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);
ESP_LOGE(TAG, "[SRC #%d] Scheduler start failed, err %d", idx, err);
return;
}
@@ -106,13 +118,14 @@ 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);
int idx = stream_index(stream);
esp_err_t err;
ESP_LOGI(TAG, "Stream %p stopped, reason 0x%02x", stream, reason);
ESP_LOGI(TAG, "[SRC #%d] Stream stopped, reason 0x%02x", idx, reason);
err = example_audio_tx_scheduler_stop(&source_stream->scheduler);
if (err) {
ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err);
ESP_LOGE(TAG, "[SRC #%d] Scheduler stop failed, err %d", idx, err);
}
}
@@ -121,13 +134,14 @@ static void stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, uint8_t r
struct broadcast_source_stream *source_stream = CONTAINER_OF(stream,
struct broadcast_source_stream,
stream);
int idx = stream_index(stream);
esp_err_t err;
ESP_LOGI(TAG, "Stream %p disconnected, reason 0x%02x", stream, reason);
ESP_LOGI(TAG, "[SRC #%d] ISO disconnected, reason 0x%02x", idx, reason);
err = example_audio_tx_scheduler_stop(&source_stream->scheduler);
if (err) {
ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err);
ESP_LOGE(TAG, "[SRC #%d] Scheduler stop failed, err %d", idx, err);
}
}
@@ -136,8 +150,10 @@ 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);
char name[24];
example_audio_tx_scheduler_on_sent(&source_stream->scheduler, user_data, TAG, "stream", stream);
snprintf(name, sizeof(name), "SRC #%d", stream_index(stream));
example_audio_tx_scheduler_on_sent(&source_stream->scheduler, user_data, TAG, name);
}
static esp_ble_audio_bap_stream_ops_t stream_ops = {
@@ -170,13 +186,16 @@ static void broadcast_source_tx(struct broadcast_source_stream *source_stream)
return;
}
int idx = stream_index(&source_stream->stream);
if (source_stream->stream.qos == NULL || source_stream->stream.qos->sdu == 0) {
ESP_LOGE(TAG, "Invalid stream qos");
ESP_LOGE(TAG, "[SRC #%d] Invalid QoS", idx);
return;
}
if (source_stream->data == NULL) {
ESP_LOGE(TAG, "Tx buffer unavailable, sdu %u", source_stream->stream.qos->sdu);
ESP_LOGE(TAG, "[SRC #%d] TX buffer unavailable (sdu %u)",
idx, source_stream->stream.qos->sdu);
return;
}
@@ -187,8 +206,7 @@ static void broadcast_source_tx(struct broadcast_source_stream *source_stream)
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);
ESP_LOGD(TAG, "[SRC #%d] send failed, err %d", idx, err);
return;
}
@@ -208,12 +226,12 @@ static void tx_scheduler_cb(void *arg)
static void source_started_cb(esp_ble_audio_bap_broadcast_source_t *source)
{
ESP_LOGI(TAG, "Broadcast source %p started", source);
ESP_LOGI(TAG, "Broadcast source started");
}
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);
ESP_LOGI(TAG, "Broadcast source stopped, reason 0x%02x", reason);
for (size_t i = 0; i < ARRAY_SIZE(streams); i++) {
if (streams[i].data != NULL) {
@@ -275,7 +293,7 @@ static esp_err_t broadcast_source_setup(void)
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",
ESP_LOGI(TAG, "Creating broadcast source: %u subgroup(s), %u stream(s)/subgroup",
ARRAY_SIZE(subgroup_param), streams_per_subgroup);
err = esp_ble_audio_bap_broadcast_source_create(&create_param, &broadcast_source);
@@ -453,7 +471,7 @@ static int ext_adv_start(void)
goto end;
}
ESP_LOGI(TAG, "Extended adv instance %u started", ADV_HANDLE);
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
end:
if (ext_data) {
@@ -525,7 +543,7 @@ void app_main(void)
tx_scheduler_cb,
&streams[i]);
if (err) {
ESP_LOGE(TAG, "Failed to initialize tx scheduler[%u], err %d", i, err);
ESP_LOGE(TAG, "[SRC #%zu] Scheduler init failed, err %d", i, err);
return;
}
}
@@ -5,60 +5,119 @@
(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.
## Overview
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.
This example implements a **BAP Unicast Client** on top of the NimBLE host stack. It scans for a peer advertising the ASCS service UUID with a complete local name of `BAP Unicast Server`, opens an ACL connection, initiates pairing, exchanges MTU, and starts GATT service discovery.
Once the peer is reachable, the client uses the BAP unicast client API (`esp_ble_audio_bap_unicast_client_discover`, `esp_ble_audio_bap_stream_config`, `esp_ble_audio_bap_unicast_group_create`, `esp_ble_audio_bap_stream_qos`, `esp_ble_audio_bap_stream_enable`, `esp_ble_audio_bap_stream_connect`, `esp_ble_audio_bap_stream_start`) to discover sink and source ASEs, configure each with the LC3 `16_2_1` unicast preset, build a unicast group, set QoS, enable, and ISO-connect the streams. Source (TX-from-server) streams are started by this client; sink (TX-to-server) streams are started by the server.
The TX path in `stream_tx.c` registers each newly started sink stream with `example_audio_tx_scheduler` and feeds it SDU-sized buffers driven by `esp_ble_audio_bap_stream_send`. The PACS callbacks (`pac_record`, `endpoint`, `location`, `available_contexts`) print the discovered peer capabilities.
## 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
* A board with BLE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31)
* Peer device running the paired example
## How to Use Example
## Configuration
Before project configuration and build, set the correct chip target:
Open menuconfig:
```bash
idf.py menuconfig
```
No build-time options — runtime defaults are baked into source.
### Security & Pairing
This example inherits a Just-Works pairing model (LE Secure Connections, no MITM) with bonding enabled from the shared init at `../common_components/example_init/ble_audio_example_init.c`.
## Build & Flash
```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.
(Exit serial monitor with `Ctrl-]`.)
## 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.
1. `app_main` initializes NVS, NimBLE, and the LE Audio common layer, registers stream ops on every sink/source slot, registers the BAP unicast client callbacks, sets the device name `BAP Unicast Client`, and calls `ext_scan_start`.
2. Passive extended scanning runs forever; `scan_data_cb` matches the complete local name `BAP Unicast Server` and parses the ASCS service-data AD for announcement type plus a 32-bit available-contexts field.
3. On a connectable match `conn_create` cancels scanning and issues `ble_gap_connect`; the resulting `ESP_BLE_AUDIO_GAP_EVENT_ACL_CONNECT` saves the handle and calls `ble_gap_security_initiate`.
4. After the security-change event the client triggers `ble_gattc_exchange_mtu`; `gatt_mtu_change` then calls `esp_ble_audio_gattc_disc_start`, and once both MTU and discovery are reported `discover_sinks` runs.
5. `discover_cb` chains sink discovery into `discover_sources`; `endpoint_cb` records each EP into the next free `sinks[]`/`sources[]` slot, and source completion kicks `configure_stream`, which configures every EP with `unicast_preset.codec_cfg`.
6. After the last `config_cb`, `create_group` builds `pair_params` (sinks first, then sources) with `ESP_BLE_ISO_PACKING_SEQUENTIAL` and calls `esp_ble_audio_bap_unicast_group_create`, then `set_stream_qos`.
7. When `is_all_stream_qos_set` returns true `enable_stream` walks every stream; on the final `enable_cb`, `connect_stream` ISO-connects each pair (sink first if available); when all are up `start_stream` starts only source streams. Sink streams are started by the server, and `stream_started_cb` calls `stream_tx_register` to begin TX.
## Example Output
## Expected Log
TAG: `BAP_UCL`.
Startup and scanning:
```
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
...
I (xxx) BAP_UCL: Scanning for unicast server...
I (xxx) BAP_UCL: Unicast server found: type ... contexts 0x...
```
If connection or discovery fails, relevant error messages are logged.
ACL, security, MTU, discovery:
```
I (xxx) BAP_UCL: Connected: handle ... role ... peer ...
I (xxx) BAP_UCL: Security: handle ... level ... bonded ...
I (xxx) BAP_UCL: MTU updated: handle ... mtu ...
I (xxx) BAP_UCL: Service discovery started: handle ...
I (xxx) BAP_UCL: Service discovery complete: handle ...
```
ASE discovery:
```
I (xxx) BAP_UCL: [SNK] Discovering...
I (xxx) BAP_UCL: [SNK #...] Endpoint discovered
I (xxx) BAP_UCL: [SNK] Discovery complete
I (xxx) BAP_UCL: [SRC] Discovering...
I (xxx) BAP_UCL: [SRC #...] Endpoint discovered
I (xxx) BAP_UCL: [SRC] Discovery complete
```
Config / group / QoS / enable / connect / start:
```
I (xxx) BAP_UCL: [SNK #...] Config ACK'd
I (xxx) BAP_UCL: [SNK #...] Stream configured, QoS preference:
I (xxx) BAP_UCL: [SNK #...] Configured
I (xxx) BAP_UCL: Pair[...]: SNK=... SRC=...
I (xxx) BAP_UCL: Unicast group created (... pair(s))
I (xxx) BAP_UCL: [SNK #...] QoS ACK'd
I (xxx) BAP_UCL: [SNK #...] QoS set
I (xxx) BAP_UCL: [SNK #...] Enable ACK'd
I (xxx) BAP_UCL: [SNK #...] Enabled
I (xxx) BAP_UCL: Pair[...] connecting (SNK first)
I (xxx) BAP_UCL: [SNK #...] ISO connected (pair[...])
I (xxx) BAP_UCL: [SNK #...] Connected
I (xxx) BAP_UCL: [SRC] Starting all source streams
I (xxx) BAP_UCL: [SRC #...] Stream started
I (xxx) BAP_UCL: [SNK #...] Stream started
I (xxx) BAP_UCL: [SNK #...] Started (SDU ..., interval ... us)
```
Disconnect:
```
I (xxx) BAP_UCL: [SNK #...] ISO disconnected, reason 0x...
I (xxx) BAP_UCL: Disconnected: handle ... reason 0x...
```
## Peer Pairing
Run [unicast_server](../unicast_server/) on a second board.
1. Server boots and starts connectable extended advertising with the ASCS UUID and `BAP Unicast Server` name.
2. Client boots and scans; on matching the name and ASCS service data it cancels scanning and issues `ble_gap_connect`.
3. After ACL is up the client initiates pairing/security; both sides log `Connected:` and the client logs `Security:`.
4. Client exchanges MTU and runs GATT discovery, then discovers sink ASEs followed by source ASEs on the server.
5. Client drives ASCS Config -> QoS -> Enable -> ISO Connect on every ASE; the server returns its QoS preference and accepts each operation.
6. Client starts source streams; server auto-starts each sink stream from `stream_enabled_cb`, after which the client's TX scheduler feeds SDUs and the server's `stream_recv_cb` accumulates RX metrics.
@@ -8,6 +8,7 @@
#include <stdint.h>
#include <stdbool.h>
#include <errno.h>
#include <string.h>
#include "nvs_flash.h"
#include "esp_system.h"
@@ -168,6 +169,11 @@ static void reset_stream_state(void)
reset_stream_pair_state();
for (size_t i = 0; i < ARRAY_SIZE(sinks); i++) {
esp_ble_audio_bap_stream_ops_t *ops = sinks[i].stream.ops;
memset(&sinks[i].stream, 0, sizeof(sinks[i].stream));
sinks[i].stream.ops = ops;
sinks[i].configured = ASCS_RSP_NONE;
sinks[i].qos_set = ASCS_RSP_NONE;
sinks[i].enabled = ASCS_RSP_NONE;
@@ -176,6 +182,11 @@ static void reset_stream_state(void)
}
for (size_t i = 0; i < ARRAY_SIZE(sources); i++) {
esp_ble_audio_bap_stream_ops_t *ops = sources[i].stream.ops;
memset(&sources[i].stream, 0, sizeof(sources[i].stream));
sources[i].stream.ops = ops;
sources[i].configured = ASCS_RSP_NONE;
sources[i].qos_set = ASCS_RSP_NONE;
sources[i].enabled = ASCS_RSP_NONE;
@@ -199,17 +210,42 @@ static bool stream_is_sink(const esp_ble_audio_bap_stream_t *stream)
return false;
}
static const char *dir_str(esp_ble_audio_dir_t dir)
{
return dir == ESP_BLE_AUDIO_DIR_SINK ? "SNK" : "SRC";
}
static const char *stream_dir_str(const esp_ble_audio_bap_stream_t *stream)
{
return stream_is_sink(stream) ? "SNK" : "SRC";
}
static int stream_index(const esp_ble_audio_bap_stream_t *stream)
{
for (size_t i = 0; i < ARRAY_SIZE(sinks); i++) {
if (&sinks[i].stream == stream) {
return (int)i;
}
}
for (size_t i = 0; i < ARRAY_SIZE(sources); i++) {
if (&sources[i].stream == stream) {
return (int)i;
}
}
return -1;
}
static int discover_sinks(void)
{
int err;
err = esp_ble_audio_bap_unicast_client_discover(default_conn_handle, ESP_BLE_AUDIO_DIR_SINK);
if (err) {
ESP_LOGE(TAG, "Failed to discover sinks, err %d", err);
ESP_LOGE(TAG, "[SNK] Discover failed, err %d", err);
return err;
}
ESP_LOGI(TAG, "Discovering sinks");
ESP_LOGI(TAG, "[SNK] Discovering...");
return 0;
}
@@ -220,10 +256,12 @@ static int discover_sources(void)
err = esp_ble_audio_bap_unicast_client_discover(default_conn_handle, ESP_BLE_AUDIO_DIR_SOURCE);
if (err) {
ESP_LOGE(TAG, "Failed to discover sources, err %d", err);
ESP_LOGE(TAG, "[SRC] Discover failed, err %d", err);
return err;
}
ESP_LOGI(TAG, "[SRC] Discovering...");
return 0;
}
@@ -240,7 +278,7 @@ static bool configure_stream(void)
&sinks[i].stream, sinks[i].ep,
&unicast_preset.codec_cfg);
if (err) {
ESP_LOGE(TAG, "Failed to configure sink stream[%u], err %d", i, err);
ESP_LOGE(TAG, "[SNK #%zu] Config request failed, err %d", i, err);
sinks[i].configured = ASCS_RSP_FAILURE;
/* Try to configure the next sink stream */
@@ -259,7 +297,7 @@ static bool configure_stream(void)
&sources[i].stream, sources[i].ep,
&unicast_preset.codec_cfg);
if (err) {
ESP_LOGE(TAG, "Failed to configure source stream[%u], err %d", i, err);
ESP_LOGE(TAG, "[SRC #%zu] Config request failed, err %d", i, err);
sources[i].configured = ASCS_RSP_FAILURE;
/* Try to configure the next source stream */
@@ -278,11 +316,11 @@ static void stream_configured(esp_ble_audio_bap_stream_t *stream, bool success)
if (sinks[i].configured == ASCS_RSP_NONE &&
&sinks[i].stream == stream) {
if (success) {
ESP_LOGI(TAG, "Sink stream[%u] configure succeeded", i);
ESP_LOGI(TAG, "[SNK #%zu] Configured", i);
sinks[i].configured = ASCS_RSP_SUCCESS;
configured_sink_stream_count++;
} else {
ESP_LOGE(TAG, "Sink stream[%u] configure failed", i);
ESP_LOGE(TAG, "[SNK #%zu] Configure failed", i);
sinks[i].configured = ASCS_RSP_FAILURE;
}
return;
@@ -293,18 +331,18 @@ static void stream_configured(esp_ble_audio_bap_stream_t *stream, bool success)
if (sources[i].configured == ASCS_RSP_NONE &&
&sources[i].stream == stream) {
if (success) {
ESP_LOGI(TAG, "Source stream[%u] configure succeeded", i);
ESP_LOGI(TAG, "[SRC #%zu] Configured", i);
sources[i].configured = ASCS_RSP_SUCCESS;
configured_source_stream_count++;
} else {
ESP_LOGE(TAG, "Source stream[%u] configure failed", i);
ESP_LOGE(TAG, "[SRC #%zu] Configure failed", i);
sources[i].configured = ASCS_RSP_FAILURE;
}
return;
}
}
ESP_LOGW(TAG, "No matching stream found for configured stream %p", stream);
ESP_LOGW(TAG, "No matching stream for configured callback");
}
static int create_group(void)
@@ -335,8 +373,9 @@ static int create_group(void)
pair_params[i].rx_param = NULL;
}
ESP_LOGI(TAG, "Stream pair[%u]: sink %p source %p", i,
stream_pairs[i].sink_stream, stream_pairs[i].source_stream);
ESP_LOGI(TAG, "Pair[%zu]: SNK=%s SRC=%s", i,
stream_pairs[i].sink_stream ? "yes" : "-",
stream_pairs[i].source_stream ? "yes" : "-");
}
group_param.params = pair_params;
@@ -349,7 +388,7 @@ static int create_group(void)
return err;
}
ESP_LOGI(TAG, "Created unicast group");
ESP_LOGI(TAG, "Unicast group created (%zu pair(s))", params_count);
return 0;
}
@@ -374,10 +413,10 @@ static void stream_qos_set(esp_ble_audio_bap_stream_t *stream, bool success)
sinks[i].qos_set == ASCS_RSP_NONE &&
&sinks[i].stream == stream) {
if (success) {
ESP_LOGI(TAG, "Sink stream[%u] QoS set", i);
ESP_LOGI(TAG, "[SNK #%zu] QoS set", i);
sinks[i].qos_set = ASCS_RSP_SUCCESS;
} else {
ESP_LOGE(TAG, "Sink stream[%u] QoS set failed", i);
ESP_LOGE(TAG, "[SNK #%zu] QoS set failed", i);
sinks[i].qos_set = ASCS_RSP_FAILURE;
}
return;
@@ -389,17 +428,17 @@ static void stream_qos_set(esp_ble_audio_bap_stream_t *stream, bool success)
sources[i].qos_set == ASCS_RSP_NONE &&
&sources[i].stream == stream) {
if (success) {
ESP_LOGI(TAG, "Source stream[%u] QoS set", i);
ESP_LOGI(TAG, "[SRC #%zu] QoS set", i);
sources[i].qos_set = ASCS_RSP_SUCCESS;
} else {
ESP_LOGE(TAG, "Source stream[%u] QoS set failed", i);
ESP_LOGE(TAG, "[SRC #%zu] QoS set failed", i);
sources[i].qos_set = ASCS_RSP_FAILURE;
}
return;
}
}
ESP_LOGW(TAG, "No matching stream found for QoS set stream %p", stream);
ESP_LOGW(TAG, "No matching stream for QoS callback");
}
static bool is_all_stream_qos_set(void)
@@ -433,7 +472,7 @@ static bool enable_stream(void)
unicast_preset.codec_cfg.meta,
unicast_preset.codec_cfg.meta_len);
if (err) {
ESP_LOGE(TAG, "Failed to enable sink stream[%u], err %d", i, err);
ESP_LOGE(TAG, "[SNK #%zu] Enable request failed, err %d", i, err);
sinks[i].enabled = ASCS_RSP_FAILURE;
/* Try to enable the next sink stream */
@@ -452,7 +491,7 @@ static bool enable_stream(void)
unicast_preset.codec_cfg.meta,
unicast_preset.codec_cfg.meta_len);
if (err) {
ESP_LOGE(TAG, "Failed to enable source stream[%u], err %d", i, err);
ESP_LOGE(TAG, "[SRC #%zu] Enable request failed, err %d", i, err);
sources[i].enabled = ASCS_RSP_FAILURE;
/* Try to enable the next source stream */
@@ -474,10 +513,10 @@ static void stream_enabled(esp_ble_audio_bap_stream_t *stream, bool success)
sinks[i].enabled == ASCS_RSP_NONE &&
&sinks[i].stream == stream) {
if (success) {
ESP_LOGI(TAG, "Sink stream[%u] enabled", i);
ESP_LOGI(TAG, "[SNK #%zu] Enabled", i);
sinks[i].enabled = ASCS_RSP_SUCCESS;
} else {
ESP_LOGE(TAG, "Sink stream[%u] enabled failed", i);
ESP_LOGE(TAG, "[SNK #%zu] Enable failed", i);
sinks[i].enabled = ASCS_RSP_FAILURE;
}
return;
@@ -490,17 +529,17 @@ static void stream_enabled(esp_ble_audio_bap_stream_t *stream, bool success)
sources[i].enabled == ASCS_RSP_NONE &&
&sources[i].stream == stream) {
if (success) {
ESP_LOGI(TAG, "Source stream[%u] enabled", i);
ESP_LOGI(TAG, "[SRC #%zu] Enabled", i);
sources[i].enabled = ASCS_RSP_SUCCESS;
} else {
ESP_LOGE(TAG, "Source stream[%u] enabled failed", i);
ESP_LOGE(TAG, "[SRC #%zu] Enable failed", i);
sources[i].enabled = ASCS_RSP_FAILURE;
}
return;
}
}
ESP_LOGW(TAG, "No matching stream found for enabled stream %p", stream);
ESP_LOGW(TAG, "No matching stream for enable callback");
}
static int connect_stream(void)
@@ -546,7 +585,7 @@ static int connect_stream(void)
err = esp_ble_audio_bap_stream_connect(stream);
if (err) {
ESP_LOGE(TAG, "Failed to connect stream pair[%u], err %d", i, err);
ESP_LOGE(TAG, "Pair[%zu] connect failed, err %d", i, err);
if (sink_ready) {
sinks[sink_index].connected = ASCS_RSP_FAILURE;
@@ -564,7 +603,8 @@ static int connect_stream(void)
sources[source_index].connected = ASCS_RSP_CONNECT;
}
ESP_LOGI(TAG, "Connecting stream pair[%u], sink %p source %p", i, sink_stream, source_stream);
ESP_LOGI(TAG, "Pair[%zu] connecting (%s)", i,
sink_ready ? "SNK first" : "SRC first");
return false;
}
@@ -603,12 +643,11 @@ static void stream_connected(esp_ble_audio_bap_stream_t *stream, bool success)
sinks[i].connected == ASCS_RSP_CONNECT &&
&sinks[i].stream == stream) {
if (success) {
ESP_LOGI(TAG, "Sink stream[%u] connect succeeded", i);
sinks[i].connected = state;
ESP_LOGI(TAG, "[SNK #%zu] Connected", i);
} else {
ESP_LOGE(TAG, "Sink stream[%u] connect failed", i);
sinks[i].connected = state;
ESP_LOGE(TAG, "[SNK #%zu] Connect failed", i);
}
sinks[i].connected = state;
return;
}
}
@@ -620,17 +659,16 @@ static void stream_connected(esp_ble_audio_bap_stream_t *stream, bool success)
sources[i].connected == ASCS_RSP_CONNECT &&
&sources[i].stream == stream) {
if (success) {
ESP_LOGI(TAG, "Source stream[%u] connect succeeded", i);
sources[i].connected = state;
ESP_LOGI(TAG, "[SRC #%zu] Connected", i);
} else {
ESP_LOGE(TAG, "Source stream[%u] connect failed", i);
sources[i].connected = state;
ESP_LOGE(TAG, "[SRC #%zu] Connect failed", i);
}
sources[i].connected = state;
return;
}
}
ESP_LOGW(TAG, "No matching stream found for connected stream %p", stream);
ESP_LOGW(TAG, "No matching stream for connected callback");
}
static int start_stream(void)
@@ -639,7 +677,7 @@ static int start_stream(void)
/* Note: sink streams are started by the unicast server */
ESP_LOGI(TAG, "Starting source streams");
ESP_LOGI(TAG, "[SRC] Starting all source streams");
for (size_t i = 0; i < ARRAY_SIZE(sources); i++) {
if (sources[i].configured == ASCS_RSP_SUCCESS &&
@@ -648,7 +686,7 @@ static int start_stream(void)
sources[i].connected == ASCS_RSP_SUCCESS) {
err = esp_ble_audio_bap_stream_start(&sources[i].stream);
if (err) {
ESP_LOGE(TAG, "Failed to start stream[%u], err %d", i, err);
ESP_LOGE(TAG, "[SRC #%zu] Start failed, err %d", i, err);
}
}
}
@@ -659,19 +697,21 @@ static int start_stream(void)
static void stream_configured_cb(esp_ble_audio_bap_stream_t *stream,
const esp_ble_audio_bap_qos_cfg_pref_t *pref)
{
ESP_LOGI(TAG, "Stream %p configured", stream);
example_print_qos_pref(pref);
ESP_LOGI(TAG, "[%s #%d] Stream configured, QoS preference:",
stream_dir_str(stream), stream_index(stream));
example_print_qos_pref(TAG, pref);
}
static void stream_qos_set_cb(esp_ble_audio_bap_stream_t *stream)
{
ESP_LOGI(TAG, "Stream %p QoS set", stream);
/* QoS set is also reported by qos_cb; skip the duplicate log here. */
(void)stream;
}
static void stream_enabled_cb(esp_ble_audio_bap_stream_t *stream)
{
ESP_LOGI(TAG, "Stream %p enabled", stream);
/* Enabled is also reported by enable_cb; skip the duplicate log here. */
(void)stream;
}
static void stream_connected_cb(esp_ble_audio_bap_stream_t *stream)
@@ -680,23 +720,17 @@ static void stream_connected_cb(esp_ble_audio_bap_stream_t *stream)
int pair_index;
bool ret;
ESP_LOGI(TAG, "Stream %p connected", stream);
pair_index = get_stream_pair_index(stream);
paired_stream = get_paired_stream(stream);
if (pair_index >= 0) {
ESP_LOGI(TAG, "Stream %p in pair[%d], paired stream %p, same_pair %u",
stream, pair_index, paired_stream,
streams_are_same_pair(stream, paired_stream));
} else {
ESP_LOGW(TAG, "Failed to find stream pair for stream %p", stream);
}
ESP_LOGI(TAG, "[%s #%d] ISO connected (pair[%d])",
stream_dir_str(stream), stream_index(stream), pair_index);
stream_connected(stream, true);
if (streams_are_same_pair(stream, paired_stream) &&
stream_is_connecting(paired_stream)) {
ESP_LOGI(TAG, "Waiting paired stream %p connected before next pair", paired_stream);
/* Wait for the paired stream before moving on */
return;
}
@@ -710,7 +744,8 @@ static void stream_connected_cb(esp_ble_audio_bap_stream_t *stream)
static void stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
{
ESP_LOGI(TAG, "Stream %p disconnected, reason 0x%02x", stream, reason);
ESP_LOGI(TAG, "[%s #%d] ISO disconnected, reason 0x%02x",
stream_dir_str(stream), stream_index(stream), reason);
/* Reset the connected state to allow reconnection */
for (size_t i = 0; i < ARRAY_SIZE(sinks); i++) {
@@ -728,36 +763,41 @@ static void stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, uint8_t r
}
}
ESP_LOGW(TAG, "No matching stream found for disconnected stream %p", stream);
ESP_LOGW(TAG, "No matching stream for disconnected callback");
}
static void stream_started_cb(esp_ble_audio_bap_stream_t *stream)
{
int err;
ESP_LOGI(TAG, "Stream %p started", stream);
ESP_LOGI(TAG, "[%s #%d] Stream started",
stream_dir_str(stream), stream_index(stream));
if (stream_is_sink(stream)) {
err = stream_tx_register(stream);
if (err) {
ESP_LOGE(TAG, "Failed to register stream %p for TX, err %d", stream, err);
ESP_LOGE(TAG, "[SNK #%d] Failed to register TX, err %d",
stream_index(stream), err);
}
}
}
static void stream_metadata_updated_cb(esp_ble_audio_bap_stream_t *stream)
{
ESP_LOGI(TAG, "Stream %p metadata updated", stream);
ESP_LOGI(TAG, "[%s #%d] Metadata updated",
stream_dir_str(stream), stream_index(stream));
}
static void stream_disabled_cb(esp_ble_audio_bap_stream_t *stream)
{
ESP_LOGI(TAG, "Stream %p disabled", stream);
ESP_LOGI(TAG, "[%s #%d] Stream disabled",
stream_dir_str(stream), stream_index(stream));
}
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);
ESP_LOGI(TAG, "[%s #%d] Stream stopped, reason 0x%02x",
stream_dir_str(stream), stream_index(stream), reason);
if (stream_is_sink(stream)) {
(void)stream_tx_unregister(stream);
@@ -766,7 +806,8 @@ static void stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason
static void stream_released_cb(esp_ble_audio_bap_stream_t *stream)
{
ESP_LOGI(TAG, "Stream %p released", stream);
ESP_LOGI(TAG, "[%s #%d] Stream released",
stream_dir_str(stream), stream_index(stream));
}
static void stream_sent_cb(esp_ble_audio_bap_stream_t *stream, void *user_data)
@@ -792,14 +833,28 @@ 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);
ESP_LOGI(TAG, "[%s] Location: 0x%08lx", dir_str(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);
ESP_LOGI(TAG, "Available contexts: SNK=0x%04x SRC=0x%04x", snk_ctx, src_ctx);
}
static void log_rsp(const char *op, 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)
{
if (rsp_code == ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_SUCCESS) {
ESP_LOGI(TAG, "[%s #%d] %s ACK'd",
stream_dir_str(stream), stream_index(stream), op);
} else {
ESP_LOGW(TAG, "[%s #%d] %s rejected (rsp 0x%02x reason 0x%02x)",
stream_dir_str(stream), stream_index(stream), op,
rsp_code, reason);
}
}
static void config_cb(esp_ble_audio_bap_stream_t *stream,
@@ -810,7 +865,7 @@ static void config_cb(esp_ble_audio_bap_stream_t *stream,
bool ret;
int err;
ESP_LOGI(TAG, "Config, stream %p rsp_code 0x%02x reason 0x%02x", stream, rsp_code, reason);
log_rsp("Config", stream, rsp_code, reason);
stream_configured(stream, rsp_code == ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_SUCCESS);
@@ -853,7 +908,7 @@ 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 0x%02x reason 0x%02x", stream, rsp_code, reason);
log_rsp("QoS", stream, rsp_code, reason);
stream_qos_set(stream, rsp_code == ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_SUCCESS);
@@ -868,7 +923,7 @@ static void enable_cb(esp_ble_audio_bap_stream_t *stream,
{
bool ret;
ESP_LOGI(TAG, "Enable, stream %p rsp_code 0x%02x reason 0x%02x", stream, rsp_code, reason);
log_rsp("Enable", stream, rsp_code, reason);
stream_enabled(stream, rsp_code == ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_SUCCESS);
@@ -884,35 +939,35 @@ 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 0x%02x reason 0x%02x", stream, rsp_code, reason);
log_rsp("Start", 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 0x%02x reason 0x%02x", stream, rsp_code, reason);
log_rsp("Stop", 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 0x%02x reason 0x%02x", stream, rsp_code, reason);
log_rsp("Disable", 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 0x%02x reason 0x%02x", stream, rsp_code, reason);
log_rsp("Metadata", 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 0x%02x reason 0x%02x", stream, rsp_code, reason);
log_rsp("Release", stream, rsp_code, reason);
}
static void pac_record_cb(esp_ble_conn_t *conn,
@@ -923,7 +978,7 @@ static void pac_record_cb(esp_ble_conn_t *conn,
return;
}
example_print_codec_cap(codec_cap);
example_print_codec_cap(TAG, codec_cap);
}
static void endpoint_cb(esp_ble_conn_t *conn,
@@ -933,23 +988,23 @@ static void endpoint_cb(esp_ble_conn_t *conn,
if (dir == ESP_BLE_AUDIO_DIR_SOURCE) {
for (size_t i = 0; i < ARRAY_SIZE(sources); i++) {
if (sources[i].ep == NULL) {
ESP_LOGI(TAG, "Source #%u: ep %p", i, ep);
ESP_LOGI(TAG, "[SRC #%zu] Endpoint discovered", i);
sources[i].ep = ep;
return;
}
}
ESP_LOGW(TAG, "Could not add source ep");
ESP_LOGW(TAG, "[SRC] Endpoint dropped (no free slot)");
} else if (dir == ESP_BLE_AUDIO_DIR_SINK) {
for (size_t i = 0; i < ARRAY_SIZE(sinks); i++) {
if (sinks[i].ep == NULL) {
ESP_LOGI(TAG, "Sink #%u: ep %p", i, ep);
ESP_LOGI(TAG, "[SNK #%zu] Endpoint discovered", i);
sinks[i].ep = ep;
return;
}
}
ESP_LOGW(TAG, "Could not add sink ep");
ESP_LOGW(TAG, "[SNK] Endpoint dropped (no free slot)");
}
}
@@ -961,17 +1016,17 @@ static void discover_cb(esp_ble_conn_t *conn, int err, esp_ble_audio_dir_t dir)
if (dir == ESP_BLE_AUDIO_DIR_SINK) {
if (err) {
ESP_LOGE(TAG, "Discovery sinks failed, err %d", err);
ESP_LOGE(TAG, "[SNK] Discovery failed, err %d", err);
} else {
ESP_LOGI(TAG, "Discover sinks complete");
ESP_LOGI(TAG, "[SNK] Discovery complete");
discover_sources();
}
} else if (dir == ESP_BLE_AUDIO_DIR_SOURCE) {
if (err) {
ESP_LOGE(TAG, "Discovery sources failed, err %d", err);
ESP_LOGE(TAG, "[SRC] Discovery failed, err %d", err);
} else {
ESP_LOGI(TAG, "Discover sources complete");
ESP_LOGI(TAG, "[SRC] Discovery complete");
configure_stream();
}
@@ -1120,8 +1175,8 @@ static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event)
return;
}
ESP_LOGI(TAG, "Unicast server found, type %u, contexts 0x%08x, meta_len %u",
adv.announcement_type, adv.audio_contexts, adv.meta_len);
ESP_LOGI(TAG, "Unicast server found: type %u contexts 0x%08x",
adv.announcement_type, adv.audio_contexts);
dst.type = event->ext_scan_recv.addr.type;
memcpy(dst.val, event->ext_scan_recv.addr.val, sizeof(dst.val));
@@ -1142,30 +1197,28 @@ 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);
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]);
ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x",
event->acl_connect.conn_handle, 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]);
default_conn_handle = event->acl_connect.conn_handle;
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",
ESP_LOGI(TAG, "Disconnected: handle %u reason 0x%02x",
event->acl_disconnect.conn_handle, event->acl_disconnect.reason);
default_conn_handle = CONN_HANDLE_INIT;
@@ -1173,13 +1226,17 @@ static void acl_disconnect(esp_ble_audio_gap_app_event_t *event)
disc_cancelled = false;
mtu_exchanged = false;
reset_stream_state();
/* Delete the group before reset_stream_state() memsets the streams:
* the library walks stream->iso / stream->group to return ISO slots
* to the pool, and zeroing them first leaks the slot.
*/
if (unicast_group != NULL) {
esp_ble_audio_bap_unicast_group_delete(unicast_group);
unicast_group = NULL;
}
reset_stream_state();
ext_scan_start();
}
@@ -1188,19 +1245,14 @@ 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);
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]);
ESP_LOGI(TAG, "Security: handle %u level %u bonded %u",
event->security_change.conn_handle,
event->security_change.sec_level,
event->security_change.bonded);
err = exchange_mtu(event->security_change.conn_handle);
if (err) {
@@ -1233,22 +1285,23 @@ 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",
ESP_LOGI(TAG, "MTU updated: 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",
ESP_LOGW(TAG, "MTU %u below minimum %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);
ESP_LOGE(TAG, "Failed to start service discovery, err %d", err);
return;
}
ESP_LOGI(TAG, "Start discovering gatt services");
ESP_LOGI(TAG, "Service discovery started: handle %u",
event->gatt_mtu_change.conn_handle);
/* Note:
* MTU exchanged event may arrived after discover completed event.
@@ -1262,14 +1315,15 @@ static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event)
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);
ESP_LOGE(TAG, "Service discovery failed, status %u",
event->gattc_disc_cmpl.status);
return;
}
ESP_LOGI(TAG, "Service discovery complete: handle %u",
event->gattc_disc_cmpl.conn_handle);
/* Note:
* Discover completed event may arrived before MTU exchanged event.
*/
@@ -1317,7 +1371,7 @@ static int ext_scan_start(void)
return err;
}
ESP_LOGI(TAG, "Extended scan started");
ESP_LOGI(TAG, "Scanning for unicast server...");
return 0;
}
@@ -8,6 +8,7 @@
#include <stdint.h>
#include <stdbool.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -46,13 +47,13 @@ static void stream_tx_send(struct tx_stream *tx_stream)
}
if (tx_stream->stream->qos == NULL || tx_stream->stream->qos->sdu == 0) {
ESP_LOGE(TAG, "Invalid stream qos");
ESP_LOGE(TAG, "[SNK TX] Invalid 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);
ESP_LOGE(TAG, "[SNK TX] Buffer unavailable (SDU %u)",
tx_stream->stream->qos->sdu);
return;
}
@@ -64,8 +65,7 @@ static void stream_tx_send(struct tx_stream *tx_stream)
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);
ESP_LOGD(TAG, "[SNK TX] send failed, err %d", err);
return;
}
@@ -76,8 +76,11 @@ 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) {
char name[24];
snprintf(name, sizeof(name), "SNK #%zu", i);
example_audio_tx_scheduler_on_sent(&tx_streams[i].scheduler, user_data,
TAG, "stream", stream);
TAG, name);
break;
}
}
@@ -100,18 +103,16 @@ int stream_tx_register(esp_ble_audio_bap_stream_t *stream)
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");
ESP_LOGE(TAG, "[SNK #%zu] Invalid QoS", i);
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);
ESP_LOGE(TAG, "[SNK #%zu] Failed to alloc buffer (SDU %u)",
i, stream->qos->sdu);
return -ENOMEM;
}
}
@@ -123,17 +124,20 @@ int stream_tx_register(esp_ble_audio_bap_stream_t *stream)
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);
ESP_LOGE(TAG, "[SNK #%zu] Scheduler start failed, err %d", i, err);
tx_streams[i].stream = NULL;
return err;
}
ESP_LOGI(TAG, "[SNK #%zu] Started (SDU %u, interval %u us)",
i, stream->qos->sdu, stream->qos->interval);
stream_tx_send(&tx_streams[i]);
return 0;
}
}
ESP_LOGE(TAG, "No free TX stream slot");
ESP_LOGE(TAG, "[SNK] No free TX slot");
return -ENOMEM;
}
@@ -148,11 +152,9 @@ int stream_tx_unregister(esp_ble_audio_bap_stream_t *stream)
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);
ESP_LOGE(TAG, "[SNK #%zu] Scheduler stop failed, err %d", i, err);
return err;
}
@@ -162,6 +164,7 @@ int stream_tx_unregister(esp_ble_audio_bap_stream_t *stream)
tx_streams[i].data = NULL;
}
ESP_LOGI(TAG, "[SNK #%zu] Stopped", i);
return 0;
}
}
@@ -178,7 +181,7 @@ void stream_tx_init(void)
tx_scheduler_cb,
&tx_streams[i]);
if (err) {
ESP_LOGE(TAG, "Failed to init tx scheduler[%u], err %d", i, err);
ESP_LOGE(TAG, "[SNK #%zu] Scheduler init failed, err %d", i, err);
return;
}
}
@@ -6,6 +6,7 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=20
CONFIG_BT_NIMBLE_ISO=y
@@ -5,57 +5,105 @@
(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.
## Overview
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`.
This example implements a **BAP Unicast Server** on top of the NimBLE host stack. It registers PACS sink and source capabilities (LC3, any frequency, 10 ms frame duration, 2-channel support, 40-120 octets per frame, up to 2 frames per SDU) and a hard-coded location/context set (front-left + front-right; UNSPECIFIED | CONVERSATIONAL | MEDIA), then starts connectable extended advertising on 1M/2M PHY whose payload carries the Flags AD, the ASCS UUID, the targeted unicast announcement service data with sink/source contexts, and the device name `BAP Unicast Server`.
The unicast server callbacks (`config`, `reconfig`, `qos`, `enable`, `start`, `metadata`, `disable`, `stop`, `release`) react to ASCS PDUs from a peer client. `config_cb` allocates a free `sink_streams[]`/`source_streams[]` slot and returns a fixed `qos_pref` (unframed PDUs, 2M PHY preference, RTN 2, 10 ms max latency, 20-40 ms presentation delay). `enable_cb` decodes the codec config to sample rate, frame duration, and frame-blocks-per-SDU before accepting the request. Reconfig is rejected unconditionally.
The stream ops handle ISO data: when the client enables a sink ASE, `stream_enabled_cb` calls `esp_ble_audio_bap_stream_start` so the server starts sink streams locally (the client starts source streams). Inbound ISO SDUs flow through `stream_recv_cb` into `example_audio_rx_metrics_on_recv`, which logs RX metrics periodically.
## 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
* A board with BLE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31)
* Peer device running the paired example
## How to Use Example
## Configuration
Before project configuration and build, set the correct chip target:
Open menuconfig:
```bash
idf.py menuconfig
```
No build-time options — runtime defaults are baked into source.
### Security & Pairing
This example inherits a Just-Works pairing model (LE Secure Connections, no MITM) with bonding enabled from the shared init at `../common_components/example_init/ble_audio_example_init.c`.
## Build & Flash
```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.
(Exit serial monitor with `Ctrl-]`.)
## 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.
1. `app_main` initializes NVS, NimBLE, and the LE Audio common layer, then calls `esp_ble_audio_pacs_register` with `snk_pac/snk_loc/src_pac/src_loc` all true.
2. It registers the unicast server (`esp_ble_audio_bap_unicast_server_register` with `CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT` / `CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT`) and its callback table, registers sink and source PACS capabilities, and attaches `stream_ops` to every preallocated stream slot.
3. PACS location is set per direction (`SINK_LOCATION` / `SOURCE_LOCATION`) and supported plus available contexts are pushed for sink and source.
4. `esp_ble_audio_common_start` and `ble_svc_gap_device_name_set("BAP Unicast Server")` run, then `ext_adv_start` builds `ext_adv_data` with Flags, ASCS UUID, ASCS service data (targeted announcement + contexts), and the device name; it configures and starts the extended advertising set on `ADV_HANDLE` 0.
5. On `ESP_BLE_AUDIO_GAP_EVENT_ACL_CONNECT` the connection is logged; after the peer client triggers MTU exchange the server logs the new MTU and calls `esp_ble_audio_gattc_disc_start`.
6. ASCS callbacks fire as the client drives the state machine: `config_cb` allocates a stream and returns the QoS preference, `qos_cb` records `max_sdu`, `enable_cb` validates the codec, `metadata_cb` parses metadata, and `disable`/`stop`/`release` log their requests.
7. `stream_enabled_cb` auto-starts sink streams server-side; `stream_started_cb` resets TX or RX counters depending on direction; `stream_recv_cb` updates per-stream RX metrics on every ISO SDU.
8. On disconnect `acl_disconnect` logs the reason and restarts `ext_adv_start`.
## Example Output
## Expected Log
TAG: `BAP_USR`.
Startup and advertising:
```
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...)
...
I (xxx) BAP_USR: PACS: locations and contexts set
I (xxx) BAP_USR: Advertising started (handle 0)
```
If the client disconnects:
Client connects:
```
I (xxx) BAP_USR: connection disconnected, reason 0x...
I (xxx) BAP_USR: Connected: handle ... role ... peer ...
I (xxx) BAP_USR: MTU updated: handle ... mtu ...
I (xxx) BAP_USR: Service discovery started: handle ...
I (xxx) BAP_USR: Service discovery complete: handle ... status ...
```
ASCS operations:
```
I (xxx) BAP_USR: [SNK #...] Config request
I (xxx) BAP_USR: [SNK #...] QoS request:
I (xxx) BAP_USR: [SNK #...] Enable request: ... Hz, ... us, ... block(s)/SDU
I (xxx) BAP_USR: [SNK #...] Stream enabled
I (xxx) BAP_USR: [SNK #...] Stream started
I (xxx) BAP_USR: [SRC #...] Config request
I (xxx) BAP_USR: [SRC #...] QoS request:
I (xxx) BAP_USR: [SRC #...] Enable request: ... Hz, ... us, ... block(s)/SDU
I (xxx) BAP_USR: [SRC #...] Stream enabled
I (xxx) BAP_USR: [SRC #...] Stream started
```
Teardown:
```
I (xxx) BAP_USR: [SNK #...] Disable request
I (xxx) BAP_USR: [SNK #...] Stop request
I (xxx) BAP_USR: [SNK #...] Stream stopped, reason 0x...
I (xxx) BAP_USR: [SNK #...] Release request
I (xxx) BAP_USR: Disconnected: handle ... reason 0x...
```
## Peer Pairing
Run [unicast_client](../unicast_client/) on a second board.
1. Server boots and starts connectable extended advertising with the ASCS UUID and `BAP Unicast Server` name.
2. Client boots and scans; on matching the name and ASCS service data it cancels scanning and issues `ble_gap_connect`.
3. After ACL is up the client initiates pairing/security; both sides log `Connected:` and the client logs `Security:`.
4. Client exchanges MTU and runs GATT discovery, then discovers sink ASEs followed by source ASEs on the server.
5. Client drives ASCS Config -> QoS -> Enable -> ISO Connect on every ASE; the server returns its QoS preference and accepts each operation.
6. Client starts source streams; server auto-starts each sink stream from `stream_enabled_cb`, after which the client's TX scheduler feeds SDUs and the server's `stream_recv_cb` accumulates RX metrics.
@@ -128,6 +128,31 @@ static esp_ble_audio_dir_t stream_dir(const esp_ble_audio_bap_stream_t *stream)
assert(0 && "Invalid stream");
}
static const char *dir_str(esp_ble_audio_dir_t dir)
{
return dir == ESP_BLE_AUDIO_DIR_SINK ? "SNK" : "SRC";
}
static const char *stream_dir_str(const esp_ble_audio_bap_stream_t *stream)
{
return dir_str(stream_dir(stream));
}
static int stream_index(const esp_ble_audio_bap_stream_t *stream)
{
for (size_t i = 0; i < ARRAY_SIZE(sink_streams); i++) {
if (stream == &sink_streams[i].stream) {
return (int)i;
}
}
for (size_t i = 0; i < ARRAY_SIZE(source_streams); i++) {
if (stream == &source_streams[i].stream) {
return (int)i;
}
}
return -1;
}
static esp_ble_audio_bap_stream_t *stream_alloc(esp_ble_audio_dir_t dir)
{
if (dir == ESP_BLE_AUDIO_DIR_SOURCE) {
@@ -159,20 +184,18 @@ static int config_cb(esp_ble_conn_t *conn,
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);
example_print_codec_cfg(TAG, codec_cfg);
*stream = stream_alloc(dir);
if (*stream == NULL) {
ESP_LOGI(TAG, "No streams available");
ESP_LOGW(TAG, "[%s] No streams available", dir_str(dir));
*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);
ESP_LOGI(TAG, "[%s #%d] Config request", dir_str(dir), stream_index(*stream));
if (dir == ESP_BLE_AUDIO_DIR_SOURCE) {
configured_source_stream_count++;
@@ -189,9 +212,8 @@ static int reconfig_cb(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, "Reconfig: stream %p dir %u", stream, dir);
example_print_codec_cfg(codec_cfg);
ESP_LOGW(TAG, "[%s #%d] Reconfig rejected (not supported)",
dir_str(dir), stream_index(stream));
*rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_CONF_UNSUPPORTED,
ESP_BLE_AUDIO_BAP_ASCS_REASON_NONE);
@@ -204,9 +226,9 @@ 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);
ESP_LOGI(TAG, "[%s #%d] QoS request:",
stream_dir_str(stream), stream_index(stream));
example_print_qos(TAG, qos);
for (size_t i = 0; i < ARRAY_SIZE(source_streams); i++) {
if (stream == &source_streams[i].stream) {
@@ -274,8 +296,9 @@ static int enable_cb(esp_ble_audio_bap_stream_t *stream,
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);
ESP_LOGI(TAG, "[%s #%d] Enable request: %lu Hz, %lu us, %u block(s)/SDU",
stream_dir_str(stream), stream_index(stream),
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);
@@ -285,8 +308,6 @@ static int enable_cb(esp_ble_audio_bap_stream_t *stream,
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;
@@ -304,7 +325,7 @@ static bool data_cb(uint8_t type, const uint8_t *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);
ESP_LOGW(TAG, "Invalid metadata type 0x%02x len %u", type, data_len);
*rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_METADATA_REJECTED,
type);
@@ -320,7 +341,9 @@ static int metadata_cb(esp_ble_audio_bap_stream_t *stream,
{
esp_err_t err;
ESP_LOGI(TAG, "Metadata: stream %p meta_len %u", stream, meta_len);
ESP_LOGI(TAG, "[%s #%d] Metadata update (%u bytes)",
stream_dir_str(stream), stream_index(stream), meta_len);
example_print_meta(TAG, meta, meta_len);
err = esp_ble_audio_data_parse(meta, meta_len, data_cb, rsp);
if (err) {
@@ -333,21 +356,24 @@ static int metadata_cb(esp_ble_audio_bap_stream_t *stream,
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);
ESP_LOGI(TAG, "[%s #%d] Disable request",
stream_dir_str(stream), stream_index(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);
ESP_LOGI(TAG, "[%s #%d] Stop request",
stream_dir_str(stream), stream_index(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);
ESP_LOGI(TAG, "[%s #%d] Release request",
stream_dir_str(stream), stream_index(stream));
if (stream_dir(stream) == ESP_BLE_AUDIO_DIR_SOURCE &&
configured_source_stream_count > 0) {
@@ -373,7 +399,8 @@ static void stream_enabled_cb(esp_ble_audio_bap_stream_t *stream)
{
esp_err_t err;
ESP_LOGI(TAG, "Stream %p enabled", stream);
ESP_LOGI(TAG, "[%s #%d] Stream enabled",
stream_dir_str(stream), stream_index(stream));
/* The unicast server is responsible for starting sink ASEs
* after the client has enabled them.
@@ -381,14 +408,16 @@ static void stream_enabled_cb(esp_ble_audio_bap_stream_t *stream)
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);
ESP_LOGE(TAG, "[SNK #%d] Failed to start stream, err %d",
stream_index(stream), err);
}
}
}
static void stream_started_cb(esp_ble_audio_bap_stream_t *stream)
{
ESP_LOGI(TAG, "Stream %p started", stream);
ESP_LOGI(TAG, "[%s #%d] Stream started",
stream_dir_str(stream), stream_index(stream));
if (stream_dir(stream) == ESP_BLE_AUDIO_DIR_SOURCE) {
struct unicast_server_stream *source_stream =
@@ -408,7 +437,8 @@ static void stream_started_cb(esp_ble_audio_bap_stream_t *stream)
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);
ESP_LOGI(TAG, "[%s #%d] Stream stopped, reason 0x%02x",
stream_dir_str(stream), stream_index(stream), reason);
}
static void stream_recv_cb(esp_ble_audio_bap_stream_t *stream,
@@ -418,10 +448,13 @@ static void stream_recv_cb(esp_ble_audio_bap_stream_t *stream,
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);
char name[24];
size_t idx = sink_stream - sink_streams;
snprintf(name, sizeof(name), "SNK #%zu", idx);
sink_stream->rx_metrics.last_sdu_len = len;
example_audio_rx_metrics_on_recv(info, &sink_stream->rx_metrics,
TAG, "stream", stream);
TAG, name);
}
}
@@ -452,8 +485,6 @@ static int set_pacs_location(void)
}
}
ESP_LOGI(TAG, "Location successfully set");
return 0;
}
@@ -477,8 +508,6 @@ static int set_pacs_supported_contexts(void)
}
}
ESP_LOGI(TAG, "Supported contexts successfully set");
return 0;
}
@@ -502,7 +531,7 @@ static int set_pacs_available_contexts(void)
}
}
ESP_LOGI(TAG, "Available contexts successfully set");
ESP_LOGI(TAG, "PACS: locations and contexts set");
return 0;
}
@@ -591,28 +620,26 @@ static void ext_adv_start(void)
return;
}
ESP_LOGI(TAG, "Extended adv instance %u started", ADV_HANDLE);
ESP_LOGI(TAG, "Advertising started (handle %u)", 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);
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]);
ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x",
event->acl_connect.conn_handle, 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",
ESP_LOGI(TAG, "Disconnected: handle %u reason 0x%02x",
event->acl_disconnect.conn_handle, event->acl_disconnect.reason);
ext_adv_start();
@@ -636,11 +663,11 @@ 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",
ESP_LOGI(TAG, "MTU updated: 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",
ESP_LOGW(TAG, "MTU %u below minimum %u",
event->gatt_mtu_change.mtu, ESP_BLE_AUDIO_ATT_MTU_MIN);
return;
}
@@ -651,17 +678,19 @@ static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event)
*/
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);
ESP_LOGE(TAG, "Failed to start service discovery, err %d", err);
return;
}
ESP_LOGI(TAG, "Start discovering gatt services");
ESP_LOGI(TAG, "Service discovery started: handle %u",
event->gatt_mtu_change.conn_handle);
}
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);
ESP_LOGI(TAG, "Service discovery complete: handle %u status %u",
event->gattc_disc_cmpl.conn_handle,
event->gattc_disc_cmpl.status);
}
static void iso_gatt_app_cb(esp_ble_audio_gatt_app_event_t *event)
@@ -6,6 +6,7 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=20
CONFIG_BT_NIMBLE_ISO=y
@@ -5,72 +5,156 @@
(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.
## Overview
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.
This example implements the **Common Audio Profile (CAP) Acceptor** role on top of the NimBLE host stack with ISO and LE Audio support. It is built in one of two mutually exclusive sub-modes selected at build time: a **CAP Acceptor / BAP Unicast Server**, or a **CAP Acceptor / BAP Broadcast Sink** (with the BAP Scan Delegator role enabled). PACS is registered in both modes with LC3 sink and source capabilities (any sampling frequency, 7.5 ms or 10 ms frame, up to 2 channels, 30..155 octets per frame, up to 2 frames per SDU). Sink and source PAC location are set to `FRONT_LEFT | FRONT_RIGHT` (note in `main.c`: with `MONO_AUDIO`, Samsung S24 declines unicast).
In **unicast** mode (`cap_acceptor_unicast.c`) the acceptor registers BAP unicast-server callbacks (config / reconfig / qos / enable / start / metadata / disable / stop / release) and CAP stream ops, and on enable of a sink ASE automatically issues `bap_stream_start` (Receiver Start Ready). When the source ASE starts, an internal TX scheduler begins sending dummy SDUs. In **broadcast** mode (`cap_acceptor_broadcast.c`) the acceptor registers BAP scan-delegator and broadcast-sink callbacks, drives PA sync (without PAST) on request, receives BASE and BIGInfo, then calls `esp_ble_audio_bap_broadcast_sink_sync` on the first BIS index. Optional **self-scan** lets the acceptor scan for a broadcast source named `CAP Broadcast Source` and use a hardcoded broadcast code `1234` instead of waiting for a Broadcast Assistant.
The acceptor advertises connectable extended advertising on handle 0 with flags, the ASCS+CAS UUID list, CAS service data with targeted-announcement byte, ASCS targeted-announcement and contexts (unicast build), BASS service data (broadcast build), and the complete device name `cap_acceptor`. The GAP/GATT device name is set to `CAP Acceptor`.
## 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)
* A board with BLE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31)
* Peer device running the paired example
## How to Use Example
## Configuration
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:
Open menuconfig:
```bash
idf.py menuconfig
```
Under **Example: CAP Acceptor**, select one of:
Under **Example: CAP Acceptor** -> **CAP Acceptor mode**:
* **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
* **Unicast** (`EXAMPLE_UNICAST`, default) — act as BAP Unicast Server; advertise CAS + ASCS for a CAP Initiator. Mutually exclusive with Broadcast.
* **Broadcast** (`EXAMPLE_BROADCAST`) — act as BAP Broadcast Sink with Scan Delegator; advertise CAS + BASS. Mutually exclusive with Unicast.
### Build and Flash
When **Broadcast** is selected, an additional option appears:
Run the following to build, flash and monitor:
* **Scan for Broadcast Sources without Broadcast Assistant** (`EXAMPLE_SCAN_SELF`) — start scanning at boot for a source advertising the name `CAP Broadcast Source`; on match, create the PA sync directly. The hardcoded broadcast code `1234` is used if the BIG is encrypted. Without this option, a separate Broadcast Assistant must drive PA / BIS sync over BASS.
### Security & Pairing
Just-Works pairing (LE Secure Connections, no MITM, `BLE_SM_IO_CAP_NO_IO`) with bonding enabled, inherited from `../../common_components/example_init/ble_audio_example_init.c`. Change pairing or IO-cap there.
## Build & Flash
```bash
idf.py set-target esp32h4
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.
(Exit serial monitor with `Ctrl-]`.)
## 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.
1. `app_main` initializes NVS, the Bluetooth controller (`bluetooth_init`) and the LE Audio common layer (`esp_ble_audio_common_init`) with GAP and GATT callbacks.
2. `init_cap_acceptor` registers PACS, registers sink+source PAC capabilities, sets PAC location (`FRONT_LEFT | FRONT_RIGHT`) and supported/available contexts.
3. The selected sub-mode initializes itself: `cap_acceptor_unicast_init` registers BAP unicast-server callbacks, CAP stream ops on the sink and source streams, and a TX scheduler; or `cap_acceptor_broadcast_init` registers BAP scan-delegator and broadcast-sink callbacks plus broadcast stream ops.
4. `esp_ble_audio_common_start` starts the audio stack and the device name is set to `CAP Acceptor`.
5. If `EXAMPLE_SCAN_SELF` is set, `check_start_scan` starts extended scanning for the broadcast source.
6. `ext_adv_start` configures and starts connectable extended advertising on handle 0 with the CAS / ASCS / BASS service data appropriate to the build.
7. **Unicast**: on ACL connect the connection handle is stored. On MTU change the acceptor starts service discovery (acting as GATT client). The unicast server then handles config -> reconfig -> qos -> enable -> start (auto Receiver Start Ready for sink ASEs) -> metadata -> disable -> stop -> release per ASE; when the source ASE starts, the TX scheduler begins sending dummy SDUs.
8. **Broadcast**: on `pa_sync_req` from a Broadcast Assistant (or on a scan match in self-scan mode) the acceptor creates a PA sync without PAST. On PA sync, it creates a broadcast sink for the broadcast ID, receives BASE and BIGInfo, then syncs to the first BIS. Stream `started` enters the synced state; `stopped` and `pa_sync_lost` clear flags and (in self-scan mode) restart scanning.
9. On ACL disconnect the connection handle is reset and `ext_adv_start` re-arms advertising.
## Example Output
## Expected Log
TAG: `CAP_ACC`.
### Unicast mode
```
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: Advertising started (handle 0)
I (xxx) CAP_ACC: Connected: handle ... role ... peer ...
I (xxx) CAP_ACC: MTU updated: handle ... mtu ...
I (xxx) CAP_ACC: Service discovery started: handle ...
I (xxx) CAP_ACC: Service discovery complete: handle ...
I (xxx) CAP_ACC: [SNK] Config request, codec cfg:
I (xxx) CAP_ACC: [SNK #0] Stream configured, QoS preference:
I (xxx) CAP_ACC: [SRC] Config request, codec cfg:
I (xxx) CAP_ACC: [SRC #0] Stream configured, QoS preference:
I (xxx) CAP_ACC: [SNK #0] QoS request:
I (xxx) CAP_ACC: [SNK #0] QoS set
I (xxx) CAP_ACC: [SRC #0] QoS request:
I (xxx) CAP_ACC: [SRC #0] QoS set
I (xxx) CAP_ACC: [SNK #0] Enable request (meta_len ...)
I (xxx) CAP_ACC: [SNK #0] Stream enabled
I (xxx) CAP_ACC: [SRC #0] Enable request (meta_len ...)
I (xxx) CAP_ACC: [SRC #0] Stream enabled
I (xxx) CAP_ACC: [SNK #0] Stream started
I (xxx) CAP_ACC: [SRC #0] Start request
I (xxx) CAP_ACC: [SRC #0] Stream started
```
On teardown / disconnect:
```
I (xxx) CAP_ACC: [SNK #0] Stop request
I (xxx) CAP_ACC: [SNK #0] Stream stopped, reason 0x...
I (xxx) CAP_ACC: [SNK #0] Release request
I (xxx) CAP_ACC: [SNK #0] Stream released
I (xxx) CAP_ACC: Sink stream released
I (xxx) CAP_ACC: Disconnected: handle ... reason 0x...
I (xxx) CAP_ACC: Advertising started (handle 0)
```
If the connection is lost:
### Broadcast mode (Broadcast Assistant)
```
I (xxx) CAP_ACC: connection disconnected, reason 0x...
I (xxx) CAP_ACC: PA sync lost, handle 0x... reason ...
I (xxx) CAP_ACC: Advertising started (handle 0)
I (xxx) CAP_ACC: Connected: handle ... role ... peer ...
I (xxx) CAP_ACC: Receive state updated, pa_sync 0x... encrypt 0x...
I (xxx) CAP_ACC: Received request to sync to PA (PAST not available): ...
I (xxx) CAP_ACC: Syncing without PAST
I (xxx) CAP_ACC: PA sync ... synced for broadcast sink
I (xxx) CAP_ACC: Creating broadcast sink for broadcast ID 0x......
I (xxx) CAP_ACC: BASE received
I (xxx) CAP_ACC: BIGInfo received
I (xxx) CAP_ACC: BIS sync request received (src_id ...): 0x........
I (xxx) CAP_ACC: Syncing to broadcast with bitfield 0x........
I (xxx) CAP_ACC: [SNK #0] Stream started
```
### Broadcast mode (self-scan)
```
I (xxx) CAP_ACC: Advertising started (handle 0)
I (xxx) CAP_ACC: Scanning for broadcast source...
I (xxx) CAP_ACC: Syncing without PAST from scan
I (xxx) CAP_ACC: PA sync ... synced for broadcast sink
I (xxx) CAP_ACC: Creating broadcast sink for broadcast ID 0x......
I (xxx) CAP_ACC: BASE received
I (xxx) CAP_ACC: BIGInfo received
I (xxx) CAP_ACC: Syncing to broadcast with bitfield 0x........
I (xxx) CAP_ACC: [SNK #0] Stream started
```
On PA sync loss:
```
I (xxx) CAP_ACC: [SNK #0] Stream stopped, reason 0x...
I (xxx) CAP_ACC: PA sync lost: sync_handle ... reason 0x...
I (xxx) CAP_ACC: Scanning for broadcast source...
```
## Peer Pairing
Run the [initiator](../initiator/) on a second board. The initiator and acceptor must be configured for the **same sub-mode** — both `EXAMPLE_UNICAST`, or both `EXAMPLE_BROADCAST` — otherwise they will not pair.
### Unicast
1. Flash this acceptor with `EXAMPLE_UNICAST`; it advertises connectable with CAS + ASCS service data and PACS contexts.
2. Flash the initiator with `EXAMPLE_UNICAST`; it scans, finds CAS, connects and pairs.
3. The acceptor exchanges MTU, runs service discovery, then handles ASCS config / qos / enable for both sink and source ASEs.
4. The sink ASE auto-starts on enable (Receiver Start Ready); the source ASE starts on the initiator's `start` and the acceptor's TX scheduler begins.
5. Bidirectional audio runs until either side disconnects.
### Broadcast
1. Flash the initiator with `EXAMPLE_BROADCAST`; it advertises as `CAP Broadcast Source` (broadcast ID `0x123456`) and starts the BIG.
2. Flash this acceptor with `EXAMPLE_BROADCAST`. Either enable `EXAMPLE_SCAN_SELF` to self-scan for the source by name (broadcast code `1234`), or leave it disabled and use a separate Broadcast Assistant that connects via BASS to drive PA / BIS sync.
3. The acceptor PA-syncs, receives BASE and BIGInfo, syncs the first BIS, and `[SNK #0] Stream started`.
4. On PA sync loss the acceptor cleans up; in self-scan mode it restarts scanning.
@@ -1,5 +1,5 @@
# SPDX-FileCopyrightText: 2022 Nordic Semiconductor ASA
# SPDX-FileContributor: 2025 Espressif Systems (Shanghai) CO LTD
# SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
menu "Example: CAP Acceptor"
@@ -106,7 +106,7 @@ static int ext_scan_start(void)
return err;
}
ESP_LOGI(TAG, "Extended scan started");
ESP_LOGI(TAG, "Scanning for broadcast source...");
return 0;
}
@@ -147,7 +147,7 @@ int check_start_scan(void)
static void broadcast_stream_started_cb(esp_ble_audio_bap_stream_t *stream)
{
ESP_LOGI(TAG, "Broadcast stream %p started", stream);
ESP_LOGI(TAG, "[SNK #0] Stream started");
example_audio_rx_metrics_reset(&rx_metrics);
@@ -157,13 +157,18 @@ static void broadcast_stream_started_cb(esp_ble_audio_bap_stream_t *stream)
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);
ESP_LOGI(TAG, "[SNK #0] Stream stopped, reason 0x%02x", reason);
atomic_clear_bit(flags, FLAG_BROADCAST_SYNCING);
atomic_clear_bit(flags, FLAG_BROADCAST_SYNCED);
#if CONFIG_EXAMPLE_SCAN_SELF
check_start_scan();
/* Defer the scan restart to broadcast_pa_lost(): if PA is still
* synced the broadcaster may just be switching streams, and a
* premature scan would race the controller's PA-loss timeout. */
if (atomic_test_bit(flags, FLAG_PA_SYNCED) == false) {
check_start_scan();
}
#endif /* CONFIG_EXAMPLE_SCAN_SELF */
}
@@ -171,9 +176,8 @@ 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);
example_audio_rx_metrics_on_recv(info, &rx_metrics, TAG, "SNK #0");
}
static int create_broadcast_sink(void)
@@ -355,7 +359,7 @@ static int pa_sync_req_cb(esp_ble_conn_t *conn,
{
int err;
ESP_LOGI(TAG, "Received request to sync to PA (PAST %savailble): %u",
ESP_LOGI(TAG, "Received request to sync to PA (PAST %savailable): %u",
past_available ? "" : "not ", recv_state->pa_sync_state);
broadcast_sink.recv_state = recv_state;
@@ -409,7 +413,7 @@ 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);
ESP_LOGI(TAG, "Broadcast code received (src_id %u)", recv_state->src_id);
broadcast_sink.recv_state = recv_state;
@@ -437,7 +441,8 @@ static int bis_sync_req_cb(esp_ble_conn_t *conn,
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]);
ESP_LOGI(TAG, "BIS sync request received (src_id %u): 0x%08lx",
recv_state->src_id, bis_sync_req[0]);
if (new_bis_sync_req != ESP_BLE_AUDIO_BAP_BIS_SYNC_NO_PREF &&
__builtin_popcount(new_bis_sync_req) > 1) {
@@ -703,5 +708,7 @@ int cap_acceptor_broadcast_init(void)
cbs_registered = true;
}
ESP_LOGI(TAG, "CAP acceptor broadcast initialized");
return 0;
}
@@ -31,8 +31,52 @@ static example_audio_tx_scheduler_t tx_scheduler;
static uint16_t tx_seq_num;
static uint8_t *iso_data;
static const struct peer_config *s_peer;
static void unicast_server_tx(void);
static const char *dir_str(esp_ble_audio_dir_t dir)
{
return dir == ESP_BLE_AUDIO_DIR_SINK ? "SNK" : "SRC";
}
static const char *stream_dir_str(const esp_ble_audio_bap_stream_t *stream)
{
esp_ble_audio_bap_ep_info_t ep_info = {0};
if (stream == NULL) {
return "???";
}
/* When the stream is released, stream->ep is already detached by the lib.
* Fall back to identifying by the stream's address.
*/
if (stream->ep == NULL) {
if (s_peer != NULL) {
if (stream == &s_peer->sink_stream.bap_stream) {
return "SNK";
}
if (stream == &s_peer->source_stream.bap_stream) {
return "SRC";
}
}
return "???";
}
if (esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info) != 0) {
return "???";
}
return dir_str(ep_info.dir);
}
static int stream_index(const esp_ble_audio_bap_stream_t *stream)
{
/* Only one sink and one source per peer in this example. */
(void)stream;
return 0;
}
static void tx_scheduler_cb(void *arg)
{
(void)arg;
@@ -49,13 +93,13 @@ static int config_cb(esp_ble_conn_t *conn,
{
esp_ble_audio_cap_stream_t *cap_stream;
ESP_LOGI(TAG, "Config: conn %p ep %p dir %u", conn, ep, dir);
ESP_LOGI(TAG, "[%s] Config request, codec cfg:", dir_str(dir));
example_print_codec_cfg(codec_cfg);
example_print_codec_cfg(TAG, codec_cfg);
cap_stream = stream_alloc(dir);
if (cap_stream == NULL) {
ESP_LOGE(TAG, "No streams available");
ESP_LOGE(TAG, "[%s] No streams available", dir_str(dir));
*rsp = ESP_BLE_AUDIO_BAP_ASCS_RSP(ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_NO_MEM,
ESP_BLE_AUDIO_BAP_ASCS_REASON_NONE);
@@ -64,8 +108,6 @@ static int config_cb(esp_ble_conn_t *conn,
*stream = &cap_stream->bap_stream;
ESP_LOGI(TAG, "Config stream %p", *stream);
*pref = qos_pref;
return 0;
@@ -77,9 +119,10 @@ static int reconfig_cb(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, "Reconfig: stream %p dir %u", stream, dir);
ESP_LOGI(TAG, "[%s #%d] Reconfig request, codec cfg:",
dir_str(dir), stream_index(stream));
example_print_codec_cfg(codec_cfg);
example_print_codec_cfg(TAG, codec_cfg);
*pref = qos_pref;
@@ -90,9 +133,10 @@ 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);
ESP_LOGI(TAG, "[%s #%d] QoS request:",
stream_dir_str(stream), stream_index(stream));
example_print_qos(qos);
example_print_qos(TAG, qos);
return 0;
}
@@ -101,14 +145,16 @@ 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);
ESP_LOGI(TAG, "[%s #%d] Enable request (meta_len %u)",
stream_dir_str(stream), stream_index(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);
ESP_LOGI(TAG, "[%s #%d] Start request",
stream_dir_str(stream), stream_index(stream));
return 0;
}
@@ -139,7 +185,8 @@ static int metadata_cb(esp_ble_audio_bap_stream_t *stream,
};
esp_err_t err;
ESP_LOGI(TAG, "Metadata: stream %p meta_len %u", stream, meta_len);
ESP_LOGI(TAG, "[%s #%d] Metadata request (meta_len %u)",
stream_dir_str(stream), stream_index(stream), meta_len);
err = esp_ble_audio_data_parse(meta, meta_len, data_func_cb, &func_param);
if (err) {
@@ -162,21 +209,24 @@ static int metadata_cb(esp_ble_audio_bap_stream_t *stream,
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);
ESP_LOGI(TAG, "[%s #%d] Disable request",
stream_dir_str(stream), stream_index(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);
ESP_LOGI(TAG, "[%s #%d] Stop request",
stream_dir_str(stream), stream_index(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);
ESP_LOGI(TAG, "[%s #%d] Release request",
stream_dir_str(stream), stream_index(stream));
return 0;
}
@@ -195,14 +245,16 @@ static const esp_ble_audio_bap_unicast_server_cb_t unicast_server_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);
ESP_LOGI(TAG, "[%s #%d] Stream configured, QoS preference:",
stream_dir_str(stream), stream_index(stream));
example_print_qos_pref(pref);
example_print_qos_pref(TAG, pref);
}
static void unicast_stream_qos_set_cb(esp_ble_audio_bap_stream_t *stream)
{
ESP_LOGI(TAG, "Unicast stream %p QoS set", stream);
ESP_LOGI(TAG, "[%s #%d] QoS set",
stream_dir_str(stream), stream_index(stream));
}
static void unicast_stream_enabled_cb(esp_ble_audio_bap_stream_t *stream)
@@ -210,7 +262,8 @@ 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);
ESP_LOGI(TAG, "[%s #%d] Stream enabled",
stream_dir_str(stream), stream_index(stream));
err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info);
if (err) {
@@ -218,9 +271,6 @@ static void unicast_stream_enabled_cb(esp_ble_audio_bap_stream_t *stream)
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);
@@ -236,7 +286,8 @@ 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);
ESP_LOGI(TAG, "[%s #%d] Stream started",
stream_dir_str(stream), stream_index(stream));
example_audio_rx_metrics_reset(&rx_metrics);
@@ -246,9 +297,6 @@ static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream)
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");
@@ -278,12 +326,14 @@ static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream)
static void unicast_stream_metadata_updated_cb(esp_ble_audio_bap_stream_t *stream)
{
ESP_LOGI(TAG, "Unicast stream %p metadata updated", stream);
ESP_LOGI(TAG, "[%s #%d] Metadata updated",
stream_dir_str(stream), stream_index(stream));
}
static void unicast_stream_disabled_cb(esp_ble_audio_bap_stream_t *stream)
{
ESP_LOGI(TAG, "Unicast stream %p disabled", stream);
ESP_LOGI(TAG, "[%s #%d] Stream disabled",
stream_dir_str(stream), stream_index(stream));
}
static void unicast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
@@ -291,7 +341,8 @@ static void unicast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_
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);
ESP_LOGI(TAG, "[%s #%d] Stream stopped, reason 0x%02x",
stream_dir_str(stream), stream_index(stream), reason);
err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info);
if (err) {
@@ -299,9 +350,6 @@ static void unicast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_
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) {
@@ -320,7 +368,8 @@ static void unicast_stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, u
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);
ESP_LOGI(TAG, "[%s #%d] ISO disconnected, reason 0x%02x",
stream_dir_str(stream), stream_index(stream), reason);
err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info);
if (err) {
@@ -347,7 +396,8 @@ static void unicast_stream_released_cb(esp_ble_audio_bap_stream_t *stream)
esp_ble_audio_cap_stream_t,
bap_stream);
ESP_LOGI(TAG, "Unicast stream %p released", stream);
ESP_LOGI(TAG, "[%s #%d] Stream released",
stream_dir_str(stream), stream_index(stream));
stream_released(cap_stream);
}
@@ -356,13 +406,21 @@ 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)
{
char name[24];
snprintf(name, sizeof(name), "%s #%d",
stream_dir_str(stream), stream_index(stream));
rx_metrics.last_sdu_len = len;
example_audio_rx_metrics_on_recv(info, &rx_metrics, TAG, "stream", stream);
example_audio_rx_metrics_on_recv(info, &rx_metrics, TAG, name);
}
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);
char name[24];
snprintf(name, sizeof(name), "%s #%d",
stream_dir_str(stream), stream_index(stream));
example_audio_tx_scheduler_on_sent(&tx_scheduler, user_data, TAG, name);
}
static esp_ble_audio_bap_stream_ops_t unicast_stream_ops = {
@@ -431,6 +489,8 @@ int cap_acceptor_unicast_init(struct peer_config *peer)
static bool cbs_registered;
esp_err_t err;
s_peer = peer;
if (cbs_registered == false) {
esp_ble_audio_bap_unicast_server_register_param_t param = {
CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT,
@@ -472,5 +532,7 @@ int cap_acceptor_unicast_init(struct peer_config *peer)
return -1;
}
ESP_LOGI(TAG, "CAP acceptor unicast initialized");
return 0;
}
@@ -134,7 +134,7 @@ static void ext_adv_start(void)
return;
}
ESP_LOGI(TAG, "Extended adv instance %u started", ADV_HANDLE);
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
}
esp_ble_audio_cap_stream_t *stream_alloc(esp_ble_audio_dir_t dir)
@@ -248,7 +248,7 @@ static void pa_sync(esp_ble_audio_gap_app_event_t *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",
ESP_LOGI(TAG, "PA sync lost: sync_handle %u reason 0x%02x",
event->pa_sync_lost.sync_handle, event->pa_sync_lost.reason);
broadcast_pa_lost(event);
@@ -258,24 +258,22 @@ static void pa_sync_lost(esp_ble_audio_gap_app_event_t *event)
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);
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]);
ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x",
event->acl_connect.conn_handle, 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",
ESP_LOGI(TAG, "Disconnected: handle %u reason 0x%02x",
event->acl_disconnect.conn_handle, event->acl_disconnect.reason);
peer.conn_handle = CONN_HANDLE_INIT;
@@ -312,10 +310,11 @@ static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event)
static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event)
{
uint16_t conn_handle = event->gatt_mtu_change.conn_handle;
int err;
ESP_LOGI(TAG, "gatt mtu change, conn_handle %u, mtu %u",
event->gatt_mtu_change.conn_handle, event->gatt_mtu_change.mtu);
ESP_LOGI(TAG, "MTU updated: handle %u mtu %u",
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",
@@ -327,19 +326,19 @@ static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event)
* - 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);
err = esp_ble_audio_gattc_disc_start(conn_handle);
if (err) {
ESP_LOGE(TAG, "Failed to start svc disc, err %d", err);
return;
}
ESP_LOGI(TAG, "Start discovering gatt services");
ESP_LOGI(TAG, "Service discovery started: handle %u", conn_handle);
}
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);
ESP_LOGI(TAG, "Service discovery complete: handle %u",
event->gattc_disc_cmpl.conn_handle);
}
static void iso_gatt_app_cb(esp_ble_audio_gatt_app_event_t *event)
@@ -6,6 +6,7 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=20
CONFIG_BT_NIMBLE_ISO=y
@@ -5,69 +5,127 @@
(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.
## Overview
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`.
This example implements the **Common Audio Profile (CAP) Initiator** role on top of the NimBLE host stack with ISO and LE Audio support. It is built in one of two mutually exclusive sub-modes selected at build time: a **CAP Unicast Initiator / BAP Unicast Client**, or a **CAP Broadcast Source**. A small TX module (`cap_initiator_tx.c`) drives audio packets onto every started stream using the LE Audio example TX scheduler.
In **unicast** mode, the initiator scans for connectable extended advertising that carries CAS service data, connects, initiates pairing, exchanges MTU, performs GATT service discovery, then discovers CAS, sink ASEs and source ASEs, configures the codec with the LC3 16_2_1 unicast preset, creates an ad-hoc unicast group, and starts the streams. In **broadcast** mode, the initiator creates a CAP broadcast source with the LC3 16_2_1 broadcast preset, starts non-connectable extended advertising plus periodic advertising carrying the Broadcast Audio Announcement UUID, a hardcoded broadcast ID (`0x123456`), the local device name (`CAP Broadcast Source`), and the BASE, then starts the broadcast stream.
The GAP/GATT device name follows the build-time mode: `CAP Initiator` in unicast mode, `CAP Broadcast Source` in broadcast mode (matching the local-name field embedded in extended advertising, which is what the acceptor's self-scan path matches against). Stream location and context are hardcoded to `MONO_AUDIO` / `UNSPECIFIED` in the LC3 preset.
## 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)
* A board with BLE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31)
* Peer device running the paired example
## How to Use Example
## Configuration
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:
Open menuconfig:
```bash
idf.py menuconfig
```
Under **Example: CAP Initiator**, select one of:
Under **Example: CAP Initiator** -> **CAP Initiator mode**:
* **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
* **Unicast** (`EXAMPLE_UNICAST`, default) — scan for a CAP Acceptor and set up unicast streams as BAP Unicast Client. Mutually exclusive with Broadcast.
* **Broadcast** (`EXAMPLE_BROADCAST`) — act as a CAP Broadcast Source; start extended + periodic advertising and a BIG so Broadcast Sinks can sync. Mutually exclusive with Unicast.
### Build and Flash
### Security & Pairing
Run the following to build, flash and monitor:
Just-Works pairing (LE Secure Connections, no MITM, `BLE_SM_IO_CAP_NO_IO`) with bonding enabled, inherited from `../../common_components/example_init/ble_audio_example_init.c`. Change pairing or IO-cap there.
## Build & Flash
```bash
idf.py set-target esp32h4
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.
(Exit serial monitor with `Ctrl-]`.)
## 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.
1. `app_main` initializes NVS, the Bluetooth controller (`bluetooth_init`) and the LE Audio common layer (`esp_ble_audio_common_init`); GAP/GATT callbacks are registered only in unicast mode.
2. The selected sub-mode initializes itself: `cap_initiator_unicast_init` registers CAP and BAP unicast-client callbacks, or `cap_initiator_broadcast_init` registers the broadcast stream ops.
3. `cap_initiator_tx_init` initializes the TX scheduler slot(s); `esp_ble_audio_common_start` starts the audio stack and the GAP device name is set per build-time mode (`CAP Initiator` for unicast, `CAP Broadcast Source` for broadcast).
4. **Unicast**: `cap_initiator_unicast_start` begins extended scanning. On a connectable advertising report containing CAS service data, it cancels scanning and calls `ble_gap_connect`.
5. **Unicast**: After ACL connect the initiator starts pairing, then on `SECURITY_CHANGE` exchanges MTU; on MTU change it starts GATT service discovery. Once both MTU and discovery are done, it calls `esp_ble_audio_cap_initiator_unicast_discover` (CAS), then BAP `discover` for SINK and SOURCE.
6. **Unicast**: It creates an ad-hoc unicast group with the LC3 16_2_1 QoS, then starts unicast audio. Stream callbacks log configured / qos_set / enabled / started; on start the sink (TX) stream is registered with the TX module.
7. **Broadcast**: `cap_initiator_broadcast_start` creates the broadcast source, configures and starts extended + periodic advertising (handle 0), adds the adv to the broadcast source, then starts the broadcast audio. On stream started the source stream is registered with the TX module.
8. On stream stopped/disconnected the TX module is unregistered; in unicast mode an ACL disconnect deletes the unicast group and re-arms scanning.
## Example Output
## Expected Log
TAG: `CAP_INI`.
### Unicast mode
```
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
...
I (xxx) CAP_INI: CAP initiator unicast initialized
I (xxx) CAP_INI: Scanning for CAP Acceptor...
I (xxx) CAP_INI: Found CAS in peer adv data!
I (xxx) CAP_INI: Connected: handle ... role ... peer ...
I (xxx) CAP_INI: Security: handle ... level ... bonded ...
I (xxx) CAP_INI: MTU updated: handle ... mtu ...
I (xxx) CAP_INI: Service discovery started: handle ...
I (xxx) CAP_INI: Service discovery complete: handle ...
I (xxx) CAP_INI: Discovering CAS
I (xxx) CAP_INI: Found CAS
I (xxx) CAP_INI: Discovering sinks
I (xxx) CAP_INI: [SNK] Endpoint discovered
I (xxx) CAP_INI: Discover sinks complete
I (xxx) CAP_INI: Discovering source ASEs
I (xxx) CAP_INI: [SRC] Endpoint discovered
I (xxx) CAP_INI: Discover sources complete
I (xxx) CAP_INI: Created unicast group
I (xxx) CAP_INI: Starting unicast streams
I (xxx) CAP_INI: [SNK #0] Stream configured, QoS preference:
I (xxx) CAP_INI: [SRC #0] Stream configured, QoS preference:
I (xxx) CAP_INI: [SNK #0] QoS set
I (xxx) CAP_INI: [SRC #0] QoS set
I (xxx) CAP_INI: [SNK #0] Stream enabled
I (xxx) CAP_INI: [SRC #0] Stream enabled
I (xxx) CAP_INI: Unicast start completed
I (xxx) CAP_INI: [SNK #0] Stream started
I (xxx) CAP_INI: [SNK #0] Started (SDU ... interval ... us)
I (xxx) CAP_INI: [SRC #0] Stream started
```
If the connection is lost:
On disconnect:
```
I (xxx) CAP_INI: connection disconnected, reason 0x...
I (xxx) CAP_INI: Disconnected: handle ... reason 0x...
I (xxx) CAP_INI: Deleted unicast group
I (xxx) CAP_INI: Scanning for CAP Acceptor...
```
### Broadcast mode
```
I (xxx) CAP_INI: CAP initiator broadcast initialized
I (xxx) CAP_INI: Creating broadcast source
I (xxx) CAP_INI: Advertising started (handle 0)
I (xxx) CAP_INI: [SRC #0] Stream started
I (xxx) CAP_INI: [SRC #0] Started (SDU ... interval ... us)
```
## Peer Pairing
Run the [acceptor](../acceptor/) on a second board. The initiator and acceptor must be configured for the **same sub-mode** — both `EXAMPLE_UNICAST`, or both `EXAMPLE_BROADCAST` — otherwise they will not pair.
### Unicast
1. Flash the acceptor with `EXAMPLE_UNICAST`; it advertises with CAS + ASCS service data.
2. Flash this initiator with `EXAMPLE_UNICAST`; it scans, finds CAS in the peer's adv data and connects.
3. Pairing, MTU exchange and GATT service discovery complete.
4. CAS, sink and source discovery complete; the initiator creates the unicast group with the LC3 16_2_1 preset.
5. Streams are configured, QoS-set, enabled and started; bidirectional audio begins.
6. On ACL disconnect the initiator deletes the unicast group and resumes scanning.
### Broadcast
1. Flash this initiator with `EXAMPLE_BROADCAST`; it advertises as `CAP Broadcast Source` (broadcast ID `0x123456`) and starts the BIG.
2. Flash the acceptor with `EXAMPLE_BROADCAST`. With `EXAMPLE_SCAN_SELF` set the acceptor scans for the broadcast source by name and broadcast ID; without it, a separate Broadcast Assistant must drive PA sync via BASS.
3. The acceptor PA-syncs, receives the BASE/BIGInfo and syncs the BIG.
4. The acceptor's `[SNK #0] Stream started` and the initiator's TX flow are now active.
@@ -1,5 +1,5 @@
# SPDX-FileCopyrightText: 2022 Nordic Semiconductor ASA
# SPDX-FileContributor: 2025 Espressif Systems (Shanghai) CO LTD
# SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
menu "Example: CAP Initiator"
@@ -30,6 +30,7 @@ struct tx_stream {
uint16_t seq_num;
uint8_t *data;
example_audio_tx_scheduler_t scheduler;
bool is_broadcast;
};
void cap_initiator_unicast_gap_cb(esp_ble_audio_gap_app_event_t *event);
@@ -46,7 +47,7 @@ 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_register_stream(esp_ble_audio_cap_stream_t *cap_stream, bool is_broadcast);
int cap_initiator_tx_unregister_stream(esp_ble_audio_cap_stream_t *cap_stream);
@@ -38,13 +38,13 @@ 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);
ESP_LOGI(TAG, "[SRC #0] Stream started");
cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream);
err = cap_initiator_tx_register_stream(cap_stream);
err = cap_initiator_tx_register_stream(cap_stream, true);
if (err) {
ESP_LOGE(TAG, "Failed to register stream %p for TX, err %d", stream, err);
ESP_LOGE(TAG, "[SRC #0] Failed to register TX, err %d", err);
}
}
@@ -52,7 +52,7 @@ static void broadcast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint
{
esp_ble_audio_cap_stream_t *cap_stream;
ESP_LOGI(TAG, "Broadcast stream %p stopped, reason 0x%02x", stream, reason);
ESP_LOGI(TAG, "[SRC #0] Stream stopped, reason 0x%02x", reason);
cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream);
@@ -63,7 +63,7 @@ static void broadcast_stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream,
{
esp_ble_audio_cap_stream_t *cap_stream;
ESP_LOGI(TAG, "Broadcast stream %p disconnected, reason 0x%02x", stream, reason);
ESP_LOGI(TAG, "[SRC #0] ISO disconnected, reason 0x%02x", reason);
cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream);
@@ -248,7 +248,7 @@ static int ext_adv_start(void)
goto end;
}
ESP_LOGI(TAG, "Extended adv instance %u started", ADV_HANDLE);
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
end:
if (ext_data) {
@@ -16,20 +16,41 @@
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)
static const char *cap_stream_tx_label(const esp_ble_audio_cap_stream_t *cap_stream)
{
esp_ble_audio_bap_ep_info_t ep_info = {0};
if (cap_stream == NULL || cap_stream->bap_stream.ep == NULL) {
/* Broadcast source streams do not expose an ep; always TX from SRC. */
return "SRC";
}
if (esp_ble_audio_bap_ep_get_info(cap_stream->bap_stream.ep, &ep_info) != 0) {
return "SRC";
}
return (ep_info.dir == ESP_BLE_AUDIO_DIR_SINK) ? "SNK" : "SRC";
}
static bool tx_stream_is_streaming(const struct tx_stream *tx_stream)
{
esp_ble_audio_bap_ep_info_t ep_info = {0};
int err;
if (cap_stream == NULL) {
if (tx_stream == NULL || tx_stream->stream == NULL) {
return false;
}
if (cap_stream->bap_stream.ep == NULL) {
/* Broadcast streams have no ep; registration from started_cb implies streaming. */
if (tx_stream->is_broadcast) {
return true;
}
if (tx_stream->stream->bap_stream.ep == NULL) {
return false;
}
err = esp_ble_audio_bap_ep_get_info(cap_stream->bap_stream.ep, &ep_info);
err = esp_ble_audio_bap_ep_get_info(tx_stream->stream->bap_stream.ep, &ep_info);
if (err) {
return false;
}
@@ -39,25 +60,28 @@ static bool cap_stream_is_streaming(const esp_ble_audio_cap_stream_t *cap_stream
static void cap_initiator_tx_send(struct tx_stream *tx_stream)
{
const char *label;
int err;
if (tx_stream == NULL || tx_stream->stream == NULL) {
return;
}
if (cap_stream_is_streaming(tx_stream->stream) == false) {
if (tx_stream_is_streaming(tx_stream) == false) {
return;
}
label = cap_stream_tx_label(tx_stream->stream);
if (tx_stream->stream->bap_stream.qos == NULL ||
tx_stream->stream->bap_stream.qos->sdu == 0) {
ESP_LOGE(TAG, "Invalid stream qos");
ESP_LOGE(TAG, "[%s] Invalid QoS", label);
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);
ESP_LOGE(TAG, "[%s] Buffer unavailable (SDU %u)",
label, tx_stream->stream->bap_stream.qos->sdu);
return;
}
@@ -67,8 +91,7 @@ static void cap_initiator_tx_send(struct tx_stream *tx_stream)
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);
ESP_LOGD(TAG, "[%s] send failed, err %d", label, err);
return;
}
@@ -79,7 +102,11 @@ void cap_initiator_tx_stream_sent(esp_ble_audio_bap_stream_t *stream, void *user
{
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);
char name[24];
snprintf(name, sizeof(name), "%s #%zu",
cap_stream_tx_label(tx_streams[i].stream), i);
example_audio_tx_scheduler_on_sent(&tx_streams[i].scheduler, user_data, TAG, name);
break;
}
}
@@ -92,7 +119,7 @@ static void tx_scheduler_cb(void *arg)
cap_initiator_tx_send(tx_stream);
}
int cap_initiator_tx_register_stream(esp_ble_audio_cap_stream_t *cap_stream)
int cap_initiator_tx_register_stream(esp_ble_audio_cap_stream_t *cap_stream, bool is_broadcast)
{
int err;
@@ -102,31 +129,41 @@ int cap_initiator_tx_register_stream(esp_ble_audio_cap_stream_t *cap_stream)
for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) {
if (tx_streams[i].stream == NULL) {
const char *label = cap_stream_tx_label(cap_stream);
if (cap_stream->bap_stream.qos == NULL || cap_stream->bap_stream.qos->sdu == 0) {
ESP_LOGE(TAG, "Invalid stream qos");
ESP_LOGE(TAG, "[%s #%zu] Invalid QoS", label, i);
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);
ESP_LOGE(TAG, "[%s #%zu] Failed to alloc buffer (SDU %u)",
label, i, cap_stream->bap_stream.qos->sdu);
return -ENOMEM;
}
}
tx_streams[i].stream = cap_stream;
tx_streams[i].is_broadcast = is_broadcast;
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);
ESP_LOGE(TAG, "[%s #%zu] Scheduler start failed, err %d",
label, i, err);
tx_streams[i].stream = NULL;
tx_streams[i].is_broadcast = false;
return err;
}
ESP_LOGI(TAG, "[%s #%zu] Started (SDU %u, interval %u us)",
label, i,
cap_stream->bap_stream.qos->sdu,
cap_stream->bap_stream.qos->interval);
cap_initiator_tx_send(&tx_streams[i]);
return 0;
}
@@ -147,18 +184,23 @@ int cap_initiator_tx_unregister_stream(esp_ble_audio_cap_stream_t *cap_stream)
for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) {
if (tx_streams[i].stream == cap_stream) {
const char *label = cap_stream_tx_label(tx_streams[i].stream);
err = example_audio_tx_scheduler_stop(&tx_streams[i].scheduler);
if (err) {
ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err);
ESP_LOGE(TAG, "[%s #%zu] Scheduler stop failed, err %d",
label, i, err);
return err;
}
tx_streams[i].stream = NULL;
tx_streams[i].is_broadcast = false;
if (tx_streams[i].data != NULL) {
free(tx_streams[i].data);
tx_streams[i].data = NULL;
}
ESP_LOGI(TAG, "[%s #%zu] Stopped", label, i);
return 0;
}
}
@@ -177,7 +219,7 @@ void cap_initiator_tx_init(void)
tx_scheduler_cb,
&tx_streams[i]);
if (err) {
ESP_LOGE(TAG, "Failed to initialize tx scheduler[%u], err %d", i, err);
ESP_LOGE(TAG, "Failed to initialize tx scheduler[%zu], err %d", i, err);
return;
}
}
@@ -47,6 +47,28 @@ static struct peer_config {
static example_audio_rx_metrics_t rx_metrics;
static const char *dir_str(esp_ble_audio_dir_t dir)
{
return dir == ESP_BLE_AUDIO_DIR_SINK ? "SNK" : "SRC";
}
static const char *stream_dir_str(const esp_ble_audio_bap_stream_t *stream)
{
if (stream == &peer.sink_stream.bap_stream) {
return "SNK";
} else if (stream == &peer.source_stream.bap_stream) {
return "SRC";
}
return "???";
}
static int stream_index(const esp_ble_audio_bap_stream_t *stream)
{
/* Only one sink and one source per peer in this example. */
(void)stream;
return 0;
}
static bool is_tx_stream(esp_ble_audio_bap_stream_t *stream)
{
esp_ble_audio_bap_ep_info_t ep_info = {0};
@@ -64,19 +86,22 @@ static bool is_tx_stream(esp_ble_audio_bap_stream_t *stream)
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);
ESP_LOGI(TAG, "[%s #%d] Stream configured, QoS preference:",
stream_dir_str(stream), stream_index(stream));
example_print_qos_pref(pref);
example_print_qos_pref(TAG, pref);
}
static void unicast_stream_qos_set_cb(esp_ble_audio_bap_stream_t *stream)
{
ESP_LOGI(TAG, "Unicast stream %p QoS set", stream);
ESP_LOGI(TAG, "[%s #%d] QoS set",
stream_dir_str(stream), stream_index(stream));
}
static void unicast_stream_enabled_cb(esp_ble_audio_bap_stream_t *stream)
{
ESP_LOGI(TAG, "Unicast stream %p enabled", stream);
ESP_LOGI(TAG, "[%s #%d] Stream enabled",
stream_dir_str(stream), stream_index(stream));
}
static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream)
@@ -84,35 +109,40 @@ 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);
ESP_LOGI(TAG, "[%s #%d] Stream started",
stream_dir_str(stream), stream_index(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);
err = cap_initiator_tx_register_stream(cap_stream, false);
if (err) {
ESP_LOGE(TAG, "Failed to register stream %p for TX, err %d", stream, err);
ESP_LOGE(TAG, "[%s #%d] Failed to register TX, err %d",
stream_dir_str(stream), stream_index(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);
ESP_LOGI(TAG, "[%s #%d] Metadata updated",
stream_dir_str(stream), stream_index(stream));
}
static void unicast_stream_disabled_cb(esp_ble_audio_bap_stream_t *stream)
{
ESP_LOGI(TAG, "Unicast stream %p disabled", stream);
ESP_LOGI(TAG, "[%s #%d] Stream disabled",
stream_dir_str(stream), stream_index(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);
ESP_LOGI(TAG, "[%s #%d] Stream stopped, reason 0x%02x",
stream_dir_str(stream), stream_index(stream), reason);
if (is_tx_stream(stream)) {
cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream);
@@ -125,7 +155,8 @@ static void unicast_stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, u
{
esp_ble_audio_cap_stream_t *cap_stream;
ESP_LOGI(TAG, "Unicast stream %p disconnected, reason 0x%02x", stream, reason);
ESP_LOGI(TAG, "[%s #%d] ISO disconnected, reason 0x%02x",
stream_dir_str(stream), stream_index(stream), reason);
if (is_tx_stream(stream)) {
cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream);
@@ -136,19 +167,20 @@ static void unicast_stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, u
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);
}
ESP_LOGI(TAG, "[%s #%d] Stream released",
stream_dir_str(stream), stream_index(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)
{
char name[24];
snprintf(name, sizeof(name), "%s #%d",
stream_dir_str(stream), stream_index(stream));
rx_metrics.last_sdu_len = len;
example_audio_rx_metrics_on_recv(info, &rx_metrics, TAG, "stream", stream);
example_audio_rx_metrics_on_recv(info, &rx_metrics, TAG, name);
}
static void unicast_stream_sent_cb(esp_ble_audio_bap_stream_t *stream, void *user_data)
@@ -357,7 +389,7 @@ 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);
example_print_codec_cap(TAG, codec_cap);
}
static void endpoint_cb(esp_ble_conn_t *conn,
@@ -366,12 +398,12 @@ static void endpoint_cb(esp_ble_conn_t *conn,
{
if (dir == ESP_BLE_AUDIO_DIR_SOURCE) {
if (peer.source_ep == NULL) {
ESP_LOGI(TAG, "Source ep: %p", ep);
ESP_LOGI(TAG, "[%s] Endpoint discovered", dir_str(dir));
peer.source_ep = ep;
}
} else if (dir == ESP_BLE_AUDIO_DIR_SINK) {
if (peer.sink_ep == NULL) {
ESP_LOGI(TAG, "Sink ep: %p", ep);
ESP_LOGI(TAG, "[%s] Endpoint discovered", dir_str(dir));
peer.sink_ep = ep;
}
}
@@ -398,7 +430,7 @@ static void unicast_discovery_complete_cb(esp_ble_conn_t *conn, int err,
return;
}
ESP_LOGI(TAG, "Found CAS with CSIS %p", csis_inst);
ESP_LOGI(TAG, "Found CAS with CSIS");
/* TODO: Do set member discovery */
} else {
@@ -529,30 +561,28 @@ 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);
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]);
ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x",
event->acl_connect.conn_handle, 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;
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",
ESP_LOGI(TAG, "Disconnected: handle %u reason 0x%02x",
event->acl_disconnect.conn_handle, event->acl_disconnect.reason);
peer.conn_handle = CONN_HANDLE_INIT;
@@ -573,19 +603,13 @@ 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);
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]);
ESP_LOGI(TAG, "Security: handle %u level %u bonded %u",
event->security_change.conn_handle, event->security_change.sec_level,
event->security_change.bonded);
err = exchange_mtu(event->security_change.conn_handle);
if (err) {
@@ -616,10 +640,11 @@ void cap_initiator_unicast_gap_cb(esp_ble_audio_gap_app_event_t *event)
static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event)
{
uint16_t conn_handle = event->gatt_mtu_change.conn_handle;
int err;
ESP_LOGI(TAG, "gatt mtu change, conn_handle %u, mtu %u",
event->gatt_mtu_change.conn_handle, event->gatt_mtu_change.mtu);
ESP_LOGI(TAG, "MTU updated: handle %u mtu %u",
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",
@@ -627,13 +652,13 @@ static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event)
return;
}
err = esp_ble_audio_gattc_disc_start(event->gatt_mtu_change.conn_handle);
err = esp_ble_audio_gattc_disc_start(conn_handle);
if (err) {
ESP_LOGE(TAG, "Failed to start svc disc, err %d", err);
return;
}
ESP_LOGI(TAG, "Start discovering gatt services");
ESP_LOGI(TAG, "Service discovery started: handle %u", conn_handle);
/* Note:
* MTU exchanged event may arrived after discover completed event.
@@ -647,8 +672,8 @@ static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event)
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);
ESP_LOGI(TAG, "Service discovery complete: handle %u",
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);
@@ -702,7 +727,7 @@ int cap_initiator_unicast_start(void)
return err;
}
ESP_LOGI(TAG, "Extended scan started");
ESP_LOGI(TAG, "Scanning for CAP Acceptor...");
return 0;
}
@@ -68,7 +68,16 @@ void app_main(void)
return;
}
/* Match the GAP device name to the role this build was compiled for, so
* the name advertised in GAP reads matches the local-name field embedded
* in extended advertising (broadcast mode uses "CAP Broadcast Source",
* which is what the acceptor's self-scan path matches against).
*/
#if CONFIG_EXAMPLE_BROADCAST
err = ble_svc_gap_device_name_set("CAP Broadcast Source");
#else
err = ble_svc_gap_device_name_set("CAP Initiator");
#endif
if (err) {
ESP_LOGE(TAG, "Failed to set device name, err %d", err);
return;
@@ -6,6 +6,7 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=20
CONFIG_BT_NIMBLE_ISO=y
@@ -21,32 +21,169 @@
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"
#define TAG "INIT"
#define TAG "EXAMPLE_INIT"
static SemaphoreHandle_t example_audio_sem;
void ble_store_config_init(void);
/* Names follow BT SIG Assigned Numbers:
* https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/uuids/
*/
static const char *example_audio_uuid16_name(const ble_uuid_t *uuid)
{
if (uuid->type != BLE_UUID_TYPE_16) {
return NULL;
}
switch (ble_uuid_u16(uuid)) {
/* GAP / GATT */
case 0x1800: return "GAP";
case 0x1801: return "GATT";
case 0x2a00: return "Device Name";
case 0x2a01: return "Appearance";
case 0x2a05: return "Service Changed";
case 0x2b29: return "Client Sup Features";
case 0x2b3a: return "Server Sup Features";
/* LE Audio services */
case 0x1843: return "AICS";
case 0x1844: return "VCS";
case 0x1845: return "VOCS";
case 0x1846: return "CSIS";
case 0x1848: return "MCS";
case 0x1849: return "GMCS";
case 0x184b: return "TBS";
case 0x184c: return "GTBS";
case 0x184d: return "MICS";
case 0x184e: return "ASCS";
case 0x184f: return "BASS";
case 0x1850: return "PACS";
case 0x1851: return "Basic Audio Ann";
case 0x1852: return "Broadcast Audio Ann";
case 0x1853: return "CAS";
case 0x1854: return "HAS";
case 0x1855: return "TMAS";
case 0x1856: return "Public Broadcast Ann";
case 0x1858: return "GMAS";
/* Common */
case 0x2b51: return "TMAP Role";
case 0x2bba: return "CCID";
/* AICS chars */
case 0x2b77: return "AICS State";
case 0x2b78: return "AICS Gain Settings";
case 0x2b79: return "AICS Input Type";
case 0x2b7a: return "AICS Input Status";
case 0x2b7b: return "AICS Control";
case 0x2b7c: return "AICS Description";
/* VCS chars */
case 0x2b7d: return "VCS State";
case 0x2b7e: return "VCS Control";
case 0x2b7f: return "VCS Flags";
/* VOCS chars */
case 0x2b80: return "VOCS State";
case 0x2b81: return "VOCS Location";
case 0x2b82: return "VOCS Control";
case 0x2b83: return "VOCS Description";
/* CSIS chars */
case 0x2b84: return "CSIS SIRK";
case 0x2b85: return "CSIS Set Size";
case 0x2b86: return "CSIS Set Lock";
case 0x2b87: return "CSIS Rank";
/* MCS chars */
case 0x2b93: return "MCS Player Name";
case 0x2b94: return "MCS Icon Obj ID";
case 0x2b95: return "MCS Icon URL";
case 0x2b96: return "MCS Track Changed";
case 0x2b97: return "MCS Track Title";
case 0x2b98: return "MCS Track Duration";
case 0x2b99: return "MCS Track Position";
case 0x2b9a: return "MCS Playback Speed";
case 0x2b9b: return "MCS Seeking Speed";
case 0x2b9c: return "MCS Track Seg Obj ID";
case 0x2b9d: return "MCS Cur Track Obj ID";
case 0x2b9e: return "MCS Next Track Obj ID";
case 0x2b9f: return "MCS Parent Grp Obj ID";
case 0x2ba0: return "MCS Cur Grp Obj ID";
case 0x2ba1: return "MCS Playing Order";
case 0x2ba2: return "MCS Playing Orders";
case 0x2ba3: return "MCS Media State";
case 0x2ba4: return "MCS Media CP";
case 0x2ba5: return "MCS Media CP Opcodes";
case 0x2ba6: return "MCS Search Res Obj ID";
case 0x2ba7: return "MCS Search CP";
/* TBS chars */
case 0x2bb3: return "TBS Provider Name";
case 0x2bb4: return "TBS UCI";
case 0x2bb5: return "TBS Technology";
case 0x2bb6: return "TBS URI List";
case 0x2bb7: return "TBS Signal Strength";
case 0x2bb8: return "TBS Signal Interval";
case 0x2bb9: return "TBS List Cur Calls";
case 0x2bbb: return "TBS Status Flags";
case 0x2bbc: return "TBS Incoming URI";
case 0x2bbd: return "TBS Call State";
case 0x2bbe: return "TBS Call CP";
case 0x2bbf: return "TBS Optional Opcodes";
case 0x2bc0: return "TBS Term Reason";
case 0x2bc1: return "TBS Incoming Call";
case 0x2bc2: return "TBS Friendly Name";
/* MICS / ASCS / BASS chars */
case 0x2bc3: return "MICS Mute";
case 0x2bc4: return "ASCS ASE Snk";
case 0x2bc5: return "ASCS ASE Src";
case 0x2bc6: return "ASCS ASE CP";
case 0x2bc7: return "BASS CP";
case 0x2bc8: return "BASS Recv State";
/* PACS chars */
case 0x2bc9: return "PACS Snk";
case 0x2bca: return "PACS Snk Loc";
case 0x2bcb: return "PACS Src";
case 0x2bcc: return "PACS Src Loc";
case 0x2bcd: return "PACS Avail Ctx";
case 0x2bce: return "PACS Sup Ctx";
default: return NULL;
}
}
static void example_audio_gatt_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg)
{
char buf[BLE_UUID_STR_LEN];
const char *name;
switch (ctxt->op) {
case BLE_GATT_REGISTER_OP_SVC:
ESP_LOGI(TAG, "Register svc %s, handle %u",
name = example_audio_uuid16_name(ctxt->svc.svc_def->uuid);
ESP_LOGI(TAG, "svc %-20s (%s), handle %u",
name ? name : "?",
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",
name = example_audio_uuid16_name(ctxt->chr.chr_def->uuid);
ESP_LOGI(TAG, "chr %-20s (%s), handles %u/%u",
name ? name : "?",
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);
name = example_audio_uuid16_name(ctxt->dsc.dsc_def->uuid);
ESP_LOGI(TAG, "dsc %-20s (%s), handle %u",
name ? name : "?",
ble_uuid_to_str(ctxt->dsc.dsc_def->uuid, buf),
ctxt->dsc.handle);
break;
default:
@@ -103,14 +240,12 @@ esp_err_t bluetooth_init(void)
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_our_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
ble_hs_cfg.sm_their_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
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();
@@ -18,11 +18,14 @@
#include "esp_ble_audio_common_api.h"
#define TAG "UTILS"
#define LOG_GREEN "\033[0;" "32" "m"
#define LOG_RESET "\033[0m"
/* Packet count between throughput summary logs.
* At 10 ms SDU interval this is ~60 s; at 7.5 ms ~45 s.
*/
#define LOG_INTERVAL_PACKETS 6000
int example_audio_gap_event_cb(struct ble_gap_event *event, void *arg)
{
if (event->type == BLE_GAP_EVENT_EXT_DISC ||
@@ -50,148 +53,363 @@ static void print_hex(const uint8_t *ptr, size_t len)
}
}
static bool print_cb(uint8_t type, const uint8_t *data,
uint8_t data_len, void *user_data)
static void append_audio_contexts(char *buf, size_t buf_size, uint16_t ctx_bitmap)
{
const char *str = (const char *)user_data;
static const char *const names[] = {
"Unspecified", "Conversational", "Media", "Game",
"Instructional", "Voice Assistants", "Live", "Sound Effects",
"Notifications", "Ringtone", "Alerts", "Emergency Alarm",
};
size_t pos = 0;
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");
buf[0] = '\0';
if (ctx_bitmap == 0) {
snprintf(buf, buf_size, "None");
return;
}
for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); i++) {
if (ctx_bitmap & (1U << i)) {
int n = snprintf(buf + pos, buf_size - pos, "%s%s",
pos == 0 ? "" : ", ", names[i]);
if (n < 0 || (size_t)n >= buf_size - pos) {
break;
}
pos += n;
}
}
}
static void copy_to_cstr(char *dst, size_t dst_size, const uint8_t *src, uint8_t src_len)
{
size_t n = (src_len < dst_size - 1) ? src_len : dst_size - 1;
memcpy(dst, src, n);
dst[n] = '\0';
}
static void phy_to_str(char *buf, size_t buf_size, uint8_t phy)
{
size_t pos = 0;
buf[0] = '\0';
if (phy & ESP_BLE_ISO_PHY_1M) {
pos += snprintf(buf + pos, buf_size - pos, "1M");
}
if (phy & ESP_BLE_ISO_PHY_2M) {
pos += snprintf(buf + pos, buf_size - pos, "%s2M", pos ? " " : "");
}
if (phy & ESP_BLE_ISO_PHY_CODED) {
pos += snprintf(buf + pos, buf_size - pos, "%sCoded", pos ? " " : "");
}
if (pos == 0) {
snprintf(buf, buf_size, "0x%02x", phy);
}
}
static bool print_meta_cb(uint8_t type, const uint8_t *data,
uint8_t data_len, void *user_data)
{
const char *tag = (const char *)user_data;
char buf[128];
switch (type) {
case ESP_BLE_AUDIO_METADATA_TYPE_PREF_CONTEXT:
case ESP_BLE_AUDIO_METADATA_TYPE_STREAM_CONTEXT: {
uint16_t ctx = (data_len >= 2)
? (uint16_t)data[0] | ((uint16_t)data[1] << 8)
: 0;
append_audio_contexts(buf, sizeof(buf), ctx);
ESP_LOGI(tag, "| %-19s: %s",
type == ESP_BLE_AUDIO_METADATA_TYPE_PREF_CONTEXT
? "Preferred Context" : "Streaming Context",
buf);
break;
}
case ESP_BLE_AUDIO_METADATA_TYPE_PROGRAM_INFO:
copy_to_cstr(buf, sizeof(buf), data, data_len);
ESP_LOGI(tag, "| %-19s: %s", "Program Info", buf);
break;
case ESP_BLE_AUDIO_METADATA_TYPE_LANG:
copy_to_cstr(buf, sizeof(buf), data, data_len);
ESP_LOGI(tag, "| %-19s: %s", "Language", buf);
break;
case ESP_BLE_AUDIO_METADATA_TYPE_CCID_LIST: {
size_t pos = 0;
buf[0] = '\0';
for (uint8_t i = 0; i < data_len; i++) {
int n = snprintf(buf + pos, sizeof(buf) - pos, "%s%u",
pos == 0 ? "" : " ", data[i]);
if (n < 0 || (size_t)n >= sizeof(buf) - pos) {
break;
}
pos += n;
}
ESP_LOGI(tag, "| %-19s: %s", "CCID List", buf);
break;
}
case ESP_BLE_AUDIO_METADATA_TYPE_PARENTAL_RATING:
ESP_LOGI(tag, "| %-19s: %u", "Parental Rating",
data_len >= 1 ? data[0] : 0);
break;
case ESP_BLE_AUDIO_METADATA_TYPE_PROGRAM_INFO_URI:
copy_to_cstr(buf, sizeof(buf), data, data_len);
ESP_LOGI(tag, "| %-19s: %s", "Program Info URI", buf);
break;
case ESP_BLE_AUDIO_METADATA_TYPE_AUDIO_STATE:
ESP_LOGI(tag, "| %-19s: 0x%02x", "Audio Active State",
data_len >= 1 ? data[0] : 0);
break;
case ESP_BLE_AUDIO_METADATA_TYPE_BROADCAST_IMMEDIATE:
ESP_LOGI(tag, "| %-19s: (set)", "Broadcast Immediate");
break;
case ESP_BLE_AUDIO_METADATA_TYPE_ASSISTED_LISTENING_STREAM:
ESP_LOGI(tag, "| %-19s: 0x%02x", "Assisted Listening",
data_len >= 1 ? data[0] : 0);
break;
case ESP_BLE_AUDIO_METADATA_TYPE_BROADCAST_NAME:
copy_to_cstr(buf, sizeof(buf), data, data_len);
ESP_LOGI(tag, "| %-19s: %s", "Broadcast Name", buf);
break;
case ESP_BLE_AUDIO_METADATA_TYPE_EXTENDED:
case ESP_BLE_AUDIO_METADATA_TYPE_VENDOR:
default:
printf(LOG_GREEN "I (%lu) %s: | meta[type 0x%02x len %u]: " LOG_RESET,
esp_log_timestamp(), tag, type, data_len);
print_hex(data, data_len);
printf("\n");
break;
}
return true;
}
void example_print_codec_cfg(const esp_ble_audio_codec_cfg_t *codec_cfg)
void example_print_meta(const char *tag, const uint8_t *meta, size_t meta_len)
{
esp_err_t err;
ESP_LOGI(TAG, "codec_cfg, id 0x%02x cid 0x%04x vid 0x%04x count %u",
if (meta_len == 0) {
return;
}
ESP_LOGI(tag, "| Metadata:");
err = esp_ble_audio_data_parse(meta, meta_len, print_meta_cb, (void *)tag);
if (err) {
ESP_LOGE(tag, "Failed to parse meta");
}
}
static void print_raw_data(const char *tag, const uint8_t *data, size_t data_len)
{
printf(LOG_GREEN "I (%lu) %s: | data: " LOG_RESET, esp_log_timestamp(), tag);
print_hex(data, data_len);
printf("\n");
}
static void print_codec_cfg_lc3(const char *tag, const esp_ble_audio_codec_cfg_t *codec_cfg)
{
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;
esp_err_t err;
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, "| %-19s: %lu Hz", "Frequency", 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, "| %-19s: %lu us", "Frame Duration", 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, "| %-19s: 0x%08lx", "Channel allocation", 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, "| %-19s: %u bytes", "Octets per frame", 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, "| %-19s: %u", "Frames per SDU", frame_blocks);
}
static void print_codec_cap_lc3(const char *tag, const esp_ble_audio_codec_cap_t *codec_cap)
{
static const uint16_t freq_khz[] = {
8, 11, 16, 22, 24, 32, 44, 48, 88, 96, 176, 192, 384,
};
esp_ble_audio_codec_cap_freq_t freq;
esp_ble_audio_codec_cap_frame_dur_t frame_dur;
esp_ble_audio_codec_cap_chan_count_t chan_count;
esp_ble_audio_codec_octets_per_codec_frame_t octets;
uint8_t max_frames;
char buf[64];
int pos;
esp_err_t err;
err = esp_ble_audio_codec_cap_get_freq(codec_cap, &freq);
if (err) {
ESP_LOGE(tag, "Failed to get supported frequencies");
return;
}
pos = 0;
buf[0] = '\0';
for (size_t i = 0; i < sizeof(freq_khz) / sizeof(freq_khz[0]); i++) {
if (freq & (1U << i)) {
int n = snprintf(buf + pos, sizeof(buf) - pos, "%s%u",
pos == 0 ? "" : " ", freq_khz[i]);
if (n < 0 || (size_t)n >= sizeof(buf) - pos) {
break;
}
pos += n;
}
}
ESP_LOGI(tag, "| %-19s: %s kHz", "Frequencies", buf);
err = esp_ble_audio_codec_cap_get_frame_dur(codec_cap, &frame_dur);
if (err) {
ESP_LOGE(tag, "Failed to get supported frame durations");
return;
}
pos = 0;
buf[0] = '\0';
if (frame_dur & ESP_BLE_AUDIO_CODEC_CAP_DURATION_7_5) {
pos += snprintf(buf + pos, sizeof(buf) - pos, "7.5");
}
if (frame_dur & ESP_BLE_AUDIO_CODEC_CAP_DURATION_10) {
pos += snprintf(buf + pos, sizeof(buf) - pos, "%s10",
pos == 0 ? "" : " ");
}
ESP_LOGI(tag, "| %-19s: %s ms", "Frame Durations", buf);
err = esp_ble_audio_codec_cap_get_supported_audio_chan_counts(codec_cap, &chan_count, true);
if (err) {
ESP_LOGE(tag, "Failed to get supported channel counts");
return;
}
pos = 0;
buf[0] = '\0';
for (size_t i = 0; i < 8; i++) {
if (chan_count & (1U << i)) {
int n = snprintf(buf + pos, sizeof(buf) - pos, "%s%u",
pos == 0 ? "" : " ", (unsigned)(i + 1));
if (n < 0 || (size_t)n >= sizeof(buf) - pos) {
break;
}
pos += n;
}
}
ESP_LOGI(tag, "| %-19s: %s", "Channel Counts", buf);
err = esp_ble_audio_codec_cap_get_octets_per_frame(codec_cap, &octets);
if (err) {
ESP_LOGE(tag, "Failed to get supported octets per frame");
return;
}
ESP_LOGI(tag, "| %-19s: %u-%u bytes", "Octets per Frame", octets.min, octets.max);
err = esp_ble_audio_codec_cap_get_max_codec_frames_per_sdu(codec_cap, &max_frames, true);
if (err) {
ESP_LOGE(tag, "Failed to get max frames per SDU");
return;
}
ESP_LOGI(tag, "| %-19s: %u", "Max Frames per SDU", max_frames);
}
void example_print_codec_cfg(const char *tag, const esp_ble_audio_codec_cfg_t *codec_cfg)
{
ESP_LOGI(tag, "Codec Configuration:");
ESP_LOGI(tag, "| id 0x%02x cid 0x%04x vid 0x%04x len %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);
print_codec_cfg_lc3(tag, codec_cfg);
} else {
print_hex(codec_cfg->data, codec_cfg->data_len);
print_raw_data(tag, 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;
}
example_print_meta(tag, codec_cfg->meta, codec_cfg->meta_len);
}
void example_print_codec_cap(const esp_ble_audio_codec_cap_t *codec_cap)
void example_print_codec_cap(const char *tag, 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",
ESP_LOGI(tag, "Codec Capability:");
ESP_LOGI(tag, "| id 0x%02x cid 0x%04x vid 0x%04x len %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;
}
print_codec_cap_lc3(tag, codec_cap);
} 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");
print_raw_data(tag, codec_cap->data, codec_cap->data_len);
}
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;
}
example_print_meta(tag, codec_cap->meta, codec_cap->meta_len);
}
void example_print_qos(const esp_ble_audio_bap_qos_cfg_t *qos)
/* Prints QoS fields only. Caller is expected to print a context header
* ending with ':' so the '| ' lines below naturally attach to that header.
*/
void example_print_qos(const char *tag, 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);
char phy_str[16];
phy_to_str(phy_str, sizeof(phy_str), qos->phy);
ESP_LOGI(tag, "| %-8s: %u us", "interval", qos->interval);
ESP_LOGI(tag, "| %-8s: %s", "framing", qos->framing ? "framed" : "unframed");
ESP_LOGI(tag, "| %-8s: %s", "phy", phy_str);
ESP_LOGI(tag, "| %-8s: %u bytes", "sdu", qos->sdu);
ESP_LOGI(tag, "| %-8s: %u retransmission(s)", "rtn", qos->rtn);
ESP_LOGI(tag, "| %-8s: %u ms", "latency", qos->latency);
ESP_LOGI(tag, "| %-8s: %u us", "pd", qos->pd);
}
void example_print_qos_pref(const esp_ble_audio_bap_qos_cfg_pref_t *pref)
/* Prints QoS Preference fields only. Same expectation as example_print_qos. */
void example_print_qos_pref(const char *tag, 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);
char phy_str[16];
phy_to_str(phy_str, sizeof(phy_str), pref->phy);
ESP_LOGI(tag, "| %-11s: %s", "unframed",
pref->unframed_supported ? "supported" : "not supported");
ESP_LOGI(tag, "| %-11s: %s", "phy", phy_str);
ESP_LOGI(tag, "| %-11s: %u retransmission(s)", "rtn", pref->rtn);
ESP_LOGI(tag, "| %-11s: %u ms", "latency", pref->latency);
ESP_LOGI(tag, "| %-11s: %u us", "pd_min", pref->pd_min);
ESP_LOGI(tag, "| %-11s: %u us", "pd_max", pref->pd_max);
ESP_LOGI(tag, "| %-11s: %u us", "pref_pd_min", pref->pref_pd_min);
ESP_LOGI(tag, "| %-11s: %u us", "pref_pd_max", pref->pref_pd_max);
}
bool example_is_substring(const char *substr, const char *str)
@@ -276,8 +494,7 @@ 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)
const char *obj_name)
{
size_t drift_count = 0;
@@ -293,9 +510,9 @@ void example_audio_tx_scheduler_on_sent(example_audio_tx_scheduler_t *scheduler,
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);
if (scheduler->count % LOG_INTERVAL_PACKETS == 0) {
ESP_LOGI(tag, "[%s] TX: %u packets",
obj_name ? obj_name : "ISO", scheduler->count);
}
}
@@ -314,8 +531,7 @@ 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)
const char *obj_name)
{
assert(info);
assert(metrics);
@@ -337,8 +553,8 @@ void example_audio_rx_metrics_on_recv(const esp_ble_iso_recv_info_t *info,
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);
if (metrics->recv_count % LOG_INTERVAL_PACKETS == 0) {
ESP_LOGI(tag, "[%s] RX: %u packets",
obj_name ? obj_name : "ISO", metrics->recv_count);
}
}
@@ -53,13 +53,15 @@
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_cfg(const char *tag, 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_codec_cap(const char *tag, 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_meta(const char *tag, const uint8_t *meta, size_t meta_len);
void example_print_qos_pref(const esp_ble_audio_bap_qos_cfg_pref_t *pref);
void example_print_qos(const char *tag, const esp_ble_audio_bap_qos_cfg_t *qos);
void example_print_qos_pref(const char *tag, const esp_ble_audio_bap_qos_cfg_pref_t *pref);
bool example_is_substring(const char *substr, const char *str);
@@ -104,15 +106,13 @@ 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);
const char *obj_name);
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);
const char *obj_name);
#endif /* BLE_AUDIO_EXAMPLE_UTILS_H_ */
@@ -5,58 +5,102 @@
(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).
## Overview
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.
This example implements the **Telephony and Media Audio Profile (TMAP) Broadcast Media Receiver (BMR)** role. The device scans for non-connectable extended advertisers that carry both the Broadcast Audio Announcement service (with a 24-bit broadcast ID) and the TMAS service with the **BMS** role bit set, then synchronizes to periodic advertising and the BIG to receive broadcast audio.
The implementation runs on the **NimBLE** host stack and uses ESP-BLE-AUDIO for TMAP role registration, the BAP broadcast sink, the BAP scan delegator, the PACS sink (LC3 capability: 48 kHz, 10 ms, 1 channel, 100 octets/frame, media context — sized for the 48_2_1 preset that the paired BMS sends), and a VCP **Volume Renderer** (initial volume 10, unmuted, step 1). Up to `CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT` streams are pre-allocated and reused for every BIG sync; received SDUs are accounted for via `example_audio_rx_metrics_*`.
## 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)
* A board with BLE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31)
* Peer device running the paired example
## How to Use Example
## Configuration
Before project configuration and build, set the correct chip target:
```bash
idf.py menuconfig
```
No build-time options — runtime defaults are baked into source.
### Security & Pairing
NimBLE host security is inherited from `../../common_components/example_init/ble_audio_example_init.c`: LE Secure Connections with bonding enabled, no MITM, **Just-Works** pairing (`BLE_SM_IO_CAP_NO_IO`). The BMR receives broadcast (unencrypted) audio, so these settings only apply if a peer (e.g. a Broadcast Assistant) opens an ATT connection to the scan delegator / VCP renderer.
## Build & Flash
```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.
(Exit serial monitor with `Ctrl-]`.)
## 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.
1. Initialize NVS, the Bluetooth controller/host, and the LE Audio common layer with a GAP app callback that handles `EXT_SCAN_RECV`, `PA_SYNC`, and `PA_SYNC_LOST`.
2. Register the TMAP role as `ESP_BLE_AUDIO_TMAP_ROLE_BMR`, then initialize the VCP Volume Renderer (`vcp_vol_renderer_init`) and the BAP broadcast sink (`bap_broadcast_sink_init`: PACS sink + sink location, broadcast sink callbacks, LC3 sink capability, scan delegator, stream ops on each pre-allocated stream).
3. Start the audio stack and begin passive extended scanning at 100 ms / 100 ms with `BLE_HS_FOREVER` duration.
4. On each scan result, parse service-data: capture broadcast ID from Broadcast Audio Announcement, set `tmap_bms_found` when TMAS carries the BMS role; if both are present and no PA sync is in progress, create the periodic-advertising sync.
5. On `PA_SYNC` success, cancel scanning, log the sync handle, and create the broadcast sink for the sync handle and broadcast ID.
6. The `base_recv` callback extracts the BIS index bitfield masked by the available stream count; the `syncable` callback then calls `esp_ble_audio_bap_broadcast_sink_sync` with the stream pointer array.
7. Per-stream `started` callback resets RX metrics; `recv` callback feeds each SDU into `example_audio_rx_metrics_on_recv` (tracking valid/error/lost/zero-length counts); `stopped` logs the reason.
8. On `PA_SYNC_LOST` matching the active sync handle, delete the broadcast sink and re-enter scanning.
## Example Output
## Expected Log
TAG is `TMAP_BMR`.
Init:
```
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: VCP volume renderer initialized
I (xxx) TMAP_BMR: BAP broadcast sink initialized
I (xxx) TMAP_BMR: Scanning for broadcaster...
```
Scan and PA sync:
```
I (xxx) TMAP_BMR: Found TMAP BMS, starting PA sync (broadcast ID 0x......)
I (xxx) TMAP_BMR: PA synced: handle .. sid .. phy .. peer xx:xx:xx:xx:xx:xx
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:
Streaming:
```
I (xxx) TMAP_BMR: [SNK #0] Stream started
```
(Subsequent per-packet metrics are emitted by `example_audio_rx_metrics_on_recv` under the `SNK #N` label.)
Stop / sync loss:
```
I (xxx) TMAP_BMR: PA sync lost, handle 0x... reason ...
I (xxx) TMAP_BMR: [SNK #0] Stream stopped, reason 0x..
I (xxx) TMAP_BMR: PA sync lost: sync_handle .. reason 0x..
I (xxx) TMAP_BMR: PA sync .. lost with reason ..
```
VCP (when remote queries volume/flags):
```
I (xxx) TMAP_BMR: VCS volume .., mute ..
I (xxx) TMAP_BMR: VCS flags 0x..
```
Warnings (malformed adv):
```
W (xxx) TMAP_BMR: Invalid ad size .. (uuid)
W (xxx) TMAP_BMR: Invalid ad size .. (Broadcast ID)
W (xxx) TMAP_BMR: Invalid ad size .. (tmap role)
```
## Peer Pairing
Run [bms](../bms/) on a second board.
1. Flash and boot this BMR; it initializes VCP and PACS, then enters extended scanning.
2. Boot the BMS peer; it advertises with TMAS BMS role and broadcast ID `0x123456` plus periodic advertising carrying the BASE.
3. This BMR matches both service-data entries on a single scan result and creates the PA sync; on success it cancels scanning and creates the broadcast sink.
4. `base_recv` derives the BIS index bitfield, then `syncable` triggers `esp_ble_audio_bap_broadcast_sink_sync` with the pre-allocated stream pointers.
5. Stream `started` fires, RX metrics reset, and `recv` accumulates valid/error/lost/zero-length SDU counters as the BMS sends.
6. If the BMS stops or moves out of range, PA sync loss tears down the sink and the BMR resumes scanning automatically.
@@ -8,6 +8,7 @@
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <errno.h>
#include "tmap_bmr.h"
@@ -34,9 +35,11 @@ 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 */
ESP_BLE_AUDIO_CODEC_CAP_CHAN_COUNT_SUPPORT(1), /* Supported channels 1 (mono) */
/* SDU size range covers the 48_2_1 LC3 broadcast preset (100 octets/frame)
* that the paired BMS sends. */
100, /* Minimum 100 octets per frame */
100, /* Maximum 100 octets per frame */
1); /* Maximum 1 codec frame per SDU */
static uint8_t codec_meta[] =
@@ -54,25 +57,38 @@ static uint32_t bis_index_bitfield;
static example_audio_rx_metrics_t rx_metrics;
static int stream_index(const esp_ble_audio_bap_stream_t *stream)
{
for (size_t i = 0; i < ARRAY_SIZE(streams); i++) {
if (&streams[i] == stream) {
return (int)i;
}
}
return -1;
}
static void stream_started_cb(esp_ble_audio_bap_stream_t *stream)
{
ESP_LOGI(TAG, "Stream %p started", stream);
ESP_LOGI(TAG, "[SNK #%d] Stream started", stream_index(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);
ESP_LOGI(TAG, "[SNK #%d] Stream stopped, reason 0x%02x",
stream_index(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)
{
char name[24];
snprintf(name, sizeof(name), "SNK #%d", stream_index(stream));
rx_metrics.last_sdu_len = len;
example_audio_rx_metrics_on_recv(info, &rx_metrics, TAG, "stream", stream);
example_audio_rx_metrics_on_recv(info, &rx_metrics, TAG, name);
}
static esp_ble_audio_bap_stream_ops_t stream_ops = {
@@ -190,8 +206,6 @@ static bool scan_check_and_sync_broadcast(uint8_t type, const uint8_t *data,
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;
}
@@ -221,6 +235,9 @@ void bap_broadcast_scan_recv(esp_ble_audio_gap_app_event_t *event)
if (broadcast_id != ESP_BLE_AUDIO_BAP_INVALID_BROADCAST_ID &&
tmap_bms_found &&
pa_syncing == false) {
ESP_LOGI(TAG, "Found TMAP BMS, starting PA sync (broadcast ID 0x%06lx)",
broadcast_id);
addr.type = event->ext_scan_recv.addr.type;
memcpy(addr.a.val, event->ext_scan_recv.addr.val, sizeof(addr.a.val));
@@ -300,7 +317,7 @@ int bap_broadcast_sink_scan(void)
return err;
}
ESP_LOGI(TAG, "Extended scan started");
ESP_LOGI(TAG, "Scanning for broadcaster...");
return 0;
}
@@ -23,22 +23,19 @@ static void ext_scan_recv(esp_ble_audio_gap_app_event_t *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,
ESP_LOGI(TAG, "PA synced: handle %u sid %u phy %u peer %02x:%02x:%02x:%02x:%02x:%02x",
event->pa_sync.sync_handle, event->pa_sync.sid,
event->pa_sync.adv_phy,
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);
event->pa_sync.addr.val[1], event->pa_sync.addr.val[0]);
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",
ESP_LOGI(TAG, "PA sync lost: sync_handle %u reason 0x%02x",
event->pa_sync_lost.sync_handle, event->pa_sync_lost.reason);
bap_broadcast_pa_lost(event);
@@ -6,6 +6,7 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=30
CONFIG_BT_NIMBLE_ISO=y
@@ -5,54 +5,92 @@
(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.
## Overview
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.
This example implements the **Telephony and Media Audio Profile (TMAP) Broadcast Media Sender (BMS)** role. The device acts as a non-connectable BAP Broadcast Source: extended advertising carries the TMAS service UUID with the BMS role flag, the Broadcast Audio Announcement service with a 24-bit broadcast ID (`0x123456`), and the complete local name (`"TMAP Broadcast Source"`); periodic advertising carries the encoded BASE.
The implementation runs on the **NimBLE** host stack and uses the ESP-BLE-AUDIO library for the TMAP role registration and the CAP initiator broadcast pieces. A single subgroup with one stream is configured from the LC3 `48_2_1` broadcast preset (front-left location, media context). After the broadcast source is created and the BIG is started, a periodic TX scheduler (`example_audio_tx_scheduler_*`) drives ISO SDU transmission at the stream's QoS interval, and a sent callback tracks packet counts and drift.
## 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
* A board with BLE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31)
* Peer device running the paired example
## How to Use Example
## Configuration
Before project configuration and build, set the correct chip target:
```bash
idf.py menuconfig
```
No build-time options — runtime defaults are baked into source.
### Security & Pairing
NimBLE host security is inherited from `../../common_components/example_init/ble_audio_example_init.c`: LE Secure Connections with bonding enabled, no MITM, **Just-Works** pairing (`BLE_SM_IO_CAP_NO_IO`). The BMS broadcast itself is non-connectable and unencrypted, so these settings only apply if a peer ever opens an ATT connection.
## Build & Flash
```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.
(Exit serial monitor with `Ctrl-]`.)
## 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.
1. Initialize NVS, the Bluetooth controller/host, and the LE Audio common layer (no GAP/GATT app callbacks).
2. Register the TMAP role as `ESP_BLE_AUDIO_TMAP_ROLE_BMS` and initialize the CAP initiator (register stream ops, init TX scheduler).
3. Start the audio stack, then build the create params (1 subgroup × 1 stream, LC3 48_2_1 preset, sequential packing, no encryption) and create the broadcast source.
4. Configure and start non-connectable extended advertising (TMAS+BMS role, Broadcast Audio + broadcast ID `0x123456`, device name) and periodic advertising carrying the BASE.
5. Add the advertising handle to the BIG and start broadcast audio; on stream-started, allocate the SDU buffer and arm the TX scheduler at `qos->interval`.
6. The scheduler callback fills SDUs with the rolling sequence number and calls `esp_ble_audio_cap_stream_send`; the sent callback updates RX-side metrics via `example_audio_tx_scheduler_on_sent`.
7. `cap_initiator_update` and `cap_initiator_stop` are exposed for runtime metadata refresh and teardown.
## Example Output
## Expected Log
TAG is `TMAP_BMS`.
Init:
```
I (xxx) TMAP_BMS: CAP initiator initialized
```
Setup:
```
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...)
...
I (xxx) TMAP_BMS: Advertising started (handle 0)
I (xxx) TMAP_BMS: CAP initiator setup
```
If the controller reports drift or untransmitted packets:
Streaming:
```
I (xxx) TMAP_BMS: [SRC #0] Stream started
```
(Subsequent per-packet metrics are emitted by `example_audio_tx_scheduler_on_sent` under the `SRC #0` label.)
Stop / teardown:
```
W (xxx) TMAP_BMS: ISO data packets ... drifted and ... not txd
I (xxx) TMAP_BMS: [SRC #0] Stream stopped, reason 0x..
I (xxx) TMAP_BMS: [SRC #0] ISO disconnected, reason 0x..
I (xxx) TMAP_BMS: Advertising stopped (handle 0)
```
Errors (representative):
```
E (xxx) TMAP_BMS: Failed to create broadcast source, err ..
E (xxx) TMAP_BMS: Failed to start broadcast source, err ..
E (xxx) TMAP_BMS: [SRC #0] Invalid stream qos
E (xxx) TMAP_BMS: [SRC #0] Failed to alloc TX buffer, SDU ..
```
## Peer Pairing
Run [bmr](../bmr/) on a second board.
1. Flash and boot the BMR peer; it starts scanning for TMAP BMS advertisers.
2. Boot this BMS; it advertises with TMAS BMS role and broadcast ID `0x123456` and starts periodic advertising carrying the BASE.
3. The BMR matches the TMAS BMS role plus Broadcast Audio UUID, creates a PA sync, and waits for the BASE.
4. After BASE is received the BMR creates a broadcast sink and syncs to the BIG using the BIS index bitfield.
5. The BMS stream-started callback fires; its TX scheduler begins emitting SDUs that the BMR receives in its stream `recv` callback.
6. Stopping or power-cycling the BMS triggers PA sync loss on the BMR, which deletes the sink and resumes scanning.
@@ -86,17 +86,17 @@ static void broadcast_started_cb(esp_ble_audio_bap_stream_t *stream)
{
esp_err_t err;
ESP_LOGI(TAG, "Stream %p started", stream);
ESP_LOGI(TAG, "[SRC #0] Stream started");
if (stream->qos == NULL || stream->qos->sdu == 0) {
ESP_LOGE(TAG, "Invalid stream qos");
ESP_LOGE(TAG, "[SRC #0] 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);
ESP_LOGE(TAG, "[SRC #0] Failed to alloc TX buffer, SDU %u", stream->qos->sdu);
return;
}
}
@@ -107,7 +107,7 @@ static void broadcast_started_cb(esp_ble_audio_bap_stream_t *stream)
/* 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);
ESP_LOGE(TAG, "[SRC #0] Scheduler start failed, err %d", err);
return;
}
@@ -118,11 +118,11 @@ static void broadcast_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t rea
{
esp_err_t err;
ESP_LOGI(TAG, "Stream %p stopped, reason 0x%02x", stream, reason);
ESP_LOGI(TAG, "[SRC #0] Stream stopped, reason 0x%02x", reason);
err = example_audio_tx_scheduler_stop(&tx_scheduler);
if (err) {
ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err);
ESP_LOGE(TAG, "[SRC #0] Scheduler stop failed, err %d", err);
}
if (iso_data != NULL) {
@@ -135,11 +135,11 @@ static void broadcast_disconnected_cb(esp_ble_audio_bap_stream_t *stream, uint8_
{
esp_err_t err;
ESP_LOGI(TAG, "Stream %p disconnected, reason 0x%02x", stream, reason);
ESP_LOGI(TAG, "[SRC #0] ISO disconnected, reason 0x%02x", reason);
err = example_audio_tx_scheduler_stop(&tx_scheduler);
if (err) {
ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err);
ESP_LOGE(TAG, "[SRC #0] Scheduler stop failed, err %d", err);
}
if (iso_data != NULL) {
@@ -150,7 +150,7 @@ static void broadcast_disconnected_cb(esp_ble_audio_bap_stream_t *stream, uint8_
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);
example_audio_tx_scheduler_on_sent(&tx_scheduler, user_data, TAG, "SRC #0");
}
static esp_ble_audio_bap_stream_ops_t broadcast_stream_ops = {
@@ -335,7 +335,7 @@ static int ext_adv_start(void)
goto end;
}
ESP_LOGI(TAG, "Extended adv instance %u started", ADV_HANDLE);
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
end:
if (ext_data) {
@@ -349,23 +349,26 @@ end:
static int ext_adv_stop(void)
{
int ret = 0;
int err;
err = ble_gap_periodic_adv_stop(ADV_HANDLE);
if (err) {
ESP_LOGE(TAG, "Failed to stop per advertising, err %d", err);
ret = err;
}
err = ble_gap_ext_adv_stop(ADV_HANDLE);
if (err) {
ESP_LOGE(TAG, "Failed to stop ext advertising, err %d", err);
ret = err;
}
if (err == 0) {
ESP_LOGI(TAG, "Extended adv instance %u stopped", ADV_HANDLE);
if (ret == 0) {
ESP_LOGI(TAG, "Advertising stopped (handle %u)", ADV_HANDLE);
}
return err;
return ret;
}
int cap_initiator_setup(void)
@@ -5,60 +5,122 @@
(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).
## Overview
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.
This example takes the **TMAP Call Gateway (CG)** and **Unicast Media Sender (UMS)** roles, registered together via `esp_ble_audio_tmap_register(ESP_BLE_AUDIO_TMAP_ROLE_CG | ESP_BLE_AUDIO_TMAP_ROLE_UMS)`. It scans for connectable extended advertising that carries TMAS service data with the **UMR** role bit set, connects to the first match, pairs, exchanges MTU, and then drives TMAP and VCP discovery before bringing up unicast audio.
The host stack is NimBLE. The example uses the ESP-BLE-AUDIO library pieces for: CAP initiator with the BAP Unicast Client (LC3 preset 48_2_1, sink direction, FRONT_LEFT, MEDIA context), VCP Volume Controller, MCP server backed by the media proxy player, and CCP server registering a single GTBS bearer (`Generic TBS`, UCI `un000`, `tel,wechat` URI schemes, 5G technology). A periodic TX scheduler in the ISO task feeds dummy ISO SDUs filled with the sequence number. Device name is set to `TMAP Central`.
## 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
* A board with BLE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31)
* Peer device running the paired example
## How to Use Example
## Configuration
Before project configuration and build, set the correct chip target:
```bash
idf.py menuconfig
```
No build-time options — runtime defaults are baked into source.
### Security & Pairing
Just-Works pairing (LE Secure Connections, no MITM, no I/O capability) with bonding enabled, inherited from `../../common_components/example_init/ble_audio_example_init.c`.
## Build & Flash
```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.
(Exit serial monitor with `Ctrl-]`.)
## 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.
1. `app_main` initializes NVS, Bluetooth, the audio common layer, and registers TMAP CG+UMS.
2. Sub-modules initialize in order: CAP initiator (with TX scheduler), VCP volume controller, MCP server (media proxy), CCP server (GTBS bearer).
3. The audio stack starts, the device name is set, and extended scanning begins.
4. Each connectable scan result is parsed for TMAS service data; if the peer's TMAP role contains UMR, scan is cancelled and a connection is created.
5. On ACL connect, security is initiated; on security change, an MTU exchange is requested.
6. After the MTU exchange and GATT discovery both complete, TMAP and VCP discovery start.
7. TMAP discovery completion triggers `cap_initiator_setup` → CAS discover → sink and source ASE discovery → unicast group create → unicast audio start.
8. When a sink stream reaches "started", the TX scheduler is started at the QoS interval and `unicast_audio_tx` sends ISO SDUs filled with the sequence number.
9. After VCP discovery, the volume state is read to sync the change counter, then a mute command is issued from the state callback.
10. On disconnect, the unicast group is deleted and scanning restarts.
## Example Output
## Expected Log
Initialization phase:
```
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...)
...
TMAP_CEN: CAP initiator initialized
TMAP_CEN: VCP volume controller initialized
TMAP_CEN: MCP server initialized
TMAP_CEN: Registered gtbs bearer <n>
TMAP_CEN: Scanning for peripheral...
```
If the connection is lost:
Scan and connect:
```
I (xxx) TMAP_CEN: connection disconnected, reason 0x...
TMAP_CEN: Found TMAS in peer adv data!
TMAP_CEN: Connected: handle <h> role <r> peer <addr>
TMAP_CEN: Security: handle <h> level <l> bonded <b>
TMAP_CEN: MTU updated: handle <h> mtu <m>
TMAP_CEN: Service discovery started: handle <h>
TMAP_CEN: Service discovery complete: handle <h> status 0
```
TMAP / VCP / CAP setup:
```
TMAP_CEN: TMAP discovery done
TMAP_CEN: CAP initiator setup
TMAP_CEN: Found CAS
TMAP_CEN: [SNK #0] Endpoint discovered
TMAP_CEN: Sink discover complete
TMAP_CEN: Source discover complete
TMAP_CEN: Created unicast group
TMAP_CEN: Started unicast audio
TMAP_CEN: VCP volume controller discovering
TMAP_CEN: VCP discovery done
TMAP_CEN: VCP state cb done, volume <v> mute <m>
```
Unicast streaming phase:
```
TMAP_CEN: [SNK #0] Stream configured, QoS preference:
TMAP_CEN: [SNK #0] QoS set
TMAP_CEN: [SNK #0] Stream enabled
TMAP_CEN: [SNK #0] Stream connected
TMAP_CEN: [SNK #0] Stream started
TMAP_CEN: Streaming, interval <i>, length <l>
TMAP_CEN: Unicast start completed
```
Call control / disconnect:
```
TMAP_CEN: CCP: Placing call to remote with id <i> to <caller>
TMAP_CEN: CCP: Call terminated for id <i> with reason <r>
TMAP_CEN: [SNK #0] Stream stopped, reason 0x<rr>
TMAP_CEN: [SNK #0] ISO disconnected, reason 0x<rr>
TMAP_CEN: Disconnected: handle <h> reason 0x<rr>
TMAP_CEN: Deleted unicast group
```
Tag is `TMAP_CEN`.
## Peer Pairing
Run [tmap_peripheral](../peripheral/) on a second board.
1. Flash the peripheral and let it start extended advertising.
2. Flash and start this central; it scans for TMAS+UMR.
3. The central connects, pairs, and exchanges MTU with the peripheral.
4. TMAP discovery completes on both sides; the central runs CAP unicast setup against the peripheral's ASCS.
5. The central starts the sink stream and feeds ISO SDUs to the peripheral at the QoS interval.
6. The central can drive the peripheral's volume via VCP and accept TBS originate/terminate from the peripheral.
@@ -8,6 +8,7 @@
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include "esp_timer.h"
@@ -33,20 +34,59 @@ 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 const char *dir_str(esp_ble_audio_dir_t dir)
{
return dir == ESP_BLE_AUDIO_DIR_SINK ? "SNK" : "SRC";
}
static bool stream_is_sink(const esp_ble_audio_bap_stream_t *stream)
{
for (size_t i = 0; i < ARRAY_SIZE(unicast_sink_streams); i++) {
if (&unicast_sink_streams[i].bap_stream == stream) {
return true;
}
}
return false;
}
static const char *stream_dir_str(const esp_ble_audio_bap_stream_t *stream)
{
return stream_is_sink(stream) ? "SNK" : "SRC";
}
static int stream_index(const esp_ble_audio_bap_stream_t *stream)
{
for (size_t i = 0; i < ARRAY_SIZE(unicast_sink_streams); i++) {
if (&unicast_sink_streams[i].bap_stream == stream) {
return (int)i;
}
}
for (size_t i = 0; i < ARRAY_SIZE(unicast_source_streams); i++) {
if (&unicast_source_streams[i].bap_stream == stream) {
return (int)i;
}
}
return -1;
}
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);
ESP_LOGI(TAG, "[%s #%d] Stream configured, QoS preference:",
stream_dir_str(stream), stream_index(stream));
example_print_qos_pref(TAG, pref);
}
static void unicast_stream_qos_set_cb(esp_ble_audio_bap_stream_t *stream)
{
ESP_LOGI(TAG, "Unicast stream %p QoS set", stream);
ESP_LOGI(TAG, "[%s #%d] QoS set",
stream_dir_str(stream), stream_index(stream));
}
static void unicast_stream_enabled_cb(esp_ble_audio_bap_stream_t *stream)
{
ESP_LOGI(TAG, "Unicast stream %p enabled", stream);
ESP_LOGI(TAG, "[%s #%d] Stream enabled",
stream_dir_str(stream), stream_index(stream));
}
static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream)
@@ -54,7 +94,8 @@ 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);
ESP_LOGI(TAG, "[%s #%d] Stream started",
stream_dir_str(stream), stream_index(stream));
err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info);
if (err) {
@@ -99,12 +140,14 @@ static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream)
static void unicast_stream_metadata_updated_cb(esp_ble_audio_bap_stream_t *stream)
{
ESP_LOGI(TAG, "Unicast stream %p metadata updated", stream);
ESP_LOGI(TAG, "[%s #%d] Metadata updated",
stream_dir_str(stream), stream_index(stream));
}
static void unicast_stream_disabled_cb(esp_ble_audio_bap_stream_t *stream)
{
ESP_LOGI(TAG, "Unicast stream %p disabled", stream);
ESP_LOGI(TAG, "[%s #%d] Stream disabled",
stream_dir_str(stream), stream_index(stream));
}
static void unicast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
@@ -112,7 +155,8 @@ static void unicast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_
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);
ESP_LOGI(TAG, "[%s #%d] Stream stopped, reason 0x%02x",
stream_dir_str(stream), stream_index(stream), reason);
err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info);
if (err) {
@@ -135,17 +179,23 @@ static void unicast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_
static void unicast_stream_released_cb(esp_ble_audio_bap_stream_t *stream)
{
ESP_LOGI(TAG, "Unicast stream %p released", stream);
ESP_LOGI(TAG, "[%s #%d] Stream released",
stream_dir_str(stream), stream_index(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);
char name[24];
snprintf(name, sizeof(name), "%s #%d",
stream_dir_str(stream), stream_index(stream));
example_audio_tx_scheduler_on_sent(&tx_scheduler, user_data, TAG, name);
}
static void unicast_stream_connected_cb(esp_ble_audio_bap_stream_t *stream)
{
ESP_LOGI(TAG, "Unicast stream %p connected", stream);
ESP_LOGI(TAG, "[%s #%d] Stream connected",
stream_dir_str(stream), stream_index(stream));
}
static void unicast_stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
@@ -153,7 +203,8 @@ static void unicast_stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, u
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);
ESP_LOGI(TAG, "[%s #%d] ISO disconnected, reason 0x%02x",
stream_dir_str(stream), stream_index(stream), reason);
err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info);
if (err) {
@@ -208,7 +259,7 @@ static void unicast_discovery_complete_cb(esp_ble_conn_t *conn, int err,
return;
}
ESP_LOGI(TAG, "Found CAS with CSIS %p", csis_inst);
ESP_LOGI(TAG, "Found CAS with CSIS");
} else {
ESP_LOGI(TAG, "Found CAS");
}
@@ -289,6 +340,17 @@ int unicast_group_delete(void)
{
int err;
/* The endpoint pointers become stale once the peer disconnects and are
* rediscovered on every reconnect. Clear them here so the next discovery
* does not find filled slots and warn "Unhandled endpoint".
*/
for (size_t i = 0; i < ARRAY_SIZE(unicast_sink_eps); i++) {
unicast_sink_eps[i] = NULL;
}
for (size_t i = 0; i < ARRAY_SIZE(unicast_source_eps); i++) {
unicast_source_eps[i] = NULL;
}
if (unicast_group == NULL) {
return 0;
}
@@ -355,56 +417,64 @@ 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);
ESP_LOGI(TAG, "[%s #%d] Config response, rsp_code %u reason %u",
stream_dir_str(stream), stream_index(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);
ESP_LOGI(TAG, "[%s #%d] QoS response, rsp_code %u reason %u",
stream_dir_str(stream), stream_index(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);
ESP_LOGI(TAG, "[%s #%d] Enable response, rsp_code %u reason %u",
stream_dir_str(stream), stream_index(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);
ESP_LOGI(TAG, "[%s #%d] Start response, rsp_code %u reason %u",
stream_dir_str(stream), stream_index(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);
ESP_LOGI(TAG, "[%s #%d] Stop response, rsp_code %u reason %u",
stream_dir_str(stream), stream_index(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);
ESP_LOGI(TAG, "[%s #%d] Disable response, rsp_code %u reason %u",
stream_dir_str(stream), stream_index(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);
ESP_LOGI(TAG, "[%s #%d] Metadata response, rsp_code %u reason %u",
stream_dir_str(stream), stream_index(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);
ESP_LOGI(TAG, "[%s #%d] Release response, rsp_code %u reason %u",
stream_dir_str(stream), stream_index(stream), rsp_code, reason);
}
static void discover_cb(esp_ble_conn_t *conn, int err, esp_ble_audio_dir_t dir)
@@ -446,7 +516,7 @@ static void discover_cb(esp_ble_conn_t *conn, int err, esp_ble_audio_dir_t dir)
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);
example_print_codec_cap(TAG, codec_cap);
}
static void endpoint_cb(esp_ble_conn_t *conn,
@@ -456,7 +526,7 @@ static void endpoint_cb(esp_ble_conn_t *conn,
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);
ESP_LOGI(TAG, "[SRC #%zu] Endpoint discovered", i);
unicast_source_eps[i] = ep;
return;
}
@@ -464,14 +534,14 @@ static void endpoint_cb(esp_ble_conn_t *conn,
} 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);
ESP_LOGI(TAG, "[SNK #%zu] Endpoint discovered", i);
unicast_sink_eps[i] = ep;
return;
}
}
}
ESP_LOGW(TAG, "Unhandled endpoint %p, dir %u", ep, dir);
ESP_LOGW(TAG, "[%s] Unhandled endpoint", dir_str(dir));
}
static esp_ble_audio_bap_unicast_client_cb_t unicast_client_cbs = {
@@ -91,7 +91,7 @@ static void ext_scan_start(void)
return;
}
ESP_LOGI(TAG, "Extended scan started");
ESP_LOGI(TAG, "Scanning for peripheral...");
}
static int conn_create(ble_addr_t *dst)
@@ -214,30 +214,28 @@ 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);
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]);
ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x",
event->acl_connect.conn_handle, 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]);
default_conn_handle = event->acl_connect.conn_handle;
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",
ESP_LOGI(TAG, "Disconnected: handle %u reason 0x%02x",
event->acl_disconnect.conn_handle, event->acl_disconnect.reason);
default_conn_handle = CONN_HANDLE_INIT;
@@ -255,19 +253,14 @@ 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);
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]);
ESP_LOGI(TAG, "Security: handle %u level %u bonded %u",
event->security_change.conn_handle,
event->security_change.sec_level,
event->security_change.bonded);
err = exchange_mtu(event->security_change.conn_handle);
if (err) {
@@ -300,22 +293,23 @@ 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",
ESP_LOGI(TAG, "MTU updated: 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",
ESP_LOGW(TAG, "MTU %u below minimum %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);
ESP_LOGE(TAG, "Failed to start service discovery, err %d", err);
return;
}
ESP_LOGI(TAG, "Start discovering gatt services");
ESP_LOGI(TAG, "Service discovery started: handle %u",
event->gatt_mtu_change.conn_handle);
/* Note:
* MTU exchanged event may arrived after discover completed event.
@@ -337,11 +331,11 @@ 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);
ESP_LOGI(TAG, "Service discovery complete: handle %u status %u",
event->gattc_disc_cmpl.conn_handle,
event->gattc_disc_cmpl.status);
if (event->gattc_disc_cmpl.status) {
ESP_LOGE(TAG, "gattc disc failed, status %u", event->gattc_disc_cmpl.status);
return;
}
@@ -13,6 +13,12 @@
#include "tmap_central.h"
static esp_ble_audio_vcp_vol_ctlr_t *vcp_vol_ctlr;
/* VCS control-point writes carry a change counter that must match the server's.
* After discovery the local counter is 0 while the server may have incremented it
* during a previous session (e.g. before the central rebooted). Read the state
* first to sync the counter, then issue the demo mute from the state callback.
*/
static bool initial_state_read_pending;
static void vcs_discover_cb(esp_ble_audio_vcp_vol_ctlr_t *vol_ctlr, int err,
uint8_t vocs_count, uint8_t aics_count)
@@ -22,11 +28,13 @@ static void vcs_discover_cb(esp_ble_audio_vcp_vol_ctlr_t *vol_ctlr, int err,
return;
}
ESP_LOGI(TAG, "VCP discovery done, %p %p", vol_ctlr, vcp_vol_ctlr);
ESP_LOGI(TAG, "VCP discovery done");
err = esp_ble_audio_vcp_vol_ctlr_mute(vol_ctlr);
initial_state_read_pending = true;
err = esp_ble_audio_vcp_vol_ctlr_read_state(vol_ctlr);
if (err) {
ESP_LOGE(TAG, "Failed to send mute command, err %d", err);
ESP_LOGE(TAG, "Failed to read VCS state, err %d", err);
initial_state_read_pending = false;
}
}
@@ -44,8 +52,19 @@ static void vcs_state_cb(esp_ble_audio_vcp_vol_ctlr_t *vol_ctlr,
{
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);
initial_state_read_pending = false;
return;
}
ESP_LOGI(TAG, "VCP state cb done, volume %u mute %u", volume, mute);
if (initial_state_read_pending) {
initial_state_read_pending = false;
err = esp_ble_audio_vcp_vol_ctlr_mute(vol_ctlr);
if (err) {
ESP_LOGE(TAG, "Failed to send mute command, err %d", err);
}
}
}
@@ -9,6 +9,7 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=40
CONFIG_BT_NIMBLE_ISO=y
@@ -5,74 +5,123 @@
(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.
## Overview
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.
This example takes the **TMAP Call Terminal (CT)** and **Unicast Media Receiver (UMR)** roles, registered together via `esp_ble_audio_tmap_register(ESP_BLE_AUDIO_TMAP_ROLE_CT | ESP_BLE_AUDIO_TMAP_ROLE_UMR)`. It runs connectable extended advertising at a 200 ms interval that includes the GAP Earbud appearance, the ASCS/CAS/TMAS UUIDs, an ASCS targeted unicast announcement carrying sink and source contexts (UNSPECIFIED | CONVERSATIONAL | MEDIA | GAME | INSTRUCTIONAL), a CAS targeted announcement, the TMAS UMR|CT role payload, and the device name `tmap_peripheral`. When the Duo earbuds option is selected, a CSIS RSI is added to the advertisement.
The host stack is NimBLE. The example uses the ESP-BLE-AUDIO library pieces for: BAP Unicast Server with PACS (LC3 cap with 16/32/48 kHz, 7.5/10 ms duration, 30–155 octets), VCP Volume Renderer (initial volume 10, unmuted, with VOCS+AICS instances built from Kconfig counts), TBS client (CCP Call Terminal — discovers GTBS, reads URI list, originates/terminates calls), MCC controller (reads player name, track title/duration/position, playback/seeking speed, playing order, media state, opcodes, CCID, plus a `mcp_send_cmd` for PLAY/PAUSE), and optionally CSIP Set Member with SIRK and rank from Kconfig. Sink streams are auto-started from the `enabled` callback. Device name is set to `TMAP Peripheral`.
## 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)
* A board with BLE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31)
* Peer device running the paired example
## 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:
## Configuration
```bash
idf.py menuconfig
```
Under **Example: TMAP Peripheral (CT & UMR)** you can set:
Under **Example: TMAP Peripheral (CT & UMR)**:
* **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
* **Earbuds type** — `Single ear headset` or `Duo headset`. Duo selects `BT_CSIP_SET_MEMBER` and `BT_CAP_ACCEPTOR_SET_MEMBER`, enabling CSIS and adding RSI to the advertisement.
* **Device rank in set** — integer 1–2, only when Duo is selected; written into the CSIS register parameters.
* **Earbud Location** — `Left Ear` or `Right Ear`. Adds `FRONT_LEFT` / `FRONT_RIGHT` to the PACS sink/source location bitmap.
### Build and Flash
### Security & Pairing
Run the following to build, flash and monitor:
Just-Works pairing (LE Secure Connections, no MITM, no I/O capability) with bonding enabled, inherited from `../../common_components/example_init/ble_audio_example_init.c`.
## Build & Flash
```bash
idf.py set-target esp32h4
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.
(Exit serial monitor with `Ctrl-]`.)
## 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.
1. `app_main` initializes NVS, Bluetooth, and the audio common layer, then registers TMAP CT+UMR.
2. If `BT_CSIP_SET_MEMBER` is on, the CSIP set member registers via the CAP acceptor, generates an RSI, and is plumbed into `start_info.csis_insts[0]`.
3. VCP volume renderer, BAP unicast server (PACS, ASCS callbacks, stream ops), CCP call control (TBS client), and MCP controller (MCC) are initialized.
4. The audio stack starts, device name is set, and extended advertising starts on handle 0.
5. On ACL connect the connection handle is stored; on MTU change GATT service discovery starts; on discovery complete `tmap_discover_tmas` runs.
6. TMAP discovery records whether the peer is CG and/or UMS, then `ccp_discover_tbs` triggers TBS discovery.
7. After GTBS is discovered the TBS URI list is read; `read_uri_schemes_string_cb` saves the first URI and chains into `mcp_discover_mcs`.
8. MCS discovery walks player name → track title → duration → position → playback speed → seeking speed → playing order → orders supported → media state → opcodes → CCID.
9. ASCS callbacks accept config/QoS/enable/start; the `enabled` stream op auto-starts sink streams (receiver-start-ready); RX metrics are reset on start and updated per received SDU.
10. Helpers `initiate_call`/`terminate_call` (gated on peer CG) and `play_media`/`pause_media` (gated on peer UMS) issue TBS and MCC commands.
11. On disconnect, the connection handle is cleared and advertising restarts.
## Example Output
## Expected Log
Initialization phase:
```
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
...
TMAP_PER: TMAP CT UMR initialized
TMAP_PER: CSIP set member initialized
TMAP_PER: PRSI: 0x<hex>
TMAP_PER: vcp vol renderer, vocs_cnt <n> aics_cnt <n>
TMAP_PER: VCP volume renderer initialized
TMAP_PER: BAP unicast server initialized
TMAP_PER: CCP call controller initialized
TMAP_PER: MCP controller initialized
TMAP_PER: Advertising started (handle 0)
```
If the connection is lost:
Connection / discovery:
```
I (xxx) TMAP_PER: connection disconnected, reason 0x...
TMAP_PER: Connected: handle <h> role <r> peer <addr>
TMAP_PER: MTU updated: handle <h> mtu <m>
TMAP_PER: Service discovery started: handle <h>
TMAP_PER: Service discovery complete: handle <h> status 0
TMAP_PER: TMAP discovery done
TMAP_PER: Discovered GTBS
TMAP_PER: Discovered remote URI <uri>
TMAP_PER: Discover MCS succeeded
TMAP_PER: Read player name succeeded, name <name>
TMAP_PER: Read track title succeeded, title <title>
TMAP_PER: Read media state succeeded, state <s>
TMAP_PER: Read content control id succeeded, ccid <c>
```
ASCS / unicast streaming:
```
TMAP_PER: [SNK] Config request:
TMAP_PER: [SNK #0] QoS request:
TMAP_PER: [SNK #0] Enable request (meta_len <n>)
TMAP_PER: [SNK #0] Stream enabled
TMAP_PER: [SNK #0] Start request
TMAP_PER: [SNK #0] Stream started
TMAP_PER: [SNK #0] Stop request
TMAP_PER: [SNK #0] Stream stopped, reason 0x<rr>
TMAP_PER: [SNK #0] Disable request
TMAP_PER: [SNK #0] Release request
```
VCP / call control / disconnect:
```
TMAP_PER: VCS volume <v>, mute <m>
TMAP_PER: VCS flags 0x<ff>
TMAP_PER: Call <i> originated
TMAP_PER: Call <i> terminated
TMAP_PER: Disconnected: handle <h> reason 0x<rr>
```
Tag is `TMAP_PER`.
## Peer Pairing
Run [tmap_central](../central/) on a second board.
1. Flash this peripheral and start it; advertising begins on handle 0.
2. Flash and start the central; it scans for TMAS+UMR and connects to this device.
3. After pairing and MTU exchange, both sides complete TMAP discovery; this peripheral then discovers GTBS and reads the URI list.
4. The MCC chain reads the central's media proxy state; the central's CAP initiator configures and starts the sink stream.
5. The peripheral auto-starts the sink stream from the `enabled` callback and logs received SDU metrics.
6. Use `initiate_call`/`terminate_call` to drive the central's TBS, and `play_media`/`pause_media` to send PLAY/PAUSE via MCC.
@@ -1,16 +1,9 @@
# SPDX-FileCopyrightText: 2022 Nordic Semiconductor ASA
# SPDX-FileContributor: 2025 Espressif Systems (Shanghai) CO LTD
# SPDX-FileContributor: 2026 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"
@@ -27,6 +20,14 @@ menu "Example: TMAP Peripheral (CT & UMR)"
endchoice
if EXAMPLE_TMAP_PER_DUO
config EXAMPLE_TMAP_PER_SET_RANK
int "Device rank in set"
range 1 2
help
Rank of this device in set.
endif
choice EXAMPLE_TMAP_PER_LOCATION
prompt "Earbud Location"
@@ -77,6 +77,41 @@ static esp_ble_audio_bap_stream_t *stream_alloc(void)
return NULL;
}
static const char *dir_str(esp_ble_audio_dir_t dir)
{
return dir == ESP_BLE_AUDIO_DIR_SINK ? "SNK" : "SRC";
}
static bool stream_is_sink(const esp_ble_audio_bap_stream_t *stream)
{
for (size_t i = 0; i < ARRAY_SIZE(sink_streams); i++) {
if (sink_streams[i].stream == stream) {
return true;
}
}
return false;
}
static const char *stream_dir_str(const esp_ble_audio_bap_stream_t *stream)
{
return stream_is_sink(stream) ? "SNK" : "SRC";
}
static int stream_index(const esp_ble_audio_bap_stream_t *stream)
{
for (size_t i = 0; i < ARRAY_SIZE(sink_streams); i++) {
if (sink_streams[i].stream == stream) {
return (int)i;
}
}
for (size_t i = 0; i < ARRAY_SIZE(source_streams); i++) {
if (source_streams[i].stream == stream) {
return (int)i;
}
}
return -1;
}
static int config_cb(esp_ble_conn_t *conn,
const esp_ble_audio_bap_ep_t *ep,
esp_ble_audio_dir_t dir,
@@ -87,21 +122,19 @@ static int config_cb(esp_ble_conn_t *conn,
{
bool stream_exist = false;
ESP_LOGI(TAG, "Config: conn %p ep %p dir %u", conn, ep, dir);
ESP_LOGI(TAG, "[%s] Config request:", dir_str(dir));
example_print_codec_cfg(codec_cfg);
example_print_codec_cfg(TAG, codec_cfg);
*stream = stream_alloc();
if (*stream == NULL) {
ESP_LOGE(TAG, "No streams available");
ESP_LOGE(TAG, "[%s] No streams available", dir_str(dir));
*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) {
@@ -112,7 +145,7 @@ static int config_cb(esp_ble_conn_t *conn,
if (stream_exist == false) {
if (configured_sink_stream_count >= ARRAY_SIZE(sink_streams)) {
ESP_LOGE(TAG, "No more sink stream available");
ESP_LOGE(TAG, "[SNK] 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);
@@ -131,7 +164,7 @@ static int config_cb(esp_ble_conn_t *conn,
if (stream_exist == false) {
if (configured_source_stream_count >= ARRAY_SIZE(source_streams)) {
ESP_LOGE(TAG, "No more source stream available");
ESP_LOGE(TAG, "[SRC] 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);
@@ -155,9 +188,10 @@ static int reconfig_cb(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, "Reconfig: stream %p", stream);
ESP_LOGI(TAG, "[%s #%d] Reconfig request:",
dir_str(dir), stream_index(stream));
example_print_codec_cfg(codec_cfg);
example_print_codec_cfg(TAG, codec_cfg);
*pref = qos_pref;
@@ -168,9 +202,10 @@ 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);
ESP_LOGI(TAG, "[%s #%d] QoS request:",
stream_dir_str(stream), stream_index(stream));
example_print_qos(qos);
example_print_qos(TAG, qos);
return 0;
}
@@ -179,14 +214,16 @@ 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);
ESP_LOGI(TAG, "[%s #%d] Enable request (meta_len %u)",
stream_dir_str(stream), stream_index(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);
ESP_LOGI(TAG, "[%s #%d] Start request",
stream_dir_str(stream), stream_index(stream));
return 0;
}
@@ -265,7 +302,8 @@ static int metadata_cb(esp_ble_audio_bap_stream_t *stream,
};
esp_err_t err;
ESP_LOGI(TAG, "Metadata: stream %p meta_len %u", stream, meta_len);
ESP_LOGI(TAG, "[%s #%d] Metadata request (meta_len %u)",
stream_dir_str(stream), stream_index(stream), meta_len);
err = esp_ble_audio_data_parse(meta, meta_len, data_func_cb, &func_param);
if (err) {
@@ -291,21 +329,24 @@ static int metadata_cb(esp_ble_audio_bap_stream_t *stream,
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);
ESP_LOGI(TAG, "[%s #%d] Disable request",
stream_dir_str(stream), stream_index(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);
ESP_LOGI(TAG, "[%s #%d] Stop request",
stream_dir_str(stream), stream_index(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);
ESP_LOGI(TAG, "[%s #%d] Release request",
stream_dir_str(stream), stream_index(stream));
for (size_t i = 0; i < ARRAY_SIZE(sink_streams); i++) {
if (sink_streams[i].stream == stream) {
@@ -327,7 +368,7 @@ static int release_cb(esp_ble_audio_bap_stream_t *stream,
}
}
ESP_LOGW(TAG, "Stream %p not found", stream);
ESP_LOGW(TAG, "Stream not found");
return -ENODEV;
}
@@ -348,7 +389,8 @@ 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);
ESP_LOGI(TAG, "[%s #%d] Stream enabled",
stream_dir_str(stream), stream_index(stream));
err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info);
if (err) {
@@ -361,7 +403,8 @@ static void stream_enabled(esp_ble_audio_bap_stream_t *stream)
/* 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);
ESP_LOGE(TAG, "[SNK #%d] Failed to start stream, err %d",
stream_index(stream), err);
return;
}
}
@@ -369,7 +412,8 @@ static void stream_enabled(esp_ble_audio_bap_stream_t *stream)
static void stream_started(esp_ble_audio_bap_stream_t *stream)
{
ESP_LOGI(TAG, "Stream %p started", stream);
ESP_LOGI(TAG, "[%s #%d] Stream started",
stream_dir_str(stream), stream_index(stream));
for (size_t i = 0; i < ARRAY_SIZE(sink_streams); i++) {
if (sink_streams[i].stream == stream) {
@@ -381,7 +425,8 @@ static void stream_started(esp_ble_audio_bap_stream_t *stream)
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);
ESP_LOGI(TAG, "[%s #%d] Stream stopped, reason 0x%02x",
stream_dir_str(stream), stream_index(stream), reason);
}
static void stream_recv(esp_ble_audio_bap_stream_t *stream,
@@ -394,10 +439,12 @@ static void stream_recv(esp_ble_audio_bap_stream_t *stream,
struct audio_sink *sink_stream = CONTAINER_OF(&sink_streams[i].stream,
struct audio_sink,
stream);
char name[24];
snprintf(name, sizeof(name), "SNK #%zu", i);
sink_stream->rx_metrics.last_sdu_len = len;
example_audio_rx_metrics_on_recv(info, &sink_stream->rx_metrics,
TAG, "stream", stream);
TAG, name);
break;
}
}
@@ -113,7 +113,7 @@ static void read_uri_schemes_string_cb(esp_ble_conn_t *conn,
}
if (i >= sizeof(remote_uri)) {
ESP_LOGW(TAG, "Cannot store URI of length %u: %s", i, value);
ESP_LOGW(TAG, "Cannot store URI of length %zu: %s", i, value);
return;
}
@@ -197,30 +197,28 @@ static void ext_adv_start(void)
return;
}
ESP_LOGI(TAG, "Extended adv instance %u started", ADV_HANDLE);
ESP_LOGI(TAG, "Advertising started (handle %u)", 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);
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]);
ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x",
event->acl_connect.conn_handle, 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]);
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",
ESP_LOGI(TAG, "Disconnected: handle %u reason 0x%02x",
event->acl_disconnect.conn_handle, event->acl_disconnect.reason);
default_conn_handle = CONN_HANDLE_INIT;
@@ -246,11 +244,11 @@ 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",
ESP_LOGI(TAG, "MTU updated: 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",
ESP_LOGW(TAG, "MTU %u below minimum %u",
event->gatt_mtu_change.mtu, ESP_BLE_AUDIO_ATT_MTU_MIN);
return;
}
@@ -261,20 +259,21 @@ static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event)
*/
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);
ESP_LOGE(TAG, "Failed to start service discovery, err %d", err);
return;
}
ESP_LOGI(TAG, "Start discovering gatt services");
ESP_LOGI(TAG, "Service discovery started: handle %u",
event->gatt_mtu_change.conn_handle);
}
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);
ESP_LOGI(TAG, "Service discovery complete: handle %u status %u",
event->gattc_disc_cmpl.conn_handle,
event->gattc_disc_cmpl.status);
if (event->gattc_disc_cmpl.status) {
ESP_LOGE(TAG, "gattc disc failed, status %u", event->gattc_disc_cmpl.status);
return;
}
@@ -190,10 +190,10 @@ int vcp_vol_renderer_init(void)
ESP_LOGI(TAG, "vcp vol renderer, vocs_cnt %u aics_cnt %u",
vcp_included.vocs_cnt, vcp_included.aics_cnt);
for (size_t i = 0; i < vcp_included.vocs_cnt; i++) {
ESP_LOGI(TAG, "vcp vol renderer, vocs_%u %p", i, vcp_included.vocs[i]);
ESP_LOGI(TAG, "vcp vol renderer, vocs_%zu %p", i, vcp_included.vocs[i]);
}
for (size_t i = 0; i < vcp_included.aics_cnt; i++) {
ESP_LOGI(TAG, "vcp vol renderer, aics_%u %p", i, vcp_included.aics[i]);
ESP_LOGI(TAG, "vcp vol renderer, aics_%zu %p", i, vcp_included.aics[i]);
}
ESP_LOGI(TAG, "VCP volume renderer initialized");
@@ -9,6 +9,7 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=30
CONFIG_BT_NIMBLE_ISO=y
@@ -1,52 +1,107 @@
| Supported Targets | ESP32-H4 | ESP32-S31 |
| ----------------- | -------- | --------- |
# BLE BIG Broadcaster Example
# BIG Broadcaster
(See the README.md file in the upper level `examples` directory for more information about examples.)
This example demonstrates the **Bluetooth LE Isochronous Broadcaster** functionality. It acts as a Broadcast Isochronous Stream (BIS) source: it starts extended advertising and periodic advertising, creates a Broadcast Isochronous Group (BIG), and sends isochronous data on the BIS channels. Receivers can synchronize to this BIG using the [big_receiver](../big_receiver) example.
## Overview
The implementation uses the NimBLE host stack with ISO support and the ESP-BLE-ISO APIs (CIG/BIG, data path, channel operations). It is intended for chips that support BLE 5.2 ISO (e.g. ESP32-H4). The advertised device name is hardcoded as `BIG Broadcaster` and the broadcast code is hardcoded as `1234`; these can be changed by editing the source code constants.
This example demonstrates a raw BLE Isochronous Broadcaster — it creates a Broadcast Isochronous Group (BIG) directly at the ISO transport layer over the NimBLE host, without any BLE Audio profile (BAP/CAP) on top.
The device acts as the **broadcaster**: it starts extended + periodic advertising, creates a BIG carrying two BIS streams, sets up an HCI input data path on each BIS, and then transmits SDUs on a fixed 10 ms cadence using a software TX scheduler. The peer (`big_receiver`) discovers the broadcaster by name, syncs to the periodic advertising train, decodes BIGInfo, and joins the BIS sub-events.
The audio data on the wire is **application-supplied dummy payload** (each SDU is filled with the byte value of its sequence number). The coding format is `ESP_BLE_ISO_CODING_FORMAT_TRANSPARENT`, so no LC3 / LE-Audio framing is involved.
## Requirements
* A board with Bluetooth LE 5.2 and ISO support (e.g. ESP32-H4)
* Optionally, a second device running the [big_receiver](../big_receiver) example to receive and sync to the BIG
* A board with BLE 5.2 and ISO support (e.g. ESP32-H4, ESP32-S31)
* Peer device running the paired example
## How to Use Example
## Configuration
Before project configuration and build, set the correct chip target:
```bash
idf.py menuconfig
```
No build-time options — runtime defaults are baked into source.
Notable hard-coded parameters in `main/main.c`:
* `LOCAL_DEVICE_NAME` — `"BIG Broadcaster"` (advertised in extended adv)
* `LOCAL_BROADCAST_CODE` — `"1234"` (BIG is encrypted)
* `BIG_SDU_INTERVAL_US` — 10 000 us
* `BIG_LATENCY_MS` — 10 ms
* `BIG_PHY` — 2M
* `BIG_RTN` — 2 (retransmissions)
* `BIS_ISO_CHAN_COUNT` — 2
* `BIS_SDU_SIZE` — 120 bytes
### Security & Pairing
The shared init at `../common_components/example_init/ble_iso_example_init.c` configures Just-Works pairing (LE Secure Connections, no MITM, `BLE_SM_IO_CAP_NO_IO`) with bonding enabled, and leaves `gatts_register_cb = NULL` (no GATT services). These settings are not exercised by this example — BIG broadcast traffic is non-connectable and BIS payload encryption is driven by the broadcast code, independent of host SMP.
## Build & Flash
```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.
(Exit serial monitor with `Ctrl-]`.)
## Example Flow
1. **Initialization**: NVS, Bluetooth stack (NimBLE), and ISO common layer (`esp_ble_iso_common_init`).
2. **Extended and periodic advertising**: Configure and start extended advertising with a fixed interval; attach periodic advertising with name `BIG Broadcaster` for receivers to discover.
3. **Create BIG**: Register the advertising set for BIG, then create a BIG with two BIS channels (hardcoded as `BIS_ISO_CHAN_COUNT` in the source; `CONFIG_BT_ISO_MAX_CHAN` must be >= 2), SDU interval 10 ms, latency 10 ms, sequential packing, unframed, and encryption with the hardcoded broadcast code `1234`. The [big_receiver](../big_receiver) must use the same broadcast code.
4. **Send ISO data**: Once all BIS channels are connected and their data paths are set up, a periodic TX scheduler based on `k_work_delayable` sends the same SDU on all BIS channels at the configured interval in the ISO task context; sequence numbers and drift handling are applied. Note that the scheduler timer resolution is in milliseconds, which may not match the exact SDU interval for all configurations.
1. NVS, NimBLE host, and the ISO common layer are initialised.
2. A TX scheduler is initialised for each of the two BIS channels.
3. Extended advertising parameters / data and periodic advertising parameters / data are configured for handle 0 (non-connectable, non-scannable, primary 1M / secondary 2M).
4. Periodic advertising and extended advertising are started.
5. `esp_ble_iso_big_ext_adv_add()` ties the adv handle to the BIG, then `esp_ble_iso_big_create()` issues the HCI `LE Create BIG` with two BIS, encryption on, and broadcast code `"1234"`.
6. As each BIS becomes ready, the connected callback installs an HCI input data path (`ESP_BLE_ISO_DATA_PATH_DIR_INPUT`, transparent coding).
7. Once both BIS are connected, the per-channel TX schedulers start firing every 10 ms, calling `esp_ble_iso_chan_send()` with a 120-byte dummy SDU and an incrementing sequence number.
8. On disconnect of all BIS the schedulers are stopped and the BIG handle is cleared.
## Example Output
## Expected Log
Tag: `BIG_BRD`.
Startup / advertising:
```
I (xxx) BIG_BRD: Extended adv instance 0 started
I (xxx) BIG_BRD: ISO channel 0x0001 connected
I (xxx) BIG_BRD: ISO channel 0x0002 connected
I (xxx) BIG_BRD: Transmitted 1000 ISO data packets (chan 0x...)
...
I (xxx) BIG_BRD: Advertising started (handle 0)
```
BIS bring-up (first BIS, then second, then TX start):
```
I (xxx) BIG_BRD: [BIS #0] Connected
I (xxx) BIG_BRD: Waiting for remaining BIS channels (1/2)
I (xxx) BIG_BRD: [BIS #1] Connected
I (xxx) BIG_BRD: All 2 BIS channels connected, starting TX
```
Steady-state (logged every `LOG_INTERVAL_PACKETS` SDUs by the shared TX helper):
```
I (xxx) BIG_BRD: [BIS #0] TX: <N> packets
I (xxx) BIG_BRD: [BIS #1] TX: <N> packets
```
Teardown:
```
I (xxx) BIG_BRD: [BIS #0] Disconnected, reason 0x<rr>
I (xxx) BIG_BRD: [BIS #1] Disconnected, reason 0x<rr>
I (xxx) BIG_BRD: All BIS channels disconnected, TX stopped
```
## Peer Pairing
Run [big_receiver](../big_receiver/) on a second board.
1. Flash and run `big_broadcaster` on board A; it begins extended + periodic advertising and creates the BIG immediately.
2. Flash and run `big_receiver` on board B; it starts passive extended scanning.
3. Board B matches on the device name `"BIG Broadcaster"` and creates a periodic advertising sync to the broadcaster's PA train.
4. Once PA-synced, board B receives the BIGInfo report and calls `LE BIG Create Sync` with broadcast code `"1234"` and both BIS in the bitfield.
5. Board A logs `[BIS #0/1] Connected` as the controller reports the BIS as established; board B logs `PA synced` followed by per-BIS `Connected` and starts counting received SDUs.
6. Resetting either side causes the receiver to drop and (via `pa_sync_lost`) restart its scan, re-establishing the link automatically.
@@ -5,6 +5,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <string.h>
#include <assert.h>
@@ -73,20 +74,21 @@ static void iso_connected_cb(esp_ble_iso_chan_t *chan)
.pid = ESP_BLE_ISO_DATA_PATH_HCI,
.format = ESP_BLE_ISO_CODING_FORMAT_TRANSPARENT,
};
int chan_idx = bis_chan_index_get(chan);
esp_err_t err;
ESP_LOGI(TAG, "ISO channel %p connected", chan);
ESP_LOGI(TAG, "[BIS #%d] Connected", chan_idx);
err = esp_ble_iso_setup_data_path(chan, ESP_BLE_ISO_DATA_PATH_DIR_INPUT, &data_path);
if (err) {
ESP_LOGE(TAG, "Failed to setup ISO data path, err %d", err);
ESP_LOGE(TAG, "[BIS #%d] Failed to setup data path, err %d", chan_idx, err);
return;
}
connected_bis_count++;
if (connected_bis_count < BIS_ISO_CHAN_COUNT) {
ESP_LOGI(TAG, "Waiting for all BIS channels (%u/%u)",
ESP_LOGI(TAG, "Waiting for remaining BIS channels (%u/%u)",
connected_bis_count, BIS_ISO_CHAN_COUNT);
return;
}
@@ -100,7 +102,7 @@ static void iso_connected_cb(esp_ble_iso_chan_t *chan)
err = example_iso_tx_scheduler_start(&chan_tx[i].scheduler, BIG_SDU_INTERVAL_US);
if (err) {
ESP_LOGE(TAG, "Failed to start tx scheduler[%u], err %d", i, err);
ESP_LOGE(TAG, "[BIS #%zu] Scheduler start failed, err %d", i, err);
continue;
}
@@ -110,7 +112,8 @@ static void iso_connected_cb(esp_ble_iso_chan_t *chan)
static void iso_disconnected_cb(esp_ble_iso_chan_t *chan, uint8_t reason)
{
ESP_LOGI(TAG, "ISO channel %p disconnected, reason 0x%02x", chan, reason);
ESP_LOGI(TAG, "[BIS #%d] Disconnected, reason 0x%02x",
bis_chan_index_get(chan), reason);
if (connected_bis_count > 0) {
connected_bis_count--;
@@ -124,7 +127,7 @@ static void iso_disconnected_cb(esp_ble_iso_chan_t *chan, uint8_t reason)
for (size_t i = 0; i < BIS_ISO_CHAN_COUNT; i++) {
err = example_iso_tx_scheduler_stop(&chan_tx[i].scheduler);
if (err) {
ESP_LOGE(TAG, "Failed to stop tx scheduler[%u], err %d", i, err);
ESP_LOGE(TAG, "[BIS #%zu] Scheduler stop failed, err %d", i, err);
}
}
@@ -135,14 +138,16 @@ static void iso_disconnected_cb(esp_ble_iso_chan_t *chan, uint8_t reason)
static void iso_sent_cb(esp_ble_iso_chan_t *chan, void *user_data)
{
int chan_idx = bis_chan_index_get(chan);
char name[24];
if (chan_idx < 0) {
ESP_LOGW(TAG, "Unknown BIS channel %p", chan);
ESP_LOGW(TAG, "Unknown BIS channel");
return;
}
snprintf(name, sizeof(name), "BIS #%d", chan_idx);
example_iso_tx_scheduler_on_sent(&chan_tx[chan_idx].scheduler,
user_data, TAG, "chan", chan);
user_data, TAG, name);
}
static esp_ble_iso_chan_ops_t iso_ops = {
@@ -281,7 +286,7 @@ static int ext_adv_start(void)
return err;
}
ESP_LOGI(TAG, "Extended adv instance %u started", ADV_HANDLE);
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
return 0;
}
@@ -334,7 +339,7 @@ static void iso_chan_send(int chan_idx)
}
if (bis_iso_chan[chan_idx].iso == NULL) {
ESP_LOGW(TAG, "No channel to transmit data, %u", chan_idx);
ESP_LOGW(TAG, "[BIS #%d] No channel to transmit", chan_idx);
return;
}
@@ -347,8 +352,7 @@ static void iso_chan_send(int chan_idx)
sizeof(chan_tx[chan_idx].data),
chan_tx[chan_idx].seq_num);
if (err) {
ESP_LOGD(TAG, "Failed to transmit data on channel 0x%04x",
bis_iso_chan[chan_idx].iso->handle);
ESP_LOGD(TAG, "[BIS #%d] send failed, err %d", chan_idx, err);
return;
}
@@ -397,7 +401,7 @@ void app_main(void)
tx_scheduler_cb,
&chan_tx[i]);
if (err) {
ESP_LOGE(TAG, "Failed to init tx scheduler[%u], err %d", i, err);
ESP_LOGE(TAG, "[BIS #%zu] Scheduler init failed, err %d", i, err);
return;
}
}
@@ -1,59 +1,105 @@
| Supported Targets | ESP32-H4 | ESP32-S31 |
| ----------------- | -------- | --------- |
# BLE BIG Receiver Example
# BIG Receiver
(See the README.md file in the upper level `examples` directory for more information about examples.)
This example demonstrates the **Bluetooth LE Synchronized Receiver** functionality. It acts as a BIS (Broadcast Isochronous Stream) sink: it scans for extended advertising, establishes periodic advertising synchronization with a broadcaster, receives BIGInfo from the periodic advertising, and synchronizes to the BIG to receive isochronous data on the BIS channels. Use it together with the [big_broadcaster](../big_broadcaster) example on another device as the source.
## Overview
The implementation uses the NimBLE host stack with ISO support and the ESP-BLE-ISO APIs (periodic sync, BIG sync, data path, channel operations). It is intended for chips that support BLE 5.2 ISO (e.g. ESP32-H4). The target broadcaster name is hardcoded as `BIG Broadcaster` and the broadcast code is hardcoded as `1234`; these must match the [big_broadcaster](../big_broadcaster) defaults.
This example demonstrates a raw BLE Isochronous Broadcast Receiver — it joins a Broadcast Isochronous Group (BIG) directly at the ISO transport layer over the NimBLE host, without any BLE Audio profile (BAP/CAP) on top.
The device acts as the **receiver (BIG sync sink)**: it scans for an extended advertiser by device name, synchronises to the peer's periodic advertising train, parses the BIGInfo report, and then issues `LE BIG Create Sync` to join two BIS sub-events. An HCI output data path is installed on each BIS and incoming SDUs are accounted for by a small RX-metrics helper.
The payloads received are **application-supplied dummy data** produced by `big_broadcaster`; this example does not decode LE-Audio / LC3 — the coding format on the data path is `ESP_BLE_ISO_CODING_FORMAT_TRANSPARENT`.
## Requirements
* A board with Bluetooth LE 5.2 and ISO support (e.g. ESP32-H4)
* Another device running the [big_broadcaster](../big_broadcaster) example, which performs periodic advertising and creates the BIG
* A board with BLE 5.2 and ISO support (e.g. ESP32-H4, ESP32-S31)
* Peer device running the paired example
## How to Use Example
## Configuration
Before project configuration and build, set the correct chip target:
```bash
idf.py menuconfig
```
No build-time options — runtime defaults are baked into source.
Notable hard-coded parameters in `main/main.c`:
* `TARGET_DEVICE_NAME` — `"BIG Broadcaster"` (matched against the AD complete-name field)
* `TARGET_BROADCAST_CODE` — `"1234"` (must match the broadcaster)
* `SCAN_INTERVAL` / `SCAN_WINDOW` — 100 ms / 100 ms (passive)
* `PA_SYNC_TIMEOUT` — 10 s
* `BIG_SYNC_TIMEOUT` — 1 s
* `BIS_ISO_CHAN_COUNT` — 2
### Security & Pairing
The shared init at `../common_components/example_init/ble_iso_example_init.c` configures Just-Works pairing (LE Secure Connections, no MITM, `BLE_SM_IO_CAP_NO_IO`) with bonding enabled, and leaves `gatts_register_cb = NULL` (no GATT services). These settings are not exercised by this example — the receiver passively syncs to PA + BIS without ATT or pairing.
## Build & Flash
```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.
(Exit serial monitor with `Ctrl-]`.)
## Example Flow
1. **Initialization**: NVS, Bluetooth stack (NimBLE), and ISO common layer (`esp_ble_iso_common_init`) with GAP callback for scan, periodic sync, and BIGInfo events.
2. **Extended scan**: Start passive extended scanning. On extended scan report, parse advertising data for the complete local name.
3. **Periodic advertising sync**: When the advertised name matches the hardcoded target name `BIG Broadcaster` and the advertiser has periodic advertising, create a periodic advertising synchronization to that advertiser.
4. **BIGInfo and BIG sync**: When BIGInfo is received in the periodic advertising, create a BIG sync (`esp_ble_iso_big_sync`) with two BIS channels (hardcoded in this example; must match the [big_broadcaster](../big_broadcaster)) and the hardcoded broadcast code `1234`.
5. **Receive ISO data**: When each BIS channel is connected, set up the output data path. Incoming ISO SDUs are reported in the receive callback; the example counts valid, error, and lost packets and logs periodically.
1. NVS, NimBLE host, and the ISO common layer are initialised; a GAP application callback is registered for ISO-related GAP events.
2. Passive extended scanning is started via `ble_gap_disc()`.
3. On each `EXT_SCAN_RECV` the AD payload is parsed; if the complete-name field equals `"BIG Broadcaster"` and the report carries a non-zero periodic-advertising interval, `ble_gap_periodic_adv_sync_create()` is called.
4. On `PA_SYNC` success the extended scan is cancelled (BIGInfo arrives over the PA channel anyway).
5. On the first `BIGINFO_RECV` event the receiver fills `esp_ble_iso_big_sync_param_t` (both BIS in `bis_bitfield`, broadcast code `"1234"`, MSE = `nse` from BIGInfo) and calls `esp_ble_iso_big_sync()` — the HCI `LE BIG Create Sync` command.
6. When each BIS becomes ready, the connected callback resets that BIS's RX metrics and installs an HCI output data path (`ESP_BLE_ISO_DATA_PATH_DIR_OUTPUT`, transparent coding).
7. Each `recv` callback updates the per-BIS counters; one log line is emitted every `LOG_INTERVAL_PACKETS` SDUs.
8. On BIS disconnect or PA-sync-lost the receiver clears its sync state and restarts extended scanning.
## Example Output
## Expected Log
Tag: `BIG_SNC`.
Discovery and PA sync:
```
I (xxx) BIG_SNC: Extended scan started
I (xxx) BIG_SNC: ISO channel 0x0001 connected
I (xxx) BIG_SNC: ISO channel 0x0002 connected
I (xxx) BIG_SNC: Received 1000(1000/0/0) ISO data packets (chan 0x...)
...
I (xxx) BIG_SNC: Scanning for broadcaster...
I (xxx) BIG_SNC: PA synced: handle <h> sid <s> phy <p> peer xx:xx:xx:xx:xx:xx
```
If periodic sync is lost:
BIS bring-up:
```
I (xxx) BIG_SNC: PA sync lost, reason ...
I (xxx) BIG_SNC: [BIS #0] Connected
I (xxx) BIG_SNC: [BIS #1] Connected
```
Steady-state (logged every `LOG_INTERVAL_PACKETS` SDUs by the shared RX helper):
```
I (xxx) BIG_SNC: [BIS #0] RX: <N> packets
I (xxx) BIG_SNC: [BIS #1] RX: <N> packets
```
Teardown / re-sync (broadcaster disappears or restarts; reason `0x08` is the typical timeout, `0x3D` is the rare MIC-fail race documented in source):
```
I (xxx) BIG_SNC: [BIS #0] Disconnected, reason 0x08
I (xxx) BIG_SNC: [BIS #1] Disconnected, reason 0x08
I (xxx) BIG_SNC: PA sync lost: sync_handle <h> reason 0x<rr>
I (xxx) BIG_SNC: Scanning for broadcaster...
```
## Peer Pairing
Run [big_broadcaster](../big_broadcaster/) on a second board.
1. Flash and run `big_broadcaster` on board A; it begins extended + periodic advertising and creates the BIG.
2. Flash and run `big_receiver` on board B; it logs `Scanning for broadcaster...` and waits.
3. Board B matches the AD name `"BIG Broadcaster"`, creates a PA sync, logs `PA synced`, and stops the extended scan.
4. On the first BIGInfo report, board B issues `LE BIG Create Sync` with broadcast code `"1234"` and both BIS selected; both sides log `[BIS #0/1] Connected`.
5. Per-BIS RX-packet milestones are logged on B at the same cadence that A logs TX milestones.
6. Resetting board A causes B to log a BIS disconnect and `PA sync lost`, then automatically restart scanning to re-pair.
@@ -5,6 +5,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <string.h>
#include <assert.h>
@@ -55,27 +56,54 @@ static void iso_connected_cb(esp_ble_iso_chan_t *chan)
.pid = ESP_BLE_ISO_DATA_PATH_HCI,
.format = ESP_BLE_ISO_CODING_FORMAT_TRANSPARENT,
};
int chan_idx = bis_chan_index_get(chan);
esp_err_t err;
ESP_LOGI(TAG, "ISO channel %p connected", chan);
ESP_LOGI(TAG, "[BIS #%d] Connected", chan_idx);
/* New BIS session — reset RX counters so milestones reflect
* this session only. Matches the session-start reset pattern
* used by cis_peripheral and the audio examples.
*/
if (chan_idx >= 0) {
example_iso_rx_metrics_reset(&rx_metrics[chan_idx]);
}
err = esp_ble_iso_setup_data_path(chan, ESP_BLE_ISO_DATA_PATH_DIR_OUTPUT, &data_path);
if (err) {
ESP_LOGE(TAG, "Failed to setup ISO data path, err %d", err);
ESP_LOGE(TAG, "[BIS #%d] Failed to setup data path, err %d", chan_idx, err);
return;
}
}
static void iso_disconnected_cb(esp_ble_iso_chan_t *chan, uint8_t reason)
{
ESP_LOGI(TAG, "ISO channel %p disconnected, reason 0x%02x", chan, reason);
/* Common BIS disconnect reasons during broadcaster restart:
*
* 0x08 CONN_TIMEOUT - no subevent received within
* BIG_Sync_Timeout (broadcaster is gone
* or out of range). This is the common
* case.
*
* 0x3D TERM_DUE_TO_MIC_FAIL - packets arrived but MIC check
* failed repeatedly. Only possible on
* encrypted BIGs; typically means the
* broadcaster restarted with a new
* session before we timed out, so the
* old session key no longer decrypts.
*
* !!! LOW PROBABILITY !!!
* Requires the broadcaster to come
* back on air within the BIG_Sync_
* Timeout window — a narrow race.
*
* Both are normal: receiver will drop PA sync and re-discover.
*/
ESP_LOGI(TAG, "[BIS #%d] Disconnected, reason 0x%02x",
bis_chan_index_get(chan), reason);
big_synced = false;
out_big = NULL;
for (size_t i = 0; i < BIS_ISO_CHAN_COUNT; i++) {
example_iso_rx_metrics_reset(&rx_metrics[i]);
}
}
static void iso_recv_cb(esp_ble_iso_chan_t *chan,
@@ -83,14 +111,16 @@ static void iso_recv_cb(esp_ble_iso_chan_t *chan,
const uint8_t *data, uint16_t len)
{
int chan_idx = bis_chan_index_get(chan);
char name[24];
if (chan_idx < 0) {
ESP_LOGW(TAG, "Unknown BIS channel %p", chan);
ESP_LOGW(TAG, "Unknown BIS channel");
return;
}
snprintf(name, sizeof(name), "BIS #%d", chan_idx);
rx_metrics[chan_idx].last_sdu_len = len;
example_iso_rx_metrics_on_recv(info, &rx_metrics[chan_idx], TAG, "chan", chan);
example_iso_rx_metrics_on_recv(info, &rx_metrics[chan_idx], TAG, name);
}
static esp_ble_iso_chan_ops_t iso_ops = {
@@ -150,7 +180,7 @@ static void ext_scan_start(void)
return;
}
ESP_LOGI(TAG, "Extended scan started");
ESP_LOGI(TAG, "Scanning for broadcaster...");
}
static int pa_sync_create(uint8_t addr_type, uint8_t addr[6], uint8_t sid)
@@ -213,26 +243,33 @@ static void ext_scan_recv(esp_ble_iso_gap_app_event_t *event)
static void pa_sync(esp_ble_iso_gap_app_event_t *event)
{
int err;
if (event->pa_sync.status) {
ESP_LOGE(TAG, "PA sync failed, status %d", event->pa_sync.status);
per_adv_synced = false;
return;
}
ESP_LOGI(TAG, "PA sync established:");
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,
ESP_LOGI(TAG, "PA synced: handle %u sid %u phy %u peer %02x:%02x:%02x:%02x:%02x:%02x",
event->pa_sync.sync_handle, event->pa_sync.sid, event->pa_sync.adv_phy,
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);
event->pa_sync.addr.val[1], event->pa_sync.addr.val[0]);
/* PA sync is established; the BIGInfo report will arrive via the
* PA sync channel, so the extended scanner is no longer needed.
* Stop it now — pa_sync_lost() will restart it on loss.
*/
err = ble_gap_disc_cancel();
if (err) {
ESP_LOGW(TAG, "Failed to stop scanning, err %d", err);
}
}
static void pa_sync_lost(esp_ble_iso_gap_app_event_t *event)
{
ESP_LOGI(TAG, "PA sync lost: sync_handle 0x%04x reason 0x%02x",
ESP_LOGI(TAG, "PA sync lost: sync_handle %u reason 0x%02x",
event->pa_sync_lost.sync_handle, event->pa_sync_lost.reason);
per_adv_synced = false;
@@ -5,51 +5,83 @@
(See the README.md file in the upper level `examples` directory for more information about examples.)
This example demonstrates how to use a **Connected Isochronous Stream (CIS)** as a central. It scans for a peripheral, establishes an ACL connection, creates a Connected Isochronous Group (CIG) and a CIS to the peer, and then sends isochronous data over the CIS every 10 ms. Run it together with the [cis_peripheral](../cis_peripheral) example on another device as the peer.
## Overview
The implementation uses the NimBLE host stack with ISO support and the ESP-BLE-ISO APIs (CIG create, CIS connect, data path, channel send). It is intended for chips that support BLE 5.2 ISO (e.g. ESP32-H4). The target peripheral name is hardcoded as `CIS Peripheral` and the connection security level is hardcoded as `ESP_BLE_ISO_SECURITY_MITM` (Level 3: encryption and authentication); the central initiates pairing after the ACL connection.
This is a raw BLE Connected Isochronous Stream (CIS) example operating directly at the ISO transport layer. It is **not** a BAP/CAP (BLE Audio profile) example — it does not implement Unicast Server/Client, ASCS, PACS, or any LC3 codec; it only exercises the underlying CIG/CIS plumbing.
The central scans for a peer advertising the name `CIS Peripheral`, opens an ACL link, optionally pairs (security level `ESP_BLE_ISO_SECURITY_NO_MITM`), creates a single-CIS CIG (10 ms SDU interval, 2M PHY, RTN 2, 120-byte SDU, sequential/unframed), connects the CIS, configures the input data path to the HCI in transparent format, and then drives a software TX scheduler that submits one SDU every 10 ms.
The transmitted payload is a dummy buffer filled with the current sequence number byte — there is no real audio data, the example just demonstrates the ISO transport mechanics on top of the NimBLE host with ISO support and the `esp_ble_iso_*` APIs.
## Requirements
* A board with Bluetooth LE 5.2 and ISO support (e.g. ESP32-H4)
* Another device running the [cis_peripheral](../cis_peripheral) example, which advertises and accepts the ACL and CIS
* A board with BLE 5.2 and ISO support (e.g. ESP32-H4, ESP32-S31)
* Peer device running the paired example
## How to Use Example
## Configuration
Before project configuration and build, set the correct chip target:
```bash
idf.py menuconfig
```
No build-time options — runtime defaults are baked into source.
### Security & Pairing
Just-Works pairing (LE Secure Connections, no MITM, `BLE_SM_IO_CAP_NO_IO`) with bonding enabled, inherited from the shared host init in `../common_components/example_init/ble_iso_example_init.c`. ISO examples do not register any GATT services (`gatts_register_cb = NULL`).
## Build & Flash
```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.
(Exit serial monitor with `Ctrl-]`.)
## Example Flow
1. **Initialization**: NVS, Bluetooth stack (NimBLE), and ISO common layer (`esp_ble_iso_common_init`) with GAP callback for scan, ACL, and security events.
2. **Extended scan**: Start passive extended scanning. On scan report, parse the complete local name; when it matches the hardcoded target name `CIS Peripheral`, initiate an ACL connection to that device.
3. **ACL connection**: On connection established, initiate pairing (security level is hardcoded as `ESP_BLE_ISO_SECURITY_MITM`); the CIG and CIS are created after security is established.
4. **Security**: On security change (after pairing), create the CIG and CIS.
5. **CIG and CIS**: Create a CIG with one CIS (10 ms latency each direction, 10 ms SDU interval, sequential packing, unframed), then connect the CIS over the ACL handle.
6. **Send ISO data**: When the CIS is connected and the input data path is set up, a periodic TX scheduler based on `k_work_delayable` sends an SDU on the CIS every 10 ms in the ISO task context; sequence numbers and drift handling are applied. Note that the scheduler timer resolution is in milliseconds, which may not match the exact SDU interval for all configurations.
1. Initialize NVS, NimBLE host, and the ISO common layer with a GAP callback.
2. Start passive extended scanning for a device whose Complete Local Name is `CIS Peripheral`.
3. Cancel scan and create an ACL connection (interval 80 ms, supervision 5 s) once the target is matched.
4. On ACL connect, initiate pairing because the configured security level is `ESP_BLE_ISO_SECURITY_NO_MITM`.
5. After the security change, call `esp_ble_iso_cig_create` (one CIS, 10 ms latencies and SDU interval, sequential/unframed, SCA unknown) and `esp_ble_iso_chan_connect` over the ACL handle.
6. On CIS connect, set up the input data path (HCI / transparent) and start the periodic TX scheduler.
7. The scheduler invokes `esp_ble_iso_chan_send` every 10 ms with an incrementing sequence number on a 120-byte dummy SDU.
## Example Output
## Expected Log
TAG: `CIS_CEN`
Scan and connection phase:
```
I (xxx) CIS_CEN: connection established, handle 0 status 0x00
I (xxx) CIS_CEN: ISO channel 0x... connected
I (xxx) CIS_CEN: Transmitted 1000 ISO data packets (chan 0x...)
...
I CIS_CEN: Scanning for peripheral...
I CIS_CEN: Connected: handle <h> role <r> peer XX:XX:XX:XX:XX:XX
I CIS_CEN: Security: handle <h> level <l> bonded <b>
```
If connection or security fails, relevant status or error messages are logged.
CIS setup and streaming phase (TX log emitted every `LOG_INTERVAL_PACKETS` SDUs by the shared utility):
```
I CIS_CEN: [CIS #0] Connected
I CIS_CEN: [CIS #0] TX: <count> packets
```
Disconnect path:
```
I CIS_CEN: [CIS #0] Disconnected, reason 0x<rr>
I CIS_CEN: Disconnected: handle <h> reason 0x<rr>
```
## Peer Pairing
Run [cis_peripheral](../cis_peripheral/) on a second board.
1. Flash and run `cis_peripheral` first; it begins extended advertising as `CIS Peripheral`.
2. Flash and run `cis_central` on the second board; it scans and matches that name.
3. The central creates the ACL connection and initiates pairing.
4. After the security change, the central creates the CIG and connects the CIS.
5. Both sides set up their data paths (input on central, output on peripheral).
6. The central streams 120-byte SDUs every 10 ms; the peripheral reports `RX: <count> packets` periodically.
@@ -40,7 +40,7 @@
#define CONN_MIN_CE_LEN 0xFFFF
#define CONN_DURATION 10000 /* 10s */
#define SECURITY_LEVEL ESP_BLE_ISO_SECURITY_MITM
#define SECURITY_LEVEL ESP_BLE_ISO_SECURITY_NO_MITM
#define CIG_LATENCY_MS 10 /* 10ms */
#define CIG_SDU_INTERVAL_US 10000 /* 10ms */
@@ -70,11 +70,11 @@ static void iso_connected_cb(esp_ble_iso_chan_t *chan)
};
esp_err_t err;
ESP_LOGI(TAG, "ISO channel %p connected", chan);
ESP_LOGI(TAG, "[CIS #0] Connected");
err = esp_ble_iso_setup_data_path(chan, ESP_BLE_ISO_DATA_PATH_DIR_INPUT, &data_path);
if (err) {
ESP_LOGE(TAG, "Failed to setup ISO data path, err %d", err);
ESP_LOGE(TAG, "[CIS #0] Failed to setup data path, err %d", err);
return;
}
@@ -84,7 +84,7 @@ static void iso_connected_cb(esp_ble_iso_chan_t *chan)
/* Note: esp timer is not accurate enough */
err = example_iso_tx_scheduler_start(&tx_scheduler, CIG_SDU_INTERVAL_US);
if (err) {
ESP_LOGE(TAG, "Failed to start tx scheduler, err %d", err);
ESP_LOGE(TAG, "[CIS #0] Scheduler start failed, err %d", err);
return;
}
@@ -95,17 +95,28 @@ static void iso_disconnected_cb(esp_ble_iso_chan_t *chan, uint8_t reason)
{
esp_err_t err;
ESP_LOGI(TAG, "ISO channel %p disconnected, reason 0x%02x", chan, reason);
ESP_LOGI(TAG, "[CIS #0] Disconnected, reason 0x%02x", reason);
err = example_iso_tx_scheduler_stop(&tx_scheduler);
if (err) {
ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err);
ESP_LOGE(TAG, "[CIS #0] Scheduler stop failed, err %d", err);
}
/* Per BT Core 6.0 §7.7.5: a CIS on the Central retains its handle and
* data path across HCI_Disconnection_Complete (only the Peripheral side
* and other connection types get auto-deleted by the controller). So on
* the Central we must remove explicitly, otherwise the next setup on
* reconnect returns Command Disallowed.
*/
err = esp_ble_iso_remove_data_path(chan, ESP_BLE_ISO_DATA_PATH_DIR_INPUT);
if (err) {
ESP_LOGE(TAG, "[CIS #0] Failed to remove data path, err %d", err);
}
}
static void iso_sent_cb(esp_ble_iso_chan_t *chan, void *user_data)
{
example_iso_tx_scheduler_on_sent(&tx_scheduler, user_data, TAG, "chan", chan);
example_iso_tx_scheduler_on_sent(&tx_scheduler, user_data, TAG, "CIS #0");
}
static esp_ble_iso_chan_ops_t iso_ops = {
@@ -181,7 +192,7 @@ static void iso_chan_send(void)
sizeof(iso_data),
iso_seq_num);
if (err) {
ESP_LOGD(TAG, "Failed to transmit data on channel %p", &iso_chan);
ESP_LOGD(TAG, "[CIS #0] Send failed, err %d", err);
return;
}
@@ -217,7 +228,7 @@ static void ext_scan_start(void)
return;
}
ESP_LOGI(TAG, "Extended scan started");
ESP_LOGI(TAG, "Scanning for peripheral...");
}
static int conn_create(uint8_t addr_type, uint8_t addr[6])
@@ -305,18 +316,16 @@ static void acl_connect(esp_ble_iso_gap_app_event_t *event)
int err;
if (event->acl_connect.status) {
ESP_LOGE(TAG, "connection failed, status %d", event->acl_connect.status);
ESP_LOGE(TAG, "Connection failed, status %d", event->acl_connect.status);
acl_connected = false;
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]);
ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x",
event->acl_connect.conn_handle, 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]);
if (iso_chan.required_sec_level == ESP_BLE_ISO_SECURITY_NO_MITM ||
iso_chan.required_sec_level == ESP_BLE_ISO_SECURITY_MITM) {
@@ -332,7 +341,7 @@ static void acl_connect(esp_ble_iso_gap_app_event_t *event)
static void acl_disconnect(esp_ble_iso_gap_app_event_t *event)
{
ESP_LOGI(TAG, "Conn terminated: conn_handle 0x%04x reason 0x%02x",
ESP_LOGI(TAG, "Disconnected: handle %u reason 0x%02x",
event->acl_disconnect.conn_handle, event->acl_disconnect.reason);
acl_connected = false;
@@ -343,19 +352,14 @@ static void acl_disconnect(esp_ble_iso_gap_app_event_t *event)
static void security_change(esp_ble_iso_gap_app_event_t *event)
{
if (event->security_change.status) {
ESP_LOGE(TAG, "security change failed, status %d", 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]);
ESP_LOGI(TAG, "Security: handle %u level %u bonded %u",
event->security_change.conn_handle,
event->security_change.sec_level,
event->security_change.bonded);
create_cig_and_cis(event->security_change.conn_handle);
}
@@ -6,6 +6,7 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_ISO=y
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
@@ -5,52 +5,84 @@
(See the README.md file in the upper level `examples` directory for more information about examples.)
This example demonstrates how to use a **Connected Isochronous Stream (CIS)** as a peripheral. It starts connectable extended advertising with the name `CIS Peripheral`, waits for a central to connect and set up an isochronous channel, and then receives and counts isochronous data on the CIS. Run it together with the [cis_central](../cis_central) example on another device as the central.
## Overview
The implementation uses the NimBLE host stack with ISO support and the ESP-BLE-ISO APIs (ISO server register, accept callback, data path, channel receive). It is intended for chips that support BLE 5.2 ISO (e.g. ESP32-H4). The connection security level is hardcoded as `ESP_BLE_ISO_SECURITY_MITM` (Level 3: encryption and authentication); it must match the [cis_central](../cis_central) side for pairing to succeed.
This is a raw BLE Connected Isochronous Stream (CIS) example operating directly at the ISO transport layer. It is **not** a BAP/CAP (BLE Audio profile) example — it does not implement Unicast Server, ASCS, PACS, or any LC3 codec; it only exercises the underlying CIS accept/receive plumbing.
The peripheral starts connectable extended advertising under the name `CIS Peripheral` (1M primary / 2M secondary PHY, 200 ms interval), registers an ISO server with security level `ESP_BLE_ISO_SECURITY_NO_MITM` and an accept callback that hands out the single CIS channel slot, sets up the output data path to the HCI in transparent format when the CIS connects, and tallies received SDUs through the shared RX-metrics helper.
There is no audio decoding — incoming SDUs are simply counted (valid / error / lost / null), so any 120-byte dummy payload from the central is consumed as opaque data by the NimBLE host with ISO support and the `esp_ble_iso_*` APIs.
## Requirements
* A board with Bluetooth LE 5.2 and ISO support (e.g. ESP32-H4)
* Another device running the [cis_central](../cis_central) example, which scans, connects, and creates the CIS
* A board with BLE 5.2 and ISO support (e.g. ESP32-H4, ESP32-S31)
* Peer device running the paired example
## How to Use Example
## Configuration
Before project configuration and build, set the correct chip target:
```bash
idf.py menuconfig
```
No build-time options — runtime defaults are baked into source.
### Security & Pairing
Just-Works pairing (LE Secure Connections, no MITM, `BLE_SM_IO_CAP_NO_IO`) with bonding enabled, inherited from the shared host init in `../common_components/example_init/ble_iso_example_init.c`. ISO examples do not register any GATT services (`gatts_register_cb = NULL`).
## Build & Flash
```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.
(Exit serial monitor with `Ctrl-]`.)
## Example Flow
1. **Initialization**: NVS, Bluetooth stack (NimBLE), and ISO common layer (`esp_ble_iso_common_init`) with GAP callback for ACL and security events.
2. **ISO server**: Register an ISO server with an accept callback and the configured security level. When a central requests a CIS, the accept callback returns the single CIS channel.
3. **Extended advertising**: Start connectable extended advertising with the local name `CIS Peripheral` so the central can discover and connect.
4. **ACL connection**: When the central connects, the connection and optional pairing/security change are handled; the central then creates the CIG and CIS.
5. **CIS connected**: When the CIS is connected, set up the output data path (receive direction). Incoming ISO SDUs are delivered in the receive callback; the example counts valid, error, and lost packets and logs periodically.
1. Initialize NVS, NimBLE host, and the ISO common layer with a GAP callback.
2. Register an ISO server (`esp_ble_iso_server_register`) whose accept callback returns the single static CIS channel.
3. Build flags + complete-local-name AD payload and start connectable extended advertising on handle 0 at a 200 ms interval.
4. On ACL connect, log the peer; the central drives subsequent pairing and CIG/CIS creation.
5. When the CIS request arrives, the accept callback supplies the channel; on CIS connect, set up the output data path (HCI / transparent) and reset RX metrics.
6. Each received SDU updates valid/error/lost counters; periodic RX summaries are logged by the shared utility.
7. On ACL disconnect, restart extended advertising.
## Example Output
## Expected Log
TAG: `CIS_PER`
Advertising and connection phase:
```
I (xxx) CIS_PER: Extended adv instance 0 started
I (xxx) CIS_PER: connection established, handle 0 status 0x00
I (xxx) CIS_PER: Incoming request from 0x0000
I (xxx) CIS_PER: ISO channel 0x... connected
I (xxx) CIS_PER: Received 1000(1000/0/0) ISO data packets (chan 0x...)
...
I CIS_PER: Advertising started (handle 0)
I CIS_PER: Connected: handle <h> role <r> peer XX:XX:XX:XX:XX:XX
I CIS_PER: Security: handle <h> level <l> bonded <b>
```
If connection or security fails, relevant status or error messages are logged.
CIS accept and streaming phase (RX log emitted every `LOG_INTERVAL_PACKETS` SDUs by the shared utility):
```
I CIS_PER: Incoming CIS request from handle <h>
I CIS_PER: [CIS #0] Connected
I CIS_PER: [CIS #0] RX: <count> packets
```
Disconnect path:
```
I CIS_PER: [CIS #0] Disconnected, reason 0x<rr>
I CIS_PER: Disconnected: handle <h> reason 0x<rr>
```
## Peer Pairing
Run [cis_central](../cis_central/) on a second board.
1. Flash and run `cis_peripheral` first; it advertises as `CIS Peripheral` on extended-advertising handle 0.
2. Flash and run `cis_central` on the second board; it scans and matches the name.
3. The central opens the ACL connection and initiates pairing; this peripheral handles the security change passively.
4. The central creates the CIG and CIS; the peripheral's accept callback returns the local channel.
5. The peripheral configures its output data path on CIS connection.
6. Incoming 120-byte SDUs are counted and `RX: <count> packets` is logged periodically.
@@ -35,7 +35,7 @@
#define ADV_SECONDARY_PHY BLE_HCI_LE_PHY_2M
#define ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(200)
#define SECURITY_LEVEL ESP_BLE_ISO_SECURITY_MITM
#define SECURITY_LEVEL ESP_BLE_ISO_SECURITY_NO_MITM
#define CIS_SDU_SIZE 120
@@ -51,11 +51,11 @@ static void iso_connected_cb(esp_ble_iso_chan_t *chan)
};
esp_err_t err;
ESP_LOGI(TAG, "ISO channel %p connected", chan);
ESP_LOGI(TAG, "[CIS #0] Connected");
err = esp_ble_iso_setup_data_path(chan, ESP_BLE_ISO_DATA_PATH_DIR_OUTPUT, &data_path);
if (err) {
ESP_LOGE(TAG, "Failed to setup ISO data path, err %d", err);
ESP_LOGE(TAG, "[CIS #0] Failed to setup data path, err %d", err);
return;
}
@@ -64,16 +64,15 @@ static void iso_connected_cb(esp_ble_iso_chan_t *chan)
static void iso_disconnected_cb(esp_ble_iso_chan_t *chan, uint8_t reason)
{
ESP_LOGI(TAG, "ISO channel %p disconnected, reason 0x%02x", chan, reason);
ESP_LOGI(TAG, "[CIS #0] Disconnected, reason 0x%02x", reason);
}
static void iso_recv_cb(esp_ble_iso_chan_t *chan,
const esp_ble_iso_recv_info_t *info,
const uint8_t *data, uint16_t len)
{
rx_metrics.last_sdu_len = len;
example_iso_rx_metrics_on_recv(info, &rx_metrics, TAG, "chan", chan);
example_iso_rx_metrics_on_recv(info, &rx_metrics, TAG, "CIS #0");
}
static esp_ble_iso_chan_ops_t iso_ops = {
@@ -99,7 +98,7 @@ static esp_ble_iso_chan_t iso_chan = {
static int iso_accept(const esp_ble_iso_accept_info_t *info,
esp_ble_iso_chan_t **chan)
{
ESP_LOGI(TAG, "Incoming request from 0x%04x", info->acl->handle);
ESP_LOGI(TAG, "Incoming CIS request from handle %u", info->acl->handle);
if (iso_chan.iso) {
ESP_LOGE(TAG, "No channels available");
@@ -177,23 +176,26 @@ static void ext_adv_start(void)
return;
}
ESP_LOGI(TAG, "Extended adv instance %u started", ADV_HANDLE);
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
}
static void acl_connect(esp_ble_iso_gap_app_event_t *event)
{
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]);
if (event->acl_connect.status) {
ESP_LOGE(TAG, "Connection failed, status %d", event->acl_connect.status);
return;
}
ESP_LOGI(TAG, "Connected: handle %u role %u peer %02x:%02x:%02x:%02x:%02x:%02x",
event->acl_connect.conn_handle, 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_iso_gap_app_event_t *event)
{
ESP_LOGI(TAG, "Conn terminated: conn_handle 0x%04x reason 0x%02x",
ESP_LOGI(TAG, "Disconnected: handle %u reason 0x%02x",
event->acl_disconnect.conn_handle, event->acl_disconnect.reason);
ext_adv_start();
@@ -201,15 +203,15 @@ static void acl_disconnect(esp_ble_iso_gap_app_event_t *event)
static void security_change(esp_ble_iso_gap_app_event_t *event)
{
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]);
if (event->security_change.status) {
ESP_LOGE(TAG, "Security change failed, status %d", event->security_change.status);
return;
}
ESP_LOGI(TAG, "Security: handle %u level %u bonded %u",
event->security_change.conn_handle,
event->security_change.sec_level,
event->security_change.bonded);
}
static void iso_gap_app_cb(esp_ble_iso_gap_app_event_t *event)
@@ -6,6 +6,7 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_EXT_ADV=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_ISO=y
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
@@ -21,7 +21,7 @@
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"
#define TAG "INIT"
#define TAG "EXAMPLE_INIT"
static SemaphoreHandle_t example_iso_sem;
@@ -74,15 +74,13 @@ esp_err_t bluetooth_init(void)
ble_hs_cfg.reset_cb = example_iso_on_reset;
ble_hs_cfg.sync_cb = example_iso_on_sync;
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
#if 0
ble_hs_cfg.gatts_register_cb = NULL;
ble_hs_cfg.sm_our_key_dist = 1;
ble_hs_cfg.sm_their_key_dist = 1;
ble_hs_cfg.sm_our_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
ble_hs_cfg.sm_their_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
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();
@@ -17,7 +17,10 @@
#include "esp_ble_iso_common_api.h"
#define TAG "UTILS"
/* Packet count between throughput summary logs.
* At 10 ms SDU interval this is ~60 s; at 7.5 ms ~45 s.
*/
#define LOG_INTERVAL_PACKETS 6000
int example_iso_gap_event_cb(struct ble_gap_event *event, void *arg)
{
@@ -94,8 +97,7 @@ int example_iso_tx_scheduler_stop(example_iso_tx_scheduler_t *scheduler)
void example_iso_tx_scheduler_on_sent(example_iso_tx_scheduler_t *scheduler,
const esp_ble_iso_tx_cb_info_t *info,
const char *tag,
const char *obj_name,
const void *obj)
const char *obj_name)
{
size_t drift_count = 0;
@@ -111,9 +113,9 @@ void example_iso_tx_scheduler_on_sent(example_iso_tx_scheduler_t *scheduler,
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);
if (scheduler->count % LOG_INTERVAL_PACKETS == 0) {
ESP_LOGI(tag, "[%s] TX: %u packets",
obj_name ? obj_name : "ISO", scheduler->count);
}
}
@@ -132,8 +134,7 @@ void example_iso_rx_metrics_reset(example_iso_rx_metrics_t *metrics)
void example_iso_rx_metrics_on_recv(const esp_ble_iso_recv_info_t *info,
example_iso_rx_metrics_t *metrics,
const char *tag,
const char *obj_name,
const void *obj)
const char *obj_name)
{
assert(info);
assert(metrics);
@@ -155,8 +156,8 @@ void example_iso_rx_metrics_on_recv(const esp_ble_iso_recv_info_t *info,
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);
if (metrics->recv_count % LOG_INTERVAL_PACKETS == 0) {
ESP_LOGI(tag, "[%s] RX: %u packets",
obj_name ? obj_name : "ISO", metrics->recv_count);
}
}
@@ -93,15 +93,13 @@ int example_iso_tx_scheduler_stop(example_iso_tx_scheduler_t *scheduler);
void example_iso_tx_scheduler_on_sent(example_iso_tx_scheduler_t *scheduler,
const esp_ble_iso_tx_cb_info_t *info,
const char *tag,
const char *obj_name,
const void *obj);
const char *obj_name);
void example_iso_rx_metrics_reset(example_iso_rx_metrics_t *metrics);
void example_iso_rx_metrics_on_recv(const esp_ble_iso_recv_info_t *info,
example_iso_rx_metrics_t *metrics,
const char *tag,
const char *obj_name,
const void *obj);
const char *obj_name);
#endif /* BLE_ISO_EXAMPLE_UTILS_H_ */