diff --git a/host/class/hid/usb_host_hid/CHANGELOG.md b/host/class/hid/usb_host_hid/CHANGELOG.md index e4a3468f..e573dc5a 100644 --- a/host/class/hid/usb_host_hid/CHANGELOG.md +++ b/host/class/hid/usb_host_hid/CHANGELOG.md @@ -1,3 +1,7 @@ +## [unreleased] + +- Added global suspend/resume support + ## 1.0.3 - Fixed a bug with interface mismatch on EP IN transfer complete while several HID devices are present. - Fixed a bug during device freeing, while detaching one of several attached HID devices. diff --git a/host/class/hid/usb_host_hid/hid_host.c b/host/class/hid/usb_host_hid/hid_host.c index 6f4f8b32..b54c6a42 100644 --- a/host/class/hid/usb_host_hid/hid_host.c +++ b/host/class/hid/usb_host_hid/hid_host.c @@ -91,6 +91,7 @@ typedef enum { HID_INTERFACE_STATE_READY, /**< HID Interface opened and ready to start transfer */ HID_INTERFACE_STATE_ACTIVE, /**< HID Interface is in use */ HID_INTERFACE_STATE_WAIT_USER_DELETION, /**< HID Interface wait user to be removed */ + HID_INTERFACE_STATE_SUSPENDED, /**< HID Interface (and the whole device) is suspended */ HID_INTERFACE_STATE_MAX } hid_iface_state_t; @@ -111,6 +112,7 @@ typedef struct hid_interface { hid_host_interface_event_cb_t user_cb; /**< Interface application callback */ void *user_cb_arg; /**< Interface application callback arg */ hid_iface_state_t state; /**< Interface state */ + hid_iface_state_t last_state; /**< Interface last state before entering suspended mode */ } hid_iface_t; /** @@ -550,6 +552,150 @@ static esp_err_t hid_host_device_disconnected(usb_device_handle_t dev_hdl) return ESP_OK; } +#ifdef HID_HOST_SUSPEND_RESUME_API_SUPPORTED + +/** + * @brief Suspend interface + * + * @note endpoints are already halted and flushed when a global suspend is issues by the USB Host lib + * @param[in] iface HID interface handle + * @param[in] stop_ep Stop (halt and flush) endpoint + * + * @return esp_err_t + */ +static esp_err_t hid_host_suspend_interface(hid_iface_t *iface, bool stop_ep) +{ + HID_RETURN_ON_INVALID_ARG(iface); + HID_RETURN_ON_INVALID_ARG(iface->parent); + + HID_RETURN_ON_FALSE(is_interface_in_list(iface), + ESP_ERR_NOT_FOUND, + "Interface handle not found"); + + HID_RETURN_ON_FALSE((HID_INTERFACE_STATE_SUSPENDED != iface->state), + ESP_ERR_INVALID_STATE, + "Interface wrong state"); + + // EP is usually stopped by usb_host_lib, in case of global suspend, thus no need to Halt->Flush EP again + if (stop_ep) { + HID_RETURN_ON_ERROR( usb_host_endpoint_halt(iface->parent->dev_hdl, iface->ep_in), + "Unable to HALT EP"); + HID_RETURN_ON_ERROR( usb_host_endpoint_flush(iface->parent->dev_hdl, iface->ep_in), + "Unable to FLUSH EP"); + // Don't clear EP, it must remain halted, when the device is in suspended state + } + + iface->last_state = iface->state; + iface->state = HID_INTERFACE_STATE_SUSPENDED; + + return ESP_OK; +} + +/** + * @brief Resume interface + * + * @note endpoints are already cleared when a global resume is issues by the USB Host lib + * @param[in] iface HID interface handle + * @param[in] resume_ep Resume (clear) endpoint + * + * @return esp_err_t + */ +static esp_err_t hid_host_resume_interface(hid_iface_t *iface, bool resume_ep) +{ + HID_RETURN_ON_INVALID_ARG(iface); + HID_RETURN_ON_INVALID_ARG(iface->parent); + + HID_RETURN_ON_FALSE(is_interface_in_list(iface), + ESP_ERR_NOT_FOUND, + "Interface handle not found"); + + HID_RETURN_ON_FALSE ((HID_INTERFACE_STATE_SUSPENDED == iface->state), + ESP_ERR_INVALID_STATE, + "Interface wrong state"); + + // EP is usually cleared by usb_host_lib, in case of global suspend, thus no need to Clear an EP again + if (resume_ep) { + usb_host_endpoint_clear(iface->parent->dev_hdl, iface->ep_in); + } + + iface->state = iface->last_state; + + if (iface->in_xfer == NULL) { + return ESP_OK; + } + + // start data transfer + return usb_host_transfer_submit(iface->in_xfer); +} + +/** + * @brief Suspend device + * + * @param[in] dev_hdl USB Device handle + * + * @return esp_err_t + */ +static esp_err_t hid_host_device_suspended(usb_device_handle_t dev_hdl) +{ + hid_device_t *hid_device = get_hid_device_by_handle(dev_hdl); + HID_RETURN_ON_INVALID_ARG(hid_device); + + HID_ENTER_CRITICAL(); + hid_iface_t *hid_iface_curr; + hid_iface_t *hid_iface_next; + // Go through list + hid_iface_curr = STAILQ_FIRST(&s_hid_driver->hid_ifaces_tailq); + while (hid_iface_curr != NULL) { + hid_iface_next = STAILQ_NEXT(hid_iface_curr, tailq_entry); + HID_EXIT_CRITICAL(); + + if (hid_iface_curr->parent && (hid_iface_curr->parent->dev_addr == hid_device->dev_addr)) { + hid_host_suspend_interface(hid_iface_curr, false); + hid_host_user_interface_callback(hid_iface_curr, HID_HOST_INTERFACE_EVENT_SUSPENDED); + } + HID_ENTER_CRITICAL(); + hid_iface_curr = hid_iface_next; + } + HID_EXIT_CRITICAL(); + + return ESP_OK; +} + +/** + * @brief Resume device + * + * @param[in] dev_hdl USB Device handle + * + * @return esp_err_t + */ +static esp_err_t hid_host_device_resumed(usb_device_handle_t dev_hdl) +{ + hid_device_t *hid_device = get_hid_device_by_handle(dev_hdl); + HID_RETURN_ON_INVALID_ARG(hid_device); + + HID_ENTER_CRITICAL(); + hid_iface_t *hid_iface_curr; + hid_iface_t *hid_iface_next; + // Go through list + hid_iface_curr = STAILQ_FIRST(&s_hid_driver->hid_ifaces_tailq); + while (hid_iface_curr != NULL) { + hid_iface_next = STAILQ_NEXT(hid_iface_curr, tailq_entry); + HID_EXIT_CRITICAL(); + + if (hid_iface_curr->parent && (hid_iface_curr->parent->dev_addr == hid_device->dev_addr)) { + hid_host_resume_interface(hid_iface_curr, false); + hid_host_user_interface_callback(hid_iface_curr, HID_HOST_INTERFACE_EVENT_RESUMED); + } + HID_ENTER_CRITICAL(); + hid_iface_curr = hid_iface_next; + } + HID_EXIT_CRITICAL(); + + return ESP_OK; +} + +#endif // HID_HOST_SUSPEND_RESUME_API_SUPPORTED + /** * @brief USB Host Client's event callback * @@ -558,10 +704,28 @@ static esp_err_t hid_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"); hid_host_device_init_attempt(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"); hid_host_device_disconnected(event->dev_gone.dev_hdl); + break; +#ifdef HID_HOST_SUSPEND_RESUME_API_SUPPORTED + case USB_HOST_CLIENT_EVENT_DEV_SUSPENDED: + ESP_LOGD(TAG, "Device suspended"); + hid_host_device_suspended(event->dev_suspend_resume.dev_hdl); + break; + case USB_HOST_CLIENT_EVENT_DEV_RESUMED: + ESP_LOGD(TAG, "Device resumed"); + hid_host_device_resumed(event->dev_suspend_resume.dev_hdl); + break; +#endif // HID_HOST_SUSPEND_RESUME_API_SUPPORTED + default: + ESP_LOGW(TAG, "Unrecognized USB Host client event"); + break; } } @@ -628,14 +792,19 @@ static esp_err_t hid_host_disable_interface(hid_iface_t *iface) ESP_ERR_NOT_FOUND, "Interface handle not found"); - HID_RETURN_ON_FALSE((HID_INTERFACE_STATE_ACTIVE == iface->state), + HID_RETURN_ON_FALSE((HID_INTERFACE_STATE_ACTIVE == iface->state || + HID_INTERFACE_STATE_SUSPENDED == iface->state), ESP_ERR_INVALID_STATE, "Interface wrong state"); - HID_RETURN_ON_ERROR( usb_host_endpoint_halt(iface->parent->dev_hdl, iface->ep_in), - "Unable to HALT EP"); - HID_RETURN_ON_ERROR( usb_host_endpoint_flush(iface->parent->dev_hdl, iface->ep_in), - "Unable to FLUSH EP"); + if (HID_INTERFACE_STATE_ACTIVE == iface->state) { + HID_RETURN_ON_ERROR( usb_host_endpoint_halt(iface->parent->dev_hdl, iface->ep_in), + "Unable to HALT EP"); + HID_RETURN_ON_ERROR( usb_host_endpoint_flush(iface->parent->dev_hdl, iface->ep_in), + "Unable to FLUSH EP"); + } + // If interface state is suspended, the EP is already flushed and halted, only clear the EP + // If suspended, may return ESP_ERR_INVALID_STATE usb_host_endpoint_clear(iface->parent->dev_hdl, iface->ep_in); iface->state = HID_INTERFACE_STATE_READY; @@ -1204,7 +1373,8 @@ esp_err_t hid_host_device_close(hid_host_device_handle_t hid_dev_handle) hid_iface->dev_params.iface_num, hid_iface->state); - if (HID_INTERFACE_STATE_ACTIVE == hid_iface->state) { + if (HID_INTERFACE_STATE_ACTIVE == hid_iface->state || + HID_INTERFACE_STATE_SUSPENDED == hid_iface->state) { HID_RETURN_ON_ERROR( hid_host_disable_interface(hid_iface), "Unable to disable HID Interface"); } @@ -1212,7 +1382,6 @@ esp_err_t hid_host_device_close(hid_host_device_handle_t hid_dev_handle) if (HID_INTERFACE_STATE_READY == hid_iface->state) { HID_RETURN_ON_ERROR( hid_host_interface_release_and_free_transfer(hid_iface), "Unable to release HID Interface"); - // If the device is closing by user before device detached we need to flush user callback here free(hid_iface->report_desc); hid_iface->report_desc = NULL; diff --git a/host/class/hid/usb_host_hid/include/usb/hid_host.h b/host/class/hid/usb_host_hid/include/usb/hid_host.h index cd981e14..39ac235e 100644 --- a/host/class/hid/usb_host_hid/include/usb/hid_host.h +++ b/host/class/hid/usb_host_hid/include/usb/hid_host.h @@ -11,6 +11,7 @@ #include "esp_err.h" #include +#include "usb/usb_host.h" #include "hid.h" #ifdef __cplusplus @@ -29,6 +30,11 @@ extern "C" { */ #define HID_STR_DESC_MAX_LENGTH 32 +// For backward compatibility, remove after IDF 5.x EOL TODO IDF version +#ifdef USB_HOST_LIB_EVENT_FLAGS_AUTO_SUSPEND +#define HID_HOST_SUSPEND_RESUME_API_SUPPORTED +#endif + typedef struct hid_interface *hid_host_device_handle_t; /**< Device Handle. Handle to a particular HID interface */ // ------------------------ USB HID Host events -------------------------------- @@ -46,6 +52,10 @@ typedef enum { HID_HOST_INTERFACE_EVENT_INPUT_REPORT = 0x00, /**< HID Device input report */ HID_HOST_INTERFACE_EVENT_TRANSFER_ERROR, /**< HID Device transfer error */ HID_HOST_INTERFACE_EVENT_DISCONNECTED, /**< HID Device has been disconnected */ +#ifdef HID_HOST_SUSPEND_RESUME_API_SUPPORTED + HID_HOST_INTERFACE_EVENT_SUSPENDED, /**< HID Device has been suspended */ + HID_HOST_INTERFACE_EVENT_RESUMED, /**< HID Device has been resumed */ +#endif // HID_HOST_SUSPEND_RESUME_API_SUPPORTED } hid_host_interface_event_t; /** diff --git a/host/class/hid/usb_host_hid/test_app/main/test_hid_basic.c b/host/class/hid/usb_host_hid/test_app/main/test_hid_basic.c index 79027023..32a93045 100644 --- a/host/class/hid/usb_host_hid/test_app/main/test_hid_basic.c +++ b/host/class/hid/usb_host_hid/test_app/main/test_hid_basic.c @@ -48,17 +48,35 @@ static const char *test_hid_proto_names[] = { "MOUSE" }; +typedef enum { + HID_DRIVER_EVENT = 0, + HID_INTERFACE_EVENT, +} event_group_t; + typedef struct { - hid_host_device_handle_t hid_device_handle; - hid_host_driver_event_t event; - void *arg; -} hid_host_test_event_queue_t; + event_group_t event_group; + union { + struct { + hid_host_driver_event_t event; + hid_host_device_handle_t hid_device_handle; + void *arg; + } driver_evt; + struct { + hid_host_interface_event_t event; + hid_host_device_handle_t hid_device_handle; + void *arg; + } interface_evt; + }; +} hid_host_event_queue_t; typedef enum { HID_HOST_TEST_TOUCH_WAY_ASSERT = 0x00, HID_HOST_TEST_TOUCH_WAY_SUDDEN_DISCONNECT = 0x01, } hid_host_test_touch_way_t; +static void test_hid_host_device_touch(hid_host_dev_params_t *dev_params, + hid_host_test_touch_way_t touch_way); + // 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) @@ -219,15 +237,181 @@ void hid_host_test_concurrent(hid_host_device_handle_t hid_device_handle, } } +#ifdef HID_HOST_SUSPEND_RESUME_API_SUPPORTED +/** + * @brief Expect interface or device events + * @note The function also checks for no events being delivered + * + * @param[in] expected_event Pointer to an expected event, NULL to expect NO event + * @param[in] ticks Ticks to wait for the event + */ +static void hid_host_test_expect_event(hid_host_event_queue_t *expected_event, TickType_t ticks) +{ + TEST_ASSERT_NOT_NULL_MESSAGE(hid_host_test_event_queue, "App queue has not been initialized"); + + // Expect NO event + if (expected_event == NULL) { + hid_host_event_queue_t event_queue; + if (pdFALSE == xQueueReceive(hid_host_test_event_queue, &event_queue, ticks)) { + // Expecting NO event, none delivered, return + return; + } else { + TEST_FAIL_MESSAGE("Expecting NO event, but an event delivered"); + } + } + + // Expect 2 events, one for each device + for (int i = 0; i < 2; i++) { + hid_host_event_queue_t event_queue; + if (pdTRUE == xQueueReceive(hid_host_test_event_queue, &event_queue, ticks)) { + TEST_ASSERT_EQUAL_MESSAGE(expected_event->event_group, event_queue.event_group, "Unexpected event group"); + if (event_queue.event_group == HID_DRIVER_EVENT) { + TEST_ASSERT_EQUAL_MESSAGE(event_queue.driver_evt.event, expected_event->driver_evt.event, "Unexpected driver event"); + } else { + TEST_ASSERT_EQUAL_MESSAGE(event_queue.interface_evt.event, expected_event->interface_evt.event, "Unexpected interface event"); + } + } else { + TEST_FAIL_MESSAGE("Device event not generated on time"); + } + } +} + +/** + * @brief HID Host interface callback with power management (suspend/resume) events + * @note The callback is pushing events to an event queue + * + * @param[in] hid_device_handle Device handle + * @param[in] event Interface event + * @param[in] arg Callback argument + */ +static void hid_host_pm_interface_callback(hid_host_device_handle_t hid_device_handle, + const hid_host_interface_event_t event, + void *arg) +{ + uint8_t data[64] = { 0 }; + size_t data_length = 0; + hid_host_dev_params_t dev_params; + TEST_ASSERT_EQUAL(ESP_OK, hid_host_device_get_params(hid_device_handle, &dev_params)); + TEST_ASSERT_EQUAL_PTR_MESSAGE(&user_arg_value, arg, "User argument has lost"); + + switch (event) { + case HID_HOST_INTERFACE_EVENT_INPUT_REPORT: + printf("USB port %d, Interface num %d: ", + dev_params.addr, + dev_params.iface_num); + + hid_host_device_get_raw_input_report_data(hid_device_handle, + data, + 64, + &data_length); + + for (int i = 0; i < data_length; i++) { + printf("%02x ", data[i]); + } + printf("\n"); + break; + case HID_HOST_INTERFACE_EVENT_DISCONNECTED: + printf("USB port %d, interface %d, '%s', '%s' DISCONNECTED\n", + dev_params.addr, + dev_params.iface_num, + test_hid_sub_class_names[dev_params.sub_class], + test_hid_proto_names[dev_params.proto]); + TEST_ASSERT_EQUAL(ESP_OK, hid_host_device_close(hid_device_handle) ); + break; + case HID_HOST_INTERFACE_EVENT_TRANSFER_ERROR: + printf("USB Host transfer error\n"); + break; + case HID_HOST_INTERFACE_EVENT_SUSPENDED: + printf("USB port %d, interface %d, '%s', '%s' SUSPENDED\n", + dev_params.addr, + dev_params.iface_num, + test_hid_sub_class_names[dev_params.sub_class], + test_hid_proto_names[dev_params.proto]); + break; + case HID_HOST_INTERFACE_EVENT_RESUMED: + printf("USB port %d, interface %d, '%s', '%s' RESUMED\n", + dev_params.addr, + dev_params.iface_num, + test_hid_sub_class_names[dev_params.sub_class], + test_hid_proto_names[dev_params.proto]); + break; + default: + TEST_FAIL_MESSAGE("HID Interface unhandled event"); + break; + } + + const hid_host_event_queue_t event_queue = { + .event_group = HID_INTERFACE_EVENT, + .interface_evt.hid_device_handle = hid_device_handle, + .interface_evt.event = event, + .interface_evt.arg = arg, + }; + + xQueueSend(hid_host_test_event_queue, &event_queue, 0); +} + +/** + * @brief HID Host driver callback + * @note The callback is pushing events to an event queue + * + * @param[in] hid_device_handle Device handle + * @param[in] event Driver event + * @param[in] arg Callback argument + */ +static void hid_host_test_pm_driver_callback(hid_host_device_handle_t hid_device_handle, + const hid_host_driver_event_t event, + void *arg) +{ + hid_host_dev_params_t dev_params; + TEST_ASSERT_EQUAL(ESP_OK, hid_host_device_get_params(hid_device_handle, &dev_params)); + TEST_ASSERT_EQUAL_PTR_MESSAGE(&user_arg_value, arg, "User argument has lost"); + + switch (event) { + case HID_HOST_DRIVER_EVENT_CONNECTED: + printf("USB port %d, interface %d, '%s', '%s' CONNECTED\n", + dev_params.addr, + dev_params.iface_num, + test_hid_sub_class_names[dev_params.sub_class], + test_hid_proto_names[dev_params.proto]); + + const hid_host_device_config_t dev_config = { + .callback = hid_host_pm_interface_callback, + .callback_arg = &user_arg_value + }; + + TEST_ASSERT_EQUAL(ESP_OK, hid_host_device_open(hid_device_handle, &dev_config) ); + TEST_ASSERT_EQUAL(ESP_OK, hid_host_device_start(hid_device_handle) ); + + global_hdl = hid_device_handle; + break; + default: + TEST_FAIL_MESSAGE("HID Driver unhandled event"); + return; + } + + const hid_host_event_queue_t event_queue = { + .event_group = HID_DRIVER_EVENT, + .driver_evt.hid_device_handle = hid_device_handle, + .driver_evt.event = event, + .driver_evt.arg = arg, + }; + + xQueueSend(hid_host_test_event_queue, &event_queue, 0); +} +#endif // HID_HOST_SUSPEND_RESUME_API_SUPPORTED + void hid_host_test_device_callback_to_queue(hid_host_device_handle_t hid_device_handle, const hid_host_driver_event_t event, void *arg) { - const hid_host_test_event_queue_t evt_queue = { - .hid_device_handle = hid_device_handle, - .event = event, - .arg = arg + + const hid_host_event_queue_t evt_queue = { + .event_group = HID_DRIVER_EVENT, + .driver_evt.hid_device_handle = hid_device_handle, + .driver_evt.event = event, + .driver_evt.arg = arg, }; + xQueueSend(hid_host_test_event_queue, &evt_queue, 0); } @@ -365,16 +549,16 @@ void hid_host_test_requests_callback(hid_host_device_handle_t hid_device_handle, void hid_host_test_task(void *pvParameters) { - hid_host_test_event_queue_t evt_queue; + hid_host_event_queue_t evt_queue; // Create queue - hid_host_test_event_queue = xQueueCreate(10, sizeof(hid_host_test_event_queue_t)); + hid_host_test_event_queue = xQueueCreate(10, sizeof(hid_host_event_queue_t)); // Wait queue while (!time_to_shutdown) { if (xQueueReceive(hid_host_test_event_queue, &evt_queue, pdMS_TO_TICKS(50))) { - hid_host_test_requests_callback(evt_queue.hid_device_handle, - evt_queue.event, - evt_queue.arg); + hid_host_test_requests_callback(evt_queue.driver_evt.hid_device_handle, + evt_queue.driver_evt.event, + evt_queue.driver_evt.arg); } } @@ -531,6 +715,13 @@ static void usb_lib_task(void *arg) // Notify that device was being disconnected xTaskNotifyGive(arg); } +#ifdef HID_HOST_SUSPEND_RESUME_API_SUPPORTED + // Auto-suspend timer + if (event_flags & USB_HOST_LIB_EVENT_FLAGS_AUTO_SUSPEND) { + printf("USB Event flags: AUTO_SUSPEND\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + } +#endif // HID_HOST_SUSPEND_RESUME_API_SUPPORTED } // Change global flag for all tasks still running @@ -701,6 +892,229 @@ TEST_CASE("sudden_disconnect", "[hid_host]") test_hid_teardown(); } +#ifdef HID_HOST_SUSPEND_RESUME_API_SUPPORTED +/** + * @brief Basic Suspend/Resume sequence + * + * Purpose: + * - Test HID Host reaction to global suspend/resume events + * + * Procedure: + * - Install USB Host lib, Install HID driver, open device and start device + * - Suspend and resume the root port, check that correct interface events are delivered + * - Teardown + */ +TEST_CASE("suspend_resume_basic", "[hid_host]") +{ + hid_host_test_event_queue = xQueueCreate(10, sizeof(hid_host_event_queue_t)); + TEST_ASSERT_NOT_NULL(hid_host_test_event_queue); + TickType_t expect_event_ticks = pdMS_TO_TICKS(1000); + + // Install USB and HID driver with 'hid_host_test_pm_driver_callback' + test_hid_setup(hid_host_test_pm_driver_callback, HID_TEST_EVENT_HANDLE_IN_DRIVER); + + // Wait, until the device is connected, expect 2 CONNECTED events + hid_host_event_queue_t expected_event = { + .event_group = HID_DRIVER_EVENT, + .driver_evt.event = HID_HOST_DRIVER_EVENT_CONNECTED + }; + hid_host_test_expect_event(&expected_event, expect_event_ticks); + + printf("Issue suspend\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + expected_event.event_group = HID_INTERFACE_EVENT; + expected_event.interface_evt.event = HID_HOST_INTERFACE_EVENT_SUSPENDED; + hid_host_test_expect_event(&expected_event, expect_event_ticks); + + printf("Issue resume\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_resume()); + expected_event.interface_evt.event = HID_HOST_INTERFACE_EVENT_RESUMED; + hid_host_test_expect_event(&expected_event, expect_event_ticks); + + // Tear down test + test_hid_teardown(); + vQueueDelete(hid_host_test_event_queue); + hid_host_test_event_queue = NULL; +} + +#define TEST_HID_PM_TIMER_INTERVAL_MS 500 +#define TEST_HID_PM_TIMER_MARGIN_MS 50 + +/** + * @brief Auto Suspend timer + * + * Purpose: + * - Test auto suspend timer functionality (One-Shot and Periodic timer settings) + * + * Procedure: + * - Install USB Host lib, Install HID driver, open device and start device + * - Set auto-suspend timer, expect the root port to be suspended by expecting interface events + * - Issue a CTRL transfer to the device, expect the root port to be resumed + * - Teardown + */ +TEST_CASE("auto_suspend_timer", "[hid_host]") +{ + hid_host_test_event_queue = xQueueCreate(10, sizeof(hid_host_event_queue_t)); + TEST_ASSERT_NOT_NULL(hid_host_test_event_queue); + TickType_t expect_event_ticks = pdMS_TO_TICKS(1000); + + // Install USB and HID driver with 'hid_host_test_pm_driver_callback' + test_hid_setup(hid_host_test_pm_driver_callback, HID_TEST_EVENT_HANDLE_IN_DRIVER); + + // Wait, until the device is connected, expect 2 CONNECTED events + hid_host_event_queue_t expected_event = { + .event_group = HID_DRIVER_EVENT, + .driver_evt.event = HID_HOST_DRIVER_EVENT_CONNECTED + }; + hid_host_test_expect_event(&expected_event, expect_event_ticks); + + // Set one-shot auto suspend timer, and expect suspended event + printf("Set One-Shot auto suspend timer\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_set_auto_pm(USB_HOST_LIB_PM_SUSPEND_ONE_SHOT, TEST_HID_PM_TIMER_INTERVAL_MS)); + expected_event.event_group = HID_INTERFACE_EVENT; + expected_event.interface_evt.event = HID_HOST_INTERFACE_EVENT_SUSPENDED; + hid_host_test_expect_event(&expected_event, pdMS_TO_TICKS(TEST_HID_PM_TIMER_INTERVAL_MS + TEST_HID_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()); + expected_event.interface_evt.event = HID_HOST_INTERFACE_EVENT_RESUMED; + hid_host_test_expect_event(&expected_event, expect_event_ticks); + + // Make sure no other event is delivered, as the PM timer is a one-shot timer + hid_host_test_expect_event(NULL, pdMS_TO_TICKS(TEST_HID_PM_TIMER_INTERVAL_MS * 2)); + + // Set periodic PM suspend timer + printf("Set Periodic auto suspend timer\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_set_auto_pm(USB_HOST_LIB_PM_SUSPEND_PERIODIC, TEST_HID_PM_TIMER_INTERVAL_MS)); + + for (int i = 0; i < 3; i++) { + // Expect suspend event from the periodic auto suspend timer + expected_event.interface_evt.event = HID_HOST_INTERFACE_EVENT_SUSPENDED; + hid_host_test_expect_event(&expected_event, pdMS_TO_TICKS(TEST_HID_PM_TIMER_INTERVAL_MS + TEST_HID_PM_TIMER_MARGIN_MS)); + + // Even though the Periodic timer is running, don't expect any event because of suspended root port + hid_host_test_expect_event(NULL, pdMS_TO_TICKS(TEST_HID_PM_TIMER_INTERVAL_MS * 2)); + + // Manually resume the root port and expect the resumed event + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_resume()); + expected_event.interface_evt.event = HID_HOST_INTERFACE_EVENT_RESUMED; + hid_host_test_expect_event(&expected_event, expect_event_ticks); + + // Verify transfer on resumed device + hid_host_dev_params_t dev_params; + TEST_ASSERT_EQUAL(ESP_OK, hid_host_device_get_params(global_hdl, &dev_params)); + test_hid_host_device_touch(&dev_params, HID_HOST_TEST_TOUCH_WAY_ASSERT); + } + + // Disable the Periodic PM timer + printf("Disable Periodic auto suspend timer\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_set_auto_pm(USB_HOST_LIB_PM_SUSPEND_PERIODIC, 0)); + // Make sure no event is delivered + hid_host_test_expect_event(NULL, pdMS_TO_TICKS(TEST_HID_PM_TIMER_INTERVAL_MS * 2)); + + // Tear down test + test_hid_teardown(); + vQueueDelete(hid_host_test_event_queue); + hid_host_test_event_queue = NULL; +} + +/** + * @brief Resume by transfer submit + * + * Purpose: + * - Test, that a device can be resumed submitting a transfer + * + * Procedure: + * - Install USB Host lib, Install HID driver, open device and start device + * - Manually suspend the root port, expect suspend event + * - Issue a CTRL transfer to the device, expect the root port to be resumed, expect resume event + * - Teardown + */ +TEST_CASE("resume_by_transfer_submit", "[hid_host]") +{ + hid_host_test_event_queue = xQueueCreate(10, sizeof(hid_host_event_queue_t)); + TEST_ASSERT_NOT_NULL(hid_host_test_event_queue); + TickType_t expect_event_ticks = pdMS_TO_TICKS(1000); + + // Install USB and HID driver with 'hid_host_test_pm_driver_callback' + test_hid_setup(hid_host_test_pm_driver_callback, HID_TEST_EVENT_HANDLE_IN_DRIVER); + + // Wait, until the device is connected, expect 2 CONNECTED events + hid_host_event_queue_t expected_event = { + .event_group = HID_DRIVER_EVENT, + .driver_evt.event = HID_HOST_DRIVER_EVENT_CONNECTED + }; + hid_host_test_expect_event(&expected_event, expect_event_ticks); + + // Suspend the root port manually + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + expected_event.event_group = HID_INTERFACE_EVENT; + expected_event.interface_evt.event = HID_HOST_INTERFACE_EVENT_SUSPENDED; + hid_host_test_expect_event(&expected_event, expect_event_ticks); + + hid_host_dev_params_t dev_params; + TEST_ASSERT_EQUAL(ESP_OK, hid_host_device_get_params(global_hdl, &dev_params)); + + // Auto resume the device by sending a ctrl transfer + test_hid_host_device_touch(&dev_params, HID_HOST_TEST_TOUCH_WAY_ASSERT); + expected_event.interface_evt.event = HID_HOST_INTERFACE_EVENT_RESUMED; + hid_host_test_expect_event(&expected_event, expect_event_ticks); + + // Tear down test + test_hid_teardown(); + vQueueDelete(hid_host_test_event_queue); + hid_host_test_event_queue = NULL; +} + +/** + * @brief Sudden disconnect with suspended device + * + * Purpose: + * - Test HID Host reaction to sudden disconnection with suspended device + * + * Procedure: + * - Install USB Host lib, Install HID driver, open device and start device + * - Suspend the root port, check that correct interface events are delivered + * - Disconnect the device, expect disconnection event to be delivered + * - Teardown + */ +TEST_CASE("sudden_disconnect_suspended_device", "[hid_host]") +{ + hid_host_test_event_queue = xQueueCreate(10, sizeof(hid_host_event_queue_t)); + TEST_ASSERT_NOT_NULL(hid_host_test_event_queue); + TickType_t expect_event_ticks = pdMS_TO_TICKS(1000); + + // Install USB and HID driver with 'hid_host_test_pm_driver_callback' + test_hid_setup(hid_host_test_pm_driver_callback, HID_TEST_EVENT_HANDLE_IN_DRIVER); + + // Wait, until the device is connected, expect 2 CONNECTED events + hid_host_event_queue_t expected_event = { + .event_group = HID_DRIVER_EVENT, + .driver_evt.event = HID_HOST_DRIVER_EVENT_CONNECTED + }; + hid_host_test_expect_event(&expected_event, expect_event_ticks); + + printf("Issue suspend\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + expected_event.event_group = HID_INTERFACE_EVENT; + expected_event.interface_evt.event = HID_HOST_INTERFACE_EVENT_SUSPENDED; + hid_host_test_expect_event(&expected_event, expect_event_ticks); + + // Disconnect the device, while the root port is suspended + force_conn_state(false, pdMS_TO_TICKS(1000)); + expected_event.interface_evt.event = HID_HOST_INTERFACE_EVENT_DISCONNECTED; + hid_host_test_expect_event(&expected_event, expect_event_ticks); + + // Tear down test + vTaskDelay(20); + TEST_ASSERT_EQUAL(ESP_OK, hid_host_uninstall() ); + ulTaskNotifyValueClear(NULL, 1); + vTaskDelay(20); + vQueueDelete(hid_host_test_event_queue); + hid_host_test_event_queue = NULL; +} +#endif // HID_HOST_SUSPEND_RESUME_API_SUPPORTED + TEST_CASE("mock_hid_device", "[hid_device][ignore]") { hid_mock_device(TUSB_IFACE_COUNT_ONE); diff --git a/host/class/hid/usb_host_hid/test_app/main/test_hid_err_handling.c b/host/class/hid/usb_host_hid/test_app/main/test_hid_err_handling.c index a06496d2..0ba7ecfc 100644 --- a/host/class/hid/usb_host_hid/test_app/main/test_hid_err_handling.c +++ b/host/class/hid/usb_host_hid/test_app/main/test_hid_err_handling.c @@ -7,13 +7,17 @@ #include #include #include +#include "esp_log.h" #include "unity.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" +#include "freertos/queue.h" #include "usb/hid_host.h" #include "test_hid_basic.h" +static const char *TAG = "hid-test"; + // ----------------------- Private ------------------------- /** * @brief USB HID Host interface callback. @@ -29,12 +33,11 @@ static void test_hid_host_interface_event_close(hid_host_device_handle_t hid_dev void *arg) { switch (event) { - case HID_HOST_INTERFACE_EVENT_INPUT_REPORT: - case HID_HOST_INTERFACE_EVENT_TRANSFER_ERROR: - break; case HID_HOST_INTERFACE_EVENT_DISCONNECTED: TEST_ASSERT_EQUAL(ESP_OK, hid_host_device_close(hid_device_handle) ); break; + default: + break; } } @@ -82,12 +85,14 @@ static void test_hid_host_event_callback_open(hid_host_device_handle_t hid_devic // Install HID driver without USB Host and without configuration static void test_install_hid_driver_without_config(void) { + ESP_LOGI(TAG, "Install driver without config"); TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, hid_host_install(NULL)); } // Install HID driver without USB Host and with configuration static void test_install_hid_driver_with_wrong_config(void) { + ESP_LOGI(TAG, "Install driver with incorrect config"); const hid_host_driver_config_t hid_host_config_callback_null = { .create_background_task = true, .task_priority = 5, @@ -143,6 +148,7 @@ void test_interface_callback_handler(hid_host_device_handle_t hid_device_handle, // Open device without installed driver static void test_claim_interface_without_driver(void) { + ESP_LOGI(TAG, "Claim interface without driver installed"); hid_host_device_handle_t hid_dev_handle = NULL; const hid_host_device_config_t dev_config = { @@ -156,6 +162,7 @@ static void test_claim_interface_without_driver(void) static void test_install_hid_driver_when_already_installed(void) { + ESP_LOGI(TAG, "Install driver while driver already installed"); // Install USB and HID driver with the stub test_hid_host_event_callback_stub test_hid_setup(test_hid_host_event_callback_stub, HID_TEST_EVENT_HANDLE_IN_DRIVER); // Try to install HID driver again @@ -175,6 +182,7 @@ static void test_install_hid_driver_when_already_installed(void) static void test_uninstall_hid_driver_while_device_was_not_opened(void) { + ESP_LOGI(TAG, "Uninstall driver with no device opened"); // Install USB and HID driver with the stub test_hid_host_event_callback_stub test_hid_setup(test_hid_host_event_callback_stub, HID_TEST_EVENT_HANDLE_IN_DRIVER); // Tear down test @@ -183,6 +191,7 @@ static void test_uninstall_hid_driver_while_device_was_not_opened(void) static void test_uninstall_hid_driver_while_device_is_present(void) { + ESP_LOGI(TAG, "Uninstall driver while device is present"); // Install USB and HID driver with the stub test_hid_host_event_callback_stub test_hid_setup(test_hid_host_event_callback_open, HID_TEST_EVENT_HANDLE_IN_DRIVER); // Wait for USB device appearing for 250 msec @@ -194,6 +203,122 @@ static void test_uninstall_hid_driver_while_device_is_present(void) test_hid_teardown(); } +#ifdef HID_HOST_SUSPEND_RESUME_API_SUPPORTED + +static QueueHandle_t test_event_queue = NULL; +typedef struct { + hid_host_device_handle_t hid_device_handle; +} test_event_queue_t; + +static void test_hid_host_event_callback_open_suspended(hid_host_device_handle_t hid_device_handle, + const hid_host_driver_event_t event, + void *arg) +{ + if (event == HID_HOST_DRIVER_EVENT_CONNECTED) { + const test_event_queue_t evt_queue = { + .hid_device_handle = hid_device_handle, + }; + + TEST_ASSERT_EQUAL(pdTRUE, xQueueSend(test_event_queue, &evt_queue, 0)); + } +} + +/** + * @brief Open suspended device + * + * Purpose: + * - Test HID Host reaction to opening a device, which is in suspended state + * + * Procedure: + * - Install USB Host lib, Install HID driver, wait for device connection + * - Suspend the root port and fail to open a device + * - Resume the root port, teardown + */ +static void test_open_suspended_device(void) +{ + ESP_LOGI(TAG, "Open suspended device"); + test_event_queue = xQueueCreate(4, sizeof(test_event_queue_t)); + TEST_ASSERT_NOT_NULL(test_event_queue); + + test_hid_setup(test_hid_host_event_callback_open_suspended, HID_TEST_EVENT_HANDLE_IN_DRIVER); + + test_event_queue_t queue_item_1, queue_item_2; + TEST_ASSERT_EQUAL(pdTRUE, xQueueReceive(test_event_queue, &queue_item_1, pdMS_TO_TICKS(500))); + TEST_ASSERT_EQUAL(pdTRUE, xQueueReceive(test_event_queue, &queue_item_2, pdMS_TO_TICKS(500))); + + printf("Issue suspend\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + vTaskDelay(pdMS_TO_TICKS(100)); + + const hid_host_device_config_t dev_config = { + .callback = test_interface_callback_handler, + .callback_arg = NULL + }; + + // Try to open devices with the root port suspended + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_STATE, hid_host_device_open(queue_item_1.hid_device_handle, &dev_config)); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_STATE, hid_host_device_open(queue_item_2.hid_device_handle, &dev_config)); + + printf("Issue resume\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_resume()); + vTaskDelay(pdMS_TO_TICKS(100)); + + // Teardown + test_hid_teardown(); + vQueueDelete(test_event_queue); + test_event_queue = NULL; +} + +/** + * @brief Start suspended device + * + * Purpose: + * - Test HID Host reaction to starting an opened device, which is in suspended state + * + * Procedure: + * - Install USB Host lib, Install HID driver, wait for device connection, open the device + * - Suspend the root port and fail to start a device + * - Resume the root port, teardown + */ +static void test_start_suspended_device(void) +{ + ESP_LOGI(TAG, "Start suspended device"); + test_event_queue = xQueueCreate(4, sizeof(test_event_queue_t)); + TEST_ASSERT_NOT_NULL(test_event_queue); + + test_hid_setup(test_hid_host_event_callback_open_suspended, HID_TEST_EVENT_HANDLE_IN_DRIVER); + + test_event_queue_t queue_item_1, queue_item_2; + TEST_ASSERT_EQUAL(pdTRUE, xQueueReceive(test_event_queue, &queue_item_1, pdMS_TO_TICKS(500))); + TEST_ASSERT_EQUAL(pdTRUE, xQueueReceive(test_event_queue, &queue_item_2, pdMS_TO_TICKS(500))); + + const hid_host_device_config_t dev_config = { + .callback = test_hid_host_interface_event_close, + .callback_arg = NULL + }; + + // Open devices normally + TEST_ASSERT_EQUAL(ESP_OK, hid_host_device_open(queue_item_1.hid_device_handle, &dev_config)); + TEST_ASSERT_EQUAL(ESP_OK, hid_host_device_open(queue_item_2.hid_device_handle, &dev_config)); + + printf("Issue suspend\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_suspend()); + vTaskDelay(pdMS_TO_TICKS(100)); + + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_STATE, hid_host_device_start(queue_item_1.hid_device_handle)); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_STATE, hid_host_device_start(queue_item_2.hid_device_handle)); + + printf("Issue resume\n"); + TEST_ASSERT_EQUAL(ESP_OK, usb_host_lib_root_port_resume()); + vTaskDelay(pdMS_TO_TICKS(100)); + + // Teardown + test_hid_teardown(); + vQueueDelete(test_event_queue); + test_event_queue = NULL; +} +#endif // HID_HOST_SUSPEND_RESUME_API_SUPPORTED + // ----------------------- Public -------------------------- /** @@ -210,4 +335,8 @@ TEST_CASE("error_handling", "[hid_host]") test_install_hid_driver_when_already_installed(); test_uninstall_hid_driver_while_device_was_not_opened(); test_uninstall_hid_driver_while_device_is_present(); +#ifdef HID_HOST_SUSPEND_RESUME_API_SUPPORTED + test_open_suspended_device(); + test_start_suspended_device(); +#endif // HID_HOST_SUSPEND_RESUME_API_SUPPORTED }