mirror of
https://github.com/esphome/esphome.git
synced 2025-09-26 15:12:21 +01:00
wip
This commit is contained in:
@@ -5,7 +5,10 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include <vector>
|
||||
#include "usb/usb_host.h"
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/queue.h>
|
||||
#include <list>
|
||||
|
||||
namespace esphome {
|
||||
@@ -13,6 +16,10 @@ namespace usb_host {
|
||||
|
||||
static const char *const TAG = "usb_host";
|
||||
|
||||
// Forward declarations
|
||||
struct TransferRequest;
|
||||
class USBClient;
|
||||
|
||||
// constants for setup packet type
|
||||
static const uint8_t USB_RECIP_DEVICE = 0;
|
||||
static const uint8_t USB_RECIP_INTERFACE = 1;
|
||||
@@ -49,6 +56,30 @@ struct TransferRequest {
|
||||
USBClient *client;
|
||||
};
|
||||
|
||||
// Lightweight event types for queue
|
||||
enum EventType {
|
||||
EVENT_DEVICE_NEW,
|
||||
EVENT_DEVICE_GONE,
|
||||
EVENT_TRANSFER_COMPLETE,
|
||||
EVENT_CONTROL_COMPLETE,
|
||||
};
|
||||
|
||||
struct UsbEvent {
|
||||
EventType type;
|
||||
union {
|
||||
struct {
|
||||
uint8_t address;
|
||||
} device_new;
|
||||
struct {
|
||||
usb_device_handle_t handle;
|
||||
} device_gone;
|
||||
struct {
|
||||
TransferRequest *trq;
|
||||
bool callback_executed; // Flag to indicate callback was already executed in USB task
|
||||
} transfer;
|
||||
} data;
|
||||
};
|
||||
|
||||
// callback function type.
|
||||
|
||||
enum ClientState {
|
||||
@@ -83,6 +114,7 @@ class USBClient : public Component {
|
||||
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<uint8_t> &data = {});
|
||||
QueueHandle_t get_event_queue() { return event_queue_; }
|
||||
|
||||
protected:
|
||||
bool register_();
|
||||
@@ -91,6 +123,13 @@ class USBClient : public Component {
|
||||
virtual void on_connected() {}
|
||||
virtual void on_disconnected() { this->init_pool(); }
|
||||
|
||||
// USB task management
|
||||
static void usb_task_fn(void *arg);
|
||||
void usb_task_loop();
|
||||
|
||||
TaskHandle_t usb_task_handle_{nullptr};
|
||||
QueueHandle_t event_queue_{nullptr}; // Queue of UsbEvent structs
|
||||
|
||||
usb_host_client_handle_t handle_{};
|
||||
usb_device_handle_t device_handle_{};
|
||||
int device_addr_{-1};
|
||||
|
@@ -139,18 +139,25 @@ static std::string get_descriptor_string(const usb_str_desc_t *desc) {
|
||||
return {buffer};
|
||||
}
|
||||
|
||||
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
|
||||
static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void *ptr) {
|
||||
auto *client = static_cast<USBClient *>(ptr);
|
||||
UsbEvent event;
|
||||
|
||||
// Queue events to be processed in main loop
|
||||
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);
|
||||
event.type = EVENT_DEVICE_NEW;
|
||||
event.data.device_new.address = event_msg->new_dev.address;
|
||||
xQueueSend(client->get_event_queue(), &event, portMAX_DELAY);
|
||||
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);
|
||||
ESP_LOGD(TAG, "Device gone");
|
||||
event.type = EVENT_DEVICE_GONE;
|
||||
event.data.device_gone.handle = event_msg->dev_gone.dev_hdl;
|
||||
xQueueSend(client->get_event_queue(), &event, portMAX_DELAY);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -173,9 +180,66 @@ void USBClient::setup() {
|
||||
usb_host_transfer_alloc(64, 0, &trq->transfer);
|
||||
trq->client = this;
|
||||
}
|
||||
|
||||
// Create event queue for communication between USB task and main loop
|
||||
this->event_queue_ = xQueueCreate(32, sizeof(UsbEvent));
|
||||
if (this->event_queue_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create event queue");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and start USB task
|
||||
xTaskCreatePinnedToCore(usb_task_fn, "usb_task",
|
||||
2048, // Stack size (minimal - just handles USB events)
|
||||
this, // Task parameter
|
||||
5, // Priority (higher than main loop)
|
||||
&this->usb_task_handle_,
|
||||
1 // Core 1
|
||||
);
|
||||
|
||||
if (this->usb_task_handle_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create USB task");
|
||||
this->mark_failed();
|
||||
}
|
||||
}
|
||||
|
||||
void USBClient::usb_task_fn(void *arg) {
|
||||
auto *client = static_cast<USBClient *>(arg);
|
||||
client->usb_task_loop();
|
||||
}
|
||||
|
||||
void USBClient::usb_task_loop() {
|
||||
ESP_LOGI(TAG, "USB task started on core %d", xPortGetCoreID());
|
||||
|
||||
// Run forever - ESPHome reboots rather than shutting down cleanly
|
||||
while (true) {
|
||||
// Handle USB events with a timeout to prevent blocking forever
|
||||
usb_host_client_handle_events(this->handle_, pdMS_TO_TICKS(10));
|
||||
}
|
||||
}
|
||||
|
||||
void USBClient::loop() {
|
||||
// Process any events from the USB task
|
||||
UsbEvent event;
|
||||
while (xQueueReceive(this->event_queue_, &event, 0) == pdTRUE) {
|
||||
switch (event.type) {
|
||||
case EVENT_DEVICE_NEW:
|
||||
this->on_opened(event.data.device_new.address);
|
||||
break;
|
||||
case EVENT_DEVICE_GONE:
|
||||
this->on_removed(event.data.device_gone.handle);
|
||||
break;
|
||||
case EVENT_TRANSFER_COMPLETE:
|
||||
case EVENT_CONTROL_COMPLETE: {
|
||||
auto *trq = event.data.transfer.trq;
|
||||
// Callback was already executed in USB task, just cleanup
|
||||
this->release_trq(trq);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (this->state_) {
|
||||
case USB_CLIENT_OPEN: {
|
||||
int err;
|
||||
@@ -228,7 +292,7 @@ void USBClient::loop() {
|
||||
}
|
||||
|
||||
default:
|
||||
usb_host_client_handle_events(this->handle_, 0);
|
||||
// USB events are now handled in the dedicated task
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -245,6 +309,7 @@ void USBClient::on_removed(usb_device_handle_t handle) {
|
||||
}
|
||||
}
|
||||
|
||||
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
|
||||
static void control_callback(const usb_transfer_t *xfer) {
|
||||
auto *trq = static_cast<TransferRequest *>(xfer->context);
|
||||
trq->status.error_code = xfer->status;
|
||||
@@ -252,9 +317,18 @@ static void control_callback(const usb_transfer_t *xfer) {
|
||||
trq->status.endpoint = xfer->bEndpointAddress;
|
||||
trq->status.data = xfer->data_buffer;
|
||||
trq->status.data_len = xfer->actual_num_bytes;
|
||||
if (trq->callback != nullptr)
|
||||
|
||||
// Execute callback in USB task context
|
||||
if (trq->callback != nullptr) {
|
||||
trq->callback(trq->status);
|
||||
trq->client->release_trq(trq);
|
||||
}
|
||||
|
||||
// Queue cleanup to main loop
|
||||
UsbEvent event;
|
||||
event.type = EVENT_CONTROL_COMPLETE;
|
||||
event.data.transfer.trq = trq;
|
||||
event.data.transfer.callback_executed = true;
|
||||
xQueueSend(trq->client->get_event_queue(), &event, portMAX_DELAY);
|
||||
}
|
||||
|
||||
TransferRequest *USBClient::get_trq_() {
|
||||
@@ -315,6 +389,7 @@ bool USBClient::control_transfer(uint8_t type, uint8_t request, uint16_t value,
|
||||
return true;
|
||||
}
|
||||
|
||||
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
|
||||
static void transfer_callback(usb_transfer_t *xfer) {
|
||||
auto *trq = static_cast<TransferRequest *>(xfer->context);
|
||||
trq->status.error_code = xfer->status;
|
||||
@@ -322,9 +397,19 @@ static void transfer_callback(usb_transfer_t *xfer) {
|
||||
trq->status.endpoint = xfer->bEndpointAddress;
|
||||
trq->status.data = xfer->data_buffer;
|
||||
trq->status.data_len = xfer->actual_num_bytes;
|
||||
if (trq->callback != nullptr)
|
||||
|
||||
// Always execute callback in USB task context
|
||||
// Callbacks should be fast and non-blocking (e.g., copy data to queue)
|
||||
if (trq->callback != nullptr) {
|
||||
trq->callback(trq->status);
|
||||
trq->client->release_trq(trq);
|
||||
}
|
||||
|
||||
// Queue cleanup to main loop
|
||||
UsbEvent event;
|
||||
event.type = EVENT_TRANSFER_COMPLETE;
|
||||
event.data.transfer.trq = trq;
|
||||
event.data.transfer.callback_executed = true;
|
||||
xQueueSend(trq->client->get_event_queue(), &event, portMAX_DELAY);
|
||||
}
|
||||
/**
|
||||
* Performs a transfer input operation.
|
||||
@@ -345,6 +430,7 @@ void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u
|
||||
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);
|
||||
|
@@ -170,7 +170,37 @@ bool USBUartChannel::read_array(uint8_t *data, size_t len) {
|
||||
return status;
|
||||
}
|
||||
void USBUartComponent::setup() { USBClient::setup(); }
|
||||
void USBUartComponent::loop() { USBClient::loop(); }
|
||||
void USBUartComponent::loop() {
|
||||
USBClient::loop();
|
||||
|
||||
// Process USB data from the lock-free queue
|
||||
UsbDataChunk *chunk;
|
||||
int chunks_processed = 0;
|
||||
while ((chunk = this->usb_data_queue_.pop()) != nullptr) {
|
||||
chunks_processed++;
|
||||
auto *channel = chunk->channel;
|
||||
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
if (channel->debug_) {
|
||||
uart::UARTDebug::log_hex(uart::UART_DIRECTION_RX, std::vector<uint8_t>(chunk->data, chunk->data + chunk->length),
|
||||
','); // NOLINT()
|
||||
}
|
||||
#endif
|
||||
|
||||
// Push data to ring buffer (now safe in main loop)
|
||||
for (size_t i = 0; i < chunk->length; i++) {
|
||||
channel->input_buffer_.push(chunk->data[i]);
|
||||
}
|
||||
|
||||
// Return chunk to pool for reuse
|
||||
this->free_chunks_.push(chunk);
|
||||
}
|
||||
|
||||
static constexpr int LOG_CHUNK_THRESHOLD = 5;
|
||||
if (chunks_processed > LOG_CHUNK_THRESHOLD) {
|
||||
ESP_LOGV(TAG, "Processed %d chunks from USB queue", chunks_processed);
|
||||
}
|
||||
}
|
||||
void USBUartComponent::dump_config() {
|
||||
USBClient::dump_config();
|
||||
for (auto &channel : this->channels_) {
|
||||
@@ -187,31 +217,46 @@ void USBUartComponent::dump_config() {
|
||||
}
|
||||
}
|
||||
void USBUartComponent::start_input(USBUartChannel *channel) {
|
||||
if (!channel->initialised_ || channel->input_started_ ||
|
||||
channel->input_buffer_.get_free_space() < channel->cdc_dev_.in_ep->wMaxPacketSize)
|
||||
if (!channel->initialised_ || channel->input_started_)
|
||||
return;
|
||||
// Note: We no longer check ring buffer space here since this may be called from USB task
|
||||
// The lock-free queue provides backpressure instead
|
||||
const auto *ep = channel->cdc_dev_.in_ep;
|
||||
// CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback
|
||||
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<uint8_t>(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->dummy_receiver_ && status.data_len > 0) {
|
||||
// Get a free chunk from the pool
|
||||
UsbDataChunk *chunk = this->free_chunks_.pop();
|
||||
if (chunk == nullptr) {
|
||||
ESP_LOGW(TAG, "No free chunks available, dropping %u bytes", status.data_len);
|
||||
// Mark input as not started so we can retry
|
||||
channel->input_started_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy data to chunk (this is fast, happens in USB task)
|
||||
memcpy(chunk->data, status.data, status.data_len);
|
||||
chunk->length = status.data_len;
|
||||
chunk->channel = channel;
|
||||
|
||||
// Push to lock-free queue for main loop processing
|
||||
if (!this->usb_data_queue_.push(chunk)) {
|
||||
ESP_LOGW(TAG, "USB data queue full, dropping %u bytes", status.data_len);
|
||||
// Return chunk to pool
|
||||
this->free_chunks_.push(chunk);
|
||||
}
|
||||
}
|
||||
if (channel->input_buffer_.get_free_space() >= channel->cdc_dev_.in_ep->wMaxPacketSize) {
|
||||
this->defer([this, channel] { this->start_input(channel); });
|
||||
}
|
||||
|
||||
// Always restart input immediately from USB task
|
||||
// The lock-free queue will handle backpressure
|
||||
channel->input_started_ = false;
|
||||
this->start_input(channel);
|
||||
};
|
||||
channel->input_started_ = true;
|
||||
this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize);
|
||||
@@ -224,9 +269,12 @@ void USBUartComponent::start_output(USBUartChannel *channel) {
|
||||
return;
|
||||
}
|
||||
const auto *ep = channel->cdc_dev_.out_ep;
|
||||
// CALLBACK CONTEXT: This lambda is stored in TransferRequest and will be executed
|
||||
// in MAIN LOOP after being queued by transfer_callback in USB task
|
||||
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;
|
||||
// DEFERRED CONTEXT: Main loop (restart output in main loop)
|
||||
this->defer([this, channel] { this->start_output(channel); });
|
||||
};
|
||||
channel->output_started_ = true;
|
||||
|
@@ -5,11 +5,13 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/uart/uart_component.h"
|
||||
#include "esphome/components/usb_host/usb_host.h"
|
||||
#include "esphome/core/lock_free_queue.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace usb_uart {
|
||||
class USBUartTypeCdcAcm;
|
||||
class USBUartComponent;
|
||||
class USBUartChannel;
|
||||
|
||||
static const char *const TAG = "usb_uart";
|
||||
|
||||
@@ -68,6 +70,14 @@ class RingBuffer {
|
||||
uint8_t *buffer_;
|
||||
};
|
||||
|
||||
// Structure for queuing received USB data chunks
|
||||
struct UsbDataChunk {
|
||||
static constexpr size_t MAX_CHUNK_SIZE = 64; // USB packet size
|
||||
uint8_t data[MAX_CHUNK_SIZE];
|
||||
size_t length;
|
||||
USBUartChannel *channel;
|
||||
};
|
||||
|
||||
class USBUartChannel : public uart::UARTComponent, public Parented<USBUartComponent> {
|
||||
friend class USBUartComponent;
|
||||
friend class USBUartTypeCdcAcm;
|
||||
@@ -104,7 +114,18 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon
|
||||
|
||||
class USBUartComponent : public usb_host::USBClient {
|
||||
public:
|
||||
USBUartComponent(uint16_t vid, uint16_t pid) : usb_host::USBClient(vid, pid) {}
|
||||
USBUartComponent(uint16_t vid, uint16_t pid) : usb_host::USBClient(vid, pid) {
|
||||
// Allocate pool of data chunks
|
||||
for (int i = 0; i < MAX_DATA_CHUNKS; i++) {
|
||||
this->data_chunk_pool_[i] = new UsbDataChunk();
|
||||
this->free_chunks_.push(this->data_chunk_pool_[i]);
|
||||
}
|
||||
}
|
||||
~USBUartComponent() {
|
||||
for (int i = 0; i < MAX_DATA_CHUNKS; i++) {
|
||||
delete this->data_chunk_pool_[i];
|
||||
}
|
||||
}
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
@@ -115,8 +136,16 @@ class USBUartComponent : public usb_host::USBClient {
|
||||
void start_input(USBUartChannel *channel);
|
||||
void start_output(USBUartChannel *channel);
|
||||
|
||||
// Lock-free data transfer from USB task to main loop
|
||||
LockFreeQueue<UsbDataChunk, 32> usb_data_queue_;
|
||||
|
||||
protected:
|
||||
std::vector<USBUartChannel *> channels_{};
|
||||
|
||||
// Pool of pre-allocated data chunks to avoid dynamic allocation
|
||||
static constexpr int MAX_DATA_CHUNKS = 32;
|
||||
UsbDataChunk *data_chunk_pool_[MAX_DATA_CHUNKS];
|
||||
LockFreeQueue<UsbDataChunk, MAX_DATA_CHUNKS> free_chunks_;
|
||||
};
|
||||
|
||||
class USBUartTypeCdcAcm : public USBUartComponent {
|
||||
|
Reference in New Issue
Block a user