mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-01 18:50:34 +03:00
feat(ble_audio): Support running ISO & LE Audio with Bluedroid Host
This commit is contained in:
@@ -9,9 +9,9 @@
|
||||
|
||||
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.
|
||||
The build runs on top of the selected BLE host stack (Bluedroid by default; NimBLE via the `sdkconfig.defaults.nimble` overlay) 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.
|
||||
After PA sync is established, `scan_stop()` halts the extended scanner — BASE/BIGInfo arrive over the PA channel — and `pa_sync_lost()` calls `scan_start()` to re-arm. The host-specific GAP/PA-sync plumbing lives in `main/bluedroid/scan.c` and `main/nimble/scan.c`; `main.c` only sees the host-agnostic interface in `scan.h`.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -34,11 +34,24 @@ The example inherits a Just-Works pairing model (LE Secure Connections, no MITM,
|
||||
|
||||
## Build & Flash
|
||||
|
||||
The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`.
|
||||
|
||||
### Bluedroid host (default)
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32h4 # or esp32s31
|
||||
idf.py set-target esp32h4
|
||||
idf.py -p PORT flash monitor
|
||||
```
|
||||
|
||||
### NimBLE host
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32h4
|
||||
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor
|
||||
```
|
||||
|
||||
For `esp32s31`, replace the chip overlay accordingly.
|
||||
|
||||
(Exit serial monitor with `Ctrl-]`.)
|
||||
|
||||
## Example Flow
|
||||
@@ -46,8 +59,8 @@ idf.py -p PORT flash monitor
|
||||
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`.
|
||||
4. `scan_init()` performs host-specific GAP setup (Bluedroid: registers the GAP callback for `*_COMPLETE_EVT` semaphore signalling + posts `EXT_ADV_REPORT` / `PERIODIC_ADV_SYNC_ESTAB` / `PERIODIC_ADV_REPORT` / `PERIODIC_ADV_SYNC_LOST` to the audio engine; NimBLE: no-op — the scan-instance callback is passed at `ble_gap_disc` / `ble_gap_periodic_adv_sync_create` time). `scan_start()` then 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(addr_type, addr, sid)` invokes the host-specific PA-sync create routine (`ble_gap_periodic_adv_sync_create` / `esp_ble_gap_periodic_adv_create_sync`) 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`.
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
set(srcs "main.c")
|
||||
|
||||
idf_component_register(SRCS "${srcs}"
|
||||
if(CONFIG_BT_BLUEDROID_ENABLED)
|
||||
list(APPEND srcs "bluedroid/scan.c")
|
||||
else()
|
||||
list(APPEND srcs "nimble/scan.c")
|
||||
endif()
|
||||
|
||||
idf_component_register(SRCS ${srcs}
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES bt nvs_flash)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#include "esp_bt_defs.h"
|
||||
#include "esp_gap_ble_api.h"
|
||||
|
||||
#include "scan.h"
|
||||
|
||||
static SemaphoreHandle_t scan_sem;
|
||||
|
||||
/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */
|
||||
static esp_bt_status_t scan_op_status;
|
||||
|
||||
#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, scan_sem, portMAX_DELAY, scan_op_status)
|
||||
|
||||
static esp_ble_ext_scan_params_t ext_scan_params = {
|
||||
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
|
||||
.filter_policy = BLE_SCAN_FILTER_ALLOW_ALL,
|
||||
.scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE,
|
||||
.cfg_mask = ESP_BLE_GAP_EXT_SCAN_CFG_UNCODE_MASK,
|
||||
.uncoded_cfg = {
|
||||
.scan_type = BLE_SCAN_TYPE_PASSIVE,
|
||||
.scan_interval = SCAN_INTERVAL,
|
||||
.scan_window = SCAN_WINDOW,
|
||||
},
|
||||
};
|
||||
|
||||
static void gap_event_handler(esp_gap_ble_cb_event_t event,
|
||||
esp_ble_gap_cb_param_t *param)
|
||||
{
|
||||
switch (event) {
|
||||
case ESP_GAP_BLE_SET_EXT_SCAN_PARAMS_COMPLETE_EVT:
|
||||
scan_op_status = param->set_ext_scan_params.status;
|
||||
xSemaphoreGive(scan_sem);
|
||||
break;
|
||||
case ESP_GAP_BLE_EXT_SCAN_START_COMPLETE_EVT:
|
||||
scan_op_status = param->ext_scan_start.status;
|
||||
xSemaphoreGive(scan_sem);
|
||||
break;
|
||||
case ESP_GAP_BLE_EXT_SCAN_STOP_COMPLETE_EVT:
|
||||
scan_op_status = param->ext_scan_stop.status;
|
||||
xSemaphoreGive(scan_sem);
|
||||
break;
|
||||
|
||||
/* PA sync / ext adv events: forwarded by adapter's BTA path, not here. */
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int app_host_init(void)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
scan_sem = xSemaphoreCreateBinary();
|
||||
if (scan_sem == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to create scan semaphore");
|
||||
return -1;
|
||||
}
|
||||
|
||||
err = esp_ble_gap_register_callback(gap_event_handler);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err);
|
||||
vSemaphoreDelete(scan_sem);
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ext_scan_start(void)
|
||||
{
|
||||
WAIT_API(esp_ble_gap_set_ext_scan_params(&ext_scan_params));
|
||||
WAIT_API(esp_ble_gap_start_ext_scan(0, 0));
|
||||
|
||||
ESP_LOGI(TAG, "Scanning for broadcast source...");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
int ext_scan_stop(void)
|
||||
{
|
||||
WAIT_API(esp_ble_gap_stop_ext_scan());
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid)
|
||||
{
|
||||
esp_ble_gap_periodic_adv_sync_params_t params = {
|
||||
.filter_policy = 0,
|
||||
.sid = sid,
|
||||
.addr_type = addr_type,
|
||||
.skip = PA_SYNC_SKIP,
|
||||
.sync_timeout = PA_SYNC_TIMEOUT,
|
||||
};
|
||||
|
||||
memcpy(params.addr, addr, sizeof(params.addr));
|
||||
|
||||
/* Fire-and-forget: sync establishment (PERIODIC_ADV_SYNC_ESTAB_EVT) is
|
||||
* air-dependent and surfaces asynchronously. */
|
||||
return esp_ble_gap_periodic_adv_create_sync(¶ms);
|
||||
}
|
||||
|
||||
int pa_sync_terminate(uint16_t sync_handle)
|
||||
{
|
||||
return esp_ble_gap_periodic_adv_sync_terminate(sync_handle);
|
||||
}
|
||||
@@ -13,10 +13,6 @@
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_system.h"
|
||||
|
||||
#include "host/ble_hs.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
|
||||
#include "esp_ble_audio_lc3_defs.h"
|
||||
#include "esp_ble_audio_bap_api.h"
|
||||
@@ -25,18 +21,13 @@
|
||||
#include "ble_audio_example_init.h"
|
||||
#include "ble_audio_example_utils.h"
|
||||
|
||||
#define TAG "BAP_BSNK"
|
||||
#include "scan.h"
|
||||
|
||||
#define TARGET_DEVICE_NAME "BAP Broadcast Source"
|
||||
#define TARGET_DEVICE_NAME_LEN (sizeof(TARGET_DEVICE_NAME) - 1)
|
||||
|
||||
#define TARGET_BROADCAST_CODE "1234"
|
||||
|
||||
#define SCAN_INTERVAL 160 /* 100ms */
|
||||
#define SCAN_WINDOW 160 /* 100ms */
|
||||
|
||||
#define PA_SYNC_SKIP 0
|
||||
#define PA_SYNC_TIMEOUT 1000 /* 1000 * 10ms = 10s */
|
||||
#define PA_SYNC_HANDLE_INIT UINT16_MAX
|
||||
|
||||
#define CONN_HANDLE_INIT UINT16_MAX
|
||||
@@ -90,61 +81,6 @@ static esp_ble_audio_pacs_cap_t cap = {
|
||||
.codec_cap = &codec_cap,
|
||||
};
|
||||
|
||||
static void ext_scan_start(void)
|
||||
{
|
||||
struct ble_gap_disc_params params = {0};
|
||||
uint8_t own_addr_type;
|
||||
int err;
|
||||
|
||||
err = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
params.passive = 1;
|
||||
params.itvl = SCAN_INTERVAL;
|
||||
params.window = SCAN_WINDOW;
|
||||
|
||||
err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms,
|
||||
example_audio_gap_event_cb, NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start scanning, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Scanning for broadcast source...");
|
||||
}
|
||||
|
||||
static int pa_sync_create(const bt_addr_le_t *addr, uint8_t adv_sid)
|
||||
{
|
||||
struct ble_gap_periodic_sync_params params = {0};
|
||||
ble_addr_t sync_addr = {0};
|
||||
|
||||
sync_addr.type = addr->type;
|
||||
memcpy(sync_addr.val, addr->a.val, sizeof(sync_addr.val));
|
||||
params.skip = PA_SYNC_SKIP;
|
||||
params.sync_timeout = PA_SYNC_TIMEOUT;
|
||||
|
||||
return ble_gap_periodic_adv_sync_create(&sync_addr, adv_sid, ¶ms,
|
||||
example_audio_gap_event_cb, NULL);
|
||||
}
|
||||
|
||||
static int pa_sync_terminate(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = ble_gap_periodic_adv_sync_terminate(sync_handle);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to terminate PA sync, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "PA sync terminated");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void recv_state_updated_cb(esp_ble_conn_t *conn,
|
||||
const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state)
|
||||
{
|
||||
@@ -199,13 +135,27 @@ static int pa_sync_term_req_cb(esp_ble_conn_t *conn,
|
||||
|
||||
req_recv_state = recv_state;
|
||||
|
||||
err = pa_sync_terminate();
|
||||
/* Nothing to terminate if PAST/PA-sync never landed (e.g., PAST setup failed
|
||||
* earlier). Issuing the HCI terminate with the sentinel handle gets 0x12
|
||||
* (Invalid HCI Command Parameters) back from the controller. Mirrors
|
||||
* cap_acceptor_broadcast.c. */
|
||||
if (sync_handle == PA_SYNC_HANDLE_INIT) {
|
||||
ESP_LOGI(TAG, "PA sync never established, skip terminate");
|
||||
return 0;
|
||||
}
|
||||
|
||||
err = pa_sync_terminate(sync_handle);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to terminate PA sync, err %d", err);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
sync_handle = PA_SYNC_HANDLE_INIT;
|
||||
ESP_LOGI(TAG, "PA sync terminated");
|
||||
|
||||
/* Let the synthesized PA_SYNC_LOST event drive cleanup via pa_sync_lost
|
||||
* (it gates on the original sync_handle). Resetting sync_handle here would
|
||||
* make that gate miss and leak broadcast_sink. Mirrors
|
||||
* cap_acceptor_broadcast.c. */
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -481,7 +431,6 @@ static bool data_cb(uint8_t type, const uint8_t *data,
|
||||
static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
struct scan_recv_data sr = {0};
|
||||
bt_addr_le_t addr;
|
||||
int err;
|
||||
|
||||
/* Periodic advertising interval. 0 if no periodic advertising. */
|
||||
@@ -500,10 +449,9 @@ static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event)
|
||||
if (pa_syncing == false && req_recv_state == NULL) {
|
||||
broadcaster_broadcast_id = sr.broadcast_id;
|
||||
|
||||
addr.type = event->ext_scan_recv.addr.type;
|
||||
memcpy(addr.a.val, event->ext_scan_recv.addr.val, sizeof(addr.a.val));
|
||||
|
||||
err = pa_sync_create(&addr, event->ext_scan_recv.sid);
|
||||
err = pa_sync_create(event->ext_scan_recv.addr.type,
|
||||
event->ext_scan_recv.addr.val,
|
||||
event->ext_scan_recv.sid);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to create PA sync, err %d", err);
|
||||
return;
|
||||
@@ -533,7 +481,7 @@ static void pa_sync(esp_ble_audio_gap_app_event_t *event)
|
||||
* 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();
|
||||
rc = ext_scan_stop();
|
||||
if (rc) {
|
||||
ESP_LOGW(TAG, "Failed to stop scanning, err %d", rc);
|
||||
}
|
||||
@@ -618,6 +566,12 @@ void app_main(void)
|
||||
return;
|
||||
}
|
||||
|
||||
err = app_host_init();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to init host, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_common_init(&info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize audio, err %d", err);
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "host/ble_gap.h"
|
||||
#include "host/ble_hs.h"
|
||||
|
||||
#include "esp_ble_audio_common_api.h"
|
||||
|
||||
#include "scan.h"
|
||||
|
||||
/* Forward only the GAP events the application consumes. */
|
||||
static int gap_event_cb(struct ble_gap_event *event, void *arg)
|
||||
{
|
||||
switch (event->type) {
|
||||
case BLE_GAP_EVENT_EXT_DISC:
|
||||
case BLE_GAP_EVENT_PERIODIC_SYNC:
|
||||
case BLE_GAP_EVENT_PERIODIC_REPORT:
|
||||
case BLE_GAP_EVENT_PERIODIC_SYNC_LOST:
|
||||
esp_ble_audio_gap_app_post_event(event->type, event);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int app_host_init(void)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ext_scan_start(void)
|
||||
{
|
||||
struct ble_gap_disc_params params = {0};
|
||||
uint8_t own_addr_type;
|
||||
int err;
|
||||
|
||||
err = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
params.passive = 1;
|
||||
params.itvl = SCAN_INTERVAL;
|
||||
params.window = SCAN_WINDOW;
|
||||
|
||||
err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms,
|
||||
gap_event_cb, NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start scanning, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Scanning for broadcast source...");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ext_scan_stop(void)
|
||||
{
|
||||
return ble_gap_disc_cancel();
|
||||
}
|
||||
|
||||
int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid)
|
||||
{
|
||||
struct ble_gap_periodic_sync_params params = {0};
|
||||
ble_addr_t sync_addr = {0};
|
||||
|
||||
sync_addr.type = addr_type;
|
||||
memcpy(sync_addr.val, addr, sizeof(sync_addr.val));
|
||||
params.skip = PA_SYNC_SKIP;
|
||||
params.sync_timeout = PA_SYNC_TIMEOUT;
|
||||
|
||||
return ble_gap_periodic_adv_sync_create(&sync_addr, sid, ¶ms,
|
||||
gap_event_cb, NULL);
|
||||
}
|
||||
|
||||
int pa_sync_terminate(uint16_t sync_handle)
|
||||
{
|
||||
return ble_gap_periodic_adv_sync_terminate(sync_handle);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "ble_audio_example_utils.h"
|
||||
|
||||
#define TAG "BAP_BSNK"
|
||||
|
||||
#define SCAN_INTERVAL 160 /* 100ms */
|
||||
#define SCAN_WINDOW 160 /* 100ms */
|
||||
|
||||
#define PA_SYNC_SKIP 0
|
||||
#define PA_SYNC_TIMEOUT 1000 /* 1000 * 10ms = 10s */
|
||||
|
||||
int app_host_init(void);
|
||||
|
||||
int ext_scan_start(void);
|
||||
int ext_scan_stop(void);
|
||||
|
||||
int pa_sync_create(uint8_t addr_type, const uint8_t addr[6], uint8_t sid);
|
||||
int pa_sync_terminate(uint16_t sync_handle);
|
||||
@@ -3,14 +3,14 @@
|
||||
#
|
||||
|
||||
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
|
||||
CONFIG_BT_NIMBLE_ENABLED=n
|
||||
CONFIG_BT_BLUEDROID_ENABLED=y
|
||||
CONFIG_BT_CLASSIC_ENABLED=n
|
||||
CONFIG_BT_CONTROLLER_ENABLED=y
|
||||
CONFIG_BT_BLE_ENABLED=y
|
||||
CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y
|
||||
CONFIG_BT_ACL_CONNECTIONS=1
|
||||
CONFIG_BT_BLE_FEAT_ISO_EN=y
|
||||
|
||||
CONFIG_BT_ISO_MAX_CHAN=2
|
||||
|
||||
@@ -24,6 +24,8 @@ CONFIG_BT_PAC_SRC_NOTIFIABLE=y
|
||||
CONFIG_BT_PAC_SRC_LOC_WRITEABLE=y
|
||||
CONFIG_BT_PAC_SRC_LOC_NOTIFIABLE=y
|
||||
CONFIG_BT_PACS_SUPPORTED_CONTEXT_NOTIFIABLE=y
|
||||
CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=30
|
||||
CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=60
|
||||
|
||||
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
|
||||
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# NimBLE host overlay for this example.
|
||||
# Use with:
|
||||
# idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.$IDF_TARGET;sdkconfig.defaults.nimble" build
|
||||
|
||||
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
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
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 build runs on top of the selected BLE host stack (Bluedroid by default; NimBLE via the `sdkconfig.defaults.nimble` overlay) 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.
|
||||
|
||||
@@ -34,11 +34,24 @@ The example inherits a Just-Works pairing model (LE Secure Connections, no MITM,
|
||||
|
||||
## Build & Flash
|
||||
|
||||
The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`.
|
||||
|
||||
### Bluedroid host (default)
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32h4 # or esp32s31
|
||||
idf.py set-target esp32h4
|
||||
idf.py -p PORT flash monitor
|
||||
```
|
||||
|
||||
### NimBLE host
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32h4
|
||||
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor
|
||||
```
|
||||
|
||||
For `esp32s31`, replace the chip overlay accordingly.
|
||||
|
||||
(Exit serial monitor with `Ctrl-]`.)
|
||||
|
||||
## Example Flow
|
||||
@@ -46,7 +59,7 @@ idf.py -p PORT flash monitor
|
||||
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.
|
||||
4. `adv_init()` performs host-specific GAP setup (Bluedroid: registers the GAP callback for `*_COMPLETE_EVT` semaphore signalling; NimBLE: no-op — adv-instance callback is passed at configure time). `ext_adv_start()` builds the extended (Broadcast Audio Service Data + Broadcast ID + complete name) and periodic (BASE from `esp_ble_audio_bap_broadcast_source_get_base()`) payloads in host-agnostic bytes, then calls `adv_start(ext_data, ext_len, per_data, per_len)`. The host-specific implementation in `main/bluedroid/adv.c` or `main/nimble/adv.c` configures non-connectable extended adv (1M primary / 2M secondary, 200 ms interval), configures periodic adv (100 ms), writes both payloads, and 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.
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
set(srcs "main.c")
|
||||
|
||||
idf_component_register(SRCS "${srcs}"
|
||||
if(CONFIG_BT_BLUEDROID_ENABLED)
|
||||
list(APPEND srcs "bluedroid/adv.c")
|
||||
else()
|
||||
list(APPEND srcs "nimble/adv.c")
|
||||
endif()
|
||||
|
||||
idf_component_register(SRCS ${srcs}
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES bt nvs_flash)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "ble_audio_example_utils.h"
|
||||
|
||||
#define TAG "BAP_BSRC"
|
||||
|
||||
#define ADV_HANDLE 0
|
||||
#define ADV_SID 0
|
||||
#define ADV_TX_POWER 127
|
||||
#define ADV_INTERVAL_MS 200
|
||||
#define PER_ADV_INTERVAL_MS 100
|
||||
|
||||
int app_host_init(void);
|
||||
|
||||
int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len,
|
||||
const uint8_t *per_data, uint8_t per_len);
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#include "esp_bt_defs.h"
|
||||
#include "esp_gap_ble_api.h"
|
||||
|
||||
#include "adv.h"
|
||||
|
||||
static SemaphoreHandle_t adv_sem;
|
||||
|
||||
/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */
|
||||
static esp_bt_status_t adv_op_status;
|
||||
|
||||
#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, adv_sem, portMAX_DELAY, adv_op_status)
|
||||
|
||||
static esp_ble_gap_ext_adv_params_t ext_adv_params = {
|
||||
.type = ESP_BLE_GAP_SET_EXT_ADV_PROP_NONCONN_NONSCANNABLE_UNDIRECTED,
|
||||
.interval_min = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS),
|
||||
.interval_max = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS),
|
||||
.channel_map = ADV_CHNL_ALL,
|
||||
.filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
|
||||
.primary_phy = ESP_BLE_GAP_PHY_1M,
|
||||
.max_skip = 0,
|
||||
.secondary_phy = ESP_BLE_GAP_PHY_2M,
|
||||
.sid = ADV_SID,
|
||||
.scan_req_notif = false,
|
||||
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
|
||||
.tx_power = ADV_TX_POWER,
|
||||
};
|
||||
|
||||
static esp_ble_gap_periodic_adv_params_t periodic_adv_params = {
|
||||
.interval_min = ESP_BLE_GAP_PERIODIC_ADV_ITVL_MS(PER_ADV_INTERVAL_MS),
|
||||
.interval_max = ESP_BLE_GAP_PERIODIC_ADV_ITVL_MS(PER_ADV_INTERVAL_MS),
|
||||
.properties = 0,
|
||||
};
|
||||
|
||||
static esp_ble_gap_ext_adv_t ext_adv_inst[1] = {
|
||||
[0] = { ADV_HANDLE, 0, 0 },
|
||||
};
|
||||
|
||||
static void gap_event_handler(esp_gap_ble_cb_event_t event,
|
||||
esp_ble_gap_cb_param_t *param)
|
||||
{
|
||||
switch (event) {
|
||||
case ESP_GAP_BLE_EXT_ADV_SET_PARAMS_COMPLETE_EVT:
|
||||
adv_op_status = param->ext_adv_set_params.status;
|
||||
xSemaphoreGive(adv_sem);
|
||||
break;
|
||||
case ESP_GAP_BLE_EXT_ADV_DATA_SET_COMPLETE_EVT:
|
||||
adv_op_status = param->ext_adv_data_set.status;
|
||||
xSemaphoreGive(adv_sem);
|
||||
break;
|
||||
case ESP_GAP_BLE_EXT_ADV_START_COMPLETE_EVT:
|
||||
adv_op_status = param->ext_adv_start.status;
|
||||
xSemaphoreGive(adv_sem);
|
||||
break;
|
||||
case ESP_GAP_BLE_PERIODIC_ADV_SET_PARAMS_COMPLETE_EVT:
|
||||
adv_op_status = param->peroid_adv_set_params.status;
|
||||
xSemaphoreGive(adv_sem);
|
||||
break;
|
||||
case ESP_GAP_BLE_PERIODIC_ADV_DATA_SET_COMPLETE_EVT:
|
||||
adv_op_status = param->period_adv_data_set.status;
|
||||
xSemaphoreGive(adv_sem);
|
||||
break;
|
||||
case ESP_GAP_BLE_PERIODIC_ADV_START_COMPLETE_EVT:
|
||||
adv_op_status = param->period_adv_start.status;
|
||||
xSemaphoreGive(adv_sem);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int app_host_init(void)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
adv_sem = xSemaphoreCreateBinary();
|
||||
if (adv_sem == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to create adv semaphore");
|
||||
return -1;
|
||||
}
|
||||
|
||||
err = esp_ble_gap_register_callback(gap_event_handler);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err);
|
||||
vSemaphoreDelete(adv_sem);
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len,
|
||||
const uint8_t *per_data, uint8_t per_len)
|
||||
{
|
||||
WAIT_API(esp_ble_gap_ext_adv_set_params(ADV_HANDLE, &ext_adv_params));
|
||||
WAIT_API(esp_ble_gap_config_ext_adv_data_raw(ADV_HANDLE, ext_len, ext_data));
|
||||
WAIT_API(esp_ble_gap_periodic_adv_set_params(ADV_HANDLE, &periodic_adv_params));
|
||||
#if CONFIG_BT_BLE_FEAT_PERIODIC_ADV_ENH
|
||||
WAIT_API(esp_ble_gap_config_periodic_adv_data_raw(ADV_HANDLE, per_len, per_data, false));
|
||||
WAIT_API(esp_ble_gap_periodic_adv_start(ADV_HANDLE, true));
|
||||
#else
|
||||
WAIT_API(esp_ble_gap_config_periodic_adv_data_raw(ADV_HANDLE, per_len, per_data));
|
||||
WAIT_API(esp_ble_gap_periodic_adv_start(ADV_HANDLE));
|
||||
#endif
|
||||
WAIT_API(esp_ble_gap_ext_adv_start(1, ext_adv_inst));
|
||||
|
||||
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
|
||||
return 0;
|
||||
}
|
||||
@@ -9,17 +9,10 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_random.h"
|
||||
#include "esp_timer.h"
|
||||
|
||||
#include "host/ble_hs.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
|
||||
#include "esp_ble_audio_lc3_defs.h"
|
||||
#include "esp_ble_audio_bap_api.h"
|
||||
@@ -29,7 +22,7 @@
|
||||
#include "ble_audio_example_init.h"
|
||||
#include "ble_audio_example_utils.h"
|
||||
|
||||
#define TAG "BAP_BSRC"
|
||||
#include "adv.h"
|
||||
|
||||
ESP_BLE_AUDIO_BAP_LC3_BROADCAST_PRESET_16_2_1_DEFINE(preset_active,
|
||||
ESP_BLE_AUDIO_LOCATION_FRONT_LEFT |
|
||||
@@ -42,16 +35,6 @@ ESP_BLE_AUDIO_BAP_LC3_BROADCAST_PRESET_16_2_1_DEFINE(preset_active,
|
||||
#define LOCAL_BROADCAST_CODE "1234" /* Maximum length is 16 */
|
||||
#define LOCAL_BROADCAST_ID 0x123456
|
||||
|
||||
#define ADV_HANDLE 0x00
|
||||
#define ADV_SID 0
|
||||
#define ADV_TX_POWER 127
|
||||
#define ADV_ADDRESS BLE_OWN_ADDR_PUBLIC
|
||||
#define ADV_PRIMARY_PHY BLE_HCI_LE_PHY_1M
|
||||
#define ADV_SECONDARY_PHY BLE_HCI_LE_PHY_2M
|
||||
#define ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(200)
|
||||
|
||||
#define PER_ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(100)
|
||||
|
||||
#define STREAM_COUNT CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT
|
||||
#define SUBGROUP_COUNT CONFIG_BT_BAP_BROADCAST_SRC_SUBGROUP_COUNT
|
||||
|
||||
@@ -369,138 +352,55 @@ static uint8_t *per_adv_data_get(uint8_t *data_len)
|
||||
return data;
|
||||
}
|
||||
|
||||
static int ext_adv_start(void)
|
||||
static int broadcast_start(void)
|
||||
{
|
||||
struct ble_gap_periodic_adv_params per_params = {0};
|
||||
struct ble_gap_ext_adv_params ext_params = {0};
|
||||
struct os_mbuf *data = NULL;
|
||||
esp_ble_audio_bap_broadcast_adv_info_t info = {
|
||||
.adv_handle = ADV_HANDLE,
|
||||
};
|
||||
/* BASE (per_adv_data_get) needs broadcast_source to exist — built earlier
|
||||
* by broadcast_source_setup(), so safe to call here. */
|
||||
uint8_t *ext_data = NULL;
|
||||
uint8_t *per_data = NULL;
|
||||
uint8_t data_len = 0;
|
||||
int err;
|
||||
uint8_t ext_len = 0;
|
||||
uint8_t per_len = 0;
|
||||
int err = 0;
|
||||
|
||||
ext_params.connectable = 0;
|
||||
ext_params.scannable = 0;
|
||||
ext_params.legacy_pdu = 0;
|
||||
ext_params.own_addr_type = ADV_ADDRESS;
|
||||
ext_params.primary_phy = ADV_PRIMARY_PHY;
|
||||
ext_params.secondary_phy = ADV_SECONDARY_PHY;
|
||||
ext_params.tx_power = ADV_TX_POWER;
|
||||
ext_params.sid = ADV_SID;
|
||||
ext_params.itvl_min = ADV_INTERVAL;
|
||||
ext_params.itvl_max = ADV_INTERVAL;
|
||||
|
||||
err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL,
|
||||
example_audio_gap_event_cb, NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
ext_data = ext_adv_data_get(&data_len);
|
||||
ext_data = ext_adv_data_get(&ext_len);
|
||||
if (ext_data == NULL) {
|
||||
err = -ENOMEM;
|
||||
goto end;
|
||||
}
|
||||
|
||||
data = os_msys_get_pkthdr(data_len, 0);
|
||||
if (data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to get ext adv mbuf");
|
||||
err = -ENOMEM;
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = os_mbuf_append(data, ext_data, data_len);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err);
|
||||
os_mbuf_free_chain(data);
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = ble_gap_ext_adv_set_data(ADV_HANDLE, data);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
per_params.include_tx_power = 0;
|
||||
per_params.itvl_min = PER_ADV_INTERVAL;
|
||||
per_params.itvl_max = PER_ADV_INTERVAL;
|
||||
|
||||
err = ble_gap_periodic_adv_configure(ADV_HANDLE, &per_params);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to configure per adv params, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
per_data = per_adv_data_get(&data_len);
|
||||
per_data = per_adv_data_get(&per_len);
|
||||
if (per_data == NULL) {
|
||||
err = -ENOMEM;
|
||||
goto end;
|
||||
}
|
||||
|
||||
data = os_msys_get_pkthdr(data_len, 0);
|
||||
if (data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to get per adv mbuf");
|
||||
err = -ENOMEM;
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = os_mbuf_append(data, per_data, data_len);
|
||||
err = ext_adv_start(ext_data, ext_len, per_data, per_len);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to append per adv data, err %d", err);
|
||||
os_mbuf_free_chain(data);
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = ble_gap_periodic_adv_set_data(ADV_HANDLE, data);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set per adv data, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = ble_gap_periodic_adv_start(ADV_HANDLE);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start per advertising, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err);
|
||||
goto end;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
|
||||
|
||||
end:
|
||||
if (ext_data) {
|
||||
free(ext_data);
|
||||
}
|
||||
if (per_data) {
|
||||
free(per_data);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
static void broadcast_start(void)
|
||||
{
|
||||
esp_ble_audio_bap_broadcast_adv_info_t info = {
|
||||
.adv_handle = ADV_HANDLE,
|
||||
};
|
||||
int err;
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_adv_add(&info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to add adv for broadcast source, err %d", err);
|
||||
return;
|
||||
goto end;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_source_start(broadcast_source, ADV_HANDLE);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start broadcast source, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
end:
|
||||
if (ext_data != NULL) {
|
||||
free(ext_data);
|
||||
}
|
||||
if (per_data != NULL) {
|
||||
free(per_data);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
@@ -521,6 +421,12 @@ void app_main(void)
|
||||
return;
|
||||
}
|
||||
|
||||
err = app_host_init();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to init host, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_common_init(NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize audio, err %d", err);
|
||||
@@ -548,10 +454,9 @@ void app_main(void)
|
||||
}
|
||||
}
|
||||
|
||||
err = ext_adv_start();
|
||||
err = broadcast_start();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start broadcast, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
broadcast_start();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "host/ble_gap.h"
|
||||
|
||||
#include "adv.h"
|
||||
|
||||
/* Non-connectable broadcaster: no GAP events for the application. */
|
||||
static int gap_event_cb(struct ble_gap_event *event, void *arg)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int app_host_init(void)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ext_adv_start(const uint8_t *ext_data, uint8_t ext_len,
|
||||
const uint8_t *per_data, uint8_t per_len)
|
||||
{
|
||||
struct ble_gap_periodic_adv_params per_params = {0};
|
||||
struct ble_gap_ext_adv_params ext_params = {0};
|
||||
struct os_mbuf *data;
|
||||
int err;
|
||||
|
||||
ext_params.connectable = 0;
|
||||
ext_params.scannable = 0;
|
||||
ext_params.legacy_pdu = 0;
|
||||
ext_params.own_addr_type = BLE_OWN_ADDR_PUBLIC;
|
||||
ext_params.primary_phy = BLE_HCI_LE_PHY_1M;
|
||||
ext_params.secondary_phy = BLE_HCI_LE_PHY_2M;
|
||||
ext_params.tx_power = ADV_TX_POWER;
|
||||
ext_params.sid = ADV_SID;
|
||||
ext_params.itvl_min = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS);
|
||||
ext_params.itvl_max = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS);
|
||||
|
||||
err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL,
|
||||
gap_event_cb, NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
data = os_msys_get_pkthdr(ext_len, 0);
|
||||
if (data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to get ext adv mbuf");
|
||||
return -1;
|
||||
}
|
||||
|
||||
err = os_mbuf_append(data, ext_data, ext_len);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err);
|
||||
os_mbuf_free_chain(data);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = ble_gap_ext_adv_set_data(ADV_HANDLE, data);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
per_params.include_tx_power = 0;
|
||||
per_params.itvl_min = BLE_GAP_PERIODIC_ITVL_MS(PER_ADV_INTERVAL_MS);
|
||||
per_params.itvl_max = BLE_GAP_PERIODIC_ITVL_MS(PER_ADV_INTERVAL_MS);
|
||||
|
||||
err = ble_gap_periodic_adv_configure(ADV_HANDLE, &per_params);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to configure per adv params, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
data = os_msys_get_pkthdr(per_len, 0);
|
||||
if (data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to get per adv mbuf");
|
||||
return -1;
|
||||
}
|
||||
|
||||
err = os_mbuf_append(data, per_data, per_len);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to append per adv data, err %d", err);
|
||||
os_mbuf_free_chain(data);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = ble_gap_periodic_adv_set_data(ADV_HANDLE, data);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set per adv data, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = ble_gap_periodic_adv_start(ADV_HANDLE);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start per advertising, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -3,14 +3,19 @@
|
||||
#
|
||||
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_BLUEDROID_ENABLED=n
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_EXT_ADV=y
|
||||
CONFIG_BT_NIMBLE_ISO=y
|
||||
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
|
||||
CONFIG_BT_NIMBLE_ENABLED=n
|
||||
CONFIG_BT_BLUEDROID_ENABLED=y
|
||||
CONFIG_BT_CLASSIC_ENABLED=n
|
||||
CONFIG_BT_CONTROLLER_ENABLED=y
|
||||
CONFIG_BT_BLE_ENABLED=y
|
||||
CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y
|
||||
CONFIG_BT_ACL_CONNECTIONS=1
|
||||
CONFIG_BT_BLE_FEAT_ISO_EN=y
|
||||
|
||||
CONFIG_BT_ISO_MAX_CHAN=2
|
||||
|
||||
CONFIG_BT_BAP_BROADCAST_SOURCE=y
|
||||
|
||||
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
|
||||
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# NimBLE host overlay for this example.
|
||||
# Use with:
|
||||
# idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.$IDF_TARGET;sdkconfig.defaults.nimble" build
|
||||
|
||||
CONFIG_BT_BLUEDROID_ENABLED=n
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_EXT_ADV=y
|
||||
CONFIG_BT_NIMBLE_ISO=y
|
||||
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
## Overview
|
||||
|
||||
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.
|
||||
This example implements a **BAP Unicast Client** on top of the selected BLE host stack (Bluedroid by default; NimBLE via the `sdkconfig.defaults.nimble` overlay). 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.
|
||||
|
||||
@@ -34,19 +34,32 @@ This example inherits a Just-Works pairing model (LE Secure Connections, no MITM
|
||||
|
||||
## Build & Flash
|
||||
|
||||
The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`.
|
||||
|
||||
### Bluedroid host (default)
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32h4
|
||||
idf.py -p PORT flash monitor
|
||||
```
|
||||
|
||||
### NimBLE host
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32h4
|
||||
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor
|
||||
```
|
||||
|
||||
For `esp32s31`, replace the chip overlay accordingly.
|
||||
|
||||
(Exit serial monitor with `Ctrl-]`.)
|
||||
|
||||
## Example Flow
|
||||
|
||||
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`.
|
||||
1. `app_main` initializes NVS, the BLE host, runs `app_host_init` for host-specific setup (GAP callback, plus a Bluedroid-only GATTC app registration so we have a `gattc_if` for outgoing aux-open), brings up the LE Audio common layer, registers stream ops on every sink/source slot, registers the BAP unicast client callbacks, calls `set_device_name()` (uses `LOCAL_DEVICE_NAME` from `central.h`), then 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.
|
||||
3. On a connectable match `ext_scan_recv` calls `ext_scan_stop` and then `conn_create` (NimBLE: `ble_gap_connect`; Bluedroid: `esp_ble_gattc_aux_open`); the resulting `ESP_BLE_AUDIO_GAP_EVENT_ACL_CONNECT` saves the handle and calls `pairing_start` (NimBLE: `ble_gap_security_initiate`; Bluedroid: `esp_ble_set_encryption`).
|
||||
4. After the security-change event the client triggers `exchange_mtu`. On NimBLE this issues `ble_gattc_exchange_mtu`; on Bluedroid the GATTC adapter auto-configures MTU inside `BTA_GATTC_Enh_Open`, so `exchange_mtu` drives `esp_ble_audio_gattc_disc_start` directly. `gatt_mtu_change` then calls `esp_ble_audio_gattc_disc_start` (the retry is idempotent — `-EALREADY` is treated as success), 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.
|
||||
@@ -116,8 +129,8 @@ I (xxx) BAP_UCL: Disconnected: handle ... reason 0x...
|
||||
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:`.
|
||||
2. Client boots and scans; on matching the name and ASCS service data it cancels scanning and issues `conn_create` (NimBLE: `ble_gap_connect`; Bluedroid: `esp_ble_gattc_aux_open`).
|
||||
3. After ACL is up the client initiates pairing/security; both sides log `Connected:` and `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.
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
set(srcs "stream_tx.c"
|
||||
"main.c")
|
||||
|
||||
idf_component_register(SRCS "${srcs}"
|
||||
if(CONFIG_BT_BLUEDROID_ENABLED)
|
||||
list(APPEND srcs "bluedroid/central.c")
|
||||
else()
|
||||
list(APPEND srcs "nimble/central.c")
|
||||
endif()
|
||||
|
||||
idf_component_register(SRCS ${srcs}
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES bt nvs_flash)
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#include "esp_bt_defs.h"
|
||||
#include "esp_gap_ble_api.h"
|
||||
#include "esp_gattc_api.h"
|
||||
|
||||
#include "esp_ble_audio_common_api.h"
|
||||
|
||||
#include "central.h"
|
||||
|
||||
static SemaphoreHandle_t scan_sem;
|
||||
|
||||
/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */
|
||||
static esp_bt_status_t scan_op_status;
|
||||
|
||||
#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, scan_sem, portMAX_DELAY, scan_op_status)
|
||||
|
||||
/* Cached peer address. Bluedroid's pairing and disconnect APIs key off
|
||||
* bd_addr rather than conn_handle, so we stash the addr at conn_create
|
||||
* time and reuse it in pairing_start / security_failed_recover. */
|
||||
static esp_bd_addr_t peer_bda;
|
||||
|
||||
static esp_ble_ext_scan_params_t ext_scan_params = {
|
||||
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
|
||||
.filter_policy = BLE_SCAN_FILTER_ALLOW_ALL,
|
||||
.scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE,
|
||||
.cfg_mask = ESP_BLE_GAP_EXT_SCAN_CFG_UNCODE_MASK,
|
||||
.uncoded_cfg = {
|
||||
.scan_type = BLE_SCAN_TYPE_PASSIVE,
|
||||
.scan_interval = SCAN_INTERVAL,
|
||||
.scan_window = SCAN_WINDOW,
|
||||
},
|
||||
};
|
||||
|
||||
static void gap_event_handler(esp_gap_ble_cb_event_t event,
|
||||
esp_ble_gap_cb_param_t *param)
|
||||
{
|
||||
switch (event) {
|
||||
case ESP_GAP_BLE_SET_EXT_SCAN_PARAMS_COMPLETE_EVT:
|
||||
scan_op_status = param->set_ext_scan_params.status;
|
||||
xSemaphoreGive(scan_sem);
|
||||
break;
|
||||
case ESP_GAP_BLE_EXT_SCAN_START_COMPLETE_EVT:
|
||||
scan_op_status = param->ext_scan_start.status;
|
||||
xSemaphoreGive(scan_sem);
|
||||
break;
|
||||
case ESP_GAP_BLE_EXT_SCAN_STOP_COMPLETE_EVT:
|
||||
scan_op_status = param->ext_scan_stop.status;
|
||||
xSemaphoreGive(scan_sem);
|
||||
break;
|
||||
|
||||
/* SMP request handling for Just Works pairing (IO_CAP=NONE). NC_REQ
|
||||
* still fires under LE Secure Connections — auto-accept. We never get
|
||||
* SEC_REQ here because the central initiates via esp_ble_set_encryption. */
|
||||
case ESP_GAP_BLE_NC_REQ_EVT:
|
||||
esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, true);
|
||||
break;
|
||||
|
||||
/* AUTH_CMPL has no BTA channel — app must forward it. EXT_ADV_REPORT
|
||||
* is forwarded by adapter's BTA path, don't re-post here. */
|
||||
case ESP_GAP_BLE_AUTH_CMPL_EVT:
|
||||
esp_ble_audio_gap_app_post_event(event, param);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int app_host_init(void)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
scan_sem = xSemaphoreCreateBinary();
|
||||
if (scan_sem == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to create scan semaphore");
|
||||
return -1;
|
||||
}
|
||||
|
||||
err = esp_ble_gap_register_callback(gap_event_handler);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err);
|
||||
vSemaphoreDelete(scan_sem);
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int set_device_name(void)
|
||||
{
|
||||
return esp_ble_gap_set_device_name(LOCAL_DEVICE_NAME);
|
||||
}
|
||||
|
||||
int ext_scan_start(void)
|
||||
{
|
||||
WAIT_API(esp_ble_gap_set_ext_scan_params(&ext_scan_params));
|
||||
WAIT_API(esp_ble_gap_start_ext_scan(0, 0));
|
||||
|
||||
ESP_LOGI(TAG, "Scanning for unicast server...");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
int ext_scan_stop(void)
|
||||
{
|
||||
WAIT_API(esp_ble_gap_stop_ext_scan());
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
int conn_create(uint8_t addr_type, const uint8_t addr[6])
|
||||
{
|
||||
/* Values shared with NimBLE side via central.h for behavioral parity.
|
||||
* Without prefer_ext_connect_params_set, L2CAP falls back to defaults
|
||||
* and logs "No extend connection parameters set". */
|
||||
const esp_ble_gap_conn_params_t conn_params = {
|
||||
.scan_interval = INIT_SCAN_INTERVAL,
|
||||
.scan_window = INIT_SCAN_WINDOW,
|
||||
.interval_min = CONN_INTERVAL,
|
||||
.interval_max = CONN_INTERVAL,
|
||||
.latency = CONN_LATENCY,
|
||||
.supervision_timeout = CONN_TIMEOUT,
|
||||
.min_ce_len = CONN_MIN_CE_LEN,
|
||||
.max_ce_len = CONN_MAX_CE_LEN,
|
||||
};
|
||||
esp_gatt_if_t gattc_if;
|
||||
esp_err_t err;
|
||||
|
||||
memcpy(peer_bda, addr, sizeof(peer_bda));
|
||||
|
||||
err = esp_ble_gap_prefer_ext_connect_params_set(
|
||||
peer_bda, ESP_BLE_GAP_PHY_1M_PREF_MASK, &conn_params, NULL, NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set ext conn params, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* Use the audio engine's GATTC if so OPEN/CONNECT events route back to
|
||||
* the engine. aux_open initiates an ACL against an extended advertiser.
|
||||
*
|
||||
* engine returns ESP_GATT_IF_NONE (0xFF) when GATTC is not yet registered;
|
||||
* feeding that into aux_open silently no-ops in BTC and no acl_connect
|
||||
* event ever fires, leaving the caller in a no-scan / no-conn dead state. */
|
||||
gattc_if = esp_ble_audio_bluedroid_get_gattc_if();
|
||||
if (gattc_if == ESP_GATT_IF_NONE) {
|
||||
ESP_LOGE(TAG, "GATTC not registered");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
return esp_ble_gattc_aux_open(gattc_if, peer_bda,
|
||||
(esp_ble_addr_type_t)addr_type, true);
|
||||
}
|
||||
|
||||
int pairing_start(uint16_t conn_handle)
|
||||
{
|
||||
(void)conn_handle;
|
||||
return esp_ble_set_encryption(peer_bda, ESP_BLE_SEC_ENCRYPT_NO_MITM);
|
||||
}
|
||||
|
||||
int exchange_mtu(uint16_t conn_handle)
|
||||
{
|
||||
(void)conn_handle;
|
||||
/* The Bluedroid GATTC adapter exchanges MTU automatically when aux_open
|
||||
* brings up the ACL — the MTU_UPDATED event arrives without an explicit
|
||||
* kick-off. NimBLE has no such auto-exchange, so this wrapper is a no-op
|
||||
* on bluedroid and a real ble_gattc_exchange_mtu on nimble. */
|
||||
return 0;
|
||||
}
|
||||
|
||||
void security_failed_recover(uint16_t conn_handle, uint8_t status)
|
||||
{
|
||||
(void)conn_handle;
|
||||
|
||||
/* Asymmetric bond state: we still hold an LTK for this peer but it
|
||||
* cleared its side, so encrypt-with-cached-key times out. Drop the bond
|
||||
* and tear down the link; the next reconnect runs fresh pairing. */
|
||||
ESP_LOGE(TAG, "Security change failed, status %u, clearing local bond and reconnecting", status);
|
||||
|
||||
esp_ble_remove_bond_device(peer_bda);
|
||||
esp_ble_gap_disconnect(peer_bda);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "ble_audio_example_utils.h"
|
||||
|
||||
#define TAG "BAP_UCL"
|
||||
|
||||
#define LOCAL_DEVICE_NAME "BAP Unicast Client"
|
||||
|
||||
#define SCAN_INTERVAL 160 /* 100ms */
|
||||
#define SCAN_WINDOW 160 /* 100ms */
|
||||
|
||||
/* ACL init parameters shared between bluedroid and nimble host wrappers.
|
||||
* Raw HCI units (scan: 0.625ms; conn interval: 1.25ms; timeout: 10ms). */
|
||||
#define INIT_SCAN_INTERVAL 16 /* 10ms */
|
||||
#define INIT_SCAN_WINDOW 16 /* 10ms */
|
||||
#define CONN_INTERVAL 24 /* 30ms */
|
||||
#define CONN_LATENCY 0
|
||||
#define CONN_TIMEOUT 500 /* 5s */
|
||||
#define CONN_MIN_CE_LEN 0xFFFF
|
||||
#define CONN_MAX_CE_LEN 0xFFFF
|
||||
|
||||
int app_host_init(void);
|
||||
|
||||
int set_device_name(void);
|
||||
|
||||
int ext_scan_start(void);
|
||||
int ext_scan_stop(void);
|
||||
|
||||
int conn_create(uint8_t addr_type, const uint8_t addr[6]);
|
||||
|
||||
int pairing_start(uint16_t conn_handle);
|
||||
|
||||
int exchange_mtu(uint16_t conn_handle);
|
||||
|
||||
/* Recover from an SMP failure on the given connection — typically deletes the
|
||||
* stale bond and terminates the link so the next reconnect runs fresh
|
||||
* pairing. Host-specific implementation. */
|
||||
void security_failed_recover(uint16_t conn_handle, uint8_t status);
|
||||
@@ -7,31 +7,16 @@
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_system.h"
|
||||
|
||||
#include "host/ble_gap.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
|
||||
#include "central.h"
|
||||
#include "stream_tx.h"
|
||||
|
||||
#define TARGET_DEVICE_NAME "BAP Unicast Server"
|
||||
#define TARGET_DEVICE_NAME_LEN (sizeof(TARGET_DEVICE_NAME) - 1)
|
||||
|
||||
#define SCAN_INTERVAL 160 /* 100ms */
|
||||
#define SCAN_WINDOW 160 /* 100ms */
|
||||
|
||||
#define INIT_SCAN_INTERVAL 16 /* 10ms */
|
||||
#define INIT_SCAN_WINDOW 16 /* 10ms */
|
||||
#define CONN_INTERVAL 64 /* 64 * 1.25 = 80ms */
|
||||
#define CONN_LATENCY 0
|
||||
#define CONN_TIMEOUT 500 /* 500 * 10ms = 5s */
|
||||
#define CONN_MAX_CE_LEN 0xFFFF
|
||||
#define CONN_MIN_CE_LEN 0xFFFF
|
||||
#define CONN_DURATION 10000 /* 10s */
|
||||
#define CONN_HANDLE_INIT 0xFFFF
|
||||
|
||||
#define ASCS_RSP_NONE (0b00)
|
||||
@@ -41,7 +26,6 @@
|
||||
|
||||
static uint16_t default_conn_handle = CONN_HANDLE_INIT;
|
||||
static bool disc_completed;
|
||||
static bool disc_cancelled;
|
||||
static bool mtu_exchanged;
|
||||
|
||||
ESP_BLE_AUDIO_BAP_LC3_UNICAST_PRESET_16_2_1_DEFINE(unicast_preset,
|
||||
@@ -81,8 +65,6 @@ static struct stream_pair_state {
|
||||
} stream_pairs[MAX(ARRAY_SIZE(sinks), ARRAY_SIZE(sources))];
|
||||
static size_t stream_pair_count;
|
||||
|
||||
static int ext_scan_start(void);
|
||||
|
||||
static void reset_stream_pair_state(void)
|
||||
{
|
||||
stream_pair_count = 0;
|
||||
@@ -1049,49 +1031,6 @@ static esp_ble_audio_bap_unicast_client_cb_t unicast_client_cbs = {
|
||||
.discover = discover_cb,
|
||||
};
|
||||
|
||||
static int conn_create(ble_addr_t *dst)
|
||||
{
|
||||
struct ble_gap_conn_params params = {0};
|
||||
uint8_t own_addr_type = 0;
|
||||
int err;
|
||||
|
||||
err = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to determine address type, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = ble_gap_disc_cancel();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop scanning, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
disc_cancelled = true;
|
||||
|
||||
params.scan_itvl = INIT_SCAN_INTERVAL;
|
||||
params.scan_window = INIT_SCAN_WINDOW;
|
||||
params.itvl_min = CONN_INTERVAL;
|
||||
params.itvl_max = CONN_INTERVAL;
|
||||
params.latency = CONN_LATENCY;
|
||||
params.supervision_timeout = CONN_TIMEOUT;
|
||||
params.max_ce_len = CONN_MAX_CE_LEN;
|
||||
params.min_ce_len = CONN_MIN_CE_LEN;
|
||||
|
||||
return ble_gap_connect(own_addr_type, dst, CONN_DURATION, ¶ms,
|
||||
example_audio_gap_event_cb, NULL);
|
||||
}
|
||||
|
||||
static int pairing_start(uint16_t conn_handle)
|
||||
{
|
||||
return ble_gap_security_initiate(conn_handle);
|
||||
}
|
||||
|
||||
static int exchange_mtu(uint16_t conn_handle)
|
||||
{
|
||||
return ble_gattc_exchange_mtu(conn_handle, NULL, NULL);
|
||||
}
|
||||
|
||||
struct unicast_server_adv_data {
|
||||
bool target_matched;
|
||||
bool ascs_found;
|
||||
@@ -1157,7 +1096,6 @@ static bool scan_data_cb(uint8_t type, const uint8_t *data,
|
||||
static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
struct unicast_server_adv_data adv = {0};
|
||||
ble_addr_t dst = {0};
|
||||
int err;
|
||||
|
||||
if (default_conn_handle != CONN_HANDLE_INIT) {
|
||||
@@ -1178,17 +1116,21 @@ static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event)
|
||||
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));
|
||||
/* Stop scanning before connect — NimBLE rejects ble_gap_connect while
|
||||
* a discovery procedure is running; Bluedroid prefers the same ordering
|
||||
* even though it accepts both. On failure, restart scanning so we don't
|
||||
* stall in a no-scan no-conn state. */
|
||||
err = ext_scan_stop();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop scanning, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = conn_create(&dst);
|
||||
err = conn_create(event->ext_scan_recv.addr.type,
|
||||
event->ext_scan_recv.addr.val);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to create conn, err %d", err);
|
||||
|
||||
if (disc_cancelled) {
|
||||
disc_cancelled = false;
|
||||
ext_scan_start();
|
||||
}
|
||||
ext_scan_start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1198,14 +1140,16 @@ 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);
|
||||
/* ext_scan_recv stopped scanning before conn_create. acl_disconnect
|
||||
* (which restarts scan) only fires on an established connection, so
|
||||
* we must resume here to avoid a no-scan / no-conn dead state. */
|
||||
ext_scan_start();
|
||||
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]);
|
||||
EXAMPLE_BT_ADDR_PRINT_ARGS(event->acl_connect.dst.val));
|
||||
|
||||
default_conn_handle = event->acl_connect.conn_handle;
|
||||
|
||||
@@ -1223,7 +1167,6 @@ static void acl_disconnect(esp_ble_audio_gap_app_event_t *event)
|
||||
|
||||
default_conn_handle = CONN_HANDLE_INIT;
|
||||
disc_completed = false;
|
||||
disc_cancelled = false;
|
||||
mtu_exchanged = false;
|
||||
|
||||
/* Delete the group before reset_stream_state() memsets the streams:
|
||||
@@ -1240,13 +1183,13 @@ static void acl_disconnect(esp_ble_audio_gap_app_event_t *event)
|
||||
ext_scan_start();
|
||||
}
|
||||
|
||||
static void security_change(esp_ble_iso_gap_app_event_t *event)
|
||||
static void security_change(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
int err;
|
||||
|
||||
if (event->security_change.status) {
|
||||
example_audio_security_failed_recover(TAG, event->security_change.conn_handle,
|
||||
event->security_change.status);
|
||||
security_failed_recover(event->security_change.conn_handle,
|
||||
event->security_change.status);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1349,33 +1292,6 @@ static void iso_gatt_app_cb(esp_ble_audio_gatt_app_event_t *event)
|
||||
}
|
||||
}
|
||||
|
||||
static int ext_scan_start(void)
|
||||
{
|
||||
struct ble_gap_disc_params params = {0};
|
||||
uint8_t own_addr_type;
|
||||
int err;
|
||||
|
||||
err = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to determine own addr type, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
params.passive = 1;
|
||||
params.itvl = SCAN_INTERVAL;
|
||||
params.window = SCAN_WINDOW;
|
||||
|
||||
err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms,
|
||||
example_audio_gap_event_cb, NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start scanning, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Scanning for unicast server...");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
esp_ble_audio_init_info_t info = {
|
||||
@@ -1398,6 +1314,12 @@ void app_main(void)
|
||||
return;
|
||||
}
|
||||
|
||||
err = app_host_init();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to init host, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_common_init(&info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize audio, err %d", err);
|
||||
@@ -1428,7 +1350,7 @@ void app_main(void)
|
||||
return;
|
||||
}
|
||||
|
||||
err = ble_svc_gap_device_name_set("BAP Unicast Client");
|
||||
err = set_device_name();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set device name, err %d", err);
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "host/ble_gap.h"
|
||||
#include "host/ble_hs.h"
|
||||
#include "host/ble_store.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
|
||||
#include "esp_ble_audio_common_api.h"
|
||||
|
||||
#include "central.h"
|
||||
|
||||
/* Init/conn parameters are shared with the bluedroid wrapper via central.h.
|
||||
* CONN_DURATION is NimBLE-specific (ble_gap_connect's discovery timeout). */
|
||||
#define CONN_DURATION 10000 /* 10s */
|
||||
|
||||
static int gap_event_cb(struct ble_gap_event *event, void *arg)
|
||||
{
|
||||
switch (event->type) {
|
||||
case BLE_GAP_EVENT_EXT_DISC:
|
||||
case BLE_GAP_EVENT_CONNECT:
|
||||
case BLE_GAP_EVENT_DISCONNECT:
|
||||
case BLE_GAP_EVENT_ENC_CHANGE:
|
||||
esp_ble_audio_gap_app_post_event(event->type, event);
|
||||
break;
|
||||
case BLE_GAP_EVENT_MTU:
|
||||
case BLE_GAP_EVENT_NOTIFY_RX:
|
||||
case BLE_GAP_EVENT_NOTIFY_TX:
|
||||
case BLE_GAP_EVENT_SUBSCRIBE:
|
||||
esp_ble_audio_gatt_app_post_event(event->type, event);
|
||||
break;
|
||||
case BLE_GAP_EVENT_REPEAT_PAIRING: {
|
||||
struct ble_gap_conn_desc desc = {0};
|
||||
int rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc);
|
||||
if (rc == 0) {
|
||||
ble_store_util_delete_peer(&desc.peer_id_addr);
|
||||
}
|
||||
return BLE_GAP_REPEAT_PAIRING_RETRY;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int app_host_init(void)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int set_device_name(void)
|
||||
{
|
||||
return ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME);
|
||||
}
|
||||
|
||||
int ext_scan_start(void)
|
||||
{
|
||||
struct ble_gap_disc_params params = {0};
|
||||
uint8_t own_addr_type;
|
||||
int err;
|
||||
|
||||
err = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to determine address type, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
params.passive = 1;
|
||||
params.itvl = SCAN_INTERVAL;
|
||||
params.window = SCAN_WINDOW;
|
||||
|
||||
err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms,
|
||||
gap_event_cb, NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start scanning, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Scanning for unicast server...");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ext_scan_stop(void)
|
||||
{
|
||||
return ble_gap_disc_cancel();
|
||||
}
|
||||
|
||||
int conn_create(uint8_t addr_type, const uint8_t addr[6])
|
||||
{
|
||||
struct ble_gap_conn_params params = {0};
|
||||
uint8_t own_addr_type = 0;
|
||||
ble_addr_t dst = {0};
|
||||
int err;
|
||||
|
||||
err = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to determine address type, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
params.scan_itvl = INIT_SCAN_INTERVAL;
|
||||
params.scan_window = INIT_SCAN_WINDOW;
|
||||
params.itvl_min = CONN_INTERVAL;
|
||||
params.itvl_max = CONN_INTERVAL;
|
||||
params.latency = CONN_LATENCY;
|
||||
params.supervision_timeout = CONN_TIMEOUT;
|
||||
params.max_ce_len = CONN_MAX_CE_LEN;
|
||||
params.min_ce_len = CONN_MIN_CE_LEN;
|
||||
|
||||
dst.type = addr_type;
|
||||
memcpy(dst.val, addr, sizeof(dst.val));
|
||||
|
||||
return ble_gap_connect(own_addr_type, &dst, CONN_DURATION,
|
||||
¶ms, gap_event_cb, NULL);
|
||||
}
|
||||
|
||||
int pairing_start(uint16_t conn_handle)
|
||||
{
|
||||
return ble_gap_security_initiate(conn_handle);
|
||||
}
|
||||
|
||||
int exchange_mtu(uint16_t conn_handle)
|
||||
{
|
||||
return ble_gattc_exchange_mtu(conn_handle, NULL, NULL);
|
||||
}
|
||||
|
||||
void security_failed_recover(uint16_t conn_handle, uint8_t status)
|
||||
{
|
||||
struct ble_gap_conn_desc desc = {0};
|
||||
int rc;
|
||||
|
||||
/* status 13 = BLE_HS_ETIMEOUT: SMP exchange did not complete. Typical
|
||||
* cause is asymmetric bond state — drop the stale entry and tear down
|
||||
* the link so the next reconnect runs fresh pairing. */
|
||||
ESP_LOGE(TAG, "Security change failed, status %u, clearing local bond and reconnecting", status);
|
||||
|
||||
rc = ble_gap_conn_find(conn_handle, &desc);
|
||||
if (rc == 0) {
|
||||
ble_store_util_delete_peer(&desc.peer_id_addr);
|
||||
}
|
||||
|
||||
ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM);
|
||||
}
|
||||
@@ -3,18 +3,21 @@
|
||||
#
|
||||
|
||||
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
|
||||
CONFIG_BT_NIMBLE_ENABLED=n
|
||||
CONFIG_BT_BLUEDROID_ENABLED=y
|
||||
CONFIG_BT_CLASSIC_ENABLED=n
|
||||
CONFIG_BT_CONTROLLER_ENABLED=y
|
||||
CONFIG_BT_BLE_ENABLED=y
|
||||
CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y
|
||||
CONFIG_BT_ACL_CONNECTIONS=1
|
||||
CONFIG_BT_GATTC_NOTIF_REG_MAX=20
|
||||
CONFIG_BT_BLE_FEAT_ISO_EN=y
|
||||
|
||||
CONFIG_BT_ISO_MAX_CHAN=2
|
||||
|
||||
CONFIG_BT_BAP_UNICAST_CLIENT=y
|
||||
CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT=2
|
||||
|
||||
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
|
||||
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# NimBLE host overlay for this example.
|
||||
# Use with:
|
||||
# idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.$IDF_TARGET;sdkconfig.defaults.nimble" build
|
||||
|
||||
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
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
## Overview
|
||||
|
||||
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`.
|
||||
This example implements a **BAP Unicast Server** on top of the selected BLE host stack (Bluedroid by default; NimBLE via the `sdkconfig.defaults.nimble` overlay). 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.
|
||||
|
||||
@@ -34,11 +34,24 @@ This example inherits a Just-Works pairing model (LE Secure Connections, no MITM
|
||||
|
||||
## Build & Flash
|
||||
|
||||
The base `sdkconfig.defaults` defaults to the **Bluedroid** host; idf.py automatically merges the per-target overlay (`sdkconfig.defaults.$IDF_TARGET`). To build with **NimBLE** host instead, layer `sdkconfig.defaults.nimble` on top via `-DSDKCONFIG_DEFAULTS`.
|
||||
|
||||
### Bluedroid host (default)
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32h4
|
||||
idf.py -p PORT flash monitor
|
||||
```
|
||||
|
||||
### NimBLE host
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32h4
|
||||
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor
|
||||
```
|
||||
|
||||
For `esp32s31`, replace the chip overlay accordingly.
|
||||
|
||||
(Exit serial monitor with `Ctrl-]`.)
|
||||
|
||||
## Example Flow
|
||||
@@ -46,7 +59,7 @@ idf.py -p PORT flash monitor
|
||||
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.
|
||||
4. `esp_ble_audio_common_start` runs, then `adv_init("BAP Unicast Server")` performs host-specific setup (Bluedroid: create completion sem + register GAP cb + `esp_ble_gap_set_device_name`; NimBLE: `ble_svc_gap_device_name_set`). `ext_adv_start` then builds `ext_adv_data` with Flags, ASCS UUID, ASCS service data (targeted announcement + contexts), and the device name in host-agnostic bytes, and calls `adv_start(ext_adv_data, sizeof(ext_adv_data))`. The host-specific implementation in `main/bluedroid/adv.c` or `main/nimble/adv.c` configures and starts the connectable 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.
|
||||
@@ -67,6 +80,7 @@ Client connects:
|
||||
|
||||
```
|
||||
I (xxx) BAP_USR: Connected: handle ... role ... peer ...
|
||||
I (xxx) BAP_USR: Security: handle ... level ... bonded ...
|
||||
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 ...
|
||||
@@ -97,13 +111,26 @@ I (xxx) BAP_USR: [SNK #...] Release request
|
||||
I (xxx) BAP_USR: Disconnected: handle ... reason 0x...
|
||||
```
|
||||
|
||||
**Note — disconnect race.** On peer-initiated disconnect (e.g. supervision timeout, peer drops the link) with active streams, an additional Bluedroid error may interleave:
|
||||
|
||||
```
|
||||
W (xxx) BT_APPL: gattc_conn_cb: if=4 st=0 id=4 rsn=0x8
|
||||
W (xxx) BT_HCI: hcif disc complete: hdl 0x0, rsn 0x8 dev_find 1
|
||||
I (xxx) BAP_USR: [SNK #0] ISO disconnected, reason 0x08
|
||||
I (xxx) BAP_USR: [SNK #0] Stream disabled
|
||||
E (xxx) BT_APPL: Unknown connection ID: 3 fail sending notification
|
||||
I (xxx) BAP_USR: [SNK #0] Stream stopped, reason 0x08
|
||||
```
|
||||
|
||||
`Unknown connection ID` is harmless. GATT notifications for the ASE state changes are queued for Bluedroid to send. If Bluedroid clears the BTA connection on the ACL disconnect before those queued sends drain, they fail with this error. The peer has already disconnected, so the missed notifications have no effect on either side.
|
||||
|
||||
## 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:`.
|
||||
3. After ACL is up the client initiates pairing/security; both sides log `Connected:` and `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.
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
set(srcs "main.c")
|
||||
|
||||
idf_component_register(SRCS "${srcs}"
|
||||
if(CONFIG_BT_BLUEDROID_ENABLED)
|
||||
list(APPEND srcs "bluedroid/peripheral.c")
|
||||
else()
|
||||
list(APPEND srcs "nimble/peripheral.c")
|
||||
endif()
|
||||
|
||||
idf_component_register(SRCS ${srcs}
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES bt nvs_flash)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#include "esp_bt_defs.h"
|
||||
#include "esp_gap_ble_api.h"
|
||||
|
||||
#include "esp_ble_audio_common_api.h"
|
||||
|
||||
#include "peripheral.h"
|
||||
|
||||
static SemaphoreHandle_t adv_sem;
|
||||
|
||||
/* Controller status latched by gap_event_handler for EXAMPLE_WAIT_API_CHECK. */
|
||||
static esp_bt_status_t adv_op_status;
|
||||
|
||||
#define WAIT_API(_call) EXAMPLE_WAIT_API_CHECK(_call, adv_sem, portMAX_DELAY, adv_op_status)
|
||||
|
||||
static esp_ble_gap_ext_adv_params_t ext_adv_params = {
|
||||
.type = ESP_BLE_GAP_SET_EXT_ADV_PROP_CONNECTABLE,
|
||||
.interval_min = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS),
|
||||
.interval_max = ESP_BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS),
|
||||
.channel_map = ADV_CHNL_ALL,
|
||||
.filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
|
||||
.primary_phy = ESP_BLE_GAP_PHY_1M,
|
||||
.max_skip = 0,
|
||||
.secondary_phy = ESP_BLE_GAP_PHY_2M,
|
||||
.sid = ADV_SID,
|
||||
.scan_req_notif = false,
|
||||
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
|
||||
.tx_power = ADV_TX_POWER,
|
||||
};
|
||||
|
||||
static esp_ble_gap_ext_adv_t ext_adv_inst[1] = {
|
||||
[0] = { ADV_HANDLE, 0, 0 },
|
||||
};
|
||||
|
||||
static void gap_event_handler(esp_gap_ble_cb_event_t event,
|
||||
esp_ble_gap_cb_param_t *param)
|
||||
{
|
||||
switch (event) {
|
||||
case ESP_GAP_BLE_EXT_ADV_SET_PARAMS_COMPLETE_EVT:
|
||||
adv_op_status = param->ext_adv_set_params.status;
|
||||
xSemaphoreGive(adv_sem);
|
||||
break;
|
||||
case ESP_GAP_BLE_EXT_ADV_DATA_SET_COMPLETE_EVT:
|
||||
adv_op_status = param->ext_adv_data_set.status;
|
||||
xSemaphoreGive(adv_sem);
|
||||
break;
|
||||
case ESP_GAP_BLE_EXT_ADV_START_COMPLETE_EVT:
|
||||
adv_op_status = param->ext_adv_start.status;
|
||||
xSemaphoreGive(adv_sem);
|
||||
break;
|
||||
|
||||
/* SMP request handling for Just Works pairing (IO_CAP=NONE). The peer
|
||||
* (central) initiates; we accept the security request and confirm the
|
||||
* numeric comparison. */
|
||||
case ESP_GAP_BLE_SEC_REQ_EVT:
|
||||
esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true);
|
||||
break;
|
||||
case ESP_GAP_BLE_NC_REQ_EVT:
|
||||
esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, true);
|
||||
break;
|
||||
|
||||
case ESP_GAP_BLE_AUTH_CMPL_EVT:
|
||||
esp_ble_audio_gap_app_post_event(event, param);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int app_host_init(void)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
adv_sem = xSemaphoreCreateBinary();
|
||||
if (adv_sem == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to create adv semaphore");
|
||||
return -1;
|
||||
}
|
||||
|
||||
err = esp_ble_gap_register_callback(gap_event_handler);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register GAP callback, err %d", err);
|
||||
vSemaphoreDelete(adv_sem);
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int set_device_name(void)
|
||||
{
|
||||
return esp_ble_gap_set_device_name(LOCAL_DEVICE_NAME);
|
||||
}
|
||||
|
||||
int ext_adv_start(const uint8_t *ext_data, uint16_t ext_len)
|
||||
{
|
||||
WAIT_API(esp_ble_gap_ext_adv_set_params(ADV_HANDLE, &ext_adv_params));
|
||||
WAIT_API(esp_ble_gap_config_ext_adv_data_raw(ADV_HANDLE, ext_len, ext_data));
|
||||
WAIT_API(esp_ble_gap_ext_adv_start(1, ext_adv_inst));
|
||||
|
||||
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -13,11 +13,6 @@
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_timer.h"
|
||||
|
||||
#include "host/ble_hs.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
|
||||
#include "esp_ble_audio_lc3_defs.h"
|
||||
#include "esp_ble_audio_bap_api.h"
|
||||
@@ -26,18 +21,7 @@
|
||||
#include "ble_audio_example_init.h"
|
||||
#include "ble_audio_example_utils.h"
|
||||
|
||||
#define TAG "BAP_USR"
|
||||
|
||||
#define LOCAL_DEVICE_NAME "BAP Unicast Server"
|
||||
#define LOCAL_DEVICE_NAME_LEN (sizeof(LOCAL_DEVICE_NAME) - 1)
|
||||
|
||||
#define ADV_HANDLE 0x00
|
||||
#define ADV_SID 0
|
||||
#define ADV_TX_POWER 127
|
||||
#define ADV_ADDRESS BLE_OWN_ADDR_PUBLIC
|
||||
#define ADV_PRIMARY_PHY BLE_HCI_LE_PHY_1M
|
||||
#define ADV_SECONDARY_PHY BLE_HCI_LE_PHY_2M
|
||||
#define ADV_INTERVAL BLE_GAP_ADV_ITVL_MS(200)
|
||||
#include "peripheral.h"
|
||||
|
||||
#define SINK_LOCATION (ESP_BLE_AUDIO_LOCATION_FRONT_LEFT | \
|
||||
ESP_BLE_AUDIO_LOCATION_FRONT_RIGHT)
|
||||
@@ -536,7 +520,7 @@ static int set_pacs_available_contexts(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void build_adv_data(void)
|
||||
static void build_ext_adv_data(void)
|
||||
{
|
||||
size_t idx = 0;
|
||||
|
||||
@@ -569,60 +553,6 @@ static void build_adv_data(void)
|
||||
memcpy(&ext_adv_data[idx], LOCAL_DEVICE_NAME, LOCAL_DEVICE_NAME_LEN);
|
||||
}
|
||||
|
||||
static void ext_adv_start(void)
|
||||
{
|
||||
struct ble_gap_ext_adv_params ext_params = {0};
|
||||
struct os_mbuf *data = NULL;
|
||||
int err;
|
||||
|
||||
build_adv_data();
|
||||
|
||||
ext_params.connectable = 1;
|
||||
ext_params.scannable = 0;
|
||||
ext_params.legacy_pdu = 0;
|
||||
ext_params.own_addr_type = ADV_ADDRESS;
|
||||
ext_params.primary_phy = ADV_PRIMARY_PHY;
|
||||
ext_params.secondary_phy = ADV_SECONDARY_PHY;
|
||||
ext_params.tx_power = ADV_TX_POWER;
|
||||
ext_params.sid = ADV_SID;
|
||||
ext_params.itvl_min = ADV_INTERVAL;
|
||||
ext_params.itvl_max = ADV_INTERVAL;
|
||||
|
||||
err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL,
|
||||
example_audio_gap_event_cb, NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
data = os_msys_get_pkthdr(sizeof(ext_adv_data), 0);
|
||||
if (data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to get ext adv mbuf");
|
||||
return;
|
||||
}
|
||||
|
||||
err = os_mbuf_append(data, ext_adv_data, sizeof(ext_adv_data));
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err);
|
||||
os_mbuf_free_chain(data);
|
||||
return;
|
||||
}
|
||||
|
||||
err = ble_gap_ext_adv_set_data(ADV_HANDLE, data);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
|
||||
}
|
||||
|
||||
static void acl_connect(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
if (event->acl_connect.status) {
|
||||
@@ -632,9 +562,7 @@ static void acl_connect(esp_ble_audio_gap_app_event_t *event)
|
||||
|
||||
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]);
|
||||
EXAMPLE_BT_ADDR_PRINT_ARGS(event->acl_connect.dst.val));
|
||||
}
|
||||
|
||||
static void acl_disconnect(esp_ble_audio_gap_app_event_t *event)
|
||||
@@ -642,7 +570,22 @@ static void acl_disconnect(esp_ble_audio_gap_app_event_t *event)
|
||||
ESP_LOGI(TAG, "Disconnected: handle %u reason 0x%02x",
|
||||
event->acl_disconnect.conn_handle, event->acl_disconnect.reason);
|
||||
|
||||
ext_adv_start();
|
||||
ext_adv_start(ext_adv_data, sizeof(ext_adv_data));
|
||||
}
|
||||
|
||||
static void security_change(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
if (event->security_change.status) {
|
||||
ESP_LOGE(TAG, "Security failed: handle %u status %d",
|
||||
event->security_change.conn_handle,
|
||||
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_audio_gap_app_event_t *event)
|
||||
@@ -654,6 +597,9 @@ static void iso_gap_app_cb(esp_ble_audio_gap_app_event_t *event)
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_ACL_DISCONNECT:
|
||||
acl_disconnect(event);
|
||||
break;
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_SECURITY_CHANGE:
|
||||
security_change(event);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -735,6 +681,12 @@ void app_main(void)
|
||||
return;
|
||||
}
|
||||
|
||||
err = app_host_init();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to init host, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_common_init(&info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize audio, err %d", err);
|
||||
@@ -800,11 +752,13 @@ void app_main(void)
|
||||
return;
|
||||
}
|
||||
|
||||
err = ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME);
|
||||
err = set_device_name();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set device name, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ext_adv_start();
|
||||
build_ext_adv_data();
|
||||
|
||||
ext_adv_start(ext_adv_data, sizeof(ext_adv_data));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "host/ble_gap.h"
|
||||
#include "host/ble_store.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
|
||||
#include "esp_ble_audio_common_api.h"
|
||||
|
||||
#include "peripheral.h"
|
||||
|
||||
static int gap_event_cb(struct ble_gap_event *event, void *arg)
|
||||
{
|
||||
switch (event->type) {
|
||||
case BLE_GAP_EVENT_CONNECT:
|
||||
case BLE_GAP_EVENT_DISCONNECT:
|
||||
case BLE_GAP_EVENT_ENC_CHANGE:
|
||||
esp_ble_audio_gap_app_post_event(event->type, event);
|
||||
break;
|
||||
case BLE_GAP_EVENT_MTU:
|
||||
case BLE_GAP_EVENT_NOTIFY_RX:
|
||||
case BLE_GAP_EVENT_NOTIFY_TX:
|
||||
case BLE_GAP_EVENT_SUBSCRIBE:
|
||||
esp_ble_audio_gatt_app_post_event(event->type, event);
|
||||
break;
|
||||
case BLE_GAP_EVENT_REPEAT_PAIRING: {
|
||||
/* Peer wants to re-pair on top of an existing bond — delete our stale
|
||||
* bond entry and tell NimBLE to retry pairing. */
|
||||
struct ble_gap_conn_desc desc = {0};
|
||||
int rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc);
|
||||
if (rc == 0) {
|
||||
ble_store_util_delete_peer(&desc.peer_id_addr);
|
||||
}
|
||||
return BLE_GAP_REPEAT_PAIRING_RETRY;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int app_host_init(void)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int set_device_name(void)
|
||||
{
|
||||
return ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME);
|
||||
}
|
||||
|
||||
int ext_adv_start(const uint8_t *ext_data, uint16_t ext_len)
|
||||
{
|
||||
struct ble_gap_ext_adv_params ext_params = {0};
|
||||
struct os_mbuf *data;
|
||||
int err;
|
||||
|
||||
ext_params.connectable = 1;
|
||||
ext_params.scannable = 0;
|
||||
ext_params.legacy_pdu = 0;
|
||||
ext_params.own_addr_type = BLE_OWN_ADDR_PUBLIC;
|
||||
ext_params.primary_phy = BLE_HCI_LE_PHY_1M;
|
||||
ext_params.secondary_phy = BLE_HCI_LE_PHY_2M;
|
||||
ext_params.tx_power = ADV_TX_POWER;
|
||||
ext_params.sid = ADV_SID;
|
||||
ext_params.itvl_min = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS);
|
||||
ext_params.itvl_max = BLE_GAP_ADV_ITVL_MS(ADV_INTERVAL_MS);
|
||||
|
||||
err = ble_gap_ext_adv_configure(ADV_HANDLE, &ext_params, NULL,
|
||||
gap_event_cb, NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to configure ext adv params, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
data = os_msys_get_pkthdr(ext_len, 0);
|
||||
if (data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to get ext adv mbuf");
|
||||
return -1;
|
||||
}
|
||||
|
||||
err = os_mbuf_append(data, ext_data, ext_len);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to append ext adv data, err %d", err);
|
||||
os_mbuf_free_chain(data);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = ble_gap_ext_adv_set_data(ADV_HANDLE, data);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set ext adv data, err %d", err);
|
||||
/* ble_gap_ext_adv_set_data takes ownership of `data` and frees it via
|
||||
* its 'done' label on both success and failure paths — do NOT free
|
||||
* here (double-free). API header doesn't document this contract. */
|
||||
return err;
|
||||
}
|
||||
|
||||
err = ble_gap_ext_adv_start(ADV_HANDLE, 0, 0);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start ext advertising, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Advertising started (handle %u)", ADV_HANDLE);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "ble_audio_example_utils.h"
|
||||
|
||||
#define TAG "BAP_USR"
|
||||
|
||||
#define LOCAL_DEVICE_NAME "BAP Unicast Server"
|
||||
#define LOCAL_DEVICE_NAME_LEN (sizeof(LOCAL_DEVICE_NAME) - 1)
|
||||
|
||||
#define ADV_HANDLE 0
|
||||
#define ADV_SID 0
|
||||
#define ADV_TX_POWER 127
|
||||
#define ADV_INTERVAL_MS 200
|
||||
|
||||
int app_host_init(void);
|
||||
|
||||
int set_device_name(void);
|
||||
|
||||
int ext_adv_start(const uint8_t *ext_data, uint16_t ext_len);
|
||||
@@ -3,14 +3,15 @@
|
||||
#
|
||||
|
||||
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
|
||||
CONFIG_BT_NIMBLE_ENABLED=n
|
||||
CONFIG_BT_BLUEDROID_ENABLED=y
|
||||
CONFIG_BT_CLASSIC_ENABLED=n
|
||||
CONFIG_BT_CONTROLLER_ENABLED=y
|
||||
CONFIG_BT_BLE_ENABLED=y
|
||||
CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y
|
||||
CONFIG_BT_ACL_CONNECTIONS=1
|
||||
CONFIG_BT_GATTC_NOTIF_REG_MAX=20
|
||||
CONFIG_BT_BLE_FEAT_ISO_EN=y
|
||||
|
||||
CONFIG_BT_ISO_MAX_CHAN=2
|
||||
|
||||
@@ -24,4 +25,6 @@ CONFIG_BT_PAC_SRC_LOC_WRITEABLE=y
|
||||
CONFIG_BT_PAC_SRC_LOC_NOTIFIABLE=y
|
||||
CONFIG_BT_PACS_SUPPORTED_CONTEXT_NOTIFIABLE=y
|
||||
|
||||
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
|
||||
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# NimBLE host overlay for this example.
|
||||
# Use with:
|
||||
# idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.$IDF_TARGET;sdkconfig.defaults.nimble" build
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user