mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-01 18:50:34 +03:00
Merge branch 'idf/ble_audio_fixes' into 'master'
fix(ble_audio): Miscellaneous fixes for ISO & LE Audio (stage 2) See merge request espressif/esp-idf!51158
This commit is contained in:
@@ -64,7 +64,7 @@ For `esp32s31`, replace the chip overlay accordingly.
|
||||
6. `ESP_BLE_AUDIO_GAP_EVENT_PA_SYNC` clears `pa_syncing`, cancels discovery, stores `sync_handle`, and calls `esp_ble_audio_bap_broadcast_sink_create()`.
|
||||
7. `base_recv_cb` extracts the subgroup count and BIS index bitfield (masked by `bis_index_mask`); when no Broadcast Assistant is connected, `requested_bis_sync` defaults to `ESP_BLE_AUDIO_BAP_BIS_SYNC_NO_PREF`.
|
||||
8. `syncable_cb` AND-masks the BASE bitfield with the requested mask, copies `TARGET_BROADCAST_CODE` if the BIG is encrypted (unless BASS already supplied one), and calls `esp_ble_audio_bap_broadcast_sink_sync()` with the chosen mask and `streams_p`.
|
||||
9. `stream_started_cb` resets per-stream RX metrics and increments `stream_count_started`; `stream_recv_cb` updates metrics via `example_audio_rx_metrics_on_recv()`. When all streams have stopped, `stream_stopped_cb` deletes the broadcast sink. `pa_sync_lost()` clears the cached `req_recv_state`, deletes any sink, and restarts the scanner.
|
||||
9. `stream_started_cb` resets per-stream RX metrics and increments `stream_count_started`; `stream_recv_cb` updates metrics via `example_audio_rx_metrics_on_recv()`. When all streams have stopped, `stream_stopped_cb` clears `stream_started`; the sink itself is deleted from `broadcast_sink_stopped_cb` (the `stopped` sink callback), which runs once BASS has cleared `bis_sync` — deleting from `stream_stopped_cb` would race `rem_src` while `bis_sync` is still non-zero. `pa_sync_lost()` clears the cached `req_recv_state`, deletes any sink, and restarts the scanner.
|
||||
|
||||
## Expected Log
|
||||
|
||||
@@ -92,6 +92,7 @@ On teardown / sync loss:
|
||||
|
||||
```
|
||||
I (xxx) BAP_BSNK: [SNK #0] Stream stopped, reason 0x... (.../...)
|
||||
I (xxx) BAP_BSNK: Broadcast sink stopped, reason 0x...
|
||||
I (xxx) BAP_BSNK: PA sync lost: sync_handle ... reason 0x...
|
||||
I (xxx) BAP_BSNK: PA sync terminated
|
||||
```
|
||||
@@ -105,4 +106,4 @@ Run [broadcast_source](../broadcast_source/) on a second board. Expected interac
|
||||
3. Source's BIGInfo advertises the BIG as encrypted (broadcast code `"1234"`); sink reports `BIG encrypted`.
|
||||
4. Source starts the BIG and the two BIS streams (`FRONT_LEFT`, `FRONT_RIGHT`); sink calls `esp_ble_audio_bap_broadcast_sink_sync()` with the chosen BIS bitfield and the matching broadcast code.
|
||||
5. Source's TX scheduler keeps pushing SDUs at `preset_active.qos.interval`; sink stream `recv` callbacks deliver the data and update RX metrics.
|
||||
6. Stopping the source (or losing PA sync) triggers `stream_stopped_cb` on the sink, which deletes the broadcast sink and resumes scanning.
|
||||
6. Stopping the source (or losing PA sync) tears down the BIG; `broadcast_sink_stopped_cb` then deletes the sink (after BASS clears `bis_sync`) and scanning resumes.
|
||||
|
||||
@@ -209,23 +209,14 @@ static int bis_sync_req_cb(esp_ble_conn_t *conn,
|
||||
stream_started ? "streaming" : "not streaming");
|
||||
|
||||
if (stream_started && requested_bis_sync == 0) {
|
||||
/* The stream stopped callback will be called as part of this, and
|
||||
* we do not need to wait for any events from the controller. Thus,
|
||||
* when this returns, the `stream_started` is back to false.
|
||||
/* stop() tears down the BIG; broadcast_sink_stopped_cb deletes the
|
||||
* sink after BASS bis_sync has been cleared.
|
||||
*/
|
||||
err = esp_ble_audio_bap_broadcast_sink_stop(broadcast_sink);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop broadcast sink, err %d", err);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to delete broadcast sink, err %d", err);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
broadcast_sink = NULL;
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -319,9 +310,36 @@ static void syncable_cb(esp_ble_audio_bap_broadcast_sink_t *sink,
|
||||
}
|
||||
}
|
||||
|
||||
static void broadcast_sink_stopped_cb(esp_ble_audio_bap_broadcast_sink_t *sink,
|
||||
uint8_t reason)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "Broadcast sink stopped, reason 0x%02x", reason);
|
||||
|
||||
stream_started = false;
|
||||
|
||||
/* Called from big_stopped after update_recv_state_big_cleared(), so
|
||||
* BASS bis_sync is already 0 and rem_src inside delete can succeed.
|
||||
* Do not delete from stream_ops.stopped — that runs before bis_sync clear.
|
||||
*/
|
||||
if (broadcast_sink == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to delete broadcast sink, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
broadcast_sink = NULL;
|
||||
}
|
||||
|
||||
static esp_ble_audio_bap_broadcast_sink_cb_t broadcast_sink_cbs = {
|
||||
.base_recv = base_recv_cb,
|
||||
.syncable = syncable_cb,
|
||||
.stopped = broadcast_sink_stopped_cb,
|
||||
};
|
||||
|
||||
static int stream_index(const esp_ble_audio_bap_stream_t *stream)
|
||||
@@ -352,21 +370,14 @@ static void stream_started_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
|
||||
static void stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "[SNK #%d] Stream stopped, reason 0x%02x (%u/%u)",
|
||||
stream_index(stream), reason, stream_count_stopped, stream_count);
|
||||
|
||||
if (++stream_count_stopped == stream_count) {
|
||||
stream_started = false;
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to delete broadcast sink, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
broadcast_sink = NULL;
|
||||
/* Sink delete is deferred to broadcast_sink_stopped_cb (after BASS
|
||||
* bis_sync clear). Deleting here races rem_src and leaves WRNs.
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,8 +61,8 @@ For `esp32s31`, replace the chip overlay accordingly.
|
||||
3. On a connectable match `ext_scan_recv` calls `ext_scan_stop` and then `conn_create` (NimBLE: `ble_gap_connect`; Bluedroid: `esp_ble_gattc_aux_open`); the resulting `ESP_BLE_AUDIO_GAP_EVENT_ACL_CONNECT` saves the handle and calls `pairing_start` (NimBLE: `ble_gap_security_initiate`; Bluedroid: `esp_ble_set_encryption`).
|
||||
4. After the security-change event the client triggers `exchange_mtu`. On NimBLE this issues `ble_gattc_exchange_mtu`; on Bluedroid the GATTC adapter auto-configures MTU inside `BTA_GATTC_Enh_Open`, so `exchange_mtu` drives `esp_ble_audio_gattc_disc_start` directly. `gatt_mtu_change` then calls `esp_ble_audio_gattc_disc_start` (the retry is idempotent — `-EALREADY` is treated as success), and once both MTU and discovery are reported `discover_sinks` runs.
|
||||
5. `discover_cb` chains sink discovery into `discover_sources`; `endpoint_cb` records each EP into the next free `sinks[]`/`sources[]` slot, and source completion kicks `configure_stream`, which configures every EP with `unicast_preset.codec_cfg`.
|
||||
6. After the last `config_cb`, `create_group` builds `pair_params` (sinks first, then sources) with `ESP_BLE_ISO_PACKING_SEQUENTIAL` and calls `esp_ble_audio_bap_unicast_group_create`, then `set_stream_qos`.
|
||||
7. When `is_all_stream_qos_set` returns true `enable_stream` walks every stream; on the final `enable_cb`, `connect_stream` ISO-connects each pair (sink first if available); when all are up `start_stream` starts only source streams. Sink streams are started by the server, and `stream_started_cb` calls `stream_tx_register` to begin TX.
|
||||
6. `config_cb` configures each EP per Config CP ACK until all are done (`all_config_acked`); `create_group` (building `pair_params`, sinks first then sources, with `ESP_BLE_ISO_PACKING_SEQUENTIAL`) and `set_stream_qos` then run from `try_create_group_and_qos`, which waits for every EP to reach codec-configured (`stream_configured_cb` / `ops.configured`) — the CP ACK can arrive before the ASE state change, so driving QoS off the ACK alone races the last EP still in idle.
|
||||
7. `enable_stream` runs once every EP reports qos-configured (`stream_qos_set_cb` / `ops.qos_set`), not off the QoS CP ACK; `connect_stream` ISO-connects each pair (sink first) once every EP reports enabling (`stream_enabled_cb` / `ops.enabled`), not off the Enable CP ACK. (The ACK callbacks `qos_cb` / `enable_cb` only handle rejects — an ACK can outrun the ASE state change.) When all are up `start_stream` starts only source streams. Sink streams are started by the server, and `stream_started_cb` calls `stream_tx_register` to begin TX.
|
||||
|
||||
## Expected Log
|
||||
|
||||
|
||||
@@ -59,6 +59,12 @@ static size_t configured_sink_stream_count;
|
||||
static size_t configured_source_stream_count;
|
||||
#define configured_stream_count (configured_sink_stream_count + configured_source_stream_count)
|
||||
|
||||
/* Number of streams that have entered codec-configured (ops.configured).
|
||||
* CP config ACK can arrive while the EP is still idle; QoS must wait for this.
|
||||
*/
|
||||
static size_t codec_configured_count;
|
||||
static bool all_config_acked;
|
||||
|
||||
static struct stream_pair_state {
|
||||
esp_ble_audio_bap_stream_t *sink_stream;
|
||||
esp_ble_audio_bap_stream_t *source_stream;
|
||||
@@ -147,6 +153,8 @@ static void reset_stream_state(void)
|
||||
{
|
||||
configured_sink_stream_count = 0;
|
||||
configured_source_stream_count = 0;
|
||||
codec_configured_count = 0;
|
||||
all_config_acked = false;
|
||||
|
||||
reset_stream_pair_state();
|
||||
|
||||
@@ -388,6 +396,51 @@ static int set_stream_qos(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void try_create_group_and_qos(void)
|
||||
{
|
||||
uint8_t stream_count = 0;
|
||||
int err;
|
||||
|
||||
/* Wait until every successful Config CP has a matching ASE state
|
||||
* notification (ops.configured → EP codec-configured). Calling
|
||||
* qos from config_cb alone races the last stream still in idle.
|
||||
*/
|
||||
if (!all_config_acked || unicast_group != NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (configured_stream_count == 0) {
|
||||
ESP_LOGW(TAG, "No streams were configured");
|
||||
return;
|
||||
}
|
||||
|
||||
if (codec_configured_count < configured_stream_count) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(sinks); i++) {
|
||||
if (sinks[i].configured == ASCS_RSP_SUCCESS) {
|
||||
streams[stream_count++] = &sinks[i].stream;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(sources); i++) {
|
||||
if (sources[i].configured == ASCS_RSP_SUCCESS) {
|
||||
streams[stream_count++] = &sources[i].stream;
|
||||
}
|
||||
}
|
||||
|
||||
err = create_group();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = set_stream_qos();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void stream_qos_set(esp_ble_audio_bap_stream_t *stream, bool success)
|
||||
{
|
||||
for (size_t i = 0; i < ARRAY_SIZE(sinks); i++) {
|
||||
@@ -682,18 +735,35 @@ static void stream_configured_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
ESP_LOGI(TAG, "[%s #%d] Stream configured, QoS preference:",
|
||||
stream_dir_str(stream), stream_index(stream));
|
||||
example_print_qos_pref(TAG, pref);
|
||||
|
||||
codec_configured_count++;
|
||||
|
||||
try_create_group_and_qos();
|
||||
}
|
||||
|
||||
static void stream_qos_set_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
/* QoS set is also reported by qos_cb; skip the duplicate log here. */
|
||||
(void)stream;
|
||||
/* ASE entered qos-configured — safe to enable. */
|
||||
stream_qos_set(stream, true);
|
||||
|
||||
if (is_all_stream_qos_set()) {
|
||||
enable_stream();
|
||||
}
|
||||
}
|
||||
|
||||
static void stream_enabled_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
{
|
||||
/* Enabled is also reported by enable_cb; skip the duplicate log here. */
|
||||
(void)stream;
|
||||
bool ret;
|
||||
|
||||
/* ASE entered enabling — safe to connect after all enables complete. */
|
||||
stream_enabled(stream, true);
|
||||
|
||||
ret = enable_stream();
|
||||
if (ret == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
connect_stream();
|
||||
}
|
||||
|
||||
static void stream_connected_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
@@ -843,9 +913,7 @@ static void config_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_bap_ascs_rsp_code_t rsp_code,
|
||||
esp_ble_audio_bap_ascs_reason_t reason)
|
||||
{
|
||||
uint8_t stream_count;
|
||||
bool ret;
|
||||
int err;
|
||||
|
||||
log_rsp("Config", stream, rsp_code, reason);
|
||||
|
||||
@@ -856,34 +924,9 @@ static void config_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
return;
|
||||
}
|
||||
|
||||
if (configured_stream_count == 0) {
|
||||
ESP_LOGW(TAG, "No streams were configured");
|
||||
return;
|
||||
}
|
||||
all_config_acked = true;
|
||||
|
||||
stream_count = 0;
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(sinks); i++) {
|
||||
if (sinks[i].configured == ASCS_RSP_SUCCESS) {
|
||||
streams[stream_count++] = &sinks[i].stream;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(sources); i++) {
|
||||
if (sources[i].configured == ASCS_RSP_SUCCESS) {
|
||||
streams[stream_count++] = &sources[i].stream;
|
||||
}
|
||||
}
|
||||
|
||||
err = create_group();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = set_stream_qos();
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
try_create_group_and_qos();
|
||||
}
|
||||
|
||||
static void qos_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
@@ -892,10 +935,14 @@ static void qos_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
{
|
||||
log_rsp("QoS", stream, rsp_code, reason);
|
||||
|
||||
stream_qos_set(stream, rsp_code == ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_SUCCESS);
|
||||
|
||||
if (is_all_stream_qos_set()) {
|
||||
enable_stream();
|
||||
/* Mark failure from CP reject. Success waits for ops.qos_set (ASE
|
||||
* qos-configured) — enabling on CP ACK alone races codec-configured.
|
||||
*/
|
||||
if (rsp_code != ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_SUCCESS) {
|
||||
stream_qos_set(stream, false);
|
||||
if (is_all_stream_qos_set()) {
|
||||
enable_stream();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -903,18 +950,19 @@ static void enable_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
esp_ble_audio_bap_ascs_rsp_code_t rsp_code,
|
||||
esp_ble_audio_bap_ascs_reason_t reason)
|
||||
{
|
||||
bool ret;
|
||||
|
||||
log_rsp("Enable", stream, rsp_code, reason);
|
||||
|
||||
stream_enabled(stream, rsp_code == ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_SUCCESS);
|
||||
/* Same as QoS: connect needs ENABLING from ASE state ntf (ops.enabled). */
|
||||
if (rsp_code != ESP_BLE_AUDIO_BAP_ASCS_RSP_CODE_SUCCESS) {
|
||||
bool ret;
|
||||
|
||||
ret = enable_stream();
|
||||
if (ret == false) {
|
||||
return;
|
||||
stream_enabled(stream, false);
|
||||
|
||||
ret = enable_stream();
|
||||
if (ret) {
|
||||
connect_stream();
|
||||
}
|
||||
}
|
||||
|
||||
connect_stream();
|
||||
}
|
||||
|
||||
static void start_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
|
||||
@@ -101,6 +101,12 @@ int stream_tx_register(esp_ble_audio_bap_stream_t *stream)
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) {
|
||||
if (tx_streams[i].stream == stream) {
|
||||
return 0; /* already armed */
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(tx_streams); i++) {
|
||||
if (tx_streams[i].stream == NULL) {
|
||||
if (stream->qos == NULL || stream->qos->sdu == 0) {
|
||||
|
||||
@@ -128,6 +128,7 @@ Source (SRC) Assistant (ASS) Acceptor (ACC)
|
||||
| `FLAG_BROADCAST_CODE_RECEIVED` | `broadcast_code_cb` (BASS Set Broadcast Code); self-scan local code | `broadcast_sink_reset` |
|
||||
| `FLAG_BROADCAST_SYNC_REQUESTED` | `bis_sync_req_cb` (bitmap ≠ 0); self-scan PA match | `bis_sync_req_cb` (bitmap = 0); `broadcast_sink_reset` |
|
||||
| `FLAG_BROADCAST_RESYNC_PENDING` | `bis_sync_req_cb` before `_stop` (bitmap change while streaming) | `stream_stopped_cb` after driving the re-sync; `_stop` failure; `broadcast_sink_reset` |
|
||||
| `FLAG_BROADCAST_STOP_PENDING` | `broadcast_stream_stopped_cb` (last BIS stops) | `stopped_cb`; `broadcast_sink_reset` |
|
||||
| `FLAG_BROADCAST_SYNCING` | `check_sync_broadcast` after `_sync` returns OK | `stream_started_cb`; `stream_stopped_cb` |
|
||||
| `FLAG_BROADCAST_SYNCED` | `stream_started_cb` | `stream_stopped_cb` |
|
||||
|
||||
@@ -161,9 +162,9 @@ BASE_RECEIVED && BROADCAST_SYNCABLE
|
||||
+-----+-----+ |
|
||||
| | BIG drops while PA gone
|
||||
| v
|
||||
+---------> stream_stopped_cb + !PA_SYNCED
|
||||
+--> stream_stopped_cb (set STOP_PENDING, !PA_SYNCED)
|
||||
|
|
||||
| _delete + broadcast_sink_reset
|
||||
| stopped_cb: _delete + broadcast_sink_reset
|
||||
v
|
||||
[end]
|
||||
```
|
||||
@@ -171,8 +172,8 @@ BASE_RECEIVED && BROADCAST_SYNCABLE
|
||||
Key invariants:
|
||||
|
||||
- **PA loss does NOT tear down a running BIS.** Per BASS § 3.2.1.6 / § 3.2.1.9, `PA_Sync_State` and `BIS_Sync_State` are independent. While BIS is streaming/syncing, `broadcast_pa_lost` only notifies the assistant (`PA_Sync_State = 0x00`) and clears PA-only local state (`sync_handle`, `FLAG_PA_SYNCED`); the BIG keeps running and audio continues to flow.
|
||||
- **PA loss with BIS idle tears down the sink.** The sink is bound to the now-dead sync handle and its cached BASE / BIGInfo are stale. `broadcast_pa_lost` calls `_delete` and clears `FLAG_BASE_RECEIVED` / `FLAG_BROADCAST_SYNCABLE` / `FLAG_BROADCAST_CODE_REQUIRED`. The assistant's subscription (`requested_bis_sync`, `FLAG_BROADCAST_SYNC_REQUESTED`, `FLAG_BROADCAST_CODE_RECEIVED`) is preserved so the next PA sync re-creates a fresh sink and resumes streaming.
|
||||
- **Sink deletion happens in `stream_stopped_cb` when both PA and BIS are gone.** Triggers: assistant unsubscribes via `Modify Source bis_sync = 0`, or the broadcaster stops the BIG while PA is already gone.
|
||||
- **PA loss with BIS idle tears down the sink.** The sink is bound to the now-dead sync handle and its cached BASE / BIGInfo are stale. `broadcast_pa_lost` calls `_delete` (unless `FLAG_BROADCAST_STOP_PENDING` is set, in which case `stopped_cb` deletes after `bis_sync` clear) and clears `FLAG_BASE_RECEIVED` / `FLAG_BROADCAST_SYNCABLE` / `FLAG_BROADCAST_CODE_REQUIRED`. The assistant's subscription (`requested_bis_sync`, `FLAG_BROADCAST_SYNC_REQUESTED`, `FLAG_BROADCAST_CODE_RECEIVED`) is preserved so the next PA sync re-creates a fresh sink and resumes streaming.
|
||||
- **Sink deletion happens in `stopped_cb` (the `stopped` sink callback) once BASS has cleared `bis_sync`, when PA is gone.** `broadcast_stream_stopped_cb` of the last BIS only sets `FLAG_BROADCAST_STOP_PENDING` (and re-`_sync`s on a pending bitmap change) — it does not delete, because deleting there races `rem_src` while `bis_sync` is still non-zero. Triggers: assistant unsubscribes via `Modify Source bis_sync = 0`, or the broadcaster stops the BIG while PA is already gone.
|
||||
- `bis_sync_req_cb` going `X → 0` (Assistant pause) only issues `_stop`, never `_delete`. Going `X → Y` (BIS bitmap switch) likewise only `_stop`s; the next `check_sync_broadcast` (called from `stream_stopped_cb` when PA still synced) re-`_sync`s the same object.
|
||||
- `pa_sync_term_req_cb` issues the HCI Periodic Advertising Terminate Sync but does **not** clear `broadcast_sink.sync_handle`. Cleanup runs from `BLE_GAP_EVENT_PERIODIC_SYNC_LOST` → `broadcast_pa_lost`. Resetting the handle early would make that gate miss.
|
||||
|
||||
@@ -181,17 +182,17 @@ Key invariants:
|
||||
| Event | Action |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
|
||||
| **PA lost while BIS active** (broadcaster moved mid-stream) | Set `PA_Sync_State = 0x00`; clear PA-only state (`sync_handle`, `FLAG_PA_SYNCED`). Sink + BIS untouched; BIG keeps running. |
|
||||
| **PA lost while BIS idle** (Assistant `PA_Sync = 0`, or PA dropped after Assistant paused BIS) | Set `PA_Sync_State = 0x00` (skipped if BASS already updated it in-place); clear PA-only state; `_delete` the sink and clear `FLAG_BASE_RECEIVED` / `FLAG_BROADCAST_SYNCABLE` / `FLAG_BROADCAST_CODE_REQUIRED`. The next PA sync starts from a clean sink and lets the lib redeliver BASE / BIGInfo. |
|
||||
| **PA lost while BIS idle** (Assistant `PA_Sync = 0`, or PA dropped after Assistant paused BIS) | Set `PA_Sync_State = 0x00` (skipped if BASS already updated it in-place); clear PA-only state; unless `FLAG_BROADCAST_STOP_PENDING` is set (BIG teardown in flight — `stopped_cb` deletes after `bis_sync` clear), `_delete` the sink and clear `FLAG_BASE_RECEIVED` / `FLAG_BROADCAST_SYNCABLE` / `FLAG_BROADCAST_CODE_REQUIRED`. The next PA sync starts from a clean sink and lets the lib redeliver BASE / BIGInfo. |
|
||||
| **Modify Source `bis_sync = 0`** (PA still synced) | `bis_sync_req_cb` clears `FLAG_BROADCAST_SYNC_REQUESTED` then `_stop`s the BIG. Sink retained. |
|
||||
| **Modify Source bitmap change** (PA still synced, streaming) | Update `requested_bis_sync` + `FLAG_BROADCAST_SYNC_REQUESTED` + set `FLAG_BROADCAST_RESYNC_PENDING`, then `_stop`. `stream_stopped_cb` clears the flag and re-`_sync`s with the new bitmap. |
|
||||
| **BIG drops while PA still synced** (e.g. broadcaster pause) | `stream_stopped_cb` clears SYNCED/SYNCING and exposes the loss via `BIS_Sync_State`. `FLAG_BROADCAST_RESYNC_PENDING` is not set, so no auto-retry — per BASS § 3.2.1.9 the assistant drives recovery via Modify Source. |
|
||||
| **BIG drops after PA lost** (broadcaster turned off) | `stream_stopped_cb` of the last active stream sees `!PA_SYNCED` → `_delete` + `broadcast_sink_reset`. Multi-BIS: earlier callbacks just decrement `active_streams` so `_delete` is not called while the sink is still in use. |
|
||||
| **BIG drops after PA lost** (broadcaster turned off) | `broadcast_stream_stopped_cb` of the last active stream sees `!PA_SYNCED` and sets `FLAG_BROADCAST_STOP_PENDING`; `stopped_cb` then runs `_delete` + `broadcast_sink_reset` once BASS has cleared `bis_sync`. Multi-BIS: earlier callbacks just decrement `active_streams` so the flag is not set while the sink is still in use. |
|
||||
| **Assistant Remove Source** | Spec allows only when BIS not synced; lib handles, app sees no special event. |
|
||||
|
||||
Two recurring patterns that drive the above behavior:
|
||||
|
||||
- **Update local state before calling `_stop`/`_delete`.** The lib may fire `stream_stopped_cb` synchronously from within `_stop`, so the callback must see the post-stop state. Applies in `bis_sync_req_cb` (updates `requested_bis_sync` + flag before `_stop`) and `broadcast_pa_lost` (no longer calls `_stop`).
|
||||
- **Sink lifetime is BIS-driven, not PA-driven.** Sink is created on first PA sync and deleted only when the BIG itself stops and PA is also gone. This matches BASS spec's independent PA/BIS state model.
|
||||
- **Sink lifetime is BIS-driven, not PA-driven.** Sink is created on first PA sync and deleted (from `stopped_cb`, after BASS clears `bis_sync`) only when the BIG itself stops and PA is also gone. This matches BASS spec's independent PA/BIS state model.
|
||||
|
||||
### Multi-BIS (stereo) configuration
|
||||
|
||||
@@ -290,6 +291,7 @@ On PA sync loss:
|
||||
|
||||
```
|
||||
I (xxx) CAP_ACC: [SNK #0] Stream stopped, reason 0x...
|
||||
I (xxx) CAP_ACC: Broadcast sink stopped, reason 0x...
|
||||
I (xxx) CAP_ACC: PA sync lost: sync_handle ... reason 0x...
|
||||
I (xxx) CAP_ACC: Scanning for broadcast source...
|
||||
```
|
||||
|
||||
@@ -29,6 +29,11 @@ enum broadcast_flag {
|
||||
FLAG_BROADCAST_SYNCING,
|
||||
FLAG_BROADCAST_SYNCED,
|
||||
FLAG_BROADCAST_RESYNC_PENDING,
|
||||
/* Set when the last BIS stream stops; cleared in stopped_cb.
|
||||
* Bridges the window where EPs are IDLE but big_stopped has not yet cleared
|
||||
* BASS bis_sync — pa_lost must not rem_src/delete during that window.
|
||||
*/
|
||||
FLAG_BROADCAST_STOP_PENDING,
|
||||
FLAG_BASE_RECEIVED,
|
||||
FLAG_PA_SYNCING,
|
||||
FLAG_PA_SYNCED,
|
||||
@@ -201,6 +206,7 @@ static void broadcast_sink_reset(void)
|
||||
flag_clear(FLAG_BROADCAST_CODE_RECEIVED);
|
||||
flag_clear(FLAG_BROADCAST_SYNC_REQUESTED);
|
||||
flag_clear(FLAG_BROADCAST_RESYNC_PENDING);
|
||||
flag_clear(FLAG_BROADCAST_STOP_PENDING);
|
||||
flag_clear(FLAG_SCANNING);
|
||||
|
||||
#if CONFIG_EXAMPLE_SCAN_SELF
|
||||
@@ -330,8 +336,6 @@ static void broadcast_stream_started_cb(esp_ble_audio_bap_stream_t *stream)
|
||||
|
||||
static void broadcast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "[SNK #%u] Stream stopped, reason 0x%02x",
|
||||
broadcast_stream_idx(stream), reason);
|
||||
|
||||
@@ -349,21 +353,14 @@ static void broadcast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint
|
||||
|
||||
flag_clear(FLAG_BROADCAST_SYNCING);
|
||||
flag_clear(FLAG_BROADCAST_SYNCED);
|
||||
/* Mark BIG teardown in progress until stopped_cb runs
|
||||
* (after update_recv_state_big_cleared). Do not delete here — that
|
||||
* races rem_src while bis_sync is still non-zero.
|
||||
*/
|
||||
flag_set(FLAG_BROADCAST_STOP_PENDING);
|
||||
|
||||
if (flag_test(FLAG_PA_SYNCED) == false) {
|
||||
/* Both PA and BIS are gone — no path to recover BIGInfo, so the
|
||||
* sink can no longer drive a new BIG sync. Delete it and clear all
|
||||
* state.
|
||||
*/
|
||||
if (broadcast_sink.sink) {
|
||||
err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink.sink);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to delete broadcast sink, err %d", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
broadcast_sink_reset();
|
||||
/* Both PA and BIS are gone — stopped_cb deletes. */
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -378,6 +375,35 @@ static void broadcast_stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint
|
||||
}
|
||||
}
|
||||
|
||||
static void stopped_cb(esp_ble_audio_bap_broadcast_sink_t *sink,
|
||||
uint8_t reason)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "Broadcast sink stopped, reason 0x%02x", reason);
|
||||
|
||||
flag_clear(FLAG_BROADCAST_STOP_PENDING);
|
||||
|
||||
/* Keep the sink when PA is still synced (assistant pause, BIS bitmap
|
||||
* resync, or spontaneous BIG drop with PA alive).
|
||||
*/
|
||||
if (flag_test(FLAG_PA_SYNCED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (broadcast_sink.sink == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink.sink);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to delete broadcast sink, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
broadcast_sink_reset();
|
||||
}
|
||||
|
||||
static void broadcast_stream_recv_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
const esp_ble_iso_recv_info_t *info,
|
||||
const uint8_t *data, uint16_t len)
|
||||
@@ -740,14 +766,15 @@ void broadcast_pa_lost(uint16_t sync_handle)
|
||||
flag_clear(FLAG_PA_SYNCED);
|
||||
flag_clear(FLAG_PA_SYNCING);
|
||||
|
||||
/* BIS still active → BIG keeps running per § 3.2.1.9, leave the sink;
|
||||
* stream_stopped_cb will tear it down when BIS eventually stops.
|
||||
/* BIS still active or BIG teardown pending → leave the sink;
|
||||
* stopped_cb will tear it down when bis_sync is clear.
|
||||
* Otherwise the sink is bound to a dead PA handle and its cached
|
||||
* BASE / BIGInfo are stale: delete the sink and clear the PA-derived
|
||||
* flags so the next PA sync creates a fresh sink and lets the lib
|
||||
* redeliver BASE / BIGInfo.
|
||||
*/
|
||||
if (flag_test(FLAG_BROADCAST_SYNCING) || flag_test(FLAG_BROADCAST_SYNCED)) {
|
||||
if (flag_test(FLAG_BROADCAST_SYNCING) || flag_test(FLAG_BROADCAST_SYNCED) ||
|
||||
flag_test(FLAG_BROADCAST_STOP_PENDING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -789,6 +816,7 @@ int cap_acceptor_broadcast_init(void)
|
||||
static esp_ble_audio_bap_broadcast_sink_cb_t broadcast_sink_cbs = {
|
||||
.base_recv = base_recv_cb,
|
||||
.syncable = syncable_cb,
|
||||
.stopped = stopped_cb,
|
||||
};
|
||||
static esp_ble_audio_bap_stream_ops_t broadcast_stream_ops = {
|
||||
.started = broadcast_stream_started_cb,
|
||||
|
||||
@@ -59,7 +59,7 @@ For `esp32s31`, replace the chip overlay accordingly.
|
||||
5. On `PA_SYNC` success, cancel scanning, log the sync handle, and create the broadcast sink for the sync handle and broadcast ID.
|
||||
6. The `base_recv` callback extracts the BIS index bitfield masked by the available stream count; the `syncable` callback then calls `esp_ble_audio_bap_broadcast_sink_sync` with the stream pointer array.
|
||||
7. Per-stream `started` callback resets RX metrics; `recv` callback feeds each SDU into `example_audio_rx_metrics_on_recv` (tracking valid/error/lost/zero-length counts); `stopped` logs the reason.
|
||||
8. On `PA_SYNC_LOST` matching the active sync handle, delete the broadcast sink and re-enter scanning.
|
||||
8. On `PA_SYNC_LOST` matching the active sync handle, the sink is deleted — by `broadcast_sink_stopped_cb` once BASS clears `bis_sync` (deleting from `stream_stopped_cb` races `rem_src` while `bis_sync` is still set), or directly here if the sink never reached BIG sync — and scanning resumes.
|
||||
|
||||
## Expected Log
|
||||
|
||||
@@ -90,6 +90,7 @@ I (xxx) TMAP_BMR: [SNK #0] Stream started
|
||||
Stop / sync loss:
|
||||
```
|
||||
I (xxx) TMAP_BMR: [SNK #0] Stream stopped, reason 0x..
|
||||
I (xxx) TMAP_BMR: Broadcast sink stopped, reason 0x..
|
||||
I (xxx) TMAP_BMR: PA sync lost: sync_handle .. reason 0x..
|
||||
I (xxx) TMAP_BMR: PA sync .. lost with reason ..
|
||||
```
|
||||
|
||||
@@ -21,6 +21,10 @@ static bool tmap_bms_found;
|
||||
|
||||
static esp_ble_audio_bap_broadcast_sink_t *broadcast_sink;
|
||||
static uint32_t bcast_id;
|
||||
/* True after a BIS stream stops until broadcast_sink_stopped_cb runs —
|
||||
* pa_lost must not delete in that window (EPs idle, bis_sync not yet cleared).
|
||||
*/
|
||||
static bool stop_pending;
|
||||
|
||||
static esp_ble_audio_bap_stream_t streams[CONFIG_BT_BAP_BROADCAST_SNK_STREAM_COUNT];
|
||||
static esp_ble_audio_bap_stream_t *streams_p[ARRAY_SIZE(streams)];
|
||||
@@ -72,6 +76,11 @@ static void stream_stopped_cb(esp_ble_audio_bap_stream_t *stream, uint8_t reason
|
||||
{
|
||||
ESP_LOGI(TAG, "[SNK #%d] Stream stopped, reason 0x%02x",
|
||||
stream_index(stream), reason);
|
||||
/* Sink delete is deferred to broadcast_sink_stopped_cb (after BASS
|
||||
* bis_sync clear). Deleting while EPs are idle but bis_sync is still
|
||||
* set races rem_src and leaves WRNs.
|
||||
*/
|
||||
stop_pending = true;
|
||||
}
|
||||
|
||||
static void stream_recv_cb(esp_ble_audio_bap_stream_t *stream,
|
||||
@@ -129,9 +138,35 @@ static void syncable_cb(esp_ble_audio_bap_broadcast_sink_t *sink,
|
||||
}
|
||||
}
|
||||
|
||||
static void broadcast_sink_stopped_cb(esp_ble_audio_bap_broadcast_sink_t *sink,
|
||||
uint8_t reason)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "Broadcast sink stopped, reason 0x%02x", reason);
|
||||
|
||||
stop_pending = false;
|
||||
|
||||
/* Called from big_stopped after update_recv_state_big_cleared(), so
|
||||
* BASS bis_sync is already 0 and rem_src inside delete can succeed.
|
||||
*/
|
||||
if (broadcast_sink == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to delete broadcast sink, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
broadcast_sink = NULL;
|
||||
}
|
||||
|
||||
static esp_ble_audio_bap_broadcast_sink_cb_t broadcast_sink_cbs = {
|
||||
.base_recv = base_recv_cb,
|
||||
.syncable = syncable_cb,
|
||||
.stopped = broadcast_sink_stopped_cb,
|
||||
};
|
||||
|
||||
static esp_ble_audio_bap_scan_delegator_cb_t scan_delegator_cbs;
|
||||
@@ -254,6 +289,8 @@ void bap_broadcast_pa_sync(esp_ble_audio_gap_app_event_t *event)
|
||||
|
||||
void bap_broadcast_pa_lost(esp_ble_audio_gap_app_event_t *event)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
if (sync_handle == event->pa_sync_lost.sync_handle) {
|
||||
ESP_LOGI(TAG, "PA sync %u lost with reason %u",
|
||||
sync_handle, event->pa_sync_lost.reason);
|
||||
@@ -261,9 +298,17 @@ void bap_broadcast_pa_lost(esp_ble_audio_gap_app_event_t *event)
|
||||
sync_handle = PA_SYNC_HANDLE_INIT;
|
||||
pa_syncing = false; /* allow the rescan below to sync a fresh broadcaster */
|
||||
|
||||
if (broadcast_sink != NULL) {
|
||||
esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink);
|
||||
broadcast_sink = NULL;
|
||||
/* Prefer delete from broadcast_sink_stopped_cb after bis_sync clear.
|
||||
* Skip while BIG teardown is in flight; if the sink never reached
|
||||
* BIG sync, delete here.
|
||||
*/
|
||||
if (broadcast_sink != NULL && !stop_pending) {
|
||||
err = esp_ble_audio_bap_broadcast_sink_delete(broadcast_sink);
|
||||
if (err) {
|
||||
ESP_LOGW(TAG, "Sink delete deferred to stopped_cb, err %d", err);
|
||||
} else {
|
||||
broadcast_sink = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
bap_broadcast_sink_scan();
|
||||
|
||||
@@ -33,7 +33,11 @@ static uint8_t codec_meta[] =
|
||||
static const esp_ble_audio_codec_cap_t lc3_codec_cap =
|
||||
ESP_BLE_AUDIO_CODEC_CAP_LC3(codec_data, codec_meta);
|
||||
|
||||
static esp_ble_audio_pacs_cap_t cap = {
|
||||
static esp_ble_audio_pacs_cap_t cap_sink = {
|
||||
.codec_cap = &lc3_codec_cap,
|
||||
};
|
||||
|
||||
static esp_ble_audio_pacs_cap_t cap_source = {
|
||||
.codec_cap = &lc3_codec_cap,
|
||||
};
|
||||
|
||||
@@ -502,7 +506,7 @@ int bap_unicast_sr_init(void)
|
||||
|
||||
#if CONFIG_BT_PAC_SNK
|
||||
/* Register CT required capabilities */
|
||||
err = esp_ble_audio_pacs_cap_register(ESP_BLE_AUDIO_DIR_SINK, &cap);
|
||||
err = esp_ble_audio_pacs_cap_register(ESP_BLE_AUDIO_DIR_SINK, &cap_sink);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register pacs capabilities, err %d", err);
|
||||
return err;
|
||||
@@ -529,7 +533,7 @@ int bap_unicast_sr_init(void)
|
||||
|
||||
#if CONFIG_BT_PAC_SRC
|
||||
/* Register CT required capabilities */
|
||||
err = esp_ble_audio_pacs_cap_register(ESP_BLE_AUDIO_DIR_SOURCE, &cap);
|
||||
err = esp_ble_audio_pacs_cap_register(ESP_BLE_AUDIO_DIR_SOURCE, &cap_source);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register pacs capabilities, err %d", err);
|
||||
return err;
|
||||
|
||||
Reference in New Issue
Block a user