diff --git a/host/class/cdc/usb_host_cdc_acm/CHANGELOG.md b/host/class/cdc/usb_host_cdc_acm/CHANGELOG.md index 7ecc81f4..8e24c35e 100644 --- a/host/class/cdc/usb_host_cdc_acm/CHANGELOG.md +++ b/host/class/cdc/usb_host_cdc_acm/CHANGELOG.md @@ -1,3 +1,7 @@ +## [Unreleased] + +- Added global suspend/resume support + ## 2.1.1 - Added support for ESP32-H4 diff --git a/host/class/cdc/usb_host_cdc_acm/cdc_acm_host.c b/host/class/cdc/usb_host_cdc_acm/cdc_acm_host.c index fe5ea126..3087ccb0 100644 --- a/host/class/cdc/usb_host_cdc_acm/cdc_acm_host.c +++ b/host/class/cdc/usb_host_cdc_acm/cdc_acm_host.c @@ -842,10 +842,35 @@ static void out_xfer_cb(usb_transfer_t *transfer) xSemaphoreGive((SemaphoreHandle_t)transfer->context); } +/** + * @brief Resume CDC device + * + * Submit poll for BULK IN and INTR IN transfers + * + * @param cdc_dev + * @return esp_err_t + */ +#ifdef CDC_HOST_SUSPEND_RESUME_API_SUPPORTED +static void cdc_acm_resume(cdc_dev_t *cdc_dev) +{ + assert(cdc_dev); + + if (cdc_dev->data.in_xfer) { + ESP_LOGD(TAG, "Submitting poll for BULK IN transfer"); + ESP_ERROR_CHECK(usb_host_transfer_submit(cdc_dev->data.in_xfer)); + } + + if (cdc_dev->notif.xfer) { + ESP_LOGD(TAG, "Submitting poll for INTR IN transfer"); + ESP_ERROR_CHECK(usb_host_transfer_submit(cdc_dev->notif.xfer)); + } +} +#endif // CDC_HOST_SUSPEND_RESUME_API_SUPPORTED + static void usb_event_cb(const usb_host_client_event_msg_t *event_msg, void *arg) { switch (event_msg->event) { - case USB_HOST_CLIENT_EVENT_NEW_DEV: + case USB_HOST_CLIENT_EVENT_NEW_DEV: { // Guard p_cdc_acm_obj->new_dev_cb from concurrent access ESP_LOGD(TAG, "New device connected"); CDC_ACM_ENTER_CRITICAL(); @@ -861,8 +886,8 @@ static void usb_event_cb(const usb_host_client_event_msg_t *event_msg, void *arg _new_dev_cb(new_dev); usb_host_device_close(p_cdc_acm_obj->cdc_acm_client_hdl, new_dev); } - break; + } case USB_HOST_CLIENT_EVENT_DEV_GONE: { ESP_LOGD(TAG, "Device suddenly disconnected"); // Find CDC pseudo-devices associated with this USB device and close them @@ -881,7 +906,54 @@ static void usb_event_cb(const usb_host_client_event_msg_t *event_msg, void *arg } break; } +#ifdef CDC_HOST_SUSPEND_RESUME_API_SUPPORTED + case USB_HOST_CLIENT_EVENT_DEV_SUSPENDED: { + ESP_LOGD(TAG, "Device suspended"); + // Find CDC pseudo-devices associated with this USB device and deliver suspend event to the user + cdc_dev_t *cdc_dev; + cdc_dev_t *tcdc_dev; + SLIST_FOREACH_SAFE(cdc_dev, &p_cdc_acm_obj->cdc_devices_list, list_entry, tcdc_dev) { + if (cdc_dev->dev_hdl == event_msg->dev_suspend_resume.dev_hdl && cdc_dev->notif.cb) { + + // The driver does not have to do anything to suspend the device, + // the usb host lib already halted and flushed all EPs + + // The suspended device was opened by this driver: inform user about this + const cdc_acm_host_dev_event_data_t suspend_event = { + .type = CDC_ACM_HOST_DEVICE_SUSPENDED, + .data.cdc_hdl = (cdc_acm_dev_hdl_t) cdc_dev, + }; + cdc_dev->notif.cb(&suspend_event, cdc_dev->cb_arg); + } + } + break; + } + case USB_HOST_CLIENT_EVENT_DEV_RESUMED: { + ESP_LOGD(TAG, "Device resumed"); + // Find CDC pseudo-devices associated with this USB device and deliver resume event to the user + cdc_dev_t *cdc_dev; + cdc_dev_t *tcdc_dev; + SLIST_FOREACH_SAFE(cdc_dev, &p_cdc_acm_obj->cdc_devices_list, list_entry, tcdc_dev) { + if (cdc_dev->dev_hdl == event_msg->dev_suspend_resume.dev_hdl) { + + // Resume the pseudo-device + cdc_acm_resume(cdc_dev); + + if (cdc_dev->notif.cb != NULL) { + // The resumed device was opened by this driver: inform user about this + const cdc_acm_host_dev_event_data_t resume_event = { + .type = CDC_ACM_HOST_DEVICE_RESUMED, + .data.cdc_hdl = (cdc_acm_dev_hdl_t) cdc_dev, + }; + cdc_dev->notif.cb(&resume_event, cdc_dev->cb_arg); + } + } + } + break; + } +#endif // CDC_HOST_SUSPEND_RESUME_API_SUPPORTED default: + ESP_LOGE(TAG, "Unrecognized USB Host client event"); assert(false); break; } diff --git a/host/class/cdc/usb_host_cdc_acm/include/usb/cdc_acm_host.h b/host/class/cdc/usb_host_cdc_acm/include/usb/cdc_acm_host.h index a0751b32..8f14620b 100644 --- a/host/class/cdc/usb_host_cdc_acm/include/usb/cdc_acm_host.h +++ b/host/class/cdc/usb_host_cdc_acm/include/usb/cdc_acm_host.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2025 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -17,6 +17,11 @@ #define CDC_HOST_ANY_VID (0) #define CDC_HOST_ANY_PID (0) +// For backward compatibility with IDF versions which do not have suspend/resume api +#ifdef USB_HOST_LIB_EVENT_FLAGS_AUTO_SUSPEND +#define CDC_HOST_SUSPEND_RESUME_API_SUPPORTED +#endif + #ifdef __cplusplus extern "C" { #endif diff --git a/host/class/cdc/usb_host_cdc_acm/include/usb/cdc_host_types.h b/host/class/cdc/usb_host_cdc_acm/include/usb/cdc_host_types.h index d308027d..718b275b 100644 --- a/host/class/cdc/usb_host_cdc_acm/include/usb/cdc_host_types.h +++ b/host/class/cdc/usb_host_cdc_acm/include/usb/cdc_host_types.h @@ -9,6 +9,12 @@ #include #include #include "usb/usb_types_cdc.h" +#include "usb/usb_host.h" // For USB Host suspend/resume API + +// For backward compatibility with IDF versions which do not have suspend/resume api +#ifdef USB_HOST_LIB_EVENT_FLAGS_AUTO_SUSPEND +#define CDC_HOST_SUSPEND_RESUME_API_SUPPORTED +#endif typedef struct cdc_dev_s *cdc_acm_dev_hdl_t; @@ -16,10 +22,14 @@ typedef struct cdc_dev_s *cdc_acm_dev_hdl_t; * @brief CDC-ACM Device Event types to upper layer */ typedef enum { - CDC_ACM_HOST_ERROR, - CDC_ACM_HOST_SERIAL_STATE, - CDC_ACM_HOST_NETWORK_CONNECTION, - CDC_ACM_HOST_DEVICE_DISCONNECTED + CDC_ACM_HOST_ERROR, /**< An error occurred on the CDC-ACM device */ + CDC_ACM_HOST_SERIAL_STATE, /**< Serial state of the CDC-ACM device has changed */ + CDC_ACM_HOST_NETWORK_CONNECTION, /**< CDC-ACM device network connection state has changed */ + CDC_ACM_HOST_DEVICE_DISCONNECTED, /**< CDC-ACM device has been disconnected */ +#ifdef CDC_HOST_SUSPEND_RESUME_API_SUPPORTED + CDC_ACM_HOST_DEVICE_SUSPENDED, /**< CDC-ACM device has been suspended */ + CDC_ACM_HOST_DEVICE_RESUMED, /**< CDC-ACM device has been resumed */ +#endif // CDC_HOST_SUSPEND_RESUME_API_SUPPORTED } cdc_acm_host_dev_event_t; /** @@ -50,7 +60,7 @@ typedef bool (*cdc_acm_data_callback_t)(const uint8_t *data, size_t data_len, vo * @brief Device event callback type * * @param[in] event Event structure - * @param[in] user_arg User's argument passed to open function + * @param[in] user_ctx User's context passed to open function */ typedef void (*cdc_acm_host_dev_callback_t)(const cdc_acm_host_dev_event_data_t *event, void *user_ctx); diff --git a/host/class/cdc/usb_host_cdc_acm/test_app/main/test_cdc_acm_host.c b/host/class/cdc/usb_host_cdc_acm/test_app/main/test_cdc_acm_host.c index 772f1d58..1a9f9f8a 100644 --- a/host/class/cdc/usb_host_cdc_acm/test_app/main/test_cdc_acm_host.c +++ b/host/class/cdc/usb_host_cdc_acm/test_app/main/test_cdc_acm_host.c @@ -13,6 +13,7 @@ #include "unity.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" +#include "freertos/queue.h" #include "esp_idf_version.h" #include "esp_log.h" #include "esp_err.h" @@ -27,6 +28,20 @@ static int nb_of_responses; static int nb_of_responses2; static bool new_dev_cb_called = false; static bool rx_overflow = false; +static QueueHandle_t app_queue = NULL; + +// Function prototypes for the default device config +static void notif_cb(const cdc_acm_host_dev_event_data_t *event, void *user_ctx); +static bool handle_rx(const uint8_t *data, size_t data_len, void *arg); + +// Default device config +static const cdc_acm_host_device_config_t default_dev_config = { + .connection_timeout_ms = 500, + .out_buffer_size = 64, + .event_cb = notif_cb, + .data_cb = handle_rx, + .user_arg = tx_buf, +}; // usb_host_lib_set_root_port_power is used to force toggle connection, primary developed for esp32p4 // esp32p4 is supported from IDF 5.3 @@ -115,6 +130,12 @@ void usb_lib_task(void *arg) printf("Get FLAGS_ALL_FREE\n"); has_clients = false; } +#ifdef CDC_HOST_SUSPEND_RESUME_API_SUPPORTED + if (event_flags & USB_HOST_LIB_EVENT_FLAGS_AUTO_SUSPEND) { + printf("Get AUTO_SUSPEND\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + } +#endif // CDC_HOST_SUSPEND_RESUME_API_SUPPORTED } printf("No more clients and devices, uninstall USB Host library\n"); @@ -174,11 +195,55 @@ static void notif_cb(const cdc_acm_host_dev_event_data_t *event, void *user_ctx) case CDC_ACM_HOST_DEVICE_DISCONNECTED: printf("Disconnection event\n"); TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_close(event->data.cdc_hdl)); - xTaskNotifyGive(user_ctx); break; +#ifdef CDC_HOST_SUSPEND_RESUME_API_SUPPORTED + case CDC_ACM_HOST_DEVICE_SUSPENDED: + printf("Device suspended event\n"); + break; + case CDC_ACM_HOST_DEVICE_RESUMED: + printf("Device resumed event\n"); + break; +#endif // CDC_HOST_SUSPEND_RESUME_API_SUPPORTED default: assert(false); } + + if (app_queue != NULL) { + xQueueSend(app_queue, event, 0); + } +} + +/** + * @brief Wait for app event + * + * @param expected_app_event expected event + * @param ticks ticks to wait for an event + */ +static void wait_for_app_event(cdc_acm_host_dev_event_t expected_app_event, TickType_t ticks) +{ + TEST_ASSERT_NOT_NULL_MESSAGE(app_queue, "App queue has not been initialized"); + cdc_acm_host_dev_event_data_t app_event; + if (pdTRUE == xQueueReceive(app_queue, &app_event, ticks)) { + TEST_ASSERT_EQUAL_MESSAGE(expected_app_event, app_event.type, "Unexpected app event"); + } else { + TEST_FAIL_MESSAGE("App event not generated on time"); + } +} + +/** + * @brief Make sure no app event is delivered during the set amount of time + * + * @param ticks ticks to check that no event is delivered + */ +static void wait_for_no_app_event(TickType_t ticks) +{ + TEST_ASSERT_NOT_NULL_MESSAGE(app_queue, "App queue has not been initialized"); + cdc_acm_host_dev_event_data_t app_event; + if (pdFALSE == xQueueReceive(app_queue, &app_event, ticks)) { + return; + } else { + TEST_FAIL_MESSAGE("Expecting NO event, but an event delivered"); + } } static void new_dev_cb(usb_device_handle_t usb_dev) @@ -205,13 +270,8 @@ TEST_CASE("read_write", "[cdc_acm]") test_install_cdc_driver(); - const cdc_acm_host_device_config_t dev_config = { - .connection_timeout_ms = 500, - .out_buffer_size = 64, - .event_cb = notif_cb, - .data_cb = handle_rx, - .user_arg = tx_buf, - }; + // Use default device config + const cdc_acm_host_device_config_t dev_config = default_dev_config; printf("Opening CDC-ACM device\n"); TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_open(0x303A, 0x4002, 0, &dev_config, &cdc_dev)); // 0x303A:0x4002 (TinyUSB Dual CDC device) @@ -332,7 +392,7 @@ TEST_CASE("multiple_devices", "[cdc_acm]") #define MULTIPLE_THREADS_TRANSFERS_NUM 5 #define MULTIPLE_THREADS_TASKS_NUM 4 -void tx_task(void *arg) +static void tx_task(void *arg) { cdc_acm_dev_hdl_t cdc_dev = (cdc_acm_dev_hdl_t) arg; // Send multiple transfers to make sure that some of them will run at the same time @@ -372,7 +432,7 @@ TEST_CASE("multiple_threads", "[cdc_acm]") TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_open(0x303A, 0x4002, 0, &dev_config, &cdc_dev)); // 0x303A:0x4002 (TinyUSB Dual CDC device) TEST_ASSERT_NOT_NULL(cdc_dev); - // Create two tasks that will try to access cdc_dev + // Create multiple tasks that will try to access cdc_dev for (int i = 0; i < MULTIPLE_THREADS_TASKS_NUM; i++) { TEST_ASSERT_EQUAL(pdTRUE, xTaskCreate(tx_task, "CDC TX", 4096, cdc_dev, i + 3, NULL)); } @@ -390,6 +450,7 @@ TEST_CASE("multiple_threads", "[cdc_acm]") /* Test CDC driver reaction to USB device sudden disconnection */ TEST_CASE("sudden_disconnection", "[cdc_acm]") { + TEST_ASSERT_NOT_NULL(app_queue = xQueueCreate(5, sizeof(cdc_acm_host_dev_event_data_t))); test_install_cdc_driver(); cdc_acm_dev_hdl_t cdc_dev; @@ -404,10 +465,12 @@ TEST_CASE("sudden_disconnection", "[cdc_acm]") TEST_ASSERT_NOT_NULL(cdc_dev); force_conn_state(false, pdMS_TO_TICKS(10)); // Simulate device disconnection - TEST_ASSERT_EQUAL(1, ulTaskNotifyTake(false, pdMS_TO_TICKS(100))); // Notify will succeed only if CDC_ACM_HOST_DEVICE_DISCONNECTED notification was generated + wait_for_app_event(CDC_ACM_HOST_DEVICE_DISCONNECTED, 100); // Clean-up TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_uninstall()); + vQueueDelete(app_queue); + app_queue = NULL; vTaskDelay(20); // Short delay to allow task to be cleaned up } @@ -537,10 +600,15 @@ TEST_CASE("new_device_connection_1", "[cdc_acm]") TEST_CASE("new_device_connection_2", "[cdc_acm]") { test_install_cdc_driver(); + TEST_ASSERT_NOT_NULL(app_queue = xQueueCreate(5, sizeof(cdc_acm_host_dev_event_data_t))); // Option 2: Register callback after driver install force_conn_state(false, 50); vTaskDelay(50); + + // Make sure no disconnection event is delivered, since the device has not been opened by any client + wait_for_no_app_event(50); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_register_new_dev_callback(new_dev_cb)); force_conn_state(true, 0); vTaskDelay(50); @@ -550,6 +618,8 @@ TEST_CASE("new_device_connection_2", "[cdc_acm]") // Clean-up TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_uninstall()); vTaskDelay(20); // Short delay to allow task to be cleaned up + vQueueDelete(app_queue); + app_queue = NULL; } /** @@ -709,13 +779,8 @@ TEST_CASE("tx_timeout", "[cdc_acm]") test_install_cdc_driver(); - const cdc_acm_host_device_config_t dev_config = { - .connection_timeout_ms = 500, - .out_buffer_size = 64, - .event_cb = notif_cb, - .data_cb = handle_rx, - .user_arg = tx_buf, - }; + // Use default device config + const cdc_acm_host_device_config_t dev_config = default_dev_config; printf("Opening CDC-ACM device\n"); TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_open(0x303A, 0x4002, 0, &dev_config, &cdc_dev)); // 0x303A:0x4002 (TinyUSB Dual CDC device) @@ -747,13 +812,8 @@ TEST_CASE("any_vid_pid", "[cdc_acm]") cdc_acm_dev_hdl_t cdc_dev = NULL; test_install_cdc_driver(); - const cdc_acm_host_device_config_t dev_config = { - .connection_timeout_ms = 500, - .out_buffer_size = 64, - .event_cb = notif_cb, - .data_cb = handle_rx, - .user_arg = tx_buf, - }; + // Use default device config + const cdc_acm_host_device_config_t dev_config = default_dev_config; printf("Opening existing CDC-ACM devices with any VID/PID\n"); TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_open(CDC_HOST_ANY_VID, CDC_HOST_ANY_PID, 0, &dev_config, &cdc_dev)); @@ -774,6 +834,386 @@ TEST_CASE("any_vid_pid", "[cdc_acm]") vTaskDelay(20); // Short delay to allow task to be cleaned up } +#ifdef CDC_HOST_SUSPEND_RESUME_API_SUPPORTED + +/** + * @brief Test: Basic suspend/resume cycle with multiple pseudo-devices + * + * #. open/read/write the device + * #. suspend/resume the device - check that the client events are delivered + * #. read/write/close the device + */ +TEST_CASE("suspend_resume_multiple_devs", "[cdc_acm]") +{ + nb_of_responses = 0; + nb_of_responses2 = 0; + TEST_ASSERT_NOT_NULL(app_queue = xQueueCreate(5, sizeof(cdc_acm_host_dev_event_data_t))); + + test_install_cdc_driver(); + + cdc_acm_dev_hdl_t cdc_dev1 = NULL, cdc_dev2 = NULL; + cdc_acm_host_device_config_t dev_config = { + .connection_timeout_ms = 500, + .out_buffer_size = 64, + .event_cb = notif_cb, + .data_cb = handle_rx, + .user_arg = tx_buf, + }; + + printf("Opening CDC-ACM device\n"); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_open(0x303A, 0x4002, 0, &dev_config, &cdc_dev1)); // 0x303A:0x4002 (TinyUSB Dual CDC device) + dev_config.data_cb = handle_rx2; + dev_config.user_arg = tx_buf2; + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_open(0x303A, 0x4002, 2, &dev_config, &cdc_dev2)); // 0x303A:0x4002 (TinyUSB Dual CDC device) + TEST_ASSERT_NOT_NULL(cdc_dev1); + TEST_ASSERT_NOT_NULL(cdc_dev2); + + for (int i = 0; i < NUM_ITERATIONS; i++) { + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_data_tx_blocking(cdc_dev1, tx_buf, sizeof(tx_buf), 1000)); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_data_tx_blocking(cdc_dev2, tx_buf2, sizeof(tx_buf2), 1000)); + } + vTaskDelay(10); // Wait until responses are processed + + TEST_ASSERT_EQUAL(NUM_ITERATIONS, nb_of_responses); + TEST_ASSERT_EQUAL(NUM_ITERATIONS, nb_of_responses2); + nb_of_responses = 0; + nb_of_responses2 = 0; + + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + wait_for_app_event(CDC_ACM_HOST_DEVICE_SUSPENDED, 100); + wait_for_app_event(CDC_ACM_HOST_DEVICE_SUSPENDED, 100); + + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_resume()); + wait_for_app_event(CDC_ACM_HOST_DEVICE_RESUMED, 100); + wait_for_app_event(CDC_ACM_HOST_DEVICE_RESUMED, 100); + + for (int i = 0; i < NUM_ITERATIONS; i++) { + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_data_tx_blocking(cdc_dev1, tx_buf, sizeof(tx_buf), 1000)); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_data_tx_blocking(cdc_dev2, tx_buf2, sizeof(tx_buf2), 1000)); + } + vTaskDelay(10); // Wait until responses are processed + + TEST_ASSERT_EQUAL(NUM_ITERATIONS, nb_of_responses); + TEST_ASSERT_EQUAL(NUM_ITERATIONS, nb_of_responses2); + + // Clean-up + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_close(cdc_dev1)); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_close(cdc_dev2)); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_uninstall()); + vQueueDelete(app_queue); + app_queue = NULL; + vTaskDelay(20); // Short delay to allow task to be cleaned up +} + +#define TEST_CDC_ACM_PM_TIMER_INTERVAL_MS 1000 +#define TEST_CDC_ACM_PM_TIMER_MARGIN_MS 50 +/** + * @brief Test: Automatic suspend timer + * + * #. open the device, set One-Shot PM timer, expect one USB_HOST_CLIENT_EVENT_DEV_SUSPENDED event + * #. Set Periodic PM timer, expect multiple USB_HOST_CLIENT_EVENT_DEV_SUSPENDED events + * #. Disable Periodic PM timer, expect no event + * #. disconnect the device + * #. cleanup + */ +TEST_CASE("automatic_suspend_timer", "[cdc_acm]") +{ + cdc_acm_dev_hdl_t cdc_dev = NULL; + TEST_ASSERT_NOT_NULL(app_queue = xQueueCreate(5, sizeof(cdc_acm_host_dev_event_data_t))); + + test_install_cdc_driver(); + + // Use default device config + const cdc_acm_host_device_config_t dev_config = default_dev_config; + + printf("Opening CDC-ACM device\n"); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_open(0x303A, 0x4002, 0, &dev_config, &cdc_dev)); // 0x303A:0x4002 (TinyUSB Dual CDC device) + TEST_ASSERT_NOT_NULL(cdc_dev); + vTaskDelay(10); + + // Set One-Shot PM timer + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_set_auto_pm(USB_HOST_LIB_PM_SUSPEND_ONE_SHOT, TEST_CDC_ACM_PM_TIMER_INTERVAL_MS)); + wait_for_app_event(CDC_ACM_HOST_DEVICE_SUSPENDED, pdMS_TO_TICKS(TEST_CDC_ACM_PM_TIMER_INTERVAL_MS + TEST_CDC_ACM_PM_TIMER_MARGIN_MS)); + + // Manually resume the root port and expect the resumed event + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_resume()); + wait_for_app_event(CDC_ACM_HOST_DEVICE_RESUMED, 20); + + // Make sure no event is delivered, since the timer is a One-Shot timer + wait_for_no_app_event(pdMS_TO_TICKS(TEST_CDC_ACM_PM_TIMER_INTERVAL_MS * 2)); + + // Set Periodic PM suspend timer + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_set_auto_pm(USB_HOST_LIB_PM_SUSPEND_PERIODIC, TEST_CDC_ACM_PM_TIMER_INTERVAL_MS)); + + for (int i = 0; i < 3; i++) { + // Expect suspended event from auto suspend timer + wait_for_app_event(CDC_ACM_HOST_DEVICE_SUSPENDED, pdMS_TO_TICKS(TEST_CDC_ACM_PM_TIMER_INTERVAL_MS + TEST_CDC_ACM_PM_TIMER_MARGIN_MS)); + + // Resume the root port manually and expect the resume event + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_resume()); + wait_for_app_event(CDC_ACM_HOST_DEVICE_RESUMED, 20); + + // Verify data transmit on resumed device + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_data_tx_blocking(cdc_dev, tx_buf, sizeof(tx_buf), 1000)); + } + + // Disable the Periodic PM timer + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_set_auto_pm(USB_HOST_LIB_PM_SUSPEND_PERIODIC, 0)); + // Make sure no event is delivered + wait_for_no_app_event(pdMS_TO_TICKS(TEST_CDC_ACM_PM_TIMER_INTERVAL_MS * 2)); + + // Clean-up + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_close(cdc_dev)); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_uninstall()); + vQueueDelete(app_queue); + app_queue = NULL; + vTaskDelay(20); // Short delay to allow task to be cleaned up +} + +/** + * @brief Test: Sudden disconnect, while in suspended state + * + * #. open/suspend the device + * #. disconnect the device + * #. cleanup + */ +TEST_CASE("suspend_resume_sudden_disconnect", "[cdc_acm]") +{ + cdc_acm_dev_hdl_t cdc_dev = NULL; + TEST_ASSERT_NOT_NULL(app_queue = xQueueCreate(5, sizeof(cdc_acm_host_dev_event_data_t))); + + test_install_cdc_driver(); + + // Use default device config + const cdc_acm_host_device_config_t dev_config = default_dev_config; + + printf("Opening CDC-ACM device\n"); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_open(0x303A, 0x4002, 0, &dev_config, &cdc_dev)); // 0x303A:0x4002 (TinyUSB Dual CDC device) + TEST_ASSERT_NOT_NULL(cdc_dev); + vTaskDelay(10); + + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + wait_for_app_event(CDC_ACM_HOST_DEVICE_SUSPENDED, 100); + + force_conn_state(false, pdMS_TO_TICKS(10)); // Simulate device disconnection + wait_for_app_event(CDC_ACM_HOST_DEVICE_DISCONNECTED, 100); + + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_uninstall()); + vQueueDelete(app_queue); + app_queue = NULL; + vTaskDelay(20); // Short delay to allow task to be cleaned up +} + +static void suspend_tx_task(void *arg) +{ + cdc_acm_dev_hdl_t cdc_dev = (cdc_acm_dev_hdl_t) arg; + // Send multiple transfers to make sure that some of them will run at the same time + for (int i = 0; i < MULTIPLE_THREADS_TRANSFERS_NUM; i++) { + // We are expecting either + // - ESP_OK: Transfer was submitted or deferred + // - ESP_ERR_INVALID_STATE: Transfer can't be submitted or deferred, when the root port is in suspending state + + // BULK endpoints + esp_err_t ret; + ret = cdc_acm_host_data_tx_blocking(cdc_dev, tx_buf, sizeof(tx_buf), 2000); + TEST_ASSERT(ret == ESP_OK || ret == ESP_ERR_INVALID_STATE); + + // CTRL endpoints + cdc_acm_line_coding_t line_coding_get; + ret = cdc_acm_host_line_coding_get(cdc_dev, &line_coding_get); + TEST_ASSERT(ret == ESP_OK || ret == ESP_ERR_INVALID_STATE); + + ret = cdc_acm_host_set_control_line_state(cdc_dev, true, false); + TEST_ASSERT(ret == ESP_OK || ret == ESP_ERR_INVALID_STATE); + } + vTaskDelete(NULL); +} + +/** + * @brief Initiate auto suspend from multiple + * + * open device and send transfer from multiple tasks + * immediately suspend the root port + * expect both SUSPEND and RESUME events (resume event, because of auto-resume by transfer submit) + * Expect transfer either to pass, or to fail, due to root being int suspending state + * close the device, cleanup + */ +TEST_CASE("auto_suspend_multiple_threads", "[cdc_acm]") +{ + TEST_ASSERT_NOT_NULL(app_queue = xQueueCreate(5, sizeof(cdc_acm_host_dev_event_data_t))); + cdc_acm_dev_hdl_t cdc_dev; + test_install_cdc_driver(); + + const cdc_acm_host_device_config_t dev_config = { + .connection_timeout_ms = 5000, + .out_buffer_size = 64, + .event_cb = notif_cb, + .data_cb = handle_rx, + .user_arg = tx_buf, + }; + + printf("Opening CDC-ACM device\n"); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_open(0x303A, 0x4002, 0, &dev_config, &cdc_dev)); // 0x303A:0x4002 (TinyUSB Dual CDC device) + TEST_ASSERT_NOT_NULL(cdc_dev); + + // Create multiple tasks that will try to access cdc_dev + for (int i = 0; i < MULTIPLE_THREADS_TASKS_NUM; i++) { + TEST_ASSERT_EQUAL(pdTRUE, xTaskCreate(suspend_tx_task, "CDC TX", 4096, cdc_dev, i + 3, NULL)); + } + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + // Expect suspend event + wait_for_app_event(CDC_ACM_HOST_DEVICE_SUSPENDED, 100); + // As there are transfer being sent from other tasks, the root port will be automatically resumed. + // Expect resume event + wait_for_app_event(CDC_ACM_HOST_DEVICE_RESUMED, 1000); + + // Wait for all transfer to finish + vTaskDelay(100); + + // Clean-up + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_close(cdc_dev)); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_uninstall()); + vQueueDelete(app_queue); + app_queue = NULL; + vTaskDelay(20); // Short delay to allow task to be cleaned up +} + +/** + * @brief Test: Device close, while in suspended state + * + * #. open/suspend the device + * #. close the device + * #. fail to open the still suspended device + * #. cleanup + */ +TEST_CASE("device_close_while_suspended", "[cdc_acm]") +{ + cdc_acm_dev_hdl_t cdc_dev = NULL; + TEST_ASSERT_NOT_NULL(app_queue = xQueueCreate(5, sizeof(cdc_acm_host_dev_event_data_t))); + + test_install_cdc_driver(); + + // Use default device config + const cdc_acm_host_device_config_t dev_config = default_dev_config; + + printf("Opening CDC-ACM device\n"); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_open(0x303A, 0x4002, 0, &dev_config, &cdc_dev)); // 0x303A:0x4002 (TinyUSB Dual CDC device) + TEST_ASSERT_NOT_NULL(cdc_dev); + vTaskDelay(10); + + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + wait_for_app_event(CDC_ACM_HOST_DEVICE_SUSPENDED, 100); + + // Close the suspended device + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_close(cdc_dev)); + // Try to open the still suspended device + TEST_ASSERT_EQUAL(ESP_ERR_NOT_FOUND, cdc_acm_host_open(0x303A, 0x4002, 0, &dev_config, &cdc_dev)); // 0x303A:0x4002 (TinyUSB Dual CDC device) + + // Cleanup + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_uninstall()); + vQueueDelete(app_queue); + app_queue = NULL; + vTaskDelay(20); // Short delay to allow task to be cleaned up +} + +/** + * @brief Test: Device open, while in suspended state + * + * #. suspend the device + * #. fail to opend the suspended device + * #. resume/opend the device + * #. cleanup + */ +TEST_CASE("device_open_while_suspended", "[cdc_acm]") +{ + cdc_acm_dev_hdl_t cdc_dev = NULL; + test_install_cdc_driver(); + vTaskDelay(100); // Some time to enumerate the device + + // Suspend the root port, but do not expect any event, since the device wan never opened + usb_host_lib_root_port_suspend(); + vTaskDelay(100); // Some time to finish the suspend procedure + + // Use default device config + const cdc_acm_host_device_config_t dev_config = default_dev_config; + + printf("Opening CDC-ACM device\n"); + TEST_ASSERT_EQUAL(ESP_ERR_NOT_FOUND, cdc_acm_host_open(0x303A, 0x4002, 0, &dev_config, &cdc_dev)); // 0x303A:0x4002 (TinyUSB Dual CDC device) + TEST_ASSERT_NULL(cdc_dev); + + // Resume the device + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_resume()); + vTaskDelay(100); + + // Open the resumed device + printf("Opening CDC-ACM device\n"); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_open(0x303A, 0x4002, 0, &dev_config, &cdc_dev)); // 0x303A:0x4002 (TinyUSB Dual CDC device) + TEST_ASSERT_NOT_NULL(cdc_dev); + + // Clean-up + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_close(cdc_dev)); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_uninstall()); + vTaskDelay(20); // Short delay to allow task to be cleaned up +} + +/** + * @brief Test: resuming the device by submitting a transfer + * + * #. open/suspend the device + * #. submit a non-ctrl transfer, expect the device to resume + * #. submit a ctrl transfer, expect the device to resume + * #. cleanup + */ +TEST_CASE("resume_by_transfer_submit", "[cdc_acm]") +{ + nb_of_responses = 0; + cdc_acm_dev_hdl_t cdc_dev = NULL; + TEST_ASSERT_NOT_NULL(app_queue = xQueueCreate(5, sizeof(cdc_acm_host_dev_event_data_t))); + + test_install_cdc_driver(); + + // Use default device config + const cdc_acm_host_device_config_t dev_config = default_dev_config; + + printf("Opening CDC-ACM device\n"); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_open(0x303A, 0x4002, 0, &dev_config, &cdc_dev)); // 0x303A:0x4002 (TinyUSB Dual CDC device) + TEST_ASSERT_NOT_NULL(cdc_dev); + vTaskDelay(10); + + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + wait_for_app_event(CDC_ACM_HOST_DEVICE_SUSPENDED, 100); + + // BULK endpoints + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_data_tx_blocking(cdc_dev, tx_buf, sizeof(tx_buf), 1000)); + wait_for_app_event(CDC_ACM_HOST_DEVICE_RESUMED, 1000); + TEST_ASSERT_EQUAL(nb_of_responses, 1); + + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + wait_for_app_event(CDC_ACM_HOST_DEVICE_SUSPENDED, 100); + + // CTRL endpoints + cdc_acm_line_coding_t line_coding_get; + const cdc_acm_line_coding_t line_coding_set = { + .dwDTERate = 9600, + .bDataBits = 7, + .bParityType = 1, + .bCharFormat = 1, + }; + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_line_coding_set(cdc_dev, &line_coding_set)); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_line_coding_get(cdc_dev, &line_coding_get)); + TEST_ASSERT_EQUAL_MEMORY(&line_coding_set, &line_coding_get, sizeof(cdc_acm_line_coding_t)); + wait_for_app_event(CDC_ACM_HOST_DEVICE_RESUMED, 1000); + + // Clean-up + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_close(cdc_dev)); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_uninstall()); + vQueueDelete(app_queue); + app_queue = NULL; + vTaskDelay(20); // Short delay to allow task to be cleaned up +} + +#endif // CDC_HOST_SUSPEND_RESUME_API_SUPPORTED + /* Following test case implements dual CDC-ACM USB device that can be used as mock device for CDC-ACM Host tests */ void run_usb_dual_cdc_device(void); TEST_CASE("mock_device_app", "[cdc_acm_device][ignore]")