mirror of
https://github.com/esphome/esphome.git
synced 2026-01-09 19:50:49 -07:00
[usb_cdc_acm] New component (#11687)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
This commit is contained in:
@@ -524,6 +524,7 @@ esphome/components/ufire_ise/* @pvizeli
|
|||||||
esphome/components/ultrasonic/* @OttoWinter
|
esphome/components/ultrasonic/* @OttoWinter
|
||||||
esphome/components/update/* @jesserockz
|
esphome/components/update/* @jesserockz
|
||||||
esphome/components/uponor_smatrix/* @kroimon
|
esphome/components/uponor_smatrix/* @kroimon
|
||||||
|
esphome/components/usb_cdc_acm/* @kbx81
|
||||||
esphome/components/usb_host/* @clydebarrow
|
esphome/components/usb_host/* @clydebarrow
|
||||||
esphome/components/usb_uart/* @clydebarrow
|
esphome/components/usb_uart/* @clydebarrow
|
||||||
esphome/components/valve/* @esphome/core
|
esphome/components/valve/* @esphome/core
|
||||||
|
|||||||
76
esphome/components/usb_cdc_acm/__init__.py
Normal file
76
esphome/components/usb_cdc_acm/__init__.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import esp32, uart
|
||||||
|
from esphome.components.esp32 import (
|
||||||
|
VARIANT_ESP32P4,
|
||||||
|
VARIANT_ESP32S2,
|
||||||
|
VARIANT_ESP32S3,
|
||||||
|
add_idf_sdkconfig_option,
|
||||||
|
)
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_ID, CONF_RX_BUFFER_SIZE, CONF_TX_BUFFER_SIZE
|
||||||
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
|
CODEOWNERS = ["@kbx81"]
|
||||||
|
AUTO_LOAD = ["uart"]
|
||||||
|
DEPENDENCIES = ["tinyusb"]
|
||||||
|
|
||||||
|
CONF_INTERFACES = "interfaces"
|
||||||
|
|
||||||
|
usb_cdc_acm_ns = cg.esphome_ns.namespace("usb_cdc_acm")
|
||||||
|
USBCDCACMComponent = usb_cdc_acm_ns.class_("USBCDCACMComponent", cg.Component)
|
||||||
|
USBCDCACMInstance = usb_cdc_acm_ns.class_(
|
||||||
|
"USBCDCACMInstance", uart.UARTComponent, cg.Parented.template(USBCDCACMComponent)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Schema for individual CDC ACM interface instances
|
||||||
|
INTERFACE_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(USBCDCACMInstance),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Main component schema
|
||||||
|
CONFIG_SCHEMA = cv.All(
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(USBCDCACMComponent),
|
||||||
|
cv.Optional(CONF_RX_BUFFER_SIZE, default=256): cv.All(
|
||||||
|
cv.validate_bytes, cv.uint16_t
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_TX_BUFFER_SIZE, default=256): cv.All(
|
||||||
|
cv.validate_bytes, cv.uint16_t
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_INTERFACES, default=[{}]): cv.All(
|
||||||
|
cv.ensure_list(INTERFACE_SCHEMA),
|
||||||
|
cv.Length(min=1, max=2), # At least 1, at most 2 interfaces
|
||||||
|
),
|
||||||
|
}
|
||||||
|
).extend(cv.COMPONENT_SCHEMA),
|
||||||
|
esp32.only_on_variant(
|
||||||
|
supported=[VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config: ConfigType) -> None:
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
|
||||||
|
# Create and register interface instances
|
||||||
|
for interface_index, interface_conf in enumerate(config[CONF_INTERFACES]):
|
||||||
|
interface = cg.new_Pvariable(interface_conf[CONF_ID])
|
||||||
|
await cg.register_parented(interface, var)
|
||||||
|
cg.add(interface.set_interface_number(interface_index))
|
||||||
|
cg.add(var.add_interface(interface))
|
||||||
|
|
||||||
|
# Configure TinyUSB with the correct number of CDC interfaces
|
||||||
|
num_interfaces = len(config[CONF_INTERFACES])
|
||||||
|
add_idf_sdkconfig_option("CONFIG_TINYUSB_CDC_ENABLED", True)
|
||||||
|
add_idf_sdkconfig_option("CONFIG_TINYUSB_CDC_COUNT", num_interfaces)
|
||||||
|
add_idf_sdkconfig_option(
|
||||||
|
"CONFIG_TINYUSB_CDC_RX_BUFSIZE", config[CONF_RX_BUFFER_SIZE]
|
||||||
|
)
|
||||||
|
add_idf_sdkconfig_option(
|
||||||
|
"CONFIG_TINYUSB_CDC_TX_BUFSIZE", config[CONF_TX_BUFFER_SIZE]
|
||||||
|
)
|
||||||
495
esphome/components/usb_cdc_acm/usb_cdc_acm.cpp
Normal file
495
esphome/components/usb_cdc_acm/usb_cdc_acm.cpp
Normal file
@@ -0,0 +1,495 @@
|
|||||||
|
#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||||
|
#include "usb_cdc_acm.h"
|
||||||
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#include <sys/param.h>
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/ringbuf.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
|
||||||
|
#include "tusb.h"
|
||||||
|
#include "tusb_cdc_acm.h"
|
||||||
|
|
||||||
|
namespace esphome::usb_cdc_acm {
|
||||||
|
|
||||||
|
static const char *TAG = "usb_cdc_acm";
|
||||||
|
|
||||||
|
static constexpr size_t USB_TX_TASK_STACK_SIZE = 4096;
|
||||||
|
static constexpr size_t USB_TX_TASK_STACK_SIZE_VV = 8192;
|
||||||
|
|
||||||
|
// Global component instance for managing USB device
|
||||||
|
USBCDCACMComponent *global_usb_cdc_component = nullptr;
|
||||||
|
|
||||||
|
static USBCDCACMInstance *get_instance_by_itf(int itf) {
|
||||||
|
if (global_usb_cdc_component == nullptr) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return global_usb_cdc_component->get_interface_by_number(itf);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tinyusb_cdc_rx_callback(int itf, cdcacm_event_t *event) {
|
||||||
|
USBCDCACMInstance *instance = get_instance_by_itf(itf);
|
||||||
|
if (instance == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "RX callback: invalid interface %d", itf);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t rx_size = 0;
|
||||||
|
static uint8_t rx_buf[CONFIG_TINYUSB_CDC_RX_BUFSIZE] = {0};
|
||||||
|
|
||||||
|
// read from USB
|
||||||
|
esp_err_t ret =
|
||||||
|
tinyusb_cdcacm_read(static_cast<tinyusb_cdcacm_itf_t>(itf), rx_buf, CONFIG_TINYUSB_CDC_RX_BUFSIZE, &rx_size);
|
||||||
|
ESP_LOGV(TAG, "tinyusb_cdc_rx_callback itf=%d (size: %u)", itf, rx_size);
|
||||||
|
ESP_LOGVV(TAG, "rx_buf = %s", format_hex_pretty(rx_buf, rx_size).c_str());
|
||||||
|
|
||||||
|
if (ret == ESP_OK && rx_size > 0) {
|
||||||
|
RingbufHandle_t rx_ringbuf = instance->get_rx_ringbuf();
|
||||||
|
if (rx_ringbuf != nullptr) {
|
||||||
|
BaseType_t send_res = xRingbufferSend(rx_ringbuf, rx_buf, rx_size, 0);
|
||||||
|
if (send_res != pdTRUE) {
|
||||||
|
ESP_LOGE(TAG, "USB RX itf=%d: buffer full, %u bytes lost", itf, rx_size);
|
||||||
|
} else {
|
||||||
|
ESP_LOGV(TAG, "USB RX itf=%d: queued %u bytes", itf, rx_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tinyusb_cdc_line_state_changed_callback(int itf, cdcacm_event_t *event) {
|
||||||
|
USBCDCACMInstance *instance = get_instance_by_itf(itf);
|
||||||
|
if (instance == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "Line state callback: invalid interface %d", itf);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int dtr = event->line_state_changed_data.dtr;
|
||||||
|
int rts = event->line_state_changed_data.rts;
|
||||||
|
ESP_LOGV(TAG, "Line state itf=%d: DTR=%d, RTS=%d", itf, dtr, rts);
|
||||||
|
|
||||||
|
// Queue event for processing in main loop
|
||||||
|
instance->queue_line_state_event(dtr != 0, rts != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tinyusb_cdc_line_coding_changed_callback(int itf, cdcacm_event_t *event) {
|
||||||
|
USBCDCACMInstance *instance = get_instance_by_itf(itf);
|
||||||
|
if (instance == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "Line coding callback: invalid interface %d", itf);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t bit_rate = event->line_coding_changed_data.p_line_coding->bit_rate;
|
||||||
|
uint8_t stop_bits = event->line_coding_changed_data.p_line_coding->stop_bits;
|
||||||
|
uint8_t parity = event->line_coding_changed_data.p_line_coding->parity;
|
||||||
|
uint8_t data_bits = event->line_coding_changed_data.p_line_coding->data_bits;
|
||||||
|
ESP_LOGV(TAG, "Line coding itf=%d: bit_rate=%" PRIu32 " stop_bits=%u parity=%u data_bits=%u", itf, bit_rate,
|
||||||
|
stop_bits, parity, data_bits);
|
||||||
|
|
||||||
|
// Queue event for processing in main loop
|
||||||
|
instance->queue_line_coding_event(bit_rate, stop_bits, parity, data_bits);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t ringbuf_read_bytes(RingbufHandle_t ring_buf, uint8_t *out_buf, size_t out_buf_sz, size_t *rx_data_size,
|
||||||
|
TickType_t xTicksToWait) {
|
||||||
|
size_t read_sz;
|
||||||
|
uint8_t *buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(ring_buf, &read_sz, xTicksToWait, out_buf_sz));
|
||||||
|
|
||||||
|
if (buf == nullptr) {
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(out_buf, buf, read_sz);
|
||||||
|
vRingbufferReturnItem(ring_buf, (void *) buf);
|
||||||
|
*rx_data_size = read_sz;
|
||||||
|
|
||||||
|
// Buffer's data can be wrapped, in which case we should perform another read
|
||||||
|
buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(ring_buf, &read_sz, 0, out_buf_sz - *rx_data_size));
|
||||||
|
if (buf != nullptr) {
|
||||||
|
memcpy(out_buf + *rx_data_size, buf, read_sz);
|
||||||
|
vRingbufferReturnItem(ring_buf, (void *) buf);
|
||||||
|
*rx_data_size += read_sz;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
// USBCDCACMInstance Implementation
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
void USBCDCACMInstance::setup() {
|
||||||
|
this->usb_tx_ringbuf_ = xRingbufferCreate(CONFIG_TINYUSB_CDC_TX_BUFSIZE, RINGBUF_TYPE_BYTEBUF);
|
||||||
|
if (this->usb_tx_ringbuf_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "USB TX buffer creation error for itf %d", this->itf_);
|
||||||
|
this->parent_->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->usb_rx_ringbuf_ = xRingbufferCreate(CONFIG_TINYUSB_CDC_RX_BUFSIZE, RINGBUF_TYPE_BYTEBUF);
|
||||||
|
if (this->usb_rx_ringbuf_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "USB RX buffer creation error for itf %d", this->itf_);
|
||||||
|
this->parent_->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure this CDC interface
|
||||||
|
const tinyusb_config_cdcacm_t acm_cfg = {
|
||||||
|
.usb_dev = TINYUSB_USBDEV_0,
|
||||||
|
.cdc_port = this->itf_,
|
||||||
|
.callback_rx = &tinyusb_cdc_rx_callback,
|
||||||
|
.callback_rx_wanted_char = NULL,
|
||||||
|
.callback_line_state_changed = &tinyusb_cdc_line_state_changed_callback,
|
||||||
|
.callback_line_coding_changed = &tinyusb_cdc_line_coding_changed_callback,
|
||||||
|
};
|
||||||
|
|
||||||
|
esp_err_t result = tusb_cdc_acm_init(&acm_cfg);
|
||||||
|
if (result != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "tusb_cdc_acm_init failed: %d", result);
|
||||||
|
this->parent_->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a larger stack size for (very) verbose logging
|
||||||
|
const size_t stack_size = esp_log_level_get(TAG) > ESP_LOG_DEBUG ? USB_TX_TASK_STACK_SIZE_VV : USB_TX_TASK_STACK_SIZE;
|
||||||
|
|
||||||
|
// Create a simple, unique task name per interface
|
||||||
|
char task_name[] = "usb_tx_0";
|
||||||
|
task_name[sizeof(task_name) - 1] = format_hex_char(static_cast<char>(this->itf_));
|
||||||
|
xTaskCreate(usb_tx_task_fn, task_name, stack_size, this, 4, &this->usb_tx_task_handle_);
|
||||||
|
|
||||||
|
if (this->usb_tx_task_handle_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "Failed to create USB TX task for itf %d", this->itf_);
|
||||||
|
this->parent_->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void USBCDCACMInstance::loop() {
|
||||||
|
// Process events from the lock-free queue
|
||||||
|
this->process_events_();
|
||||||
|
}
|
||||||
|
|
||||||
|
void USBCDCACMInstance::queue_line_state_event(bool dtr, bool rts) {
|
||||||
|
// Allocate event from pool
|
||||||
|
CDCEvent *event = this->event_pool_.allocate();
|
||||||
|
if (event == nullptr) {
|
||||||
|
ESP_LOGW(TAG, "Event pool exhausted, line state event dropped (itf=%d)", this->itf_);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event->type = CDC_EVENT_LINE_STATE_CHANGED;
|
||||||
|
event->data.line_state.dtr = dtr;
|
||||||
|
event->data.line_state.rts = rts;
|
||||||
|
|
||||||
|
if (!this->event_queue_.push(event)) {
|
||||||
|
ESP_LOGW(TAG, "Event queue full, line state event dropped (itf=%d)", this->itf_);
|
||||||
|
// Return event to pool since we couldn't queue it
|
||||||
|
this->event_pool_.release(event);
|
||||||
|
} else {
|
||||||
|
// Wake main loop immediately to process event
|
||||||
|
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||||
|
App.wake_loop_threadsafe();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void USBCDCACMInstance::queue_line_coding_event(uint32_t bit_rate, uint8_t stop_bits, uint8_t parity,
|
||||||
|
uint8_t data_bits) {
|
||||||
|
// Allocate event from pool
|
||||||
|
CDCEvent *event = this->event_pool_.allocate();
|
||||||
|
if (event == nullptr) {
|
||||||
|
ESP_LOGW(TAG, "Event pool exhausted, line coding event dropped (itf=%d)", this->itf_);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event->type = CDC_EVENT_LINE_CODING_CHANGED;
|
||||||
|
event->data.line_coding.bit_rate = bit_rate;
|
||||||
|
event->data.line_coding.stop_bits = stop_bits;
|
||||||
|
event->data.line_coding.parity = parity;
|
||||||
|
event->data.line_coding.data_bits = data_bits;
|
||||||
|
|
||||||
|
if (!this->event_queue_.push(event)) {
|
||||||
|
ESP_LOGW(TAG, "Event queue full, line coding event dropped (itf=%d)", this->itf_);
|
||||||
|
// Return event to pool since we couldn't queue it
|
||||||
|
this->event_pool_.release(event);
|
||||||
|
} else {
|
||||||
|
// Wake main loop immediately to process event
|
||||||
|
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||||
|
App.wake_loop_threadsafe();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void USBCDCACMInstance::process_events_() {
|
||||||
|
// Process all pending events from the queue
|
||||||
|
CDCEvent *event;
|
||||||
|
while ((event = this->event_queue_.pop()) != nullptr) {
|
||||||
|
switch (event->type) {
|
||||||
|
case CDC_EVENT_LINE_STATE_CHANGED: {
|
||||||
|
bool dtr = event->data.line_state.dtr;
|
||||||
|
bool rts = event->data.line_state.rts;
|
||||||
|
|
||||||
|
// Invoke user callback in main loop context
|
||||||
|
if (this->line_state_callback_ != nullptr) {
|
||||||
|
this->line_state_callback_(dtr, rts);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CDC_EVENT_LINE_CODING_CHANGED: {
|
||||||
|
uint32_t bit_rate = event->data.line_coding.bit_rate;
|
||||||
|
uint8_t stop_bits = event->data.line_coding.stop_bits;
|
||||||
|
uint8_t parity = event->data.line_coding.parity;
|
||||||
|
uint8_t data_bits = event->data.line_coding.data_bits;
|
||||||
|
|
||||||
|
// Update UART configuration based on CDC line coding
|
||||||
|
this->baud_rate_ = bit_rate;
|
||||||
|
this->data_bits_ = data_bits;
|
||||||
|
|
||||||
|
// Convert CDC stop bits to UART stop bits format
|
||||||
|
// CDC: 0=1 stop bit, 1=1.5 stop bits, 2=2 stop bits
|
||||||
|
this->stop_bits_ = (stop_bits == 0) ? 1 : (stop_bits == 1) ? 1 : 2;
|
||||||
|
|
||||||
|
// Convert CDC parity to UART parity format
|
||||||
|
// CDC: 0=None, 1=Odd, 2=Even, 3=Mark, 4=Space
|
||||||
|
switch (parity) {
|
||||||
|
case 0:
|
||||||
|
this->parity_ = uart::UART_CONFIG_PARITY_NONE;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
this->parity_ = uart::UART_CONFIG_PARITY_ODD;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
this->parity_ = uart::UART_CONFIG_PARITY_EVEN;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Mark and Space parity are not commonly supported, default to None
|
||||||
|
this->parity_ = uart::UART_CONFIG_PARITY_NONE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoke user callback in main loop context
|
||||||
|
if (this->line_coding_callback_ != nullptr) {
|
||||||
|
this->line_coding_callback_(bit_rate, stop_bits, parity, data_bits);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Return event to pool for reuse
|
||||||
|
this->event_pool_.release(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void USBCDCACMInstance::usb_tx_task_fn(void *arg) {
|
||||||
|
auto *instance = static_cast<USBCDCACMInstance *>(arg);
|
||||||
|
instance->usb_tx_task();
|
||||||
|
}
|
||||||
|
|
||||||
|
void USBCDCACMInstance::usb_tx_task() {
|
||||||
|
uint8_t data[CONFIG_TINYUSB_CDC_TX_BUFSIZE] = {0};
|
||||||
|
size_t tx_data_size = 0;
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
// Wait for a notification from the bridge component
|
||||||
|
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||||
|
|
||||||
|
// When we do wake up, we can be sure there is data in the ring buffer
|
||||||
|
esp_err_t ret = ringbuf_read_bytes(this->usb_tx_ringbuf_, data, CONFIG_TINYUSB_CDC_TX_BUFSIZE, &tx_data_size, 0);
|
||||||
|
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "USB TX itf=%d: RingBuf read failed", this->itf_);
|
||||||
|
continue;
|
||||||
|
} else if (tx_data_size == 0) {
|
||||||
|
ESP_LOGD(TAG, "USB TX itf=%d: RingBuf empty, skipping", this->itf_);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGV(TAG, "USB TX itf=%d: Read %d bytes from buffer", this->itf_, tx_data_size);
|
||||||
|
ESP_LOGVV(TAG, "data = %s", format_hex_pretty(data, tx_data_size).c_str());
|
||||||
|
|
||||||
|
// Serial data will be split up into 64 byte chunks to be sent over USB so this
|
||||||
|
// usually will take multiple iterations
|
||||||
|
uint8_t *data_head = &data[0];
|
||||||
|
|
||||||
|
while (tx_data_size > 0) {
|
||||||
|
size_t queued = tinyusb_cdcacm_write_queue(this->itf_, data_head, tx_data_size);
|
||||||
|
ESP_LOGV(TAG, "USB TX itf=%d: enqueued: size=%d, queued=%u", this->itf_, tx_data_size, queued);
|
||||||
|
|
||||||
|
tx_data_size -= queued;
|
||||||
|
data_head += queued;
|
||||||
|
|
||||||
|
ESP_LOGV(TAG, "USB TX itf=%d: waiting 10ms for flush", this->itf_);
|
||||||
|
esp_err_t flush_ret = tinyusb_cdcacm_write_flush(this->itf_, pdMS_TO_TICKS(10));
|
||||||
|
|
||||||
|
if (flush_ret != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "USB TX itf=%d: flush failed", this->itf_);
|
||||||
|
tud_cdc_n_write_clear(this->itf_);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
// UARTComponent Interface Implementation
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
void USBCDCACMInstance::write_array(const uint8_t *data, size_t len) {
|
||||||
|
if (len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write data to TX ring buffer
|
||||||
|
BaseType_t send_res = xRingbufferSend(this->usb_tx_ringbuf_, data, len, 0);
|
||||||
|
if (send_res != pdTRUE) {
|
||||||
|
ESP_LOGW(TAG, "USB TX itf=%d: buffer full, %u bytes dropped", this->itf_, len);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify TX task that data is available
|
||||||
|
if (this->usb_tx_task_handle_ != nullptr) {
|
||||||
|
xTaskNotifyGive(this->usb_tx_task_handle_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool USBCDCACMInstance::peek_byte(uint8_t *data) {
|
||||||
|
if (this->has_peek_) {
|
||||||
|
*data = this->peek_buffer_;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->read_byte(&this->peek_buffer_)) {
|
||||||
|
*data = this->peek_buffer_;
|
||||||
|
this->has_peek_ = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool USBCDCACMInstance::read_array(uint8_t *data, size_t len) {
|
||||||
|
if (len == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t original_len = len;
|
||||||
|
size_t bytes_read = 0;
|
||||||
|
|
||||||
|
// First, use the peek buffer if available
|
||||||
|
if (this->has_peek_) {
|
||||||
|
data[0] = this->peek_buffer_;
|
||||||
|
this->has_peek_ = false;
|
||||||
|
bytes_read = 1;
|
||||||
|
data++;
|
||||||
|
if (--len == 0) { // Decrement len first, then check it...
|
||||||
|
return true; // No more to read
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read remaining bytes from RX ring buffer
|
||||||
|
size_t rx_size = 0;
|
||||||
|
uint8_t *buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(this->usb_rx_ringbuf_, &rx_size, 0, len));
|
||||||
|
if (buf == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(data, buf, rx_size);
|
||||||
|
vRingbufferReturnItem(this->usb_rx_ringbuf_, (void *) buf);
|
||||||
|
bytes_read += rx_size;
|
||||||
|
data += rx_size;
|
||||||
|
len -= rx_size;
|
||||||
|
if (len == 0) {
|
||||||
|
return true; // No more to read
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer's data may wrap around, in which case we should perform another read
|
||||||
|
buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(this->usb_rx_ringbuf_, &rx_size, 0, len));
|
||||||
|
if (buf == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(data, buf, rx_size);
|
||||||
|
vRingbufferReturnItem(this->usb_rx_ringbuf_, (void *) buf);
|
||||||
|
bytes_read += rx_size;
|
||||||
|
|
||||||
|
return bytes_read == original_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
int USBCDCACMInstance::available() {
|
||||||
|
UBaseType_t waiting = 0;
|
||||||
|
if (this->usb_rx_ringbuf_ != nullptr) {
|
||||||
|
vRingbufferGetInfo(this->usb_rx_ringbuf_, nullptr, nullptr, nullptr, nullptr, &waiting);
|
||||||
|
}
|
||||||
|
return static_cast<int>(waiting) + (this->has_peek_ ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void USBCDCACMInstance::flush() {
|
||||||
|
// Wait for TX ring buffer to be empty
|
||||||
|
if (this->usb_tx_ringbuf_ == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UBaseType_t waiting = 1;
|
||||||
|
while (waiting > 0) {
|
||||||
|
vRingbufferGetInfo(this->usb_tx_ringbuf_, nullptr, nullptr, nullptr, nullptr, &waiting);
|
||||||
|
if (waiting > 0) {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also wait for USB to finish transmitting
|
||||||
|
tinyusb_cdcacm_write_flush(this->itf_, pdMS_TO_TICKS(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
// USBCDCACMComponent Implementation
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
USBCDCACMComponent::USBCDCACMComponent() { global_usb_cdc_component = this; }
|
||||||
|
|
||||||
|
void USBCDCACMComponent::setup() {
|
||||||
|
// Setup all registered interfaces
|
||||||
|
for (auto interface : this->interfaces_) {
|
||||||
|
if (interface != nullptr) {
|
||||||
|
interface->setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void USBCDCACMComponent::loop() {
|
||||||
|
// Call loop() on all registered interfaces to process events
|
||||||
|
for (auto interface : this->interfaces_) {
|
||||||
|
if (interface != nullptr) {
|
||||||
|
interface->loop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void USBCDCACMComponent::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG,
|
||||||
|
"USB CDC-ACM:\n"
|
||||||
|
" Number of Interfaces: %d",
|
||||||
|
this->interfaces_[MAX_USB_CDC_INSTANCES - 1] != nullptr ? MAX_USB_CDC_INSTANCES : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void USBCDCACMComponent::add_interface(USBCDCACMInstance *interface) {
|
||||||
|
uint8_t itf_num = static_cast<uint8_t>(interface->get_itf());
|
||||||
|
if (itf_num < MAX_USB_CDC_INSTANCES) {
|
||||||
|
this->interfaces_[itf_num] = interface;
|
||||||
|
} else {
|
||||||
|
ESP_LOGE(TAG, "Interface number must be less than %u", MAX_USB_CDC_INSTANCES);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
USBCDCACMInstance *USBCDCACMComponent::get_interface_by_number(uint8_t itf) {
|
||||||
|
for (auto interface : this->interfaces_) {
|
||||||
|
if ((interface != nullptr) && (interface->get_itf() == static_cast<tinyusb_cdcacm_itf_t>(itf))) {
|
||||||
|
return interface;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::usb_cdc_acm
|
||||||
|
#endif
|
||||||
135
esphome/components/usb_cdc_acm/usb_cdc_acm.h
Normal file
135
esphome/components/usb_cdc_acm/usb_cdc_acm.h
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
#pragma once
|
||||||
|
#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/event_pool.h"
|
||||||
|
#include "esphome/core/lock_free_queue.h"
|
||||||
|
#include "esphome/components/uart/uart_component.h"
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include "freertos/ringbuf.h"
|
||||||
|
#include "tusb_cdc_acm.h"
|
||||||
|
|
||||||
|
namespace esphome::usb_cdc_acm {
|
||||||
|
|
||||||
|
static const uint8_t EVENT_QUEUE_SIZE = 12;
|
||||||
|
static const uint8_t MAX_USB_CDC_INSTANCES = 2;
|
||||||
|
|
||||||
|
// Callback types for line coding and line state changes
|
||||||
|
using LineCodingCallback = std::function<void(uint32_t bit_rate, uint8_t stop_bits, uint8_t parity, uint8_t data_bits)>;
|
||||||
|
using LineStateCallback = std::function<void(bool dtr, bool rts)>;
|
||||||
|
|
||||||
|
// Event types
|
||||||
|
enum CDCEventType : uint8_t {
|
||||||
|
CDC_EVENT_LINE_STATE_CHANGED,
|
||||||
|
CDC_EVENT_LINE_CODING_CHANGED,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event structure for the queue
|
||||||
|
struct CDCEvent {
|
||||||
|
CDCEventType type;
|
||||||
|
union {
|
||||||
|
struct {
|
||||||
|
bool dtr;
|
||||||
|
bool rts;
|
||||||
|
} line_state;
|
||||||
|
struct {
|
||||||
|
uint32_t bit_rate;
|
||||||
|
uint8_t stop_bits;
|
||||||
|
uint8_t parity;
|
||||||
|
uint8_t data_bits;
|
||||||
|
} line_coding;
|
||||||
|
} data;
|
||||||
|
|
||||||
|
// Required by EventPool - called before returning to pool
|
||||||
|
void release() {
|
||||||
|
// No dynamic memory to clean up, data is stored inline
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Forward declaration
|
||||||
|
class USBCDCACMComponent;
|
||||||
|
|
||||||
|
/// Represents a single CDC ACM interface instance
|
||||||
|
class USBCDCACMInstance : public uart::UARTComponent, public Parented<USBCDCACMComponent> {
|
||||||
|
public:
|
||||||
|
void set_interface_number(uint8_t itf) { this->itf_ = static_cast<tinyusb_cdcacm_itf_t>(itf); }
|
||||||
|
|
||||||
|
void setup();
|
||||||
|
void loop();
|
||||||
|
|
||||||
|
// Get the CDC port number for this instance
|
||||||
|
tinyusb_cdcacm_itf_t get_itf() const { return this->itf_; }
|
||||||
|
|
||||||
|
// Ring buffer accessors for bridge components
|
||||||
|
RingbufHandle_t get_tx_ringbuf() const { return this->usb_tx_ringbuf_; }
|
||||||
|
RingbufHandle_t get_rx_ringbuf() const { return this->usb_rx_ringbuf_; }
|
||||||
|
|
||||||
|
// Task handle accessor for notifying TX task
|
||||||
|
TaskHandle_t get_tx_task_handle() const { return this->usb_tx_task_handle_; }
|
||||||
|
|
||||||
|
// Callback registration for line coding and line state changes
|
||||||
|
void set_line_coding_callback(LineCodingCallback callback) { this->line_coding_callback_ = std::move(callback); }
|
||||||
|
void set_line_state_callback(LineStateCallback callback) { this->line_state_callback_ = std::move(callback); }
|
||||||
|
|
||||||
|
// Called from TinyUSB task context (SPSC producer) - queues event for processing in main loop
|
||||||
|
void queue_line_coding_event(uint32_t bit_rate, uint8_t stop_bits, uint8_t parity, uint8_t data_bits);
|
||||||
|
void queue_line_state_event(bool dtr, bool rts);
|
||||||
|
|
||||||
|
static void usb_tx_task_fn(void *arg);
|
||||||
|
void usb_tx_task();
|
||||||
|
|
||||||
|
// UARTComponent interface implementation
|
||||||
|
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;
|
||||||
|
void flush() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void check_logger_conflict() override {}
|
||||||
|
|
||||||
|
// Process queued events and invoke callbacks (called from main loop)
|
||||||
|
void process_events_();
|
||||||
|
|
||||||
|
TaskHandle_t usb_tx_task_handle_{nullptr};
|
||||||
|
tinyusb_cdcacm_itf_t itf_{TINYUSB_CDC_ACM_0};
|
||||||
|
|
||||||
|
RingbufHandle_t usb_tx_ringbuf_{nullptr};
|
||||||
|
RingbufHandle_t usb_rx_ringbuf_{nullptr};
|
||||||
|
|
||||||
|
// User-registered callbacks (called from main loop)
|
||||||
|
LineCodingCallback line_coding_callback_{nullptr};
|
||||||
|
LineStateCallback line_state_callback_{nullptr};
|
||||||
|
|
||||||
|
// Lock-free queue and event pool for cross-task event passing
|
||||||
|
EventPool<CDCEvent, EVENT_QUEUE_SIZE> event_pool_;
|
||||||
|
LockFreeQueue<CDCEvent, EVENT_QUEUE_SIZE> event_queue_;
|
||||||
|
|
||||||
|
// RX buffer for peek functionality
|
||||||
|
uint8_t peek_buffer_{0};
|
||||||
|
bool has_peek_{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Main USB CDC ACM component that manages the USB device and all CDC interfaces
|
||||||
|
class USBCDCACMComponent : public Component {
|
||||||
|
public:
|
||||||
|
USBCDCACMComponent();
|
||||||
|
|
||||||
|
void setup() override;
|
||||||
|
void loop() override;
|
||||||
|
void dump_config() override;
|
||||||
|
float get_setup_priority() const override { return setup_priority::IO; }
|
||||||
|
|
||||||
|
// Interface management
|
||||||
|
void add_interface(USBCDCACMInstance *interface);
|
||||||
|
USBCDCACMInstance *get_interface_by_number(uint8_t itf);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::array<USBCDCACMInstance *, MAX_USB_CDC_INSTANCES> interfaces_{nullptr, nullptr};
|
||||||
|
};
|
||||||
|
|
||||||
|
extern USBCDCACMComponent *global_usb_cdc_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
|
||||||
|
} // namespace esphome::usb_cdc_acm
|
||||||
|
#endif
|
||||||
5
tests/components/usb_cdc_acm/test.esp32-p4-idf.yaml
Normal file
5
tests/components/usb_cdc_acm/test.esp32-p4-idf.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<<: !include tinyusb_common.yaml
|
||||||
|
|
||||||
|
usb_cdc_acm:
|
||||||
|
interfaces:
|
||||||
|
id: usb_cdc_acm1
|
||||||
5
tests/components/usb_cdc_acm/test.esp32-s2-idf.yaml
Normal file
5
tests/components/usb_cdc_acm/test.esp32-s2-idf.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<<: !include tinyusb_common.yaml
|
||||||
|
|
||||||
|
usb_cdc_acm:
|
||||||
|
interfaces:
|
||||||
|
- id: usb_cdc_acm1
|
||||||
6
tests/components/usb_cdc_acm/test.esp32-s3-idf.yaml
Normal file
6
tests/components/usb_cdc_acm/test.esp32-s3-idf.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<<: !include tinyusb_common.yaml
|
||||||
|
|
||||||
|
usb_cdc_acm:
|
||||||
|
interfaces:
|
||||||
|
- id: usb_cdc_acm1
|
||||||
|
- id: usb_cdc_acm2
|
||||||
8
tests/components/usb_cdc_acm/tinyusb_common.yaml
Normal file
8
tests/components/usb_cdc_acm/tinyusb_common.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
tinyusb:
|
||||||
|
id: tinyusb_test
|
||||||
|
usb_lang_id: 0x0123
|
||||||
|
usb_manufacturer_str: ESPHomeTestManufacturer
|
||||||
|
usb_product_id: 0x1234
|
||||||
|
usb_product_str: ESPHomeTestProduct
|
||||||
|
usb_serial_str: ESPHomeTestSerialNumber
|
||||||
|
usb_vendor_id: 0x2345
|
||||||
Reference in New Issue
Block a user