diff --git a/CODEOWNERS b/CODEOWNERS index 5013c0cd33..c7423e9eae 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -479,6 +479,8 @@ esphome/components/ufire_ise/* @pvizeli esphome/components/ultrasonic/* @OttoWinter esphome/components/update/* @jesserockz esphome/components/uponor_smatrix/* @kroimon +esphome/components/usb_host/* @clydebarrow +esphome/components/usb_uart/* @clydebarrow esphome/components/valve/* @esphome/core esphome/components/vbus/* @ssieb esphome/components/veml3235/* @kbx81 diff --git a/esphome/components/usb_host/__init__.py b/esphome/components/usb_host/__init__.py new file mode 100644 index 0000000000..b6ca779706 --- /dev/null +++ b/esphome/components/usb_host/__init__.py @@ -0,0 +1,64 @@ +import esphome.codegen as cg +from esphome.components.esp32 import ( + VARIANT_ESP32S2, + VARIANT_ESP32S3, + add_idf_sdkconfig_option, + only_on_variant, +) +import esphome.config_validation as cv +from esphome.const import CONF_ID +from esphome.cpp_types import Component + +AUTO_LOAD = ["bytebuffer"] +CODEOWNERS = ["@clydebarrow"] +DEPENDENCIES = ["esp32"] +usb_host_ns = cg.esphome_ns.namespace("usb_host") +USBHost = usb_host_ns.class_("USBHost", Component) +USBClient = usb_host_ns.class_("USBClient", Component) + +CONF_DEVICES = "devices" +CONF_VID = "vid" +CONF_PID = "pid" + + +def usb_device_schema(cls=USBClient, vid: int = None, pid: [int] = None) -> cv.Schema: + schema = cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(cls), + } + ) + if vid: + schema = schema.extend({cv.Optional(CONF_VID, default=vid): cv.hex_uint16_t}) + else: + schema = schema.extend({cv.Required(CONF_VID): cv.hex_uint16_t}) + if pid: + schema = schema.extend({cv.Optional(CONF_PID, default=pid): cv.hex_uint16_t}) + else: + schema = schema.extend({cv.Required(CONF_PID): cv.hex_uint16_t}) + return schema + + +CONFIG_SCHEMA = cv.All( + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(USBHost), + cv.Optional(CONF_DEVICES): cv.ensure_list(usb_device_schema()), + } + ), + cv.only_with_esp_idf, + only_on_variant(supported=[VARIANT_ESP32S2, VARIANT_ESP32S3]), +) + + +async def register_usb_client(config): + var = cg.new_Pvariable(config[CONF_ID], config[CONF_VID], config[CONF_PID]) + await cg.register_component(var, config) + return var + + +async def to_code(config): + add_idf_sdkconfig_option("CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE", 1024) + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + for device in config.get(CONF_DEVICES) or (): + await register_usb_client(device) diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h new file mode 100644 index 0000000000..c5466eb1f0 --- /dev/null +++ b/esphome/components/usb_host/usb_host.h @@ -0,0 +1,116 @@ +#pragma once + +// Should not be needed, but it's required to pass CI clang-tidy checks +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#include "esphome/core/component.h" +#include +#include "usb/usb_host.h" + +#include + +namespace esphome { +namespace usb_host { + +static const char *const TAG = "usb_host"; + +// constants for setup packet type +static const uint8_t USB_RECIP_DEVICE = 0; +static const uint8_t USB_RECIP_INTERFACE = 1; +static const uint8_t USB_RECIP_ENDPOINT = 2; +static const uint8_t USB_TYPE_STANDARD = 0 << 5; +static const uint8_t USB_TYPE_CLASS = 1 << 5; +static const uint8_t USB_TYPE_VENDOR = 2 << 5; +static const uint8_t USB_DIR_MASK = 1 << 7; +static const uint8_t USB_DIR_IN = 1 << 7; +static const uint8_t USB_DIR_OUT = 0; +static const size_t SETUP_PACKET_SIZE = 8; + +static const size_t MAX_REQUESTS = 16; // maximum number of outstanding requests possible. + +// used to report a transfer status +struct TransferStatus { + bool success; + uint16_t error_code; + uint8_t *data; + size_t data_len; + uint8_t endpoint; + void *user_data; +}; + +using transfer_cb_t = std::function; + +class USBClient; + +// struct used to capture all data needed for a transfer +struct TransferRequest { + usb_transfer_t *transfer; + transfer_cb_t callback; + TransferStatus status; + USBClient *client; +}; + +// callback function type. + +enum ClientState { + USB_CLIENT_INIT = 0, + USB_CLIENT_OPEN, + USB_CLIENT_CLOSE, + USB_CLIENT_GET_DESC, + USB_CLIENT_GET_INFO, + USB_CLIENT_CONNECTED, +}; +class USBClient : public Component { + friend class USBHost; + + public: + USBClient(uint16_t vid, uint16_t pid) : vid_(vid), pid_(pid) { init_pool(); } + + void init_pool() { + this->trq_pool_.clear(); + for (size_t i = 0; i != MAX_REQUESTS; i++) + this->trq_pool_.push_back(&this->requests_[i]); + } + void setup() override; + void loop() override; + // setup must happen after the host bus has been setup + float get_setup_priority() const override { return setup_priority::IO; } + void on_opened(uint8_t addr); + void on_removed(usb_device_handle_t handle); + void control_transfer_callback(const usb_transfer_t *xfer) const; + void transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length); + void transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length); + void dump_config() override; + void release_trq(TransferRequest *trq); + bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback, + const std::vector &data = {}); + + protected: + bool register_(); + TransferRequest *get_trq_(); + virtual void disconnect(); + virtual void on_connected() {} + virtual void on_disconnected() { this->init_pool(); } + + usb_host_client_handle_t handle_{}; + usb_device_handle_t device_handle_{}; + int device_addr_{-1}; + int state_{USB_CLIENT_INIT}; + uint16_t vid_{}; + uint16_t pid_{}; + std::list trq_pool_{}; + TransferRequest requests_[MAX_REQUESTS]{}; +}; +class USBHost : public Component { + public: + float get_setup_priority() const override { return setup_priority::BUS; } + void loop() override; + void setup() override; + + protected: + std::vector clients_{}; +}; + +} // namespace usb_host +} // namespace esphome + +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp new file mode 100644 index 0000000000..09422f570f --- /dev/null +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -0,0 +1,392 @@ +// Should not be needed, but it's required to pass CI clang-tidy checks +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#include "usb_host.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include "esphome/components/bytebuffer/bytebuffer.h" + +#include +#include +namespace esphome { +namespace usb_host { + +#pragma GCC diagnostic ignored "-Wparentheses" + +using namespace bytebuffer; + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE +static void print_ep_desc(const usb_ep_desc_t *ep_desc) { + const char *ep_type_str; + int type = ep_desc->bmAttributes & USB_BM_ATTRIBUTES_XFERTYPE_MASK; + + switch (type) { + case USB_BM_ATTRIBUTES_XFER_CONTROL: + ep_type_str = "CTRL"; + break; + case USB_BM_ATTRIBUTES_XFER_ISOC: + ep_type_str = "ISOC"; + break; + case USB_BM_ATTRIBUTES_XFER_BULK: + ep_type_str = "BULK"; + break; + case USB_BM_ATTRIBUTES_XFER_INT: + ep_type_str = "INT"; + break; + default: + ep_type_str = NULL; + break; + } + + ESP_LOGV(TAG, "\t\t*** Endpoint descriptor ***"); + ESP_LOGV(TAG, "\t\tbLength %d", ep_desc->bLength); + ESP_LOGV(TAG, "\t\tbDescriptorType %d", ep_desc->bDescriptorType); + ESP_LOGV(TAG, "\t\tbEndpointAddress 0x%x\tEP %d %s", ep_desc->bEndpointAddress, USB_EP_DESC_GET_EP_NUM(ep_desc), + USB_EP_DESC_GET_EP_DIR(ep_desc) ? "IN" : "OUT"); + ESP_LOGV(TAG, "\t\tbmAttributes 0x%x\t%s", ep_desc->bmAttributes, ep_type_str); + ESP_LOGV(TAG, "\t\twMaxPacketSize %d", ep_desc->wMaxPacketSize); + ESP_LOGV(TAG, "\t\tbInterval %d", ep_desc->bInterval); +} + +static void usbh_print_intf_desc(const usb_intf_desc_t *intf_desc) { + ESP_LOGV(TAG, "\t*** Interface descriptor ***"); + ESP_LOGV(TAG, "\tbLength %d", intf_desc->bLength); + ESP_LOGV(TAG, "\tbDescriptorType %d", intf_desc->bDescriptorType); + ESP_LOGV(TAG, "\tbInterfaceNumber %d", intf_desc->bInterfaceNumber); + ESP_LOGV(TAG, "\tbAlternateSetting %d", intf_desc->bAlternateSetting); + ESP_LOGV(TAG, "\tbNumEndpoints %d", intf_desc->bNumEndpoints); + ESP_LOGV(TAG, "\tbInterfaceClass 0x%x", intf_desc->bInterfaceProtocol); + ESP_LOGV(TAG, "\tiInterface %d", intf_desc->iInterface); +} + +static void usbh_print_cfg_desc(const usb_config_desc_t *cfg_desc) { + ESP_LOGV(TAG, "*** Configuration descriptor ***"); + ESP_LOGV(TAG, "bLength %d", cfg_desc->bLength); + ESP_LOGV(TAG, "bDescriptorType %d", cfg_desc->bDescriptorType); + ESP_LOGV(TAG, "wTotalLength %d", cfg_desc->wTotalLength); + ESP_LOGV(TAG, "bNumInterfaces %d", cfg_desc->bNumInterfaces); + ESP_LOGV(TAG, "bConfigurationValue %d", cfg_desc->bConfigurationValue); + ESP_LOGV(TAG, "iConfiguration %d", cfg_desc->iConfiguration); + ESP_LOGV(TAG, "bmAttributes 0x%x", cfg_desc->bmAttributes); + ESP_LOGV(TAG, "bMaxPower %dmA", cfg_desc->bMaxPower * 2); +} + +void usb_client_print_device_descriptor(const usb_device_desc_t *devc_desc) { + if (devc_desc == NULL) { + return; + } + + ESP_LOGV(TAG, "*** Device descriptor ***"); + ESP_LOGV(TAG, "bLength %d", devc_desc->bLength); + ESP_LOGV(TAG, "bDescriptorType %d", devc_desc->bDescriptorType); + ESP_LOGV(TAG, "bcdUSB %d.%d0", ((devc_desc->bcdUSB >> 8) & 0xF), ((devc_desc->bcdUSB >> 4) & 0xF)); + ESP_LOGV(TAG, "bDeviceClass 0x%x", devc_desc->bDeviceClass); + ESP_LOGV(TAG, "bDeviceSubClass 0x%x", devc_desc->bDeviceSubClass); + ESP_LOGV(TAG, "bDeviceProtocol 0x%x", devc_desc->bDeviceProtocol); + ESP_LOGV(TAG, "bMaxPacketSize0 %d", devc_desc->bMaxPacketSize0); + ESP_LOGV(TAG, "idVendor 0x%x", devc_desc->idVendor); + ESP_LOGV(TAG, "idProduct 0x%x", devc_desc->idProduct); + ESP_LOGV(TAG, "bcdDevice %d.%d0", ((devc_desc->bcdDevice >> 8) & 0xF), ((devc_desc->bcdDevice >> 4) & 0xF)); + ESP_LOGV(TAG, "iManufacturer %d", devc_desc->iManufacturer); + ESP_LOGV(TAG, "iProduct %d", devc_desc->iProduct); + ESP_LOGV(TAG, "iSerialNumber %d", devc_desc->iSerialNumber); + ESP_LOGV(TAG, "bNumConfigurations %d", devc_desc->bNumConfigurations); +} + +void usb_client_print_config_descriptor(const usb_config_desc_t *cfg_desc, + print_class_descriptor_cb class_specific_cb) { + if (cfg_desc == nullptr) { + return; + } + + int offset = 0; + uint16_t w_total_length = cfg_desc->wTotalLength; + const usb_standard_desc_t *next_desc = (const usb_standard_desc_t *) cfg_desc; + + do { + switch (next_desc->bDescriptorType) { + case USB_W_VALUE_DT_CONFIG: + usbh_print_cfg_desc((const usb_config_desc_t *) next_desc); + break; + case USB_W_VALUE_DT_INTERFACE: + usbh_print_intf_desc((const usb_intf_desc_t *) next_desc); + break; + case USB_W_VALUE_DT_ENDPOINT: + print_ep_desc((const usb_ep_desc_t *) next_desc); + break; + default: + if (class_specific_cb) { + class_specific_cb(next_desc); + } + break; + } + + next_desc = usb_parse_next_descriptor(next_desc, w_total_length, &offset); + + } while (next_desc != NULL); +} +#endif +static std::string get_descriptor_string(const usb_str_desc_t *desc) { + char buffer[256]; + if (desc == nullptr) + return "(unknown)"; + char *p = buffer; + for (size_t i = 0; i != desc->bLength / 2; i++) { + auto c = desc->wData[i]; + if (c < 0x100) + *p++ = static_cast(c); + } + *p = '\0'; + return {buffer}; +} + +static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void *ptr) { + auto *client = static_cast(ptr); + switch (event_msg->event) { + case USB_HOST_CLIENT_EVENT_NEW_DEV: { + auto addr = event_msg->new_dev.address; + ESP_LOGD(TAG, "New device %d", event_msg->new_dev.address); + client->on_opened(addr); + break; + } + case USB_HOST_CLIENT_EVENT_DEV_GONE: { + client->on_removed(event_msg->dev_gone.dev_hdl); + ESP_LOGD(TAG, "Device gone %d", event_msg->new_dev.address); + break; + } + default: + ESP_LOGD(TAG, "Unknown event %d", event_msg->event); + break; + } +} +void USBClient::setup() { + usb_host_client_config_t config{.is_synchronous = false, + .max_num_event_msg = 5, + .async = {.client_event_callback = client_event_cb, .callback_arg = this}}; + auto err = usb_host_client_register(&config, &this->handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "client register failed: %s", esp_err_to_name(err)); + this->status_set_error("Client register failed"); + this->mark_failed(); + return; + } + for (auto trq : this->trq_pool_) { + usb_host_transfer_alloc(64, 0, &trq->transfer); + trq->client = this; + } + ESP_LOGCONFIG(TAG, "client setup complete"); +} + +void USBClient::loop() { + switch (this->state_) { + case USB_CLIENT_OPEN: { + int err; + ESP_LOGD(TAG, "Open device %d", this->device_addr_); + err = usb_host_device_open(this->handle_, this->device_addr_, &this->device_handle_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Device open failed: %s", esp_err_to_name(err)); + this->state_ = USB_CLIENT_INIT; + break; + } + ESP_LOGD(TAG, "Get descriptor device %d", this->device_addr_); + const usb_device_desc_t *desc; + err = usb_host_get_device_descriptor(this->device_handle_, &desc); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Device get_desc failed: %s", esp_err_to_name(err)); + this->disconnect(); + } else { + ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct); + if (desc->idVendor == this->vid_ && desc->idProduct == this->pid_ || this->vid_ == 0 && this->pid_ == 0) { + usb_device_info_t dev_info; + if ((err = usb_host_device_info(this->device_handle_, &dev_info)) != ESP_OK) { + ESP_LOGW(TAG, "Device info failed: %s", esp_err_to_name(err)); + this->disconnect(); + break; + } + this->state_ = USB_CLIENT_CONNECTED; + ESP_LOGD(TAG, "Device connected: Manuf: %s; Prod: %s; Serial: %s", + get_descriptor_string(dev_info.str_desc_manufacturer).c_str(), + get_descriptor_string(dev_info.str_desc_product).c_str(), + get_descriptor_string(dev_info.str_desc_serial_num).c_str()); + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + const usb_device_desc_t *device_desc; + err = usb_host_get_device_descriptor(this->device_handle_, &device_desc); + if (err == ESP_OK) + usb_client_print_device_descriptor(device_desc); + const usb_config_desc_t *config_desc; + err = usb_host_get_active_config_descriptor(this->device_handle_, &config_desc); + if (err == ESP_OK) + usb_client_print_config_descriptor(config_desc, nullptr); +#endif + this->on_connected(); + } else { + ESP_LOGD(TAG, "Not our device, closing"); + this->disconnect(); + } + } + break; + } + + default: + usb_host_client_handle_events(this->handle_, 0); + break; + } +} + +void USBClient::on_opened(uint8_t addr) { + if (this->state_ == USB_CLIENT_INIT) { + this->device_addr_ = addr; + this->state_ = USB_CLIENT_OPEN; + } +} +void USBClient::on_removed(usb_device_handle_t handle) { + if (this->device_handle_ == handle) { + this->disconnect(); + } +} + +static void control_callback(const usb_transfer_t *xfer) { + auto *trq = static_cast(xfer->context); + trq->status.error_code = xfer->status; + trq->status.success = xfer->status == USB_TRANSFER_STATUS_COMPLETED; + trq->status.endpoint = xfer->bEndpointAddress; + trq->status.data = xfer->data_buffer; + trq->status.data_len = xfer->actual_num_bytes; + if (trq->callback != nullptr) + trq->callback(trq->status); + trq->client->release_trq(trq); +} + +TransferRequest *USBClient::get_trq_() { + if (this->trq_pool_.empty()) { + ESP_LOGE(TAG, "Too many requests queued"); + return nullptr; + } + auto *trq = this->trq_pool_.front(); + this->trq_pool_.pop_front(); + trq->client = this; + trq->transfer->context = trq; + trq->transfer->device_handle = this->device_handle_; + return trq; +} +void USBClient::disconnect() { + this->on_disconnected(); + auto err = usb_host_device_close(this->handle_, this->device_handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Device close failed: %s", esp_err_to_name(err)); + } + this->state_ = USB_CLIENT_INIT; + this->device_handle_ = nullptr; + this->device_addr_ = -1; +} + +bool USBClient::control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, + const transfer_cb_t &callback, const std::vector &data) { + auto *trq = this->get_trq_(); + if (trq == nullptr) + return false; + auto length = data.size(); + if (length > sizeof(trq->transfer->data_buffer_size) - SETUP_PACKET_SIZE) { + ESP_LOGE(TAG, "Control transfer data size too large: %u > %u", length, + sizeof(trq->transfer->data_buffer_size) - sizeof(usb_setup_packet_t)); + this->release_trq(trq); + return false; + } + auto control_packet = ByteBuffer(SETUP_PACKET_SIZE, LITTLE); + control_packet.put_uint8(type); + control_packet.put_uint8(request); + control_packet.put_uint16(value); + control_packet.put_uint16(index); + control_packet.put_uint16(length); + memcpy(trq->transfer->data_buffer, control_packet.get_data().data(), SETUP_PACKET_SIZE); + if (length != 0 && !(type & USB_DIR_IN)) { + memcpy(trq->transfer->data_buffer + SETUP_PACKET_SIZE, data.data(), length); + } + trq->callback = callback; + trq->transfer->bEndpointAddress = type & USB_DIR_MASK; + trq->transfer->num_bytes = static_cast(length + SETUP_PACKET_SIZE); + trq->transfer->callback = reinterpret_cast(control_callback); + auto err = usb_host_transfer_submit_control(this->handle_, trq->transfer); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to submit control transfer, err=%s", esp_err_to_name(err)); + this->release_trq(trq); + return false; + } + return true; +} + +static void transfer_callback(usb_transfer_t *xfer) { + auto *trq = static_cast(xfer->context); + trq->status.error_code = xfer->status; + trq->status.success = xfer->status == USB_TRANSFER_STATUS_COMPLETED; + trq->status.endpoint = xfer->bEndpointAddress; + trq->status.data = xfer->data_buffer; + trq->status.data_len = xfer->actual_num_bytes; + if (trq->callback != nullptr) + trq->callback(trq->status); + trq->client->release_trq(trq); +} +/** + * Performs a transfer input operation. + * + * @param ep_address The endpoint address. + * @param callback The callback function to be called when the transfer is complete. + * @param length The length of the data to be transferred. + * + * @throws None. + */ +void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) { + auto trq = this->get_trq_(); + if (trq == nullptr) { + ESP_LOGE(TAG, "Too many requests queued"); + return; + } + trq->callback = callback; + trq->transfer->callback = transfer_callback; + trq->transfer->bEndpointAddress = ep_address | USB_DIR_IN; + trq->transfer->num_bytes = length; + auto err = usb_host_transfer_submit(trq->transfer); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); + this->release_trq(trq); + this->disconnect(); + } +} + +/** + * Performs an output transfer operation. + * + * @param ep_address The endpoint address. + * @param callback The callback function to be called when the transfer is complete. + * @param data The data to be transferred. + * @param length The length of the data to be transferred. + * + * @throws None. + */ +void USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length) { + auto trq = this->get_trq_(); + if (trq == nullptr) { + ESP_LOGE(TAG, "Too many requests queued"); + return; + } + trq->callback = callback; + trq->transfer->callback = transfer_callback; + trq->transfer->bEndpointAddress = ep_address | USB_DIR_OUT; + trq->transfer->num_bytes = length; + memcpy(trq->transfer->data_buffer, data, length); + auto err = usb_host_transfer_submit(trq->transfer); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); + this->release_trq(trq); + } +} +void USBClient::dump_config() { + ESP_LOGCONFIG(TAG, "USBClient"); + ESP_LOGCONFIG(TAG, " Vendor id %04X", this->vid_); + ESP_LOGCONFIG(TAG, " Product id %04X", this->pid_); +} +void USBClient::release_trq(TransferRequest *trq) { this->trq_pool_.push_back(trq); } + +} // namespace usb_host +} // namespace esphome +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_host/usb_host_component.cpp b/esphome/components/usb_host/usb_host_component.cpp new file mode 100644 index 0000000000..63a2ab77cc --- /dev/null +++ b/esphome/components/usb_host/usb_host_component.cpp @@ -0,0 +1,35 @@ +// Should not be needed, but it's required to pass CI clang-tidy checks +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#include "usb_host.h" +#include +#include "esphome/core/log.h" + +namespace esphome { +namespace usb_host { + +void USBHost::setup() { + ESP_LOGCONFIG(TAG, "Setup starts"); + usb_host_config_t config{}; + + if (usb_host_install(&config) != ESP_OK) { + this->status_set_error("usb_host_install failed"); + this->mark_failed(); + return; + } +} +void USBHost::loop() { + int err; + uint32_t event_flags; + err = usb_host_lib_handle_events(0, &event_flags); + if (err != ESP_OK && err != ESP_ERR_TIMEOUT) { + ESP_LOGD(TAG, "lib_handle_events failed failed: %s", esp_err_to_name(err)); + } + if (event_flags != 0) { + ESP_LOGD(TAG, "Event flags %" PRIu32 "X", event_flags); + } +} + +} // namespace usb_host +} // namespace esphome + +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py new file mode 100644 index 0000000000..6999b1b955 --- /dev/null +++ b/esphome/components/usb_uart/__init__.py @@ -0,0 +1,134 @@ +import esphome.codegen as cg +from esphome.components.uart import ( + CONF_DATA_BITS, + CONF_PARITY, + CONF_STOP_BITS, + UARTComponent, +) +from esphome.components.usb_host import register_usb_client, usb_device_schema +import esphome.config_validation as cv +from esphome.const import ( + CONF_BAUD_RATE, + CONF_BUFFER_SIZE, + CONF_CHANNELS, + CONF_DEBUG, + CONF_DUMMY_RECEIVER, + CONF_ID, +) +from esphome.cpp_types import Component + +AUTO_LOAD = ["uart", "usb_host", "bytebuffer"] +CODEOWNERS = ["@clydebarrow"] + +usb_uart_ns = cg.esphome_ns.namespace("usb_uart") +USBUartComponent = usb_uart_ns.class_("USBUartComponent", Component) +USBUartChannel = usb_uart_ns.class_("USBUartChannel", UARTComponent) + + +UARTParityOptions = usb_uart_ns.enum("UARTParityOptions") +UART_PARITY_OPTIONS = { + "NONE": UARTParityOptions.UART_CONFIG_PARITY_NONE, + "EVEN": UARTParityOptions.UART_CONFIG_PARITY_EVEN, + "ODD": UARTParityOptions.UART_CONFIG_PARITY_ODD, + "MARK": UARTParityOptions.UART_CONFIG_PARITY_MARK, + "SPACE": UARTParityOptions.UART_CONFIG_PARITY_SPACE, +} + +UARTStopBitsOptions = usb_uart_ns.enum("UARTStopBitsOptions") +UART_STOP_BITS_OPTIONS = { + "1": UARTStopBitsOptions.UART_CONFIG_STOP_BITS_1, + "1.5": UARTStopBitsOptions.UART_CONFIG_STOP_BITS_1_5, + "2": UARTStopBitsOptions.UART_CONFIG_STOP_BITS_2, +} + +DEFAULT_BAUD_RATE = 9600 + + +class Type: + def __init__(self, name, vid, pid, cls, max_channels=1, baud_rate_required=True): + self.name = name + cls = cls or name + self.vid = vid + self.pid = pid + self.cls = usb_uart_ns.class_(f"USBUartType{cls}", USBUartComponent) + self.max_channels = max_channels + self.baud_rate_required = baud_rate_required + + +uart_types = ( + Type("CH34X", 0x1A86, 0x55D5, "CH34X", 3), + Type("CH340", 0x1A86, 0x7523, "CH34X", 1), + Type("ESP_JTAG", 0x303A, 0x1001, "CdcAcm", 1, baud_rate_required=False), + Type("STM32_VCP", 0x0483, 0x5740, "CdcAcm", 1, baud_rate_required=False), + Type("CDC_ACM", 0, 0, "CdcAcm", 1, baud_rate_required=False), + Type("CP210X", 0x10C4, 0xEA60, "CP210X", 3), +) + + +def channel_schema(channels, baud_rate_required): + return cv.Schema( + { + cv.Required(CONF_CHANNELS): cv.All( + cv.ensure_list( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(USBUartChannel), + cv.Optional(CONF_BUFFER_SIZE, default=256): cv.int_range( + min=64, max=8192 + ), + ( + cv.Required(CONF_BAUD_RATE) + if baud_rate_required + else cv.Optional( + CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE + ) + ): cv.int_range(min=300, max=1000000), + cv.Optional(CONF_STOP_BITS, default="1"): cv.enum( + UART_STOP_BITS_OPTIONS, upper=True + ), + cv.Optional(CONF_PARITY, default="NONE"): cv.enum( + UART_PARITY_OPTIONS, upper=True + ), + cv.Optional(CONF_DATA_BITS, default=8): cv.int_range( + min=5, max=8 + ), + cv.Optional(CONF_DUMMY_RECEIVER, default=False): cv.boolean, + cv.Optional(CONF_DEBUG, default=False): cv.boolean, + } + ) + ), + cv.Length(max=channels), + ) + } + ) + + +CONFIG_SCHEMA = cv.ensure_list( + cv.typed_schema( + { + it.name: usb_device_schema(it.cls, it.vid, it.pid).extend( + channel_schema(it.max_channels, it.baud_rate_required) + ) + for it in uart_types + }, + upper=True, + ) +) + + +async def to_code(config): + for device in config: + var = await register_usb_client(device) + for index, channel in enumerate(device[CONF_CHANNELS]): + chvar = cg.new_Pvariable(channel[CONF_ID], index, channel[CONF_BUFFER_SIZE]) + await cg.register_parented(chvar, var) + cg.add(chvar.set_rx_buffer_size(channel[CONF_BUFFER_SIZE])) + cg.add(chvar.set_stop_bits(channel[CONF_STOP_BITS])) + cg.add(chvar.set_data_bits(channel[CONF_DATA_BITS])) + cg.add(chvar.set_parity(channel[CONF_PARITY])) + cg.add(chvar.set_baud_rate(channel[CONF_BAUD_RATE])) + cg.add(chvar.set_dummy_receiver(channel[CONF_DUMMY_RECEIVER])) + cg.add(chvar.set_debug(channel[CONF_DEBUG])) + cg.add(var.add_channel(chvar)) + if channel[CONF_DEBUG]: + cg.add_define("USE_UART_DEBUGGER") diff --git a/esphome/components/usb_uart/ch34x.cpp b/esphome/components/usb_uart/ch34x.cpp new file mode 100644 index 0000000000..74e7933824 --- /dev/null +++ b/esphome/components/usb_uart/ch34x.cpp @@ -0,0 +1,80 @@ +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#include "usb_uart.h" +#include "usb/usb_host.h" +#include "esphome/core/log.h" + +#include "esphome/components/bytebuffer/bytebuffer.h" + +namespace esphome { +namespace usb_uart { + +using namespace bytebuffer; +/** + * CH34x + */ + +void USBUartTypeCH34X::enable_channels() { + // enable the channels + for (auto channel : this->channels_) { + if (!channel->initialised_) + continue; + usb_host::transfer_cb_t callback = [=](const usb_host::TransferStatus &status) { + if (!status.success) { + ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code)); + channel->initialised_ = false; + } + }; + + uint8_t divisor = 7; + uint32_t clk = 12000000; + + auto baud_rate = channel->baud_rate_; + if (baud_rate < 256000) { + if (baud_rate > 6000000 / 255) { + divisor = 3; + clk = 6000000; + } else if (baud_rate > 750000 / 255) { + divisor = 2; + clk = 750000; + } else if (baud_rate > 93750 / 255) { + divisor = 1; + clk = 93750; + } else { + divisor = 0; + clk = 11719; + } + } + ESP_LOGV(TAG, "baud_rate: %" PRIu32 ", divisor: %d, clk: %" PRIu32, baud_rate, divisor, clk); + auto factor = static_cast(clk / baud_rate); + if (factor == 0 || factor == 0xFF) { + ESP_LOGE(TAG, "Invalid baud rate %" PRIu32, baud_rate); + channel->initialised_ = false; + continue; + } + if ((clk / factor - baud_rate) > (baud_rate - clk / (factor + 1))) + factor++; + factor = 256 - factor; + + uint16_t value = 0xC0; + if (channel->stop_bits_ == UART_CONFIG_STOP_BITS_2) + value |= 4; + switch (channel->parity_) { + case UART_CONFIG_PARITY_NONE: + break; + default: + value |= 8 | ((channel->parity_ - 1) << 4); + break; + } + value |= channel->data_bits_ - 5; + value <<= 8; + value |= 0x8C; + uint8_t cmd = 0xA1 + channel->index_; + if (channel->index_ >= 2) + cmd += 0xE; + this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, cmd, value, (factor << 8) | divisor, callback); + } + USBUartTypeCdcAcm::enable_channels(); +} +} // namespace usb_uart +} // namespace esphome +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_uart/cp210x.cpp b/esphome/components/usb_uart/cp210x.cpp new file mode 100644 index 0000000000..267385d1bd --- /dev/null +++ b/esphome/components/usb_uart/cp210x.cpp @@ -0,0 +1,126 @@ +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#include "usb_uart.h" +#include "usb/usb_host.h" +#include "esphome/core/log.h" + +#include "esphome/components/bytebuffer/bytebuffer.h" + +namespace esphome { +namespace usb_uart { + +using namespace bytebuffer; +/** + * Silabs CP210x Commands + */ + +static constexpr uint8_t IFC_ENABLE = 0x00; // Enable or disable the interface. +static constexpr uint8_t SET_BAUDDIV = 0x01; // Set the baud rate divisor. +static constexpr uint8_t GET_BAUDDIV = 0x02; // Get the baud rate divisor. +static constexpr uint8_t SET_LINE_CTL = 0x03; // Set the line control. +static constexpr uint8_t GET_LINE_CTL = 0x04; // Get the line control. +static constexpr uint8_t SET_BREAK = 0x05; // Set a BREAK. +static constexpr uint8_t IMM_CHAR = 0x06; // Send character out of order. +static constexpr uint8_t SET_MHS = 0x07; // Set modem handshaking. +static constexpr uint8_t GET_MDMSTS = 0x08; // Get modem status. +static constexpr uint8_t SET_XON = 0x09; // Emulate XON. +static constexpr uint8_t SET_XOFF = 0x0A; // Emulate XOFF. +static constexpr uint8_t SET_EVENTMASK = 0x0B; // Set the event mask. +static constexpr uint8_t GET_EVENTMASK = 0x0C; // Get the event mask. +static constexpr uint8_t GET_EVENTSTATE = 0x16; // Get the event state. +static constexpr uint8_t SET_RECEIVE = 0x17; // Set receiver max timeout. +static constexpr uint8_t GET_RECEIVE = 0x18; // Get receiver max timeout. +static constexpr uint8_t SET_CHAR = 0x0D; // Set special character individually. +static constexpr uint8_t GET_CHARS = 0x0E; // Get special characters. +static constexpr uint8_t GET_PROPS = 0x0F; // Get properties. +static constexpr uint8_t GET_COMM_STATUS = 0x10; // Get the serial status. +static constexpr uint8_t RESET = 0x11; // Reset. +static constexpr uint8_t PURGE = 0x12; // Purge. +static constexpr uint8_t SET_FLOW = 0x13; // Set flow control. +static constexpr uint8_t GET_FLOW = 0x14; // Get flow control. +static constexpr uint8_t EMBED_EVENTS = 0x15; // Control embedding of events in the data stream. +static constexpr uint8_t GET_BAUDRATE = 0x1D; // Get the baud rate. +static constexpr uint8_t SET_BAUDRATE = 0x1E; // Set the baud rate. +static constexpr uint8_t SET_CHARS = 0x19; // Set special characters. +static constexpr uint8_t VENDOR_SPECIFIC = 0xFF; // Vendor specific command. + +std::vector USBUartTypeCP210X::parse_descriptors_(usb_device_handle_t dev_hdl) { + const usb_config_desc_t *config_desc; + const usb_device_desc_t *device_desc; + int conf_offset = 0, ep_offset; + std::vector cdc_devs{}; + + // Get required descriptors + if (usb_host_get_device_descriptor(dev_hdl, &device_desc) != ESP_OK) { + ESP_LOGE(TAG, "get_device_descriptor failed"); + return {}; + } + if (usb_host_get_active_config_descriptor(dev_hdl, &config_desc) != ESP_OK) { + ESP_LOGE(TAG, "get_active_config_descriptor failed"); + return {}; + } + ESP_LOGD(TAG, "bDeviceClass: %u, bDeviceSubClass: %u", device_desc->bDeviceClass, device_desc->bDeviceSubClass); + ESP_LOGD(TAG, "bNumInterfaces: %u", config_desc->bNumInterfaces); + if (device_desc->bDeviceClass != 0) { + ESP_LOGE(TAG, "bDeviceClass != 0"); + return {}; + } + + for (uint8_t i = 0; i != config_desc->bNumInterfaces; i++) { + auto data_desc = usb_parse_interface_descriptor(config_desc, 0, 0, &conf_offset); + if (!data_desc) { + ESP_LOGE(TAG, "data_desc: usb_parse_interface_descriptor failed"); + break; + } + if (data_desc->bNumEndpoints != 2 || data_desc->bInterfaceClass != USB_CLASS_VENDOR_SPEC) { + ESP_LOGE(TAG, "data_desc: bInterfaceClass == %u, bInterfaceSubClass == %u, bNumEndpoints == %u", + data_desc->bInterfaceClass, data_desc->bInterfaceSubClass, data_desc->bNumEndpoints); + continue; + } + ep_offset = conf_offset; + auto out_ep = usb_parse_endpoint_descriptor_by_index(data_desc, 0, config_desc->wTotalLength, &ep_offset); + if (!out_ep) { + ESP_LOGE(TAG, "out_ep: usb_parse_endpoint_descriptor_by_index failed"); + continue; + } + ep_offset = conf_offset; + auto in_ep = usb_parse_endpoint_descriptor_by_index(data_desc, 1, config_desc->wTotalLength, &ep_offset); + if (!in_ep) { + ESP_LOGE(TAG, "in_ep: usb_parse_endpoint_descriptor_by_index failed"); + continue; + } + if (in_ep->bEndpointAddress & usb_host::USB_DIR_IN) { + cdc_devs.push_back({CdcEps{nullptr, in_ep, out_ep, data_desc->bInterfaceNumber}}); + } else { + cdc_devs.push_back({CdcEps{nullptr, out_ep, in_ep, data_desc->bInterfaceNumber}}); + } + } + return cdc_devs; +} + +void USBUartTypeCP210X::enable_channels() { + // enable the channels + for (auto channel : this->channels_) { + if (!channel->initialised_) + continue; + usb_host::transfer_cb_t callback = [=](const usb_host::TransferStatus &status) { + if (!status.success) { + ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code)); + channel->initialised_ = false; + } + }; + this->control_transfer(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, IFC_ENABLE, 1, channel->index_, callback); + uint16_t line_control = channel->stop_bits_; + line_control |= static_cast(channel->parity_) << 4; + line_control |= channel->data_bits_ << 8; + ESP_LOGD(TAG, "Line control value 0x%X", line_control); + this->control_transfer(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, SET_LINE_CTL, line_control, channel->index_, + callback); + auto baud = ByteBuffer::wrap(channel->baud_rate_, LITTLE); + this->control_transfer(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, SET_BAUDRATE, 0, channel->index_, callback, + baud.get_data()); + } + USBUartTypeCdcAcm::enable_channels(); +} +} // namespace usb_uart +} // namespace esphome +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp new file mode 100644 index 0000000000..30a45f9cb0 --- /dev/null +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -0,0 +1,325 @@ +// Should not be needed, but it's required to pass CI clang-tidy checks +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#include "usb_uart.h" +#include "esphome/core/log.h" +#include "esphome/components/uart/uart_debugger.h" + +#include + +namespace esphome { +namespace usb_uart { + +/** + * + * Given a configuration, look for the required interfaces defining a CDC-ACM device + * @param config_desc The configuration descriptor + * @param intf_idx The index of the interface to be examined + * @return + */ +static optional get_cdc(const usb_config_desc_t *config_desc, uint8_t intf_idx) { + int conf_offset, ep_offset; + const usb_ep_desc_t *notify_ep{}, *in_ep{}, *out_ep{}; + uint8_t interface_number = 0; + // look for an interface with one interrupt endpoint (notify), and an interface with two bulk endpoints (data in/out) + for (;;) { + auto intf_desc = usb_parse_interface_descriptor(config_desc, intf_idx++, 0, &conf_offset); + if (!intf_desc) { + ESP_LOGE(TAG, "usb_parse_interface_descriptor failed"); + return nullopt; + } + if (intf_desc->bNumEndpoints == 1) { + ep_offset = conf_offset; + notify_ep = usb_parse_endpoint_descriptor_by_index(intf_desc, 0, config_desc->wTotalLength, &ep_offset); + if (!notify_ep) { + ESP_LOGE(TAG, "notify_ep: usb_parse_endpoint_descriptor_by_index failed"); + return nullopt; + } + if (notify_ep->bmAttributes != USB_BM_ATTRIBUTES_XFER_INT) + notify_ep = nullptr; + } else if (USB_CLASS_CDC_DATA && intf_desc->bNumEndpoints == 2) { + interface_number = intf_desc->bInterfaceNumber; + ep_offset = conf_offset; + out_ep = usb_parse_endpoint_descriptor_by_index(intf_desc, 0, config_desc->wTotalLength, &ep_offset); + if (!out_ep) { + ESP_LOGE(TAG, "out_ep: usb_parse_endpoint_descriptor_by_index failed"); + return nullopt; + } + if (out_ep->bmAttributes != USB_BM_ATTRIBUTES_XFER_BULK) + out_ep = nullptr; + ep_offset = conf_offset; + in_ep = usb_parse_endpoint_descriptor_by_index(intf_desc, 1, config_desc->wTotalLength, &ep_offset); + if (!in_ep) { + ESP_LOGE(TAG, "in_ep: usb_parse_endpoint_descriptor_by_index failed"); + return nullopt; + } + if (in_ep->bmAttributes != USB_BM_ATTRIBUTES_XFER_BULK) + in_ep = nullptr; + } + if (in_ep != nullptr && out_ep != nullptr && notify_ep != nullptr) + break; + } + if (in_ep->bEndpointAddress & usb_host::USB_DIR_IN) + return CdcEps{notify_ep, in_ep, out_ep, interface_number}; + return CdcEps{notify_ep, out_ep, in_ep, interface_number}; +} + +std::vector USBUartTypeCdcAcm::parse_descriptors_(usb_device_handle_t dev_hdl) { + const usb_config_desc_t *config_desc; + const usb_device_desc_t *device_desc; + int desc_offset = 0; + std::vector cdc_devs{}; + + // Get required descriptors + if (usb_host_get_device_descriptor(dev_hdl, &device_desc) != ESP_OK) { + ESP_LOGE(TAG, "get_device_descriptor failed"); + return {}; + } + if (usb_host_get_active_config_descriptor(dev_hdl, &config_desc) != ESP_OK) { + ESP_LOGE(TAG, "get_active_config_descriptor failed"); + return {}; + } + if (device_desc->bDeviceClass == USB_CLASS_COMM) { + // single CDC-ACM device + if (auto eps = get_cdc(config_desc, 0)) { + ESP_LOGV(TAG, "Found CDC-ACM device"); + cdc_devs.push_back(*eps); + } + return cdc_devs; + } + if (((device_desc->bDeviceClass == USB_CLASS_MISC) && (device_desc->bDeviceSubClass == USB_SUBCLASS_COMMON) && + (device_desc->bDeviceProtocol == USB_DEVICE_PROTOCOL_IAD)) || + ((device_desc->bDeviceClass == USB_CLASS_PER_INTERFACE) && (device_desc->bDeviceSubClass == USB_SUBCLASS_NULL) && + (device_desc->bDeviceProtocol == USB_PROTOCOL_NULL))) { + // This is a composite device, that uses Interface Association Descriptor + const auto *this_desc = reinterpret_cast(config_desc); + for (;;) { + this_desc = usb_parse_next_descriptor_of_type(this_desc, config_desc->wTotalLength, + USB_B_DESCRIPTOR_TYPE_INTERFACE_ASSOCIATION, &desc_offset); + if (!this_desc) + break; + const auto *iad_desc = reinterpret_cast(this_desc); + + if (iad_desc->bFunctionClass == USB_CLASS_COMM && iad_desc->bFunctionSubClass == USB_CDC_SUBCLASS_ACM) { + ESP_LOGV(TAG, "Found CDC-ACM device in composite device"); + if (auto eps = get_cdc(config_desc, iad_desc->bFirstInterface)) + cdc_devs.push_back(*eps); + } + } + } + return cdc_devs; +} + +void RingBuffer::push(uint8_t item) { + this->buffer_[this->insert_pos_] = item; + this->insert_pos_ = (this->insert_pos_ + 1) % this->buffer_size_; +} +void RingBuffer::push(const uint8_t *data, size_t len) { + for (size_t i = 0; i != len; i++) { + this->buffer_[this->insert_pos_] = *data++; + this->insert_pos_ = (this->insert_pos_ + 1) % this->buffer_size_; + } +} + +uint8_t RingBuffer::pop() { + uint8_t item = this->buffer_[this->read_pos_]; + this->read_pos_ = (this->read_pos_ + 1) % this->buffer_size_; + return item; +} +size_t RingBuffer::pop(uint8_t *data, size_t len) { + len = std::min(len, this->get_available()); + for (size_t i = 0; i != len; i++) { + *data++ = this->buffer_[this->read_pos_]; + this->read_pos_ = (this->read_pos_ + 1) % this->buffer_size_; + } + return len; +} +void USBUartChannel::write_array(const uint8_t *data, size_t len) { + if (!this->initialised_) { + ESP_LOGV(TAG, "Channel not initialised - write ignored"); + return; + } + while (this->output_buffer_.get_free_space() != 0 && len-- != 0) { + this->output_buffer_.push(*data++); + } + len++; + if (len > 0) { + ESP_LOGE(TAG, "Buffer full - failed to write %d bytes", len); + } + this->parent_->start_output(this); +} + +bool USBUartChannel::peek_byte(uint8_t *data) { + if (this->input_buffer_.is_empty()) { + return false; + } + *data = this->input_buffer_.peek(); + return true; +} +bool USBUartChannel::read_array(uint8_t *data, size_t len) { + if (!this->initialised_) { + ESP_LOGV(TAG, "Channel not initialised - read ignored"); + return false; + } + auto available = this->available(); + bool status = true; + if (len > available) { + ESP_LOGV(TAG, "underflow: requested %zu but returned %d, bytes", len, available); + len = available; + status = false; + } + for (size_t i = 0; i != len; i++) { + *data++ = this->input_buffer_.pop(); + } + this->parent_->start_input(this); + return status; +} +void USBUartComponent::setup() { USBClient::setup(); } +void USBUartComponent::loop() { USBClient::loop(); } +void USBUartComponent::dump_config() { + USBClient::dump_config(); + for (auto &channel : this->channels_) { + ESP_LOGCONFIG(TAG, " UART Channel %d", channel->index_); + ESP_LOGCONFIG(TAG, " Baud Rate: %" PRIu32 " baud", channel->baud_rate_); + ESP_LOGCONFIG(TAG, " Data Bits: %u", channel->data_bits_); + ESP_LOGCONFIG(TAG, " Parity: %s", PARITY_NAMES[channel->parity_]); + ESP_LOGCONFIG(TAG, " Stop bits: %s", STOP_BITS_NAMES[channel->stop_bits_]); + ESP_LOGCONFIG(TAG, " Debug: %s", YESNO(channel->debug_)); + ESP_LOGCONFIG(TAG, " Dummy receiver: %s", YESNO(channel->dummy_receiver_)); + } +} +void USBUartComponent::start_input(USBUartChannel *channel) { + if (!channel->initialised_ || channel->input_started_ || + channel->input_buffer_.get_free_space() < channel->cdc_dev_.in_ep->wMaxPacketSize) + return; + auto ep = channel->cdc_dev_.in_ep; + auto callback = [this, channel](const usb_host::TransferStatus &status) { + ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code); + if (!status.success) { + ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code)); + return; + } +#ifdef USE_UART_DEBUGGER + if (channel->debug_) { + uart::UARTDebug::log_hex(uart::UART_DIRECTION_RX, + std::vector(status.data, status.data + status.data_len), ','); // NOLINT() + } +#endif + channel->input_started_ = false; + if (!channel->dummy_receiver_) { + for (size_t i = 0; i != status.data_len; i++) { + channel->input_buffer_.push(status.data[i]); + } + } + if (channel->input_buffer_.get_free_space() >= channel->cdc_dev_.in_ep->wMaxPacketSize) { + this->defer([this, channel] { this->start_input(channel); }); + } + }; + channel->input_started_ = true; + this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize); +} + +void USBUartComponent::start_output(USBUartChannel *channel) { + if (channel->output_started_) + return; + if (channel->output_buffer_.is_empty()) { + return; + } + auto ep = channel->cdc_dev_.out_ep; + auto callback = [this, channel](const usb_host::TransferStatus &status) { + ESP_LOGV(TAG, "Output Transfer result: length: %u; status %X", status.data_len, status.error_code); + channel->output_started_ = false; + this->defer([this, channel] { this->start_output(channel); }); + }; + channel->output_started_ = true; + uint8_t data[ep->wMaxPacketSize]; + auto len = channel->output_buffer_.pop(data, ep->wMaxPacketSize); + this->transfer_out(ep->bEndpointAddress, callback, data, len); +#ifdef USE_UART_DEBUGGER + if (channel->debug_) { + uart::UARTDebug::log_hex(uart::UART_DIRECTION_TX, std::vector(data, data + len), ','); // NOLINT() + } +#endif + ESP_LOGV(TAG, "Output %d bytes started", len); +} + +/** + * Hacky fix for some devices that report incorrect MPS values + * @param ep The endpoint descriptor + */ +static void fix_mps(const usb_ep_desc_t *ep) { + if (ep != nullptr) { + auto *ep_mutable = const_cast(ep); + if (ep->wMaxPacketSize > 64) { + ESP_LOGW(TAG, "Corrected MPS of EP %u from %u to 64", ep->bEndpointAddress, ep->wMaxPacketSize); + ep_mutable->wMaxPacketSize = 64; + } + } +} +void USBUartTypeCdcAcm::on_connected() { + auto cdc_devs = this->parse_descriptors_(this->device_handle_); + if (cdc_devs.empty()) { + this->status_set_error("No CDC-ACM device found"); + this->disconnect(); + return; + } + ESP_LOGD(TAG, "Found %zu CDC-ACM devices", cdc_devs.size()); + auto i = 0; + for (auto channel : this->channels_) { + if (i == cdc_devs.size()) { + ESP_LOGE(TAG, "No configuration found for channel %d", channel->index_); + this->status_set_warning("No configuration found for channel"); + break; + } + channel->cdc_dev_ = cdc_devs[i++]; + fix_mps(channel->cdc_dev_.in_ep); + fix_mps(channel->cdc_dev_.out_ep); + channel->initialised_ = true; + auto err = usb_host_interface_claim(this->handle_, this->device_handle_, channel->cdc_dev_.interface_number, 0); + if (err != ESP_OK) { + ESP_LOGE(TAG, "usb_host_interface_claim failed: %s, channel=%d, intf=%d", esp_err_to_name(err), channel->index_, + channel->cdc_dev_.interface_number); + this->status_set_error("usb_host_interface_claim failed"); + this->disconnect(); + return; + } + } + this->enable_channels(); +} + +void USBUartTypeCdcAcm::on_disconnected() { + for (auto channel : this->channels_) { + if (channel->cdc_dev_.in_ep != nullptr) { + usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.in_ep->bEndpointAddress); + usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.in_ep->bEndpointAddress); + } + if (channel->cdc_dev_.out_ep != nullptr) { + usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.out_ep->bEndpointAddress); + usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.out_ep->bEndpointAddress); + } + if (channel->cdc_dev_.notify_ep != nullptr) { + usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress); + usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress); + } + usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.interface_number); + channel->initialised_ = false; + channel->input_started_ = false; + channel->output_started_ = false; + channel->input_buffer_.clear(); + channel->output_buffer_.clear(); + } + USBClient::on_disconnected(); +} + +void USBUartTypeCdcAcm::enable_channels() { + for (auto channel : this->channels_) { + if (!channel->initialised_) + continue; + channel->input_started_ = false; + channel->output_started_ = false; + this->start_input(channel); + } +} + +} // namespace usb_uart +} // namespace esphome +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h new file mode 100644 index 0000000000..fd0fb2c59a --- /dev/null +++ b/esphome/components/usb_uart/usb_uart.h @@ -0,0 +1,151 @@ +#pragma once + +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/components/uart/uart_component.h" +#include "esphome/components/usb_host/usb_host.h" + +namespace esphome { +namespace usb_uart { +class USBUartTypeCdcAcm; +class USBUartComponent; + +static const char *const TAG = "usb_uart"; + +static constexpr uint8_t USB_CDC_SUBCLASS_ACM = 0x02; +static constexpr uint8_t USB_SUBCLASS_COMMON = 0x02; +static constexpr uint8_t USB_SUBCLASS_NULL = 0x00; +static constexpr uint8_t USB_PROTOCOL_NULL = 0x00; +static constexpr uint8_t USB_DEVICE_PROTOCOL_IAD = 0x01; +static constexpr uint8_t USB_VENDOR_IFC = usb_host::USB_TYPE_VENDOR | usb_host::USB_RECIP_INTERFACE; +static constexpr uint8_t USB_VENDOR_DEV = usb_host::USB_TYPE_VENDOR | usb_host::USB_RECIP_DEVICE; + +struct CdcEps { + const usb_ep_desc_t *notify_ep; + const usb_ep_desc_t *in_ep; + const usb_ep_desc_t *out_ep; + uint8_t interface_number; +}; + +enum UARTParityOptions { + UART_CONFIG_PARITY_NONE = 0, + UART_CONFIG_PARITY_ODD, + UART_CONFIG_PARITY_EVEN, + UART_CONFIG_PARITY_MARK, + UART_CONFIG_PARITY_SPACE, +}; + +enum UARTStopBitsOptions { + UART_CONFIG_STOP_BITS_1 = 0, + UART_CONFIG_STOP_BITS_1_5, + UART_CONFIG_STOP_BITS_2, +}; + +static const char *const PARITY_NAMES[] = {"NONE", "ODD", "EVEN", "MARK", "SPACE"}; +static const char *const STOP_BITS_NAMES[] = {"1", "1.5", "2"}; + +class RingBuffer { + public: + RingBuffer(uint16_t buffer_size) : buffer_size_(buffer_size), buffer_(new uint8_t[buffer_size]) {} + bool is_empty() const { return this->read_pos_ == this->insert_pos_; } + size_t get_available() const { + return (this->insert_pos_ + this->buffer_size_ - this->read_pos_) % this->buffer_size_; + }; + size_t get_free_space() const { return this->buffer_size_ - 1 - this->get_available(); } + uint8_t peek() const { return this->buffer_[this->read_pos_]; } + void push(uint8_t item); + void push(const uint8_t *data, size_t len); + uint8_t pop(); + size_t pop(uint8_t *data, size_t len); + void clear() { this->read_pos_ = this->insert_pos_ = 0; } + + protected: + uint16_t insert_pos_ = 0; + uint16_t read_pos_ = 0; + uint16_t buffer_size_; + uint8_t *buffer_; +}; + +class USBUartChannel : public uart::UARTComponent, public Parented { + friend class USBUartComponent; + friend class USBUartTypeCdcAcm; + friend class USBUartTypeCP210X; + friend class USBUartTypeCH34X; + + public: + USBUartChannel(uint8_t index, uint16_t buffer_size) + : index_(index), input_buffer_(RingBuffer(buffer_size)), output_buffer_(RingBuffer(buffer_size)) {} + void write_array(const uint8_t *data, size_t len) override; + ; + bool peek_byte(uint8_t *data) override; + ; + bool read_array(uint8_t *data, size_t len) override; + int available() override { return static_cast(this->input_buffer_.get_available()); } + void flush() override {} + void check_logger_conflict() override {} + void set_parity(UARTParityOptions parity) { this->parity_ = parity; } + void set_debug(bool debug) { this->debug_ = debug; } + void set_dummy_receiver(bool dummy_receiver) { this->dummy_receiver_ = dummy_receiver; } + + protected: + const uint8_t index_; + RingBuffer input_buffer_; + RingBuffer output_buffer_; + UARTParityOptions parity_{UART_CONFIG_PARITY_NONE}; + bool input_started_{true}; + bool output_started_{true}; + CdcEps cdc_dev_{}; + bool debug_{}; + bool dummy_receiver_{}; + bool initialised_{}; +}; + +class USBUartComponent : public usb_host::USBClient { + public: + USBUartComponent(uint16_t vid, uint16_t pid) : usb_host::USBClient(vid, pid) {} + void setup() override; + void loop() override; + void dump_config() override; + std::vector get_channels() { return this->channels_; } + + void add_channel(USBUartChannel *channel) { this->channels_.push_back(channel); } + + void start_input(USBUartChannel *channel); + void start_output(USBUartChannel *channel); + + protected: + std::vector channels_{}; +}; + +class USBUartTypeCdcAcm : public USBUartComponent { + public: + USBUartTypeCdcAcm(uint16_t vid, uint16_t pid) : USBUartComponent(vid, pid) {} + + protected: + virtual std::vector parse_descriptors_(usb_device_handle_t dev_hdl); + void on_connected() override; + virtual void enable_channels(); + void on_disconnected() override; +}; + +class USBUartTypeCP210X : public USBUartTypeCdcAcm { + public: + USBUartTypeCP210X(uint16_t vid, uint16_t pid) : USBUartTypeCdcAcm(vid, pid) {} + + protected: + std::vector parse_descriptors_(usb_device_handle_t dev_hdl) override; + void enable_channels() override; +}; +class USBUartTypeCH34X : public USBUartTypeCdcAcm { + public: + USBUartTypeCH34X(uint16_t vid, uint16_t pid) : USBUartTypeCdcAcm(vid, pid) {} + + protected: + void enable_channels() override; +}; + +} // namespace usb_uart +} // namespace esphome + +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/tests/components/usb_host/test.esp32-s3-idf.yaml b/tests/components/usb_host/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..a2892872e5 --- /dev/null +++ b/tests/components/usb_host/test.esp32-s3-idf.yaml @@ -0,0 +1,5 @@ +usb_host: + devices: + - id: device_1 + vid: 0x1234 + pid: 0x1234 diff --git a/tests/components/usb_uart/common.yaml b/tests/components/usb_uart/common.yaml new file mode 100644 index 0000000000..46ad6291f9 --- /dev/null +++ b/tests/components/usb_uart/common.yaml @@ -0,0 +1,33 @@ +usb_uart: + - id: uart_0 + type: cdc_acm + vid: 0x1234 + pid: 0x5678 + channels: + - id: channel_0_1 + - id: uart_1 + type: cp210x + channels: + - id: channel_1_1 + baud_rate: 115200 + stop_bits: 2 + data_bits: 7 + parity: even + - id: uart_2 + type: ch34x + channels: + - id: channel_2_1 + baud_rate: 115200 + - id: channel_2_2 + baud_rate: 9600 + - id: uart_3 + type: ch340 + channels: + - id: channel_3_1 + baud_rate: 57600 + - id: uart_4 + type: esp_jtag + channels: + - id: channel_4_1 + debug: true + dummy_receiver: true diff --git a/tests/components/usb_uart/test.esp32-s3-idf.yaml b/tests/components/usb_uart/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..380ca87628 --- /dev/null +++ b/tests/components/usb_uart/test.esp32-s3-idf.yaml @@ -0,0 +1 @@ +!include common.yaml