/* Copyright (c) 2024 Bert Melis. All rights reserved. This work is licensed under the terms of the MIT license. For a copy, see or the LICENSE file. Based on example code: SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD SPDX-License-Identifier: CC0-1.0 */ #include "USBHostSerial.h" using namespace esp_usb; USBHostSerial::USBHostSerial(uint16_t vid, uint16_t pid) : _host_config{} , _line_coding{} , _tx_buf_mem{} , _tx_buf_handle(nullptr) , _tx_buf_data{} , _rx_buf_mem{} , _rx_buf_handle(nullptr) , _rx_buf_data{} , _setupDone(false) , _fallback(false) , _vid(vid) , _pid(pid) , _device_disconnected_sem(nullptr) , _usb_lib_task_handle(nullptr) , _logger(nullptr) { _tx_buf_handle = xRingbufferCreateStatic(USBHOSTSERIAL_BUFFERSIZE, RINGBUF_TYPE_BYTEBUF, _tx_buf_mem, &_tx_buf_data); _rx_buf_handle = xRingbufferCreateStatic(USBHOSTSERIAL_BUFFERSIZE, RINGBUF_TYPE_BYTEBUF, _rx_buf_mem, &_rx_buf_data); if (!_tx_buf_handle || !_rx_buf_handle) { abort(); } } USBHostSerial::~USBHostSerial() { // TODO (bertmelis): implement destruction. vRingbufferDelete(_tx_buf_handle); vRingbufferDelete(_rx_buf_handle); abort(); } USBHostSerial::operator bool() const { if (xSemaphoreTake(_device_disconnected_sem, 0) == pdTRUE) { xSemaphoreGive(_device_disconnected_sem); return false; } return true; } bool USBHostSerial::begin(int baud, int stopbits, int parity, int databits) { if (!_setupDone) { _setupDone = true; _setup(); } _line_coding.dwDTERate = baud; _line_coding.bCharFormat = stopbits; _line_coding.bParityType = parity; _line_coding.bDataBits = databits; if (xTaskCreate(_USBHostSerial_task, "usb_dev_lib", 4096, this, 1, &_USBHostSerial_task_handle) == pdTRUE) { _log("USB setup done"); return true; } _log("USB setup failed"); return false; } void USBHostSerial::end() { // TODO (bertmelis): implement end, together with destruction. } std::size_t USBHostSerial::write(uint8_t data) { if (xRingbufferSend(_tx_buf_handle, &data, 1, pdMS_TO_TICKS(1)) == pdTRUE) { return 1; } return 0; } std::size_t USBHostSerial::write(const uint8_t *data, std::size_t len) { UBaseType_t numItemsWaiting; vRingbufferGetInfo(_tx_buf_handle, nullptr, nullptr, nullptr, nullptr, &numItemsWaiting); std::size_t maxSize = USBHOSTSERIAL_BUFFERSIZE - numItemsWaiting; if (maxSize < len) { char buf[40]; snprintf(buf, 40, "USB buf overflow: %u-%u", len, maxSize); _log(buf); } else { if (xRingbufferSend(_tx_buf_handle, data, len, pdMS_TO_TICKS(1)) == pdTRUE) { return len; } } return 0; } std::size_t USBHostSerial::available() { UBaseType_t numItemsWaiting; vRingbufferGetInfo(_rx_buf_handle, nullptr, nullptr, nullptr, nullptr, &numItemsWaiting); return numItemsWaiting; } uint8_t USBHostSerial::read() { std::size_t pxItemSize = 0; uint8_t retVal = 0; void* ret = xRingbufferReceiveUpTo(_rx_buf_handle, &pxItemSize, pdMS_TO_TICKS(1), 1); if (pxItemSize > 0) { retVal = *reinterpret_cast(ret); vRingbufferReturnItem(_rx_buf_handle, ret); } return retVal; } std::size_t USBHostSerial::read(uint8_t *dest, std::size_t size) { std::size_t retVal = 0; std::size_t pxItemSize = 0; while (size > retVal) { std::size_t remaining = size - retVal; if(remaining == 0){ break; } void *ret = xRingbufferReceiveUpTo(_rx_buf_handle, &pxItemSize, pdMS_TO_TICKS(1), remaining); if(!ret || pxItemSize == 0){ break; } std::memcpy(dest + retVal, ret, pxItemSize); retVal += pxItemSize; vRingbufferReturnItem(_rx_buf_handle, ret); } return retVal; } void USBHostSerial::setLogger(USBHostSerialLoggerFunc logger) { _logger = logger; } void USBHostSerial::_setup() { _device_disconnected_sem = xSemaphoreCreateBinary(); assert(_device_disconnected_sem); xSemaphoreGive(_device_disconnected_sem); // make available for first use // Install USB Host driver. Should only be called once in entire application _host_config.skip_phy_setup = false; _host_config.intr_flags = ESP_INTR_FLAG_LEVEL1; ESP_ERROR_CHECK(usb_host_install(&_host_config)); // Create a task that will handle USB library events BaseType_t task_created = xTaskCreate(_usb_lib_task, "usb_lib", 4096, this, 1, &_usb_lib_task_handle); assert(task_created == pdTRUE); ESP_ERROR_CHECK(cdc_acm_host_install(NULL)); // Register VCP drivers to VCP service VCP::register_driver(); VCP::register_driver(); VCP::register_driver(); } bool USBHostSerial::_handle_rx(const uint8_t *data, size_t data_len, void *arg) { std::size_t lenReceived = 0; while (lenReceived < data_len && xRingbufferSend(static_cast(arg)->_rx_buf_handle, &data[lenReceived], 1, pdMS_TO_TICKS(10)) == pdTRUE) { ++lenReceived; } if (lenReceived < data_len) { // log overflow warning static_cast(arg)->_log("USB rx buf overflow"); } return true; } void USBHostSerial::_handle_event(const cdc_acm_host_dev_event_data_t *event, void *user_ctx) { if (event->type == CDC_ACM_HOST_DEVICE_DISCONNECTED) { static_cast(user_ctx)->_log("USB device disconnected event"); xSemaphoreGive(static_cast(user_ctx)->_device_disconnected_sem); } } void USBHostSerial::_usb_lib_task(void *arg) { while (1) { uint32_t event_flags; usb_host_lib_handle_events(portMAX_DELAY, &event_flags); if (event_flags & USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS) { ESP_ERROR_CHECK(usb_host_device_free_all()); } } } void USBHostSerial::_USBHostSerial_task(void *arg) { USBHostSerial* thisInstance = static_cast(arg); esp_err_t err = ESP_OK; // reusable while (1) { // try to open USB VCP device const cdc_acm_host_device_config_t dev_config = { .connection_timeout_ms = 10, .out_buffer_size = USBHOSTSERIAL_BUFFERSIZE, .in_buffer_size = USBHOSTSERIAL_BUFFERSIZE, .event_cb = _handle_event, .data_cb = _handle_rx, .user_arg = thisInstance, }; cdc_acm_dev_hdl_t cdc_dev = NULL; auto vcp = std::unique_ptr(VCP::open(&dev_config)); if (vcp == nullptr) { thisInstance->_log("USB VCP open returned null, falling back to CDC"); // try to fallback to CDC err = cdc_acm_host_open(thisInstance->_vid, thisInstance->_pid, 0, &dev_config, &cdc_dev); if (err != ESP_OK) { vTaskDelay(pdMS_TO_TICKS(500)); continue; } thisInstance->_fallback = true; thisInstance->_log("USB CDC device opened"); } else { thisInstance->_fallback = false; thisInstance->_log("USB VCP device opened"); } // Clear any stale disconnect token from a previous cycle. while (xSemaphoreTake(thisInstance->_device_disconnected_sem, 0) == pdTRUE) { } // set line coding and control line states err = ESP_OK; if (thisInstance->_fallback) { err = cdc_acm_host_line_coding_set(cdc_dev, &(thisInstance->_line_coding)); if (err != ESP_OK) { thisInstance->_log("USB line coding set error"); if (cdc_dev != NULL) { cdc_acm_host_close(cdc_dev); cdc_dev = NULL; } vTaskDelay(pdMS_TO_TICKS(500)); continue; } thisInstance->_log("USB line coding set"); err = cdc_acm_host_set_control_line_state(cdc_dev, true, true); if (err == ESP_ERR_NOT_SUPPORTED) { thisInstance->_log("USB control line state not supported"); } else if (err != ESP_OK) { thisInstance->_log("USB control line state set error"); if (cdc_dev != NULL) { cdc_acm_host_close(cdc_dev); cdc_dev = NULL; } vTaskDelay(pdMS_TO_TICKS(500)); continue; } else { thisInstance->_log("USB control line state set"); } } else { err = vcp->line_coding_set(&(thisInstance->_line_coding)); if (err != ESP_OK) { thisInstance->_log("USB line coding set error"); vTaskDelay(pdMS_TO_TICKS(500)); continue; } thisInstance->_log("USB line coding set"); err = vcp->set_control_line_state(true, true); if (err == ESP_ERR_NOT_SUPPORTED) { thisInstance->_log("USB control line state not supported"); } else if (err != ESP_OK) { thisInstance->_log("USB control line state set error"); vTaskDelay(pdMS_TO_TICKS(500)); continue; } else { thisInstance->_log("USB control line state set"); } } // all set, enter loop to start sending while (1) { // check if still connected if (xSemaphoreTake(thisInstance->_device_disconnected_sem, 0) == pdTRUE) { break; } // check for data to send std::size_t pxItemSize = 0; void *data = xRingbufferReceiveUpTo(thisInstance->_tx_buf_handle, &pxItemSize, pdMS_TO_TICKS(10), USBHOSTSERIAL_BUFFERSIZE); if (data) { if (thisInstance->_fallback) { err = cdc_acm_host_data_tx_blocking(cdc_dev, (uint8_t*)data, pxItemSize, 1000); } else { err = vcp->tx_blocking((uint8_t*)data, pxItemSize, 1000); } if (err == ESP_OK) { vRingbufferReturnItem(thisInstance->_tx_buf_handle, data); } else { vRingbufferReturnItem(thisInstance->_tx_buf_handle, data); thisInstance->_log("Error writing to USB"); // Device likely reset/disconnected; break to cleanup/reopen path. if (err == ESP_ERR_INVALID_STATE || err == ESP_ERR_INVALID_RESPONSE || err == ESP_FAIL) { break; } } } taskYIELD(); } // Ensure CDC handles are released before attempting to reopen. if (thisInstance->_fallback && cdc_dev != NULL) { cdc_acm_host_close(cdc_dev); cdc_dev = NULL; } vTaskDelay(pdMS_TO_TICKS(500)); } } void USBHostSerial::_log(const char* msg) { if (_logger) { _logger(msg); } }