diff --git a/examples/peripherals/usb/host/cdc/common/cdc_acm_host/CMakeLists.txt b/examples/peripherals/usb/host/cdc/common/cdc_acm_host/CMakeLists.txt new file mode 100644 index 0000000..39bec48 --- /dev/null +++ b/examples/peripherals/usb/host/cdc/common/cdc_acm_host/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "cdc_acm_host.c" + INCLUDE_DIRS "include" + REQUIRES usb) diff --git a/examples/peripherals/usb/host/cdc/common/cdc_acm_host/README.md b/examples/peripherals/usb/host/cdc/common/cdc_acm_host/README.md new file mode 100644 index 0000000..aeb28b1 --- /dev/null +++ b/examples/peripherals/usb/host/cdc/common/cdc_acm_host/README.md @@ -0,0 +1,46 @@ +# USB Host CDC-ACM Class Driver + +This directory contains an implementation of a USB CDC-ACM Host Class Driver that is implemented on top of the [USB Host Library](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s2/api-reference/peripherals/usb_host.html). + +## Supported Devices + +The CDC-ACM Host driver supports the following types of CDC devices: + +1. CDC-ACM devices +2. CDC-like vendor specific devices (usually found on USB to UART bridge devices) + +### CDC-ACM Devices + +The CDC-ACM Class driver supports CDC-ACM devices that meet the following requirements: +- The device class code must be set to the CDC class `0x02` or implement Interface Association Descriptor (IAD) +- The CDC-ACM must contain the following interfaces: + - A Communication Class Interface containing a management element (EP0) and may also contain a notification element (an interrupt endpoint). The driver will check this interface for CDC Functional Descriptors. + - A Data Class Interface with two BULK endpoints (IN and OUT). Other transfer types are not supported by the driver + +### CDC-Like Vendor Specific Devices + +The CDC-ACM Class driver supports CDC-like devices that meet the following requirements: +- The device class code must be set to the vendor specific class code `0xFF` +- The device needs to provide and interface containing the following endpoints: + - (Mandatory) Two Bulk endpoints (IN and OUT) for data + - (Optional) An interrupt endpoint (IN) for the notification element + +For CDC-like devices, users are responsible for ensuring that they only call APIs (e.g., `cdc_acm_host_send_break()`) that are supported by the target device. + + +## Usage + +The following steps outline the typical API call pattern of the CDC-ACM Class Driver + +1. Install the USB Host Library via `usb_host_install()` +2. Install the CDC-ACM driver via `cdc_acm_host_install()` +3. Call `cdc_acm_host_open()`/`cdc_acm_host_open_vendor_specific()` to open a target CDC-ACM/CDC-like device. These functions will block until the target device is connected +4. To transmit data, call `cdc_acm_host_data_tx_blocking()` +5. When data is received, the driver will automatically run the receive data callback +6. An opened device can be closed via `cdc_acm_host_close()` +7. The CDC-ACM driver can be uninstalled via `cdc_acm_host_uninstall()` + +## Examples + +- For an example with a CDC-ACM device, refer to [cdc_acm_host](../../cdc_acm_host) +- For an example with a CDC-like device, refer to [cdc_acm_host_bg96](../../cdc_acm_bg96) diff --git a/examples/peripherals/usb/host/cdc/common/cdc_acm_host/cdc_acm_host.c b/examples/peripherals/usb/host/cdc/common/cdc_acm_host/cdc_acm_host.c new file mode 100644 index 0000000..fb9eb26 --- /dev/null +++ b/examples/peripherals/usb/host/cdc/common/cdc_acm_host/cdc_acm_host.c @@ -0,0 +1,1170 @@ +/* + * SPDX-FileCopyrightText: 2015-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "esp_log.h" +#include +#include +#include +#include "usb/usb_host.h" +#include "usb/cdc_acm_host.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "freertos/event_groups.h" +#include "esp_check.h" +#include "esp_system.h" + +#define TAG "cdc_acm" + +// CDC devices often implement Interface Association Descriptor (IAD). Parse IAD only when +// bDeviceClass = 0xEF (Miscellaneous Device Class), bDeviceSubClass = 0x02 (Common Class), bDeviceProtocol = 0x01 (Interface Association Descriptor) +// @see USB Interface Association Descriptor: Device Class Code and Use Model rev 1.0, Table 1-1 +#define USB_SUBCLASS_COMMON 0x02 +#define USB_DEVICE_PROTOCOL_IAD 0x01 + +// CDC-ACM spinlock +static portMUX_TYPE cdc_acm_lock = portMUX_INITIALIZER_UNLOCKED; +#define CDC_ACM_ENTER_CRITICAL() portENTER_CRITICAL(&cdc_acm_lock) +#define CDC_ACM_EXIT_CRITICAL() portEXIT_CRITICAL(&cdc_acm_lock) + +// CDC-ACM events +#define CDC_ACM_TEARDOWN BIT0 +#define CDC_ACM_TEARDOWN_COMPLETE BIT1 + +// CDC-ACM check macros +#define CDC_ACM_CHECK(cond, ret_val) ({ \ + if (!(cond)) { \ + return (ret_val); \ + } \ +}) + +#define CDC_ACM_CHECK_FROM_CRIT(cond, ret_val) ({ \ + if (!(cond)) { \ + CDC_ACM_EXIT_CRITICAL(); \ + return ret_val; \ + } \ +}) + +// CDC-ACM driver object +typedef struct { + usb_host_client_handle_t cdc_acm_client_hdl; /*!< USB Host handle reused for all CDC-ACM devices in the system */ + SemaphoreHandle_t open_close_mutex; + EventGroupHandle_t event_group; + SLIST_HEAD(list_dev, cdc_dev_s) cdc_devices_list; /*!< List of open pseudo devices */ +} cdc_acm_obj_t; + +static cdc_acm_obj_t *p_cdc_acm_obj = NULL; + +/** + * @brief Default CDC-ACM driver configuration + * + * This configuration is used when user passes NULL to config pointer during device open. + */ +static const cdc_acm_host_driver_config_t cdc_acm_driver_config_default = { + .driver_task_stack_size = 4096, + .driver_task_priority = 10, + .xCoreID = 0 +}; + +/** + * @brief USB CDC PSTN Call Descriptor + * + * @see Table 3, USB CDC-PSTN specification rev. 1.2 + */ +typedef struct { + uint8_t bFunctionLength; + const uint8_t bDescriptorType; + const cdc_desc_subtype_t bDescriptorSubtype; + union { + struct { + uint8_t call_management: 1; // Device handles call management itself + uint8_t call_over_data_if: 1; // Device sends/receives call management information over Data Class interface + uint8_t reserved: 6; + }; + uint8_t val; + } bmCapabilities; + uint8_t bDataInterface; // Interface number of Data Class interface optionally used for call management +} __attribute__((packed)) cdc_acm_call_desc_t; + +/** + * @brief USB CDC PSTN Abstract Control Model Descriptor + * + * @see Table 4, USB CDC-PSTN specification rev. 1.2 + */ +typedef struct { + uint8_t bFunctionLength; + const uint8_t bDescriptorType; + const cdc_desc_subtype_t bDescriptorSubtype; + union { + struct { + uint8_t feature: 1; // Device supports Set/Clear/Get_Comm_Feature requests + uint8_t serial: 1; // Device supports Set/Get_Line_Coding, Set_Control_Line_State and Serial_State request and notifications + uint8_t send_break: 1; // Device supports Send_Break request + uint8_t network: 1; // Device supports Network_Connection notification + uint8_t reserved: 4; + }; + uint8_t val; + } bmCapabilities; +} __attribute__((packed)) cdc_acm_acm_desc_t; + +typedef struct cdc_dev_s cdc_dev_t; +struct cdc_dev_s{ + usb_device_handle_t dev_hdl; // USB device handle + void *cb_arg; // Common argument for user's callbacks (data IN and Notification) + struct { + usb_transfer_t *out_xfer; // OUT data transfer + usb_transfer_t *in_xfer; // IN data transfer + cdc_acm_data_callback_t in_cb; // User's callback for async (non-blocking) data IN + const usb_intf_desc_t *intf_desc; // Pointer to data interface descriptor + SemaphoreHandle_t out_mux; // OUT mutex + } data; + + struct { + usb_transfer_t *xfer; // IN notification transfer + const usb_intf_desc_t *intf_desc; // Pointer to notification interface descriptor, can be NULL if there is no notification channel in the device + cdc_acm_host_dev_callback_t cb; // User's callback for device events + } notif; // Structure with Notif pipe data + + usb_transfer_t *ctrl_transfer; // CTRL (endpoint 0) transfer + SemaphoreHandle_t ctrl_mux; // CTRL mutex + cdc_acm_uart_state_t serial_state; // Serial State + cdc_comm_protocol_t comm_protocol; + cdc_data_protocol_t data_protocol; + int num_cdc_intf_desc; // Number of CDC Interface descriptors in following array + const usb_standard_desc_t **cdc_intf_desc; // CDC Interface descriptors + SLIST_ENTRY(cdc_dev_s) list_entry; +}; + +/** + * @brief Notification received callback + * + * Notification (interrupt) IN transfer is submitted at the end of this function to ensure periodic poll of IN endpoint. + * + * @param[in] transfer Transfer that triggered the callback + */ +static void notif_xfer_cb(usb_transfer_t *transfer); + +/** + * @brief Data received callback + * + * Data (bulk) IN transfer is submitted at the end of this function to ensure continuous poll of IN endpoint. + * + * @param[in] transfer Transfer that triggered the callback + */ +static void in_xfer_cb(usb_transfer_t *transfer); + +/** + * @brief Data send callback + * + * Reused for bulk OUT and CTRL transfers + * + * @param[in] transfer Transfer that triggered the callback + */ +static void out_xfer_cb(usb_transfer_t *transfer); + +/** + * @brief USB Host Client event callback + * + * Handling of USB device connection/disconnection to/from root HUB. + * + * @param[in] event_msg Event message type + * @param[in] arg Caller's argument (not used in this driver) + */ +static void usb_event_cb(const usb_host_client_event_msg_t *event_msg, void *arg); + +/** + * @brief Send CDC specific request + * + * Helper function that will send CDC specific request to default endpoint. + * Both IN and OUT requests are sent through this API, depending on the in_transfer parameter. + * + * @see Chapter 6.2, USB CDC specification rev. 1.2 + * @note CDC specific requests are only supported by devices that have dedicated management element. + * + * @param[in] cdc_dev Pointer to CDC device + * @param[in] in_transfer Direction of data phase. true: IN, false: OUT + * @param[in] request CDC request code + * @param[inout] data Pointer to data buffer. Input for OUT transfers, output for IN transfers. + * @param[in] data_len Length of data buffer + * @param[in] value Value to be set in bValue of Setup packet + * @return esp_err_t + */ +static esp_err_t send_cdc_request(cdc_dev_t *cdc_dev, bool in_transfer, cdc_request_code_t request, uint8_t *data, uint16_t data_len, uint16_t value); + +/** + * @brief CDC-ACM driver handling task + * + * USB host client registration and deregistration is handled here. + * + * @param[in] arg User's argument. Handle of a task that started this task. + */ +static void cdc_acm_client_task(void *arg) +{ + vTaskSuspend(NULL); // Task will be resumed from cdc_acm_host_install() + cdc_acm_obj_t *cdc_acm_obj = p_cdc_acm_obj; // Make local copy of the driver's handle + assert(cdc_acm_obj->cdc_acm_client_hdl); + + // Start handling client's events + while (1) { + usb_host_client_handle_events(cdc_acm_obj->cdc_acm_client_hdl, portMAX_DELAY); + EventBits_t events = xEventGroupGetBits(cdc_acm_obj->event_group); + if (events & CDC_ACM_TEARDOWN) { + break; + } + } + + ESP_LOGD(TAG, "Deregistering client"); + ESP_ERROR_CHECK(usb_host_client_deregister(cdc_acm_obj->cdc_acm_client_hdl)); + xEventGroupSetBits(cdc_acm_obj->event_group, CDC_ACM_TEARDOWN_COMPLETE); + vTaskDelete(NULL); +} + +/** + * @brief Cancel transfer and reset endpoint + * + * This function will cancel ongoing transfer a reset its endpoint to ready state. + * + * @param[in] dev_hdl USB device handle + * @param[in] transfer Transfer to be cancelled + * @return esp_err_t + */ +static esp_err_t cdc_acm_reset_transfer_endpoint(usb_device_handle_t dev_hdl, usb_transfer_t *transfer) +{ + assert(dev_hdl); + assert(transfer); + + ESP_RETURN_ON_ERROR(usb_host_endpoint_halt(dev_hdl, transfer->bEndpointAddress), TAG,); + ESP_RETURN_ON_ERROR(usb_host_endpoint_flush(dev_hdl, transfer->bEndpointAddress), TAG,); + usb_host_endpoint_clear(dev_hdl, transfer->bEndpointAddress); + return ESP_OK; +} + +/** + * @brief Start CDC device + * + * After this call, USB host peripheral will continuously poll IN endpoints. + * + * @param cdc_dev + * @param[in] event_cb Device event callback + * @param[in] in_cb Data received callback + * @param[in] user_arg Optional user's argument, that will be passed to the callbacks + * @return esp_err_t + */ +static esp_err_t cdc_acm_start(cdc_dev_t *cdc_dev, cdc_acm_host_dev_callback_t event_cb, cdc_acm_data_callback_t in_cb, void *user_arg) +{ + esp_err_t ret = ESP_OK; + assert(cdc_dev); + + CDC_ACM_ENTER_CRITICAL(); + cdc_dev->notif.cb = event_cb; + cdc_dev->data.in_cb = in_cb; + cdc_dev->cb_arg = user_arg; + CDC_ACM_EXIT_CRITICAL(); + + // Claim data interface and start polling its IN endpoint + ESP_GOTO_ON_ERROR(usb_host_interface_claim(p_cdc_acm_obj->cdc_acm_client_hdl, cdc_dev->dev_hdl, cdc_dev->data.intf_desc->bInterfaceNumber, 0), err, TAG,); + ESP_LOGD("CDC_ACM", "Submitting poll for BULK IN transfer"); + ESP_ERROR_CHECK(usb_host_transfer_submit(cdc_dev->data.in_xfer)); + + // If notification are supported, claim its interface and start polling its IN endpoint + if (cdc_dev->notif.intf_desc != NULL) { + if (cdc_dev->notif.intf_desc != cdc_dev->data.intf_desc) { + ESP_GOTO_ON_ERROR(usb_host_interface_claim(p_cdc_acm_obj->cdc_acm_client_hdl, cdc_dev->dev_hdl, + cdc_dev->notif.intf_desc->bInterfaceNumber, 0), err, TAG,); + } + ESP_LOGD("CDC_ACM", "Submitting poll for INTR IN transfer"); + ESP_ERROR_CHECK(usb_host_transfer_submit(cdc_dev->notif.xfer)); + } + + // Everything OK, add the device into list and return + CDC_ACM_ENTER_CRITICAL(); + SLIST_INSERT_HEAD(&p_cdc_acm_obj->cdc_devices_list, cdc_dev, list_entry); + CDC_ACM_EXIT_CRITICAL(); + return ret; + +err: + usb_host_interface_release(p_cdc_acm_obj->cdc_acm_client_hdl, cdc_dev->dev_hdl, cdc_dev->data.intf_desc->bInterfaceNumber); + if (cdc_dev->notif.intf_desc != NULL) { + usb_host_interface_release(p_cdc_acm_obj->cdc_acm_client_hdl, cdc_dev->dev_hdl, cdc_dev->notif.intf_desc->bInterfaceNumber); + } + return ret; +} + +static void cdc_acm_transfers_free(cdc_dev_t *cdc_dev); +/** + * @brief Helper function that releases resources claimed by CDC device + * + * Close underlying USB device, free device driver memory + * + * @note All interfaces claimed by this device must be release before calling this function + * @param cdc_dev CDC device handle to be removed + */ +static void cdc_acm_device_remove(cdc_dev_t *cdc_dev) +{ + assert(cdc_dev); + cdc_acm_transfers_free(cdc_dev); + free(cdc_dev->cdc_intf_desc); + // We don't check the error code of usb_host_device_close, as the close might fail, if someone else is still using the device (not all interfaces are released) + usb_host_device_close(p_cdc_acm_obj->cdc_acm_client_hdl, cdc_dev->dev_hdl); // Gracefully continue on error + free(cdc_dev); +} + +/** + * @brief Open USB device with requested VID/PID + * + * This function has two regular return paths: + * 1. USB device with matching VID/PID is already opened by this driver: allocate new CDC device on top of the already opened USB device. + * 2. USB device with matching VID/PID is NOT opened by this driver yet: poll USB connected devices until it is found. + * + * @note This function will block for timeout_ms, if the device is not enumerated at the moment of calling this function. + * @param[in] vid Vendor ID + * @param[in] pid Product ID + * @param[in] timeout_ms Connection timeout [ms] + * @param[out] dev CDC-ACM device + * @return esp_err_t + */ +static esp_err_t cdc_acm_find_and_open_usb_device(uint16_t vid, uint16_t pid, int timeout_ms, cdc_dev_t **dev) +{ + assert(p_cdc_acm_obj); + assert(dev); + + *dev = calloc(1, sizeof(cdc_dev_t)); + if (*dev == NULL) { + return ESP_ERR_NO_MEM; + } + + // First, check list of already opened CDC devices + ESP_LOGD(TAG, "Checking list of opened USB devices"); + cdc_dev_t *cdc_dev; + SLIST_FOREACH(cdc_dev, &p_cdc_acm_obj->cdc_devices_list, list_entry) { + const usb_device_desc_t *device_desc; + ESP_ERROR_CHECK(usb_host_get_device_descriptor(cdc_dev->dev_hdl, &device_desc)); + if (device_desc->idVendor == vid && device_desc->idProduct == pid) { + // Return path 1: + (*dev)->dev_hdl = cdc_dev->dev_hdl; + return ESP_OK; + } + } + + // Second, poll connected devices until new device is connected or timeout + TickType_t timeout_ticks = (timeout_ms == 0) ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms); + TimeOut_t connection_timeout; + vTaskSetTimeOutState(&connection_timeout); + + while (true) { + ESP_LOGD(TAG, "Checking list of connected USB devices"); + uint8_t dev_addr_list[10]; + int num_of_devices; + ESP_ERROR_CHECK(usb_host_device_addr_list_fill(sizeof(dev_addr_list), dev_addr_list, &num_of_devices)); + + // Go through device address list and find the one we are looking for + for (int i = 0; i < num_of_devices; i++) { + usb_device_handle_t current_device; + // Open USB device + if (usb_host_device_open(p_cdc_acm_obj->cdc_acm_client_hdl, dev_addr_list[i], ¤t_device) != ESP_OK) { + continue; // In case we failed to open this device, continue with next one in the list + } + assert(current_device); + const usb_device_desc_t *device_desc; + ESP_ERROR_CHECK(usb_host_get_device_descriptor(current_device, &device_desc)); + if (device_desc->idVendor == vid && device_desc->idProduct == pid) { + // Return path 2: + (*dev)->dev_hdl = current_device; + return ESP_OK; + } + usb_host_device_close(p_cdc_acm_obj->cdc_acm_client_hdl, current_device); + } + + if (xTaskCheckForTimeOut(&connection_timeout, &timeout_ticks) != pdFALSE) { + break; // Timeout elapsed and the device is not connected + } + vTaskDelay(pdMS_TO_TICKS(50)); + } + + // Timeout was reached, clean-up + free(*dev); + *dev = NULL; + return ESP_ERR_NOT_FOUND; +} + +esp_err_t cdc_acm_host_install(const cdc_acm_host_driver_config_t *driver_config) +{ + CDC_ACM_CHECK(!p_cdc_acm_obj, ESP_ERR_INVALID_STATE); + + // Check driver configuration, use default if NULL is passed + if (driver_config == NULL) { + driver_config = &cdc_acm_driver_config_default; + } + + // Allocate all we need for this driver + esp_err_t ret; + cdc_acm_obj_t *cdc_acm_obj = heap_caps_calloc(1, sizeof(cdc_acm_obj_t), MALLOC_CAP_DEFAULT); + EventGroupHandle_t event_group = xEventGroupCreate(); + SemaphoreHandle_t mutex = xSemaphoreCreateMutex(); + TaskHandle_t driver_task_h = NULL; + xTaskCreatePinnedToCore( + cdc_acm_client_task, "USB-CDC", driver_config->driver_task_stack_size, NULL, + driver_config->driver_task_priority, &driver_task_h, driver_config->xCoreID); + + if (cdc_acm_obj == NULL || driver_task_h == NULL || event_group == NULL || mutex == NULL) { + ret = ESP_ERR_NO_MEM; + goto err; + } + + // Register USB Host client + usb_host_client_handle_t usb_client = NULL; + const usb_host_client_config_t client_config = { + .is_synchronous = false, + .max_num_event_msg = 3, + .async.client_event_callback = usb_event_cb, + .async.callback_arg = NULL + }; + ESP_GOTO_ON_ERROR(usb_host_client_register(&client_config, &usb_client), err, TAG, "Failed to register USB host client"); + + // Initialize CDC-ACM driver structure + SLIST_INIT(&(cdc_acm_obj->cdc_devices_list)); + cdc_acm_obj->event_group = event_group; + cdc_acm_obj->open_close_mutex = mutex; + cdc_acm_obj->cdc_acm_client_hdl = usb_client; + + // Between 1st call of this function and following section, another task might try to install this driver: + // Make sure that there is only one instance of this driver in the system + CDC_ACM_ENTER_CRITICAL(); + if (p_cdc_acm_obj) { + // Already created + ret = ESP_ERR_INVALID_STATE; + CDC_ACM_EXIT_CRITICAL(); + goto client_err; + } else { + p_cdc_acm_obj = cdc_acm_obj; + } + CDC_ACM_EXIT_CRITICAL(); + + // Everything OK: Start CDC-Driver task and return + vTaskResume(driver_task_h); + return ESP_OK; + +client_err: + usb_host_client_deregister(usb_client); +err: // Clean-up + free(cdc_acm_obj); + if (event_group) { + vEventGroupDelete(event_group); + } + if (driver_task_h) { + vTaskDelete(driver_task_h); + } + if (mutex) { + vSemaphoreDelete(mutex); + } + return ret; +} + +esp_err_t cdc_acm_host_uninstall() +{ + esp_err_t ret; + + CDC_ACM_ENTER_CRITICAL(); + CDC_ACM_CHECK_FROM_CRIT(p_cdc_acm_obj, ESP_ERR_INVALID_STATE); + cdc_acm_obj_t *cdc_acm_obj = p_cdc_acm_obj; // Save Driver's handle to temporary handle + CDC_ACM_EXIT_CRITICAL(); + + xSemaphoreTake(p_cdc_acm_obj->open_close_mutex, portMAX_DELAY); // Wait for all open/close calls to finish + + CDC_ACM_ENTER_CRITICAL(); + if (SLIST_EMPTY(&p_cdc_acm_obj->cdc_devices_list)) { // Check that device list is empty (all devices closed) + p_cdc_acm_obj = NULL; // NULL static driver pointer: No open/close calls form this point + } else { + ret = ESP_ERR_INVALID_STATE; + CDC_ACM_EXIT_CRITICAL(); + goto unblock; + } + CDC_ACM_EXIT_CRITICAL(); + + // Signal to CDC task to stop, unblock it and wait for its deletion + xEventGroupSetBits(cdc_acm_obj->event_group, CDC_ACM_TEARDOWN); + usb_host_client_unblock(cdc_acm_obj->cdc_acm_client_hdl); + ESP_GOTO_ON_FALSE( + xEventGroupWaitBits(cdc_acm_obj->event_group, CDC_ACM_TEARDOWN_COMPLETE, pdFALSE, pdFALSE, pdMS_TO_TICKS(100)), + ESP_ERR_NOT_FINISHED, unblock, TAG,); + + // Free remaining resources and return + vEventGroupDelete(cdc_acm_obj->event_group); + xSemaphoreGive(cdc_acm_obj->open_close_mutex); + vSemaphoreDelete(cdc_acm_obj->open_close_mutex); + free(cdc_acm_obj); + return ESP_OK; + +unblock: + xSemaphoreGive(cdc_acm_obj->open_close_mutex); + return ret; +} + +/** + * @brief Free USB transfers used by this device + * + * @note There can be no transfers in flight, at the moment of calling this function. + * @param[in] cdc_dev Pointer to CDC device + */ +static void cdc_acm_transfers_free(cdc_dev_t *cdc_dev) +{ + assert(cdc_dev); + usb_host_transfer_free(cdc_dev->notif.xfer); + usb_host_transfer_free(cdc_dev->data.in_xfer); + if (cdc_dev->data.out_xfer != NULL) { + if (cdc_dev->data.out_xfer->context != NULL) { + vSemaphoreDelete((SemaphoreHandle_t)cdc_dev->data.out_xfer->context); + } + if (cdc_dev->data.out_mux != NULL) { + vSemaphoreDelete(cdc_dev->data.out_mux); + } + usb_host_transfer_free(cdc_dev->data.out_xfer); + } + if (cdc_dev->ctrl_transfer != NULL) { + if (cdc_dev->ctrl_transfer->context != NULL) { + vSemaphoreDelete((SemaphoreHandle_t)cdc_dev->ctrl_transfer->context); + } + if (cdc_dev->ctrl_mux != NULL) { + vSemaphoreDelete(cdc_dev->ctrl_mux); + } + usb_host_transfer_free(cdc_dev->ctrl_transfer); + } +} + +/** + * @brief Allocate CDC transfers + * + * @param[in] cdc_dev Pointer to CDC device + * @param[in] notif_ep_desc Pointer to notification EP descriptor + * @param[in] in_ep_desc- Pointer to data IN EP descriptor + * @param[in] out_ep_desc Pointer to data OUT EP descriptor + * @param[in] out_buf_len Length of data OUT buffer + * @return esp_err_t + */ +static esp_err_t cdc_acm_transfers_allocate(cdc_dev_t *cdc_dev, const usb_ep_desc_t *notif_ep_desc, const usb_ep_desc_t *in_ep_desc, const usb_ep_desc_t *out_ep_desc, size_t out_buf_len) +{ + esp_err_t ret; + + // 1. Setup notification and control transfers if they are supported + if (notif_ep_desc) { + ESP_GOTO_ON_ERROR( + usb_host_transfer_alloc(USB_EP_DESC_GET_MPS(notif_ep_desc), 0, &cdc_dev->notif.xfer), + err, TAG,); + cdc_dev->notif.xfer->device_handle = cdc_dev->dev_hdl; + cdc_dev->notif.xfer->bEndpointAddress = notif_ep_desc->bEndpointAddress; + cdc_dev->notif.xfer->callback = notif_xfer_cb; + cdc_dev->notif.xfer->context = cdc_dev; + cdc_dev->notif.xfer->num_bytes = USB_EP_DESC_GET_MPS(notif_ep_desc); + + usb_device_info_t dev_info; + ESP_ERROR_CHECK(usb_host_device_info(cdc_dev->dev_hdl, &dev_info)); + ESP_GOTO_ON_ERROR( + usb_host_transfer_alloc(dev_info.bMaxPacketSize0, 0, &cdc_dev->ctrl_transfer), + err, TAG,); + cdc_dev->ctrl_transfer->timeout_ms = 1000; + cdc_dev->ctrl_transfer->bEndpointAddress = 0; + cdc_dev->ctrl_transfer->device_handle = cdc_dev->dev_hdl; + cdc_dev->ctrl_transfer->context = cdc_dev; + cdc_dev->ctrl_transfer->callback = out_xfer_cb; + cdc_dev->ctrl_transfer->context = xSemaphoreCreateBinary(); + ESP_GOTO_ON_FALSE(cdc_dev->ctrl_transfer->context, ESP_ERR_NO_MEM, err, TAG,); + cdc_dev->ctrl_mux = xSemaphoreCreateMutex(); + ESP_GOTO_ON_FALSE(cdc_dev->ctrl_mux, ESP_ERR_NO_MEM, err, TAG,); + } + + // 2. Setup IN data transfer + ESP_GOTO_ON_ERROR( + usb_host_transfer_alloc(USB_EP_DESC_GET_MPS(in_ep_desc), 0, &cdc_dev->data.in_xfer), + err, TAG, + ); + assert(cdc_dev->data.in_xfer); + cdc_dev->data.in_xfer->callback = in_xfer_cb; + cdc_dev->data.in_xfer->num_bytes = USB_EP_DESC_GET_MPS(in_ep_desc); + cdc_dev->data.in_xfer->bEndpointAddress = in_ep_desc->bEndpointAddress; + cdc_dev->data.in_xfer->device_handle = cdc_dev->dev_hdl; + cdc_dev->data.in_xfer->context = cdc_dev; + + // 3. Setup OUT bulk transfer (if it is required (out_buf_len > 0)) + if (out_buf_len != 0) { + ESP_GOTO_ON_ERROR( + usb_host_transfer_alloc(out_buf_len, 0, &cdc_dev->data.out_xfer), + err, TAG, + ); + assert(cdc_dev->data.out_xfer); + cdc_dev->data.out_xfer->device_handle = cdc_dev->dev_hdl; + cdc_dev->data.out_xfer->context = xSemaphoreCreateBinary(); + ESP_GOTO_ON_FALSE(cdc_dev->data.out_xfer->context, ESP_ERR_NO_MEM, err, TAG,); + cdc_dev->data.out_mux = xSemaphoreCreateMutex(); + ESP_GOTO_ON_FALSE(cdc_dev->data.out_mux, ESP_ERR_NO_MEM, err, TAG,); + cdc_dev->data.out_xfer->bEndpointAddress = out_ep_desc->bEndpointAddress; + cdc_dev->data.out_xfer->callback = out_xfer_cb; + } + return ESP_OK; + +err: + cdc_acm_transfers_free(cdc_dev); + return ret; +} + +/** + * @brief Find CDC interface descriptor and its endpoint descriptors + * + * @note This function is called in open procedure of CDC compliant devices only. + * @param[in] cdc_dev Pointer to CDC device + * @param[in] intf_idx Index of CDC interface that should be used for this device + * @param[out] notif_ep Pointer to notification EP descriptor + * @param[out] in_ep Pointer to data IN EP descriptor + * @param[out] out_ep Pointer to data OUT EP descriptor + * @return esp_err_t + */ +static esp_err_t cdc_acm_find_intf_and_ep_desc(cdc_dev_t *cdc_dev, uint8_t intf_idx, const usb_ep_desc_t **notif_ep, const usb_ep_desc_t **in_ep, const usb_ep_desc_t **out_ep) +{ + bool interface_found = false; + const usb_config_desc_t *config_desc; + const usb_device_desc_t *device_desc; + int data_intf_idx, notif_intf_idx; + int desc_offset = 0; + + // Get required descriptors + ESP_ERROR_CHECK(usb_host_get_device_descriptor(cdc_dev->dev_hdl, &device_desc)); + ESP_ERROR_CHECK(usb_host_get_active_config_descriptor(cdc_dev->dev_hdl, &config_desc)); + + if ((device_desc->bDeviceClass == USB_CLASS_MISC) && (device_desc->bDeviceSubClass == USB_SUBCLASS_COMMON) && + (device_desc->bDeviceProtocol == USB_DEVICE_PROTOCOL_IAD)) { + // This is a composite device, that uses Interface Association Descriptor + const usb_standard_desc_t *this_desc = (const usb_standard_desc_t *)config_desc; + do { + this_desc = usb_parse_next_descriptor_of_type( + this_desc, config_desc->wTotalLength, USB_B_DESCRIPTOR_TYPE_INTERFACE_ASSOCIATION, &desc_offset); + + if (this_desc == NULL) + break; // Reached end of configuration descriptor + + const usb_iad_desc_t *iad_desc = (const usb_iad_desc_t *)this_desc; + if (iad_desc->bFirstInterface == intf_idx) { + // IAD with correct interface number was found: Check Class/Subclass codes, save Interface indexes + assert(iad_desc->bInterfaceCount == 2); + assert(iad_desc->bFunctionClass == USB_CLASS_COMM); + assert(iad_desc->bFunctionSubClass == CDC_SUBCLASS_ACM); + notif_intf_idx = iad_desc->bFirstInterface; + data_intf_idx = iad_desc->bFirstInterface + 1; + interface_found = true; + } + } while (!interface_found); + } else if ((device_desc->bDeviceClass == USB_CLASS_COMM) && (intf_idx == 0)) { + // This is a Communication Device Class + notif_intf_idx = 0; + data_intf_idx = 1; + interface_found = true; + } + + // Save found interfaces descriptors: + if (interface_found) { + // Notification IF and EP + cdc_dev->notif.intf_desc = usb_parse_interface_descriptor(config_desc, notif_intf_idx, 0, &desc_offset); + assert(cdc_dev->notif.intf_desc); + + // CDC specific descriptors should be right after CDC-Communication interface descriptor + // Note: That's why we use usb_parse_next_descriptor instead of usb_parse_next_descriptor_of_type. + // The latter could return CDC specific descriptors that don't belong to this interface + const usb_standard_desc_t *cdc_desc = (usb_standard_desc_t *)cdc_dev->notif.intf_desc; + do { + cdc_desc = usb_parse_next_descriptor(cdc_desc, config_desc->wTotalLength, &desc_offset); + if ((cdc_desc == NULL) || (cdc_desc->bDescriptorType != ((USB_CLASS_COMM << 4) | USB_W_VALUE_DT_INTERFACE))) + break; // We found all CDC specific descriptors + cdc_dev->num_cdc_intf_desc++; + cdc_dev->cdc_intf_desc = + realloc(cdc_dev->cdc_intf_desc, cdc_dev->num_cdc_intf_desc * (sizeof(usb_standard_desc_t *))); + assert(cdc_dev->cdc_intf_desc); + cdc_dev->cdc_intf_desc[cdc_dev->num_cdc_intf_desc - 1] = cdc_desc; + } while (1); + *notif_ep = usb_parse_endpoint_descriptor_by_index(cdc_dev->notif.intf_desc, 0, config_desc->wTotalLength, &desc_offset); + assert(notif_ep); + + // Data IF and EP + cdc_dev->data.intf_desc = usb_parse_interface_descriptor(config_desc, data_intf_idx, 0, &desc_offset); + assert(cdc_dev->data.intf_desc); + int temp_offset = desc_offset; + for (int i = 0; i < 2; i++) { + const usb_ep_desc_t *this_ep = usb_parse_endpoint_descriptor_by_index(cdc_dev->data.intf_desc, i, config_desc->wTotalLength, &desc_offset); + assert(this_ep); + if (USB_EP_DESC_GET_EP_DIR(this_ep)) { + *in_ep = this_ep; + } else { + *out_ep = this_ep; + } + desc_offset = temp_offset; + } + return ESP_OK; + } + return ESP_ERR_NOT_FOUND; +} + +esp_err_t cdc_acm_host_open(uint16_t vid, uint16_t pid, uint8_t interface_idx, const cdc_acm_host_device_config_t *dev_config, cdc_acm_dev_hdl_t *cdc_hdl_ret) +{ + esp_err_t ret; + CDC_ACM_CHECK(p_cdc_acm_obj, ESP_ERR_INVALID_STATE); + CDC_ACM_CHECK(dev_config, ESP_ERR_INVALID_ARG); + CDC_ACM_CHECK(cdc_hdl_ret, ESP_ERR_INVALID_ARG); + + xSemaphoreTake(p_cdc_acm_obj->open_close_mutex, portMAX_DELAY); + // Find underlying USB device + cdc_dev_t *cdc_dev; + ESP_GOTO_ON_ERROR( + cdc_acm_find_and_open_usb_device(vid, pid, dev_config->connection_timeout_ms, &cdc_dev), + exit, TAG, "USB device with VID: 0x%04X, PID: 0x%04X not found", vid, pid); + + // Find and save relevant interface and endpoint descriptors + const usb_ep_desc_t *notif_ep = NULL; + const usb_ep_desc_t *in_ep = NULL; + const usb_ep_desc_t *out_ep = NULL; + ESP_GOTO_ON_ERROR( + cdc_acm_find_intf_and_ep_desc(cdc_dev, interface_idx, ¬if_ep, &in_ep, &out_ep), + err, TAG, "Could not find required interface"); + + // Check whether found Interfaces are really CDC-ACM + assert(cdc_dev->notif.intf_desc->bInterfaceClass == USB_CLASS_COMM); + assert(cdc_dev->notif.intf_desc->bInterfaceSubClass == CDC_SUBCLASS_ACM); + assert(cdc_dev->notif.intf_desc->bNumEndpoints == 1); + assert(cdc_dev->data.intf_desc->bInterfaceClass == USB_CLASS_CDC_DATA); + assert(cdc_dev->data.intf_desc->bNumEndpoints == 2); + + // Save Communication and Data protocols + cdc_dev->comm_protocol = (cdc_comm_protocol_t)cdc_dev->notif.intf_desc->bInterfaceProtocol; + cdc_dev->data_protocol = (cdc_data_protocol_t)cdc_dev->data.intf_desc->bInterfaceProtocol; + + // Allocate USB transfers, claim CDC interfaces and return CDC-ACM handle + ESP_GOTO_ON_ERROR(cdc_acm_transfers_allocate(cdc_dev, notif_ep, in_ep, out_ep, dev_config->out_buffer_size), err, TAG,); + ESP_GOTO_ON_ERROR(cdc_acm_start(cdc_dev, dev_config->event_cb, dev_config->data_cb, dev_config->user_arg), err, TAG,); + *cdc_hdl_ret = (cdc_acm_dev_hdl_t)cdc_dev; + xSemaphoreGive(p_cdc_acm_obj->open_close_mutex); + return ESP_OK; + +err: + cdc_acm_device_remove(cdc_dev); +exit: + xSemaphoreGive(p_cdc_acm_obj->open_close_mutex); + *cdc_hdl_ret = NULL; + return ret; +} + +esp_err_t cdc_acm_host_open_vendor_specific(uint16_t vid, uint16_t pid, uint8_t interface_num, const cdc_acm_host_device_config_t *dev_config, cdc_acm_dev_hdl_t *cdc_hdl_ret) +{ + esp_err_t ret; + CDC_ACM_CHECK(p_cdc_acm_obj, ESP_ERR_INVALID_STATE); + CDC_ACM_CHECK(dev_config, ESP_ERR_INVALID_ARG); + CDC_ACM_CHECK(cdc_hdl_ret, ESP_ERR_INVALID_ARG); + + xSemaphoreTake(p_cdc_acm_obj->open_close_mutex, portMAX_DELAY); + + // Find underlying USB device + cdc_dev_t *cdc_dev; + ESP_GOTO_ON_ERROR( + cdc_acm_find_and_open_usb_device(vid, pid, dev_config->connection_timeout_ms, &cdc_dev), + exit, TAG, "USB device with VID: 0x%04X, PID: 0x%04X not found", vid, pid); + + // Open procedure for CDC-ACM non-compliant devices: + const usb_config_desc_t *config_desc; + int desc_offset; + ESP_ERROR_CHECK(usb_host_get_active_config_descriptor(cdc_dev->dev_hdl, &config_desc)); + cdc_dev->data.intf_desc = usb_parse_interface_descriptor(config_desc, interface_num, 0, &desc_offset); + const int temp_offset = desc_offset; // Save this offset for later + assert(cdc_dev->data.intf_desc); + + // The interface can have 2-3 endpoints. 2 for data and 1 optional for notifications + const usb_ep_desc_t *in_ep = NULL; + const usb_ep_desc_t *out_ep = NULL; + const usb_ep_desc_t *notif_ep = NULL; + int ep_idx = 0; + if (cdc_dev->data.intf_desc->bNumEndpoints == 3) { + // Notification channel does not have its dedicated interface (data and notif interface is the same) + // First endpoint of this interface is used as notification channel + cdc_dev->notif.intf_desc = cdc_dev->data.intf_desc; + notif_ep = usb_parse_endpoint_descriptor_by_index(cdc_dev->data.intf_desc, 0, config_desc->wTotalLength, &desc_offset); + desc_offset = temp_offset; + ep_idx++; + } + + for (int i = ep_idx; i < ep_idx + 2; i++) { + const usb_ep_desc_t *this_ep = usb_parse_endpoint_descriptor_by_index(cdc_dev->data.intf_desc, i, config_desc->wTotalLength, &desc_offset); + assert(this_ep); + if (USB_EP_DESC_GET_EP_DIR(this_ep)) { + in_ep = this_ep; + } else { + out_ep = this_ep; + } + desc_offset = temp_offset; + } + + // Allocate USB transfers, claim CDC interfaces and return CDC-ACM handle + ESP_GOTO_ON_ERROR(cdc_acm_transfers_allocate(cdc_dev, notif_ep, in_ep, out_ep, dev_config->out_buffer_size), err, TAG, ); + ESP_GOTO_ON_ERROR(cdc_acm_start(cdc_dev, dev_config->event_cb, dev_config->data_cb, dev_config->user_arg), err, TAG,); + *cdc_hdl_ret = (cdc_acm_dev_hdl_t)cdc_dev; + xSemaphoreGive(p_cdc_acm_obj->open_close_mutex); + return ESP_OK; +err: + cdc_acm_device_remove(cdc_dev); +exit: + xSemaphoreGive(p_cdc_acm_obj->open_close_mutex); + return ret; +} + +esp_err_t cdc_acm_host_close(cdc_acm_dev_hdl_t cdc_hdl) +{ + CDC_ACM_CHECK(p_cdc_acm_obj, ESP_ERR_INVALID_STATE); + CDC_ACM_CHECK(cdc_hdl, ESP_ERR_INVALID_ARG); + + xSemaphoreTake(p_cdc_acm_obj->open_close_mutex, portMAX_DELAY); + + cdc_dev_t *cdc_dev = (cdc_dev_t *)cdc_hdl; + + // Cancel polling of BULK IN and INTERRUPT IN endpoints + cdc_dev->notif.cb = NULL; + cdc_dev->data.in_cb = NULL; + ESP_ERROR_CHECK(cdc_acm_reset_transfer_endpoint(cdc_dev->dev_hdl, cdc_dev->data.in_xfer)); + if (cdc_dev->notif.intf_desc != NULL) { + ESP_ERROR_CHECK(cdc_acm_reset_transfer_endpoint(cdc_dev->dev_hdl, cdc_dev->notif.xfer)); + } + + // Release all interfaces + ESP_ERROR_CHECK(usb_host_interface_release(p_cdc_acm_obj->cdc_acm_client_hdl, cdc_dev->dev_hdl, cdc_dev->data.intf_desc->bInterfaceNumber)); + if ((cdc_dev->notif.intf_desc != NULL) && (cdc_dev->notif.intf_desc != cdc_dev->data.intf_desc)) { + ESP_ERROR_CHECK(usb_host_interface_release(p_cdc_acm_obj->cdc_acm_client_hdl, cdc_dev->dev_hdl, cdc_dev->notif.intf_desc->bInterfaceNumber)); + } + + CDC_ACM_ENTER_CRITICAL(); + SLIST_REMOVE(&p_cdc_acm_obj->cdc_devices_list, cdc_dev, cdc_dev_s, list_entry); + CDC_ACM_EXIT_CRITICAL(); + + cdc_acm_device_remove(cdc_dev); + xSemaphoreGive(p_cdc_acm_obj->open_close_mutex); + return ESP_OK; +} + +void cdc_acm_host_desc_print(cdc_acm_dev_hdl_t cdc_hdl) +{ + assert(cdc_hdl); + cdc_dev_t *cdc_dev = (cdc_dev_t *)cdc_hdl; + + ESP_RETURN_ON_FALSE(cdc_dev->num_cdc_intf_desc > 0,, TAG, "No CDC-ACM specific descriptors found"); + + for (int i = 0; i < cdc_dev->num_cdc_intf_desc; i++) { + switch (((cdc_header_desc_t *)cdc_dev->cdc_intf_desc[i])->bDescriptorSubtype) { + case CDC_DESC_SUBTYPE_HEADER: { + cdc_header_desc_t *desc = (cdc_header_desc_t *)cdc_dev->cdc_intf_desc[i]; + printf("CDC Header Descriptor:\n"); + printf("\tbcdCDC: %d.%d0\n", ((desc->bcdCDC >> 8) & 0xF), ((desc->bcdCDC >> 4) & 0xF)); + break; + } + case CDC_DESC_SUBTYPE_CALL: { + cdc_acm_call_desc_t *desc = (cdc_acm_call_desc_t *)cdc_dev->cdc_intf_desc[i]; + printf("CDC Call Descriptor:\n"); + printf("\tbmCapabilities: 0x%02X\n", desc->bmCapabilities.val); + printf("\tbDataInterface: %d\n", desc->bDataInterface); + break; + } + case CDC_DESC_SUBTYPE_ACM: { + cdc_acm_acm_desc_t *desc = (cdc_acm_acm_desc_t *)cdc_dev->cdc_intf_desc[i]; + printf("CDC ACM Descriptor:\n"); + printf("\tbmCapabilities: 0x%02X\n", desc->bmCapabilities.val); + break; + } + case CDC_DESC_SUBTYPE_UNION: { + cdc_union_desc_t *desc = (cdc_union_desc_t *)cdc_dev->cdc_intf_desc[i]; + printf("CDC Union Descriptor:\n"); + printf("\tbControlInterface: %d\n", desc->bControlInterface); + printf("\tbSubordinateInterface[0]: %d\n", desc->bSubordinateInterface[0]); + break; + } + default: + ESP_LOGW(TAG, "Unsupported CDC specific descriptor"); + break; + } + } +} + +/** + * @brief Check finished transfer status + * + * Return to on transfer completed OK. + * Cancel the transfer and issue user's callback in case of an error. + * + * @param[in] transfer Transfer to be checked + * @return true Transfer completed + * @return false Transfer NOT completed + */ +static bool cdc_acm_is_transfer_completed(usb_transfer_t *transfer) +{ + cdc_dev_t *cdc_dev = (cdc_dev_t *)transfer->context; + bool completed = false; + + switch (transfer->status) { + case USB_TRANSFER_STATUS_COMPLETED: + completed = true; + break; + case USB_TRANSFER_STATUS_NO_DEVICE: // User is notified about device disconnection from usb_event_cb + case USB_TRANSFER_STATUS_CANCELED: + break; + case USB_TRANSFER_STATUS_ERROR: + case USB_TRANSFER_STATUS_TIMED_OUT: + case USB_TRANSFER_STATUS_STALL: + case USB_TRANSFER_STATUS_OVERFLOW: + case USB_TRANSFER_STATUS_SKIPPED: + default: + // Transfer was not completed or cancelled by user. Inform user about this + if (cdc_dev->notif.cb) { + const cdc_acm_host_dev_event_data_t error_event = { + .type = CDC_ACM_HOST_ERROR, + .data.error = (int) transfer->status + }; + cdc_dev->notif.cb((cdc_acm_dev_hdl_t) cdc_dev, &error_event, cdc_dev->cb_arg); + } + } + return completed; +} + +static void in_xfer_cb(usb_transfer_t *transfer) +{ + ESP_LOGD("CDC_ACM", "in xfer cb"); + cdc_dev_t *cdc_dev = (cdc_dev_t *)transfer->context; + + if (cdc_acm_is_transfer_completed(transfer)) { + if (cdc_dev->data.in_cb) { + cdc_dev->data.in_cb(transfer->data_buffer, transfer->actual_num_bytes, cdc_dev->cb_arg); + } + + ESP_LOGD("CDC_ACM", "Submitting poll for BULK IN transfer"); + usb_host_transfer_submit(cdc_dev->data.in_xfer); + } +} + +static void notif_xfer_cb(usb_transfer_t *transfer) +{ + ESP_LOGD("CDC_ACM", "notif xfer cb"); + cdc_dev_t *cdc_dev = (cdc_dev_t *)transfer->context; + + if (cdc_acm_is_transfer_completed(transfer)) { + cdc_notification_t *notif = (cdc_notification_t *)transfer->data_buffer; + switch (notif->bNotificationCode) { + case CDC_NOTIF_NETWORK_CONNECTION: { + if (cdc_dev->notif.cb) { + const cdc_acm_host_dev_event_data_t net_conn_event = { + .type = CDC_ACM_HOST_NETWORK_CONNECTION, + .data.network_connected = (bool) notif->wValue + }; + cdc_dev->notif.cb((cdc_acm_dev_hdl_t) cdc_dev, &net_conn_event, cdc_dev->cb_arg); + } + break; + } + case CDC_NOTIF_SERIAL_STATE: { + cdc_dev->serial_state.val = *((uint16_t *)notif->Data); + if (cdc_dev->notif.cb) { + const cdc_acm_host_dev_event_data_t serial_state_event = { + .type = CDC_ACM_HOST_SERIAL_STATE, + .data.serial_state = cdc_dev->serial_state + }; + cdc_dev->notif.cb((cdc_acm_dev_hdl_t) cdc_dev, &serial_state_event, cdc_dev->cb_arg); + } + break; + } + case CDC_NOTIF_RESPONSE_AVAILABLE: // Encapsulated commands not implemented - fallthrough + default: + ESP_LOGW("CDC_ACM", "Unsupported notification type 0x%02X", notif->bNotificationCode); + ESP_LOG_BUFFER_HEX("CDC_ACM", transfer->data_buffer, transfer->actual_num_bytes); + break; + } + + // Start polling for new data again + ESP_LOGD("CDC_ACM", "Submitting poll for INTR IN transfer"); + usb_host_transfer_submit(cdc_dev->notif.xfer); + } +} + +static void out_xfer_cb(usb_transfer_t *transfer) +{ + ESP_LOGD("CDC_ACM", "out/ctrl xfer cb"); + assert(transfer->context); + xSemaphoreGive((SemaphoreHandle_t)transfer->context); +} + +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: + ESP_LOGD(TAG, "New device connected"); + 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 + cdc_dev_t *cdc_dev; + cdc_dev_t *tcdc_dev; + // We are using 'SAFE' version of 'SLIST_FOREACH' which enables user to close the disconnected device in the callback + SLIST_FOREACH_SAFE(cdc_dev, &p_cdc_acm_obj->cdc_devices_list, list_entry, tcdc_dev) { + if (cdc_dev->dev_hdl == event_msg->dev_gone.dev_hdl && cdc_dev->notif.cb) { + // The suddenly disconnected device was opened by this driver: inform user about this + const cdc_acm_host_dev_event_data_t disconn_event = { + .type = CDC_ACM_HOST_DEVICE_DISCONNECTED, + }; + cdc_dev->notif.cb((cdc_acm_dev_hdl_t) cdc_dev, &disconn_event, cdc_dev->cb_arg); + } + } + break; + } + default: + assert(false); + break; + } +} + +esp_err_t cdc_acm_host_data_tx_blocking(cdc_acm_dev_hdl_t cdc_hdl, const uint8_t *data, size_t data_len, uint32_t timeout_ms) +{ + esp_err_t ret; + CDC_ACM_CHECK(cdc_hdl, ESP_ERR_INVALID_ARG); + cdc_dev_t *cdc_dev = (cdc_dev_t *)cdc_hdl; + CDC_ACM_CHECK(data && (data_len > 0), ESP_ERR_INVALID_ARG); + CDC_ACM_CHECK(cdc_dev->data.out_xfer, ESP_ERR_NOT_SUPPORTED); // Device was opened as read-only. + CDC_ACM_CHECK(data_len <= cdc_dev->data.out_xfer->data_buffer_size, ESP_ERR_INVALID_SIZE); + + // Take OUT mutex and fill the OUT transfer + BaseType_t taken = xSemaphoreTake(cdc_dev->data.out_mux, pdMS_TO_TICKS(timeout_ms)); + if (taken != pdTRUE) { + return ESP_ERR_TIMEOUT; + } + + ESP_LOGD("CDC_ACM", "Submitting BULK OUT transfer"); + memcpy(cdc_dev->data.out_xfer->data_buffer, data, data_len); + cdc_dev->data.out_xfer->num_bytes = data_len; + cdc_dev->data.out_xfer->timeout_ms = timeout_ms; + ESP_GOTO_ON_ERROR(usb_host_transfer_submit(cdc_dev->data.out_xfer), unblock, TAG,); + + // Wait for OUT transfer completion + taken = xSemaphoreTake((SemaphoreHandle_t)cdc_dev->data.out_xfer->context, pdMS_TO_TICKS(timeout_ms)); + if (!taken) { + // Reset the endpoint + cdc_acm_reset_transfer_endpoint(cdc_dev->dev_hdl, cdc_dev->data.out_xfer); + ret = ESP_ERR_TIMEOUT; + goto unblock; + } + + ESP_GOTO_ON_FALSE(cdc_dev->data.out_xfer->status == USB_TRANSFER_STATUS_COMPLETED, ESP_ERR_INVALID_RESPONSE, unblock, TAG, "Bulk OUT transfer error"); + ESP_GOTO_ON_FALSE(cdc_dev->data.out_xfer->actual_num_bytes == data_len, ESP_ERR_INVALID_RESPONSE, unblock, TAG, "Incorrect number of bytes transferred"); + ret = ESP_OK; + +unblock: + xSemaphoreGive(cdc_dev->data.out_mux); + return ret; +} + +esp_err_t cdc_acm_host_line_coding_get(cdc_acm_dev_hdl_t cdc_hdl, cdc_acm_line_coding_t *line_coding) +{ + CDC_ACM_CHECK(cdc_hdl && line_coding, ESP_ERR_INVALID_ARG); + + ESP_RETURN_ON_ERROR( + send_cdc_request((cdc_dev_t *)cdc_hdl, true, CDC_REQ_GET_LINE_CODING, (uint8_t *)line_coding, sizeof(cdc_acm_line_coding_t), 0), + TAG,); + ESP_LOGD(TAG, "Line Get: Rate: %d, Stop bits: %d, Parity: %d, Databits: %d", line_coding->dwDTERate, + line_coding->bCharFormat, line_coding->bParityType, line_coding->bDataBits); + return ESP_OK; +} + +esp_err_t cdc_acm_host_line_coding_set(cdc_acm_dev_hdl_t cdc_hdl, const cdc_acm_line_coding_t *line_coding) +{ + CDC_ACM_CHECK(cdc_hdl && line_coding, ESP_ERR_INVALID_ARG); + + ESP_RETURN_ON_ERROR( + send_cdc_request((cdc_dev_t *)cdc_hdl, false, CDC_REQ_SET_LINE_CODING, (uint8_t *)line_coding, sizeof(cdc_acm_line_coding_t), 0), + TAG,); + ESP_LOGD(TAG, "Line Set: Rate: %d, Stop bits: %d, Parity: %d, Databits: %d", line_coding->dwDTERate, + line_coding->bCharFormat, line_coding->bParityType, line_coding->bDataBits); + return ESP_OK; +} + +esp_err_t cdc_acm_host_set_control_line_state(cdc_acm_dev_hdl_t cdc_hdl, bool dtr, bool rts) +{ + CDC_ACM_CHECK(cdc_hdl, ESP_ERR_INVALID_ARG); + + const uint16_t ctrl_bitmap = (uint16_t)dtr | ((uint16_t)rts << 1); + + ESP_RETURN_ON_ERROR( + send_cdc_request((cdc_dev_t *)cdc_hdl, false, CDC_REQ_SET_CONTROL_LINE_STATE, NULL, 0, ctrl_bitmap), + TAG,); + ESP_LOGD(TAG, "Control Line Set: DTR: %d, RTS: %d", dtr, rts); + return ESP_OK; +} + +esp_err_t cdc_acm_host_send_break(cdc_acm_dev_hdl_t cdc_hdl, uint16_t duration_ms) +{ + CDC_ACM_CHECK(cdc_hdl, ESP_ERR_INVALID_ARG); + + ESP_RETURN_ON_ERROR( + send_cdc_request((cdc_dev_t *)cdc_hdl, false, CDC_REQ_SEND_BREAK, NULL, 0, duration_ms), + TAG,); + + // Block until break is deasserted + vTaskDelay(pdMS_TO_TICKS(duration_ms + 1)); + return ESP_OK; +} + +static esp_err_t send_cdc_request(cdc_dev_t *cdc_dev, bool in_transfer, cdc_request_code_t request, uint8_t *data, uint16_t data_len, uint16_t value) +{ + esp_err_t ret; + CDC_ACM_CHECK(cdc_dev->ctrl_transfer, ESP_ERR_NOT_SUPPORTED); + CDC_ACM_CHECK(cdc_dev->ctrl_transfer->data_buffer_size >= data_len, ESP_ERR_INVALID_SIZE); + + // Take Mutex and fill the CTRL request + BaseType_t taken = xSemaphoreTake(cdc_dev->ctrl_mux, pdMS_TO_TICKS(1000)); + if (!taken) { + return ESP_ERR_TIMEOUT; + } + usb_setup_packet_t *req = (usb_setup_packet_t *)(cdc_dev->ctrl_transfer->data_buffer); + uint8_t *start_of_data = (uint8_t *)req + sizeof(usb_setup_packet_t); + req->bmRequestType = USB_BM_REQUEST_TYPE_DIR_OUT | USB_BM_REQUEST_TYPE_TYPE_CLASS | USB_BM_REQUEST_TYPE_RECIP_INTERFACE; + req->bRequest = request; + req->wValue = value; + req->wIndex = cdc_dev->notif.intf_desc->bInterfaceNumber; + req->wLength = data_len; + + if (in_transfer) { + req->bmRequestType |= USB_BM_REQUEST_TYPE_DIR_IN; + } else { + memcpy(start_of_data, data, data_len); + } + + cdc_dev->ctrl_transfer->num_bytes = data_len + sizeof(usb_setup_packet_t); + ESP_GOTO_ON_ERROR( + usb_host_transfer_submit_control(p_cdc_acm_obj->cdc_acm_client_hdl, cdc_dev->ctrl_transfer), + unblock, TAG, "CTRL transfer failed"); + + taken = xSemaphoreTake((SemaphoreHandle_t)cdc_dev->ctrl_transfer->context, pdMS_TO_TICKS(1000)); // This is a fixed timeout. Every CDC device should be able to respond to CTRL transfer in 1 second + if (!taken) { + // Transfer was not finished, error in USB LIB. Reset the endpoint + cdc_acm_reset_transfer_endpoint(cdc_dev->dev_hdl, cdc_dev->ctrl_transfer); + ret = ESP_ERR_TIMEOUT; + goto unblock; + } + + ESP_GOTO_ON_FALSE(cdc_dev->ctrl_transfer->status == USB_TRANSFER_STATUS_COMPLETED, ESP_ERR_INVALID_RESPONSE, unblock, TAG, "Control transfer error"); + ESP_GOTO_ON_FALSE(cdc_dev->ctrl_transfer->actual_num_bytes == cdc_dev->ctrl_transfer->num_bytes, ESP_ERR_INVALID_RESPONSE, unblock, TAG, "Incorrect number of bytes transferred"); + + if (in_transfer) { + memcpy(data, start_of_data, data_len); + } + ret = ESP_OK; + +unblock: + xSemaphoreGive(cdc_dev->ctrl_mux); + return ret; +} + +esp_err_t cdc_acm_host_protocols_get(cdc_acm_dev_hdl_t cdc_hdl, cdc_comm_protocol_t *comm, cdc_data_protocol_t *data) +{ + CDC_ACM_CHECK(cdc_hdl, ESP_ERR_INVALID_ARG); + cdc_dev_t *cdc_dev = (cdc_dev_t *)cdc_hdl; + + if (comm != NULL) *comm = cdc_dev->comm_protocol; + if (data != NULL) *data = cdc_dev->data_protocol; + return ESP_OK; +} diff --git a/examples/peripherals/usb/host/cdc/common/cdc_acm_host/include/usb/cdc_acm_host.h b/examples/peripherals/usb/host/cdc/common/cdc_acm_host/include/usb/cdc_acm_host.h new file mode 100644 index 0000000..2709a8b --- /dev/null +++ b/examples/peripherals/usb/host/cdc/common/cdc_acm_host/include/usb/cdc_acm_host.h @@ -0,0 +1,305 @@ +/* + * SPDX-FileCopyrightText: 2015-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include "usb_types_cdc.h" +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct cdc_dev_s *cdc_acm_dev_hdl_t; + +/** + * @brief Line Coding structure + * @see Table 17, USB CDC-PSTN specification rev. 1.2 + */ +typedef struct { + uint32_t dwDTERate; // in bits per second + uint8_t bCharFormat; // 0: 1 stopbit, 1: 1.5 stopbits, 2: 2 stopbits + uint8_t bParityType; // 0: None, 1: Odd, 2: Even, 3: Mark, 4: Space + uint8_t bDataBits; // 5, 6, 7, 8 or 16 +} __attribute__((packed)) cdc_acm_line_coding_t; + +/** + * @brief UART State Bitmap + * @see Table 31, USB CDC-PSTN specification rev. 1.2 + */ +typedef union { + struct { + uint16_t bRxCarrier : 1; // State of receiver carrier detection mechanism of device. This signal corresponds to V.24 signal 109 and RS-232 signal DCD. + uint16_t bTxCarrier : 1; // State of transmission carrier. This signal corresponds to V.24 signal 106 and RS-232 signal DSR. + uint16_t bBreak : 1; // State of break detection mechanism of the device. + uint16_t bRingSignal : 1; // State of ring signal detection of the device. + uint16_t bFraming : 1; // A framing error has occurred. + uint16_t bParity : 1; // A parity error has occurred. + uint16_t bOverRun : 1; // Received data has been discarded due to overrun in the device. + uint16_t reserved : 9; + }; + uint16_t val; +} cdc_acm_uart_state_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_dev_event_t; + +/** + * @brief CDC-ACM Device Event data structure + * + */ +typedef struct { + cdc_acm_host_dev_event_t type; + union { + int error; // Error code from USB Host + cdc_acm_uart_state_t serial_state; // Serial (UART) state + bool network_connected; // Network connection event + } data; +} cdc_acm_host_dev_event_data_t; + +/** + * @brief Data receive callback type + */ +typedef void (*cdc_acm_data_callback_t)(uint8_t* data, size_t data_len, void *user_arg); + +/** + * @brief Device event callback type + * @see cdc_acm_host_dev_event_t + */ +typedef void (*cdc_acm_host_dev_callback_t)(cdc_acm_dev_hdl_t cdc_hdl, const cdc_acm_host_dev_event_data_t *event, void *user_ctx); + +/** + * @brief Configuration structure of USB Host CDC-ACM driver + * + */ +typedef struct { + size_t driver_task_stack_size; /**< Stack size of the driver's task */ + unsigned driver_task_priority; /**< Priority of the driver's task */ + int xCoreID; /**< Core affinity of the driver's task */ +} cdc_acm_host_driver_config_t; + +/** + * @brief Configuration structure of CDC-ACM device + * + */ +typedef struct { + uint32_t connection_timeout_ms; /**< Timeout for USB device connection in [ms] */ + size_t out_buffer_size; /**< Maximum size of USB bulk out transfer, set to 0 for read-only devices */ + cdc_acm_host_dev_callback_t event_cb; /**< Device's event callback function. Can be NULL */ + cdc_acm_data_callback_t data_cb; /**< Device's data RX callback function. Can be NULL for write-only devices */ + void *user_arg; /**< User's argument that will be passed to the callbacks */ +} cdc_acm_host_device_config_t; + +/** + * @brief Install CDC-ACM driver + * + * - USB Host Library must already be installed before calling this function (via usb_host_install()) + * - This function should be called before calling any other CDC driver functions + * + * @param[in] driver_config Driver configuration structure. If set to NULL, a default configuration will be used. + * @return esp_err_t + */ +esp_err_t cdc_acm_host_install(const cdc_acm_host_driver_config_t *driver_config); + +/** + * @brief Uninstall CDC-ACM driver + * + * - Users must ensure that all CDC devices must be closed via cdc_acm_host_close() before calling this function + * + * @return esp_err_t + */ +esp_err_t cdc_acm_host_uninstall(void); + +/** + * @brief Open CDC-ACM compliant device + * + * CDC-ACM compliant device must contain either an Interface Association Descriptor or CDC-Union descriptor, + * which are used for the driver's configuration. + * + * @param[in] vid Device's Vendor ID + * @param[in] pid Device's Product ID + * @param[in] interface_idx Index of device's interface used for CDC-ACM communication + * @param[in] dev_config Configuration structure of the device + * @param[out] cdc_hdl_ret CDC device handle + * @return esp_err_t + */ +esp_err_t cdc_acm_host_open(uint16_t vid, uint16_t pid, uint8_t interface_idx, const cdc_acm_host_device_config_t *dev_config, cdc_acm_dev_hdl_t *cdc_hdl_ret); + +/** + * @brief Open CDC-ACM non-compliant device + * + * CDC-ACM non-compliant device acts as CDC-ACM device but doesn't support all its features. + * User must provide the interface index that will be used (zero for non-composite devices). + * + * @param[in] vid Device's Vendor ID + * @param[in] pid Device's Product ID + * @param[in] interface_idx Index of device's interface used for CDC-ACM like communication + * @param[in] dev_config Configuration structure of the device + * @param[out] cdc_hdl_ret CDC device handle + * @return esp_err_t + */ +esp_err_t cdc_acm_host_open_vendor_specific(uint16_t vid, uint16_t pid, uint8_t interface_num, const cdc_acm_host_device_config_t *dev_config, cdc_acm_dev_hdl_t *cdc_hdl_ret); + +/** + * @brief Close CDC device and release its resources + * + * @note All in-flight transfers will be prematurely canceled. + * @param cdc_hdl CDC handle obtained from cdc_acm_host_open() + * @return esp_err_t + */ +esp_err_t cdc_acm_host_close(cdc_acm_dev_hdl_t cdc_hdl); + +/** + * @brief Transmit data - blocking mode + * + * @param cdc_hdl CDC handle obtained from cdc_acm_host_open() + * @param[in] data Data to be sent + * @param[in] data_len Data length + * @param[in] timeout_ms Timeout in [ms] + * @return esp_err_t + */ +esp_err_t cdc_acm_host_data_tx_blocking(cdc_acm_dev_hdl_t cdc_hdl, const uint8_t *data, size_t data_len, uint32_t timeout_ms); + +/** + * @brief SetLineCoding function + * + * @see Chapter 6.3.10, USB CDC-PSTN specification rev. 1.2 + * + * @param cdc_hdl CDC handle obtained from cdc_acm_host_open() + * @param[in] line_coding Line Coding structure + * @return esp_err_t + */ +esp_err_t cdc_acm_host_line_coding_set(cdc_acm_dev_hdl_t cdc_hdl, const cdc_acm_line_coding_t *line_coding); + +/** + * @brief GetLineCoding function + * + * @see Chapter 6.3.11, USB CDC-PSTN specification rev. 1.2 + * + * @param cdc_hdl CDC handle obtained from cdc_acm_host_open() + * @param[out] line_coding Line Coding structure to be filled + * @return esp_err_t + */ +esp_err_t cdc_acm_host_line_coding_get(cdc_acm_dev_hdl_t cdc_hdl, cdc_acm_line_coding_t *line_coding); + +/** + * @brief SetControlLineState function + * + * @see Chapter 6.3.12, USB CDC-PSTN specification rev. 1.2 + * + * @param cdc_hdl CDC handle obtained from cdc_acm_host_open() + * @param[in] dtr Indicates to DCE if DTE is present or not. This signal corresponds to V.24 signal 108/2 and RS-232 signal Data Terminal Ready. + * @param[in] rts Carrier control for half duplex modems. This signal corresponds to V.24 signal 105 and RS-232 signal Request To Send. + * @return esp_err_t + */ +esp_err_t cdc_acm_host_set_control_line_state(cdc_acm_dev_hdl_t cdc_hdl, bool dtr, bool rts); + +/** + * @brief SendBreak function + * + * This function will block until the duration_ms has passed. + * + * @see Chapter 6.3.13, USB CDC-PSTN specification rev. 1.2 + * + * @param cdc_hdl CDC handle obtained from cdc_acm_host_open() + * @param[in] duration_ms Duration of the Break signal in [ms] + * @return esp_err_t + */ +esp_err_t cdc_acm_host_send_break(cdc_acm_dev_hdl_t cdc_hdl, uint16_t duration_ms); + +/** + * @brief Print CDC-ACM specific descriptors + * + * Descriptors are printed in human readable format to stdout. + * Intended for debugging and for CDC-ACM compliant devices only. + * + * @param cdc_hdl CDC handle obtained from cdc_acm_host_open() + */ +void cdc_acm_host_desc_print(cdc_acm_dev_hdl_t cdc_hdl); + +/** + * @brief Get protocols defined in USB-CDC interface descriptors + * + * @param cdc_hdl CDC handle obtained from cdc_acm_host_open() + * @param[out] comm Communication protocol + * @param[out] data Data protocol + * @return esp_err_t + */ +esp_err_t cdc_acm_host_protocols_get(cdc_acm_dev_hdl_t cdc_hdl, cdc_comm_protocol_t *comm, cdc_data_protocol_t *data); + +#ifdef __cplusplus +} +class CdcAcmDevice +{ +public: + // Operators + CdcAcmDevice() : cdc_hdl(NULL){}; + ~CdcAcmDevice() + { + // Close CDC-ACM device, if it wasn't explicitly closed + if (this->cdc_hdl != NULL) { + this->close(); + } + } + + inline esp_err_t tx_blocking(uint8_t *data, size_t len, uint32_t timeout_ms = 100) + { + return cdc_acm_host_data_tx_blocking(this->cdc_hdl, data, len, timeout_ms); + } + + inline esp_err_t open(uint16_t vid, uint16_t pid, uint8_t interface_idx, const cdc_acm_host_device_config_t* dev_config) + { + return cdc_acm_host_open(vid, pid, interface_idx, dev_config, &this->cdc_hdl); + } + + inline esp_err_t open_vendor_specific(uint16_t vid, uint16_t pid, uint8_t interface_idx, const cdc_acm_host_device_config_t* dev_config) + { + return cdc_acm_host_open_vendor_specific(vid, pid, interface_idx, dev_config, &this->cdc_hdl); + } + + inline void close() + { + cdc_acm_host_close(this->cdc_hdl); + this->cdc_hdl = NULL; + } + + inline esp_err_t line_coding_get(cdc_acm_line_coding_t *line_coding) + { + return cdc_acm_host_line_coding_get(this->cdc_hdl, line_coding); + } + + inline esp_err_t line_coding_set(cdc_acm_line_coding_t *line_coding) + { + return cdc_acm_host_line_coding_set(this->cdc_hdl, line_coding); + } + + inline esp_err_t set_control_line_state(bool dtr, bool rts) + { + return cdc_acm_host_set_control_line_state(this->cdc_hdl, dtr, rts); + } + + inline esp_err_t send_break(uint16_t duration_ms) + { + return cdc_acm_host_send_break(this->cdc_hdl, duration_ms); + } + +private: + CdcAcmDevice(const CdcAcmDevice &Copy); + CdcAcmDevice &operator= (const CdcAcmDevice &Copy); + bool operator== (const CdcAcmDevice ¶m) const; + bool operator!= (const CdcAcmDevice ¶m) const; + cdc_acm_dev_hdl_t cdc_hdl; +}; +#endif diff --git a/examples/peripherals/usb/host/cdc/common/cdc_acm_host/include/usb/usb_types_cdc.h b/examples/peripherals/usb/host/cdc/common/cdc_acm_host/include/usb/usb_types_cdc.h new file mode 100644 index 0000000..242d4e2 --- /dev/null +++ b/examples/peripherals/usb/host/cdc/common/cdc_acm_host/include/usb/usb_types_cdc.h @@ -0,0 +1,206 @@ +/* + * SPDX-FileCopyrightText: 2015-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once +#include + +/** + * @brief USB CDC Descriptor Subtypes + * + * @see Table 13, USB CDC specification rev. 1.2 + */ +typedef enum { + CDC_DESC_SUBTYPE_HEADER = 0x00, // Header Functional Descriptor + CDC_DESC_SUBTYPE_CALL = 0x01, // Call Management Functional Descriptor + CDC_DESC_SUBTYPE_ACM = 0x02, // Abstract Control Management Functional Descriptor + CDC_DESC_SUBTYPE_DLM = 0x03, // Direct Line Management Functional Descriptor + CDC_DESC_SUBTYPE_TEL_RINGER = 0x04, // Telephone Ringer Functional Descriptor + CDC_DESC_SUBTYPE_TEL_CLSR = 0x05, // Telephone Call and Line State Reporting Capabilities Functional Descriptor + CDC_DESC_SUBTYPE_UNION = 0x06, // Union Functional Descriptor + CDC_DESC_SUBTYPE_COUNTRY = 0x07, // Country Selection Functional Descriptor + CDC_DESC_SUBTYPE_TEL_MODE = 0x08, // Telephone Operational Modes Functional Descriptor + CDC_DESC_SUBTYPE_TERMINAL = 0x09, // USB Terminal + CDC_DESC_SUBTYPE_NCHT = 0x0A, // Network Channel Terminal + CDC_DESC_SUBTYPE_PROTOCOL = 0x08, // Protocol Unit + CDC_DESC_SUBTYPE_EXTENSION = 0x0C, // Extension Unit + CDC_DESC_SUBTYPE_MULTI_CHAN = 0x0D, // Multi-Channel Management Functional Descriptor + CDC_DESC_SUBTYPE_CAPI = 0x0E, // CAPI Control + CDC_DESC_SUBTYPE_ETH = 0x0F, // Ethernet Networking + CDC_DESC_SUBTYPE_ATM = 0x10, // ATM Networking + CDC_DESC_SUBTYPE_WHANDSET = 0x11, // Wireless Handset Control Model Functional Descriptor + CDC_DESC_SUBTYPE_MDLM = 0x12, // Mobile Direct Line Model + CDC_DESC_SUBTYPE_MDLM_DETAIL = 0x13, // MDLM Detail + CDC_DESC_SUBTYPE_DMM = 0x14, // Device Management Model + CDC_DESC_SUBTYPE_OBEX = 0x15, // OBEX Functional + CDC_DESC_SUBTYPE_COMMAND_SET = 0x16, // Command Set + CDC_DESC_SUBTYPE_COMMAND_SET_DETAIL = 0x17, // Command Set Detail Functional Descriptor + CDC_DESC_SUBTYPE_TEL_CM = 0x18, // Telephone Control Model Functional Descriptor + CDC_DESC_SUBTYPE_OBEX_SERVICE = 0x19, // OBEX Service Identifier Functional Descriptor + CDC_DESC_SUBTYPE_NCM = 0x1A // NCM Functional Descriptor +} __attribute__((packed)) cdc_desc_subtype_t; + +/** + * @brief USB CDC Subclass codes + * + * @see Table 4, USB CDC specification rev. 1.2 + */ +typedef enum { + CDC_SUBCLASS_DLCM = 0x01, // Direct Line Control Model + CDC_SUBCLASS_ACM = 0x02, // Abstract Control Model + CDC_SUBCLASS_TCM = 0x03, // Telephone Control Model + CDC_SUBCLASS_MCHCM = 0x04, // Multi-Channel Control Model + CDC_SUBCLASS_CAPI = 0x05, // CAPI Control Model + CDC_SUBCLASS_ECM = 0x06, // Ethernet Networking Control Model + CDC_SUBCLASS_ATM = 0x07, // ATM Networking Model + CDC_SUBCLASS_HANDSET = 0x08, // Wireless Handset Control Model + CDC_SUBCLASS_DEV_MAN = 0x09, // Device Management + CDC_SUBCLASS_MOBILE = 0x0A, // Mobile Direct Line Model + CDC_SUBCLASS_OBEX = 0x0B, // OBEX + CDC_SUBCLASS_EEM = 0x0C, // Ethernet Emulation Model + CDC_SUBCLASS_NCM = 0x0D // Network Control Model +} __attribute__((packed)) cdc_subclass_t; + +/** + * @brief USB CDC Communications Protocol Codes + * + * @see Table 5, USB CDC specification rev. 1.2 + */ +typedef enum { + CDC_COMM_PROTOCOL_NONE = 0x00, // No class specific protocol required + CDC_COMM_PROTOCOL_V250 = 0x01, // AT Commands: V.250 etc + CDC_COMM_PROTOCOL_PCAA = 0x02, // AT Commands defined by PCCA-101 + CDC_COMM_PROTOCOL_PCAA_A = 0x03, // AT Commands defined by PCAA-101 & Annex O + CDC_COMM_PROTOCOL_GSM = 0x04, // AT Commands defined by GSM 07.07 + CDC_COMM_PROTOCOL_3GPP = 0x05, // AT Commands defined by 3GPP 27.007 + CDC_COMM_PROTOCOL_TIA = 0x06, // AT Commands defined by TIA for CDMA + CDC_COMM_PROTOCOL_EEM = 0x07, // Ethernet Emulation Model + CDC_COMM_PROTOCOL_EXT = 0xFE, // External Protocol: Commands defined by Command Set Functional Descriptor + CDC_COMM_PROTOCOL_VENDOR = 0xFF // Vendor-specific +} __attribute__((packed)) cdc_comm_protocol_t; + +/** + * @brief USB CDC Data Protocol Codes + * + * @see Table 7, USB CDC specification rev. 1.2 + */ +typedef enum { + CDC_DATA_PROTOCOL_NONE = 0x00, // No class specific protocol required + CDC_DATA_PROTOCOL_NCM = 0x01, // Network Transfer Block + CDC_DATA_PROTOCOL_I430 = 0x30, // Physical interface protocol for ISDN BRI + CDC_DATA_PROTOCOL_HDLC = 0x31, // HDLC + CDC_DATA_PROTOCOL_Q921M = 0x50, // Management protocol for Q.921 data link protocol + CDC_DATA_PROTOCOL_Q921 = 0x51, // Data link protocol for Q.931 + CDC_DATA_PROTOCOL_Q921TM = 0x52, // TEI-multiplexor for Q.921 data link protocol + CDC_DATA_PROTOCOL_V42BIS = 0x90, // Data compression procedures + CDC_DATA_PROTOCOL_Q931 = 0x91, // Euro-ISDN protocol control + CDC_DATA_PROTOCOL_V120 = 0x92, // V.24 rate adaptation to ISDN + CDC_DATA_PROTOCOL_CAPI = 0x93, // CAPI Commands + CDC_DATA_PROTOCOL_VENDOR = 0xFF // Vendor-specific +} __attribute__((packed)) cdc_data_protocol_t; + +/** + * @brief USB CDC Request Codes + * + * @see Table 19, USB CDC specification rev. 1.2 + */ +typedef enum { + CDC_REQ_SEND_ENCAPSULATED_COMMAND = 0x00, + CDC_REQ_GET_ENCAPSULATED_RESPONSE = 0x01, + CDC_REQ_SET_COMM_FEATURE = 0x02, + CDC_REQ_GET_COMM_FEATURE = 0x03, + CDC_REQ_CLEAR_COMM_FEATURE = 0x04, + CDC_REQ_SET_AUX_LINE_STATE = 0x10, + CDC_REQ_SET_HOOK_STATE = 0x11, + CDC_REQ_PULSE_SETUP = 0x12, + CDC_REQ_SEND_PULSE = 0x13, + CDC_REQ_SET_PULSE_TIME = 0x14, + CDC_REQ_RING_AUX_JACK = 0x15, + CDC_REQ_SET_LINE_CODING = 0x20, + CDC_REQ_GET_LINE_CODING = 0x21, + CDC_REQ_SET_CONTROL_LINE_STATE = 0x22, + CDC_REQ_SEND_BREAK = 0x23, + CDC_REQ_SET_RINGER_PARMS = 0x30, + CDC_REQ_GET_RINGER_PARMS = 0x31, + CDC_REQ_SET_OPERATION_PARMS = 0x32, + CDC_REQ_GET_OPERATION_PARMS = 0x33, + CDC_REQ_SET_LINE_PARMS = 0x34, + CDC_REQ_GET_LINE_PARMS = 0x35, + CDC_REQ_DIAL_DIGITS = 0x36, + CDC_REQ_SET_UNIT_PARAMETER = 0x37, + CDC_REQ_GET_UNIT_PARAMETER = 0x38, + CDC_REQ_CLEAR_UNIT_PARAMETER = 0x39, + CDC_REQ_GET_PROFILE = 0x3A, + CDC_REQ_SET_ETHERNET_MULTICAST_FILTERS = 0x40, + CDC_REQ_SET_ETHERNET_POWER_MANAGEMENT_PATTERN_FILTER = 0x41, + CDC_REQ_GET_ETHERNET_POWER_MANAGEMENT_PATTERN_FILTER = 0x42, + CDC_REQ_SET_ETHERNET_PACKET_FILTER = 0x43, + CDC_REQ_GET_ETHERNET_STATISTIC = 0x44, + CDC_REQ_SET_ATM_DATA_FORMAT = 0x50, + CDC_REQ_GET_ATM_DEVICE_STATISTICS = 0x51, + CDC_REQ_SET_ATM_DEFAULT_VC = 0x52, + CDC_REQ_GET_ATM_VC_STATISTICS = 0x53, + CDC_REQ_GET_NTB_PARAMETERS = 0x80, + CDC_REQ_GET_NET_ADDRESS = 0x81, + CDC_REQ_SET_NET_ADDRESS = 0x82, + CDC_REQ_GET_NTB_FORMAT = 0x83, + CDC_REQ_SET_NTB_FORMAT = 0x84, + CDC_REQ_GET_NTB_INPUT_SIZE = 0x85, + CDC_REQ_SET_NTB_INPUT_SIZE = 0x86, + CDC_REQ_GET_MAX_DATAGRAM_SIZE = 0x87, + CDC_REQ_SET_MAX_DATAGRAM_SIZE = 0x88, + CDC_REQ_GET_CRC_MODE = 0x89, + CDC_REQ_SET_CRC_MODE = 0x8A +} __attribute__((packed)) cdc_request_code_t; + +/** + * @brief USB CDC Notification Codes + * + * @see Table 20, USB CDC specification rev. 1.2 + */ +typedef enum { + CDC_NOTIF_NETWORK_CONNECTION = 0x00, + CDC_NOTIF_RESPONSE_AVAILABLE = 0x01, + CDC_NOTIF_AUX_JACK_HOOK_STATE = 0x08, + CDC_NOTIF_RING_DETECT = 0x09, + CDC_NOTIF_SERIAL_STATE = 0x20, + CDC_NOTIF_CALL_STATE_CHANGE = 0x28, + CDC_NOTIF_LINE_STATE_CHANGE = 0x29, + CDC_NOTIF_CONNECTION_SPEED_CHANGE = 0x2A +} __attribute__((packed)) cdc_notification_code_t; + +typedef struct { + uint8_t bmRequestType; + cdc_notification_code_t bNotificationCode; + uint16_t wValue; + uint16_t wIndex; + uint16_t wLength; + uint8_t Data[]; +} __attribute__((packed)) cdc_notification_t; + +/** + * @brief USB CDC Header Functional Descriptor + * + * @see Table 15, USB CDC specification rev. 1.2 + */ +typedef struct { + uint8_t bFunctionLength; + const uint8_t bDescriptorType; // Upper nibble: CDC code 0x02, Lower nibble: intf/ep descriptor type 0x04/0x05 + const cdc_desc_subtype_t bDescriptorSubtype; + uint16_t bcdCDC; // CDC version as binary-coded decimal. This driver is written for version 1.2 +} __attribute__((packed)) cdc_header_desc_t; + +/** + * @brief USB CDC Union Functional Descriptor + * + * @see Table 16, USB CDC specification rev. 1.2 + */ +typedef struct { + uint8_t bFunctionLength; + const uint8_t bDescriptorType; // Upper nibble: CDC code 0x02, Lower nibble: intf/ep descriptor type 0x04/0x05 + const cdc_desc_subtype_t bDescriptorSubtype; + const uint8_t bControlInterface; // Master/controlling interface + uint8_t bSubordinateInterface[]; // Slave/subordinate interfaces +} __attribute__((packed)) cdc_union_desc_t; diff --git a/examples/peripherals/usb/host/cdc/common/cdc_acm_host/test/CMakeLists.txt b/examples/peripherals/usb/host/cdc/common/cdc_acm_host/test/CMakeLists.txt new file mode 100644 index 0000000..067cc9b --- /dev/null +++ b/examples/peripherals/usb/host/cdc/common/cdc_acm_host/test/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "test_cdc_acm_host.c" + INCLUDE_DIRS "." + REQUIRES cdc_acm_host unity) diff --git a/examples/peripherals/usb/host/cdc/common/cdc_acm_host/test/test_cdc_acm_host.c b/examples/peripherals/usb/host/cdc/common/cdc_acm_host/test/test_cdc_acm_host.c new file mode 100644 index 0000000..fff3c91 --- /dev/null +++ b/examples/peripherals/usb/host/cdc/common/cdc_acm_host/test/test_cdc_acm_host.c @@ -0,0 +1,375 @@ +/* + * SPDX-FileCopyrightText: 2015-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "soc/soc_caps.h" +#if SOC_USB_OTG_SUPPORTED + +#include +#include "esp_system.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "esp_err.h" + +#include "usb/usb_host.h" +#include "usb/cdc_acm_host.h" +#include + +#include "esp_intr_alloc.h" + +#include "unity.h" +#include "soc/usb_wrap_struct.h" + +static uint8_t tx_buf[] = "HELLO"; +static uint8_t tx_buf2[] = "WORLD"; +static int nb_of_responses; +static int nb_of_responses2; + +void test_usb_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); + } + usb_wrap_dev_t *wrap = &USB_WRAP; + if (connected) { + //Disable test mode to return to previous internal PHY configuration + wrap->test_conf.test_enable = 0; + } else { + /* + Mimic a disconnection by using the internal PHY's test mode. + Force Output Enable to 1 (even if the controller isn't outputting). With test_tx_dp and test_tx_dm set to 0, + this will look like a disconnection. + */ + wrap->test_conf.val = 0; + wrap->test_conf.test_usb_wrap_oe = 1; + wrap->test_conf.test_enable = 1; + } +} + +void usb_lib_task(void *arg) +{ + // Install USB Host driver. Should only be called once in entire application + const usb_host_config_t host_config = { + .intr_flags = ESP_INTR_FLAG_LEVEL1, + }; + TEST_ASSERT_EQUAL(ESP_OK, usb_host_install(&host_config)); + printf("USB Host installed\n"); + xTaskNotifyGive(arg); + + while (1) { + // 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: clean up\n"); + // The device should not have been freed yet, so we expect an ESP_ERR_NOT_FINISHED + TEST_ASSERT_EQUAL(ESP_ERR_NOT_FINISHED, usb_host_device_free_all()); + } + if (event_flags & USB_HOST_LIB_EVENT_FLAGS_ALL_FREE) { + printf("All free: uninstall USB lib\n"); + break; + } + } + + // Clean up USB Host + vTaskDelay(10); // Short delay to allow clients clean-up + usb_host_lib_handle_events(0, NULL); // Make sure there are now pending events + TEST_ASSERT_EQUAL(ESP_OK, usb_host_uninstall()); + vTaskDelete(NULL); +} + +void test_install_cdc_driver(void) +{ + // Create a task that will handle USB library events + TEST_ASSERT_EQUAL(pdTRUE, xTaskCreate(usb_lib_task, "usb_lib", 4*4096, xTaskGetCurrentTaskHandle(), 10, NULL)); + ulTaskNotifyTake(false, 1000); + + printf("Installing CDC-ACM driver\n"); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_install(NULL)); +} + +/* ------------------------------- Callbacks -------------------------------- */ +static void handle_rx(uint8_t *data, size_t data_len, void *arg) +{ + printf("Data received\n"); + nb_of_responses++; + TEST_ASSERT_EQUAL_STRING_LEN(data, arg, data_len); +} + +static void handle_rx2(uint8_t *data, size_t data_len, void *arg) +{ + printf("Data received 2\n"); + nb_of_responses2++; + TEST_ASSERT_EQUAL_STRING_LEN(data, arg, data_len); +} + +static void notif_cb(cdc_acm_dev_hdl_t cdc_hdl, const cdc_acm_host_dev_event_data_t *event, void *user_ctx) +{ + switch (event->type) { + case CDC_ACM_HOST_ERROR: + printf("Error event %d\n", event->data.error); + break; + case CDC_ACM_HOST_SERIAL_STATE: + break; + case CDC_ACM_HOST_NETWORK_CONNECTION: + break; + case CDC_ACM_HOST_DEVICE_DISCONNECTED: + printf("Disconnection event\n"); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_close(cdc_hdl)); + xTaskNotifyGive(user_ctx); + break; + default: + assert(false); + } +} + +/* Basic test to check CDC communication: + * open/read/write/close device + * CDC-ACM specific commands: set/get_line_coding, set_control_line_state */ +TEST_CASE("USB Host CDC-ACM driver: Basic test", "[cdc_acm][ignore]") +{ + nb_of_responses = 0; + 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, + }; + + 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); + cdc_acm_host_desc_print(cdc_dev); + vTaskDelay(100); + + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_data_tx_blocking(cdc_dev, tx_buf, sizeof(tx_buf), 1000)); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_data_tx_blocking(cdc_dev, tx_buf, sizeof(tx_buf), 1000)); + vTaskDelay(100); // Wait until responses are processed + + // We sent two messages, should get two responses + TEST_ASSERT_EQUAL(2, nb_of_responses); + + 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)); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_set_control_line_state(cdc_dev, true, false)); + + 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 +} + +/* Test communication with multiple CDC-ACM devices from one thread */ +TEST_CASE("USB Host CDC-ACM driver: Multiple devices test", "[cdc_acm][ignore]") +{ + nb_of_responses = 0; + nb_of_responses2 = 0; + + test_install_cdc_driver(); + + printf("Opening 2 CDC-ACM devices\n"); + cdc_acm_dev_hdl_t cdc_dev1, cdc_dev2; + cdc_acm_host_device_config_t dev_config = { + .connection_timeout_ms = 1000, + .out_buffer_size = 64, + .event_cb = notif_cb, + .data_cb = handle_rx, + .user_arg = tx_buf, + }; + 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); + + 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(100); // Wait for RX callbacks + + // We sent two messages, should get two responses + TEST_ASSERT_EQUAL(1, nb_of_responses); + TEST_ASSERT_EQUAL(1, nb_of_responses2); + + 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()); + + //Short delay to allow task to be cleaned up + vTaskDelay(20); +} + +#define MULTIPLE_THREADS_TRANSFERS_NUM 5 +#define MULTIPLE_THREADS_TASKS_NUM 4 +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 + for (int i = 0; i < MULTIPLE_THREADS_TRANSFERS_NUM; i++) { + // BULK endpoints + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_data_tx_blocking(cdc_dev, tx_buf, sizeof(tx_buf), 1000)); + + // CTRL endpoints + cdc_acm_line_coding_t line_coding_get; + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_line_coding_get(cdc_dev, &line_coding_get)); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_set_control_line_state(cdc_dev, true, false)); + } + vTaskDelete(NULL); +} + +/** + * @brief Multiple threads test + * + * In this test, one CDC device is accessed from multiple threads. + * It has to be opened/closed just once, though. + */ +TEST_CASE("USB Host CDC-ACM driver: Multiple threads test", "[cdc_acm][ignore]") +{ + nb_of_responses = 0; + 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 two 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)); + } + + // Wait until all tasks finish + vTaskDelay(pdMS_TO_TICKS(500)); + TEST_ASSERT_EQUAL(MULTIPLE_THREADS_TASKS_NUM * MULTIPLE_THREADS_TRANSFERS_NUM, nb_of_responses); + + // Clean-up + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_close(cdc_dev)); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_uninstall()); + vTaskDelay(20); +} + +/* Test CDC driver reaction to USB device sudden disconnection */ +TEST_CASE("USB Host CDC-ACM driver: Sudden disconnection test", "[cdc_acm][ignore]") +{ + test_install_cdc_driver(); + + cdc_acm_dev_hdl_t cdc_dev; + cdc_acm_host_device_config_t dev_config = { + .connection_timeout_ms = 1000, + .out_buffer_size = 64, + .event_cb = notif_cb, + .data_cb = handle_rx + }; + dev_config.user_arg = xTaskGetCurrentTaskHandle(); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_open(0x303A, 0x4002, 0, &dev_config, &cdc_dev)); + TEST_ASSERT_NOT_NULL(cdc_dev); + + test_usb_force_conn_state(false, pdMS_TO_TICKS(10)); + // Notify will succeed only if CDC_ACM_HOST_DEVICE_DISCONNECTED notification was generated + TEST_ASSERT_EQUAL(1, ulTaskNotifyTake(false, pdMS_TO_TICKS(100))); + + test_usb_force_conn_state(true, 0); // Switch back to real PHY + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_uninstall()); + vTaskDelay(20); //Short delay to allow task to be cleaned up +} + +/** + * @brief CDC-ACM error handling test + * + * There are multiple erroneous scenarios checked in this test: + * + * -# Install CDC-ACM driver without USB Host + * -# Open device without installed driver + * -# Uninstall driver before installing it + * -# Open non-existent device + * -# Open the same device twice + * -# Uninstall driver with open devices + * -# Send data that is too large + * -# Send unsupported CDC request + * -# Write to read-only device + */ +TEST_CASE("USB Host CDC-ACM driver: Error handling", "[cdc_acm][ignore]") +{ + cdc_acm_dev_hdl_t cdc_dev; + cdc_acm_host_device_config_t dev_config = { + .connection_timeout_ms = 500, + .out_buffer_size = 64, + .event_cb = notif_cb, + .data_cb = handle_rx + }; + + // Install CDC-ACM driver without USB Host + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_STATE, cdc_acm_host_install(NULL)); + + // Open device without installed driver + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_STATE, cdc_acm_host_open(0x303A, 0x4002, 0, &dev_config, &cdc_dev)); + + // Uninstall driver before installing it + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_STATE, cdc_acm_host_uninstall()); + + // Properly install USB and CDC drivers + test_install_cdc_driver(); + + // Open non-existent device + TEST_ASSERT_EQUAL(ESP_ERR_NOT_FOUND, cdc_acm_host_open(0x303A, 0x1234, 0, &dev_config, &cdc_dev)); // 0x303A:0x1234 this device is not connected to USB Host + TEST_ASSERT_NULL(cdc_dev); + + // Open regular device + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_open(0x303A, 0x4002, 0, &dev_config, &cdc_dev)); + TEST_ASSERT_NOT_NULL(cdc_dev); + + // Open one CDC-ACM device twice //@todo this test is commented out due to bug in usb_host + //cdc_acm_dev_hdl_t cdc_dev_test; + //TEST_ASSERT_EQUAL(ESP_ERR_INVALID_STATE, cdc_acm_host_open(0x303A, 0x4002, 0, &dev_config, &cdc_dev_test)); + //TEST_ASSERT_NULL(cdc_dev_test); + + // Uninstall driver with open devices + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_STATE, cdc_acm_host_uninstall()); + + // Send data that is too large and NULL data + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_SIZE, cdc_acm_host_data_tx_blocking(cdc_dev, tx_buf, 1024, 1000)); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, cdc_acm_host_data_tx_blocking(cdc_dev, NULL, 10, 1000)); + + // Change mode to read-only and try to write to it + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_close(cdc_dev)); + dev_config.out_buffer_size = 0; // Read-only device + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_open(0x303A, 0x4002, 0, &dev_config, &cdc_dev)); + TEST_ASSERT_NOT_NULL(cdc_dev); + TEST_ASSERT_EQUAL(ESP_ERR_NOT_SUPPORTED, cdc_acm_host_data_tx_blocking(cdc_dev, tx_buf, sizeof(tx_buf), 1000)); + + // Send unsupported CDC request (TinyUSB accepts SendBreak command, eventhough it doesn't support it) + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_send_break(cdc_dev, 100)); + + // Clean-up + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_close(cdc_dev)); + TEST_ASSERT_EQUAL(ESP_OK, cdc_acm_host_uninstall()); + vTaskDelay(20); +} + +#endif