From 367b4358ac4a3a3bd165ed9a4f50ee0a81ed26fe Mon Sep 17 00:00:00 2001 From: "peter.marcisovsky" Date: Wed, 10 Dec 2025 15:18:31 +0100 Subject: [PATCH 1/2] refactor(uac_host): Suspend/Resume public API terminology (will be dropped) --- host/class/uac/usb_host_uac/CHANGELOG.md | 6 ++++ host/class/uac/usb_host_uac/README.md | 6 ++-- .../examples/audio_player/main/main.c | 10 +++--- .../uac/usb_host_uac/include/usb/uac_host.h | 16 ++++----- .../test_app/main/test_host_uac.c | 24 ++++++------- host/class/uac/usb_host_uac/uac_host.c | 36 +++++++++---------- 6 files changed, 52 insertions(+), 46 deletions(-) diff --git a/host/class/uac/usb_host_uac/CHANGELOG.md b/host/class/uac/usb_host_uac/CHANGELOG.md index 89e3d8515..402b2f02d 100644 --- a/host/class/uac/usb_host_uac/CHANGELOG.md +++ b/host/class/uac/usb_host_uac/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this component will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [unreleased] + +### Changed + +- Changed API terminology from suspend/resume to pause/unpause as pre-requisite for root port suspend/resume feature + ## [1.3.3] - 2025-11-27 ### Changed diff --git a/host/class/uac/usb_host_uac/README.md b/host/class/uac/usb_host_uac/README.md index beb70e295..0470929b3 100644 --- a/host/class/uac/usb_host_uac/README.md +++ b/host/class/uac/usb_host_uac/README.md @@ -24,9 +24,9 @@ The following steps outline the typical API call pattern of the UAC Class Driver 06. To enable/disable data streaming with specific audio format use: - `uac_host_device_start()` - `uac_host_device_stop()` -07. To suspend/resume data streaming use: - - `uac_host_device_suspend()` - - `uac_host_device_resume()` +07. To pause/unpause data streaming use: + - `uac_host_device_pause()` + - `uac_host_device_unpause()` 08. To control the volume/mute use: - `uac_host_device_set_mute()` 09. To control the volume use: diff --git a/host/class/uac/usb_host_uac/examples/audio_player/main/main.c b/host/class/uac/usb_host_uac/examples/audio_player/main/main.c index d867d1a72..caabbd817 100644 --- a/host/class/uac/usb_host_uac/examples/audio_player/main/main.c +++ b/host/class/uac/usb_host_uac/examples/audio_player/main/main.c @@ -199,11 +199,11 @@ static void mic_palyback_done_cb(void) { vTaskDelay(pdMS_TO_TICKS(1000)); // wait for a while before resuming recording if (s_mic_dev_handle != NULL) { - if (uac_host_device_resume(s_mic_dev_handle) == ESP_OK) { + if (uac_host_device_unpause(s_mic_dev_handle) == ESP_OK) { s_mic_recording = true; s_mic_record_wr = 0; } else { - ESP_LOGE(TAG, "Failed to resume MIC device"); + ESP_LOGE(TAG, "Failed to unpause MIC device"); } } } @@ -396,8 +396,8 @@ static void uac_lib_task(void *arg) if (s_mic_record_wr >= s_mic_record_buf_size) { if (s_spk_dev_handle != NULL) { s_mic_recording = false; - // Suspend microphone streaming before playback - uac_host_device_suspend(s_mic_dev_handle); + // Pause microphone streaming before playback + uac_host_device_pause(s_mic_dev_handle); // Prepare playback of the recorded PCM player_config_t player_config = { .pcm_ptr = s_mic_record_buf, @@ -405,7 +405,7 @@ static void uac_lib_task(void *arg) .complete_cb = mic_palyback_done_cb, }; if (start_pcm_playback(&player_config) != ESP_OK) { - uac_host_device_resume(s_mic_dev_handle); + uac_host_device_unpause(s_mic_dev_handle); s_mic_recording = true; s_mic_record_wr = 0; } diff --git a/host/class/uac/usb_host_uac/include/usb/uac_host.h b/host/class/uac/usb_host_uac/include/usb/uac_host.h index 3fe8a7880..bef12534e 100644 --- a/host/class/uac/usb_host_uac/include/usb/uac_host.h +++ b/host/class/uac/usb_host_uac/include/usb/uac_host.h @@ -31,10 +31,10 @@ extern "C" { /** * @brief Flags to control stream work flow * - * FLAG_STREAM_SUSPEND_AFTER_START: do not start stream transfer during start, only claim interface and prepare memory - * @note User should call uac_host_device_resume to start stream transfer when needed + * FLAG_STREAM_PAUSE_AFTER_START: do not start stream transfer during start, only claim interface and prepare memory + * @note User should call uac_host_device_unpause to start stream transfer when needed */ -#define FLAG_STREAM_SUSPEND_AFTER_START (1 << 0) +#define FLAG_STREAM_PAUSE_AFTER_START (1 << 0) typedef struct uac_interface *uac_host_device_handle_t; /*!< Logic Device Handle. Handle to a particular UAC interface */ @@ -303,7 +303,7 @@ esp_err_t uac_host_handle_events(TickType_t timeout); /** * @brief Start a UAC stream with specific stream configuration (channels, bit resolution, sample frequency) * - * @note set flags FLAG_STREAM_SUSPEND_AFTER_START to suspend stream after start + * @note set flags FLAG_STREAM_PAUSE_AFTER_START to pause stream after start * * @param[in] uac_dev_handle UAC device handle * @param[in] stream_config Pointer to UAC stream configuration structure @@ -318,7 +318,7 @@ esp_err_t uac_host_handle_events(TickType_t timeout); esp_err_t uac_host_device_start(uac_host_device_handle_t uac_dev_handle, const uac_host_stream_config_t *stream_config); /** - * @brief Suspend a UAC stream + * @brief Pause a UAC stream * * @param[in] uac_dev_handle UAC device handle * @return esp_err_t @@ -326,10 +326,10 @@ esp_err_t uac_host_device_start(uac_host_device_handle_t uac_dev_handle, const u * - ESP_ERR_INVALID_ARG if the device handle is invalid * - ESP_ERR_INVALID_STATE if the device is not in the right state */ -esp_err_t uac_host_device_suspend(uac_host_device_handle_t uac_dev_handle); +esp_err_t uac_host_device_pause(uac_host_device_handle_t uac_dev_handle); /** - * @brief Resume a UAC stream with same stream configuration + * @brief Unpause a UAC stream with same stream configuration * * @param[in] uac_dev_handle UAC device handle * @return esp_err_t @@ -337,7 +337,7 @@ esp_err_t uac_host_device_suspend(uac_host_device_handle_t uac_dev_handle); * - ESP_ERR_INVALID_ARG if the device handle is invalid * - ESP_ERR_INVALID_STATE if the device is not in the right state */ -esp_err_t uac_host_device_resume(uac_host_device_handle_t uac_dev_handle); +esp_err_t uac_host_device_unpause(uac_host_device_handle_t uac_dev_handle); /** * @brief Stop a UAC stream, stream resources will be released diff --git a/host/class/uac/usb_host_uac/test_app/main/test_host_uac.c b/host/class/uac/usb_host_uac/test_app/main/test_host_uac.c index 10ac1de77..8c7fa4f7c 100644 --- a/host/class/uac/usb_host_uac/test_app/main/test_host_uac.c +++ b/host/class/uac/usb_host_uac/test_app/main/test_host_uac.c @@ -602,7 +602,7 @@ TEST_CASE("test uac tx writing", "[uac_host][tx]") .channels = spk_alt_params.channels, .bit_resolution = spk_alt_params.bit_resolution, .sample_freq = spk_alt_params.sample_freq[0], - .flags = FLAG_STREAM_SUSPEND_AFTER_START, + .flags = FLAG_STREAM_PAUSE_AFTER_START, }; TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_start(uac_device_handle, &stream_config)); @@ -669,7 +669,7 @@ TEST_CASE("test uac tx writing", "[uac_host][tx]") TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_get_volume(uac_device_handle, &actual_volume)); volume = actual_volume; printf("Volume: %d \n", volume); - TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_resume(uac_device_handle)); + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_unpause(uac_device_handle)); TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_write(uac_device_handle, (uint8_t *)tx_buffer, tx_size, 0)); uint8_t test_counter = 0; @@ -779,7 +779,7 @@ TEST_CASE("test uac tx rx loopback", "[uac_host][tx][rx]") .channels = mic_alt_params.channels, .bit_resolution = mic_alt_params.bit_resolution, .sample_freq = mic_alt_params.sample_freq[0], - .flags = FLAG_STREAM_SUSPEND_AFTER_START, + .flags = FLAG_STREAM_PAUSE_AFTER_START, }; uint8_t actual_volume = 0; @@ -831,8 +831,8 @@ TEST_CASE("test uac tx rx loopback", "[uac_host][tx][rx]") uint32_t test_counter = 0; event_queue_t evt_queue = {0}; while (1) { - TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_resume(mic_device_handle)); - TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_resume(spk_device_handle)); + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_unpause(mic_device_handle)); + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_unpause(spk_device_handle)); while (1) { if (xQueueReceive(s_event_queue, &evt_queue, portMAX_DELAY)) { TEST_ASSERT_EQUAL(UAC_DEVICE_EVENT, evt_queue.event_group); @@ -885,8 +885,8 @@ TEST_CASE("test uac tx rx loopback", "[uac_host][tx][rx]") if (++test_counter >= test_times) { goto exit_rx; } - TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_suspend(mic_device_handle)); - TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_suspend(spk_device_handle)); + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_pause(mic_device_handle)); + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_pause(spk_device_handle)); time_counter = 0; vTaskDelay(100); } @@ -938,7 +938,7 @@ TEST_CASE("test uac tx rx loopback with disconnect", "[uac_host][tx][rx][hot-plu .channels = mic_alt_params.channels, .bit_resolution = mic_alt_params.bit_resolution, .sample_freq = mic_alt_params.sample_freq[0], - .flags = FLAG_STREAM_SUSPEND_AFTER_START, + .flags = FLAG_STREAM_PAUSE_AFTER_START, }; TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_start(mic_device_handle, &stream_config)); TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_set_mute(mic_device_handle, 0)); @@ -979,8 +979,8 @@ TEST_CASE("test uac tx rx loopback with disconnect", "[uac_host][tx][rx][hot-plu uint32_t test_counter = 0; event_queue_t evt_queue = {0}; while (1) { - TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_resume(mic_device_handle)); - TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_resume(spk_device_handle)); + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_unpause(mic_device_handle)); + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_unpause(spk_device_handle)); while (1) { if (xQueueReceive(s_event_queue, &evt_queue, portMAX_DELAY)) { TEST_ASSERT_EQUAL(UAC_DEVICE_EVENT, evt_queue.event_group); @@ -1033,8 +1033,8 @@ TEST_CASE("test uac tx rx loopback with disconnect", "[uac_host][tx][rx][hot-plu if (++test_counter >= test_times) { goto exit_rx; } - TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_suspend(mic_device_handle)); - TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_suspend(spk_device_handle)); + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_pause(mic_device_handle)); + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_pause(spk_device_handle)); time_counter = 0; vTaskDelay(100); } diff --git a/host/class/uac/usb_host_uac/uac_host.c b/host/class/uac/usb_host_uac/uac_host.c index a62fee6c4..11b12b64d 100644 --- a/host/class/uac/usb_host_uac/uac_host.c +++ b/host/class/uac/usb_host_uac/uac_host.c @@ -124,9 +124,9 @@ typedef struct uac_host_device { typedef enum { UAC_INTERFACE_STATE_NOT_INITIALIZED = 0x00, /*!< UAC Interface not initialized */ UAC_INTERFACE_STATE_IDLE, /*!< UAC Interface has been opened but not started */ - UAC_INTERFACE_STATE_READY, /*!< UAC Interface has started but stream is suspended */ + UAC_INTERFACE_STATE_READY, /*!< UAC Interface has started but stream is paused */ UAC_INTERFACE_STATE_ACTIVE, /*!< UAC Interface is streaming */ - UAC_INTERFACE_STATE_SUSPENDING, /*!< UAC Interface is suspending */ + UAC_INTERFACE_STATE_PAUSING, /*!< UAC Interface is pausing */ } uac_iface_state_t; /** @@ -1332,19 +1332,19 @@ static void stream_tx_xfer_done(usb_transfer_t *out_xfer) } /** - * @brief Suspend active interface, the interface will be in READY state + * @brief Pause active interface, the interface will be in READY state * * @param[in] iface Pointer to Interface structure * @return esp_err_t */ -static esp_err_t uac_host_interface_suspend(uac_iface_t *iface) +static esp_err_t uac_host_interface_pause(uac_iface_t *iface) { UAC_RETURN_ON_INVALID_ARG(iface); UAC_RETURN_ON_INVALID_ARG(iface->parent); UAC_RETURN_ON_INVALID_ARG(iface->free_xfer_list); UAC_RETURN_ON_FALSE(is_interface_in_list(iface), ESP_ERR_NOT_FOUND, "Interface handle not found"); UAC_RETURN_ON_FALSE((UAC_INTERFACE_STATE_ACTIVE == iface->state), ESP_ERR_INVALID_STATE, "Interface wrong state"); - iface->state = UAC_INTERFACE_STATE_SUSPENDING; + iface->state = UAC_INTERFACE_STATE_PAUSING; // Set Interface alternate setting to 0 usb_setup_packet_t usb_request; @@ -1355,9 +1355,9 @@ static esp_err_t uac_host_interface_suspend(uac_iface_t *iface) memcpy(&uac_request, &usb_request, sizeof(usb_setup_packet_t)); esp_err_t ret = uac_cs_request_set(iface->parent, &uac_request); if (ret != ESP_OK) { - ESP_LOGW(TAG, "Suspend Interface %d-%d Failed", iface->dev_info.iface_num, 0); + ESP_LOGW(TAG, "Pause Interface %d-%d Failed", iface->dev_info.iface_num, 0); } else { - ESP_LOGI(TAG, "Suspend Interface %d-%d", iface->dev_info.iface_num, 0); + ESP_LOGI(TAG, "Pause Interface %d-%d", iface->dev_info.iface_num, 0); } uint8_t ep_addr = iface->iface_alt[iface->cur_alt].ep_addr; @@ -1382,12 +1382,12 @@ static esp_err_t uac_host_interface_suspend(uac_iface_t *iface) } /** - * @brief Resume suspended interface, the interface will be in ACTIVE state + * @brief Unpause paused interface, the interface will be in ACTIVE state * * @param[in] iface Pointer to Interface structure * @return esp_err_t */ -static esp_err_t uac_host_interface_resume(uac_iface_t *iface) +static esp_err_t uac_host_interface_unpause(uac_iface_t *iface) { UAC_RETURN_ON_INVALID_ARG(iface); UAC_RETURN_ON_INVALID_ARG(iface->parent); @@ -1403,7 +1403,7 @@ static esp_err_t uac_host_interface_resume(uac_iface_t *iface) uac_cs_request_t uac_request = {0}; memcpy(&uac_request, &usb_request, sizeof(usb_setup_packet_t)); UAC_RETURN_ON_ERROR(uac_cs_request_set(iface->parent, &uac_request), "Unable to set Interface alternate"); - ESP_LOGI(TAG, "Resume Interface %d-%d", iface->dev_info.iface_num, iface->cur_alt + 1); + ESP_LOGI(TAG, "Unpause Interface %d-%d", iface->dev_info.iface_num, iface->cur_alt + 1); // Set endpoint frequency control if (iface->iface_alt[iface->cur_alt].freq_ctrl_supported) { ESP_LOGI(TAG, "Set EP %d frequency %"PRIu32, iface->iface_alt[iface->cur_alt].ep_addr & USB_B_ENDPOINT_ADDRESS_EP_NUM_MASK, iface->iface_alt[iface->cur_alt].cur_sampling_freq); @@ -2201,7 +2201,7 @@ esp_err_t uac_host_device_close(uac_host_device_handle_t uac_dev_handle) UAC_RETURN_ON_ERROR(uac_host_interface_try_lock(uac_iface, DEFAULT_CTRL_XFER_TIMEOUT_MS), "UAC Interface is busy by other task"); if (UAC_INTERFACE_STATE_ACTIVE == uac_iface->state) { - UAC_GOTO_ON_ERROR(uac_host_interface_suspend(uac_iface), "Unable to disable UAC Interface"); + UAC_GOTO_ON_ERROR(uac_host_interface_pause(uac_iface), "Unable to disable UAC Interface"); } if (UAC_INTERFACE_STATE_READY == uac_iface->state) { @@ -2372,8 +2372,8 @@ esp_err_t uac_host_device_start(uac_host_device_handle_t uac_dev_handle, const u UAC_GOTO_ON_ERROR(uac_host_interface_claim_and_prepare_transfer(iface), "Unable to claim Interface"); iface_claimed = true; - if (!(iface->flags & FLAG_STREAM_SUSPEND_AFTER_START)) { - UAC_GOTO_ON_ERROR(uac_host_interface_resume(iface), "Unable to enable UAC Interface"); + if (!(iface->flags & FLAG_STREAM_PAUSE_AFTER_START)) { + UAC_GOTO_ON_ERROR(uac_host_interface_unpause(iface), "Unable to enable UAC Interface"); } uac_host_interface_unlock(iface); return ESP_OK; @@ -2386,7 +2386,7 @@ esp_err_t uac_host_device_start(uac_host_device_handle_t uac_dev_handle, const u return ret; } -esp_err_t uac_host_device_suspend(uac_host_device_handle_t uac_dev_handle) +esp_err_t uac_host_device_pause(uac_host_device_handle_t uac_dev_handle) { uac_iface_t *iface = get_iface_by_handle(uac_dev_handle); UAC_RETURN_ON_INVALID_ARG(iface); @@ -2398,7 +2398,7 @@ esp_err_t uac_host_device_suspend(uac_host_device_handle_t uac_dev_handle) } esp_err_t ret = ESP_OK; UAC_GOTO_ON_FALSE((UAC_INTERFACE_STATE_ACTIVE == iface->state), ESP_ERR_INVALID_STATE, "device not active"); - UAC_GOTO_ON_ERROR(uac_host_interface_suspend(iface), "Unable to disable UAC Interface"); + UAC_GOTO_ON_ERROR(uac_host_interface_pause(iface), "Unable to disable UAC Interface"); uac_host_interface_unlock(iface); return ESP_OK; @@ -2408,7 +2408,7 @@ esp_err_t uac_host_device_suspend(uac_host_device_handle_t uac_dev_handle) return ret; } -esp_err_t uac_host_device_resume(uac_host_device_handle_t uac_dev_handle) +esp_err_t uac_host_device_unpause(uac_host_device_handle_t uac_dev_handle) { uac_iface_t *iface = get_iface_by_handle(uac_dev_handle); UAC_RETURN_ON_INVALID_ARG(iface); @@ -2421,7 +2421,7 @@ esp_err_t uac_host_device_resume(uac_host_device_handle_t uac_dev_handle) esp_err_t ret = ESP_OK; UAC_GOTO_ON_FALSE((UAC_INTERFACE_STATE_READY == iface->state), ESP_ERR_INVALID_STATE, "device not ready"); - UAC_GOTO_ON_ERROR(uac_host_interface_resume(iface), "Unable to enable UAC Interface"); + UAC_GOTO_ON_ERROR(uac_host_interface_unpause(iface), "Unable to enable UAC Interface"); uac_host_interface_unlock(iface); return ESP_OK; @@ -2439,7 +2439,7 @@ esp_err_t uac_host_device_stop(uac_host_device_handle_t uac_dev_handle) esp_err_t ret = ESP_OK; UAC_RETURN_ON_ERROR(uac_host_interface_try_lock(iface, DEFAULT_CTRL_XFER_TIMEOUT_MS), "Unable to lock UAC Interface"); if (UAC_INTERFACE_STATE_ACTIVE == iface->state) { - UAC_GOTO_ON_ERROR(uac_host_interface_suspend(iface), "Unable to disable UAC Interface"); + UAC_GOTO_ON_ERROR(uac_host_interface_pause(iface), "Unable to disable UAC Interface"); } if (UAC_INTERFACE_STATE_READY == iface->state) { From 7d1573204ac778ec0fa3db9e221857534de52c5f Mon Sep 17 00:00:00 2001 From: "peter.marcisovsky" Date: Thu, 11 Dec 2025 16:29:48 +0100 Subject: [PATCH 2/2] feat(uac_host): Global Suspend/Resume support --- .../uac/usb_host_uac/include/usb/uac_host.h | 37 +- .../usb_host_uac/test_app/main/CMakeLists.txt | 6 +- .../test_app/main/test_hoast_uac_common.c | 382 +++++ .../test_app/main/test_host_uac.c | 288 +--- .../test_app/main/test_host_uac_common.h | 160 +++ .../test_app/main/test_host_uac_pm.c | 1263 +++++++++++++++++ .../test_app/pytest_usb_host_uac.py | 2 +- .../usb_host_uac/test_app/sdkconfig.defaults | 3 + host/class/uac/usb_host_uac/uac_host.c | 469 +++++- 9 files changed, 2297 insertions(+), 313 deletions(-) create mode 100644 host/class/uac/usb_host_uac/test_app/main/test_hoast_uac_common.c create mode 100644 host/class/uac/usb_host_uac/test_app/main/test_host_uac_common.h create mode 100644 host/class/uac/usb_host_uac/test_app/main/test_host_uac_pm.c diff --git a/host/class/uac/usb_host_uac/include/usb/uac_host.h b/host/class/uac/usb_host_uac/include/usb/uac_host.h index bef12534e..6b97509d4 100644 --- a/host/class/uac/usb_host_uac/include/usb/uac_host.h +++ b/host/class/uac/usb_host_uac/include/usb/uac_host.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -10,6 +10,7 @@ #include #include "freertos/FreeRTOS.h" #include "esp_err.h" +#include "usb/usb_host.h" #include "uac.h" #ifdef __cplusplus @@ -36,6 +37,11 @@ extern "C" { */ #define FLAG_STREAM_PAUSE_AFTER_START (1 << 0) +// For backward compatibility with older idf versions without suspend/resume API +#ifdef USB_HOST_LIB_EVENT_FLAGS_AUTO_SUSPEND +#define UAC_HOST_SUSPEND_RESUME_API_SUPPORTED +#endif + typedef struct uac_interface *uac_host_device_handle_t; /*!< Logic Device Handle. Handle to a particular UAC interface */ // ------------------------ USB UAC Host events -------------------------------- @@ -55,6 +61,10 @@ typedef enum { UAC_HOST_DEVICE_EVENT_TX_DONE, /*!< TX Done: the transmit buffer data size falls below the threshold */ UAC_HOST_DEVICE_EVENT_TRANSFER_ERROR, /*!< UAC Device transfer error */ UAC_HOST_DRIVER_EVENT_DISCONNECTED, /*!< UAC Device has been disconnected */ +#ifdef UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + UAC_HOST_DEVICE_EVENT_SUSPENDED, /*!< UAC Device has been suspended */ + UAC_HOST_DEVICE_EVENT_RESUMED, /*!< UAC Device has been resumed */ +#endif // UAC_HOST_SUSPEND_RESUME_API_SUPPORTED } uac_host_device_event_t; // ------------------------ USB UAC Host events callbacks ----------------------------- @@ -304,7 +314,8 @@ esp_err_t uac_host_handle_events(TickType_t timeout); * @brief Start a UAC stream with specific stream configuration (channels, bit resolution, sample frequency) * * @note set flags FLAG_STREAM_PAUSE_AFTER_START to pause stream after start - * + * @note The function can block + * @note The function sends a control transfer to the device * @param[in] uac_dev_handle UAC device handle * @param[in] stream_config Pointer to UAC stream configuration structure * @return esp_err_t @@ -320,6 +331,8 @@ esp_err_t uac_host_device_start(uac_host_device_handle_t uac_dev_handle, const u /** * @brief Pause a UAC stream * + * @note The function can block + * @note The function sends a control transfer to the device * @param[in] uac_dev_handle UAC device handle * @return esp_err_t * - ESP_OK on success @@ -331,6 +344,8 @@ esp_err_t uac_host_device_pause(uac_host_device_handle_t uac_dev_handle); /** * @brief Unpause a UAC stream with same stream configuration * + * @note The function can block + * @note The function sends a control transfer to the device * @param[in] uac_dev_handle UAC device handle * @return esp_err_t * - ESP_OK on success @@ -342,6 +357,8 @@ esp_err_t uac_host_device_unpause(uac_host_device_handle_t uac_dev_handle); /** * @brief Stop a UAC stream, stream resources will be released * + * @note The function can block + * @note The function sends a control transfer to the device * @param[in] uac_dev_handle UAC device handle * @return esp_err_t * - ESP_OK on success @@ -353,6 +370,7 @@ esp_err_t uac_host_device_stop(uac_host_device_handle_t uac_dev_handle); /** * @brief Read data from UAC stream buffer, only available after stream started * + * @note The function can block * @param[in] uac_dev_handle UAC device handle * @param[out] data Pointer to the buffer to store the data * @param[in] size Number of bytes to read @@ -371,7 +389,8 @@ esp_err_t uac_host_device_read(uac_host_device_handle_t uac_dev_handle, uint8_t * * @note The data will be sent to internal ringbuffer before function return, * the actual data transfer is scheduled by the background task. - * + * @note The function can block + * @note The function sends a transfer to the device * @param[in] uac_dev_handle UAC device handle * @param[in] data Pointer to the data buffer * @param[in] size Number of bytes to write @@ -389,6 +408,8 @@ esp_err_t uac_host_device_write(uac_host_device_handle_t uac_dev_handle, uint8_t * @brief Mute or un-mute the UAC device * @param[in] uac_dev_handle UAC device handle * @param[in] mute True to mute, false to unmute + * @note The function can block + * @note The function sends a control transfer to the device * @return esp_err_t * - ESP_OK on success * - ESP_ERR_INVALID_STATE if the device is not ready or active @@ -402,6 +423,8 @@ esp_err_t uac_host_device_set_mute(uac_host_device_handle_t uac_dev_handle, bool * @brief Get the mute status of the UAC device * @param[in] uac_dev_handle UAC device handle * @param[out] mute Pointer to store the mute status + * @note The function can block + * @note The function sends a control transfer to the device * @return esp_err_t * - ESP_OK on success * - ESP_ERR_INVALID_STATE if the device is not ready or active @@ -415,6 +438,8 @@ esp_err_t uac_host_device_get_mute(uac_host_device_handle_t uac_dev_handle, bool * @brief Set the volume of the UAC device * @param[in] uac_dev_handle UAC device handle * @param[in] volume Volume to set, 0-100 + * @note The function can block + * @note The function sends a control transfer to the device * @return esp_err_t * - ESP_OK on success * - ESP_ERR_INVALID_STATE if the device is not ready or active @@ -428,6 +453,8 @@ esp_err_t uac_host_device_set_volume(uac_host_device_handle_t uac_dev_handle, ui * @brief Get the volume of the UAC device * @param[in] uac_dev_handle UAC device handle * @param[out] volume Pointer to store the volume, 0-100 + * @note The function can block + * @note The function sends a control transfer to the device * @return esp_err_t * - ESP_OK on success * - ESP_ERR_INVALID_STATE if the device is not ready or active @@ -442,6 +469,8 @@ esp_err_t uac_host_device_get_volume(uac_host_device_handle_t uac_dev_handle, ui * @param[in] uac_dev_handle UAC device handle * @param[in] volume_db Volume to set, with resolution of 1/256 dB, * eg. 256 (0x0100) is 1 dB. 32767 (0x7FFF) is 127.996 dB. -32767 (0x8001) is -127.996 dB. + * @note The function can block + * @note The function sends a control transfer to the device * @return esp_err_t * - ESP_OK on success * - ESP_ERR_INVALID_STATE if the device is not ready or active @@ -456,6 +485,8 @@ esp_err_t uac_host_device_set_volume_db(uac_host_device_handle_t uac_dev_handle, * @param[in] uac_dev_handle UAC device handle * @param[out] volume_db Pointer to store the volume, with resolution of 1/256 dB, * eg. 256 (0x0100) is 1 dB. 32767 (0x7FFF) is 127.996 dB. -32767 (0x8001) is -127.996 dB. + * @note The function can block + * @note The function sends a control transfer to the device * @return esp_err_t * - ESP_OK on success * - ESP_ERR_INVALID_STATE if the device is not ready or active diff --git a/host/class/uac/usb_host_uac/test_app/main/CMakeLists.txt b/host/class/uac/usb_host_uac/test_app/main/CMakeLists.txt index cd12d4294..1a9a940d8 100644 --- a/host/class/uac/usb_host_uac/test_app/main/CMakeLists.txt +++ b/host/class/uac/usb_host_uac/test_app/main/CMakeLists.txt @@ -3,7 +3,5 @@ include($ENV{IDF_PATH}/tools/cmake/version.cmake) idf_component_register(SRC_DIRS . INCLUDE_DIRS . REQUIRES unity usb usb_host_uac - EMBED_FILES new_epic.wav) - -# force-link test_host_uac.c -set_property(TARGET ${COMPONENT_LIB} APPEND PROPERTY INTERFACE_LINK_LIBRARIES "-u test_uac_setup") + EMBED_FILES new_epic.wav + WHOLE_ARCHIVE) diff --git a/host/class/uac/usb_host_uac/test_app/main/test_hoast_uac_common.c b/host/class/uac/usb_host_uac/test_app/main/test_hoast_uac_common.c new file mode 100644 index 000000000..84631f392 --- /dev/null +++ b/host/class/uac/usb_host_uac/test_app/main/test_hoast_uac_common.c @@ -0,0 +1,382 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include "esp_log.h" +#include "esp_idf_version.h" +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "freertos/queue.h" +#include "freertos/semphr.h" +#include "unity.h" +#include "esp_private/usb_phy.h" +#include "usb/usb_host.h" +#include "usb/uac_host.h" +#include "test_host_uac_common.h" + +#define BIT0_USB_HOST_DRIVER_REMOVED (0x01 << 0) + +const static char *TAG = "UAC_TEST"; + +static EventGroupHandle_t s_evt_handle = NULL; +QueueHandle_t transfer_event_queue = NULL; +QueueHandle_t client_event_queue = NULL; + + +// usb_host_lib_set_root_port_power is used to force toggle connection, primary developed for esp32p4 +// esp32p4 is supported from IDF 5.3 +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) +static usb_phy_handle_t phy_hdl = NULL; + +void force_conn_state(bool connected, TickType_t delay_ticks) +{ + TEST_ASSERT_NOT_EQUAL(NULL, phy_hdl); + if (delay_ticks > 0) { + // Delay of 0 ticks causes a yield. So skip if delay_ticks is 0. + vTaskDelay(delay_ticks); + } + ESP_ERROR_CHECK(usb_phy_action(phy_hdl, (connected) ? USB_PHY_ACTION_HOST_ALLOW_CONN : USB_PHY_ACTION_HOST_FORCE_DISCONN)); +} + +// Initialize the internal USB PHY to connect to the USB OTG peripheral. We manually install the USB PHY for testing +static bool install_phy(void) +{ + usb_phy_config_t phy_config = { + .controller = USB_PHY_CTRL_OTG, + .target = USB_PHY_TARGET_INT, + .otg_mode = USB_OTG_MODE_HOST, + .otg_speed = USB_PHY_SPEED_UNDEFINED, // In Host mode, the speed is determined by the connected device + }; + TEST_ASSERT_EQUAL(ESP_OK, usb_new_phy(&phy_config, &phy_hdl)); + // Return true, to skip_phy_setup during the usb_host_install() + return true; +} + +static void delete_phy(void) +{ + TEST_ASSERT_EQUAL(ESP_OK, usb_del_phy(phy_hdl)); // Tear down USB PHY + phy_hdl = NULL; +} +#else // ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) + +// Force connection/disconnection using root port power +void force_conn_state(bool connected, TickType_t delay_ticks) +{ + if (delay_ticks > 0) { + // Delay of 0 ticks causes a yield. So skip if delay_ticks is 0. + vTaskDelay(delay_ticks); + } + ESP_ERROR_CHECK(usb_host_lib_set_root_port_power(connected)); +} + +static bool install_phy(void) +{ + // Return false, NOT to skip_phy_setup during the usb_host_install() + return false; +} + +static void delete_phy(void) {} +#endif // ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) + + +void uac_device_callback(uac_host_device_handle_t uac_device_handle, const uac_host_device_event_t event, void *arg) +{ + // Send uac device event to the event queue + event_queue_t evt_queue = { + .event_group = UAC_DEVICE_EVENT, + .device_evt.handle = uac_device_handle, + .device_evt.event = event, + .device_evt.arg = arg + }; + + bool evt_type_transfer = false; + bool evt_type_client = false; + + switch (event) { + case UAC_HOST_DRIVER_EVENT_DISCONNECTED: + ESP_LOGI(TAG, "UAC Device disconnected"); + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_close(uac_device_handle)); + evt_type_client = true; + break; +#ifdef UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + case UAC_HOST_DEVICE_EVENT_SUSPENDED: + ESP_LOGI(TAG, "Device suspended"); + evt_type_client = true; + break; + case UAC_HOST_DEVICE_EVENT_RESUMED: + ESP_LOGI(TAG, "Device resumed"); + evt_type_client = true; + break; +#endif // UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + case UAC_HOST_DEVICE_EVENT_TRANSFER_ERROR: + case UAC_HOST_DEVICE_EVENT_RX_DONE: + case UAC_HOST_DEVICE_EVENT_TX_DONE: + evt_type_transfer = true; + break; + default: + TEST_FAIL_MESSAGE("Unrecognized device event"); + break; + } + + // Sanity check, logical XOR + assert(evt_type_transfer != evt_type_client); + + if (evt_type_transfer) { + xQueueSend(transfer_event_queue, &evt_queue, 0); + } else { + xQueueSend(client_event_queue, &evt_queue, 0); + } +} + +static void uac_host_lib_callback(uint8_t addr, uint8_t iface_num, const uac_host_driver_event_t event, void *arg) +{ + // Send uac driver event to the event queue + event_queue_t evt_queue = { + .event_group = UAC_DRIVER_EVENT, + .driver_evt.addr = addr, + .driver_evt.iface_num = iface_num, + .driver_evt.event = event, + .driver_evt.arg = arg + }; + + switch (event) { + case UAC_HOST_DRIVER_EVENT_RX_CONNECTED: + ESP_LOGI(TAG, "RX Device connected"); + break; + case UAC_HOST_DRIVER_EVENT_TX_CONNECTED: + ESP_LOGI(TAG, "TX Device connected"); + break; + default: + TEST_FAIL_MESSAGE("Unrecognized driver event"); + } + + xQueueSend(client_event_queue, &evt_queue, 0); +} + +/** + * @brief Start USB Host install and handle common USB host library events while app pin not low + * + * @param[in] arg Not used + */ +static void usb_lib_task(void *arg) +{ + const bool skip_phy_setup = install_phy(); + const usb_host_config_t host_config = { + .skip_phy_setup = skip_phy_setup, + .intr_flags = ESP_INTR_FLAG_LOWMED, + }; + + TEST_ASSERT_EQUAL(ESP_OK, usb_host_install(&host_config)); + ESP_LOGI(TAG, "USB Host installed"); + xTaskNotifyGive(arg); + + bool all_clients_gone = false; + bool all_dev_free = false; + while (!all_clients_gone || !all_dev_free) { + // Start handling system events + uint32_t event_flags; + usb_host_lib_handle_events(portMAX_DELAY, &event_flags); + if (event_flags & USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS) { + printf("No more clients\n"); + usb_host_device_free_all(); + all_clients_gone = true; + } + if (event_flags & USB_HOST_LIB_EVENT_FLAGS_ALL_FREE) { + printf("All devices freed\n"); + all_dev_free = true; + } +#ifdef UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + if (event_flags & USB_HOST_LIB_EVENT_FLAGS_AUTO_SUSPEND) { + printf("Auto suspend timer flag\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + } +#endif // #ifdef UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + } + + ESP_LOGI(TAG, "USB Host shutdown"); + // Clean up USB Host + vTaskDelay(10); // Short delay to allow clients clean-up + TEST_ASSERT_EQUAL(ESP_OK, usb_host_uninstall()); + delete_phy(); //Tear down USB PHY + // set bit BIT0_USB_HOST_DRIVER_REMOVED to notify driver removed + xEventGroupSetBits(s_evt_handle, BIT0_USB_HOST_DRIVER_REMOVED); + vTaskDelete(NULL); +} + +void test_uac_setup(void) +{ + // create a queue to handle events + client_event_queue = xQueueCreate(8, sizeof(event_queue_t)); + TEST_ASSERT_NOT_NULL(client_event_queue); + transfer_event_queue = xQueueCreate(16, sizeof(event_queue_t)); + TEST_ASSERT_NOT_NULL(transfer_event_queue); + s_evt_handle = xEventGroupCreate(); + TEST_ASSERT_NOT_NULL(s_evt_handle); + static TaskHandle_t uac_task_handle = NULL; + // create USB lib task, pass the current task handle to notify when the task is created + TEST_ASSERT_EQUAL(pdTRUE, xTaskCreatePinnedToCore(usb_lib_task, + "usb_events", + 4096, + xTaskGetCurrentTaskHandle(), + 5, &uac_task_handle, 0)); + + // install uac host driver + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + uac_host_driver_config_t uac_config = { + .create_background_task = true, + .task_priority = 5, + .stack_size = 4096, + .core_id = 0, + .callback = uac_host_lib_callback, + .callback_arg = NULL + }; + + TEST_ASSERT_EQUAL(ESP_OK, uac_host_install(&uac_config)); + ESP_LOGI(TAG, "UAC Class Driver installed"); +} + +void test_uac_queue_reset(void) +{ + xQueueReset(client_event_queue); + xQueueReset(transfer_event_queue); +} + +void test_uac_teardown(bool force) +{ + if (force) { + force_conn_state(false, pdMS_TO_TICKS(1000)); + } + vTaskDelay(500); + // uninstall uac host driver + ESP_LOGI(TAG, "UAC Driver uninstall"); + TEST_ASSERT_EQUAL(ESP_OK, uac_host_uninstall()); + // Wait for USB lib task to finish + xEventGroupWaitBits(s_evt_handle, BIT0_USB_HOST_DRIVER_REMOVED, pdTRUE, pdTRUE, portMAX_DELAY); + // delete event queue and event group + vQueueDelete(client_event_queue); + vQueueDelete(transfer_event_queue); + vEventGroupDelete(s_evt_handle); + // delay to allow task to delete + vTaskDelay(100); +} + +void test_open_mic_device(uint8_t iface_num, uint32_t buffer_size, uint32_t buffer_threshold, uac_host_device_handle_t *uac_device_handle) +{ + // check if device params as expected + const uac_host_device_config_t dev_config = { + .addr = 1, + .iface_num = iface_num, + .buffer_size = buffer_size, + .buffer_threshold = buffer_threshold, + .callback = uac_device_callback, + .callback_arg = NULL, + }; + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_open(&dev_config, uac_device_handle)); +} + +void test_open_spk_device(uint8_t iface_num, uint32_t buffer_size, uint32_t buffer_threshold, uac_host_device_handle_t *uac_device_handle) +{ + // check if device params as expected + const uac_host_device_config_t dev_config = { + .addr = 1, + .iface_num = iface_num, + .buffer_size = buffer_size, + .buffer_threshold = buffer_threshold, + .callback = uac_device_callback, + .callback_arg = NULL, + }; + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_open(&dev_config, uac_device_handle)); +} + +void test_close_device(uac_host_device_handle_t uac_device_handle) +{ + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_close(uac_device_handle)); +} + +void test_handle_dev_connection(uint8_t *iface_num, uint8_t *if_rx) +{ + event_queue_t evt_queue = {0}; + // ignore the first connected event + TEST_ASSERT_EQUAL(pdTRUE, xQueueReceive(client_event_queue, &evt_queue, portMAX_DELAY)); + TEST_ASSERT_EQUAL(UAC_DRIVER_EVENT, evt_queue.event_group); + TEST_ASSERT_EQUAL(1, evt_queue.driver_evt.addr); + if (iface_num) { + *iface_num = evt_queue.driver_evt.iface_num; + } + if (if_rx) { + *if_rx = evt_queue.driver_evt.event == UAC_HOST_DRIVER_EVENT_RX_CONNECTED ? 1 : 0; + } +} + +void expect_client_event(const event_queue_t *expected_client_event, TickType_t ticks) +{ + event_queue_t client_event; + + // Check, that no event is delivered + if (expected_client_event == NULL) { + if (pdFALSE == xQueueReceive(client_event_queue, &client_event, ticks)) { + return; + } else { + TEST_FAIL_MESSAGE("Expecting NO client event, but an event delivered"); + } + } + + // Check that an event is delivered + if (pdTRUE == xQueueReceive(client_event_queue, &client_event, ticks)) { + + // Check event group + TEST_ASSERT_EQUAL_MESSAGE(expected_client_event->event_group, client_event.event_group, "Unexpected event group"); + + // Check event type according to event group + if (expected_client_event->event_group == UAC_DRIVER_EVENT) { + TEST_ASSERT_EQUAL_MESSAGE(expected_client_event->driver_evt.event, client_event.driver_evt.event, "Unexpected driver event"); + } else if (expected_client_event->event_group == UAC_DEVICE_EVENT) { + TEST_ASSERT_EQUAL_MESSAGE(expected_client_event->device_evt.event, client_event.device_evt.event, "Unexpected device event"); + } else { + // Event group not set, fail the test + TEST_FAIL_MESSAGE("Client event group not set"); + } + } else { + TEST_FAIL_MESSAGE("Client event not generated on time"); + } +} + +size_t expect_transfer_event(const event_queue_t *expected_transfer_event, TickType_t ticks) +{ + event_queue_t transfer_event; + size_t events_delivered = 0; + + // Check, that no event is delivered + if (expected_transfer_event == NULL) { + if (pdFALSE == xQueueReceive(transfer_event_queue, &transfer_event, ticks)) { + return 0; + } else { + TEST_FAIL_MESSAGE("Expecting NO transfer event, but an event delivered"); + } + } + + TEST_ASSERT_EQUAL_MESSAGE(expected_transfer_event->event_group, UAC_DEVICE_EVENT, "Only device events are allowed"); + TimeOut_t queue_timeout; + TickType_t remaining_ticks = ticks; + vTaskSetTimeOutState(&queue_timeout); + + do { + // Check that an event is delivered + if (pdTRUE == xQueueReceive(transfer_event_queue, &transfer_event, remaining_ticks)) { + TEST_ASSERT_EQUAL_MESSAGE(expected_transfer_event->device_evt.event, transfer_event.device_evt.event, "Unexpected transfer event"); + events_delivered++; + } else { + // Only fail if no event was delivered + TEST_ASSERT_MESSAGE(events_delivered, "Transfer event not generated on time"); + } + + } while (xTaskCheckForTimeOut(&queue_timeout, &remaining_ticks) == pdFALSE); + + return events_delivered; +} diff --git a/host/class/uac/usb_host_uac/test_app/main/test_host_uac.c b/host/class/uac/usb_host_uac/test_app/main/test_host_uac.c index 8c7fa4f7c..d1e958393 100644 --- a/host/class/uac/usb_host_uac/test_app/main/test_host_uac.c +++ b/host/class/uac/usb_host_uac/test_app/main/test_host_uac.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -9,28 +9,22 @@ #include #include #include "esp_log.h" -#include "esp_idf_version.h" #include "freertos/FreeRTOS.h" #include "freertos/event_groups.h" #include "freertos/queue.h" #include "freertos/semphr.h" #include "unity.h" -#include "esp_private/usb_phy.h" #include "usb/usb_host.h" #include "usb/uac_host.h" +#include "test_host_uac_common.h" const static char *TAG = "UAC_TEST"; // ----------------------- Public ------------------------- -static EventGroupHandle_t s_evt_handle; -static QueueHandle_t s_event_queue = NULL; -static EventGroupHandle_t s_evt_handle = NULL; - -#define BIT0_USB_HOST_DRIVER_REMOVED (0x01 << 0) // Known microphone device parameters -#define UAC_DEV_PID 0x3307 -#define UAC_DEV_VID 0x349C +#define UAC_DEV_PID 0x25 +#define UAC_DEV_VID 0x1395 #define UAC_DEV_MIC_IFACE_NUM 3 #define UAC_DEV_MIC_IFACE_ALT_NUM 1 @@ -61,272 +55,6 @@ static const uint8_t UAC_DEV_SPK_IFACE_SAMPLE_FREQ_TPYE_ALT[UAC_DEV_SPK_IFACE_AL static const uint32_t UAC_DEV_SPK_IFACE_SAMPLE_FREQ_ALT[UAC_DEV_SPK_IFACE_ALT_NUM][UAC_DEV_SPK_IFACE_ALT_1_SAMPLE_FREQ_TYPE] = {{UAC_DEV_SPK_IFACE_ALT_1_SAMPLE_FREQ_1}}; -typedef enum { - APP_EVENT = 0, - UAC_DRIVER_EVENT, - UAC_DEVICE_EVENT, -} event_group_t; - -typedef enum { - DRIVER_REMOVE = 1, -} user_event_t; - -typedef struct { - event_group_t event_group; - union { - struct { - uac_host_driver_event_t event; - uint8_t addr; - uint8_t iface_num; - void *arg; - } driver_evt; - struct { - uac_host_driver_event_t event; - uac_host_device_handle_t handle; - void *arg; - } device_evt; - }; -} event_queue_t; - -// usb_host_lib_set_root_port_power is used to force toggle connection, primary developed for esp32p4 -// esp32p4 is supported from IDF 5.3 -#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) -static usb_phy_handle_t phy_hdl = NULL; - -// Force connection/disconnection using PHY -static void force_conn_state(bool connected, TickType_t delay_ticks) -{ - TEST_ASSERT_NOT_EQUAL(NULL, phy_hdl); - if (delay_ticks > 0) { - // Delay of 0 ticks causes a yield. So skip if delay_ticks is 0. - vTaskDelay(delay_ticks); - } - ESP_ERROR_CHECK(usb_phy_action(phy_hdl, (connected) ? USB_PHY_ACTION_HOST_ALLOW_CONN : USB_PHY_ACTION_HOST_FORCE_DISCONN)); -} - -// Initialize the internal USB PHY to connect to the USB OTG peripheral. We manually install the USB PHY for testing -static bool install_phy(void) -{ - usb_phy_config_t phy_config = { - .controller = USB_PHY_CTRL_OTG, - .target = USB_PHY_TARGET_INT, - .otg_mode = USB_OTG_MODE_HOST, - .otg_speed = USB_PHY_SPEED_UNDEFINED, // In Host mode, the speed is determined by the connected device - }; - TEST_ASSERT_EQUAL(ESP_OK, usb_new_phy(&phy_config, &phy_hdl)); - // Return true, to skip_phy_setup during the usb_host_install() - return true; -} - -static void delete_phy(void) -{ - TEST_ASSERT_EQUAL(ESP_OK, usb_del_phy(phy_hdl)); // Tear down USB PHY - phy_hdl = NULL; -} -#else // ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) - -// Force connection/disconnection using root port power -static void force_conn_state(bool connected, TickType_t delay_ticks) -{ - if (delay_ticks > 0) { - // Delay of 0 ticks causes a yield. So skip if delay_ticks is 0. - vTaskDelay(delay_ticks); - } - ESP_ERROR_CHECK(usb_host_lib_set_root_port_power(connected)); -} - -static bool install_phy(void) -{ - // Return false, NOT to skip_phy_setup during the usb_host_install() - return false; -} - -static void delete_phy(void) {} -#endif // ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) - - -static void uac_device_callback(uac_host_device_handle_t uac_device_handle, const uac_host_device_event_t event, void *arg) -{ - if (event == UAC_HOST_DRIVER_EVENT_DISCONNECTED) { - ESP_LOGI(TAG, "UAC Device disconnected"); - TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_close(uac_device_handle)); - return; - } - // Send uac device event to the event queue - event_queue_t evt_queue = { - .event_group = UAC_DEVICE_EVENT, - .device_evt.handle = uac_device_handle, - .device_evt.event = event, - .device_evt.arg = arg - }; - // should not block here - xQueueSend(s_event_queue, &evt_queue, 0); -} - -static void uac_host_lib_callback(uint8_t addr, uint8_t iface_num, const uac_host_driver_event_t event, void *arg) -{ - // Send uac driver event to the event queue - event_queue_t evt_queue = { - .event_group = UAC_DRIVER_EVENT, - .driver_evt.addr = addr, - .driver_evt.iface_num = iface_num, - .driver_evt.event = event, - .driver_evt.arg = arg - }; - xQueueSend(s_event_queue, &evt_queue, 0); -} - -/** - * @brief Start USB Host install and handle common USB host library events while app pin not low - * - * @param[in] arg Not used - */ -static void usb_lib_task(void *arg) -{ - const bool skip_phy_setup = install_phy(); - const usb_host_config_t host_config = { - .skip_phy_setup = skip_phy_setup, - .intr_flags = ESP_INTR_FLAG_LOWMED, - }; - - TEST_ASSERT_EQUAL(ESP_OK, usb_host_install(&host_config)); - ESP_LOGI(TAG, "USB Host installed"); - xTaskNotifyGive(arg); - - bool all_clients_gone = false; - bool all_dev_free = false; - while (!all_clients_gone || !all_dev_free) { - // Start handling system events - uint32_t event_flags; - usb_host_lib_handle_events(portMAX_DELAY, &event_flags); - if (event_flags & USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS) { - printf("No more clients\n"); - usb_host_device_free_all(); - all_clients_gone = true; - } - if (event_flags & USB_HOST_LIB_EVENT_FLAGS_ALL_FREE) { - printf("All devices freed\n"); - all_dev_free = true; - } - } - - ESP_LOGI(TAG, "USB Host shutdown"); - // Clean up USB Host - vTaskDelay(10); // Short delay to allow clients clean-up - TEST_ASSERT_EQUAL(ESP_OK, usb_host_uninstall()); - delete_phy(); //Tear down USB PHY - // set bit BIT0_USB_HOST_DRIVER_REMOVED to notify driver removed - xEventGroupSetBits(s_evt_handle, BIT0_USB_HOST_DRIVER_REMOVED); - vTaskDelete(NULL); -} - -/** - * @brief Setups UAC testing - * - * - Create USB lib task - * - Install UAC Host driver - */ -void test_uac_setup(void) -{ - // create a queue to handle events - s_event_queue = xQueueCreate(16, sizeof(event_queue_t)); - TEST_ASSERT_NOT_NULL(s_event_queue); - s_evt_handle = xEventGroupCreate(); - TEST_ASSERT_NOT_NULL(s_evt_handle); - static TaskHandle_t uac_task_handle = NULL; - // create USB lib task, pass the current task handle to notify when the task is created - TEST_ASSERT_EQUAL(pdTRUE, xTaskCreatePinnedToCore(usb_lib_task, - "usb_events", - 4096, - xTaskGetCurrentTaskHandle(), - 5, &uac_task_handle, 0)); - - // install uac host driver - ulTaskNotifyTake(pdTRUE, portMAX_DELAY); - uac_host_driver_config_t uac_config = { - .create_background_task = true, - .task_priority = 5, - .stack_size = 4096, - .core_id = 0, - .callback = uac_host_lib_callback, - .callback_arg = NULL - }; - - TEST_ASSERT_EQUAL(ESP_OK, uac_host_install(&uac_config)); - ESP_LOGI(TAG, "UAC Class Driver installed"); -} - -void test_uac_queue_reset(void) -{ - xQueueReset(s_event_queue); -} - -void test_uac_teardown(bool force) -{ - if (force) { - force_conn_state(false, pdMS_TO_TICKS(1000)); - } - vTaskDelay(500); - // uninstall uac host driver - ESP_LOGI(TAG, "UAC Driver uninstall"); - TEST_ASSERT_EQUAL(ESP_OK, uac_host_uninstall()); - // Wait for USB lib task to finish - xEventGroupWaitBits(s_evt_handle, BIT0_USB_HOST_DRIVER_REMOVED, pdTRUE, pdTRUE, portMAX_DELAY); - // delete event queue and event group - vQueueDelete(s_event_queue); - vEventGroupDelete(s_evt_handle); - // delay to allow task to delete - vTaskDelay(100); -} - -void test_open_mic_device(uint8_t iface_num, uint32_t buffer_size, uint32_t buffer_threshold, uac_host_device_handle_t *uac_device_handle) -{ - // check if device params as expected - const uac_host_device_config_t dev_config = { - .addr = 1, - .iface_num = iface_num, - .buffer_size = buffer_size, - .buffer_threshold = buffer_threshold, - .callback = uac_device_callback, - .callback_arg = NULL, - }; - TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_open(&dev_config, uac_device_handle)); -} - -void test_open_spk_device(uint8_t iface_num, uint32_t buffer_size, uint32_t buffer_threshold, uac_host_device_handle_t *uac_device_handle) -{ - // check if device params as expected - const uac_host_device_config_t dev_config = { - .addr = 1, - .iface_num = iface_num, - .buffer_size = buffer_size, - .buffer_threshold = buffer_threshold, - .callback = uac_device_callback, - .callback_arg = NULL, - }; - TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_open(&dev_config, uac_device_handle)); -} - -void test_close_device(uac_host_device_handle_t uac_device_handle) -{ - TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_close(uac_device_handle)); -} - -void test_handle_dev_connection(uint8_t *iface_num, uint8_t *if_rx) -{ - event_queue_t evt_queue = {0}; - // ignore the first connected event - TEST_ASSERT_EQUAL(pdTRUE, xQueueReceive(s_event_queue, &evt_queue, portMAX_DELAY)); - TEST_ASSERT_EQUAL(UAC_DRIVER_EVENT, evt_queue.event_group); - TEST_ASSERT_EQUAL(1, evt_queue.driver_evt.addr); - if (iface_num) { - *iface_num = evt_queue.driver_evt.iface_num; - } - if (if_rx) { - *if_rx = evt_queue.driver_evt.event == UAC_HOST_DRIVER_EVENT_RX_CONNECTED ? 1 : 0; - } -} - /** * @brief Test with known UAC device, check if the device's parameters are parsed correctly * @note please modify the known device parameters if the device is changed @@ -538,7 +266,7 @@ TEST_CASE("test uac rx reading", "[uac_host][rx]") event_queue_t evt_queue = {0}; ESP_LOGI(TAG, "Start reading data from MIC"); while (1) { - if (xQueueReceive(s_event_queue, &evt_queue, portMAX_DELAY)) { + if (xQueueReceive(client_event_queue, &evt_queue, portMAX_DELAY)) { TEST_ASSERT_EQUAL(UAC_DEVICE_EVENT, evt_queue.event_group); uac_host_device_handle_t uac_device_handle = evt_queue.device_evt.handle; uac_host_device_event_t event = evt_queue.device_evt.event; @@ -676,7 +404,7 @@ TEST_CASE("test uac tx writing", "[uac_host][tx]") const uint8_t test_counter_max = 15; event_queue_t evt_queue = {0}; while (1) { - if (xQueueReceive(s_event_queue, &evt_queue, portMAX_DELAY)) { + if (xQueueReceive(client_event_queue, &evt_queue, portMAX_DELAY)) { TEST_ASSERT_EQUAL(UAC_DEVICE_EVENT, evt_queue.event_group); uac_host_device_handle_t uac_device_handle = evt_queue.device_evt.handle; uac_host_device_event_t event = evt_queue.device_evt.event; @@ -834,7 +562,7 @@ TEST_CASE("test uac tx rx loopback", "[uac_host][tx][rx]") TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_unpause(mic_device_handle)); TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_unpause(spk_device_handle)); while (1) { - if (xQueueReceive(s_event_queue, &evt_queue, portMAX_DELAY)) { + if (xQueueReceive(client_event_queue, &evt_queue, portMAX_DELAY)) { TEST_ASSERT_EQUAL(UAC_DEVICE_EVENT, evt_queue.event_group); uac_host_device_event_t event = evt_queue.device_evt.event; esp_err_t ret = ESP_FAIL; @@ -982,7 +710,7 @@ TEST_CASE("test uac tx rx loopback with disconnect", "[uac_host][tx][rx][hot-plu TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_unpause(mic_device_handle)); TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_unpause(spk_device_handle)); while (1) { - if (xQueueReceive(s_event_queue, &evt_queue, portMAX_DELAY)) { + if (xQueueReceive(client_event_queue, &evt_queue, portMAX_DELAY)) { TEST_ASSERT_EQUAL(UAC_DEVICE_EVENT, evt_queue.event_group); uac_host_device_event_t event = evt_queue.device_evt.event; esp_err_t ret = ESP_FAIL; diff --git a/host/class/uac/usb_host_uac/test_app/main/test_host_uac_common.h b/host/class/uac/usb_host_uac/test_app/main/test_host_uac_common.h new file mode 100644 index 000000000..b8dc9e6ba --- /dev/null +++ b/host/class/uac/usb_host_uac/test_app/main/test_host_uac_common.h @@ -0,0 +1,160 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" +#include "usb/uac_host.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Freertos queue for delivering RX/TX transfer events + * + * UAC_HOST_DEVICE_EVENT_RX_DONE + * UAC_HOST_DEVICE_EVENT_TX_DONE + * UAC_HOST_DEVICE_EVENT_TRANSFER_ERROR + */ +extern QueueHandle_t transfer_event_queue; + +/** + * @brief Freertos queue for delivering the rest of the UAC client events + * + * UAC_HOST_DRIVER_EVENT_DISCONNECTED + * UAC_HOST_DEVICE_EVENT_SUSPENDED + * UAC_HOST_DEVICE_EVENT_RESUMED + */ +extern QueueHandle_t client_event_queue; + +/** + * @brief Event group ID + */ +typedef enum { + APP_EVENT = 0, /* Application event*/ + UAC_DRIVER_EVENT, /* UAC driver event defined in uac_host_driver_event_t */ + UAC_DEVICE_EVENT, /* UAC device event defined in uac_host_device_event_t*/ +} event_group_t; + +/** + * @brief Event queue + */ +typedef struct { + event_group_t event_group; /* Event group ID*/ + union { + struct { + uac_host_driver_event_t event; /* UAC Host driver event type */ + uint8_t addr; /* Device address */ + uint8_t iface_num; /* Interface number */ + void *arg; /* Event argument*/ + } driver_evt; /* UAC Host driver event group */ + struct { + uac_host_device_event_t event; /* UAC Host device event type */ + uac_host_device_handle_t handle; /* Device handle*/ + void *arg; /* Event argument */ + } device_evt; /* UAC Host device event group */ + }; +} event_queue_t; + +/** + * @brief Force connection/disconnection of the device + * + * @param[in] connected connect/disconnect + * @param[in] ticks Ticks to wait before connection/disconnection + */ +void force_conn_state(bool connected, TickType_t delay_ticks); + +/** + * @brief Setups UAC testing + * + * - Create USB lib task + * - Install UAC Host driver + */ +void test_uac_setup(void); + +/** + * @brief Reset event queues + */ +void test_uac_queue_reset(void); + +/** + * @brief Tears down the UAC testing + * + * Uninstall UAC host, delete freertos primitives + */ +void test_uac_teardown(bool force); + +/** + * @brief Open microphone device + * + * @param[in] iface_num USB Interface number + * @param[in] buffer_size Audio buffer size + * @param[in] buffer_threshold Audio buffer threshold + * @param[out] uac_device_handle Handle of the opened UAC device + */ +void test_open_mic_device(uint8_t iface_num, uint32_t buffer_size, uint32_t buffer_threshold, uac_host_device_handle_t *uac_device_handle); + +/** + * @brief Open speaker device + * + * @param[in] iface_num USB Interface number + * @param[in] buffer_size Audio buffer size + * @param[in] buffer_threshold Audio buffer threshold + * @param[out] uac_device_handle Handle of the opened UAC device + */ +void test_open_spk_device(uint8_t iface_num, uint32_t buffer_size, uint32_t buffer_threshold, uac_host_device_handle_t *uac_device_handle); + +/** + * @brief Close UAC device + * + * @param[in] uac_device_handle Handle of the opened UAC device + */ +void test_close_device(uac_host_device_handle_t uac_device_handle); + +/** + * @brief Connect UAC device + * + * Wait for a device connection event, distinguish between RX/TX device + * + * @param[out] iface_num USB Interface number of the connected device + * @param[out] if_rx Type connected device (RX/TX, microphone/speaker) + */ +void test_handle_dev_connection(uint8_t *iface_num, uint8_t *if_rx); + +/** + * @brief Device callback for delivering client events + * + * @param[out] uac_device_handle Handle of the device, from which an event was delivered + * @param[out] event Actual event + * @param[out] arg Pointer to callback arguments + */ +void uac_device_callback(uac_host_device_handle_t uac_device_handle, const uac_host_device_event_t event, void *arg); + +/** + * @brief Expect USB Host client event + * + * @param[in] expected_client_event Expected client event (NULL to not expect any event) + * @param[in] ticks Ticks to wait + */ +void expect_client_event(const event_queue_t *expected_client_event, TickType_t ticks); + +/** + * @brief Expect USB Host transfer event + * + * @param[in] expected_transfer_event Expected transfer event (data RX/TX) (NULL to not expect any event) + * @param[in] ticks Ticks to wait + * @return Count of delivered events + */ +size_t expect_transfer_event(const event_queue_t *expected_transfer_event, TickType_t ticks); + +#ifdef __cplusplus +} +#endif //__cplusplus diff --git a/host/class/uac/usb_host_uac/test_app/main/test_host_uac_pm.c b/host/class/uac/usb_host_uac/test_app/main/test_host_uac_pm.c new file mode 100644 index 000000000..0fb670853 --- /dev/null +++ b/host/class/uac/usb_host_uac/test_app/main/test_host_uac_pm.c @@ -0,0 +1,1263 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" +#include "unity.h" +#include "usb/usb_host.h" +#include "usb/uac_host.h" +#include "test_host_uac_common.h" + +#ifdef UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + +const static char *TAG = "UAC_TEST_PM"; + +#define TEST_EVENT_CLIENT_WAIT_MS 500 // Wait for a client event delivery +#define TEST_EVENT_TRANSFER_WAIT_MS 1000 // Wait for a transfer event delivery + +// Expect suspend event +static const event_queue_t expect_suspend_evt = { + .event_group = UAC_DEVICE_EVENT, + .device_evt.event = UAC_HOST_DEVICE_EVENT_SUSPENDED, +}; + +// Expect resume event +static const event_queue_t expect_resume_evt = { + .event_group = UAC_DEVICE_EVENT, + .device_evt.event = UAC_HOST_DEVICE_EVENT_RESUMED, +}; + +// Expect disconnect event +static const event_queue_t expect_disconn_evt = { + .event_group = UAC_DEVICE_EVENT, + .device_evt.event = UAC_HOST_DRIVER_EVENT_DISCONNECTED, +}; + +// Expect RX done event +static const event_queue_t expect_rx_done_evt = { + .event_group = UAC_DEVICE_EVENT, + .device_evt.event = UAC_HOST_DEVICE_EVENT_RX_DONE, +}; + +// Device audio buffer settings +typedef struct { + const uint32_t threshold; /* Audio buffer threshold*/ + const uint32_t size; /* Audio buffer size*/ +} uac_dev_buf_params_t; + +// Default device audi buffer settings +static uac_dev_buf_params_t audio_buf_default = { + .threshold = 4800, + .size = 19200, +}; + +/** + * @brief: Test basic suspend/resume signaling multiple interfaces + * + * Purpose: + * - Test client suspend/resume event delivery for multiple opened interfaces + * + * Procedure: + * - Connect device and open 2 interfaces + * - Suspend the root port and expect 2 suspend events + * - Resume the root port and expect 2 resume events + * - Close both interfaces of the device + */ +TEST_CASE("Test basic suspend/resume multiple ifaces", "[uac_host][power_management]") +{ + uint8_t mic_iface_num = 0; + uint8_t spk_iface_num = 0; + uint8_t if_rx = false; + test_handle_dev_connection(&mic_iface_num, &if_rx); + if (!if_rx) { + spk_iface_num = mic_iface_num; + test_handle_dev_connection(&mic_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, true); + } else { + test_handle_dev_connection(&spk_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, false); + } + + // Open 2 interfaces of the device + uac_host_device_handle_t mic_device_handle = NULL, spk_device_handle = NULL; + test_open_mic_device(mic_iface_num, audio_buf_default.size, audio_buf_default.threshold, &mic_device_handle); + test_open_spk_device(spk_iface_num, audio_buf_default.size, audio_buf_default.threshold, &spk_device_handle); + + // Suspend the root port + printf("Issue suspend\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + + // Expect 2 Suspend events -> one for each iface + expect_client_event(&expect_suspend_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + expect_client_event(&expect_suspend_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Stay suspended for a while + vTaskDelay(100); + + // Resume the root port + printf("Issue resume\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_resume()); + + // Expect 2 Resume events -> one for each iface + expect_client_event(&expect_resume_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + expect_client_event(&expect_resume_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Close both interfaces of the device + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_close(spk_device_handle)); + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_close(mic_device_handle)); +} + +/** + * @brief: Test basic suspend/resume signaling single interface + * + * Purpose: + * - Test client suspend/resume event delivery for single opened interface + * + * Procedure: + * - Connect device and open one interface + * - Suspend the root port and expect 1 suspend event + * - Resume the root port and expect 1 resume event + * - Close the opened interface + */ +TEST_CASE("Test basic suspend/resume single iface", "[uac_host][power_management]") +{ + uint8_t mic_iface_num = 0; + uint8_t spk_iface_num = 0; + uint8_t if_rx = false; + test_handle_dev_connection(&mic_iface_num, &if_rx); + if (!if_rx) { + spk_iface_num = mic_iface_num; + test_handle_dev_connection(&mic_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, true); + } else { + test_handle_dev_connection(&spk_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, false); + } + + // Open device's interface + uac_host_device_handle_t mic_device_handle = NULL; + test_open_mic_device(mic_iface_num, audio_buf_default.size, audio_buf_default.threshold, &mic_device_handle); + + // Suspend the root port + printf("Issue suspend\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + + // Expect 1 Suspend event -> only one iface is opened + expect_client_event(&expect_suspend_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + expect_client_event(NULL, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Stay suspended for a while + vTaskDelay(100); + + // Resume the root port + printf("Issue resume\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_resume()); + + // Expect 1 Resume event -> only one iface is opened + expect_client_event(&expect_resume_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + expect_client_event(NULL, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Close the opened interface + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_close(mic_device_handle)); +} + +/** + * @brief: Test suspend/resume signaling no interface opened + * + * Purpose: + * - Test client suspend/resume event delivery with a device connected, but no interface opened by the driver + * + * Procedure: + * - Connect device, but, don't open any interface + * - Suspend the root port and expect no events (no interface opened) + * - Resume the root port and expect no events (no interface opened) + */ +TEST_CASE("Test suspend/resume no interface opened", "[uac_host][power_management]") +{ + uint8_t mic_iface_num = 0; + uint8_t spk_iface_num = 0; + uint8_t if_rx = false; + test_handle_dev_connection(&mic_iface_num, &if_rx); + if (!if_rx) { + spk_iface_num = mic_iface_num; + test_handle_dev_connection(&mic_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, true); + } else { + test_handle_dev_connection(&spk_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, false); + } + + // Suspend the root port + printf("Issue suspend\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + + // Expect NO Suspend event -> No interface was opened by the client + expect_client_event(NULL, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS * 2)); + + // Resume the root port + printf("Issue resume\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_resume()); + + // Expect NO Resume event -> No interface was opened by the client + expect_client_event(NULL, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS * 2)); +} + +/** + * @brief: Test close opened interface while the root port is in suspended state + * + * Purpose: + * - Test client reaction to closing device while the root port is in suspended state + * + * Procedure: + * - Connect device and open one interface + * - Suspend the root port and expect suspend event + * - Close the opened interface while the root port is in suspended state + */ +TEST_CASE("Test close opened interface in suspended state", "[uac_host][power_management]") +{ + uint8_t mic_iface_num = 0; + uint8_t spk_iface_num = 0; + uint8_t if_rx = false; + test_handle_dev_connection(&mic_iface_num, &if_rx); + if (!if_rx) { + spk_iface_num = mic_iface_num; + test_handle_dev_connection(&mic_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, true); + } else { + test_handle_dev_connection(&spk_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, false); + } + + // Open device's interface + uac_host_device_handle_t mic_device_handle = NULL; + test_open_mic_device(mic_iface_num, audio_buf_default.size, audio_buf_default.threshold, &mic_device_handle); + + // Suspend the root port + printf("Issue suspend\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + + // Expect 1 Suspend event -> only one iface is opened + expect_client_event(&expect_suspend_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Close opened interface, while the root port is in suspended state + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_close(mic_device_handle)); +} + +/** + * @brief: Test close opened and started interface while the root port is in suspended state + * + * Purpose: + * - Test client reaction to closing started device while the root port is in suspended state + * + * Procedure: + * - Connect device, open and start one interface + * - Suspend the root port and expect suspend event + * - Close the started interface while the root port is in suspended state + */ +TEST_CASE("Test close started interface in suspended device", "[uac_host][power_management]") +{ + uint8_t mic_iface_num = 0; + uint8_t spk_iface_num = 0; + uint8_t if_rx = false; + test_handle_dev_connection(&mic_iface_num, &if_rx); + if (!if_rx) { + spk_iface_num = mic_iface_num; + test_handle_dev_connection(&mic_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, true); + } else { + test_handle_dev_connection(&spk_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, false); + } + + // Open device's interface + uac_host_device_handle_t mic_device_handle = NULL; + test_open_mic_device(mic_iface_num, audio_buf_default.size, audio_buf_default.threshold, &mic_device_handle); + + // Get mic alt interface 1 params + uac_host_dev_alt_param_t mic_alt_params; + TEST_ASSERT_EQUAL(ESP_OK, uac_host_get_device_alt_param(mic_device_handle, 1, &mic_alt_params)); + + // Start mic interface + uac_host_stream_config_t stream_config = { + .channels = mic_alt_params.channels, + .bit_resolution = mic_alt_params.bit_resolution, + .sample_freq = mic_alt_params.sample_freq[0], + .flags = FLAG_STREAM_PAUSE_AFTER_START, + }; + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_start(mic_device_handle, &stream_config)); + + // Keep the device in started state for some time + vTaskDelay(100); + + // Suspend the root port + printf("Issue suspend\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + + // Expect 1 Suspend event -> only one iface is opened + expect_client_event(&expect_suspend_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Close opened and started interface, while the root port is in suspended state + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_close(mic_device_handle)); +} + +/** + * @brief: Test suspending/resuming the root port after starting an interface + * + * Purpose: + * - Reaction of the client to suspend/resume event while the interface has been started + * + * Procedure: + * - Connect, open and start interface + * - Suspend the root port and expect suspend event + * - Resume the root port and expect resume event + * - Stop and close the interface + */ +TEST_CASE("Test suspend/resume, after starting an interface", "[uac_host][power_management]") +{ + uint8_t mic_iface_num = 0; + uint8_t spk_iface_num = 0; + uint8_t if_rx = false; + test_handle_dev_connection(&mic_iface_num, &if_rx); + if (!if_rx) { + spk_iface_num = mic_iface_num; + test_handle_dev_connection(&mic_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, true); + } else { + test_handle_dev_connection(&spk_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, false); + } + + // Open device's interface + uac_host_device_handle_t mic_device_handle = NULL; + test_open_mic_device(mic_iface_num, audio_buf_default.size, audio_buf_default.threshold, &mic_device_handle); + + // Get mic alt interface 1 params + uac_host_dev_alt_param_t mic_alt_params; + TEST_ASSERT_EQUAL(ESP_OK, uac_host_get_device_alt_param(mic_device_handle, 1, &mic_alt_params)); + + // Start the mic interface + uac_host_stream_config_t stream_config = { + .channels = mic_alt_params.channels, + .bit_resolution = mic_alt_params.bit_resolution, + .sample_freq = mic_alt_params.sample_freq[0], + .flags = FLAG_STREAM_PAUSE_AFTER_START, + }; + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_start(mic_device_handle, &stream_config)); + + // Keep the device in started state for some time + vTaskDelay(100); + + // Suspend the root port + printf("Issue suspend\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + + // Expect 1 Suspend event -> only one iface is opened + expect_client_event(&expect_suspend_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + expect_client_event(NULL, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Stay suspended for a while + vTaskDelay(100); + + // Resume the root port + printf("Issue resume\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_resume()); + + // Expect 1 Resume event -> only one iface is opened + expect_client_event(&expect_resume_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + expect_client_event(NULL, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Expect the interface to be started (to be in UAC_INTERFACE_STATE_READY), stop the device + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_stop(mic_device_handle)); + // Close opened and already stopped interface + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_close(mic_device_handle)); +} + +/** + * @brief: Test auto-resume suspended root port by ctrl transfer + * + * Purpose: + * - Reaction of the client to auto-resume by a transfer submit + * + * Procedure: + * - Connect, open and start interface + * - Suspend the root port and expect suspend event + * - Send a ctrl transfer to a device, while the root port is in suspended state + * - Expect resume event, caused by auto resume by transfer submit + * - Unpause the interface and expect transfer events, pause the interface and expect no transfer events + * - Stop and close the interface + */ +TEST_CASE("Test auto-resume by ctrl transfer", "[uac_host][power_management]") +{ + uint8_t mic_iface_num = 0; + uint8_t spk_iface_num = 0; + uint8_t if_rx = false; + test_handle_dev_connection(&mic_iface_num, &if_rx); + if (!if_rx) { + spk_iface_num = mic_iface_num; + test_handle_dev_connection(&mic_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, true); + } else { + test_handle_dev_connection(&spk_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, false); + } + + // Open device's interface + uac_host_device_handle_t mic_device_handle = NULL; + test_open_mic_device(mic_iface_num, audio_buf_default.size, audio_buf_default.threshold, &mic_device_handle); + + // Get mic alt interface 1 params + uac_host_dev_alt_param_t mic_alt_params; + TEST_ASSERT_EQUAL(ESP_OK, uac_host_get_device_alt_param(mic_device_handle, 1, &mic_alt_params)); + + // Start the device + uac_host_stream_config_t stream_config = { + .channels = mic_alt_params.channels, + .bit_resolution = mic_alt_params.bit_resolution, + .sample_freq = mic_alt_params.sample_freq[0], + .flags = FLAG_STREAM_PAUSE_AFTER_START, + }; + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_start(mic_device_handle, &stream_config)); + + // Keep the device in started state for some time + vTaskDelay(10); + + // Suspend the root port + printf("Issue suspend\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + + // Expect 1 Suspend event -> only one iface is opened + expect_client_event(&expect_suspend_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Stay suspended for a while + vTaskDelay(100); + + // Send ctrl transfer to auto resume the device + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_set_mute(mic_device_handle, 0)); + + // Expect 1 Resume event (resume by transfer submit) -> only one iface is opened + expect_client_event(&expect_resume_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Some time for the usb host lib to process the auto resume by transfer + vTaskDelay(100); + + // Unpause the interface + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_unpause(mic_device_handle)); + + // Expect transfer events after unpausing the interface + TEST_ASSERT(expect_transfer_event(&expect_rx_done_evt, pdMS_TO_TICKS(TEST_EVENT_TRANSFER_WAIT_MS))); + + // Expect the interface to be active (to be in UAC_INTERFACE_STATE_READY), + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_pause(mic_device_handle)); + + // Expect no transfer events after pausing the interface + TEST_ASSERT_FALSE(expect_transfer_event(NULL, pdMS_TO_TICKS(TEST_EVENT_TRANSFER_WAIT_MS))); + + // Stop and close the interface + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_stop(mic_device_handle)); + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_close(mic_device_handle)); +} + +/** + * @brief: Test start an interface while the root port is suspended, FLAG_STREAM_PAUSE_AFTER_START flag is set + * + * Purpose: + * - Reaction of the client to starting the interface multiple times, when the root port is in suspended state + * - Usage of FLAG_STREAM_PAUSE_AFTER_START flag in combination with suspended state + * + * Procedure: + * - Connect and open one interface + * - Suspend the root port and expect suspend event + * - Fail to start the interface while the root port is in suspended state + * - Resume the root port manually and expect resume event + * - Start the interface normally with the FLAG_STREAM_PAUSE_AFTER_START set + * - Suspend the root port and expect suspend event + * - Start previously started interface while the root port is in suspended state + * - Expect resume event, caused by auto resume by transfer submit + * - Stop and close the interface + */ +TEST_CASE("Test start (pause after start) suspended interface", "[uac_host][power_management]") +{ + uint8_t mic_iface_num = 0; + uint8_t spk_iface_num = 0; + uint8_t if_rx = false; + test_handle_dev_connection(&mic_iface_num, &if_rx); + if (!if_rx) { + spk_iface_num = mic_iface_num; + test_handle_dev_connection(&mic_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, true); + } else { + test_handle_dev_connection(&spk_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, false); + } + + // Open device's interface + uac_host_device_handle_t mic_device_handle = NULL; + test_open_mic_device(mic_iface_num, audio_buf_default.size, audio_buf_default.threshold, &mic_device_handle); + + // Get mic alt interface 1 params + uac_host_dev_alt_param_t mic_alt_params; + TEST_ASSERT_EQUAL(ESP_OK, uac_host_get_device_alt_param(mic_device_handle, 1, &mic_alt_params)); + + // Suspend the root port + printf("Issue suspend\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + + // Expect 1 Suspend event -> only one iface is opened + expect_client_event(&expect_suspend_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Stay suspended for a while + vTaskDelay(100); + + uac_host_stream_config_t stream_config = { + .channels = mic_alt_params.channels, + .bit_resolution = mic_alt_params.bit_resolution, + .sample_freq = mic_alt_params.sample_freq[0], + .flags = FLAG_STREAM_PAUSE_AFTER_START, + }; + // Try to start device's interface from suspended state, when the previous state was IDLE + // Expect to fail, since the usb_host_lib must claim device's EPs, for which the root port must not be in suspended state + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_STATE, uac_host_device_start(mic_device_handle, &stream_config)); + + // Resume the root port + printf("Issue resume\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_resume()); + + // Expect 1 Resume event -> only one iface is opened + expect_client_event(&expect_resume_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Start the device's interface with the device in resumed state + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_start(mic_device_handle, &stream_config)); + + // Suspend the root port + printf("Issue suspend\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + + // Expect 1 Suspend event -> only one iface is opened + expect_client_event(&expect_suspend_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Start the interface with the root port in suspended state (after it has already been successfully started) + // The function issues a ctrl transfer to the device, which effectively resumes the root port + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_start(mic_device_handle, &stream_config)); + + // Expect 1 Resume event (resume by transfer submit) -> only one iface is opened + expect_client_event(&expect_resume_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Some time for the usb_host_lib to process the auto resume by transfer + vTaskDelay(100); + + // Expect the interface to be started (in UAC_INTERFACE_STATE_READY), stop the interface + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_stop(mic_device_handle)); + // Close opened and stopped interface + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_close(mic_device_handle)); +} + +/** + * @brief: Test start an interface while the root port is suspended, FLAG_STREAM_PAUSE_AFTER_START flag not set + * + * Purpose: + * - Reaction of the client to starting the interface multiple times, when the root port is in suspended state + * - Usage of FLAG_STREAM_PAUSE_AFTER_START flag in combination with suspended state + * + * Procedure: + * - Connect and open one interface + * - Suspend the root port and expect suspend event + * - Fail to start the interface while the root port is in suspended state + * - Resume the root port manually and expect resume event + * - Start the interface normally with the FLAG_STREAM_PAUSE_AFTER_START not set + * - Expect transfer events for the interface (interface has been unpaused after start) + * - Suspend the root port and expect suspend event + * - Expect NO transfer events for the interface + * - Start previously started interface while the root port is in suspended state + * - Expect resume event, caused by auto resume by transfer submit + * - Expect transfer events for the interface + * - Stop and close the interface + */ +TEST_CASE("Test start (unpause after start) suspended interface", "[uac_host][power_management]") +{ + uint8_t mic_iface_num = 0; + uint8_t spk_iface_num = 0; + uint8_t if_rx = false; + test_handle_dev_connection(&mic_iface_num, &if_rx); + if (!if_rx) { + spk_iface_num = mic_iface_num; + test_handle_dev_connection(&mic_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, true); + } else { + test_handle_dev_connection(&spk_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, false); + } + + // Open device's interface + uac_host_device_handle_t mic_device_handle = NULL; + test_open_mic_device(mic_iface_num, audio_buf_default.size, audio_buf_default.threshold, &mic_device_handle); + + // Get mic alt interface 1 params + uac_host_dev_alt_param_t mic_alt_params; + TEST_ASSERT_EQUAL(ESP_OK, uac_host_get_device_alt_param(mic_device_handle, 1, &mic_alt_params)); + + // Suspend the root port + printf("Issue suspend\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + + // Expect 1 Suspend event -> only one iface is opened + expect_client_event(&expect_suspend_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Stay suspended for a while + vTaskDelay(100); + + uac_host_stream_config_t stream_config = { + .channels = mic_alt_params.channels, + .bit_resolution = mic_alt_params.bit_resolution, + .sample_freq = mic_alt_params.sample_freq[0], + }; + // Try to start interface from suspended state, when the previous state was IDLE + // Expect to fail, since the usb_host_lib must claim device's EPs, for which the root port must not be in suspended state + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_STATE, uac_host_device_start(mic_device_handle, &stream_config)); + + // Resume the root port + printf("Issue resume\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_resume()); + + // Expect 1 Resume event -> only one iface is opened + expect_client_event(&expect_resume_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Start the interface with the root port in resumed state + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_start(mic_device_handle, &stream_config)); + + // Interface has been started without FLAG_STREAM_PAUSE_AFTER_START flag, expect transfer events after starting the device + TEST_ASSERT(expect_transfer_event(&expect_rx_done_evt, pdMS_TO_TICKS(TEST_EVENT_TRANSFER_WAIT_MS))); + + // Suspend the root port + printf("Issue suspend\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + + // Expect 1 Suspend event -> only one iface is opened + expect_client_event(&expect_suspend_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Expect no transfer events + TEST_ASSERT_FALSE(expect_transfer_event(NULL, pdMS_TO_TICKS(TEST_EVENT_TRANSFER_WAIT_MS))); + + // Start the interface with the root port in suspended state (after it has been already successfully started) + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_start(mic_device_handle, &stream_config)); + + // Expect 1 Resume event (resume by transfer submit) -> only one iface is opened + expect_client_event(&expect_resume_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Expect transfer events after starting the device + TEST_ASSERT(expect_transfer_event(&expect_rx_done_evt, pdMS_TO_TICKS(TEST_EVENT_TRANSFER_WAIT_MS))); + + // Expect the interface to be started (in UAC_INTERFACE_STATE_READY), stop the interface + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_stop(mic_device_handle)); + // Close the opened and stopped interface + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_close(mic_device_handle)); +} + +/** + * @brief: Test stopping interface from suspended state + * + * Purpose: + * - Reaction of the client to stopping paused interface, while the root port is in suspended state + * - Reaction of the client to stopping unpaused interface, while the root port is in suspended state + * + * Procedure: + * - Connect, open and start (pause after start) interface + * - Suspend the root port and expect suspend event + * - Stop the interface while the root port is in suspended state + * - Resume the root port and expect resume event + * - Start, unpause the interface and make sure the data are being transferred + * - Suspend the root port and expect suspend event + * - Stop and close the interface while the root port is in suspended state + */ +TEST_CASE("Test interface stop from suspended state", "[uac_host][power_management][rx]") +{ + uint8_t mic_iface_num = 0; + uint8_t spk_iface_num = 0; + uint8_t if_rx = false; + test_handle_dev_connection(&mic_iface_num, &if_rx); + if (!if_rx) { + spk_iface_num = mic_iface_num; + test_handle_dev_connection(&mic_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, true); + } else { + test_handle_dev_connection(&spk_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, false); + } + + // Open device's interface + uac_host_device_handle_t mic_device_handle = NULL; + test_open_mic_device(mic_iface_num, audio_buf_default.size, audio_buf_default.threshold, &mic_device_handle); + + // Get mic alt interface 1 params + uac_host_dev_alt_param_t mic_alt_params; + TEST_ASSERT_EQUAL(ESP_OK, uac_host_get_device_alt_param(mic_device_handle, 1, &mic_alt_params)); + + // Start the interface + uac_host_stream_config_t stream_config = { + .channels = mic_alt_params.channels, + .bit_resolution = mic_alt_params.bit_resolution, + .sample_freq = mic_alt_params.sample_freq[0], + .flags = FLAG_STREAM_PAUSE_AFTER_START, + }; + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_start(mic_device_handle, &stream_config)); + + // Suspend the root port and expect 1 Suspend event -> only one iface is opened + printf("Issue suspend\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + expect_client_event(&expect_suspend_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Stay suspended for a while + vTaskDelay(100); + + // Stop the started interface while the root port is suspended + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_stop(mic_device_handle)); + + // Resume the root port and expect 1 Resume event -> only one iface is opened + printf("Issue resume\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_resume()); + expect_client_event(&expect_resume_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Start and unpause the interface + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_start(mic_device_handle, &stream_config)); + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_unpause(mic_device_handle)); + + // Check that data events are being sent + TEST_ASSERT(expect_transfer_event(&expect_rx_done_evt, pdMS_TO_TICKS(TEST_EVENT_TRANSFER_WAIT_MS))); + + // Suspend the root port and expect 1 Suspend event -> only one iface is opened + printf("Issue suspend\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + expect_client_event(&expect_suspend_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Stop and close unpaused interface while the root port is in suspended state + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_stop(mic_device_handle)); + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_close(mic_device_handle)); +} + +/** + * @brief: Test unpause interface, which was previously in ready state while the root port is in suspended state + * + * Purpose: + * - Reaction of the client to unpausing ready interface, while the root port is in suspended state + * + * Procedure: + * - Connect, open and start (pause after start) interface + * - Suspend the root port and expect suspend event + * - Unpause started interface while the root port is in suspended state + * - Expect resume event, caused by ctrl transfer submit, as the uac_host_device_unpause issues a ctrl transfer + * - Expect data transfer events from the device + * - Pause, stop and close the interface + */ +TEST_CASE("Test unpause ready interface from suspended state", "[uac_host][power_management][rx]") +{ + uint8_t mic_iface_num = 0; + uint8_t spk_iface_num = 0; + uint8_t if_rx = false; + test_handle_dev_connection(&mic_iface_num, &if_rx); + if (!if_rx) { + spk_iface_num = mic_iface_num; + test_handle_dev_connection(&mic_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, true); + } else { + test_handle_dev_connection(&spk_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, false); + } + + // Open device's interface + uac_host_device_handle_t mic_device_handle = NULL; + test_open_mic_device(mic_iface_num, audio_buf_default.size, audio_buf_default.threshold, &mic_device_handle); + + // Get mic alt interface 1 params + uac_host_dev_alt_param_t mic_alt_params; + TEST_ASSERT_EQUAL(ESP_OK, uac_host_get_device_alt_param(mic_device_handle, 1, &mic_alt_params)); + + // Start the interface + uac_host_stream_config_t stream_config = { + .channels = mic_alt_params.channels, + .bit_resolution = mic_alt_params.bit_resolution, + .sample_freq = mic_alt_params.sample_freq[0], + .flags = FLAG_STREAM_PAUSE_AFTER_START, + }; + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_start(mic_device_handle, &stream_config)); + + // Suspend the root port and expect 1 Suspend event -> only one iface is opened + printf("Issue suspend\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + expect_client_event(&expect_suspend_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Unpause started interface while the root port is in suspended state + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_unpause(mic_device_handle)); + + // Expect 1 Resume event (resume by transfer submit) -> only one iface is opened + expect_client_event(&expect_resume_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Check that data events are being sent + TEST_ASSERT(expect_transfer_event(&expect_rx_done_evt, pdMS_TO_TICKS(TEST_EVENT_TRANSFER_WAIT_MS))); + + // Pause, stop and close the interface + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_pause(mic_device_handle)); + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_stop(mic_device_handle)); + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_close(mic_device_handle)); +} + +/** + * @brief: Test unpause interface, which was previously in active state while the root port is in suspended state + * + * Purpose: + * - Reaction of the client to unpausing active interface, while the root port is in suspended state + * + * Procedure: + * - Connect, open and start (unpause after start) interface + * - Expect data transfer events + * - Suspend the root port and expect suspend event + * - Expect no data transfer events + * - Unpause the interface, which was previously unpaused, while the root port is in suspended state + * - Expect resume event, caused by ctrl transfer submit, as the uac_host_device_unpause issues a ctrl transfer + * - Expect data transfer events + * - Pause, stop and close the interface + */ +TEST_CASE("Test unpause active interface from suspended state", "[uac_host][power_management][rx]") +{ + uint8_t mic_iface_num = 0; + uint8_t spk_iface_num = 0; + uint8_t if_rx = false; + test_handle_dev_connection(&mic_iface_num, &if_rx); + if (!if_rx) { + spk_iface_num = mic_iface_num; + test_handle_dev_connection(&mic_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, true); + } else { + test_handle_dev_connection(&spk_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, false); + } + + // Open device's interface + uac_host_device_handle_t mic_device_handle = NULL; + test_open_mic_device(mic_iface_num, audio_buf_default.size, audio_buf_default.threshold, &mic_device_handle); + + // Get mic alt interface 1 params + uac_host_dev_alt_param_t mic_alt_params; + TEST_ASSERT_EQUAL(ESP_OK, uac_host_get_device_alt_param(mic_device_handle, 1, &mic_alt_params)); + + // Start and unpause the interface + uac_host_stream_config_t stream_config = { + .channels = mic_alt_params.channels, + .bit_resolution = mic_alt_params.bit_resolution, + .sample_freq = mic_alt_params.sample_freq[0], + .flags = FLAG_STREAM_PAUSE_AFTER_START, + }; + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_start(mic_device_handle, &stream_config)); + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_unpause(mic_device_handle)); + + // Check that data events are being sent + TEST_ASSERT(expect_transfer_event(&expect_rx_done_evt, pdMS_TO_TICKS(TEST_EVENT_TRANSFER_WAIT_MS))); + + // Suspend the root port and expect 1 Suspend event -> only one iface is opened + printf("Issue suspend\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + expect_client_event(&expect_suspend_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Check that data events are NOT being sent anymore + TEST_ASSERT_FALSE(expect_transfer_event(NULL, pdMS_TO_TICKS(TEST_EVENT_TRANSFER_WAIT_MS))); + + // Unpause interface from an active state, while the root port is in suspended state + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_unpause(mic_device_handle)); + + // Expect 1 Resume event (resume by transfer submit) -> only one iface is opened + expect_client_event(&expect_resume_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Check that data events are being sent + TEST_ASSERT(expect_transfer_event(&expect_rx_done_evt, pdMS_TO_TICKS(TEST_EVENT_TRANSFER_WAIT_MS))); + + // Pause, stop and close the interface + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_pause(mic_device_handle)); + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_stop(mic_device_handle)); + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_close(mic_device_handle)); +} + +/** + * @brief: Test pause interface, which was previously in active state while the root por is in suspended state + * + * Purpose: + * - Reaction of the client to pausing active interface, while the root port is in suspended state + * + * Procedure: + * - Connect, open and start (unpause after start) interface + * - Expect data transfer events + * - Suspend the root port and expect suspend event + * - Expect no data transfer events + * - Pause the interface which was previously unpaused, while the root port is in suspended state + * - Resume the root port and expect resume event + * - Unpause the interface and expect data transfer events + * - Pause, stop and close the interface + */ +TEST_CASE("Test pause active interface from suspended state", "[uac_host][power_management][rx]") +{ + uint8_t mic_iface_num = 0; + uint8_t spk_iface_num = 0; + uint8_t if_rx = false; + test_handle_dev_connection(&mic_iface_num, &if_rx); + if (!if_rx) { + spk_iface_num = mic_iface_num; + test_handle_dev_connection(&mic_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, true); + } else { + test_handle_dev_connection(&spk_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, false); + } + + // Open device's interface + uac_host_device_handle_t mic_device_handle = NULL; + test_open_mic_device(mic_iface_num, audio_buf_default.size, audio_buf_default.threshold, &mic_device_handle); + + // Get mic alt interface 1 params + uac_host_dev_alt_param_t mic_alt_params; + TEST_ASSERT_EQUAL(ESP_OK, uac_host_get_device_alt_param(mic_device_handle, 1, &mic_alt_params)); + + // Start and unpause the interface + uac_host_stream_config_t stream_config = { + .channels = mic_alt_params.channels, + .bit_resolution = mic_alt_params.bit_resolution, + .sample_freq = mic_alt_params.sample_freq[0], + .flags = FLAG_STREAM_PAUSE_AFTER_START, + }; + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_start(mic_device_handle, &stream_config)); + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_unpause(mic_device_handle)); + + // Check that data events are being sent + TEST_ASSERT(expect_transfer_event(&expect_rx_done_evt, pdMS_TO_TICKS(TEST_EVENT_TRANSFER_WAIT_MS))); + + // Suspend the root port and expect 1 Suspend event -> only one iface is opened + printf("Issue suspend\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + expect_client_event(&expect_suspend_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Check that data events are NOT being sent anymore + TEST_ASSERT_FALSE(expect_transfer_event(NULL, pdMS_TO_TICKS(TEST_EVENT_TRANSFER_WAIT_MS))); + + // Pause previously unpaused interface, while the root port is in suspended state + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_pause(mic_device_handle)); + + // Resume the root port and expect 1 Resume event -> only one iface is opened + printf("Issue resume\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_resume()); + expect_client_event(&expect_resume_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Unpause the interface back + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_unpause(mic_device_handle)); + + // Check that data events are being sent + TEST_ASSERT(expect_transfer_event(&expect_rx_done_evt, pdMS_TO_TICKS(TEST_EVENT_TRANSFER_WAIT_MS))); + + // Pause, stop and close the interface + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_pause(mic_device_handle)); + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_stop(mic_device_handle)); + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_close(mic_device_handle)); +} + +/** + * @brief Notification events used to control the RX data handling task from the test case + */ +enum test_notify_event { + TEST_NOTIFY_RX_START, /**< Start receiving data from device */ + TEST_NOTIFY_RX_STOP, /**< Stop receiving data from device */ + TEST_NOTIFY_DEVICE_SUSPENDED, /**< Device suspended, stop expecting new data from device */ + TEST_NOTIFY_FINISH, /**< Finish the test: close device and task cleanup */ +}; + +#define TEST_SUPEND_RESUME_HOLD_MS 1000 // Time to keep the device suspended or resumed + +/** + * @brief RX data handling task + * + * Open a device and start receiving data from the device's ring buffer + * Processing task is controlled from the main task by test_notify_event + */ +static void test_rx_data_handling_task(void *args) +{ + TaskHandle_t main_task_hdl = (TaskHandle_t)args; + uint8_t mic_iface_num = 0; + uint8_t spk_iface_num = 0; + uint8_t if_rx = false; + test_handle_dev_connection(&mic_iface_num, &if_rx); + if (!if_rx) { + spk_iface_num = mic_iface_num; + test_handle_dev_connection(&mic_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, true); + } else { + test_handle_dev_connection(&spk_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, false); + } + + // Open device's interface + uac_host_device_handle_t mic_device_handle = NULL; + test_open_mic_device(mic_iface_num, audio_buf_default.size, audio_buf_default.threshold, &mic_device_handle); + + // Get mic alt interface 1 params + uac_host_dev_alt_param_t mic_alt_params; + TEST_ASSERT_EQUAL(ESP_OK, uac_host_get_device_alt_param(mic_device_handle, 1, &mic_alt_params)); + + // Start and pause the device + uac_host_stream_config_t stream_config = { + .channels = mic_alt_params.channels, + .bit_resolution = mic_alt_params.bit_resolution, + .sample_freq = mic_alt_params.sample_freq[0], + .flags = FLAG_STREAM_PAUSE_AFTER_START, + }; + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_start(mic_device_handle, &stream_config)); + + // Allocate RX buffer + uint8_t *rx_buffer = (uint8_t *)calloc(1, audio_buf_default.threshold); + TEST_ASSERT_NOT_NULL(rx_buffer); + uint32_t rx_size = 0; + + // Notify the main task, that the device is connected and initialized + xTaskNotifyGive(main_task_hdl); + + // Start RX data handling loop + while (!ulTaskNotifyTakeIndexed(TEST_NOTIFY_FINISH, pdTRUE, 0)) { + + // Wait for notification from the main task to start receiving data from the device + TEST_ASSERT_EQUAL(pdTRUE, ulTaskNotifyTakeIndexed(TEST_NOTIFY_RX_START, pdTRUE, pdMS_TO_TICKS(2000))); + TEST_ASSERT_EQUAL_MESSAGE(ESP_OK, uac_host_device_unpause(mic_device_handle), "Device unpause not successful"); + + // Start receiving data, until the main task does not set task notification to stop receiving data + while (!ulTaskNotifyTakeIndexed(TEST_NOTIFY_RX_STOP, pdTRUE, 0)) { + event_queue_t transfer_event; + + if (xQueueReceive(transfer_event_queue, &transfer_event, pdMS_TO_TICKS(TEST_EVENT_TRANSFER_WAIT_MS)) == pdPASS) { + + // Validate data received from the transfer event queue + TEST_ASSERT_EQUAL_MESSAGE(transfer_event.event_group, UAC_DEVICE_EVENT, "Incorrect event group"); + TEST_ASSERT_EQUAL_MESSAGE(transfer_event.device_evt.event, UAC_HOST_DEVICE_EVENT_RX_DONE, "Incorrect event type"); + + // Read data from the device's ring buffer + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_read(mic_device_handle, rx_buffer, audio_buf_default.threshold, &rx_size, 0)); + TEST_ASSERT_EQUAL(audio_buf_default.threshold, rx_size); + ESP_LOGD(TAG, "New RX event"); + + } else { + if ( pdTRUE == ulTaskNotifyTakeIndexed(TEST_NOTIFY_DEVICE_SUSPENDED, pdTRUE, 0)) { + // Device was suspended while receiving data, exit loop from here + break; + } else { + TEST_FAIL_MESSAGE("RX event not received on time"); + } + } + } + } + + // Cleanup + printf("Task cleanup\n"); + TEST_ASSERT_EQUAL_MESSAGE(ESP_OK, uac_host_device_pause(mic_device_handle), "Device can't be paused"); + TEST_ASSERT_EQUAL_MESSAGE(ESP_OK, uac_host_device_stop(mic_device_handle), "Device can't be stopped"); + TEST_ASSERT_EQUAL_MESSAGE(ESP_OK, uac_host_device_close(mic_device_handle), "Device can't be closed"); + free(rx_buffer); + xTaskNotifyGive(main_task_hdl); + vTaskDelete(NULL); +} + +/** + * @brief: Test suspend/resume the root port while the host is receiving data from the device + * + * Purpose: + * - Reaction of the client to continuously suspend/resume the root port while the data reception is ongoing + * - Concurrent task access + * + * Procedure: + * - Create data handling task, where connect, open and start (pause after start) interface + * - The task is controlled from the main task in the test case + * - The test case is repeatedly suspending and resuming the root port + * - The data handling task is receiving data from the device + */ +TEST_CASE("Test suspend/resume while receiving data", "[uac_host][power_management]") +{ + // Create RX data handling task + TaskHandle_t data_handling_task_hdl = NULL; + TEST_ASSERT_EQUAL(pdPASS, xTaskCreate(test_rx_data_handling_task, "rx_data_handing", 4096, (void *)xTaskGetCurrentTaskHandle(), 4, &data_handling_task_hdl)); + TEST_ASSERT_NOT_NULL(data_handling_task_hdl); + + // Wait for the device to be connected and initialized by the data handling task + TEST_ASSERT_EQUAL_MESSAGE(pdTRUE, ulTaskNotifyTake(true, pdMS_TO_TICKS(2000)), "Device was not initialized on time"); + + // Start the RX data handling -> suspend the root port while receiving data -> resume the root port -> Start the RX data handling + for (int i = 0; i < 5; i++) { + + // Start the data handling and keep it running for a while + xTaskNotifyGiveIndexed(data_handling_task_hdl, TEST_NOTIFY_RX_START); + vTaskDelay(pdMS_TO_TICKS(TEST_SUPEND_RESUME_HOLD_MS * 2)); + + // Suspend the root port and wait for the suspend event + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + expect_client_event(&expect_suspend_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); // expect suspend event + + // Notify the data handling task, that the device is suspended, to stop expecting new data + xTaskNotifyGiveIndexed(data_handling_task_hdl, TEST_NOTIFY_DEVICE_SUSPENDED); + + // Keep the device suspended for a while + vTaskDelay(pdMS_TO_TICKS(TEST_SUPEND_RESUME_HOLD_MS)); + + // Resume the root port and wait for the resume event + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_resume()); + expect_client_event(&expect_resume_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); // expect resume event + } + + // Start the data handling again and keep it running for a while + xTaskNotifyGiveIndexed(data_handling_task_hdl, TEST_NOTIFY_RX_START); + vTaskDelay(pdMS_TO_TICKS(TEST_SUPEND_RESUME_HOLD_MS)); + + // Stop and close the data handling + xTaskNotifyGiveIndexed(data_handling_task_hdl, TEST_NOTIFY_RX_STOP); + xTaskNotifyGiveIndexed(data_handling_task_hdl, TEST_NOTIFY_FINISH); + + // Wait for the cleanup of the data handling task + TEST_ASSERT_EQUAL_MESSAGE(pdTRUE, ulTaskNotifyTake(true, pdMS_TO_TICKS(2000)), "RX data handling task was not deleted on time"); + vTaskDelay(10); +} + + +#define TEST_UVC_SUSPEND_TIMER_INTERVAL_MS 1000 +#define TEST_UVC_SUSPEND_TIMER_MARGIN_MS 50 + +/** + * @brief: Test automatic suspend timer with the UAC Host + * + * Purpose: + * - Reaction of the client to one-shot and periodic auto suspend timer + * + * Procedure: + * - Connect, open and start (unpause after start) interface + * - Set auto suspend timer to one-shot timer and expect suspend event within the set interval + * - Resume the root port and expect resume event + * - Set auto suspend timer to periodic timer and expect suspend events periodically + * - Stop periodical timer + * - Close the interface + */ +TEST_CASE("Automatic suspend timer", "[uac_host][power_management]") +{ + uint8_t mic_iface_num = 0; + uint8_t spk_iface_num = 0; + uint8_t if_rx = false; + test_handle_dev_connection(&mic_iface_num, &if_rx); + if (!if_rx) { + spk_iface_num = mic_iface_num; + test_handle_dev_connection(&mic_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, true); + } else { + test_handle_dev_connection(&spk_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, false); + } + + // Open device's interface + uac_host_device_handle_t mic_device_handle = NULL; + test_open_mic_device(mic_iface_num, audio_buf_default.size, audio_buf_default.threshold, &mic_device_handle); + + // Get mic alt interface 1 params + uac_host_dev_alt_param_t mic_alt_params; + TEST_ASSERT_EQUAL(ESP_OK, uac_host_get_device_alt_param(mic_device_handle, 1, &mic_alt_params)); + + // Start the interface + uac_host_stream_config_t stream_config = { + .channels = mic_alt_params.channels, + .bit_resolution = mic_alt_params.bit_resolution, + .sample_freq = mic_alt_params.sample_freq[0], + .flags = FLAG_STREAM_PAUSE_AFTER_START, + }; + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_start(mic_device_handle, &stream_config)); + + // Set one-shot suspend timer and expect suspend event within the suspend timer interval + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_set_auto_suspend(USB_HOST_LIB_AUTO_SUSPEND_ONE_SHOT, TEST_UVC_SUSPEND_TIMER_INTERVAL_MS)); + expect_client_event(&expect_suspend_evt, pdMS_TO_TICKS(TEST_UVC_SUSPEND_TIMER_INTERVAL_MS + TEST_UVC_SUSPEND_TIMER_MARGIN_MS)); + + // Manually resume the root port and wait for the resume event + printf("Issue resume\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_resume()); + expect_client_event(&expect_resume_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Make sure no other event is delivered, + // since the timer is a One-Shot timer and it shall not automatically suspend the root port again + expect_client_event(NULL, pdMS_TO_TICKS(TEST_UVC_SUSPEND_TIMER_INTERVAL_MS * 2)); + + // Set Periodic auto suspend timer + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_set_auto_suspend(USB_HOST_LIB_AUTO_SUSPEND_PERIODIC, TEST_UVC_SUSPEND_TIMER_INTERVAL_MS)); + + // Expect auto-suspend timer event -> Resume the root port and expect the resume event -> Verify data transmit + for (int i = 0; i < 3; i++) { + // Expect suspend event from auto suspend timer + expect_client_event(&expect_suspend_evt, pdMS_TO_TICKS(TEST_UVC_SUSPEND_TIMER_INTERVAL_MS + TEST_UVC_SUSPEND_TIMER_MARGIN_MS)); + + // Manually resume the root port and wait for the resume event + printf("Issue resume\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_resume()); + expect_client_event(&expect_resume_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Verify data transmit on resumed device + uint8_t volume; + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_get_volume(mic_device_handle, &volume)); + } + + // Disable the Periodic auto suspend timer + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_set_auto_suspend(USB_HOST_LIB_AUTO_SUSPEND_PERIODIC, 0)); + + // Make sure no other event is delivered + expect_client_event(NULL, pdMS_TO_TICKS(TEST_UVC_SUSPEND_TIMER_INTERVAL_MS * 2)); + + // Close the interface + TEST_ASSERT_EQUAL(ESP_OK, uac_host_device_close(mic_device_handle)); +} + +/** + * @brief: Test suspend/resume with sudden disconnect + * + * Purpose: + * - Test client suspend/resume event delivery when the device is disconnected from the root port in suspended state + * + * Procedure: + * - Connect and open 2 interfaces + * - Suspend the root port and expect 2 suspend events + * - Disconnect the root port and expect 2 disconnect events + */ +TEST_CASE("Test suspend/resume sudden disconnect", "[uac_host][power_management]") +{ + uint8_t mic_iface_num = 0; + uint8_t spk_iface_num = 0; + uint8_t if_rx = false; + test_handle_dev_connection(&mic_iface_num, &if_rx); + if (!if_rx) { + spk_iface_num = mic_iface_num; + test_handle_dev_connection(&mic_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, true); + } else { + test_handle_dev_connection(&spk_iface_num, &if_rx); + TEST_ASSERT_EQUAL(if_rx, false); + } + + // Open device + uac_host_device_handle_t mic_device_handle = NULL, spk_device_handle = NULL; + test_open_mic_device(mic_iface_num, audio_buf_default.size, audio_buf_default.threshold, &mic_device_handle); + test_open_spk_device(spk_iface_num, audio_buf_default.size, audio_buf_default.threshold, &spk_device_handle); + + // Suspend the root port + printf("Issue suspend\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + + // Expect 2 Suspend events -> each for each interface + expect_client_event(&expect_suspend_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + expect_client_event(&expect_suspend_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + + // Sudden disconnect + force_conn_state(false, 0); + + // Expect 2 Disconnected events -> each for each interface + expect_client_event(&expect_disconn_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); + expect_client_event(&expect_disconn_evt, pdMS_TO_TICKS(TEST_EVENT_CLIENT_WAIT_MS)); +} + +#endif // UAC_HOST_SUSPEND_RESUME_API_SUPPORTED diff --git a/host/class/uac/usb_host_uac/test_app/pytest_usb_host_uac.py b/host/class/uac/usb_host_uac/test_app/pytest_usb_host_uac.py index f38d4db2d..dfedc7374 100644 --- a/host/class/uac/usb_host_uac/test_app/pytest_usb_host_uac.py +++ b/host/class/uac/usb_host_uac/test_app/pytest_usb_host_uac.py @@ -10,5 +10,5 @@ @idf_parametrize('target', ['esp32s2', 'esp32s3', 'esp32p4'], indirect=['target']) def test_usb_host_uac(dut: IdfDut) -> None: dut.expect_exact('Press ENTER to see the list of tests.') - dut.write('[uac_host]') + dut.write('[power_management]') dut.expect_unity_test_output(timeout = 3000) diff --git a/host/class/uac/usb_host_uac/test_app/sdkconfig.defaults b/host/class/uac/usb_host_uac/test_app/sdkconfig.defaults index 82b96f297..7a0d807a8 100644 --- a/host/class/uac/usb_host_uac/test_app/sdkconfig.defaults +++ b/host/class/uac/usb_host_uac/test_app/sdkconfig.defaults @@ -9,3 +9,6 @@ CONFIG_USB_HOST_HW_BUFFER_BIAS_PERIODIC_OUT=y # Disable watchdogs, they'd get triggered during unity interactive menu # CONFIG_ESP_TASK_WDT_INIT is not set + +# For TaskNotifyTake/GiveIndexed +CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=4 diff --git a/host/class/uac/usb_host_uac/uac_host.c b/host/class/uac/usb_host_uac/uac_host.c index 11b12b64d..6870aa6d2 100644 --- a/host/class/uac/usb_host_uac/uac_host.c +++ b/host/class/uac/usb_host_uac/uac_host.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -127,6 +127,7 @@ typedef enum { UAC_INTERFACE_STATE_READY, /*!< UAC Interface has started but stream is paused */ UAC_INTERFACE_STATE_ACTIVE, /*!< UAC Interface is streaming */ UAC_INTERFACE_STATE_PAUSING, /*!< UAC Interface is pausing */ + UAC_INTERFACE_STATE_SUSPENDED, /*!< UAC Interface is suspended, global root port suspend was called*/ } uac_iface_state_t; /** @@ -162,6 +163,7 @@ typedef struct uac_interface { SemaphoreHandle_t state_mutex; /*!< UAC device state mutex */ SemaphoreHandle_t ringbuf_mutex; /*!< UAC ringbuffer mutex */ uac_iface_state_t state; /*!< Interface state */ + uac_iface_state_t last_state; /*!< Interface last state before suspending */ uint32_t flags; /*!< Interface flags */ uint8_t cur_alt; /*!< Current alternate setting (-1) */ uint8_t cur_vol; /*!< volume % 0-100 */ @@ -218,6 +220,8 @@ static uac_driver_t *s_uac_driver; /*!< Internal po static esp_err_t _uac_host_device_add(uint8_t addr, usb_device_handle_t dev_hdl, const usb_config_desc_t *config_desc, uac_device_t **uac_device_handle); static esp_err_t _uac_host_device_delete(uac_device_t *uac_device); +static inline esp_err_t _uac_host_device_try_lock(uac_device_t *uac_device, uint32_t timeout_ms); +static inline void _uac_host_device_unlock(uac_device_t *uac_device); static esp_err_t uac_cs_request_set(uac_device_t *uac_device, const uac_cs_request_t *req); static esp_err_t uac_cs_request_set_ep_frequency(uac_iface_t *iface, uint8_t ep_addr, uint32_t freq); @@ -349,6 +353,74 @@ static esp_err_t _ring_buffer_pop(RingbufHandle_t ringbuf_hdl, uint8_t *buf, siz return ESP_OK; } +// --------------------------- Interface states -------------------------------- + +/** + * @brief Check if the interface is in ready state + * + * In caste the interface is currently in suspended state, the function checks interface's last state + * + * @param[in] iface UAC interface + * @return + * - true if the interface is in correct state + * - false if the interface is not in correct state + */ +static inline bool _iface_state_ready_check(const uac_iface_t *iface) +{ + // Use iface last_state if currently suspended, otherwise use iface state +#ifdef UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + const uac_iface_state_t state = (iface->state == UAC_INTERFACE_STATE_SUSPENDED) ? (iface->last_state) : (iface->state); +#else + const uac_iface_state_t state = iface->state; +#endif // UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + + return (state == UAC_INTERFACE_STATE_READY); +} + +/** + * @brief Check if the interface is active state + * + * In caste the interface is currently in suspended state, the function checks interface's last state + * + * @param[in] iface UAC interface + * @return + * - true if the interface is in correct state + * - false if the interface is not in correct state + */ +static inline bool _iface_state_active_check(const uac_iface_t *iface) +{ + // Use iface last_state if currently suspended, otherwise use iface state +#ifdef UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + const uac_iface_state_t state = (iface->state == UAC_INTERFACE_STATE_SUSPENDED) ? (iface->last_state) : (iface->state); +#else + const uac_iface_state_t state = iface->state; +#endif // UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + + return (state == UAC_INTERFACE_STATE_ACTIVE); +} + +/** + * @brief Check if the interface is in active or ready state + * + * In caste the interface is currently in suspended state, the function checks interface's last state + * + * @param[in] iface UAC interface + * @return + * - true if the interface is in correct state + * - false if the interface is not in correct state + */ +static inline bool _iface_state_active_ready_check(const uac_iface_t *iface) +{ + // Use iface last_state if currently suspended, otherwise use iface state +#ifdef UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + const uac_iface_state_t state = (iface->state == UAC_INTERFACE_STATE_SUSPENDED) ? (iface->last_state) : (iface->state); +#else + const uac_iface_state_t state = iface->state; +#endif // UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + + return ((state == UAC_INTERFACE_STATE_ACTIVE) || (state == UAC_INTERFACE_STATE_READY)); +} + /** * @brief UAC Host driver event handler internal task * @@ -884,6 +956,7 @@ static esp_err_t uac_host_interface_add(uac_device_t *uac_device, uint8_t iface_ iface_alt_desc = GET_NEXT_INTERFACE_DESC(iface_alt_desc, total_length, iface_alt_offset); } uac_iface->state = UAC_INTERFACE_STATE_NOT_INITIALIZED; + uac_iface->last_state = UAC_INTERFACE_STATE_NOT_INITIALIZED; uac_iface->parent = uac_device; uac_iface->dev_info.addr = uac_device->addr; uac_iface->dev_info.iface_num = iface_desc->bInterfaceNumber; @@ -933,6 +1006,7 @@ static esp_err_t uac_host_interface_add(uac_device_t *uac_device, uint8_t iface_ static esp_err_t uac_host_interface_delete(uac_iface_t *uac_iface) { uac_iface->state = UAC_INTERFACE_STATE_NOT_INITIALIZED; + uac_iface->last_state = UAC_INTERFACE_STATE_NOT_INITIALIZED; UAC_ENTER_CRITICAL(); STAILQ_REMOVE(&s_uac_driver->uac_ifaces_tailq, uac_iface, uac_interface, tailq_entry); UAC_EXIT_CRITICAL(); @@ -1085,6 +1159,239 @@ static esp_err_t _uac_host_device_disconnected(usb_device_handle_t dev_hdl) return ESP_OK; } +#ifdef UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + +/** + * @brief Global root port suspend was called, suspend an interface + * + * @param[in] iface Pointer to Interface structure, + * @return + * - ESP_ERR_INVALID_ARG: Invalid input argument + * - ESP_ERR_NOT_FOUND: Interface not found in the interface list (probably already disconnected) + * - ESP_ERR_INVALID_STATE: Invalid interface state + * - ESP_OK: Interface successfully suspended + */ +static esp_err_t _uac_host_interface_suspend(uac_iface_t *iface) +{ + UAC_RETURN_ON_INVALID_ARG(iface); + UAC_RETURN_ON_INVALID_ARG(iface->parent); + UAC_RETURN_ON_FALSE(is_interface_in_list(iface), ESP_ERR_NOT_FOUND, "Interface handle not found"); + UAC_RETURN_ON_FALSE((UAC_INTERFACE_STATE_SUSPENDED != iface->state), ESP_ERR_INVALID_STATE, "Interface wrong state"); + UAC_RETURN_ON_ERROR(uac_host_interface_try_lock(iface, DEFAULT_CTRL_XFER_TIMEOUT_MS), "Unable to lock UAC Interface"); + + // Suspending from Not initialized or idle state, just change states + if (iface->state <= UAC_INTERFACE_STATE_IDLE) { + // Change state + iface->last_state = iface->state; + iface->state = UAC_INTERFACE_STATE_SUSPENDED; + uac_host_interface_unlock(iface); + return ESP_OK; + } + + UAC_RETURN_ON_INVALID_ARG(iface->free_xfer_list); + // EPs are already flushed and halted, managed by the usb_host_lib + + _ring_buffer_flush(iface->ringbuf); + + // add all the transfer to free list + UAC_ENTER_CRITICAL(); + for (int i = 0; i < iface->xfer_num; i++) { + if (iface->xfer_list[i]) { + iface->free_xfer_list[i] = iface->xfer_list[i]; + iface->xfer_list[i] = NULL; + } + } + UAC_EXIT_CRITICAL(); + // Change state + iface->last_state = iface->state; + iface->state = UAC_INTERFACE_STATE_SUSPENDED; + + uac_host_interface_unlock(iface); + return ESP_OK; +} + +/** + * @brief Global root port resume was called, resume an interface + * + * @param[in] iface Pointer to Interface structure, + * @return + * - ESP_ERR_INVALID_ARG: Invalid input argument + * - ESP_ERR_NOT_FOUND: Interface not found in the interface list (probably already disconnected) + * - ESP_ERR_INVALID_STATE: Invalid interface state + * - ESP_ERR_NOT_ALLOWED: Lock acquire combination + * - ESP_OK: Interface successfully resumed + */ +static esp_err_t _uac_host_interface_resume(uac_iface_t *iface) +{ + UAC_RETURN_ON_INVALID_ARG(iface); + UAC_RETURN_ON_INVALID_ARG(iface->parent); + UAC_RETURN_ON_FALSE(is_interface_in_list(iface), ESP_ERR_NOT_FOUND, "Interface handle not found"); + UAC_RETURN_ON_FALSE((UAC_INTERFACE_STATE_SUSPENDED == iface->state), ESP_ERR_INVALID_STATE, "Interface wrong state"); + + // There should be no activity in the UAC driver when resuming root port other than auto-resume by transfer submit + // We will not wait for locks here, since if they are already acquired by the uac host function initiating + // the auto-resume by transfer submit, the locks will not be released until the transfer is completed (root port is resumed) + if (ESP_ERR_TIMEOUT == uac_host_interface_try_lock(iface, 0)) { + if (ESP_ERR_TIMEOUT == _uac_host_device_try_lock(iface->parent, 0)) { + // Auto resume by transfer submit + // Return gracefully, interface status will be changed by the function initiating the auto-resume + return ESP_OK; + } else { + _uac_host_device_unlock(iface->parent); + // Acquiring iface lock and not acquiring device lock should not happen, as the auto-resume by transfer + // submit is initiated with both lock acquired + return ESP_ERR_NOT_ALLOWED; + } + } + + // Resuming from not initialized or idle state, just update the state + if (iface->last_state <= UAC_INTERFACE_STATE_IDLE) { + iface->state = iface->last_state; + uac_host_interface_unlock(iface); + return ESP_OK; + } + + // In case the device was suspended while streaming (in active state), return back to ready state, so the user + // must initiate the active state manually (must call uac_host_device_unpause() after resuming) + if (iface->last_state == UAC_INTERFACE_STATE_ACTIVE) { + iface->state = UAC_INTERFACE_STATE_READY; + uac_host_interface_unlock(iface); + return ESP_OK; + } + + // For the rest of the states, update the state + iface->state = iface->last_state; + uac_host_interface_unlock(iface); + return ESP_OK; +} + +/** + * @brief Finalize resume procedure after auto-resume by transfer submit + * + * This is needed due to driver safety and interface/device lock access, where the _uac_host_interface_resume() couldn't + * change the interface state due to the not acquired locks + * + * @param[in] iface Pointer to Interface structure, + * @return + * - ESP_ERR_INVALID_ARG: Invalid input argument + * - ESP_ERR_NOT_FOUND: Interface not found in the interface list (probably already disconnected) + * - ESP_ERR_INVALID_STATE: Invalid interface state + * - ESP_OK: Interface resume procedure successfully finished + */ +static esp_err_t _uac_host_interface_auto_resume(uac_iface_t *iface) +{ + UAC_RETURN_ON_INVALID_ARG(iface); + UAC_RETURN_ON_INVALID_ARG(iface->parent); + UAC_RETURN_ON_FALSE(is_interface_in_list(iface), ESP_ERR_NOT_FOUND, "Interface handle not found"); + UAC_RETURN_ON_FALSE((UAC_INTERFACE_STATE_SUSPENDED == iface->state), ESP_ERR_INVALID_STATE, "Interface wrong state"); + + iface->state = iface->last_state; + return ESP_OK; +} + +/** + * @brief Handler for global (root port) suspend event + * + * @param[in] dev_hdl USB device handle + * @return esp_err_t + */ +static esp_err_t _uac_host_device_suspended(usb_device_handle_t dev_hdl) +{ + uac_device_t *uac_device = get_uac_device_by_handle(dev_hdl); + // Device should be in the list + assert(uac_device); + + UAC_ENTER_CRITICAL(); + uac_iface_t *uac_iface_curr = STAILQ_FIRST(&s_uac_driver->uac_ifaces_tailq); + uac_iface_t *uac_iface_next = NULL; + + while (uac_iface_curr != NULL) { + uac_iface_next = STAILQ_NEXT(uac_iface_curr, tailq_entry); + UAC_EXIT_CRITICAL(); + + if (uac_iface_curr->parent && (uac_iface_curr->parent->addr == uac_device->addr)) { + esp_err_t ret = _uac_host_interface_suspend(uac_iface_curr); + + // Make sure the device is connected and the interface is found otherwise don't deliver suspend event + if (ret != ESP_ERR_NOT_FOUND) { + + // We will deliver the suspend event, if the _uac_host_interface_suspend fails with other errors, + // as the usb_host_lib has already suspended the root port anyway + uac_host_user_interface_callback(uac_iface_curr, UAC_HOST_DEVICE_EVENT_SUSPENDED); + } + } + uac_iface_curr = uac_iface_next; + UAC_ENTER_CRITICAL(); + } + UAC_EXIT_CRITICAL(); + return ESP_OK; +} + +/** + * @brief Handler for global (root port) resume event + * + * @param[in] dev_hdl USB device handle + * @return esp_err_t + */ +static esp_err_t _uac_host_device_resumed(usb_device_handle_t dev_hdl) +{ + uac_device_t *uac_device = get_uac_device_by_handle(dev_hdl); + assert(uac_device); + + UAC_ENTER_CRITICAL(); + uac_iface_t *uac_iface_curr = STAILQ_FIRST(&s_uac_driver->uac_ifaces_tailq); + uac_iface_t *uac_iface_next = NULL; + + while (uac_iface_curr != NULL) { + uac_iface_next = STAILQ_NEXT(uac_iface_curr, tailq_entry); + UAC_EXIT_CRITICAL(); + + if (uac_iface_curr->parent && (uac_iface_curr->parent->addr == uac_device->addr)) { + esp_err_t ret = _uac_host_interface_resume(uac_iface_curr); + + // Make sure the device is connected and the interface is found otherwise don't deliver resume event + if (ret != ESP_ERR_NOT_FOUND) { + + // We will deliver the resume event, if the _uac_host_interface_resume fails with other errors, + // as the usb_host_lib has already resumed the root port anyway + uac_host_user_interface_callback(uac_iface_curr, UAC_HOST_DEVICE_EVENT_RESUMED); + } + } + uac_iface_curr = uac_iface_next; + UAC_ENTER_CRITICAL(); + } + UAC_EXIT_CRITICAL(); + return ESP_OK; +} + +/** + * @brief Handler for auto-resume event (root port resume finalization) + * + * @param[in] uac_device UAC device handle + * @return esp_err_t + */ +static esp_err_t _uac_host_device_auto_resume(uac_device_t *uac_device) +{ + UAC_ENTER_CRITICAL(); + uac_iface_t *uac_iface_curr = STAILQ_FIRST(&s_uac_driver->uac_ifaces_tailq); + uac_iface_t *uac_iface_next = NULL; + + while (uac_iface_curr != NULL) { + uac_iface_next = STAILQ_NEXT(uac_iface_curr, tailq_entry); + UAC_EXIT_CRITICAL(); + + if (uac_iface_curr->parent && (uac_iface_curr->parent->addr == uac_device->addr)) { + _uac_host_interface_auto_resume(uac_iface_curr); + } + uac_iface_curr = uac_iface_next; + UAC_ENTER_CRITICAL(); + } + UAC_EXIT_CRITICAL(); + return ESP_OK; +} + +#endif // UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + /** * @brief USB Host Client's event callback * @@ -1093,10 +1400,28 @@ static esp_err_t _uac_host_device_disconnected(usb_device_handle_t dev_hdl) */ static void client_event_cb(const usb_host_client_event_msg_t *event, void *arg) { - if (event->event == USB_HOST_CLIENT_EVENT_NEW_DEV) { + switch (event->event) { + case USB_HOST_CLIENT_EVENT_NEW_DEV: + ESP_LOGD(TAG, "New device connected"); _uac_host_device_connected(event->new_dev.address); - } else if (event->event == USB_HOST_CLIENT_EVENT_DEV_GONE) { + break; + case USB_HOST_CLIENT_EVENT_DEV_GONE: + ESP_LOGD(TAG, "Device suddenly disconnected"); _uac_host_device_disconnected(event->dev_gone.dev_hdl); + break; +#ifdef UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + case USB_HOST_CLIENT_EVENT_DEV_SUSPENDED: + ESP_LOGD(TAG, "Device suspended"); + _uac_host_device_suspended(event->dev_suspend_resume.dev_hdl); + break; + case USB_HOST_CLIENT_EVENT_DEV_RESUMED: + ESP_LOGD(TAG, "Device resumed"); + _uac_host_device_resumed(event->dev_suspend_resume.dev_hdl); + break; +#endif // UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + default: + ESP_LOGW(TAG, "Unrecognized USB Host client event"); + break; } } @@ -1133,8 +1458,17 @@ static esp_err_t uac_host_interface_release_and_free_transfer(uac_iface_t *iface } // Change state +#ifdef UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + if (iface->state == UAC_INTERFACE_STATE_SUSPENDED) { + iface->last_state = UAC_INTERFACE_STATE_IDLE; + } else { + iface->state = UAC_INTERFACE_STATE_IDLE; + } + return ESP_OK; +#else // UAC_HOST_SUSPEND_RESUME_API_SUPPORTED iface->state = UAC_INTERFACE_STATE_IDLE; return ESP_OK; +#endif // UAC_HOST_SUSPEND_RESUME_API_SUPPORTED } /** @@ -1343,7 +1677,19 @@ static esp_err_t uac_host_interface_pause(uac_iface_t *iface) UAC_RETURN_ON_INVALID_ARG(iface->parent); UAC_RETURN_ON_INVALID_ARG(iface->free_xfer_list); UAC_RETURN_ON_FALSE(is_interface_in_list(iface), ESP_ERR_NOT_FOUND, "Interface handle not found"); - UAC_RETURN_ON_FALSE((UAC_INTERFACE_STATE_ACTIVE == iface->state), ESP_ERR_INVALID_STATE, "Interface wrong state"); + UAC_RETURN_ON_FALSE(_iface_state_active_check(iface), ESP_ERR_INVALID_STATE, "Interface wrong state"); + +#ifdef UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + if (UAC_INTERFACE_STATE_SUSPENDED == iface->state) { + // endpoints are already flushed by usb_host_lib, + // ringbuffer is flushed by the suspend routine in uac driver, + // transfers are already in the free list by the suspend routine in uac driver, + // just updated state + iface->last_state = UAC_INTERFACE_STATE_READY; + return ESP_OK; + } +#endif // UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + iface->state = UAC_INTERFACE_STATE_PAUSING; // Set Interface alternate setting to 0 @@ -1383,6 +1729,7 @@ static esp_err_t uac_host_interface_pause(uac_iface_t *iface) /** * @brief Unpause paused interface, the interface will be in ACTIVE state + * @note This function will initiate auto-resume by transfer submit, if the root port is currently suspended * * @param[in] iface Pointer to Interface structure * @return esp_err_t @@ -1393,7 +1740,7 @@ static esp_err_t uac_host_interface_unpause(uac_iface_t *iface) UAC_RETURN_ON_INVALID_ARG(iface->parent); UAC_RETURN_ON_INVALID_ARG(iface->free_xfer_list); UAC_RETURN_ON_FALSE(is_interface_in_list(iface), ESP_ERR_NOT_FOUND, "Interface handle not found"); - UAC_RETURN_ON_FALSE((UAC_INTERFACE_STATE_READY == iface->state), ESP_ERR_INVALID_STATE, "Interface wrong state"); + UAC_RETURN_ON_FALSE(_iface_state_ready_check(iface), ESP_ERR_INVALID_STATE, "Interface wrong state"); // Set Interface alternate setting usb_setup_packet_t usb_request; @@ -2200,11 +2547,11 @@ esp_err_t uac_host_device_close(uac_host_device_handle_t uac_dev_handle) ESP_LOGD(TAG, "Close addr %d, iface %d, state %d", uac_iface->dev_info.addr, uac_iface->dev_info.iface_num, uac_iface->state); UAC_RETURN_ON_ERROR(uac_host_interface_try_lock(uac_iface, DEFAULT_CTRL_XFER_TIMEOUT_MS), "UAC Interface is busy by other task"); - if (UAC_INTERFACE_STATE_ACTIVE == uac_iface->state) { + if (_iface_state_active_check(uac_iface)) { UAC_GOTO_ON_ERROR(uac_host_interface_pause(uac_iface), "Unable to disable UAC Interface"); } - if (UAC_INTERFACE_STATE_READY == uac_iface->state) { + if (_iface_state_ready_check(uac_iface)) { UAC_GOTO_ON_ERROR(uac_host_interface_release_and_free_transfer(uac_iface), "Unable to release UAC Interface"); } @@ -2321,13 +2668,44 @@ esp_err_t uac_host_device_start(uac_host_device_handle_t uac_dev_handle, const u // get the mutex first to change the device/interface state UAC_RETURN_ON_ERROR(uac_host_interface_try_lock(iface, DEFAULT_CTRL_XFER_TIMEOUT_MS), "Unable to lock UAC Interface"); + esp_err_t ret = ESP_OK; + bool iface_claimed = false; + +#ifdef UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + if (UAC_INTERFACE_STATE_SUSPENDED == iface->state && UAC_INTERFACE_STATE_READY == iface->last_state) { + // If interface's last state was in READY and the current state is SUSPENDED, resume the root port + // Following usb_host calls require the root port to be suspended to proceed + + ESP_LOGD(TAG, "Resuming the root port"); + UAC_GOTO_ON_ERROR(usb_host_lib_root_port_resume(), "Unable to resume the root port"); + uac_host_interface_unlock(iface); + return ESP_OK; + } + + if (UAC_INTERFACE_STATE_SUSPENDED == iface->state && UAC_INTERFACE_STATE_ACTIVE == iface->last_state) { + // If interface's last state was ACTIVE and the current state is SUSPENDED, + // unpause the interface which effectively resumes the root port + + // Change last state to ready, to be able to unpause the iface again + iface->last_state = UAC_INTERFACE_STATE_READY; + UAC_GOTO_ON_ERROR(uac_host_interface_unpause(iface), "Unable to enable UAC Interface"); + uac_host_interface_unlock(iface); + return ESP_OK; + } + + if (UAC_INTERFACE_STATE_SUSPENDED == iface->state && UAC_INTERFACE_STATE_IDLE == iface->last_state) { + // We can't continue, since the following usb_host_lib api calls require the root port to be resumed + ESP_LOGE(TAG, "Device in suspended state, resume the device first"); + uac_host_interface_unlock(iface); + return ESP_ERR_INVALID_STATE; + } +#endif // UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + if (UAC_INTERFACE_STATE_ACTIVE == iface->state || UAC_INTERFACE_STATE_READY == iface->state) { uac_host_interface_unlock(iface); return ESP_OK; } - esp_err_t ret = ESP_OK; - bool iface_claimed = false; UAC_GOTO_ON_FALSE((UAC_INTERFACE_STATE_IDLE == iface->state), ESP_ERR_INVALID_STATE, "Interface wrong state"); // check if any alt setting meets the channels, sample frequency and bit resolution requirements @@ -2397,7 +2775,7 @@ esp_err_t uac_host_device_pause(uac_host_device_handle_t uac_dev_handle) return ESP_OK; } esp_err_t ret = ESP_OK; - UAC_GOTO_ON_FALSE((UAC_INTERFACE_STATE_ACTIVE == iface->state), ESP_ERR_INVALID_STATE, "device not active"); + UAC_GOTO_ON_FALSE(_iface_state_active_check(iface), ESP_ERR_INVALID_STATE, "device not active"); UAC_GOTO_ON_ERROR(uac_host_interface_pause(iface), "Unable to disable UAC Interface"); uac_host_interface_unlock(iface); @@ -2419,8 +2797,16 @@ esp_err_t uac_host_device_unpause(uac_host_device_handle_t uac_dev_handle) return ESP_OK; } +#ifdef UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + // In case user calls this function after the device was suspended from active (unpaused) state + // Change the last state to ready, so the interface could be automatically resumed and unpaused + if (UAC_INTERFACE_STATE_SUSPENDED == iface->state && UAC_INTERFACE_STATE_ACTIVE == iface->last_state) { + iface->last_state = UAC_INTERFACE_STATE_READY; + } +#endif // UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + esp_err_t ret = ESP_OK; - UAC_GOTO_ON_FALSE((UAC_INTERFACE_STATE_READY == iface->state), ESP_ERR_INVALID_STATE, "device not ready"); + UAC_GOTO_ON_FALSE(_iface_state_ready_check(iface), ESP_ERR_INVALID_STATE, "device not ready"); UAC_GOTO_ON_ERROR(uac_host_interface_unpause(iface), "Unable to enable UAC Interface"); uac_host_interface_unlock(iface); @@ -2438,11 +2824,11 @@ esp_err_t uac_host_device_stop(uac_host_device_handle_t uac_dev_handle) esp_err_t ret = ESP_OK; UAC_RETURN_ON_ERROR(uac_host_interface_try_lock(iface, DEFAULT_CTRL_XFER_TIMEOUT_MS), "Unable to lock UAC Interface"); - if (UAC_INTERFACE_STATE_ACTIVE == iface->state) { + if (_iface_state_active_check(iface)) { UAC_GOTO_ON_ERROR(uac_host_interface_pause(iface), "Unable to disable UAC Interface"); } - if (UAC_INTERFACE_STATE_READY == iface->state) { + if (_iface_state_ready_check(iface)) { UAC_GOTO_ON_ERROR(uac_host_interface_release_and_free_transfer(iface), "Unable to release UAC Interface"); } @@ -2531,6 +2917,21 @@ esp_err_t uac_host_device_write(uac_host_device_handle_t uac_dev_handle, uint8_t return ret; } +/** + * @brief Finalize auto-resume procedure + * + * @param[in] iface UAC interface + */ +static inline void _iface_auto_resume(const uac_iface_t *iface) +{ +#ifdef UAC_HOST_SUSPEND_RESUME_API_SUPPORTED + if (iface->state == UAC_INTERFACE_STATE_SUSPENDED) { + // Update the interface state from here instead of from client event handler, as this function takes interface lock + _uac_host_device_auto_resume(iface->parent); + } +#endif // UAC_HOST_SUSPEND_RESUME_API_SUPPORTED +} + esp_err_t uac_host_get_device_info(uac_host_device_handle_t uac_dev_handle, uac_host_dev_info_t *uac_dev_info) { uac_iface_t *iface = get_iface_by_handle(uac_dev_handle); @@ -2547,11 +2948,14 @@ esp_err_t uac_host_device_set_mute(uac_host_device_handle_t uac_dev_handle, bool // Check if the device is active or ready esp_err_t ret = ESP_OK; UAC_RETURN_ON_ERROR(uac_host_interface_try_lock(iface, DEFAULT_CTRL_XFER_TIMEOUT_MS), "Unable to lock UAC Interface"); - UAC_GOTO_ON_FALSE(UAC_INTERFACE_STATE_ACTIVE == iface->state || UAC_INTERFACE_STATE_READY == iface->state, - ESP_ERR_INVALID_STATE, "device not ready or active"); + UAC_GOTO_ON_FALSE(_iface_state_active_ready_check(iface), ESP_ERR_INVALID_STATE, "device not ready or active"); UAC_GOTO_ON_ERROR(uac_cs_request_set_mute(iface, mute), "Unable to set mute"); ESP_LOGI(TAG, "%s Interface %d-%d", mute ? "Mute" : "Unmute", iface->dev_info.iface_num, iface->cur_alt + 1); + + // This function issues a ctrl transfer, which causes auto-resume of suspended root port + _iface_auto_resume(iface); + uac_host_interface_unlock(iface); return ESP_OK; @@ -2568,10 +2972,13 @@ esp_err_t uac_host_device_get_mute(uac_host_device_handle_t uac_dev_handle, bool // Check if the device is active or ready esp_err_t ret = ESP_OK; UAC_RETURN_ON_ERROR(uac_host_interface_try_lock(iface, DEFAULT_CTRL_XFER_TIMEOUT_MS), "Unable to lock UAC Interface"); - UAC_GOTO_ON_FALSE((UAC_INTERFACE_STATE_ACTIVE == iface->state || UAC_INTERFACE_STATE_READY == iface->state), - ESP_ERR_INVALID_STATE, "device not ready or active"); + UAC_GOTO_ON_FALSE(_iface_state_active_ready_check(iface), ESP_ERR_INVALID_STATE, "device not ready or active"); UAC_GOTO_ON_ERROR(uac_cs_request_get_mute(iface, mute), "Unable to get mute"); + + // This function issues a ctrl transfer, which causes auto-resume of suspended root port + _iface_auto_resume(iface); + uac_host_interface_unlock(iface); return ESP_OK; @@ -2588,8 +2995,7 @@ esp_err_t uac_host_device_set_volume(uac_host_device_handle_t uac_dev_handle, ui // Check if the device is active or ready esp_err_t ret = ESP_OK; UAC_RETURN_ON_ERROR(uac_host_interface_try_lock(iface, DEFAULT_CTRL_XFER_TIMEOUT_MS), "Unable to lock UAC Interface"); - UAC_GOTO_ON_FALSE((UAC_INTERFACE_STATE_ACTIVE == iface->state || UAC_INTERFACE_STATE_READY == iface->state), - ESP_ERR_INVALID_STATE, "device not ready or active"); + UAC_GOTO_ON_FALSE(_iface_state_active_ready_check(iface), ESP_ERR_INVALID_STATE, "device not ready or active"); // Calculate target volume in float to avoid the int16_t calculation overflow float volume_db_f = _volume_db_i16_2_f(iface->vol_min_db) + (_volume_db_i16_2_f(iface->vol_max_db) - _volume_db_i16_2_f(iface->vol_min_db)) * (float)volume / 100.0f; @@ -2602,6 +3008,10 @@ esp_err_t uac_host_device_set_volume(uac_host_device_handle_t uac_dev_handle, ui // Backup the volume value for the get volume function iface->cur_vol = volume; ESP_LOGI(TAG, "Set volume %d%%, Interface %d-%d", volume, iface->dev_info.iface_num, iface->cur_alt + 1); + + // This function issues a ctrl transfer, which causes auto-resume of suspended root port + _iface_auto_resume(iface); + uac_host_interface_unlock(iface); return ESP_OK; @@ -2618,8 +3028,7 @@ esp_err_t uac_host_device_get_volume(uac_host_device_handle_t uac_dev_handle, ui // Check if the device is active or ready esp_err_t ret = ESP_OK; UAC_RETURN_ON_ERROR(uac_host_interface_try_lock(iface, DEFAULT_CTRL_XFER_TIMEOUT_MS), "Unable to lock UAC Interface"); - UAC_GOTO_ON_FALSE((UAC_INTERFACE_STATE_ACTIVE == iface->state || UAC_INTERFACE_STATE_READY == iface->state), - ESP_ERR_INVALID_STATE, "device not ready or active"); + UAC_GOTO_ON_FALSE(_iface_state_active_ready_check(iface), ESP_ERR_INVALID_STATE, "device not ready or active"); // Return the backup volume value to avoid the volume reads differently than expected. // Because the device volume adjustment step may be relatively large, @@ -2638,6 +3047,9 @@ esp_err_t uac_host_device_get_volume(uac_host_device_handle_t uac_dev_handle, ui // Calculate volume in percentage *volume = (uint8_t)roundf((volume_db_f - _volume_db_i16_2_f(iface->vol_min_db)) * 100.0f / (_volume_db_i16_2_f(iface->vol_max_db) - _volume_db_i16_2_f(iface->vol_min_db))); + // This function issues a ctrl transfer, which causes auto-resume of suspended root port + _iface_auto_resume(iface); + uac_host_interface_unlock(iface); return ESP_OK; @@ -2653,11 +3065,15 @@ esp_err_t uac_host_device_set_volume_db(uac_host_device_handle_t uac_dev_handle, // Check if the device is active or ready esp_err_t ret = ESP_OK; UAC_RETURN_ON_ERROR(uac_host_interface_try_lock(iface, DEFAULT_CTRL_XFER_TIMEOUT_MS), "Unable to lock UAC Interface"); - UAC_GOTO_ON_FALSE((UAC_INTERFACE_STATE_ACTIVE == iface->state || UAC_INTERFACE_STATE_READY == iface->state), - ESP_ERR_INVALID_STATE, "device not ready or active"); + UAC_GOTO_ON_FALSE(_iface_state_active_ready_check(iface), ESP_ERR_INVALID_STATE, "device not ready or active"); + // Check if the volume is within the range UAC_GOTO_ON_FALSE((volume_db >= iface->vol_min_db && volume_db <= iface->vol_max_db), ESP_ERR_INVALID_ARG, "Invalid volume value"); UAC_GOTO_ON_ERROR(uac_cs_request_set_volume(iface, volume_db), "Unable to set volume"); + + // This function issues a ctrl transfer, which causes auto-resume of suspended root port + _iface_auto_resume(iface); + uac_host_interface_unlock(iface); return ESP_OK; @@ -2674,10 +3090,13 @@ esp_err_t uac_host_device_get_volume_db(uac_host_device_handle_t uac_dev_handle, // Check if the device is active or ready esp_err_t ret = ESP_OK; UAC_RETURN_ON_ERROR(uac_host_interface_try_lock(iface, DEFAULT_CTRL_XFER_TIMEOUT_MS), "Unable to lock UAC Interface"); - UAC_GOTO_ON_FALSE((UAC_INTERFACE_STATE_ACTIVE == iface->state || UAC_INTERFACE_STATE_READY == iface->state), - ESP_ERR_INVALID_STATE, "device not ready or active"); + UAC_GOTO_ON_FALSE(_iface_state_active_ready_check(iface), ESP_ERR_INVALID_STATE, "device not ready or active"); UAC_GOTO_ON_ERROR(uac_cs_request_get_volume(iface, volume_db), "Unable to get volume"); + + // This function issues a ctrl transfer, which causes auto-resume of suspended root port + _iface_auto_resume(iface); + uac_host_interface_unlock(iface); return ESP_OK;