diff --git a/components/bt/esp_ble_iso/Kconfig.in b/components/bt/esp_ble_iso/Kconfig.in index 4e977e7718d..8de1b82ca46 100644 --- a/components/bt/esp_ble_iso/Kconfig.in +++ b/components/bt/esp_ble_iso/Kconfig.in @@ -104,6 +104,40 @@ if BT_ISO endif # BT_ISO_BROADCAST + config BT_ISO_DISPATCH_MONITOR + bool "Monitor ISO task callback dispatch latency" + depends on !BT_ISO_NO_LOG + default n + help + Time every callback dispatched by the ISO task and keep per-event- + type stats (count / max / slow-count), logged periodically and at + deinit. Used to find callbacks that run long enough to delay the + latency-critical ISO data path. + + Off by default: it adds per-dispatch timing overhead, and the + measurement is wall-clock, so callbacks that log over UART read as + "slow". Enable only while profiling. + + config BT_ISO_DISPATCH_THRESHOLD_US + int "ISO dispatch threshold (in microseconds)" + depends on BT_ISO_DISPATCH_MONITOR + range 100 100000 + default 2000 + help + A callback dispatched by the ISO task that runs longer than this + many microseconds is counted as slow in the stats. Default 2000 is + ~1/4 of a 7.5-10ms SDU interval, well above any healthy callback. + + config BT_ISO_DISPATCH_DUMP_PERIOD_S + int "ISO dispatch stats dump period (in seconds)" + depends on BT_ISO_DISPATCH_MONITOR + range 1 600 + default 10 + help + The ISO task logs the per-type timing table (count / max / slow- + count) every this many seconds while running, in addition to the + dump at deinit. + config BT_ISO_NO_LOG bool "Disable BLE ISO Debug Log" default n diff --git a/components/bt/esp_ble_iso/host/adapter/bluedroid/gap.c b/components/bt/esp_ble_iso/host/adapter/bluedroid/gap.c index 4f34eff91fc..d34d6ecee7d 100644 --- a/components/bt/esp_ble_iso/host/adapter/bluedroid/gap.c +++ b/components/bt/esp_ble_iso/host/adapter/bluedroid/gap.c @@ -178,6 +178,7 @@ static void bt_le_bluedroid_gap_post_event_bta(tBTA_DM_BLE_5_GAP_EVENT event, tBTA_DM_BLE_5_GAP_CB_PARAMS *params) { struct bt_le_gap_app_param *qev = NULL; + enum iso_queue_item_type q_type; int err; qev = calloc(1, sizeof(*qev)); @@ -350,9 +351,31 @@ static void bt_le_bluedroid_gap_post_event_bta(tBTA_DM_BLE_5_GAP_EVENT event, return; } - err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GAP_EVENT, qev, sizeof(*qev)); + /* High-volume reports go to the droppable floodable queue (named 1:1 after + * the HCI report event); connection / PA-sync lifecycle events stay + * reliable on the normal queue. Keep this mapping in sync with + * nimble/gap.c. */ + switch (qev->type) { + case BT_LE_GAP_APP_PARAM_EXT_SCAN_RECV: + q_type = ISO_QUEUE_ITEM_TYPE_EXT_ADV_REPORT; + break; + case BT_LE_GAP_APP_PARAM_PA_SYNC_RECV: + q_type = ISO_QUEUE_ITEM_TYPE_PER_ADV_REPORT; + break; + default: + q_type = ISO_QUEUE_ITEM_TYPE_GAP_EVENT; + break; + } + + err = bt_le_iso_task_post(q_type, qev, sizeof(*qev)); if (err) { - LOG_ERR("[B]GapPostEvtBtaFail[%d][%u]", err, qev->type); + /* Floodable reports drop by design when the queue is full; only a + * failure on the reliable (normal-queue) path is a real error. */ + if (q_type == ISO_QUEUE_ITEM_TYPE_GAP_EVENT) { + LOG_ERR("[B]GapPostEvtBtaFail[%d][%u]", err, qev->type); + } else { + LOG_DBG("[B]GapRptDrop[%u]", qev->type); + } goto free; } diff --git a/components/bt/esp_ble_iso/host/adapter/bluedroid/iso.c b/components/bt/esp_ble_iso/host/adapter/bluedroid/iso.c index e9c7bd94912..009043b2cde 100644 --- a/components/bt/esp_ble_iso/host/adapter/bluedroid/iso.c +++ b/components/bt/esp_ble_iso/host/adapter/bluedroid/iso.c @@ -829,6 +829,7 @@ int bt_le_bluedroid_hci_iso_cmd_send_sync(uint16_t opcode, static void iso_evt_handler(tBTM_BLE_ISO_EVENT event, tBTM_BLE_ISO_CB_PARAMS *params) { + enum iso_queue_item_type q_type; uint8_t *qdata = NULL; size_t qdata_len = 0; int err; @@ -1039,7 +1040,14 @@ static void iso_evt_handler(tBTM_BLE_ISO_EVENT event, tBTM_BLE_ISO_CB_PARAMS *pa return; } - err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_ISO_HCI_EVENT, qdata, qdata_len); + /* BIGInfo reports arrive at the PA interval rate (one per PA report that + * carries a BIGInfo field): route them to the droppable floodable queue. + * Other ISO HCI events (CIS/BIG lifecycle) stay reliable. Keep the + * classification in sync with nimble/iso.c. */ + q_type = (event == BTM_BLE_ISO_BIGINFO_ADV_REPORT_EVT) + ? ISO_QUEUE_ITEM_TYPE_BIGINFO_ADV_REPORT : ISO_QUEUE_ITEM_TYPE_ISO_HCI_EVENT; + + err = bt_le_iso_task_post(q_type, qdata, qdata_len); if (err) { LOG_ERR("[B]IsoPostEvtFail[%d][%02x]", err, event); free(qdata); diff --git a/components/bt/esp_ble_iso/host/adapter/nimble/gap.c b/components/bt/esp_ble_iso/host/adapter/nimble/gap.c index b64d6847a0c..d6996c54640 100644 --- a/components/bt/esp_ble_iso/host/adapter/nimble/gap.c +++ b/components/bt/esp_ble_iso/host/adapter/nimble/gap.c @@ -79,6 +79,7 @@ void bt_le_nimble_gap_post_event(void *param) struct bt_le_gap_app_param *qev = NULL; struct ble_gap_conn_desc desc = {0}; struct ble_gap_event *ev = NULL; + enum iso_queue_item_type q_type; int err; qev = calloc(1, sizeof(*qev)); @@ -222,9 +223,31 @@ void bt_le_nimble_gap_post_event(void *param) return; } - err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_GAP_EVENT, qev, sizeof(*qev)); + /* High-volume reports go to the droppable floodable queue (named 1:1 after + * the HCI report event); connection / PA-sync lifecycle events stay + * reliable on the normal queue. Keep this mapping in sync with + * bluedroid/gap.c. */ + switch (qev->type) { + case BT_LE_GAP_APP_PARAM_EXT_SCAN_RECV: + q_type = ISO_QUEUE_ITEM_TYPE_EXT_ADV_REPORT; + break; + case BT_LE_GAP_APP_PARAM_PA_SYNC_RECV: + q_type = ISO_QUEUE_ITEM_TYPE_PER_ADV_REPORT; + break; + default: + q_type = ISO_QUEUE_ITEM_TYPE_GAP_EVENT; + break; + } + + err = bt_le_iso_task_post(q_type, qev, sizeof(*qev)); if (err) { - LOG_ERR("[N]GapPostEvtFail[%d][%u]", err, qev->type); + /* Floodable reports drop by design when the queue is full; only a + * failure on the reliable (normal-queue) path is a real error. */ + if (q_type == ISO_QUEUE_ITEM_TYPE_GAP_EVENT) { + LOG_ERR("[N]GapPostEvtFail[%d][%u]", err, qev->type); + } else { + LOG_DBG("[N]GapRptDrop[%u]", qev->type); + } goto free; } diff --git a/components/bt/esp_ble_iso/host/adapter/nimble/iso.c b/components/bt/esp_ble_iso/host/adapter/nimble/iso.c index 16f75c10a9f..992c9d1f26e 100644 --- a/components/bt/esp_ble_iso/host/adapter/nimble/iso.c +++ b/components/bt/esp_ble_iso/host/adapter/nimble/iso.c @@ -780,6 +780,7 @@ int bt_le_nimble_iso_cmd_send_sync(uint16_t opcode, static void iso_evt_rx(uint8_t event, const void *data, unsigned int len, bool le_meta) { + enum iso_queue_item_type q_type; size_t qdata_len; uint8_t *qdata; int err; @@ -797,7 +798,14 @@ static void iso_evt_rx(uint8_t event, const void *data, qdata[1] = event; memcpy(qdata + 2, data, len); - err = bt_le_iso_task_post(ISO_QUEUE_ITEM_TYPE_ISO_HCI_EVENT, qdata, qdata_len); + /* BIGInfo reports arrive at the PA interval rate (one per PA report that + * carries a BIGInfo field): route them to the droppable floodable queue. + * Other ISO HCI events (CIS/BIG lifecycle) stay reliable. Keep the + * classification in sync with bluedroid/iso.c. */ + q_type = (event == BT_HCI_EVT_LE_BIGINFO_ADV_REPORT) + ? ISO_QUEUE_ITEM_TYPE_BIGINFO_ADV_REPORT : ISO_QUEUE_ITEM_TYPE_ISO_HCI_EVENT; + + err = bt_le_iso_task_post(q_type, qdata, qdata_len); if (err) { LOG_ERR("[N]IsoPostEvtFail[%d][%02x]", err, event); free(qdata); diff --git a/components/bt/esp_ble_iso/host/common/include/common/task.h b/components/bt/esp_ble_iso/host/common/include/common/task.h index ff00f17d40f..5a3a8dfc395 100644 --- a/components/bt/esp_ble_iso/host/common/include/common/task.h +++ b/components/bt/esp_ble_iso/host/common/include/common/task.h @@ -47,13 +47,33 @@ extern "C" { #define ISO_TASK_PRIO (configMAX_PRIORITIES - 4) #endif +/* Ordered by priority tier, highest first. The numeric value is not used for + * prioritization (that comes from which queue bt_le_iso_task_post routes to); + * the order is for readability of the three tiers. */ enum iso_queue_item_type { + /* critical: latency-critical ISO data, drained first, posted non-blocking + * from the controller; an overflow drops the newest. */ + ISO_QUEUE_ITEM_TYPE_ISO_RX_DATA, + ISO_QUEUE_ITEM_TYPE_ISO_TX_COMP, + + /* normal: reliable events (blocking send, never dropped). */ ISO_QUEUE_ITEM_TYPE_TIMER_EVENT, ISO_QUEUE_ITEM_TYPE_GAP_EVENT, ISO_QUEUE_ITEM_TYPE_GATT_EVENT, ISO_QUEUE_ITEM_TYPE_ISO_HCI_EVENT, - ISO_QUEUE_ITEM_TYPE_ISO_TX_COMP, - ISO_QUEUE_ITEM_TYPE_ISO_RX_DATA, + + /* floodable: high-volume best-effort reports, posted non-blocking, newest + * dropped on overflow. Named 1:1 after the HCI report events. They share + * the floodable queue but EXT/PER use bt_le_gap_handle_event while BIGInfo + * uses bt_le_iso_handle_hci_event: + * EXT_ADV_REPORT - LE Extended Advertising Report + * PER_ADV_REPORT - LE Periodic Advertising Report + * BIGINFO_ADV_REPORT - LE BIGInfo Advertising Report (per spec it follows + * every PA report carrying a BIGInfo field, i.e. at + * the PA interval rate) */ + ISO_QUEUE_ITEM_TYPE_EXT_ADV_REPORT, + ISO_QUEUE_ITEM_TYPE_PER_ADV_REPORT, + ISO_QUEUE_ITEM_TYPE_BIGINFO_ADV_REPORT, ISO_QUEUE_ITEM_TYPE_MAX, }; @@ -64,8 +84,27 @@ struct iso_queue_item { size_t data_len; }; -#define ISO_QUEUE_ITEM_COUNT 100 -#define ISO_QUEUE_ITEM_SIZE sizeof(struct iso_queue_item) +/* Per-priority queue depths. The single FIFO is split into three queues so a + * flood of GAP reports cannot delay the latency-critical ISO data path: + * critical - ISO_RX_DATA / ISO_TX_COMP (drained first, non-blocking send) + * normal - timer / gatt / hci / GAP lifecycle (reliable, blocking send) + * floodable - GAP scan / PA data reports (droppable, non-blocking send) + */ +#define ISO_CRITICAL_QUEUE_LEN 32 +#define ISO_NORMAL_QUEUE_LEN 64 +#define ISO_FLOODABLE_QUEUE_LEN 32 +/* The set must be able to hold one token per item across all three queues. */ +#define ISO_QUEUE_SET_LEN (ISO_CRITICAL_QUEUE_LEN + \ + ISO_NORMAL_QUEUE_LEN + \ + ISO_FLOODABLE_QUEUE_LEN) +#define ISO_QUEUE_ITEM_SIZE sizeof(struct iso_queue_item) + +#if CONFIG_BT_ISO_DISPATCH_MONITOR +#define ISO_DISPATCH_THRESHOLD_US CONFIG_BT_ISO_DISPATCH_THRESHOLD_US +#define ISO_STATS_DUMP_PERIOD_US (CONFIG_BT_ISO_DISPATCH_DUMP_PERIOD_S * 1000 * 1000) + +void bt_le_iso_dispatch_stats_dump(void); +#endif /* CONFIG_BT_ISO_DISPATCH_MONITOR */ int bt_le_iso_task_post(enum iso_queue_item_type type, void *data, size_t data_len); diff --git a/components/bt/esp_ble_iso/host/common/task.c b/components/bt/esp_ble_iso/host/common/task.c index 62a24d068d9..60f0898efa2 100644 --- a/components/bt/esp_ble_iso/host/common/task.c +++ b/components/bt/esp_ble_iso/host/common/task.c @@ -14,6 +14,8 @@ #include #include +#include "esp_timer.h" + #include <../host/conn_internal.h> #include "common/host.h" @@ -23,45 +25,138 @@ LOG_MODULE_REGISTER(ISO_TASK, CONFIG_BT_ISO_LOG_LEVEL); -static QueueHandle_t iso_queue_handle; +/* Three priority tiers share one task via a queue set. The task drains + * critical before normal before floodable, so a flood of GAP reports cannot + * delay the latency-critical ISO data path. See common/task.h for the mapping. + */ +static QueueHandle_t iso_critical_queue; +static QueueHandle_t iso_normal_queue; +static QueueHandle_t iso_floodable_queue; +static QueueSetHandle_t iso_queue_set; static TaskHandle_t iso_task_handle; extern void bt_le_timer_handle_event(void *arg); +#if CONFIG_BT_ISO_DISPATCH_MONITOR +/* Per-type dispatch timing, indexed by iso_queue_item_type. Written only by + * iso_task (single writer); read by bt_le_iso_dispatch_stats_dump. */ +static struct iso_dispatch_stats { + int64_t max_us; + uint32_t count; + uint32_t slow_count; +} iso_stats[ISO_QUEUE_ITEM_TYPE_MAX]; + +static void iso_dispatch_record(uint8_t type, int64_t elapsed_us) +{ + struct iso_dispatch_stats *st; + + if (type >= ISO_QUEUE_ITEM_TYPE_MAX) { + return; + } + + st = &iso_stats[type]; + st->count++; + if (elapsed_us > st->max_us) { + st->max_us = elapsed_us; + } + if (elapsed_us > ISO_DISPATCH_THRESHOLD_US) { + st->slow_count++; + LOG_WRN("IsoCbSlow[%u][%lld]", type, (long long)elapsed_us); + } +} + +void bt_le_iso_dispatch_stats_dump(void) +{ + int i; + + LOG_INF("IsoCbStats thr=%dus", ISO_DISPATCH_THRESHOLD_US); + + for (i = 0; i < ISO_QUEUE_ITEM_TYPE_MAX; i++) { + if (iso_stats[i].count == 0) { + continue; + } + + LOG_INF(" type[%d] cnt=%u max=%lld slow=%u", i, iso_stats[i].count, + (long long)iso_stats[i].max_us, iso_stats[i].slow_count); + } +} +#endif /* CONFIG_BT_ISO_DISPATCH_MONITOR */ + +static void iso_dispatch_item(const struct iso_queue_item *item) +{ +#if CONFIG_BT_ISO_DISPATCH_MONITOR + int64_t elapsed_us; + int64_t start_us; + + start_us = esp_timer_get_time(); +#endif /* CONFIG_BT_ISO_DISPATCH_MONITOR */ + + switch (item->type) { + case ISO_QUEUE_ITEM_TYPE_TIMER_EVENT: + bt_le_timer_handle_event(item->data); + break; + case ISO_QUEUE_ITEM_TYPE_GAP_EVENT: + case ISO_QUEUE_ITEM_TYPE_EXT_ADV_REPORT: + case ISO_QUEUE_ITEM_TYPE_PER_ADV_REPORT: + bt_le_gap_handle_event(item->data, item->data_len); + break; + case ISO_QUEUE_ITEM_TYPE_GATT_EVENT: + bt_le_gatt_handle_event(item->data, item->data_len); + break; + case ISO_QUEUE_ITEM_TYPE_ISO_HCI_EVENT: + case ISO_QUEUE_ITEM_TYPE_BIGINFO_ADV_REPORT: + bt_le_iso_handle_hci_event(item->data, item->data_len); + break; + case ISO_QUEUE_ITEM_TYPE_ISO_TX_COMP: + bt_le_iso_handle_tx_comp(item->data, item->data_len); + break; + case ISO_QUEUE_ITEM_TYPE_ISO_RX_DATA: + bt_le_iso_handle_rx_data(item->data, item->data_len); + break; + default: + if (item->data) { + free(item->data); + } + assert(0); + break; + } + +#if CONFIG_BT_ISO_DISPATCH_MONITOR + elapsed_us = esp_timer_get_time() - start_us; + iso_dispatch_record(item->type, elapsed_us); +#endif /* CONFIG_BT_ISO_DISPATCH_MONITOR */ +} + static void iso_task(void *p) { +#if CONFIG_BT_ISO_DISPATCH_MONITOR + int64_t last_dump_us = esp_timer_get_time(); +#endif /* CONFIG_BT_ISO_DISPATCH_MONITOR */ struct iso_queue_item item = {0}; while (1) { - xQueueReceive(iso_queue_handle, &item, portMAX_DELAY); + /* Block until any tier has data. The returned member handle is ignored: + * we always service by strict priority below (critical > normal > + * floodable), processing one item per wakeup and re-checking critical + * first on the next loop. A pdFALSE receive is tolerated as a benign + * side effect of servicing queues outside xQueueSelectFromSet. */ + (void)xQueueSelectFromSet(iso_queue_set, portMAX_DELAY); - switch (item.type) { - case ISO_QUEUE_ITEM_TYPE_TIMER_EVENT: - bt_le_timer_handle_event(item.data); - break; - case ISO_QUEUE_ITEM_TYPE_GAP_EVENT: - bt_le_gap_handle_event(item.data, item.data_len); - break; - case ISO_QUEUE_ITEM_TYPE_GATT_EVENT: - bt_le_gatt_handle_event(item.data, item.data_len); - break; - case ISO_QUEUE_ITEM_TYPE_ISO_HCI_EVENT: - bt_le_iso_handle_hci_event(item.data, item.data_len); - break; - case ISO_QUEUE_ITEM_TYPE_ISO_TX_COMP: - bt_le_iso_handle_tx_comp(item.data, item.data_len); - break; - case ISO_QUEUE_ITEM_TYPE_ISO_RX_DATA: - bt_le_iso_handle_rx_data(item.data, item.data_len); - break; - default: - if (item.data) { - free(item.data); - } - assert(0); - break; + if (xQueueReceive(iso_critical_queue, &item, 0) == pdTRUE) { + iso_dispatch_item(&item); + } else if (xQueueReceive(iso_normal_queue, &item, 0) == pdTRUE) { + iso_dispatch_item(&item); + } else if (xQueueReceive(iso_floodable_queue, &item, 0) == pdTRUE) { + iso_dispatch_item(&item); } + +#if CONFIG_BT_ISO_DISPATCH_MONITOR + if (esp_timer_get_time() - last_dump_us >= ISO_STATS_DUMP_PERIOD_US) { + bt_le_iso_dispatch_stats_dump(); + last_dump_us = esp_timer_get_time(); + } +#endif /* CONFIG_BT_ISO_DISPATCH_MONITOR */ } } @@ -69,31 +164,103 @@ int bt_le_iso_task_post(enum iso_queue_item_type type, void *data, size_t data_len) { struct iso_queue_item item = {0}; + QueueHandle_t queue; + TickType_t wait; int ret; item.type = type; item.data = data; item.data_len = data_len; - ret = xQueueSend(iso_queue_handle, &item, portMAX_DELAY); + switch (type) { + case ISO_QUEUE_ITEM_TYPE_ISO_RX_DATA: + case ISO_QUEUE_ITEM_TYPE_ISO_TX_COMP: + /* Latency-critical, posted from the controller task: never block. + * On a full queue the caller drops + frees the payload. */ + queue = iso_critical_queue; + wait = 0; + break; + case ISO_QUEUE_ITEM_TYPE_EXT_ADV_REPORT: + case ISO_QUEUE_ITEM_TYPE_PER_ADV_REPORT: + case ISO_QUEUE_ITEM_TYPE_BIGINFO_ADV_REPORT: + /* High-volume best-effort reports: never block, drop newest on full. */ + queue = iso_floodable_queue; + wait = 0; + break; + default: + /* Timer / GATT / HCI / GAP lifecycle: reliable, block until space. */ + queue = iso_normal_queue; + wait = portMAX_DELAY; + break; + } + + ret = xQueueSend(queue, &item, wait); if (ret != pdTRUE) { - LOG_ERR("IsoQPostFail[%d]", ret); + /* A non-blocking send failing is an expected drop (handled + logged by + * the caller); only a blocking send failing is a genuine error. */ + if (wait != 0) { + LOG_ERR("IsoQPostFail[%d][%u]", ret, type); + } return -1; } return 0; } +static void iso_queue_destroy_one(QueueHandle_t *queue) +{ + struct iso_queue_item item = {0}; + + if (*queue == NULL) { + return; + } + + /* Empty it so it can leave the set; pending payloads are not freed here + * (matches the pre-split deinit behavior). */ + while (xQueueReceive(*queue, &item, 0) == pdTRUE) { + } + + if (iso_queue_set) { + xQueueRemoveFromSet(*queue, iso_queue_set); + } + + vQueueDelete(*queue); + *queue = NULL; +} + +static void iso_queues_destroy(void) +{ + iso_queue_destroy_one(&iso_critical_queue); + iso_queue_destroy_one(&iso_normal_queue); + iso_queue_destroy_one(&iso_floodable_queue); + + if (iso_queue_set) { + vQueueDelete(iso_queue_set); + iso_queue_set = NULL; + } +} + int bt_le_iso_task_init(void) { int ret; LOG_DBG("IsoTaskInit"); - iso_queue_handle = xQueueCreate(ISO_QUEUE_ITEM_COUNT, ISO_QUEUE_ITEM_SIZE); - if (iso_queue_handle == NULL) { + iso_critical_queue = xQueueCreate(ISO_CRITICAL_QUEUE_LEN, ISO_QUEUE_ITEM_SIZE); + iso_normal_queue = xQueueCreate(ISO_NORMAL_QUEUE_LEN, ISO_QUEUE_ITEM_SIZE); + iso_floodable_queue = xQueueCreate(ISO_FLOODABLE_QUEUE_LEN, ISO_QUEUE_ITEM_SIZE); + iso_queue_set = xQueueCreateSet(ISO_QUEUE_SET_LEN); + if (iso_critical_queue == NULL || iso_normal_queue == NULL || + iso_floodable_queue == NULL || iso_queue_set == NULL) { LOG_ERR("IsoQCreateFail"); - return -EIO; + goto fail; + } + + if (xQueueAddToSet(iso_critical_queue, iso_queue_set) != pdPASS || + xQueueAddToSet(iso_normal_queue, iso_queue_set) != pdPASS || + xQueueAddToSet(iso_floodable_queue, iso_queue_set) != pdPASS) { + LOG_ERR("IsoQSetAddFail"); + goto fail; } ret = xTaskCreatePinnedToCore(iso_task, @@ -105,12 +272,14 @@ int bt_le_iso_task_init(void) ISO_TASK_CORE); if (ret != pdTRUE) { LOG_ERR("IsoTaskCreateFail[%d]", ret); - vQueueDelete(iso_queue_handle); - iso_queue_handle = NULL; - return -EIO; + goto fail; } return 0; + +fail: + iso_queues_destroy(); + return -EIO; } void bt_le_iso_task_deinit(void) @@ -122,8 +291,10 @@ void bt_le_iso_task_deinit(void) iso_task_handle = NULL; } - if (iso_queue_handle) { - vQueueDelete(iso_queue_handle); - iso_queue_handle = NULL; - } +#if CONFIG_BT_ISO_DISPATCH_MONITOR + /* Task is gone: no concurrent writer, safe to read the stats. */ + bt_le_iso_dispatch_stats_dump(); +#endif /* CONFIG_BT_ISO_DISPATCH_MONITOR */ + + iso_queues_destroy(); }