mirror of
https://github.com/espressif/esp-idf.git
synced 2026-09-22 13:01:16 +03:00
feat(ble_audio): Add CAP Handover (U2B & B2U) example
This commit is contained in:
@@ -102,7 +102,17 @@ esp_err_t esp_ble_audio_gattc_disc_start(uint16_t conn_handle);
|
||||
#define ESP_BLE_AUDIO_GAP_EVENT_ACL_DISCONNECT BT_LE_GAP_APP_EVENT_ACL_DISCONNECT
|
||||
/*!< Audio GAP Security Change event */
|
||||
#define ESP_BLE_AUDIO_GAP_EVENT_SECURITY_CHANGE BT_LE_GAP_APP_EVENT_SECURITY_CHANGE
|
||||
/** Audio GAP application event structure */
|
||||
/**
|
||||
* @brief Audio GAP application event structure
|
||||
*
|
||||
* @note Addresses carried by these events are in the **active host's own byte
|
||||
* order**: on-air/LSB-first under NimBLE, MSB-first under Bluedroid
|
||||
* (the order every `esp_ble_gap_*` API takes). Feeding one straight back
|
||||
* to a host API is therefore always correct; comparing one against an
|
||||
* address the audio layer holds, such as a Broadcast Receive State,
|
||||
* needs a reversal under Bluedroid. See `struct bt_le_addr` in
|
||||
* common/app/gap.h for the full convention.
|
||||
*/
|
||||
typedef struct bt_le_gap_app_event esp_ble_audio_gap_app_event_t;
|
||||
|
||||
/*!< Audio GATT MTU exchange complete event */
|
||||
|
||||
@@ -185,7 +185,7 @@ static const uint16_t ext_structs[] = {
|
||||
sizeof(struct bt_bond_info),
|
||||
};
|
||||
|
||||
#define LEA_VERSION (0x20260824)
|
||||
#define LEA_VERSION (0x20260828)
|
||||
|
||||
struct lib_ext_cfgs {
|
||||
/* BLE */
|
||||
|
||||
@@ -1976,7 +1976,8 @@ struct bt_bap_unicast_client_cb {
|
||||
* @param dir The type of remote endpoints and capabilities discovered.
|
||||
* @param codec_cap Remote capabilities.
|
||||
*
|
||||
* If discovery procedure has complete both @p codec and @p ep are set to NULL.
|
||||
* Called once per record; the end of the procedure is reported by the
|
||||
* discover callback below, not by a NULL @p codec_cap.
|
||||
*/
|
||||
void (*pac_record)(struct bt_conn *conn, enum bt_audio_dir dir,
|
||||
const struct bt_audio_codec_cap *codec_cap);
|
||||
@@ -1990,21 +1991,21 @@ struct bt_bap_unicast_client_cb {
|
||||
* @param dir The type of remote endpoints and capabilities discovered.
|
||||
* @param ep Remote endpoint.
|
||||
*
|
||||
* If discovery procedure has complete both @p codec and @p ep are set to NULL.
|
||||
* Called once per endpoint; the end of the procedure is reported by the
|
||||
* discover callback below, not by a NULL @p ep.
|
||||
*/
|
||||
void (*endpoint)(struct bt_conn *conn, enum bt_audio_dir dir, struct bt_bap_ep *ep);
|
||||
|
||||
/**
|
||||
* @brief BAP discovery callback function.
|
||||
*
|
||||
* If discovery procedure has completed @p ep is set to NULL and @p err is 0.
|
||||
* Called once the discovery procedure has completed, for the direction it
|
||||
* covered.
|
||||
*
|
||||
* @param conn Connection to the remote unicast server.
|
||||
* @param err Error value. 0 on success, GATT error on positive value or errno on
|
||||
* negative value.
|
||||
* @param dir The type of remote endpoints and capabilities discovered.
|
||||
*
|
||||
* If discovery procedure has complete both @p codec and @p ep are set to NULL.
|
||||
*/
|
||||
void (*discover)(struct bt_conn *conn, int err, enum bt_audio_dir dir);
|
||||
|
||||
|
||||
@@ -996,19 +996,21 @@ struct bt_cap_handover_broadcast_to_unicast_param {
|
||||
|
||||
/** @brief Broadcast ID of the @p broadcast_source
|
||||
*
|
||||
* Ignored if @p reception_stop_param is not NULL.
|
||||
* Always required: receive state notifications are matched against the
|
||||
* {broadcast_id, adv_sid, adv_type} triple to decide that reception stopped,
|
||||
* whether or not @p reception_stop_param is given.
|
||||
*/
|
||||
uint32_t broadcast_id;
|
||||
|
||||
/** @brief Advertising set ID of the @p broadcast_source
|
||||
*
|
||||
* Ignored if @p reception_stop_param is not NULL.
|
||||
* Always required, see @p broadcast_id.
|
||||
*/
|
||||
uint8_t adv_sid;
|
||||
|
||||
/** @brief Advertising type of the advertising address of @p broadcast_source
|
||||
*
|
||||
* Ignored if @p reception_stop_param is not NULL.
|
||||
* Always required, see @p broadcast_id.
|
||||
*/
|
||||
uint8_t adv_type;
|
||||
|
||||
|
||||
Submodule components/bt/esp_ble_audio/lib/lib updated: 3ada1f9e75...774d777766
@@ -571,7 +571,17 @@ esp_err_t esp_ble_iso_chan_send_ts(esp_ble_iso_chan_t *chan,
|
||||
#define ESP_BLE_ISO_GAP_EVENT_SECURITY_CHANGE BT_LE_GAP_APP_EVENT_SECURITY_CHANGE
|
||||
/*!< ISO GAP BIGInfo Adv Report event */
|
||||
#define ESP_BLE_ISO_GAP_EVENT_BIGINFO_RECV BT_LE_GAP_APP_EVENT_BIGINFO_RECV
|
||||
/** ISO GAP application event structure */
|
||||
/**
|
||||
* @brief ISO GAP application event structure
|
||||
*
|
||||
* @note Addresses carried by these events are in the **active host's own byte
|
||||
* order**: on-air/LSB-first under NimBLE, MSB-first under Bluedroid
|
||||
* (the order every `esp_ble_gap_*` API takes). Feeding one straight back
|
||||
* to a host API is therefore always correct; comparing one against an
|
||||
* address the audio layer holds, such as a Broadcast Receive State,
|
||||
* needs a reversal under Bluedroid. See `struct bt_le_addr` in
|
||||
* common/app/gap.h for the full convention.
|
||||
*/
|
||||
typedef struct bt_le_gap_app_event esp_ble_iso_gap_app_event_t;
|
||||
|
||||
/** ISO initialization information structure */
|
||||
|
||||
@@ -1881,7 +1881,7 @@ static void handle_gattc_notify_event(struct bt_le_gattc_notify_rx_event *event)
|
||||
* tearing down a core subscription like the ASCS control point
|
||||
* over one bad PDU would drop every later notification. Tolerate
|
||||
* the bad PDU and keep the subscription. */
|
||||
params->notify(conn, params, event->value, event->len);
|
||||
params->notify(conn, params, NOTIFY_VALUE(event), event->len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ static void handle_gattc_notify_rx_event_safe(struct bt_le_gattc_notify_rx_event
|
||||
* tearing down a core subscription like the ASCS control point
|
||||
* over one bad PDU would drop every later notification. Tolerate
|
||||
* the bad PDU and keep the subscription. */
|
||||
params->notify(conn, params, event->value, event->len);
|
||||
params->notify(conn, params, NOTIFY_VALUE(event), event->len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,7 @@ static struct bt_le_ext_adv *ext_adv_find(uint8_t adv_handle)
|
||||
struct bt_le_ext_adv *adv = NULL;
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(ext_adv_pool); i++) {
|
||||
if (atomic_test_bit(ext_adv_pool[i].flags,
|
||||
BT_PER_ADV_PARAMS_SET) &&
|
||||
if (atomic_test_bit(ext_adv_pool[i].flags, BT_PER_ADV_PARAMS_SET) &&
|
||||
ext_adv_pool[i].handle == adv_handle) {
|
||||
LOG_DBG("ExtAdvFound[%u][%u]", i, adv_handle);
|
||||
adv = &ext_adv_pool[i];
|
||||
@@ -44,8 +43,7 @@ static struct bt_le_ext_adv *ext_adv_new(void)
|
||||
struct bt_le_ext_adv *adv = NULL;
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(ext_adv_pool); i++) {
|
||||
if (atomic_test_bit(ext_adv_pool[i].flags,
|
||||
BT_PER_ADV_PARAMS_SET) == false) {
|
||||
if (atomic_test_bit(ext_adv_pool[i].flags, BT_PER_ADV_PARAMS_SET) == false) {
|
||||
adv = &ext_adv_pool[i];
|
||||
|
||||
memset(adv, 0, sizeof(*adv));
|
||||
@@ -160,3 +158,23 @@ int bt_le_ext_adv_get_info(const struct bt_le_ext_adv *adv,
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
_LIB_ONLY
|
||||
struct bt_le_ext_adv *bt_le_ext_adv_lookup_addr(const bt_addr_le_t *adv_addr, uint8_t sid)
|
||||
{
|
||||
struct bt_le_ext_adv *adv = NULL;
|
||||
|
||||
BT_LE_ASSERT(adv_addr);
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(ext_adv_pool); i++) {
|
||||
if (atomic_test_bit(ext_adv_pool[i].flags, BT_PER_ADV_PARAMS_SET) &&
|
||||
bt_addr_le_eq(&ext_adv_pool[i].addr, adv_addr) &&
|
||||
ext_adv_pool[i].sid == sid) {
|
||||
LOG_INF("ExtAdvLookupAddrFound[%u][%u]", i, sid);
|
||||
adv = &ext_adv_pool[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return adv;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,30 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Byte order of `val` is the active host's, NOT a single fixed convention:
|
||||
*
|
||||
* NimBLE on-air / LSB-first, val[0] is the least significant octet.
|
||||
* Bluedroid MSB-first, val[0] is the most significant octet - the order
|
||||
* esp_bd_addr_t uses and the order every esp_ble_gap_* API expects.
|
||||
*
|
||||
* That is deliberate: an address delivered by an event is normally handed
|
||||
* straight back to the same host's API (create sync, connect, disconnect), so
|
||||
* leaving it untouched keeps those paths correct on both hosts.
|
||||
*
|
||||
* The cost is that `bt_addr_le_t` in this port does not mean one thing either.
|
||||
* On Bluedroid its contents follow whichever path filled it:
|
||||
*
|
||||
* conn->le.dst MSB - stored verbatim from the connect event and
|
||||
* passed back to BTA_GATTC_* unchanged.
|
||||
* bt_bond_info.addr MSB - read from the Bluedroid bond store as is.
|
||||
* per_adv_sync->addr LSB - bt_le_per_adv_sync_new() reverses it, because
|
||||
* it is compared against addresses off the air.
|
||||
* BASS recv_state->addr LSB - arrives over ATT, where BASS defines LSB-first.
|
||||
*
|
||||
* So an application only has to convert where the two meet: comparing an event
|
||||
* address against one the audio layer holds (a Broadcast Receive State, say)
|
||||
* needs a reversal under CONFIG_BT_BLUEDROID_ENABLED and none under NimBLE.
|
||||
*/
|
||||
struct bt_le_addr {
|
||||
uint8_t type;
|
||||
uint8_t val[6];
|
||||
|
||||
@@ -72,6 +72,15 @@ struct bt_le_gattc_notify_rx_event {
|
||||
uint8_t *value;
|
||||
};
|
||||
|
||||
/* Neither adapter allocates a buffer for a zero-length notification, but NULL data is
|
||||
* how gatt.c completes an unsubscribe: a lib notify handler that sees it drops its
|
||||
* subscription. A zero-length notification is a real PDU (BASS sends one for an emptied
|
||||
* Broadcast Receive State), so keep the pointer non-NULL when handing it to the lib.
|
||||
*/
|
||||
#define NOTIFY_VALUE(_event) \
|
||||
((const void *)((_event)->value != NULL ? (const uint8_t *)(_event)->value \
|
||||
: (const uint8_t *)""))
|
||||
|
||||
struct bt_le_gatts_notify_tx_event {
|
||||
bool is_notify;
|
||||
uint16_t conn_handle;
|
||||
|
||||
@@ -406,6 +406,16 @@ int bt_le_per_adv_sync_get_info(struct bt_le_per_adv_sync *per_adv_sync,
|
||||
struct bt_le_per_adv_sync *bt_le_per_adv_sync_lookup_addr(const bt_addr_le_t *adv_addr,
|
||||
uint8_t sid);
|
||||
|
||||
/**
|
||||
* @brief Look up a local extended advertising set by advertiser address.
|
||||
*
|
||||
* @param adv_addr Advertiser address.
|
||||
* @param sid The advertising set ID.
|
||||
*
|
||||
* @return Extended advertising set object or NULL if not found.
|
||||
*/
|
||||
struct bt_le_ext_adv *bt_le_ext_adv_lookup_addr(const bt_addr_le_t *adv_addr, uint8_t sid);
|
||||
|
||||
/**
|
||||
* @brief Register periodic advertising sync callbacks.
|
||||
*
|
||||
|
||||
@@ -51,6 +51,7 @@ Application Examples
|
||||
|
||||
* :example:`bluetooth/esp_ble_audio/cap/acceptor` demonstrates how to act as a CAP Acceptor for unicast and broadcast flows.
|
||||
* :example:`bluetooth/esp_ble_audio/cap/initiator` demonstrates how to act as a CAP Initiator for unicast and broadcast flows.
|
||||
* :example:`bluetooth/esp_ble_audio/cap/handover` demonstrates how to perform CAP handovers between unicast and broadcast flows.
|
||||
|
||||
* **TMAP (Telephony and Media Audio Profile)**
|
||||
|
||||
|
||||
@@ -298,7 +298,7 @@ I (xxx) CAP_ACC: Scanning for broadcast source...
|
||||
|
||||
## Peer Pairing
|
||||
|
||||
Run the [initiator](../initiator/) on a second board. The initiator and acceptor must be configured for the **same sub-mode** — both `EXAMPLE_UNICAST`, or both `EXAMPLE_BROADCAST` — otherwise they will not pair.
|
||||
Run the [initiator](../initiator/) on a second board with the matching role — both `EXAMPLE_UNICAST`, or both `EXAMPLE_BROADCAST`. For a CAP handover, run the [handover](../handover/) example instead, which needs both roles on this acceptor.
|
||||
|
||||
### Unicast
|
||||
|
||||
@@ -312,5 +312,60 @@ Run the [initiator](../initiator/) on a second board. The initiator and acceptor
|
||||
|
||||
1. Flash the initiator with `EXAMPLE_BROADCAST`; it advertises as `CAP Broadcast Source` (broadcast ID `0x123456`) and starts the BIG.
|
||||
2. Flash this acceptor with `EXAMPLE_BROADCAST`. Either enable `EXAMPLE_SCAN_SELF` to self-scan for the source by name (broadcast code `1234`), or leave it disabled and use a separate Broadcast Assistant that connects via BASS to drive PA / BIS sync.
|
||||
3. The acceptor PA-syncs, receives BASE and BIGInfo, syncs the first BIS, and `[SNK #0] Stream started`.
|
||||
3. The acceptor PA-syncs, receives BASE and BIGInfo, and syncs one BIS per sink stream it has (`CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT`), up to what the first subgroup of the BASE offers: `[SNK #0] Stream started`, `[SNK #1] Stream started`.
|
||||
4. On PA sync loss the acceptor cleans up; in self-scan mode it restarts scanning.
|
||||
|
||||
### Handover
|
||||
|
||||
1. Build this acceptor with both `EXAMPLE_UNICAST` and `EXAMPLE_BROADCAST` (the defaults),
|
||||
so it exposes ASCS and BASS at the same time. No code change is needed.
|
||||
2. Flash the [handover](../handover/) example on the other board. It connects, discovers
|
||||
CAS, the ASEs and BASS, starts unicast audio, then alternates between unicast and broadcast.
|
||||
3. On a unicast-to-broadcast handover the acceptor sees its sink ASEs released, then a
|
||||
BASS Add Source from the collocated Commander, and PA/BIS-syncs to the new source.
|
||||
4. On the way back the receive state is cleared and the sink ASEs are configured again.
|
||||
|
||||
Only the sink direction moves; a broadcast Audio Stream has no return path, so the source
|
||||
ASE is not part of the procedure.
|
||||
|
||||
#### The sink stream pool is shared
|
||||
|
||||
Unicast and broadcast draw sink streams from the **same** pool: `stream_alloc(SINK)` hands out
|
||||
`peer.sink_streams[]` entries whose endpoint is unbound. That is what lets a handover reuse the
|
||||
objects, and it also means that while every sink stream carries a BIS there is none left to accept
|
||||
a unicast Config.
|
||||
|
||||
The acceptor says so rather than letting the Initiator discover the shortage through a `NO_MEM`:
|
||||
`sink_availability_update()` sets the **PACS Available Audio Contexts** for the sink direction to
|
||||
`NONE` while receiving, and restores them when reception stops. CAP §7.3.1.8 / §7.3.1.9 describe
|
||||
exactly this ("Start of reception **can** affect an Acceptor's availability for unicast Audio
|
||||
Streams. In this case, the Acceptor **will** update its Available Audio Contexts characteristic").
|
||||
|
||||
It is driven off **BIS_Sync** in the Broadcast Receive State rather than off a local
|
||||
"broadcasting" flag, because BIS_Sync is the field an Assistant clears *first* when it stops our
|
||||
reception — which puts the restore comfortably ahead of the unicast Config that follows in a
|
||||
broadcast-to-unicast handover.
|
||||
|
||||
#### PAST is expected, self-scan is not
|
||||
|
||||
The handover example is a collocated broadcaster and hands its periodic advertising train over
|
||||
with **Set Info Transfer**. This acceptor must therefore be built **without** `EXAMPLE_SCAN_SELF`
|
||||
(its Kconfig already makes that mutually exclusive with `EXAMPLE_UNICAST`). It reports
|
||||
`PA_Sync_State = 1` (*SyncInfo Request*) and waits for the transfer; the Source ID arrives in the
|
||||
**high octet** of the transfer's service data.
|
||||
|
||||
#### Source IDs are ours to assign, and they are reused
|
||||
|
||||
BASS Table 3.9: the Source_ID is *assigned by the server* and only has to be unique among the
|
||||
receive states **currently exposed**. `next_src_id()` is a byte counter that skips values held by
|
||||
active receive states, so a number becomes available again as soon as its receive state is
|
||||
removed, and wraps after 256 allocations.
|
||||
|
||||
Two consequences worth knowing when reading logs:
|
||||
|
||||
* A receive state **outlives the Initiator's reboot** — it lives here. That is why the handover
|
||||
example sweeps and clears leftovers when it connects; without that, its first Add Source after a
|
||||
reflash is rejected with `0xFC` for duplicating the {address, SID, Broadcast ID} triple
|
||||
(BAP §6.5.4).
|
||||
* Source IDs keep climbing across the peer's restarts and only restart from 0 when **this** board
|
||||
reboots. A jump back to 0 in the log means the acceptor restarted, not that a counter wrapped.
|
||||
|
||||
@@ -13,7 +13,6 @@ menu "Example: CAP Acceptor"
|
||||
config EXAMPLE_BROADCAST
|
||||
bool "Broadcast"
|
||||
default y if !EXAMPLE_UNICAST
|
||||
select BT_NIMBLE_PERIODIC_ADV_SYNC_TRANSFER
|
||||
help
|
||||
If set, advertise as a Broadcast acceptor (BASS Scan Delegator)
|
||||
for syncable broadcast audio. Can coexist with EXAMPLE_UNICAST
|
||||
|
||||
@@ -45,9 +45,23 @@
|
||||
ESP_BLE_AUDIO_CONTEXT_TYPE_MEDIA | \
|
||||
ESP_BLE_AUDIO_CONTEXT_TYPE_GAME)
|
||||
|
||||
/* Number of stream objects the application backs per direction.
|
||||
*
|
||||
* Sink: one per exposed ASE, so a stereo Initiator can configure two independent
|
||||
* sink streams. The CAP handover procedures hand over all streaming sink streams
|
||||
* at once (CAP 7.3.1.10 applies to every CIS carrying Initiator-to-Acceptor audio),
|
||||
* so the sink pool has to match the number of ASEs that can be configured.
|
||||
*
|
||||
* Source: one. Only the bidirectional (call) configuration uses it, and the TX
|
||||
* pump in cap_acceptor_unicast.c drives a single stream. A Config request beyond
|
||||
* these counts is answered with NO_MEM by stream_alloc().
|
||||
*/
|
||||
#define ACCEPTOR_SINK_STREAM_COUNT CONFIG_BT_ASCS_MAX_ASE_SNK_COUNT
|
||||
#define ACCEPTOR_SOURCE_STREAM_COUNT 1
|
||||
|
||||
struct peer_config {
|
||||
esp_ble_audio_cap_stream_t source_stream;
|
||||
esp_ble_audio_cap_stream_t sink_stream;
|
||||
esp_ble_audio_cap_stream_t source_streams[ACCEPTOR_SOURCE_STREAM_COUNT];
|
||||
esp_ble_audio_cap_stream_t sink_streams[ACCEPTOR_SINK_STREAM_COUNT];
|
||||
uint16_t conn_handle;
|
||||
/* Assistant's BD addr — used by Bluedroid pa_sync_with_past to set per-peer
|
||||
* PAST receive params. Populated at ACL_CONNECT, cleared at ACL_DISCONNECT. */
|
||||
|
||||
@@ -63,6 +63,21 @@ static inline bool flag_test_and_set(uint8_t bit)
|
||||
return was;
|
||||
}
|
||||
|
||||
/* The audio stack keeps addresses on-air (LSB-first) in bt_addr_le_t, Bluedroid takes
|
||||
* and reports them MSB-first, NimBLE on-air. Convert whenever one meets the other; the
|
||||
* reversal is its own inverse, so this serves both directions.
|
||||
*/
|
||||
static void addr_order_copy(uint8_t dst[6], const uint8_t src[6])
|
||||
{
|
||||
#if CONFIG_BT_BLUEDROID_ENABLED
|
||||
for (size_t i = 0; i < 6; i++) {
|
||||
dst[i] = src[5 - i];
|
||||
}
|
||||
#else
|
||||
memcpy(dst, src, 6);
|
||||
#endif
|
||||
}
|
||||
|
||||
static struct broadcast_sink {
|
||||
esp_ble_audio_cap_stream_t cap_streams[CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT];
|
||||
const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state;
|
||||
@@ -188,8 +203,15 @@ void broadcast_scan_recv(esp_ble_audio_gap_app_event_t *event)
|
||||
}
|
||||
#endif /* CONFIG_EXAMPLE_SCAN_SELF */
|
||||
|
||||
static void sink_availability_update(bool receiving);
|
||||
|
||||
static void broadcast_sink_reset(void)
|
||||
{
|
||||
/* Reception is over however we got here (Remove Source, PA loss, ACL drop),
|
||||
* so the sink pool is free again even if no receive state said so.
|
||||
*/
|
||||
sink_availability_update(false);
|
||||
|
||||
broadcast_sink.recv_state = NULL;
|
||||
broadcast_sink.sink = NULL;
|
||||
broadcast_sink.requested_bis_sync = 0;
|
||||
@@ -214,6 +236,29 @@ static void broadcast_sink_reset(void)
|
||||
#endif /* CONFIG_EXAMPLE_SCAN_SELF */
|
||||
}
|
||||
|
||||
struct base_subgroup_pick {
|
||||
uint32_t bis_indexes;
|
||||
bool found;
|
||||
};
|
||||
|
||||
static bool base_pick_subgroup_cb(const esp_ble_audio_bap_base_subgroup_t *subgroup,
|
||||
void *user_data)
|
||||
{
|
||||
struct base_subgroup_pick *pick = user_data;
|
||||
|
||||
/* Keep the first subgroup only. BIS of different subgroups carry different
|
||||
* codec configurations, so a sink synchronizes within one subgroup.
|
||||
*/
|
||||
if (pick->found == false &&
|
||||
esp_ble_audio_bap_base_subgroup_get_bis_indexes(subgroup,
|
||||
&pick->bis_indexes) == ESP_OK) {
|
||||
pick->found = true;
|
||||
}
|
||||
|
||||
/* Always continue: stopping early makes the iterator return -ECANCELED. */
|
||||
return true;
|
||||
}
|
||||
|
||||
static void check_sync_broadcast(void)
|
||||
{
|
||||
esp_ble_audio_bap_stream_t *streams[CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT];
|
||||
@@ -258,22 +303,33 @@ static void check_sync_broadcast(void)
|
||||
}
|
||||
|
||||
if (broadcast_sink.requested_bis_sync == ESP_BLE_AUDIO_BAP_BIS_SYNC_NO_PREF) {
|
||||
struct base_subgroup_pick pick = {0};
|
||||
uint32_t base_bis;
|
||||
|
||||
/* Get the first BIS index from the BASE */
|
||||
err = esp_ble_audio_bap_base_get_bis_indexes(
|
||||
(esp_ble_audio_bap_base_t *)broadcast_sink.received_base, &base_bis);
|
||||
if (err) {
|
||||
/* Get the BIS indexes offered by the first subgroup of the BASE */
|
||||
err = esp_ble_audio_bap_base_foreach_subgroup(
|
||||
(esp_ble_audio_bap_base_t *)broadcast_sink.received_base,
|
||||
base_pick_subgroup_cb, &pick);
|
||||
if (err || pick.found == false) {
|
||||
ESP_LOGE(TAG, "Failed to get BIS indexes from BASE, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
base_bis = pick.bis_indexes;
|
||||
sync_bitfield = 0;
|
||||
|
||||
/* No Broadcast Assistant told us what to sync to, so take as many BIS as
|
||||
* this sink has streams for: a stereo source advertises one BIS per
|
||||
* channel and syncing to only the first would drop the other channel.
|
||||
*/
|
||||
for (uint8_t i = ESP_BLE_ISO_BIS_INDEX_MIN; i <= ESP_BLE_ISO_BIS_INDEX_MAX; i++) {
|
||||
if (base_bis & ESP_BLE_ISO_BIS_INDEX_BIT(i)) {
|
||||
sync_bitfield = ESP_BLE_ISO_BIS_INDEX_BIT(i);
|
||||
break;
|
||||
sync_bitfield |= ESP_BLE_ISO_BIS_INDEX_BIT(i);
|
||||
|
||||
if (__builtin_popcount(sync_bitfield) >=
|
||||
CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,7 +383,9 @@ static void broadcast_stream_started_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
|
||||
ESP_LOGI(TAG, "[SNK #%u] Stream started", idx);
|
||||
|
||||
example_audio_rx_metrics_reset(&rx_metrics[idx]);
|
||||
if (idx < ARRAY_SIZE(rx_metrics)) {
|
||||
example_audio_rx_metrics_reset(&rx_metrics[idx]);
|
||||
}
|
||||
|
||||
broadcast_sink.active_streams++;
|
||||
flag_clear(FLAG_BROADCAST_SYNCING);
|
||||
@@ -411,6 +469,10 @@ static void broadcast_stream_recv_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
uint8_t idx = broadcast_stream_idx(stream);
|
||||
char obj_name[10];
|
||||
|
||||
if (idx >= ARRAY_SIZE(rx_metrics)) {
|
||||
return;
|
||||
}
|
||||
|
||||
rx_metrics[idx].last_sdu_len = len;
|
||||
snprintf(obj_name, sizeof(obj_name), "SNK #%u", idx);
|
||||
example_audio_rx_metrics_on_recv(info, &rx_metrics[idx], TAG, obj_name);
|
||||
@@ -460,16 +522,64 @@ static void syncable_cb(esp_ble_audio_bap_broadcast_sink_t *sink,
|
||||
}
|
||||
}
|
||||
|
||||
/* CAP 7.3.1.8 / 7.3.1.9: "Start of reception can affect an Acceptor's
|
||||
* availability for unicast Audio Streams. In this case, the Acceptor will
|
||||
* update its Available Audio Contexts characteristic."
|
||||
*
|
||||
* It does affect us: stream_alloc() hands unicast and broadcast the same sink
|
||||
* pool, so while every sink stream carries a BIS there is none left to accept a
|
||||
* unicast Config. Saying so keeps the Initiator's view honest instead of
|
||||
* letting it discover the shortage through a NO_MEM.
|
||||
*
|
||||
* Driven off BIS_Sync rather than off a local "broadcasting" flag because that
|
||||
* is the field the Assistant clears first when it stops our reception - which
|
||||
* puts the restore well ahead of the unicast Config that follows in a
|
||||
* broadcast-to-unicast handover.
|
||||
*/
|
||||
static void sink_availability_update(bool receiving)
|
||||
{
|
||||
static bool reported_unavailable;
|
||||
esp_ble_audio_context_t context;
|
||||
esp_err_t err;
|
||||
|
||||
if (receiving == reported_unavailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
context = receiving ? ESP_BLE_AUDIO_CONTEXT_TYPE_NONE : SINK_CONTEXT;
|
||||
|
||||
err = esp_ble_audio_pacs_set_available_contexts(ESP_BLE_AUDIO_DIR_SINK, context);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to update sink available contexts, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
reported_unavailable = receiving;
|
||||
|
||||
ESP_LOGI(TAG, "Sink available contexts now 0x%04x (%s broadcast)",
|
||||
context, receiving ? "receiving" : "not receiving");
|
||||
}
|
||||
|
||||
static void recv_state_updated_cb(esp_ble_conn_t *conn,
|
||||
const esp_ble_audio_bap_scan_delegator_recv_state_t *recv_state)
|
||||
{
|
||||
bool receiving = false;
|
||||
|
||||
ESP_LOGI(TAG, "Receive state updated, pa_sync 0x%02x encrypt 0x%02x",
|
||||
recv_state->pa_sync_state, recv_state->encrypt_state);
|
||||
|
||||
for (uint8_t i = 0; i < recv_state->num_subgroups; i++) {
|
||||
ESP_LOGI(TAG, "subgroup %d bis_sync 0x%08x", i, recv_state->subgroups[i].bis_sync);
|
||||
|
||||
/* BIS_SYNC_FAILED is non-zero but carries no BIS (BASS 3.1.1.5). */
|
||||
if (recv_state->subgroups[i].bis_sync != 0 &&
|
||||
recv_state->subgroups[i].bis_sync != ESP_BLE_AUDIO_BAP_BIS_SYNC_FAILED) {
|
||||
receiving = true;
|
||||
}
|
||||
}
|
||||
|
||||
sink_availability_update(receiving);
|
||||
|
||||
if (recv_state->pa_sync_state == ESP_BLE_AUDIO_BAP_PA_STATE_SYNCED) {
|
||||
broadcast_sink.recv_state = recv_state;
|
||||
}
|
||||
@@ -516,8 +626,11 @@ static int pa_sync_req_cb(esp_ble_conn_t *conn,
|
||||
|
||||
ESP_LOGI(TAG, "Waiting for PAST...");
|
||||
} else {
|
||||
err = pa_sync_create(recv_state->addr.type, recv_state->addr.a.val,
|
||||
recv_state->adv_sid);
|
||||
uint8_t addr[6];
|
||||
|
||||
addr_order_copy(addr, recv_state->addr.a.val);
|
||||
|
||||
err = pa_sync_create(recv_state->addr.type, addr, recv_state->adv_sid);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
@@ -662,7 +775,7 @@ void broadcast_pa_synced(esp_ble_audio_gap_app_event_t *event)
|
||||
int err;
|
||||
|
||||
addr.type = event->pa_sync.addr.type;
|
||||
memcpy(addr.a.val, event->pa_sync.addr.val, sizeof(addr.a.val));
|
||||
addr_order_copy(addr.a.val, event->pa_sync.addr.val);
|
||||
|
||||
if (broadcast_sink.sync_handle == PA_SYNC_HANDLE_INIT ||
|
||||
(broadcast_sink.recv_state &&
|
||||
|
||||
@@ -25,11 +25,20 @@ static const esp_ble_audio_bap_qos_cfg_pref_t qos_pref =
|
||||
20000, /* Preferred Minimum Presentation Delay (usec) */
|
||||
40000); /* Preferred Maximum Presentation Delay (usec) */
|
||||
|
||||
static example_audio_rx_metrics_t rx_metrics;
|
||||
/* One set of metrics per sink stream: only the sink direction receives, and each
|
||||
* stream has to be counted separately or a stereo setup reports the sum of both
|
||||
* channels against whichever stream happened to cross the reporting threshold.
|
||||
*/
|
||||
static example_audio_rx_metrics_t rx_metrics[ACCEPTOR_SINK_STREAM_COUNT];
|
||||
|
||||
static example_audio_tx_scheduler_t tx_scheduler;
|
||||
static uint16_t tx_seq_num;
|
||||
static uint8_t *iso_data;
|
||||
/* Source stream currently driven by the TX pump. Set when a source stream starts
|
||||
* and cleared when it stops: stream_alloc() hands out *free* streams, so it can
|
||||
* no longer be used to look this one up once it is attached.
|
||||
*/
|
||||
static esp_ble_audio_cap_stream_t *tx_cap_stream;
|
||||
|
||||
static const struct peer_config *s_peer;
|
||||
|
||||
@@ -53,11 +62,15 @@ static const char *stream_dir_str(const esp_ble_audio_bap_stream_t *stream)
|
||||
*/
|
||||
if (stream->ep == NULL) {
|
||||
if (s_peer != NULL) {
|
||||
if (stream == &s_peer->sink_stream.bap_stream) {
|
||||
return "SNK";
|
||||
for (size_t i = 0; i < ARRAY_SIZE(s_peer->sink_streams); i++) {
|
||||
if (stream == &s_peer->sink_streams[i].bap_stream) {
|
||||
return "SNK";
|
||||
}
|
||||
}
|
||||
if (stream == &s_peer->source_stream.bap_stream) {
|
||||
return "SRC";
|
||||
for (size_t i = 0; i < ARRAY_SIZE(s_peer->source_streams); i++) {
|
||||
if (stream == &s_peer->source_streams[i].bap_stream) {
|
||||
return "SRC";
|
||||
}
|
||||
}
|
||||
}
|
||||
return "???";
|
||||
@@ -72,9 +85,26 @@ static const char *stream_dir_str(const esp_ble_audio_bap_stream_t *stream)
|
||||
|
||||
static int stream_index(const esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
/* Only one sink and one source per peer in this example. */
|
||||
(void)stream;
|
||||
return 0;
|
||||
if (s_peer == NULL || stream == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Index within the pool of its own direction, so logs read as "SNK #0" /
|
||||
* "SNK #1" for the two sink streams of a stereo Initiator.
|
||||
*/
|
||||
for (size_t i = 0; i < ARRAY_SIZE(s_peer->sink_streams); i++) {
|
||||
if (stream == &s_peer->sink_streams[i].bap_stream) {
|
||||
return (int)i;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(s_peer->source_streams); i++) {
|
||||
if (stream == &s_peer->source_streams[i].bap_stream) {
|
||||
return (int)i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void tx_scheduler_cb(void *arg)
|
||||
@@ -289,14 +319,20 @@ static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
ESP_LOGI(TAG, "[%s #%d] Stream started",
|
||||
stream_dir_str(stream), stream_index(stream));
|
||||
|
||||
example_audio_rx_metrics_reset(&rx_metrics);
|
||||
|
||||
err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get ep info, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ep_info.dir == ESP_BLE_AUDIO_DIR_SINK) {
|
||||
const int idx = stream_index(stream);
|
||||
|
||||
if (idx >= 0 && idx < (int)ARRAY_SIZE(rx_metrics)) {
|
||||
example_audio_rx_metrics_reset(&rx_metrics[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
if (ep_info.dir == ESP_BLE_AUDIO_DIR_SOURCE) {
|
||||
if (stream->qos == NULL || stream->qos->sdu == 0) {
|
||||
ESP_LOGE(TAG, "Invalid stream qos");
|
||||
@@ -311,12 +347,14 @@ static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
}
|
||||
}
|
||||
|
||||
tx_cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream);
|
||||
tx_seq_num = 0;
|
||||
example_audio_tx_scheduler_reset(&tx_scheduler);
|
||||
|
||||
err = example_audio_tx_scheduler_start(&tx_scheduler, stream->qos->interval);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start tx scheduler, err %d", err);
|
||||
tx_cap_stream = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -356,6 +394,8 @@ static void unicast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_
|
||||
ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err);
|
||||
}
|
||||
|
||||
tx_cap_stream = NULL;
|
||||
|
||||
if (iso_data != NULL) {
|
||||
free(iso_data);
|
||||
iso_data = NULL;
|
||||
@@ -383,6 +423,8 @@ static void unicast_stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, u
|
||||
ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err);
|
||||
}
|
||||
|
||||
tx_cap_stream = NULL;
|
||||
|
||||
if (iso_data != NULL) {
|
||||
free(iso_data);
|
||||
iso_data = NULL;
|
||||
@@ -406,12 +448,17 @@ static void unicast_stream_recv_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
const esp_ble_iso_recv_info_t *info,
|
||||
const uint8_t *data, uint16_t len)
|
||||
{
|
||||
const int idx = stream_index(stream);
|
||||
char name[24];
|
||||
|
||||
snprintf(name, sizeof(name), "%s #%d",
|
||||
stream_dir_str(stream), stream_index(stream));
|
||||
rx_metrics.last_sdu_len = len;
|
||||
example_audio_rx_metrics_on_recv(info, &rx_metrics, TAG, name);
|
||||
/* Only sink streams receive, so the index is always within the sink pool. */
|
||||
if (idx < 0 || idx >= (int)ARRAY_SIZE(rx_metrics)) {
|
||||
return;
|
||||
}
|
||||
|
||||
snprintf(name, sizeof(name), "%s #%d", stream_dir_str(stream), idx);
|
||||
rx_metrics[idx].last_sdu_len = len;
|
||||
example_audio_rx_metrics_on_recv(info, &rx_metrics[idx], TAG, name);
|
||||
}
|
||||
|
||||
static void unicast_stream_sent_cb(esp_ble_audio_bap_stream_t *stream, void *user_data)
|
||||
@@ -444,8 +491,10 @@ static void unicast_server_tx(void)
|
||||
esp_ble_audio_bap_stream_t *bap_stream;
|
||||
esp_err_t err;
|
||||
|
||||
cap_stream = stream_alloc(ESP_BLE_AUDIO_DIR_SOURCE);
|
||||
assert(cap_stream);
|
||||
cap_stream = tx_cap_stream;
|
||||
if (cap_stream == NULL) {
|
||||
return;
|
||||
}
|
||||
bap_stream = &cap_stream->bap_stream;
|
||||
|
||||
if (bap_stream->ep == NULL) {
|
||||
@@ -512,16 +561,20 @@ int cap_acceptor_unicast_init(struct peer_config *peer)
|
||||
cbs_registered = true;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_cap_stream_ops_register(&peer->source_stream, &unicast_stream_ops);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register source stream ops, err %d", err);
|
||||
return -1;
|
||||
for (size_t i = 0; i < ARRAY_SIZE(peer->source_streams); i++) {
|
||||
err = esp_ble_audio_cap_stream_ops_register(&peer->source_streams[i], &unicast_stream_ops);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register source stream ops [%zu], err %d", i, err);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
err = esp_ble_audio_cap_stream_ops_register(&peer->sink_stream, &unicast_stream_ops);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register sink stream ops, err %d", err);
|
||||
return -1;
|
||||
for (size_t i = 0; i < ARRAY_SIZE(peer->sink_streams); i++) {
|
||||
err = esp_ble_audio_cap_stream_ops_register(&peer->sink_streams[i], &unicast_stream_ops);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register sink stream ops [%zu], err %d", i, err);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
err = example_audio_tx_scheduler_init(&tx_scheduler,
|
||||
|
||||
@@ -16,7 +16,11 @@ static uint8_t codec_data[] =
|
||||
ESP_BLE_AUDIO_CODEC_CAP_FREQ_ANY, /* Sampling frequency Any */
|
||||
ESP_BLE_AUDIO_CODEC_CAP_DURATION_7_5 | \
|
||||
ESP_BLE_AUDIO_CODEC_CAP_DURATION_10, /* Frame duration 7.5ms/10ms */
|
||||
ESP_BLE_AUDIO_CODEC_CAP_CHAN_COUNT_SUPPORT(2), /* Supported channels 2 */
|
||||
/* Bitfield, not a maximum: SUPPORT(1, 2) advertises both 1- and
|
||||
* 2-channel configurations. A stereo Initiator may either configure two
|
||||
* 1-channel streams (one per ASE) or a single 2-channel stream.
|
||||
*/
|
||||
ESP_BLE_AUDIO_CODEC_CAP_CHAN_COUNT_SUPPORT(1, 2),
|
||||
30, /* Minimum 30 octets per frame */
|
||||
155, /* Maximum 155 octets per frame */
|
||||
2); /* Maximum 2 codec frames per SDU */
|
||||
@@ -86,12 +90,27 @@ static uint8_t ext_adv_data[] = {
|
||||
|
||||
esp_ble_audio_cap_stream_t *stream_alloc(esp_ble_audio_dir_t dir)
|
||||
{
|
||||
esp_ble_audio_cap_stream_t *pool;
|
||||
size_t count;
|
||||
|
||||
if (dir == ESP_BLE_AUDIO_DIR_SINK) {
|
||||
return &peer.sink_stream;
|
||||
pool = peer.sink_streams;
|
||||
count = ARRAY_SIZE(peer.sink_streams);
|
||||
} else if (dir == ESP_BLE_AUDIO_DIR_SOURCE) {
|
||||
pool = peer.source_streams;
|
||||
count = ARRAY_SIZE(peer.source_streams);
|
||||
} else {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (dir == ESP_BLE_AUDIO_DIR_SOURCE) {
|
||||
return &peer.source_stream;
|
||||
/* A stream is free while no endpoint is attached to it: the stack attaches
|
||||
* the endpoint right after the Config callback returns and detaches it on
|
||||
* release, so the pool needs no separate in-use flag.
|
||||
*/
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
if (pool[i].bap_stream.ep == NULL) {
|
||||
return &pool[i];
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
@@ -99,10 +118,21 @@ esp_ble_audio_cap_stream_t *stream_alloc(esp_ble_audio_dir_t dir)
|
||||
|
||||
void stream_released(const esp_ble_audio_cap_stream_t *cap_stream)
|
||||
{
|
||||
if (cap_stream == &peer.source_stream) {
|
||||
ESP_LOGI(TAG, "Source stream released");
|
||||
} else if (cap_stream == &peer.sink_stream) {
|
||||
ESP_LOGI(TAG, "Sink stream released");
|
||||
/* Releasing detached the endpoint, which already returned the object to the
|
||||
* pool (see stream_alloc), so there is nothing to book-keep here.
|
||||
*/
|
||||
for (size_t i = 0; i < ARRAY_SIZE(peer.sink_streams); i++) {
|
||||
if (cap_stream == &peer.sink_streams[i]) {
|
||||
ESP_LOGI(TAG, "Sink stream #%zu released", i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(peer.source_streams); i++) {
|
||||
if (cap_stream == &peer.source_streams[i]) {
|
||||
ESP_LOGI(TAG, "Source stream #%zu released", i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ CONFIG_BT_GATTC_NOTIF_REG_MAX=20
|
||||
CONFIG_BT_BLE_FEAT_ISO_EN=y
|
||||
CONFIG_BT_BLE_FEAT_PERIODIC_ADV_SYNC_TRANSFER=y
|
||||
|
||||
CONFIG_BT_ISO_MAX_CHAN=2
|
||||
CONFIG_BT_ISO_MAX_CHAN=4
|
||||
|
||||
CONFIG_BT_CAP_ACCEPTOR=y
|
||||
CONFIG_BT_BAP_UNICAST_SERVER=y
|
||||
@@ -33,6 +33,7 @@ CONFIG_BT_AUDIO_CODEC_CFG_MAX_METADATA_SIZE=60
|
||||
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
|
||||
|
||||
CONFIG_EXAMPLE_UNICAST=y
|
||||
CONFIG_EXAMPLE_BROADCAST=y
|
||||
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# The following lines of boilerplate have to be in your project's CMakeLists
|
||||
# in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
|
||||
idf_build_set_property(MINIMAL_BUILD ON)
|
||||
project(cap_handover)
|
||||
243
examples/bluetooth/esp_ble_audio/cap/handover/README.md
Normal file
243
examples/bluetooth/esp_ble_audio/cap/handover/README.md
Normal file
@@ -0,0 +1,243 @@
|
||||
| Supported Targets | ESP32-H4 | ESP32-S31 |
|
||||
| ----------------- | -------- | --------- |
|
||||
|
||||
# CAP Handover Example
|
||||
|
||||
(See the README.md file in the upper level `examples` directory for more information about examples.)
|
||||
|
||||
## Overview
|
||||
|
||||
This example implements a device with a **collocated CAP Initiator and CAP Commander** and moves
|
||||
the audio back and forth between unicast and broadcast using the **CAP Handover procedures**
|
||||
(CAP v1.0.1 §7.3.1.10 Unicast to Broadcast, §7.3.1.11 Broadcast to Unicast).
|
||||
|
||||
It connects to a CAP Acceptor exactly like [`../initiator/`](../initiator/) does — scan, connect,
|
||||
pair, exchange MTU, discover CAS / sink ASEs / source ASEs — and additionally discovers the
|
||||
Acceptor's **Broadcast Audio Scan Service (BASS)**, which the Commander writes into to tell the
|
||||
Acceptor about the broadcast source. Once unicast audio is streaming, a timer alternates between
|
||||
the two procedures every `EXAMPLE_HANDOVER_PERIOD_S`.
|
||||
|
||||
**Only the sink direction is handed over.** A broadcast Audio Stream is one-directional, so the
|
||||
return (Acceptor to Initiator) stream is not part of the procedure: §7.3.1.10 applies to "all CISes
|
||||
within the CIG that is carrying an Audio Stream from the Initiator to the Acceptor", and leaves the
|
||||
reverse direction implementation specific.
|
||||
|
||||
The very same `bt_cap_stream` objects carry the audio before and after a handover — that is a
|
||||
requirement of the CAP handover API, not a shortcut: the procedure validates that every broadcast
|
||||
stream is one of the unicast group's streaming sink streams, and that *all* of them are handed over.
|
||||
Consequently there is **no standalone broadcast source** in this example: it is created by the
|
||||
unicast-to-broadcast procedure and deleted by the reverse one.
|
||||
|
||||
## Requirements
|
||||
|
||||
* A board with BLE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31)
|
||||
* A second board running [`../acceptor/`](../acceptor/) with **both** `EXAMPLE_UNICAST` and
|
||||
`EXAMPLE_BROADCAST` enabled (its `sdkconfig.defaults` already does), so it exposes ASCS and BASS
|
||||
at the same time
|
||||
* The Acceptor must **not** be built with `EXAMPLE_SCAN_SELF` (its Kconfig already makes that
|
||||
mutually exclusive with `EXAMPLE_UNICAST`) — see *Periodic advertising sync* below
|
||||
|
||||
## Periodic advertising sync: PAST only
|
||||
|
||||
This example is a **collocated broadcaster**: the same device is the Broadcast Source and the
|
||||
Commander that tells the Acceptor about it. It therefore never scans for its own broadcast, and it
|
||||
hands the periodic advertising train over with **PAST** rather than asking the Acceptor to find it.
|
||||
|
||||
* The Add Source operation is written with `PA_Sync = 0x01` (*synchronize, PAST available*).
|
||||
* The Acceptor answers by putting its Broadcast Receive State into `PA_Sync_State = 1`
|
||||
(*SyncInfo Request*).
|
||||
* Seeing that state, this example sends **LE Periodic Advertising Set Info Transfer** — the
|
||||
variant for handing over one of *our own* advertising sets. (The other variant, Sync Transfer,
|
||||
applies when an Assistant relays a sync it holds to a third-party broadcaster; that is not this
|
||||
case.) The Source ID goes in the **high octet** of the service data, which is where the Acceptor
|
||||
reads it from.
|
||||
|
||||
There is deliberately **no self-scan fallback**: a real earbud is not asked to scan for a source
|
||||
its phone already knows about. If the Acceptor were built to self-scan instead, it would sit at
|
||||
"Waiting for BASE" forever.
|
||||
|
||||
## Configuration
|
||||
|
||||
```bash
|
||||
idf.py menuconfig
|
||||
```
|
||||
|
||||
Under **Example: CAP Handover**:
|
||||
|
||||
* `EXAMPLE_HANDOVER_PERIOD_S` (default 300) — seconds between switches. Set to 0 to stay on
|
||||
unicast after the initial setup.
|
||||
|
||||
> **Size note.** The Broadcast Assistant (BASS *client*) is genuinely used — it is how the
|
||||
> Commander writes Add Source / Modify Source / Remove Source. The Scan Delegator (BASS
|
||||
> *server*) is **not**: it is pulled in only because every branch of `BT_CAP_COMMANDER`'s
|
||||
> `depends on` that avoids it requires some other client role we do not need either
|
||||
> (`BT_VCP_VOL_CTLR`, `BT_MICP_MIC_CTLR`, `BT_TBS_CLIENT`, `BT_MCC`). CAP §7.3.1.8 only requires
|
||||
> a Commander to act as Scan Delegator when it is **not collocated** with the Initiator **and**
|
||||
> the stream is encrypted — neither holds here — so this is an over-constraint in the Kconfig
|
||||
> rather than a real dependency.
|
||||
|
||||
### Security & Pairing
|
||||
|
||||
Just-Works pairing (LE Secure Connections, no MITM) with bonding, inherited from
|
||||
`../../common_components/example_init/ble_audio_example_init.c`.
|
||||
|
||||
## Build & Flash
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32h4
|
||||
idf.py -p PORT flash monitor
|
||||
```
|
||||
|
||||
For the **NimBLE** host, layer the overlay:
|
||||
|
||||
```bash
|
||||
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor
|
||||
```
|
||||
|
||||
(Exit serial monitor with `Ctrl-]`.)
|
||||
|
||||
## Example Flow
|
||||
|
||||
1. `app_main` brings up NVS, the controller, the LE Audio common layer, the unicast role, the
|
||||
handover module and the TX pump, then starts scanning.
|
||||
2. On finding CAS in a connectable advertisement: connect, pair, exchange MTU, GATT discovery.
|
||||
3. Discover CAS, then the sink and source ASEs, then **BASS**.
|
||||
4. **Sweep the Acceptor's Broadcast Receive States.** Discovery only subscribes to them, so the
|
||||
example reads each one once and removes any left over from an earlier run of ours (matched by
|
||||
Broadcast ID). Receive states live on the Acceptor and survive a reflash of *this* board, and
|
||||
BAP §6.5.4 forbids an Add Source that would duplicate the
|
||||
{address, SID, Broadcast ID} triple — without the sweep the first handover after a reboot
|
||||
fails with `0xFC` (Write Request Rejected). The sweep then continues into
|
||||
`cap_handover_unicast_setup_and_start()`.
|
||||
5. A unicast group is created with one CIS per sink stream (the return direction shares the first
|
||||
CIS) and the streams start.
|
||||
6. **Unicast to broadcast**, every `EXAMPLE_HANDOVER_PERIOD_S`:
|
||||
* the advertising set is brought up (extended advertising started, periodic advertising
|
||||
configured but *stopped* — the BASE does not exist yet);
|
||||
* the local public address and SID are registered with the audio stack, because the BASS Add
|
||||
Source operation carries them;
|
||||
* `esp_ble_audio_cap_handover_unicast_to_broadcast()` stops and releases the unicast group,
|
||||
creates the broadcast source, and has the Commander write Add Source to the Acceptor;
|
||||
* from the `created` callback the BASE is encoded and periodic advertising is started.
|
||||
7. **Broadcast to unicast**, one period later: the Commander stops the reception on the Acceptor
|
||||
(using the Source ID learned from the Broadcast Receive State), the broadcast source is stopped
|
||||
and deleted, a new unicast group is created and the streams start again; advertising is stopped.
|
||||
8. On ACL disconnect any broadcast source left over is deleted and scanning resumes.
|
||||
|
||||
## Expected Log
|
||||
|
||||
TAG: `CAP_HOV`.
|
||||
|
||||
```
|
||||
I (xxx) CAP_HOV: CAP initiator unicast initialized
|
||||
I (xxx) CAP_HOV: CAP initiator handover initialized (period 60 s)
|
||||
I (xxx) CAP_HOV: Scanning for CAP Acceptor...
|
||||
I (xxx) CAP_HOV: Found CAS in peer adv data!
|
||||
I (xxx) CAP_HOV: Connected: handle ... role ... peer ...
|
||||
I (xxx) CAP_HOV: Discover sources complete
|
||||
I (xxx) CAP_HOV: Discovering BASS
|
||||
I (xxx) CAP_HOV: BASS discovered (1 receive state(s))
|
||||
I (xxx) CAP_HOV: Created unicast group
|
||||
I (xxx) CAP_HOV: [SNK #0] Stream started
|
||||
I (xxx) CAP_HOV: [SNK #1] Stream started
|
||||
I (xxx) CAP_HOV: Unicast start completed
|
||||
|
||||
I (xxx) CAP_HOV: Handover: unicast -> broadcast (2 stream(s))
|
||||
I (xxx) CAP_HOV: Advertising started, BASE pending (handle 0)
|
||||
I (xxx) CAP_HOV: Periodic advertising started (handle 0)
|
||||
I (xxx) CAP_HOV: Acceptor receive state: src_id 1 pa_sync 2
|
||||
I (xxx) CAP_HOV: Handover to broadcast completed
|
||||
|
||||
I (xxx) CAP_HOV: Handover: broadcast -> unicast (2 stream(s))
|
||||
I (xxx) CAP_HOV: Advertising stopped (handle 0)
|
||||
I (xxx) CAP_HOV: Handover to unicast completed
|
||||
```
|
||||
|
||||
Note the TX label follows the transport: `[SNK #0]` while the stream runs on a CIS, `[SRC #0]`
|
||||
once the same stream object is bound to a broadcast source endpoint. Seeing `[SRC #0] TX: 6000
|
||||
packets` during a broadcast period is the check that audio really is going out over the BIS.
|
||||
|
||||
On the acceptor side each switch shows the sink ASEs being released and a BASS Add Source arriving,
|
||||
then the reverse.
|
||||
|
||||
### Timing
|
||||
|
||||
Each procedure is strictly sequential, so there is a silent gap at every switch. Measured on
|
||||
ESP32-S31 with two sink streams, it is stable to within a few milliseconds:
|
||||
|
||||
| Direction | Duration |
|
||||
|---|---|
|
||||
| unicast to broadcast | ~1.69 s |
|
||||
| broadcast to unicast | ~2.65 s |
|
||||
|
||||
## Diagnostics
|
||||
|
||||
Either side of every handover the example samples the ISO timing of each stream and logs it under
|
||||
`[<phase>]`, where phase is `u2b-pre`, `u2b-post`, `b2u-pre` or `b2u-post`:
|
||||
|
||||
```
|
||||
I (xxx) CAP_HOV: [u2b-pre][0] state 4 ts 66125612 us offset 0 us seq 6025
|
||||
I (xxx) CAP_HOV: [u2b-pre][0] iso_interval 10000 us cig_sync_delay 2256 us cis_sync_delay 2256 us
|
||||
I (xxx) CAP_HOV: [u2b-pre][0] c2p {ft 1 bn 1 latency 2256 us sdu_interval unknown (v1 event)}
|
||||
I (xxx) CAP_HOV: [u2b-pre][0] p2c {ft 1 bn 0 latency 0 us sdu_interval unknown (v1 event)}
|
||||
I (xxx) CAP_HOV: [u2b-pre][0] pd_pref [20000, 40000] us pd_range [20000, 40000] us
|
||||
```
|
||||
|
||||
A BIS reports the same first line plus its own terms, and no QoS preference:
|
||||
|
||||
```
|
||||
I (xxx) CAP_HOV: [u2b-post][0] state 4 (timestamp not read: transport just started)
|
||||
I (xxx) CAP_HOV: [u2b-post][0] iso_interval 10000 us big_sync_delay 1974 us
|
||||
I (xxx) CAP_HOV: [u2b-post][0] bis {pto 0 bn 1 latency 1974 us}
|
||||
```
|
||||
|
||||
These are the terms CAP §7.3.1.10's rendering-point alignment would need. Reading them:
|
||||
|
||||
* `ts` is `TX_Time_Stamp`: the CIG reference point or BIG anchor point, taken from the
|
||||
controller's free-running clock. Both transports use the same clock, so the two can be
|
||||
subtracted directly.
|
||||
* The **post** samples deliberately do not read `ts`. HCI answers *Command Disallowed* until a
|
||||
stream has sent its first SDU, and a transport that has just come up has not; the timestamp for
|
||||
the new transport shows up in the next **pre** sample instead.
|
||||
* `sdu_interval` is reported as unknown because it is only carried by
|
||||
`HCI_LE_CIS_Established_V2`; the controller sends the v1 event. Use the QoS value instead.
|
||||
* `latency` is the transport latency the controller actually achieved, not the maximum the preset
|
||||
asked for.
|
||||
* `ft` is derived: the host stores `flush_timeout = FT x ISO_Interval`, so it is divided back out
|
||||
here — the alignment arithmetic wants FT itself.
|
||||
* Only a CIS prints `pd_pref` / `pd_range`. Those come from the peer ASE's Codec Configured
|
||||
notification, and a broadcast source has no peer to state a preference, so the fields would
|
||||
only ever read zero.
|
||||
|
||||
## Peer Pairing
|
||||
|
||||
1. Flash [`../acceptor/`](../acceptor/) on one board with both roles enabled (default).
|
||||
2. Flash this example on the other board.
|
||||
3. Audio starts on CISes, then alternates with BISes every period.
|
||||
|
||||
## Notes
|
||||
|
||||
* The handover procedures in this stack are **sequential**: the unicast streams are stopped before
|
||||
the broadcast source is created (CAP allows this as the "Initiator does not have the resources to
|
||||
concurrently run both" path). There is therefore an audible gap at each switch. CAP §7.3.1.10 also
|
||||
recommends aligning the rendering points of the two streams, which only makes sense on the
|
||||
concurrent path and is not implemented.
|
||||
* `Streaming_Audio_Contexts` and `CCID_List` are required by the specification to be the same on
|
||||
both sides of a handover. So is the **codec configuration**: a different sampling frequency,
|
||||
frame duration or frame size would make the Acceptor's decoder reconfigure exactly at the
|
||||
switching point. Both paths therefore take their configuration from one
|
||||
`HANDOVER_LC3_PRESET_DEFINE()` in `cap_handover.h`, and `handover_audio_config_check()` rejects
|
||||
the handover if they ever diverge. Only the channel allocation differs by design: unicast puts
|
||||
one channel in each stream, broadcast puts it in a per-BIS LTV.
|
||||
* The **Source ID is assigned by the Acceptor**, not chosen here — the Add Source operation has no
|
||||
such field. It is learned from the Broadcast Receive State and then used for Modify/Remove
|
||||
Source. It keeps counting across a reflash of this board and only restarts when the *Acceptor*
|
||||
reboots, which is why the receive-state sweep in step 4 exists.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause |
|
||||
|---|---|
|
||||
| First handover after reflashing this board fails with `err 252` | A receive state left on the Acceptor by the previous run. The sweep in step 4 clears it; if it was removed, this is what comes back. |
|
||||
| Acceptor logs "Syncing without PAST", then waits for the BASE forever | The Add Source went out with `PA_Sync = 0x02`. Check that the advertising set is registered with `esp_ble_audio_bap_broadcast_adv_add()` before the handover — its address and SID are what make PAST available. |
|
||||
| Broadcast period is silent, no `[SRC #x] TX:` line | The TX pump did not register for the broadcast streams. It decides with `bt_bap_ep_info.can_send`; deriving it from `dir` instead does not work, because a broadcast source endpoint reports `dir = SOURCE` while a unicast client endpoint reports the peer's direction. |
|
||||
@@ -0,0 +1,18 @@
|
||||
set(srcs "main.c"
|
||||
"cap_handover_proc.c"
|
||||
"cap_handover_unicast.c"
|
||||
"cap_handover_broadcast.c"
|
||||
"cap_handover_tx.c"
|
||||
"cap_handover_diag.c")
|
||||
|
||||
if(CONFIG_BT_BLUEDROID_ENABLED)
|
||||
list(APPEND srcs "bluedroid/central.c"
|
||||
"bluedroid/adv.c")
|
||||
else()
|
||||
list(APPEND srcs "nimble/central.c"
|
||||
"nimble/adv.c")
|
||||
endif()
|
||||
|
||||
idf_component_register(SRCS ${srcs}
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES bt nvs_flash)
|
||||
@@ -0,0 +1,14 @@
|
||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
menu "Example: CAP Handover"
|
||||
|
||||
config EXAMPLE_HANDOVER_PERIOD_S
|
||||
int "Seconds between automatic handovers (0 to disable)"
|
||||
default 300
|
||||
help
|
||||
Once unicast audio is streaming the sample alternates between unicast
|
||||
and broadcast every this many seconds. Set to 0 to stay on unicast
|
||||
after the initial setup.
|
||||
|
||||
endmenu
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* 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_bt_device.h"
|
||||
#include "esp_gap_ble_api.h"
|
||||
|
||||
#include "adv.h"
|
||||
#include "cap_handover.h"
|
||||
|
||||
static SemaphoreHandle_t adv_sem;
|
||||
|
||||
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 },
|
||||
};
|
||||
|
||||
void adv_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_EXT_ADV_STOP_COMPLETE_EVT:
|
||||
adv_op_status = param->ext_adv_stop.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;
|
||||
case ESP_GAP_BLE_PERIODIC_ADV_STOP_COMPLETE_EVT:
|
||||
adv_op_status = param->period_adv_stop.status;
|
||||
xSemaphoreGive(adv_sem);
|
||||
break;
|
||||
case ESP_GAP_BLE_PERIODIC_ADV_SET_INFO_TRANS_COMPLETE_EVT:
|
||||
/* Not awaited: the Acceptor reports the result through its receive state. */
|
||||
if (param->period_adv_set_info_trans.status != ESP_BT_STATUS_SUCCESS) {
|
||||
ESP_LOGE(TAG, "PAST failed, status %d", param->period_adv_set_info_trans.status);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int adv_host_init(void)
|
||||
{
|
||||
adv_sem = xSemaphoreCreateBinary();
|
||||
if (adv_sem == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to create adv semaphore");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int adv_set_configure(const uint8_t *ext_data, uint8_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_periodic_adv_set_params(ADV_HANDLE, &periodic_adv_params));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int per_adv_data_start(const uint8_t *per_data, uint8_t per_len)
|
||||
{
|
||||
#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
|
||||
|
||||
ESP_LOGI(TAG, "Periodic advertising started (handle %u)", ADV_HANDLE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int ext_adv_start_without_base(const uint8_t *ext_data, uint8_t ext_len)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = adv_set_configure(ext_data, ext_len);
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
|
||||
WAIT_API(esp_ble_gap_ext_adv_start(1, ext_adv_inst));
|
||||
|
||||
ESP_LOGI(TAG, "Advertising started, BASE pending (handle %u)", ADV_HANDLE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int adv_stop(void)
|
||||
{
|
||||
uint8_t instance = ADV_HANDLE;
|
||||
|
||||
WAIT_API(esp_ble_gap_periodic_adv_stop(ADV_HANDLE));
|
||||
WAIT_API(esp_ble_gap_ext_adv_stop(1, &instance));
|
||||
|
||||
ESP_LOGI(TAG, "Advertising stopped (handle %u)", ADV_HANDLE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int local_public_addr_get(uint8_t addr[6])
|
||||
{
|
||||
const uint8_t *bda = esp_bt_dev_get_address();
|
||||
|
||||
if (bda == NULL) {
|
||||
ESP_LOGE(TAG, "Local BD address unavailable");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Bluedroid hands out addresses MSB-first; the caller feeds this to the audio
|
||||
* stack, which stores a bt_addr_le_t and puts it on air LSB-first.
|
||||
*/
|
||||
for (size_t i = 0; i < 6; i++) {
|
||||
addr[i] = bda[5 - i];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int pa_set_info_transfer(uint16_t conn_handle, const uint8_t peer_addr[6],
|
||||
uint16_t service_data)
|
||||
{
|
||||
esp_bd_addr_t addr;
|
||||
|
||||
(void)conn_handle;
|
||||
memcpy(addr, peer_addr, sizeof(addr));
|
||||
|
||||
return esp_ble_gap_periodic_adv_set_info_trans(addr, service_data, ADV_HANDLE);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esp_gap_ble_api.h"
|
||||
|
||||
void adv_gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
|
||||
|
||||
int adv_host_init(void);
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* 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 "adv.h"
|
||||
#include "cap_handover.h"
|
||||
|
||||
static SemaphoreHandle_t scan_sem;
|
||||
|
||||
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_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;
|
||||
|
||||
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:
|
||||
/* Bluedroid has one GAP callback slot; the advertising events belong
|
||||
* to the other wrapper. The two event sets are disjoint. */
|
||||
adv_gap_event_handler(event, param);
|
||||
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 = adv_host_init();
|
||||
if (err) {
|
||||
vSemaphoreDelete(scan_sem);
|
||||
return err;
|
||||
}
|
||||
|
||||
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 CAP Acceptor...");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ext_scan_stop(void)
|
||||
{
|
||||
WAIT_API(esp_ble_gap_stop_ext_scan());
|
||||
return 0;
|
||||
}
|
||||
|
||||
int conn_create(uint8_t addr_type, const uint8_t addr[6])
|
||||
{
|
||||
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 — events route back to the engine
|
||||
* which forwards them to the lib. Registering a separate example GATTC
|
||||
* app would steer events to a handler the engine never sees.
|
||||
*
|
||||
* 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;
|
||||
|
||||
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,150 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#include "esp_ble_audio_lc3_defs.h"
|
||||
#include "esp_ble_audio_bap_api.h"
|
||||
#include "esp_ble_audio_cap_api.h"
|
||||
#include "esp_ble_audio_pacs_api.h"
|
||||
#include "esp_ble_audio_bap_lc3_preset_defs.h"
|
||||
|
||||
#include "ble_audio_example_init.h"
|
||||
#include "ble_audio_example_utils.h"
|
||||
|
||||
#define TAG "CAP_HOV"
|
||||
|
||||
#define CONN_HANDLE_INIT 0xFFFF
|
||||
|
||||
#define SINK_STREAM_COUNT MIN(CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT, \
|
||||
CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT)
|
||||
|
||||
#define LOCAL_DEVICE_NAME "CAP Handover"
|
||||
|
||||
#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 24 /* 30ms */
|
||||
#define CONN_LATENCY 0
|
||||
#define CONN_TIMEOUT 500 /* 5s */
|
||||
#define CONN_MIN_CE_LEN 0xFFFF
|
||||
#define CONN_MAX_CE_LEN 0xFFFF
|
||||
|
||||
#define ADV_HANDLE 0
|
||||
#define ADV_SID 0
|
||||
#define ADV_TX_POWER 127
|
||||
#define ADV_INTERVAL_MS 200
|
||||
#define PER_ADV_INTERVAL_MS 100
|
||||
|
||||
#define LOCAL_BROADCAST_ID 0x123456
|
||||
|
||||
#define HANDOVER_LC3_PRESET_DEFINE(_name, _loc) \
|
||||
ESP_BLE_AUDIO_BAP_LC3_UNICAST_PRESET_16_2_1_DEFINE( \
|
||||
_name, _loc, ESP_BLE_AUDIO_CONTEXT_TYPE_UNSPECIFIED)
|
||||
|
||||
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);
|
||||
|
||||
void security_failed_recover(uint16_t conn_handle, uint8_t status);
|
||||
|
||||
int ext_adv_start_without_base(const uint8_t *ext_data, uint8_t ext_len);
|
||||
|
||||
int per_adv_data_start(const uint8_t *per_data, uint8_t per_len);
|
||||
|
||||
int adv_stop(void);
|
||||
|
||||
int local_public_addr_get(uint8_t addr[6]);
|
||||
|
||||
int pa_set_info_transfer(uint16_t conn_handle, const uint8_t peer_addr[6],
|
||||
uint16_t service_data);
|
||||
|
||||
struct tx_stream {
|
||||
esp_ble_audio_cap_stream_t *stream;
|
||||
uint16_t seq_num;
|
||||
uint8_t *data;
|
||||
example_audio_tx_scheduler_t scheduler;
|
||||
bool is_broadcast;
|
||||
};
|
||||
|
||||
struct peer_config {
|
||||
esp_ble_audio_cap_stream_t sink_streams[SINK_STREAM_COUNT];
|
||||
esp_ble_audio_bap_ep_t *sink_eps[SINK_STREAM_COUNT];
|
||||
size_t sink_ep_count;
|
||||
|
||||
esp_ble_conn_t *conn;
|
||||
uint16_t conn_handle;
|
||||
uint8_t dst[6];
|
||||
|
||||
bool disc_completed;
|
||||
bool mtu_exchanged;
|
||||
};
|
||||
|
||||
extern struct peer_config peer;
|
||||
|
||||
esp_ble_audio_cap_unicast_group_t *unicast_group_get(void);
|
||||
|
||||
void unicast_group_set(esp_ble_audio_cap_unicast_group_t *group);
|
||||
|
||||
int unicast_group_delete(void);
|
||||
|
||||
esp_ble_audio_bap_lc3_preset_t *sink_preset_get(size_t idx);
|
||||
|
||||
int cap_handover_unicast_setup_and_start(void);
|
||||
|
||||
void cap_handover_unicast_gap_cb(esp_ble_audio_gap_app_event_t *event);
|
||||
|
||||
void cap_handover_unicast_gatt_cb(esp_ble_audio_gatt_app_event_t *event);
|
||||
|
||||
int cap_handover_unicast_start(void);
|
||||
|
||||
int cap_handover_unicast_init(void);
|
||||
|
||||
uint8_t *broadcast_ext_adv_data_get(uint8_t *data_len);
|
||||
|
||||
uint8_t *broadcast_base_data_get(esp_ble_audio_cap_broadcast_source_t *source,
|
||||
uint8_t *data_len);
|
||||
|
||||
esp_ble_audio_bap_lc3_preset_t *broadcast_preset_get(void);
|
||||
|
||||
esp_ble_audio_location_t broadcast_bis_location_get(size_t idx);
|
||||
|
||||
int cap_handover_proc_init(void);
|
||||
|
||||
int cap_handover_proc_discover(uint16_t conn_handle);
|
||||
|
||||
void cap_handover_proc_unicast_started(void);
|
||||
|
||||
void cap_handover_proc_broadcast_stopped(esp_ble_audio_cap_broadcast_source_t *source);
|
||||
|
||||
void cap_handover_diag_sample(const char *phase, bool has_sent);
|
||||
|
||||
void cap_handover_proc_reset(void);
|
||||
|
||||
void cap_handover_tx_stream_sent(esp_ble_audio_bap_stream_t *stream, void *user_data);
|
||||
|
||||
int cap_handover_tx_register_stream(esp_ble_audio_cap_stream_t *cap_stream, bool is_broadcast);
|
||||
|
||||
int cap_handover_tx_unregister_stream(esp_ble_audio_cap_stream_t *cap_stream);
|
||||
|
||||
void cap_handover_tx_init(void);
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "cap_handover.h"
|
||||
|
||||
/* Shared subgroup configuration; the per-BIS channel allocation goes in a
|
||||
* separate LTV, so the location here stays mono.
|
||||
*/
|
||||
HANDOVER_LC3_PRESET_DEFINE(broadcast_preset, ESP_BLE_AUDIO_LOCATION_MONO_AUDIO);
|
||||
|
||||
/* One BIS per channel; the handover module turns these into per-BIS LTVs. */
|
||||
static const esp_ble_audio_location_t bis_locations[] = {
|
||||
ESP_BLE_AUDIO_LOCATION_FRONT_LEFT,
|
||||
ESP_BLE_AUDIO_LOCATION_FRONT_RIGHT,
|
||||
};
|
||||
|
||||
_Static_assert(ARRAY_SIZE(bis_locations) >= SINK_STREAM_COUNT,
|
||||
"Need one channel allocation per broadcast stream");
|
||||
|
||||
uint8_t *broadcast_ext_adv_data_get(uint8_t *data_len)
|
||||
{
|
||||
uint32_t broadcast_id;
|
||||
uint8_t *data;
|
||||
|
||||
broadcast_id = LOCAL_BROADCAST_ID;
|
||||
|
||||
/* - Broadcast Audio Announcement Service UUID (2 octets)
|
||||
* - Broadcast ID (3 octets)
|
||||
* - Complete Device Name
|
||||
*/
|
||||
*data_len = 7 + 2 + strlen(LOCAL_DEVICE_NAME);
|
||||
|
||||
data = calloc(1, *data_len);
|
||||
if (data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to alloc ext adv data (%u octets)", *data_len);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
data[0] = 0x06; /* 1 + 2 + 3 */
|
||||
data[1] = EXAMPLE_AD_TYPE_SERVICE_DATA16;
|
||||
data[2] = (ESP_BLE_AUDIO_UUID_BROADCAST_AUDIO_VAL & 0xFF);
|
||||
data[3] = ((ESP_BLE_AUDIO_UUID_BROADCAST_AUDIO_VAL >> 8) & 0xFF);
|
||||
data[4] = (broadcast_id & 0xFF);
|
||||
data[5] = ((broadcast_id >> 8) & 0xFF);
|
||||
data[6] = ((broadcast_id >> 16) & 0xFF);
|
||||
|
||||
data[7] = strlen(LOCAL_DEVICE_NAME) + 1;
|
||||
data[8] = EXAMPLE_AD_TYPE_NAME_COMPLETE;
|
||||
memcpy(data + 9, LOCAL_DEVICE_NAME, strlen(LOCAL_DEVICE_NAME));
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
uint8_t *broadcast_base_data_get(esp_ble_audio_cap_broadcast_source_t *source,
|
||||
uint8_t *data_len)
|
||||
{
|
||||
NET_BUF_SIMPLE_DEFINE(base_buf, 128);
|
||||
uint8_t *data;
|
||||
esp_err_t err;
|
||||
|
||||
/* Broadcast Audio Announcement Service UUID (2 octets) and
|
||||
* Broadcast Audio Source Endpoint (BASE)
|
||||
*/
|
||||
|
||||
err = esp_ble_audio_cap_initiator_broadcast_get_base(source, &base_buf);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get encoded BASE, err %d", err);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
*data_len = 2 + base_buf.len;
|
||||
|
||||
data = calloc(1, *data_len);
|
||||
if (data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to alloc per adv data (%u octets)", *data_len);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* base_buf.len has included the UUID length (2 octets) */
|
||||
data[0] = 1 + base_buf.len;
|
||||
data[1] = EXAMPLE_AD_TYPE_SERVICE_DATA16;
|
||||
memcpy(data + 2, base_buf.data, base_buf.len);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
esp_ble_audio_bap_lc3_preset_t *broadcast_preset_get(void)
|
||||
{
|
||||
return &broadcast_preset;
|
||||
}
|
||||
|
||||
esp_ble_audio_location_t broadcast_bis_location_get(size_t idx)
|
||||
{
|
||||
return (idx < ARRAY_SIZE(bis_locations)) ? bis_locations[idx]
|
||||
: ESP_BLE_AUDIO_LOCATION_MONO_AUDIO;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "esp_ble_iso_common_api.h"
|
||||
|
||||
#include "cap_handover.h"
|
||||
|
||||
/* flush_timeout holds FT x ISO_Interval, so divide FT back out. sdu_interval
|
||||
* only comes with HCI_LE_CIS_Established_V2; a v1 event leaves it UNKNOWN.
|
||||
*/
|
||||
static void diag_log_cis_dir(const char *phase, size_t idx, const char *dir,
|
||||
uint16_t iso_interval,
|
||||
const struct bt_iso_unicast_tx_info *info)
|
||||
{
|
||||
ESP_LOGI(TAG, "[%s][%zu] %s {ft %u bn %u latency %lu us sdu_interval %s}",
|
||||
phase, idx, dir,
|
||||
iso_interval != 0 ? (unsigned)(info->flush_timeout / iso_interval) : 0U,
|
||||
info->bn, (unsigned long)info->latency,
|
||||
info->sdu_interval == BT_ISO_SDU_INTERVAL_UNKNOWN ? "unknown (v1 event)"
|
||||
: "see qos");
|
||||
}
|
||||
|
||||
static void diag_sample_stream(const char *phase, size_t idx,
|
||||
esp_ble_audio_cap_stream_t *stream, bool has_sent)
|
||||
{
|
||||
esp_ble_audio_bap_ep_info_t ep_info = {0};
|
||||
esp_ble_iso_tx_info_t tx_info = {0};
|
||||
esp_ble_iso_info_t chan_info = {0};
|
||||
esp_ble_audio_bap_ep_t *ep = stream->bap_stream.ep;
|
||||
esp_err_t err;
|
||||
|
||||
if (ep == NULL) {
|
||||
ESP_LOGI(TAG, "[%s][%zu] no endpoint", phase, idx);
|
||||
return;
|
||||
}
|
||||
|
||||
if (esp_ble_audio_bap_ep_get_info(ep, &ep_info) != ESP_OK) {
|
||||
ESP_LOGW(TAG, "[%s][%zu] endpoint info unavailable", phase, idx);
|
||||
return;
|
||||
}
|
||||
|
||||
/* HCI answers Command Disallowed until the stream has sent an SDU, and the
|
||||
* adapter logs that as an HCI error - so do not ask a transport that just
|
||||
* came up. Its timestamp shows up in the next pre-handover sample.
|
||||
*/
|
||||
if (!has_sent) {
|
||||
ESP_LOGI(TAG, "[%s][%zu] state %d (timestamp not read: transport just started)",
|
||||
phase, idx, ep_info.state);
|
||||
} else if ((err = esp_ble_audio_cap_stream_get_tx_sync(stream, &tx_info)) != ESP_OK) {
|
||||
ESP_LOGW(TAG, "[%s][%zu] state %d tx_sync failed, err %d",
|
||||
phase, idx, ep_info.state, err);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "[%s][%zu] state %d ts %lu us offset %lu us seq %u",
|
||||
phase, idx, ep_info.state,
|
||||
(unsigned long)tx_info.ts, (unsigned long)tx_info.offset,
|
||||
tx_info.seq_num);
|
||||
}
|
||||
|
||||
if (ep_info.iso_chan == NULL ||
|
||||
esp_ble_iso_chan_get_info(ep_info.iso_chan, &chan_info) != ESP_OK) {
|
||||
ESP_LOGI(TAG, "[%s][%zu] no ISO channel info", phase, idx);
|
||||
return;
|
||||
}
|
||||
|
||||
/* iso_interval is in 1.25 ms units; print us like every other duration. The
|
||||
* channel kind shows in the field names, so it is not printed separately.
|
||||
*/
|
||||
if (chan_info.type == BT_ISO_CHAN_TYPE_BROADCASTER) {
|
||||
ESP_LOGI(TAG, "[%s][%zu] iso_interval %lu us big_sync_delay %lu us",
|
||||
phase, idx,
|
||||
(unsigned long)chan_info.iso_interval * 1250UL,
|
||||
(unsigned long)chan_info.broadcaster.sync_delay);
|
||||
ESP_LOGI(TAG, "[%s][%zu] bis {pto %lu bn %u latency %lu us}",
|
||||
phase, idx,
|
||||
(unsigned long)chan_info.broadcaster.pto,
|
||||
chan_info.broadcaster.bn,
|
||||
(unsigned long)chan_info.broadcaster.latency);
|
||||
|
||||
/* No qos_pref: it comes from a peer ASE's Codec Configured, and a
|
||||
* broadcast source has no peer. Printing four zeros would mislead.
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "[%s][%zu] iso_interval %lu us cig_sync_delay %lu us cis_sync_delay %lu us",
|
||||
phase, idx,
|
||||
(unsigned long)chan_info.iso_interval * 1250UL,
|
||||
(unsigned long)chan_info.unicast.cig_sync_delay,
|
||||
(unsigned long)chan_info.unicast.cis_sync_delay);
|
||||
|
||||
diag_log_cis_dir(phase, idx, "c2p", chan_info.iso_interval, &chan_info.unicast.central);
|
||||
diag_log_cis_dir(phase, idx, "p2c", chan_info.iso_interval, &chan_info.unicast.peripheral);
|
||||
|
||||
if (ep_info.qos_pref != NULL) {
|
||||
ESP_LOGI(TAG, "[%s][%zu] pd_pref [%lu, %lu] us pd_range [%lu, %lu] us",
|
||||
phase, idx,
|
||||
(unsigned long)ep_info.qos_pref->pref_pd_min,
|
||||
(unsigned long)ep_info.qos_pref->pref_pd_max,
|
||||
(unsigned long)ep_info.qos_pref->pd_min,
|
||||
(unsigned long)ep_info.qos_pref->pd_max);
|
||||
}
|
||||
}
|
||||
|
||||
void cap_handover_diag_sample(const char *phase, bool has_sent)
|
||||
{
|
||||
for (size_t i = 0; i < peer.sink_ep_count && i < ARRAY_SIZE(peer.sink_streams); i++) {
|
||||
diag_sample_stream(phase, i, &peer.sink_streams[i], has_sent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,725 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "esp_timer.h"
|
||||
|
||||
#include "cap_handover.h"
|
||||
|
||||
/* 1.25 ms units. The real interval, not "unknown": the Acceptor's PA sync needs it. */
|
||||
#define HANDOVER_PA_INTERVAL ((PER_ADV_INTERVAL_MS * 1000) / 1250)
|
||||
|
||||
/* Must stay valid until the procedure completes, so not on the stack. */
|
||||
static esp_ble_audio_cap_initiator_broadcast_stream_param_t bcast_stream_params[SINK_STREAM_COUNT];
|
||||
static esp_ble_audio_cap_initiator_broadcast_subgroup_param_t bcast_subgroup_param;
|
||||
static esp_ble_audio_cap_initiator_broadcast_create_param_t bcast_create_param;
|
||||
static uint8_t bcast_bis_data[SINK_STREAM_COUNT][6];
|
||||
|
||||
static esp_ble_audio_cap_unicast_group_stream_param_t uni_stream_params[SINK_STREAM_COUNT];
|
||||
static esp_ble_audio_cap_unicast_group_stream_pair_param_t uni_pair_params[SINK_STREAM_COUNT];
|
||||
static esp_ble_audio_cap_unicast_group_param_t uni_group_param;
|
||||
static esp_ble_audio_cap_unicast_audio_start_stream_param_t uni_start_stream_params[SINK_STREAM_COUNT];
|
||||
static esp_ble_audio_cap_unicast_audio_start_param_t uni_start_param;
|
||||
|
||||
static esp_ble_audio_cap_commander_broadcast_reception_stop_member_param_t stop_member_param;
|
||||
static esp_ble_audio_cap_commander_broadcast_reception_stop_param_t stop_param;
|
||||
|
||||
static esp_ble_audio_cap_broadcast_source_t *broadcast_source;
|
||||
static esp_ble_audio_bap_broadcast_adv_info_t adv_info;
|
||||
static esp_timer_handle_t switch_timer;
|
||||
static esp_timer_handle_t start_timer;
|
||||
|
||||
static bool adv_registered;
|
||||
static bool proc_active;
|
||||
static bool on_broadcast;
|
||||
|
||||
/* Source ID of our broadcast on the Acceptor, needed to stop its reception. */
|
||||
static uint8_t recv_state_src_id;
|
||||
static bool recv_state_valid;
|
||||
|
||||
/* Sweep of the Acceptor's receive states, run once per connection. */
|
||||
static uint8_t recv_state_cnt;
|
||||
static uint8_t recv_state_next;
|
||||
static bool recv_state_sweeping;
|
||||
|
||||
static void switch_timer_arm(void)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
if (CONFIG_EXAMPLE_HANDOVER_PERIOD_S == 0 || switch_timer == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
(void)esp_timer_stop(switch_timer);
|
||||
|
||||
err = esp_timer_start_once(switch_timer,
|
||||
(uint64_t)CONFIG_EXAMPLE_HANDOVER_PERIOD_S * 1000000);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to arm handover timer, err %d", err);
|
||||
}
|
||||
}
|
||||
|
||||
static void start_timer_cb(void *arg)
|
||||
{
|
||||
if (peer.conn == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
(void)cap_handover_unicast_setup_and_start();
|
||||
}
|
||||
|
||||
/* CAP holds one procedure at a time and the library shares our callback lists,
|
||||
* so starting a procedure from inside a callback changes the active procedure
|
||||
* under a library handler that has not run yet. Leave the dispatch first.
|
||||
*/
|
||||
static void unicast_start_defer(void)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
(void)esp_timer_stop(start_timer);
|
||||
|
||||
err = esp_timer_start_once(start_timer, 0);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to schedule unicast start, err %d", err);
|
||||
}
|
||||
}
|
||||
|
||||
/* What the Acceptor's decoder must keep doing across the switch. Channel
|
||||
* allocation is absent on purpose: it differs between the two paths by design.
|
||||
*/
|
||||
struct codec_shape {
|
||||
esp_ble_audio_codec_cfg_freq_t freq;
|
||||
esp_ble_audio_codec_cfg_frame_dur_t frame_dur;
|
||||
uint16_t octets_per_frame;
|
||||
uint8_t frame_blocks;
|
||||
};
|
||||
|
||||
static int codec_shape_get(const esp_ble_audio_codec_cfg_t *codec_cfg,
|
||||
struct codec_shape *out)
|
||||
{
|
||||
if (esp_ble_audio_codec_cfg_get_freq(codec_cfg, &out->freq) != ESP_OK ||
|
||||
esp_ble_audio_codec_cfg_get_frame_dur(codec_cfg, &out->frame_dur) != ESP_OK ||
|
||||
esp_ble_audio_codec_cfg_get_octets_per_frame(codec_cfg,
|
||||
&out->octets_per_frame) != ESP_OK ||
|
||||
esp_ble_audio_codec_cfg_get_frame_blocks_per_sdu(codec_cfg, &out->frame_blocks,
|
||||
true) != ESP_OK) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Never memcmp: the trailing padding is uninitialised stack. */
|
||||
static bool codec_shape_eq(const struct codec_shape *a, const struct codec_shape *b)
|
||||
{
|
||||
return a->freq == b->freq &&
|
||||
a->frame_dur == b->frame_dur &&
|
||||
a->octets_per_frame == b->octets_per_frame &&
|
||||
a->frame_blocks == b->frame_blocks;
|
||||
}
|
||||
|
||||
/* A mismatch makes the Acceptor reconfigure its decoder at the switching point,
|
||||
* which is audible. One macro feeds both paths, so this only fires if someone
|
||||
* later points one of them elsewhere - better loudly than silently.
|
||||
*/
|
||||
static int handover_audio_config_check(void)
|
||||
{
|
||||
const esp_ble_audio_bap_lc3_preset_t *bcast = broadcast_preset_get();
|
||||
struct codec_shape bcast_shape;
|
||||
|
||||
if (bcast == NULL || codec_shape_get(&bcast->codec_cfg, &bcast_shape) != 0) {
|
||||
ESP_LOGE(TAG, "Cannot read broadcast codec configuration");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < SINK_STREAM_COUNT; i++) {
|
||||
const esp_ble_audio_bap_lc3_preset_t *uni = sink_preset_get(i);
|
||||
struct codec_shape uni_shape;
|
||||
|
||||
if (uni == NULL || codec_shape_get(&uni->codec_cfg, &uni_shape) != 0) {
|
||||
ESP_LOGE(TAG, "Cannot read sink %u codec configuration", (unsigned)i);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
if (!codec_shape_eq(&uni_shape, &bcast_shape)) {
|
||||
ESP_LOGE(TAG, "Codec config differs on sink %u: "
|
||||
"unicast freq %d dur %d octets %u blocks %u vs "
|
||||
"broadcast freq %d dur %d octets %u blocks %u",
|
||||
(unsigned)i,
|
||||
uni_shape.freq, uni_shape.frame_dur,
|
||||
uni_shape.octets_per_frame, uni_shape.frame_blocks,
|
||||
bcast_shape.freq, bcast_shape.frame_dur,
|
||||
bcast_shape.octets_per_frame, bcast_shape.frame_blocks);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
/* Same SDU rate and framing, or the paths cannot carry the same frame
|
||||
* sequence. ISO_Interval is the controller's and only observable once
|
||||
* the streams run - cap_handover_diag_sample() prints it.
|
||||
*/
|
||||
if (uni->qos.interval != bcast->qos.interval ||
|
||||
uni->qos.framing != bcast->qos.framing) {
|
||||
ESP_LOGE(TAG, "QoS differs on sink %u: unicast interval %lu framing %d vs "
|
||||
"broadcast interval %lu framing %d", (unsigned)i,
|
||||
(unsigned long)uni->qos.interval, uni->qos.framing,
|
||||
(unsigned long)bcast->qos.interval, bcast->qos.framing);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
/* PD may legitimately differ: it is how a rendering-point offset would
|
||||
* be absorbed. Nothing uses that yet, so warn rather than fail.
|
||||
*/
|
||||
if (uni->qos.pd != bcast->qos.pd) {
|
||||
ESP_LOGW(TAG, "Presentation delay differs on sink %u: %lu vs %lu us",
|
||||
(unsigned)i, (unsigned long)uni->qos.pd, (unsigned long)bcast->qos.pd);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int handover_unicast_to_broadcast(void)
|
||||
{
|
||||
esp_ble_audio_cap_handover_unicast_to_broadcast_param_t param = {0};
|
||||
uint8_t *ext_data = NULL;
|
||||
uint8_t ext_len = 0;
|
||||
size_t count = 0;
|
||||
int err;
|
||||
|
||||
err = handover_audio_config_check();
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
|
||||
/* All of them: CAP 7.3.1.10 applies to every CIS carrying Initiator-to-Acceptor audio. */
|
||||
for (size_t i = 0; i < peer.sink_ep_count && count < ARRAY_SIZE(bcast_stream_params); i++) {
|
||||
esp_ble_audio_cap_stream_t *stream = &peer.sink_streams[i];
|
||||
esp_ble_audio_bap_ep_info_t ep_info = {0};
|
||||
esp_ble_audio_location_t loc;
|
||||
|
||||
if (stream->bap_stream.ep == NULL ||
|
||||
esp_ble_audio_bap_ep_get_info(stream->bap_stream.ep, &ep_info) != ESP_OK ||
|
||||
ep_info.state != ESP_BLE_AUDIO_BAP_EP_STATE_STREAMING) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Shared subgroup codec cfg, so the channel allocation goes per BIS. */
|
||||
loc = broadcast_bis_location_get(count);
|
||||
bcast_bis_data[count][0] = 5;
|
||||
bcast_bis_data[count][1] = ESP_BLE_AUDIO_CODEC_CFG_CHAN_ALLOC;
|
||||
bcast_bis_data[count][2] = (uint8_t)loc;
|
||||
bcast_bis_data[count][3] = (uint8_t)(loc >> 8);
|
||||
bcast_bis_data[count][4] = (uint8_t)(loc >> 16);
|
||||
bcast_bis_data[count][5] = (uint8_t)(loc >> 24);
|
||||
|
||||
bcast_stream_params[count].stream = stream;
|
||||
bcast_stream_params[count].data = bcast_bis_data[count];
|
||||
bcast_stream_params[count].data_len = sizeof(bcast_bis_data[count]);
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count == 0) {
|
||||
ESP_LOGW(TAG, "No streaming sink stream to hand over");
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
bcast_subgroup_param.stream_count = count;
|
||||
bcast_subgroup_param.stream_params = bcast_stream_params;
|
||||
bcast_subgroup_param.codec_cfg = &broadcast_preset_get()->codec_cfg;
|
||||
|
||||
bcast_create_param.subgroup_count = 1;
|
||||
bcast_create_param.subgroup_params = &bcast_subgroup_param;
|
||||
bcast_create_param.qos = &broadcast_preset_get()->qos;
|
||||
|
||||
/* PA stays stopped: the BASE only exists once the procedure created the source. */
|
||||
ext_data = broadcast_ext_adv_data_get(&ext_len);
|
||||
if (ext_data == NULL) {
|
||||
ESP_LOGE(TAG, "No adv data, cannot hand over to broadcast");
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
err = ext_adv_start_without_base(ext_data, ext_len);
|
||||
free(ext_data);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start advertising for handover, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
if (adv_registered == false) {
|
||||
adv_info.adv_handle = ADV_HANDLE;
|
||||
adv_info.addr_type = 0; /* public */
|
||||
adv_info.sid = ADV_SID;
|
||||
|
||||
if (local_public_addr_get(adv_info.addr) != 0) {
|
||||
ESP_LOGE(TAG, "No local address for the BASS Add Source");
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_adv_add(&adv_info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to add adv for broadcast source, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
adv_registered = true;
|
||||
}
|
||||
|
||||
param.type = ESP_BLE_AUDIO_CAP_SET_TYPE_AD_HOC;
|
||||
param.unicast_group = unicast_group_get();
|
||||
param.adv_handle = ADV_HANDLE;
|
||||
param.pa_interval = HANDOVER_PA_INTERVAL;
|
||||
param.broadcast_id = LOCAL_BROADCAST_ID;
|
||||
param.broadcast_create_param = &bcast_create_param;
|
||||
|
||||
ESP_LOGI(TAG, "Handover: unicast -> broadcast (%u stream(s))", (unsigned)count);
|
||||
|
||||
cap_handover_diag_sample("u2b-pre", true);
|
||||
|
||||
err = esp_ble_audio_cap_handover_unicast_to_broadcast(¶m);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to hand over to broadcast, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int handover_broadcast_to_unicast(void)
|
||||
{
|
||||
esp_ble_audio_cap_handover_broadcast_to_unicast_param_t param = {0};
|
||||
size_t count = 0;
|
||||
int err;
|
||||
|
||||
if (recv_state_valid == false) {
|
||||
ESP_LOGW(TAG, "Acceptor has no receive state for our broadcast");
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
/* Must be the broadcast source's own streams. Sink only: a BIS has no return path. */
|
||||
for (size_t i = 0; i < peer.sink_ep_count && count < ARRAY_SIZE(uni_stream_params); i++) {
|
||||
esp_ble_audio_bap_lc3_preset_t *preset = sink_preset_get(i);
|
||||
|
||||
if (preset == NULL || peer.sink_eps[i] == NULL) {
|
||||
continue;
|
||||
}
|
||||
|
||||
uni_stream_params[count].qos_cfg = &preset->qos;
|
||||
uni_stream_params[count].stream = &peer.sink_streams[i];
|
||||
|
||||
uni_pair_params[count].rx_param = NULL;
|
||||
uni_pair_params[count].tx_param = &uni_stream_params[count];
|
||||
|
||||
uni_start_stream_params[count].member.member = peer.conn;
|
||||
uni_start_stream_params[count].stream = &peer.sink_streams[i];
|
||||
uni_start_stream_params[count].ep = peer.sink_eps[i];
|
||||
uni_start_stream_params[count].codec_cfg = &preset->codec_cfg;
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count == 0) {
|
||||
ESP_LOGW(TAG, "No endpoint to hand back to unicast");
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
uni_group_param.params_count = count;
|
||||
uni_group_param.params = uni_pair_params;
|
||||
|
||||
uni_start_param.type = ESP_BLE_AUDIO_CAP_SET_TYPE_AD_HOC;
|
||||
uni_start_param.count = count;
|
||||
uni_start_param.stream_params = uni_start_stream_params;
|
||||
|
||||
stop_member_param.member.member = peer.conn;
|
||||
stop_member_param.src_id = recv_state_src_id;
|
||||
stop_member_param.num_subgroups = bcast_create_param.subgroup_count;
|
||||
|
||||
stop_param.type = ESP_BLE_AUDIO_CAP_SET_TYPE_AD_HOC;
|
||||
stop_param.param = &stop_member_param;
|
||||
stop_param.count = 1;
|
||||
|
||||
/* Documented as ignored once reception_stop_param is set, but the procedure
|
||||
* still matches receive state notifications against this triple. Leaving it
|
||||
* zero stalls the handover after the broadcast source stops.
|
||||
*/
|
||||
param.broadcast_id = LOCAL_BROADCAST_ID;
|
||||
param.adv_sid = ADV_SID;
|
||||
param.adv_type = 0; /* public */
|
||||
|
||||
param.reception_stop_param = &stop_param;
|
||||
param.broadcast_source = broadcast_source;
|
||||
param.unicast_group_param = &uni_group_param;
|
||||
param.unicast_start_param = &uni_start_param;
|
||||
|
||||
ESP_LOGI(TAG, "Handover: broadcast -> unicast (%u stream(s))", (unsigned)count);
|
||||
|
||||
cap_handover_diag_sample("b2u-pre", true);
|
||||
|
||||
err = esp_ble_audio_cap_handover_broadcast_to_unicast(¶m);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to hand back to unicast, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* Stop feeding the broadcast now, not from the stream callbacks: the ISO
|
||||
* channel goes down with the BIG terminate, but the disconnected callback
|
||||
* trails the controller's event by ~100 ms and every SDU in between is
|
||||
* rejected. Unicast re-registers from its own started callback. The other
|
||||
* direction does not need this - ASCS delivers CIS callbacks promptly.
|
||||
*/
|
||||
for (size_t i = 0; i < ARRAY_SIZE(peer.sink_streams); i++) {
|
||||
(void)cap_handover_tx_unregister_stream(&peer.sink_streams[i]);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void switch_timer_cb(void *arg)
|
||||
{
|
||||
int err;
|
||||
|
||||
if (proc_active) {
|
||||
/* CAP runs one procedure at a time; retry on the next tick. */
|
||||
switch_timer_arm();
|
||||
return;
|
||||
}
|
||||
|
||||
if (peer.conn == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
proc_active = true;
|
||||
|
||||
err = on_broadcast ? handover_broadcast_to_unicast()
|
||||
: handover_unicast_to_broadcast();
|
||||
if (err) {
|
||||
proc_active = false;
|
||||
switch_timer_arm();
|
||||
}
|
||||
}
|
||||
|
||||
static void unicast_to_broadcast_created_cb(esp_ble_audio_cap_broadcast_source_t *source)
|
||||
{
|
||||
uint8_t *per_data;
|
||||
uint8_t per_len = 0;
|
||||
int err;
|
||||
|
||||
/* The source exists now, so publish its BASE before the Acceptor syncs. */
|
||||
per_data = broadcast_base_data_get(source, &per_len);
|
||||
if (per_data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to build BASE for handover");
|
||||
return;
|
||||
}
|
||||
|
||||
err = per_adv_data_start(per_data, per_len);
|
||||
free(per_data);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to publish BASE, err %d", err);
|
||||
}
|
||||
}
|
||||
|
||||
static void unicast_to_broadcast_complete_cb(int err, esp_ble_conn_t *conn,
|
||||
esp_ble_audio_cap_unicast_group_t *group,
|
||||
esp_ble_audio_cap_broadcast_source_t *source)
|
||||
{
|
||||
/* On success the procedure deleted the unicast group. */
|
||||
unicast_group_set(group);
|
||||
broadcast_source = source;
|
||||
proc_active = false;
|
||||
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Handover to broadcast failed, err %d", err);
|
||||
|
||||
/* The group is already gone, so a failure leaves us broadcasting to
|
||||
* nobody. The stop callback deletes the source and restores unicast.
|
||||
*/
|
||||
if (source != NULL &&
|
||||
esp_ble_audio_cap_initiator_broadcast_audio_stop(source) != 0) {
|
||||
ESP_LOGE(TAG, "Failed to stop broadcast source");
|
||||
}
|
||||
} else {
|
||||
on_broadcast = true;
|
||||
|
||||
ESP_LOGI(TAG, "Handover to broadcast completed");
|
||||
|
||||
/* BIG terms are readable now; its timestamp is not, until the first
|
||||
* SDU. The next b2u-pre sample catches that side with real data.
|
||||
*/
|
||||
cap_handover_diag_sample("u2b-post", false);
|
||||
}
|
||||
|
||||
switch_timer_arm();
|
||||
}
|
||||
|
||||
static void broadcast_to_unicast_complete_cb(int err, esp_ble_conn_t *conn,
|
||||
esp_ble_audio_cap_broadcast_source_t *source,
|
||||
esp_ble_audio_cap_unicast_group_t *group)
|
||||
{
|
||||
/* On success the procedure deleted the broadcast source. */
|
||||
unicast_group_set(group);
|
||||
broadcast_source = source;
|
||||
proc_active = false;
|
||||
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Handover to unicast failed, err %d", err);
|
||||
} else {
|
||||
on_broadcast = false;
|
||||
|
||||
/* Nothing left to announce: drop the announcement and the periodic
|
||||
* advertising carrying its BASE before declaring the handover done.
|
||||
*/
|
||||
if (adv_stop() != 0) {
|
||||
ESP_LOGW(TAG, "Failed to stop advertising after handover");
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Handover to unicast completed");
|
||||
|
||||
/* Mirror of the above: CIS terms readable, timestamp not yet. */
|
||||
cap_handover_diag_sample("b2u-post", false);
|
||||
}
|
||||
|
||||
switch_timer_arm();
|
||||
}
|
||||
|
||||
static esp_ble_audio_cap_handover_cb_t handover_cb = {
|
||||
.unicast_to_broadcast_created = unicast_to_broadcast_created_cb,
|
||||
.unicast_to_broadcast_complete = unicast_to_broadcast_complete_cb,
|
||||
.broadcast_to_unicast_complete = broadcast_to_unicast_complete_cb,
|
||||
};
|
||||
|
||||
/* Discovery only subscribes, so read the receive states once to see what the
|
||||
* Acceptor already holds. They outlive our reboot, and BAP 6.5.4 rejects an Add
|
||||
* Source duplicating the {address, SID, Broadcast_ID} triple - an entry left by
|
||||
* an earlier run of ours would fail every handover.
|
||||
*/
|
||||
static void recv_state_sweep_next(void)
|
||||
{
|
||||
while (recv_state_next < recv_state_cnt) {
|
||||
uint8_t idx = recv_state_next++;
|
||||
int err;
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_assistant_read_recv_state(peer.conn_handle, idx);
|
||||
if (err == 0) {
|
||||
return; /* Continues in assistant_recv_state_cb */
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Failed to read receive state %u, err %d", idx, err);
|
||||
}
|
||||
|
||||
recv_state_sweeping = false;
|
||||
|
||||
unicast_start_defer();
|
||||
}
|
||||
|
||||
static void assistant_discover_cb(esp_ble_conn_t *conn, int err, uint8_t recv_state_count)
|
||||
{
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "BASS discovery failed, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "BASS discovered (%u receive state(s))", recv_state_count);
|
||||
|
||||
recv_state_cnt = recv_state_count;
|
||||
recv_state_next = 0;
|
||||
recv_state_sweeping = true;
|
||||
|
||||
recv_state_sweep_next();
|
||||
}
|
||||
|
||||
static void assistant_recv_state_cb(esp_ble_conn_t *conn, int err,
|
||||
const esp_ble_audio_bap_scan_delegator_recv_state_t *state)
|
||||
{
|
||||
bool ours = (err == 0 && state != NULL && state->broadcast_id == LOCAL_BROADCAST_ID);
|
||||
uint32_t bis_sync = 0;
|
||||
|
||||
if (recv_state_sweeping) {
|
||||
if (ours) {
|
||||
ESP_LOGW(TAG, "Removing leftover receive state (src_id %u)", state->src_id);
|
||||
|
||||
if (esp_ble_audio_bap_broadcast_assistant_rem_src(peer.conn_handle,
|
||||
state->src_id) == 0) {
|
||||
return; /* Continues in assistant_rem_src_cb */
|
||||
}
|
||||
|
||||
ESP_LOGE(TAG, "Failed to remove leftover receive state");
|
||||
}
|
||||
|
||||
recv_state_sweep_next();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ours) {
|
||||
return;
|
||||
}
|
||||
|
||||
recv_state_src_id = state->src_id;
|
||||
recv_state_valid = true;
|
||||
|
||||
/* The Acceptor notifies per field change, so the same pa_sync arrives more
|
||||
* than once (PA synced, then BIG synced). bis_sync tells them apart, and is
|
||||
* also what says whether audio is flowing.
|
||||
*/
|
||||
for (uint8_t i = 0; i < state->num_subgroups; i++) {
|
||||
bis_sync |= state->subgroups[i].bis_sync;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Acceptor receive state: src_id %u pa_sync %u bis_sync 0x%08lx",
|
||||
state->src_id, state->pa_sync_state, (unsigned long)bis_sync);
|
||||
|
||||
/* The Acceptor asks for the periodic advertising train it was told about.
|
||||
* We are the advertiser, so hand it over instead of making it scan.
|
||||
*/
|
||||
if (state->pa_sync_state == ESP_BLE_AUDIO_BAP_PA_STATE_INFO_REQ) {
|
||||
/* The Source ID goes in the high octet of the service data; the Acceptor
|
||||
* reads it from there to match the transfer to a receive state. */
|
||||
err = pa_set_info_transfer(peer.conn_handle, peer.dst,
|
||||
(uint16_t)state->src_id << 8);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to transfer PA sync info, err %d", err);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "PA sync info transferred (src_id %u)", state->src_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void assistant_recv_state_removed_cb(esp_ble_conn_t *conn, uint8_t src_id)
|
||||
{
|
||||
if (recv_state_valid && src_id == recv_state_src_id) {
|
||||
recv_state_valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
static void assistant_rem_src_cb(esp_ble_conn_t *conn, int err)
|
||||
{
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Remove source failed, err %d", err);
|
||||
}
|
||||
|
||||
if (recv_state_sweeping) {
|
||||
recv_state_sweep_next();
|
||||
}
|
||||
}
|
||||
|
||||
static esp_ble_audio_bap_broadcast_assistant_cb_t assistant_cb = {
|
||||
.discover = assistant_discover_cb,
|
||||
.recv_state = assistant_recv_state_cb,
|
||||
.recv_state_removed = assistant_recv_state_removed_cb,
|
||||
.rem_src = assistant_rem_src_cb,
|
||||
};
|
||||
|
||||
int cap_handover_proc_discover(uint16_t conn_handle)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_assistant_discover(conn_handle);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to discover BASS, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Discovering BASS");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void cap_handover_proc_unicast_started(void)
|
||||
{
|
||||
if (on_broadcast) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch_timer_arm();
|
||||
}
|
||||
|
||||
void cap_handover_proc_broadcast_stopped(esp_ble_audio_cap_broadcast_source_t *source)
|
||||
{
|
||||
if (source != broadcast_source) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* A source is only deletable once its BIG is down, so finish it here. */
|
||||
if (esp_ble_audio_cap_initiator_broadcast_audio_delete(source) != 0) {
|
||||
ESP_LOGE(TAG, "Failed to delete broadcast source");
|
||||
return;
|
||||
}
|
||||
|
||||
broadcast_source = NULL;
|
||||
on_broadcast = false;
|
||||
|
||||
(void)adv_stop();
|
||||
|
||||
/* A failed handover left the ASEs released; restore unicast while connected. */
|
||||
if (peer.conn != NULL) {
|
||||
unicast_start_defer();
|
||||
}
|
||||
}
|
||||
|
||||
void cap_handover_proc_reset(void)
|
||||
{
|
||||
if (switch_timer != NULL) {
|
||||
(void)esp_timer_stop(switch_timer);
|
||||
}
|
||||
|
||||
if (start_timer != NULL) {
|
||||
(void)esp_timer_stop(start_timer);
|
||||
}
|
||||
|
||||
if (broadcast_source != NULL) {
|
||||
/* Deleted by cap_handover_proc_broadcast_stopped once the BIG is down. */
|
||||
if (esp_ble_audio_cap_initiator_broadcast_audio_stop(broadcast_source) != 0) {
|
||||
ESP_LOGE(TAG, "Failed to stop broadcast source");
|
||||
}
|
||||
}
|
||||
|
||||
proc_active = false;
|
||||
on_broadcast = false;
|
||||
recv_state_valid = false;
|
||||
recv_state_sweeping = false;
|
||||
}
|
||||
|
||||
int cap_handover_proc_init(void)
|
||||
{
|
||||
const esp_timer_create_args_t timer_args = {
|
||||
.callback = switch_timer_cb,
|
||||
.name = "cap_handover",
|
||||
};
|
||||
const esp_timer_create_args_t start_timer_args = {
|
||||
.callback = start_timer_cb,
|
||||
.name = "cap_uni_start",
|
||||
};
|
||||
int err;
|
||||
|
||||
err = esp_ble_audio_cap_handover_register_cb(&handover_cb);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register handover callbacks, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_assistant_register_cb(&assistant_cb);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register broadcast assistant callbacks, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_timer_create(&timer_args, &switch_timer);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to create handover timer, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_timer_create(&start_timer_args, &start_timer);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to create unicast start timer, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "CAP initiator handover initialized (period %u s)",
|
||||
(unsigned)CONFIG_EXAMPLE_HANDOVER_PERIOD_S);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "cap_handover.h"
|
||||
|
||||
/* The same stream objects move between unicast and broadcast, so one slot each. */
|
||||
static struct tx_stream tx_streams[SINK_STREAM_COUNT];
|
||||
|
||||
static const char *cap_stream_tx_label(const esp_ble_audio_cap_stream_t *cap_stream)
|
||||
{
|
||||
esp_ble_audio_bap_ep_info_t ep_info = {0};
|
||||
|
||||
if (cap_stream == NULL || cap_stream->bap_stream.ep == NULL) {
|
||||
return "SRC";
|
||||
}
|
||||
|
||||
if (esp_ble_audio_bap_ep_get_info(cap_stream->bap_stream.ep, &ep_info) != 0) {
|
||||
return "SRC";
|
||||
}
|
||||
|
||||
return (ep_info.dir == ESP_BLE_AUDIO_DIR_SINK) ? "SNK" : "SRC";
|
||||
}
|
||||
|
||||
static bool tx_stream_is_streaming(const struct tx_stream *tx_stream)
|
||||
{
|
||||
esp_ble_audio_bap_ep_info_t ep_info = {0};
|
||||
int err;
|
||||
|
||||
if (tx_stream == NULL || tx_stream->stream == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Broadcast streams have no ep; registration from started_cb implies streaming. */
|
||||
if (tx_stream->is_broadcast) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (tx_stream->stream->bap_stream.ep == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_ep_get_info(tx_stream->stream->bap_stream.ep, &ep_info);
|
||||
if (err) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (ep_info.state == ESP_BLE_AUDIO_BAP_EP_STATE_STREAMING);
|
||||
}
|
||||
|
||||
static void cap_handover_tx_send(struct tx_stream *tx_stream)
|
||||
{
|
||||
const char *label;
|
||||
int err;
|
||||
|
||||
if (tx_stream == NULL || tx_stream->stream == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tx_stream_is_streaming(tx_stream) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
label = cap_stream_tx_label(tx_stream->stream);
|
||||
|
||||
if (tx_stream->stream->bap_stream.qos == NULL ||
|
||||
tx_stream->stream->bap_stream.qos->sdu == 0) {
|
||||
ESP_LOGE(TAG, "[%s] Invalid QoS", label);
|
||||
return;
|
||||
}
|
||||
|
||||
if (tx_stream->data == NULL) {
|
||||
ESP_LOGE(TAG, "[%s] Buffer unavailable (SDU %u)",
|
||||
label, tx_stream->stream->bap_stream.qos->sdu);
|
||||
return;
|
||||
}
|
||||
|
||||
memset(tx_stream->data, (uint8_t)tx_stream->seq_num, tx_stream->stream->bap_stream.qos->sdu);
|
||||
|
||||
err = esp_ble_audio_cap_stream_send(tx_stream->stream, tx_stream->data,
|
||||
tx_stream->stream->bap_stream.qos->sdu,
|
||||
tx_stream->seq_num);
|
||||
if (err) {
|
||||
ESP_LOGD(TAG, "[%s] send failed, err %d", label, err);
|
||||
return;
|
||||
}
|
||||
|
||||
tx_stream->seq_num++;
|
||||
}
|
||||
|
||||
void cap_handover_tx_stream_sent(esp_ble_audio_bap_stream_t *stream, void *user_data)
|
||||
{
|
||||
for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) {
|
||||
if (tx_streams[i].stream && &tx_streams[i].stream->bap_stream == stream) {
|
||||
char name[24];
|
||||
|
||||
snprintf(name, sizeof(name), "%s #%zu",
|
||||
cap_stream_tx_label(tx_streams[i].stream), i);
|
||||
example_audio_tx_scheduler_on_sent(&tx_streams[i].scheduler, user_data, TAG, name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void tx_scheduler_cb(void *arg)
|
||||
{
|
||||
struct tx_stream *tx_stream = arg;
|
||||
|
||||
cap_handover_tx_send(tx_stream);
|
||||
}
|
||||
|
||||
int cap_handover_tx_register_stream(esp_ble_audio_cap_stream_t *cap_stream, bool is_broadcast)
|
||||
{
|
||||
int err;
|
||||
|
||||
if (cap_stream == NULL) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) {
|
||||
if (tx_streams[i].stream == NULL) {
|
||||
const char *label = cap_stream_tx_label(cap_stream);
|
||||
|
||||
if (cap_stream->bap_stream.qos == NULL || cap_stream->bap_stream.qos->sdu == 0) {
|
||||
ESP_LOGE(TAG, "[%s #%zu] Invalid QoS", label, i);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
if (tx_streams[i].data == NULL) {
|
||||
tx_streams[i].data = calloc(1, cap_stream->bap_stream.qos->sdu);
|
||||
if (tx_streams[i].data == NULL) {
|
||||
ESP_LOGE(TAG, "[%s #%zu] Failed to alloc buffer (SDU %u)",
|
||||
label, i, cap_stream->bap_stream.qos->sdu);
|
||||
return -ENOMEM;
|
||||
}
|
||||
}
|
||||
|
||||
tx_streams[i].stream = cap_stream;
|
||||
tx_streams[i].is_broadcast = is_broadcast;
|
||||
tx_streams[i].seq_num = 0;
|
||||
example_audio_tx_scheduler_reset(&tx_streams[i].scheduler);
|
||||
|
||||
err = example_audio_tx_scheduler_start(&tx_streams[i].scheduler, cap_stream->bap_stream.qos->interval);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "[%s #%zu] Scheduler start failed, err %d",
|
||||
label, i, err);
|
||||
tx_streams[i].stream = NULL;
|
||||
tx_streams[i].is_broadcast = false;
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "[%s #%zu] Started (SDU %u, interval %u us)",
|
||||
label, i,
|
||||
cap_stream->bap_stream.qos->sdu,
|
||||
cap_stream->bap_stream.qos->interval);
|
||||
|
||||
cap_handover_tx_send(&tx_streams[i]);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGE(TAG, "No free TX stream slot");
|
||||
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
int cap_handover_tx_unregister_stream(esp_ble_audio_cap_stream_t *cap_stream)
|
||||
{
|
||||
int err;
|
||||
|
||||
if (cap_stream == NULL) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) {
|
||||
if (tx_streams[i].stream == cap_stream) {
|
||||
const char *label = cap_stream_tx_label(tx_streams[i].stream);
|
||||
|
||||
err = example_audio_tx_scheduler_stop(&tx_streams[i].scheduler);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "[%s #%zu] Scheduler stop failed, err %d",
|
||||
label, i, err);
|
||||
return err;
|
||||
}
|
||||
|
||||
tx_streams[i].stream = NULL;
|
||||
tx_streams[i].is_broadcast = false;
|
||||
if (tx_streams[i].data != NULL) {
|
||||
free(tx_streams[i].data);
|
||||
tx_streams[i].data = NULL;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "[%s #%zu] Stopped", label, i);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return -ENODATA;
|
||||
}
|
||||
|
||||
void cap_handover_tx_init(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
memset(tx_streams, 0, sizeof(tx_streams));
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) {
|
||||
err = example_audio_tx_scheduler_init(&tx_streams[i].scheduler,
|
||||
tx_scheduler_cb,
|
||||
&tx_streams[i]);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize tx scheduler[%zu], err %d", i, err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,693 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "cap_handover.h"
|
||||
|
||||
/* One preset per sink stream: each owns its codec cfg buffer, which is what
|
||||
* lets the streams carry different channel allocations.
|
||||
*/
|
||||
HANDOVER_LC3_PRESET_DEFINE(unicast_preset_left, ESP_BLE_AUDIO_LOCATION_FRONT_LEFT);
|
||||
HANDOVER_LC3_PRESET_DEFINE(unicast_preset_right, ESP_BLE_AUDIO_LOCATION_FRONT_RIGHT);
|
||||
static esp_ble_audio_bap_lc3_preset_t *const sink_presets[] = {
|
||||
&unicast_preset_left,
|
||||
&unicast_preset_right,
|
||||
};
|
||||
|
||||
_Static_assert(ARRAY_SIZE(sink_presets) >= SINK_STREAM_COUNT,
|
||||
"Need one preset (one channel allocation) per sink stream");
|
||||
|
||||
static esp_ble_audio_cap_unicast_group_t *unicast_group;
|
||||
|
||||
struct peer_config peer = {
|
||||
.conn_handle = CONN_HANDLE_INIT,
|
||||
};
|
||||
|
||||
esp_ble_audio_cap_unicast_group_t *unicast_group_get(void)
|
||||
{
|
||||
return unicast_group;
|
||||
}
|
||||
|
||||
void unicast_group_set(esp_ble_audio_cap_unicast_group_t *group)
|
||||
{
|
||||
unicast_group = group;
|
||||
}
|
||||
|
||||
esp_ble_audio_bap_lc3_preset_t *sink_preset_get(size_t idx)
|
||||
{
|
||||
return (idx < ARRAY_SIZE(sink_presets)) ? sink_presets[idx] : NULL;
|
||||
}
|
||||
|
||||
static const char *dir_str(esp_ble_audio_dir_t dir)
|
||||
{
|
||||
return dir == ESP_BLE_AUDIO_DIR_SINK ? "SNK" : "SRC";
|
||||
}
|
||||
|
||||
static const char *stream_dir_str(const esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
for (size_t i = 0; i < ARRAY_SIZE(peer.sink_streams); i++) {
|
||||
if (stream == &peer.sink_streams[i].bap_stream) {
|
||||
return "SNK";
|
||||
}
|
||||
}
|
||||
|
||||
return "???";
|
||||
}
|
||||
|
||||
static int stream_index(const esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
/* Index within its own direction's pool, so logs read "SNK #0" / "SNK #1". */
|
||||
for (size_t i = 0; i < ARRAY_SIZE(peer.sink_streams); i++) {
|
||||
if (stream == &peer.sink_streams[i].bap_stream) {
|
||||
return (int)i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Ask whether the endpoint transmits; do not derive it from dir. These streams
|
||||
* migrate, and dir means opposite things on the two endpoint kinds: the peer's
|
||||
* direction on a unicast ASE (SINK), our own on a broadcast source (SOURCE).
|
||||
* Testing dir == SINK therefore goes silent the moment a stream moves to a BIS.
|
||||
*/
|
||||
static bool is_tx_stream(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
esp_ble_audio_bap_ep_info_t ep_info = {0};
|
||||
esp_err_t err;
|
||||
|
||||
/* Detached already: only teardown sees this, and unregistering is a no-op. */
|
||||
if (stream->ep == NULL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_ep_get_info(stream->ep, &ep_info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to get ep info, err %d", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
return ep_info.can_send;
|
||||
}
|
||||
|
||||
static void unicast_stream_configured_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
const esp_ble_audio_bap_qos_cfg_pref_t *pref)
|
||||
{
|
||||
ESP_LOGI(TAG, "[%s #%d] Stream configured, QoS preference:",
|
||||
stream_dir_str(stream), stream_index(stream));
|
||||
|
||||
example_print_qos_pref(TAG, pref);
|
||||
}
|
||||
|
||||
static void unicast_stream_qos_set_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
ESP_LOGI(TAG, "[%s #%d] QoS set",
|
||||
stream_dir_str(stream), stream_index(stream));
|
||||
}
|
||||
|
||||
static void unicast_stream_enabled_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
ESP_LOGI(TAG, "[%s #%d] Stream enabled",
|
||||
stream_dir_str(stream), stream_index(stream));
|
||||
}
|
||||
|
||||
static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
esp_ble_audio_cap_stream_t *cap_stream;
|
||||
int err;
|
||||
|
||||
ESP_LOGI(TAG, "[%s #%d] Stream started",
|
||||
stream_dir_str(stream), stream_index(stream));
|
||||
|
||||
if (is_tx_stream(stream)) {
|
||||
cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream);
|
||||
|
||||
err = cap_handover_tx_register_stream(cap_stream, false);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "[%s #%d] Failed to register TX, err %d",
|
||||
stream_dir_str(stream), stream_index(stream), err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void unicast_stream_metadata_updated_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
ESP_LOGI(TAG, "[%s #%d] Metadata updated",
|
||||
stream_dir_str(stream), stream_index(stream));
|
||||
}
|
||||
|
||||
static void unicast_stream_disabled_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
ESP_LOGI(TAG, "[%s #%d] Stream disabled",
|
||||
stream_dir_str(stream), stream_index(stream));
|
||||
}
|
||||
|
||||
static void unicast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
|
||||
{
|
||||
esp_ble_audio_cap_stream_t *cap_stream;
|
||||
|
||||
ESP_LOGI(TAG, "[%s #%d] Stream stopped, reason 0x%02x",
|
||||
stream_dir_str(stream), stream_index(stream), reason);
|
||||
|
||||
if (is_tx_stream(stream)) {
|
||||
cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream);
|
||||
|
||||
(void)cap_handover_tx_unregister_stream(cap_stream);
|
||||
}
|
||||
}
|
||||
|
||||
static void unicast_stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
|
||||
{
|
||||
esp_ble_audio_cap_stream_t *cap_stream;
|
||||
|
||||
ESP_LOGI(TAG, "[%s #%d] ISO disconnected, reason 0x%02x",
|
||||
stream_dir_str(stream), stream_index(stream), reason);
|
||||
|
||||
if (is_tx_stream(stream)) {
|
||||
cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream);
|
||||
|
||||
(void)cap_handover_tx_unregister_stream(cap_stream);
|
||||
}
|
||||
}
|
||||
|
||||
static void unicast_stream_released_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
ESP_LOGI(TAG, "[%s #%d] Stream released",
|
||||
stream_dir_str(stream), stream_index(stream));
|
||||
}
|
||||
|
||||
static void unicast_stream_sent_cb(esp_ble_audio_bap_stream_t *stream, void *user_data)
|
||||
{
|
||||
cap_handover_tx_stream_sent(stream, user_data);
|
||||
}
|
||||
|
||||
static esp_ble_audio_bap_stream_ops_t unicast_stream_ops = {
|
||||
.configured = unicast_stream_configured_cb,
|
||||
.qos_set = unicast_stream_qos_set_cb,
|
||||
.enabled = unicast_stream_enabled_cb,
|
||||
.started = unicast_stream_started_cb,
|
||||
.metadata_updated = unicast_stream_metadata_updated_cb,
|
||||
.disabled = unicast_stream_disabled_cb,
|
||||
.stopped = unicast_stream_stopped_cb,
|
||||
.released = unicast_stream_released_cb,
|
||||
.sent = unicast_stream_sent_cb,
|
||||
.disconnected = unicast_stream_disconnected_cb,
|
||||
};
|
||||
|
||||
static int discover_cas(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = esp_ble_audio_cap_initiator_unicast_discover(peer.conn_handle);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to discover CAS, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Discovering CAS");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int discover_sinks(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(peer.sink_streams); i++) {
|
||||
esp_ble_audio_cap_stream_ops_register(&peer.sink_streams[i], &unicast_stream_ops);
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_unicast_client_discover(peer.conn_handle, ESP_BLE_AUDIO_DIR_SINK);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to discover sinks, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Discovering sinks");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int unicast_group_create(void)
|
||||
{
|
||||
/* Referenced by the group while it exists, so they outlive this call. */
|
||||
static esp_ble_audio_cap_unicast_group_stream_param_t sink_stream_params[SINK_STREAM_COUNT];
|
||||
static esp_ble_audio_cap_unicast_group_stream_pair_param_t pair_params[SINK_STREAM_COUNT];
|
||||
esp_ble_audio_cap_unicast_group_param_t group_param = {0};
|
||||
size_t pair_count = 0;
|
||||
int err;
|
||||
|
||||
/* One CIS per sink stream. */
|
||||
for (size_t i = 0; i < peer.sink_ep_count; i++) {
|
||||
sink_stream_params[i].qos_cfg = &sink_presets[i]->qos;
|
||||
sink_stream_params[i].stream = &peer.sink_streams[i];
|
||||
|
||||
pair_params[pair_count].rx_param = NULL;
|
||||
pair_params[pair_count].tx_param = &sink_stream_params[i];
|
||||
pair_count++;
|
||||
}
|
||||
|
||||
if (pair_count == 0) {
|
||||
ESP_LOGW(TAG, "No endpoints available, skip creating unicast group");
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
group_param.params_count = pair_count;
|
||||
group_param.params = pair_params;
|
||||
|
||||
err = esp_ble_audio_cap_unicast_group_create(&group_param, &unicast_group);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to create unicast group, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Created unicast group");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int unicast_group_delete(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
if (unicast_group == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_cap_unicast_group_delete(unicast_group);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to delete unicast group, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
unicast_group = NULL;
|
||||
|
||||
ESP_LOGI(TAG, "Deleted unicast group");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int unicast_audio_start(void)
|
||||
{
|
||||
/* codec_cfg has to stay valid while the stream is non-idle. */
|
||||
static esp_ble_audio_cap_unicast_audio_start_stream_param_t stream_param[SINK_STREAM_COUNT];
|
||||
esp_ble_audio_cap_unicast_audio_start_param_t param = {0};
|
||||
int err;
|
||||
|
||||
for (size_t i = 0; i < peer.sink_ep_count; i++) {
|
||||
stream_param[param.count].member.member = peer.conn;
|
||||
stream_param[param.count].stream = &peer.sink_streams[i];
|
||||
stream_param[param.count].ep = peer.sink_eps[i];
|
||||
stream_param[param.count].codec_cfg = &sink_presets[i]->codec_cfg;
|
||||
param.count++;
|
||||
}
|
||||
|
||||
if (param.count == 0) {
|
||||
ESP_LOGW(TAG, "No endpoints available, skip starting unicast audio");
|
||||
return 0;
|
||||
}
|
||||
|
||||
param.type = ESP_BLE_AUDIO_CAP_SET_TYPE_AD_HOC;
|
||||
param.stream_params = stream_param;
|
||||
|
||||
err = esp_ble_audio_cap_initiator_unicast_audio_start(¶m);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start unicast audio, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Starting unicast streams");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cap_handover_unicast_setup_and_start(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = unicast_group_create();
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
|
||||
err = unicast_audio_start();
|
||||
if (err) {
|
||||
unicast_group_delete();
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void discover_cb(esp_ble_conn_t *conn, int err, esp_ble_audio_dir_t dir)
|
||||
{
|
||||
if (conn->handle != peer.conn_handle) {
|
||||
return;
|
||||
}
|
||||
|
||||
peer.conn = conn;
|
||||
|
||||
/* Sink only: a broadcast Audio Stream has no return path, so the Acceptor's
|
||||
* source ASEs take no part in a handover.
|
||||
*/
|
||||
if (dir != ESP_BLE_AUDIO_DIR_SINK) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Discovery sinks failed, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Discover sinks complete");
|
||||
|
||||
/* The collocated Commander writes the broadcast source into the Acceptor's
|
||||
* BASS, so discover that too. Its callback continues the unicast setup.
|
||||
*/
|
||||
(void)cap_handover_proc_discover(peer.conn_handle);
|
||||
}
|
||||
|
||||
static void pac_record_cb(esp_ble_conn_t *conn,
|
||||
esp_ble_audio_dir_t dir,
|
||||
const esp_ble_audio_codec_cap_t *codec_cap)
|
||||
{
|
||||
example_print_codec_cap(TAG, codec_cap);
|
||||
}
|
||||
|
||||
static void endpoint_cb(esp_ble_conn_t *conn,
|
||||
esp_ble_audio_dir_t dir,
|
||||
esp_ble_audio_bap_ep_t *ep)
|
||||
{
|
||||
/* Keep every sink endpoint: a stereo Acceptor exposes one per channel and
|
||||
* they are handed over together.
|
||||
*/
|
||||
if (dir == ESP_BLE_AUDIO_DIR_SINK &&
|
||||
peer.sink_ep_count < ARRAY_SIZE(peer.sink_eps)) {
|
||||
ESP_LOGI(TAG, "[%s #%zu] Endpoint discovered", dir_str(dir), peer.sink_ep_count);
|
||||
peer.sink_eps[peer.sink_ep_count++] = ep;
|
||||
}
|
||||
}
|
||||
|
||||
static esp_ble_audio_bap_unicast_client_cb_t unicast_client_cbs = {
|
||||
.discover = discover_cb,
|
||||
.pac_record = pac_record_cb,
|
||||
.endpoint = endpoint_cb,
|
||||
};
|
||||
|
||||
static void unicast_discovery_complete_cb(esp_ble_conn_t *conn, int err,
|
||||
const esp_ble_audio_csip_set_coordinator_set_member_t *member,
|
||||
const esp_ble_audio_csip_set_coordinator_csis_inst_t *csis_inst)
|
||||
{
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Unicast discovery completed, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (IS_ENABLED(CONFIG_BT_CAP_ACCEPTOR_SET_MEMBER)) {
|
||||
if (csis_inst == NULL) {
|
||||
ESP_LOGW(TAG, "Failed to discover CAS CSIS");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Found CAS with CSIS");
|
||||
|
||||
/* TODO: Do set member discovery */
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Found CAS");
|
||||
}
|
||||
|
||||
(void)discover_sinks();
|
||||
}
|
||||
|
||||
static void unicast_start_complete_cb(int err, esp_ble_conn_t *conn)
|
||||
{
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Unicast start completed, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Unicast start completed");
|
||||
|
||||
cap_handover_proc_unicast_started();
|
||||
}
|
||||
|
||||
static void broadcast_stopped_cb(esp_ble_audio_cap_broadcast_source_t *source, uint8_t reason)
|
||||
{
|
||||
ESP_LOGI(TAG, "Broadcast source stopped, reason 0x%02x", reason);
|
||||
|
||||
cap_handover_proc_broadcast_stopped(source);
|
||||
}
|
||||
|
||||
static esp_ble_audio_cap_initiator_cb_t cap_cb = {
|
||||
.unicast_discovery_complete = unicast_discovery_complete_cb,
|
||||
.unicast_start_complete = unicast_start_complete_cb,
|
||||
.broadcast_stopped = broadcast_stopped_cb,
|
||||
};
|
||||
|
||||
static bool check_and_connect(uint8_t type, const uint8_t *data,
|
||||
uint8_t data_len, void *user_data)
|
||||
{
|
||||
esp_ble_audio_gap_app_event_t *event;
|
||||
uint16_t uuid_val;
|
||||
int err;
|
||||
|
||||
event = user_data;
|
||||
assert(event);
|
||||
|
||||
if (type != EXAMPLE_AD_TYPE_SERVICE_DATA16) {
|
||||
return true; /* Continue parsing to next AD data type */
|
||||
}
|
||||
|
||||
if (data_len < sizeof(uuid_val)) {
|
||||
ESP_LOGW(TAG, "Invalid ad size %u (cas uuid)", data_len);
|
||||
return true; /* Continue parsing to next AD data type */
|
||||
}
|
||||
|
||||
uuid_val = sys_get_le16(data);
|
||||
|
||||
if (uuid_val != ESP_BLE_AUDIO_UUID_CAS_VAL) {
|
||||
/* We are looking for the TMAS service data */
|
||||
return true; /* Continue parsing to next AD data type */
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Found CAS in peer adv data!");
|
||||
|
||||
/* Stop scanning before connect — NimBLE rejects ble_gap_connect while
|
||||
* a discovery procedure is running. On failure restart scanning so we
|
||||
* don't stall in no-scan no-conn state. */
|
||||
err = ext_scan_stop();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop scanning, err %d", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
cap_handover_unicast_start();
|
||||
}
|
||||
|
||||
return false; /* Stop parsing */
|
||||
}
|
||||
|
||||
static void ext_scan_recv(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
if (peer.conn_handle != CONN_HANDLE_INIT) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Check if the advertising is connectable and if TMAS is supported */
|
||||
if (event->ext_scan_recv.event_type & EXAMPLE_ADV_PROP_CONNECTABLE) {
|
||||
esp_ble_audio_data_parse(event->ext_scan_recv.data,
|
||||
event->ext_scan_recv.data_len,
|
||||
check_and_connect, (void *)event);
|
||||
}
|
||||
}
|
||||
|
||||
static void acl_connect(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
int err;
|
||||
|
||||
if (event->acl_connect.status) {
|
||||
ESP_LOGE(TAG, "Connection failed, status %d", event->acl_connect.status);
|
||||
/* Scanning was stopped before conn_create and acl_disconnect only fires on
|
||||
* an established connection, so resume here or nothing runs. */
|
||||
cap_handover_unicast_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,
|
||||
EXAMPLE_BT_ADDR_PRINT_ARGS(event->acl_connect.dst.val));
|
||||
|
||||
peer.conn_handle = event->acl_connect.conn_handle;
|
||||
memcpy(peer.dst, event->acl_connect.dst.val, sizeof(peer.dst));
|
||||
|
||||
err = pairing_start(event->acl_connect.conn_handle);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initiate security, err %d", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
peer.conn_handle = CONN_HANDLE_INIT;
|
||||
peer.conn = NULL;
|
||||
memset(peer.dst, 0, sizeof(peer.dst));
|
||||
memset(peer.sink_eps, 0, sizeof(peer.sink_eps));
|
||||
peer.sink_ep_count = 0;
|
||||
peer.disc_completed = false;
|
||||
peer.mtu_exchanged = false;
|
||||
|
||||
/* Drop a broadcast source left running before reusing the stream objects. */
|
||||
cap_handover_proc_reset();
|
||||
|
||||
unicast_group_delete();
|
||||
|
||||
cap_handover_unicast_start();
|
||||
}
|
||||
|
||||
static void security_change(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
int err;
|
||||
|
||||
if (event->security_change.status) {
|
||||
security_failed_recover(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);
|
||||
|
||||
err = exchange_mtu(event->security_change.conn_handle);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to exchange MTU, err %d", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void cap_handover_unicast_gap_cb(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
switch (event->type) {
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_EXT_SCAN_RECV:
|
||||
ext_scan_recv(event);
|
||||
break;
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_ACL_CONNECT:
|
||||
acl_connect(event);
|
||||
break;
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_ACL_DISCONNECT:
|
||||
acl_disconnect(event);
|
||||
break;
|
||||
case ESP_BLE_AUDIO_GAP_EVENT_SECURITY_CHANGE:
|
||||
security_change(event);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void gatt_mtu_change(esp_ble_audio_gatt_app_event_t *event)
|
||||
{
|
||||
uint16_t conn_handle = event->gatt_mtu_change.conn_handle;
|
||||
int err;
|
||||
|
||||
ESP_LOGI(TAG, "MTU updated: handle %u mtu %u",
|
||||
conn_handle, event->gatt_mtu_change.mtu);
|
||||
|
||||
if (event->gatt_mtu_change.mtu < ESP_BLE_AUDIO_ATT_MTU_MIN) {
|
||||
ESP_LOGW(TAG, "Invalid new mtu %u, shall be at least %u",
|
||||
event->gatt_mtu_change.mtu, ESP_BLE_AUDIO_ATT_MTU_MIN);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_gattc_disc_start(conn_handle);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start svc disc, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Service discovery started: handle %u", conn_handle);
|
||||
|
||||
/* Note:
|
||||
* MTU exchanged event may arrived after discover completed event.
|
||||
*/
|
||||
peer.mtu_exchanged = true;
|
||||
|
||||
if (peer.disc_completed) {
|
||||
(void)discover_cas();
|
||||
}
|
||||
}
|
||||
|
||||
static void gattc_disc_cmpl(esp_ble_audio_gatt_app_event_t *event)
|
||||
{
|
||||
ESP_LOGI(TAG, "Service discovery complete: handle %u",
|
||||
event->gattc_disc_cmpl.conn_handle);
|
||||
|
||||
if (event->gattc_disc_cmpl.status) {
|
||||
ESP_LOGE(TAG, "gattc disc failed, status %u", event->gattc_disc_cmpl.status);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Note:
|
||||
* Discover completed event may arrived before MTU exchanged event.
|
||||
*/
|
||||
peer.disc_completed = true;
|
||||
|
||||
if (peer.mtu_exchanged) {
|
||||
(void)discover_cas();
|
||||
}
|
||||
}
|
||||
|
||||
void cap_handover_unicast_gatt_cb(esp_ble_audio_gatt_app_event_t *event)
|
||||
{
|
||||
switch (event->type) {
|
||||
case ESP_BLE_AUDIO_GATT_EVENT_GATT_MTU_CHANGE:
|
||||
gatt_mtu_change(event);
|
||||
break;
|
||||
case ESP_BLE_AUDIO_GATT_EVENT_GATTC_DISC_CMPL:
|
||||
gattc_disc_cmpl(event);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int cap_handover_unicast_start(void)
|
||||
{
|
||||
return ext_scan_start();
|
||||
}
|
||||
|
||||
int cap_handover_unicast_init(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = esp_ble_audio_cap_initiator_register_cb(&cap_cb);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register CAP callbacks, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_unicast_client_register_cb(&unicast_client_cbs);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register BAP unicast client callbacks, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "CAP initiator unicast initialized");
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
dependencies:
|
||||
example_init:
|
||||
path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_init
|
||||
example_utils:
|
||||
path: ${IDF_PATH}/examples/bluetooth/esp_ble_audio/common_components/example_utils
|
||||
77
examples/bluetooth/esp_ble_audio/cap/handover/main/main.c
Normal file
77
examples/bluetooth/esp_ble_audio/cap/handover/main/main.c
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "nvs_flash.h"
|
||||
|
||||
#include "cap_handover.h"
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
esp_ble_audio_init_info_t info = {
|
||||
.gap_cb = cap_handover_unicast_gap_cb,
|
||||
.gatt_cb = cap_handover_unicast_gatt_cb,
|
||||
};
|
||||
esp_err_t err;
|
||||
|
||||
/* Initialize NVS — it is used to store PHY calibration data */
|
||||
err = nvs_flash_init();
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
err = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(err);
|
||||
|
||||
err = bluetooth_init();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize BLE, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = 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);
|
||||
return;
|
||||
}
|
||||
|
||||
err = cap_handover_unicast_init();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = cap_handover_proc_init();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
cap_handover_tx_init();
|
||||
|
||||
err = esp_ble_audio_common_start(NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start audio, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = set_device_name();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set device name, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Scanning only: the broadcast source is created by the handover procedure. */
|
||||
err = cap_handover_unicast_start();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
170
examples/bluetooth/esp_ble_audio/cap/handover/main/nimble/adv.c
Normal file
170
examples/bluetooth/esp_ble_audio/cap/handover/main/nimble/adv.c
Normal file
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 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_hs.h"
|
||||
|
||||
#include "os/os_mbuf.h"
|
||||
|
||||
#include "cap_handover.h"
|
||||
|
||||
static int gap_event_cb(struct ble_gap_event *event, void *arg)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int adv_data_set(const uint8_t *payload, uint8_t payload_len, bool periodic)
|
||||
{
|
||||
struct os_mbuf *data;
|
||||
int err;
|
||||
|
||||
data = os_msys_get_pkthdr(payload_len, 0);
|
||||
if (data == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to get %s adv mbuf", periodic ? "per" : "ext");
|
||||
return -1;
|
||||
}
|
||||
|
||||
err = os_mbuf_append(data, payload, payload_len);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to append %s adv data, err %d", periodic ? "per" : "ext", err);
|
||||
os_mbuf_free_chain(data);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = periodic ? ble_gap_periodic_adv_set_data(ADV_HANDLE, data)
|
||||
: ble_gap_ext_adv_set_data(ADV_HANDLE, data);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set %s adv data, err %d", periodic ? "per" : "ext", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int adv_set_configure(const uint8_t *ext_data, uint8_t ext_len)
|
||||
{
|
||||
struct ble_gap_periodic_adv_params per_params = {0};
|
||||
struct ble_gap_ext_adv_params ext_params = {0};
|
||||
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;
|
||||
}
|
||||
|
||||
err = adv_data_set(ext_data, ext_len, false);
|
||||
if (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;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int per_adv_data_start(const uint8_t *per_data, uint8_t per_len)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = adv_data_set(per_data, per_len, true);
|
||||
if (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;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Periodic advertising started (handle %u)", ADV_HANDLE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int ext_adv_start_without_base(const uint8_t *ext_data, uint8_t ext_len)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = adv_set_configure(ext_data, ext_len);
|
||||
if (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, BASE pending (handle %u)", ADV_HANDLE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int adv_stop(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = ble_gap_periodic_adv_stop(ADV_HANDLE);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop per advertising, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = ble_gap_ext_adv_stop(ADV_HANDLE);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop ext advertising, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Advertising stopped (handle %u)", ADV_HANDLE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int local_public_addr_get(uint8_t addr[6])
|
||||
{
|
||||
int err = ble_hs_id_copy_addr(BLE_ADDR_PUBLIC, addr, NULL);
|
||||
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Local BD address unavailable, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int pa_set_info_transfer(uint16_t conn_handle, const uint8_t peer_addr[6],
|
||||
uint16_t service_data)
|
||||
{
|
||||
(void)peer_addr;
|
||||
|
||||
return ble_gap_periodic_adv_sync_set_info(ADV_HANDLE, conn_handle, service_data);
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 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 "cap_handover.h"
|
||||
|
||||
/* Init/conn parameters are shared with the bluedroid wrapper via cap_handover.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);
|
||||
}
|
||||
|
||||
#define OWN_ADDR_TYPE BLE_OWN_ADDR_PUBLIC
|
||||
|
||||
int ext_scan_start(void)
|
||||
{
|
||||
struct ble_gap_disc_params params = {0};
|
||||
int 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 CAP Acceptor...");
|
||||
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};
|
||||
ble_addr_t dst = {0};
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
# This file was generated using idf.py save-defconfig. It can be edited manually.
|
||||
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
|
||||
#
|
||||
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_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_BLE_FEAT_PERIODIC_ADV_SYNC_TRANSFER=y
|
||||
|
||||
CONFIG_BT_ISO_MAX_CHAN=4
|
||||
|
||||
CONFIG_BT_CAP_INITIATOR=y
|
||||
CONFIG_BT_BAP_UNICAST_CLIENT=y
|
||||
CONFIG_BT_BAP_UNICAST_CLIENT_GROUP_STREAM_COUNT=2
|
||||
CONFIG_BT_BAP_BROADCAST_SOURCE=y
|
||||
CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT=2
|
||||
|
||||
CONFIG_BT_CSIP_SET_COORDINATOR=y
|
||||
CONFIG_BT_BAP_SCAN_DELEGATOR=y
|
||||
CONFIG_BT_BAP_BROADCAST_ASSISTANT=y
|
||||
CONFIG_BT_CAP_COMMANDER=y
|
||||
CONFIG_BT_CAP_HANDOVER=y
|
||||
|
||||
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
|
||||
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
|
||||
# To place LE Audio .bss + control-plane heap in PSRAM, uncomment the
|
||||
# lines below (needs a PSRAM-capable target, e.g. esp32s31/esp32h4).
|
||||
# CONFIG_SPIRAM=y
|
||||
# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y
|
||||
# CONFIG_BT_ISO_BSS_SEG_EXTERNAL_MEMORY=y
|
||||
# CONFIG_BT_ISO_HEAP_EXTERNAL_MEMORY=y
|
||||
# CONFIG_BT_AUDIO_BSS_SEG_EXTERNAL_MEMORY=y
|
||||
# CONFIG_BT_AUDIO_HEAP_EXTERNAL_MEMORY=y
|
||||
|
||||
# Bandwidth-optimized (critical-only) compressed BLE Audio/ISO logging over
|
||||
# UART DMA. Pins shown are for esp32s31 (UART0 TX = GPIO58, 3000000 baud); set
|
||||
# CONFIG_BLE_LOG_PRPH_UART_DMA_* values to the port/pin your capture reads.
|
||||
# CONFIG_BLE_LOG_ENABLED=y
|
||||
# CONFIG_BT_LOG_CRITICAL_ONLY=y
|
||||
# CONFIG_BLE_COMPRESSED_LOG_ENABLE=y
|
||||
# CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE=y
|
||||
# CONFIG_BLE_LOG_PRPH_UART_DMA=y
|
||||
# CONFIG_BLE_LOG_PRPH_UART_DMA_PORT=0
|
||||
# CONFIG_BLE_LOG_PRPH_UART_DMA_BAUD_RATE=3000000
|
||||
# CONFIG_BLE_LOG_PRPH_UART_DMA_TX_IO_NUM=58
|
||||
@@ -0,0 +1,6 @@
|
||||
# Override some defaults so BT stack is enabled
|
||||
# by default in this example
|
||||
|
||||
CONFIG_IDF_TARGET="esp32h4"
|
||||
|
||||
CONFIG_BT_LE_ISO_SUPPORT=y
|
||||
@@ -0,0 +1,6 @@
|
||||
# Override some defaults so BT stack is enabled
|
||||
# by default in this example
|
||||
|
||||
CONFIG_IDF_TARGET="esp32s31"
|
||||
|
||||
CONFIG_BT_LE_ISO_SUPPORT=y
|
||||
@@ -0,0 +1,13 @@
|
||||
# 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_PERIODIC_ADV_SYNC_TRANSFER=y
|
||||
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
|
||||
@@ -24,6 +24,17 @@
|
||||
|
||||
#define CONN_HANDLE_INIT 0xFFFF
|
||||
|
||||
/* Number of sink streams the sample drives towards the Acceptor (one per sink ASE,
|
||||
* so a stereo Acceptor gets one stream per channel).
|
||||
*
|
||||
* The same stream objects are reused for unicast and, after a CAP handover, for
|
||||
* broadcast, so the count is bounded by both the number of sink ASEs that can be
|
||||
* discovered on the Acceptor and the number of broadcast source streams that can
|
||||
* be created locally.
|
||||
*/
|
||||
#define SINK_STREAM_COUNT MIN(CONFIG_BT_BAP_UNICAST_CLIENT_ASE_SNK_COUNT, \
|
||||
CONFIG_BT_BAP_BROADCAST_SRC_STREAM_COUNT)
|
||||
|
||||
#if CONFIG_EXAMPLE_UNICAST
|
||||
#define LOCAL_DEVICE_NAME "CAP Initiator"
|
||||
|
||||
|
||||
@@ -18,20 +18,44 @@ ESP_BLE_AUDIO_BAP_LC3_BROADCAST_PRESET_16_2_1_DEFINE(broadcast_preset_16_2_1,
|
||||
ESP_BLE_AUDIO_CONTEXT_TYPE_UNSPECIFIED);
|
||||
|
||||
static esp_ble_audio_cap_broadcast_source_t *broadcast_source;
|
||||
static esp_ble_audio_cap_stream_t broadcast_stream;
|
||||
static esp_ble_audio_cap_stream_t broadcast_streams[SINK_STREAM_COUNT];
|
||||
|
||||
/* One BIS per channel. The subgroup codec configuration is shared, so the channel
|
||||
* allocation is carried per BIS as Codec Specific Configuration.
|
||||
*/
|
||||
static const esp_ble_audio_location_t bis_locations[] = {
|
||||
ESP_BLE_AUDIO_LOCATION_FRONT_LEFT,
|
||||
ESP_BLE_AUDIO_LOCATION_FRONT_RIGHT,
|
||||
};
|
||||
_Static_assert(ARRAY_SIZE(bis_locations) >= SINK_STREAM_COUNT,
|
||||
"Need one channel allocation per broadcast stream");
|
||||
|
||||
static uint8_t bis_data[SINK_STREAM_COUNT][6];
|
||||
|
||||
static int broadcast_stream_index(const esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
for (size_t i = 0; i < ARRAY_SIZE(broadcast_streams); i++) {
|
||||
if (stream == &broadcast_streams[i].bap_stream) {
|
||||
return (int)i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void broadcast_stream_started_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
esp_ble_audio_cap_stream_t *cap_stream;
|
||||
int err;
|
||||
|
||||
ESP_LOGI(TAG, "[SRC #0] Stream started");
|
||||
ESP_LOGI(TAG, "[SRC #%d] Stream started", broadcast_stream_index(stream));
|
||||
|
||||
cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream);
|
||||
|
||||
err = cap_initiator_tx_register_stream(cap_stream, true);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "[SRC #0] Failed to register TX, err %d", err);
|
||||
ESP_LOGE(TAG, "[SRC #%d] Failed to register TX, err %d",
|
||||
broadcast_stream_index(stream), err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +63,8 @@ static void broadcast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint
|
||||
{
|
||||
esp_ble_audio_cap_stream_t *cap_stream;
|
||||
|
||||
ESP_LOGI(TAG, "[SRC #0] Stream stopped, reason 0x%02x", reason);
|
||||
ESP_LOGI(TAG, "[SRC #%d] Stream stopped, reason 0x%02x",
|
||||
broadcast_stream_index(stream), reason);
|
||||
|
||||
cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream);
|
||||
|
||||
@@ -50,7 +75,8 @@ static void broadcast_stream_disconnected_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
{
|
||||
esp_ble_audio_cap_stream_t *cap_stream;
|
||||
|
||||
ESP_LOGI(TAG, "[SRC #0] ISO disconnected, reason 0x%02x", reason);
|
||||
ESP_LOGI(TAG, "[SRC #%d] ISO disconnected, reason 0x%02x",
|
||||
broadcast_stream_index(stream), reason);
|
||||
|
||||
cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream);
|
||||
|
||||
@@ -135,13 +161,11 @@ static uint8_t *per_adv_data_get(uint8_t *data_len)
|
||||
|
||||
int cap_initiator_broadcast_start(void)
|
||||
{
|
||||
esp_ble_audio_cap_initiator_broadcast_stream_param_t stream_params = {
|
||||
.stream = &broadcast_stream,
|
||||
};
|
||||
esp_ble_audio_cap_initiator_broadcast_stream_param_t stream_params[SINK_STREAM_COUNT] = {0};
|
||||
esp_ble_audio_cap_initiator_broadcast_subgroup_param_t subgroup_param = {
|
||||
.codec_cfg = &broadcast_preset_16_2_1.codec_cfg,
|
||||
.stream_params = &stream_params,
|
||||
.stream_count = 1,
|
||||
.stream_params = stream_params,
|
||||
.stream_count = ARRAY_SIZE(stream_params),
|
||||
};
|
||||
const esp_ble_audio_cap_initiator_broadcast_create_param_t create_param = {
|
||||
.qos = &broadcast_preset_16_2_1.qos,
|
||||
@@ -160,6 +184,22 @@ int cap_initiator_broadcast_start(void)
|
||||
|
||||
ESP_LOGI(TAG, "Creating broadcast source");
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(stream_params); i++) {
|
||||
const esp_ble_audio_location_t loc = bis_locations[i];
|
||||
|
||||
/* LTV: length, type, 4-octet Audio_Channel_Allocation (little endian) */
|
||||
bis_data[i][0] = 5;
|
||||
bis_data[i][1] = ESP_BLE_AUDIO_CODEC_CFG_CHAN_ALLOC;
|
||||
bis_data[i][2] = (uint8_t)loc;
|
||||
bis_data[i][3] = (uint8_t)(loc >> 8);
|
||||
bis_data[i][4] = (uint8_t)(loc >> 16);
|
||||
bis_data[i][5] = (uint8_t)(loc >> 24);
|
||||
|
||||
stream_params[i].stream = &broadcast_streams[i];
|
||||
stream_params[i].data = bis_data[i];
|
||||
stream_params[i].data_len = sizeof(bis_data[i]);
|
||||
}
|
||||
|
||||
err = esp_ble_audio_cap_initiator_broadcast_audio_create(&create_param, &broadcast_source);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to create broadcast source, err %d", err);
|
||||
@@ -212,7 +252,9 @@ end:
|
||||
|
||||
int cap_initiator_broadcast_init(void)
|
||||
{
|
||||
esp_ble_audio_cap_stream_ops_register(&broadcast_stream, &broadcast_stream_ops);
|
||||
for (size_t i = 0; i < ARRAY_SIZE(broadcast_streams); i++) {
|
||||
esp_ble_audio_cap_stream_ops_register(&broadcast_streams[i], &broadcast_stream_ops);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "CAP initiator broadcast initialized");
|
||||
|
||||
|
||||
@@ -13,7 +13,12 @@
|
||||
|
||||
#include "cap_initiator.h"
|
||||
|
||||
static struct tx_stream tx_streams[IS_ENABLED(CONFIG_EXAMPLE_UNICAST) + IS_ENABLED(CONFIG_EXAMPLE_BROADCAST)];
|
||||
/* One slot per stream the sample can transmit on, in every enabled mode: the
|
||||
* unicast sink streams and the broadcast source streams are distinct objects.
|
||||
*/
|
||||
static struct tx_stream tx_streams[SINK_STREAM_COUNT *
|
||||
(IS_ENABLED(CONFIG_EXAMPLE_UNICAST) +
|
||||
IS_ENABLED(CONFIG_EXAMPLE_BROADCAST))];
|
||||
|
||||
static const char *cap_stream_tx_label(const esp_ble_audio_cap_stream_t *cap_stream)
|
||||
{
|
||||
|
||||
@@ -7,20 +7,41 @@
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "cap_initiator.h"
|
||||
|
||||
ESP_BLE_AUDIO_BAP_LC3_UNICAST_PRESET_16_2_1_DEFINE(unicast_preset_16_2_1,
|
||||
/* One preset per sink stream: each ..._DEFINE allocates its own codec configuration
|
||||
* buffer, which is what lets the streams carry different Audio_Channel_Allocation
|
||||
* values. Extend both this list and sink_presets[] to drive more than two channels.
|
||||
*/
|
||||
ESP_BLE_AUDIO_BAP_LC3_UNICAST_PRESET_16_2_1_DEFINE(unicast_preset_left,
|
||||
ESP_BLE_AUDIO_LOCATION_FRONT_LEFT,
|
||||
ESP_BLE_AUDIO_CONTEXT_TYPE_UNSPECIFIED);
|
||||
ESP_BLE_AUDIO_BAP_LC3_UNICAST_PRESET_16_2_1_DEFINE(unicast_preset_right,
|
||||
ESP_BLE_AUDIO_LOCATION_FRONT_RIGHT,
|
||||
ESP_BLE_AUDIO_CONTEXT_TYPE_UNSPECIFIED);
|
||||
/* The return direction carries a single microphone channel. */
|
||||
ESP_BLE_AUDIO_BAP_LC3_UNICAST_PRESET_16_2_1_DEFINE(unicast_preset_mono,
|
||||
ESP_BLE_AUDIO_LOCATION_MONO_AUDIO,
|
||||
ESP_BLE_AUDIO_CONTEXT_TYPE_UNSPECIFIED);
|
||||
|
||||
static esp_ble_audio_bap_lc3_preset_t *const sink_presets[] = {
|
||||
&unicast_preset_left,
|
||||
&unicast_preset_right,
|
||||
};
|
||||
_Static_assert(ARRAY_SIZE(sink_presets) >= SINK_STREAM_COUNT,
|
||||
"Need one preset (one channel allocation) per sink stream");
|
||||
|
||||
static esp_ble_audio_cap_unicast_group_t *unicast_group;
|
||||
|
||||
static struct peer_config {
|
||||
esp_ble_audio_cap_stream_t source_stream;
|
||||
esp_ble_audio_cap_stream_t sink_stream;
|
||||
esp_ble_audio_cap_stream_t sink_streams[SINK_STREAM_COUNT];
|
||||
esp_ble_audio_bap_ep_t *source_ep;
|
||||
esp_ble_audio_bap_ep_t *sink_ep;
|
||||
esp_ble_audio_bap_ep_t *sink_eps[SINK_STREAM_COUNT];
|
||||
size_t sink_ep_count;
|
||||
|
||||
esp_ble_conn_t *conn;
|
||||
uint16_t conn_handle;
|
||||
@@ -39,19 +60,35 @@ static const char *dir_str(esp_ble_audio_dir_t dir)
|
||||
|
||||
static const char *stream_dir_str(const esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
if (stream == &peer.sink_stream.bap_stream) {
|
||||
return "SNK";
|
||||
} else if (stream == &peer.source_stream.bap_stream) {
|
||||
for (size_t i = 0; i < ARRAY_SIZE(peer.sink_streams); i++) {
|
||||
if (stream == &peer.sink_streams[i].bap_stream) {
|
||||
return "SNK";
|
||||
}
|
||||
}
|
||||
|
||||
if (stream == &peer.source_stream.bap_stream) {
|
||||
return "SRC";
|
||||
}
|
||||
|
||||
return "???";
|
||||
}
|
||||
|
||||
static int stream_index(const esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
/* Only one sink and one source per peer in this example. */
|
||||
(void)stream;
|
||||
return 0;
|
||||
/* Index within the pool of its own direction, so logs read as "SNK #0" /
|
||||
* "SNK #1" for the two channels of a stereo Acceptor.
|
||||
*/
|
||||
for (size_t i = 0; i < ARRAY_SIZE(peer.sink_streams); i++) {
|
||||
if (stream == &peer.sink_streams[i].bap_stream) {
|
||||
return (int)i;
|
||||
}
|
||||
}
|
||||
|
||||
if (stream == &peer.source_stream.bap_stream) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool is_tx_stream(esp_ble_audio_bap_stream_t *stream)
|
||||
@@ -97,9 +134,10 @@ static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
ESP_LOGI(TAG, "[%s #%d] Stream started",
|
||||
stream_dir_str(stream), stream_index(stream));
|
||||
|
||||
example_audio_rx_metrics_reset(&rx_metrics);
|
||||
|
||||
if (is_tx_stream(stream)) {
|
||||
/* Only the source stream receives, so resetting the metrics here would let
|
||||
* a sink stream starting later wipe the counters of a running source.
|
||||
*/
|
||||
cap_stream = CONTAINER_OF(stream, esp_ble_audio_cap_stream_t, bap_stream);
|
||||
|
||||
err = cap_initiator_tx_register_stream(cap_stream, false);
|
||||
@@ -107,6 +145,8 @@ static void unicast_stream_started_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
ESP_LOGE(TAG, "[%s #%d] Failed to register TX, err %d",
|
||||
stream_dir_str(stream), stream_index(stream), err);
|
||||
}
|
||||
} else {
|
||||
example_audio_rx_metrics_reset(&rx_metrics);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +246,9 @@ static int discover_sinks(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
esp_ble_audio_cap_stream_ops_register(&peer.sink_stream, &unicast_stream_ops);
|
||||
for (size_t i = 0; i < ARRAY_SIZE(peer.sink_streams); i++) {
|
||||
esp_ble_audio_cap_stream_ops_register(&peer.sink_streams[i], &unicast_stream_ops);
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_unicast_client_discover(peer.conn_handle, ESP_BLE_AUDIO_DIR_SINK);
|
||||
if (err) {
|
||||
@@ -238,28 +280,44 @@ static int discover_sources(void)
|
||||
|
||||
static int unicast_group_create(void)
|
||||
{
|
||||
esp_ble_audio_cap_unicast_group_stream_param_t source_stream_param = {
|
||||
.qos_cfg = &unicast_preset_16_2_1.qos,
|
||||
.stream = &peer.source_stream,
|
||||
};
|
||||
esp_ble_audio_cap_unicast_group_stream_param_t sink_stream_param = {
|
||||
.qos_cfg = &unicast_preset_16_2_1.qos,
|
||||
.stream = &peer.sink_stream,
|
||||
};
|
||||
esp_ble_audio_cap_unicast_group_stream_pair_param_t pair_params = {0};
|
||||
/* The group keeps referencing these while it exists, so they outlive this call. */
|
||||
static esp_ble_audio_cap_unicast_group_stream_param_t source_stream_param;
|
||||
static esp_ble_audio_cap_unicast_group_stream_param_t sink_stream_params[SINK_STREAM_COUNT];
|
||||
static esp_ble_audio_cap_unicast_group_stream_pair_param_t pair_params[SINK_STREAM_COUNT];
|
||||
esp_ble_audio_cap_unicast_group_param_t group_param = {0};
|
||||
size_t pair_count = 0;
|
||||
int err;
|
||||
|
||||
/* One CIS per sink stream. */
|
||||
for (size_t i = 0; i < peer.sink_ep_count; i++) {
|
||||
sink_stream_params[i].qos_cfg = &sink_presets[i]->qos;
|
||||
sink_stream_params[i].stream = &peer.sink_streams[i];
|
||||
|
||||
pair_params[pair_count].rx_param = NULL;
|
||||
pair_params[pair_count].tx_param = &sink_stream_params[i];
|
||||
pair_count++;
|
||||
}
|
||||
|
||||
/* The return direction shares the first CIS, making it bidirectional. */
|
||||
if (peer.source_ep) {
|
||||
pair_params.rx_param = &source_stream_param;
|
||||
source_stream_param.qos_cfg = &unicast_preset_mono.qos;
|
||||
source_stream_param.stream = &peer.source_stream;
|
||||
|
||||
if (pair_count == 0) {
|
||||
pair_params[0].tx_param = NULL;
|
||||
pair_count = 1;
|
||||
}
|
||||
|
||||
pair_params[0].rx_param = &source_stream_param;
|
||||
}
|
||||
|
||||
if (peer.sink_ep) {
|
||||
pair_params.tx_param = &sink_stream_param;
|
||||
if (pair_count == 0) {
|
||||
ESP_LOGW(TAG, "No endpoints available, skip creating unicast group");
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
group_param.params_count = 1;
|
||||
group_param.params = &pair_params;
|
||||
group_param.params_count = pair_count;
|
||||
group_param.params = pair_params;
|
||||
|
||||
err = esp_ble_audio_cap_unicast_group_create(&group_param, &unicast_group);
|
||||
if (err) {
|
||||
@@ -295,15 +353,19 @@ int unicast_group_delete(void)
|
||||
|
||||
static int unicast_audio_start(void)
|
||||
{
|
||||
esp_ble_audio_cap_unicast_audio_start_stream_param_t stream_param[2] = {0};
|
||||
/* codec_cfg is assigned to the stream and has to stay valid while it is
|
||||
* non-idle, so the parameters cannot live on the stack.
|
||||
*/
|
||||
static esp_ble_audio_cap_unicast_audio_start_stream_param_t
|
||||
stream_param[SINK_STREAM_COUNT + 1];
|
||||
esp_ble_audio_cap_unicast_audio_start_param_t param = {0};
|
||||
int err;
|
||||
|
||||
if (peer.sink_ep) {
|
||||
for (size_t i = 0; i < peer.sink_ep_count; i++) {
|
||||
stream_param[param.count].member.member = peer.conn;
|
||||
stream_param[param.count].stream = &peer.sink_stream;
|
||||
stream_param[param.count].ep = peer.sink_ep;
|
||||
stream_param[param.count].codec_cfg = &unicast_preset_16_2_1.codec_cfg;
|
||||
stream_param[param.count].stream = &peer.sink_streams[i];
|
||||
stream_param[param.count].ep = peer.sink_eps[i];
|
||||
stream_param[param.count].codec_cfg = &sink_presets[i]->codec_cfg;
|
||||
param.count++;
|
||||
}
|
||||
|
||||
@@ -311,7 +373,7 @@ static int unicast_audio_start(void)
|
||||
stream_param[param.count].member.member = peer.conn;
|
||||
stream_param[param.count].stream = &peer.source_stream;
|
||||
stream_param[param.count].ep = peer.source_ep;
|
||||
stream_param[param.count].codec_cfg = &unicast_preset_16_2_1.codec_cfg;
|
||||
stream_param[param.count].codec_cfg = &unicast_preset_mono.codec_cfg;
|
||||
param.count++;
|
||||
}
|
||||
|
||||
@@ -383,13 +445,16 @@ static void endpoint_cb(esp_ble_conn_t *conn,
|
||||
{
|
||||
if (dir == ESP_BLE_AUDIO_DIR_SOURCE) {
|
||||
if (peer.source_ep == NULL) {
|
||||
ESP_LOGI(TAG, "[%s] Endpoint discovered", dir_str(dir));
|
||||
ESP_LOGI(TAG, "[%s #0] Endpoint discovered", dir_str(dir));
|
||||
peer.source_ep = ep;
|
||||
}
|
||||
} else if (dir == ESP_BLE_AUDIO_DIR_SINK) {
|
||||
if (peer.sink_ep == NULL) {
|
||||
ESP_LOGI(TAG, "[%s] Endpoint discovered", dir_str(dir));
|
||||
peer.sink_ep = ep;
|
||||
/* Keep every sink endpoint, up to one per stream: a stereo Acceptor
|
||||
* exposes one sink ASE per channel and all of them are used.
|
||||
*/
|
||||
if (peer.sink_ep_count < ARRAY_SIZE(peer.sink_eps)) {
|
||||
ESP_LOGI(TAG, "[%s #%zu] Endpoint discovered", dir_str(dir), peer.sink_ep_count);
|
||||
peer.sink_eps[peer.sink_ep_count++] = ep;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -534,7 +599,8 @@ static void acl_disconnect(esp_ble_audio_gap_app_event_t *event)
|
||||
peer.conn_handle = CONN_HANDLE_INIT;
|
||||
peer.conn = NULL;
|
||||
peer.source_ep = NULL;
|
||||
peer.sink_ep = NULL;
|
||||
memset(peer.sink_eps, 0, sizeof(peer.sink_eps));
|
||||
peer.sink_ep_count = 0;
|
||||
peer.disc_completed = false;
|
||||
peer.mtu_exchanged = false;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ 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_ISO_MAX_CHAN=4
|
||||
|
||||
CONFIG_BT_CAP_INITIATOR=y
|
||||
CONFIG_BT_BAP_UNICAST_CLIENT=y
|
||||
|
||||
@@ -34,6 +34,10 @@ CONFIG_BT_MCTL_LOCAL_PLAYER_REMOTE_CONTROL=y
|
||||
CONFIG_BT_TBS=y
|
||||
CONFIG_BT_TBS_SUPPORTED_FEATURES=3
|
||||
|
||||
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
|
||||
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
|
||||
# To place LE Audio .bss + control-plane heap in PSRAM, uncomment the
|
||||
# lines below (needs a PSRAM-capable target, e.g. esp32s31/esp32h4).
|
||||
# CONFIG_SPIRAM=y
|
||||
@@ -54,7 +58,3 @@ CONFIG_BT_TBS_SUPPORTED_FEATURES=3
|
||||
# CONFIG_BLE_LOG_PRPH_UART_DMA_PORT=0
|
||||
# CONFIG_BLE_LOG_PRPH_UART_DMA_BAUD_RATE=3000000
|
||||
# CONFIG_BLE_LOG_PRPH_UART_DMA_TX_IO_NUM=58
|
||||
|
||||
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
|
||||
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
|
||||
Reference in New Issue
Block a user