Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions components/usb_device/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,16 @@ Key methods:
- `bool write_hid_report(uint8_t report_id, std::span<const uint8_t> report, ...)` —
send a HID input report on the HID interrupt IN endpoint.
- `void set_cdc_receive_callback(...)` / `void set_vendor_receive_callback(...)`.
- `void set_mount_callback(...)` / `void set_unmount_callback(...)` — register
device mount / unmount handlers. `esp_tinyusb` owns the raw `tud_mount_cb` /
`tud_umount_cb`, so register here instead of defining those yourself (which
would be a duplicate symbol). On unmount the component first clears the vendor
+ CDC TX FIFOs — so a departed host's queued backlog is not delivered to the
next host that mounts — then invokes your callback.
- `size_t vendor_write_available() const` / `size_t cdc_write_available() const`
and `void vendor_write_clear()` / `void cdc_write_clear()` — TX-FIFO free space
and flush helpers (skip/defer or drop a streaming frame when the host stops
draining).
- `bool is_cdc_connected() const` / `bool is_vendor_connected() const` /
`bool is_hid_ready() const`.

Expand Down Expand Up @@ -192,3 +202,8 @@ the USB-Serial-JTAG peripheral.
- Only one `espp::UsbDevice` / `espp::UsbCdc` instance may exist at a time.
- The receive callbacks run in the TinyUSB device task; keep them short and
non-blocking.
- The TinyUSB device lifecycle callbacks (`tud_mount_cb` / `tud_umount_cb` /
`tud_suspend_cb` / `tud_resume_cb`) are owned by `esp_tinyusb`. Register mount
/ unmount handlers via `set_mount_callback()` / `set_unmount_callback()`
rather than defining those callbacks yourself. The mount / unmount handlers
also run in the TinyUSB device task.
Comment on lines +205 to +209
36 changes: 36 additions & 0 deletions components/usb_device/include/usb_device.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <vector>

#include "base_component.hpp"
#include "tinyusb.h" // for tinyusb_event_t (esp_tinyusb is already a REQUIRES dependency)

namespace espp {

Expand Down Expand Up @@ -56,6 +57,13 @@ namespace espp {
* \section usb_device_ex1 UsbDevice (composite CDC + Vendor/WebUSB) Example
* \snippet usb_cdc_example.cpp usb_cdc_example
*/

// Forward-declare the extern "C" trampoline (defined in usb_device.cpp, inside
// `namespace espp`) so the in-class friend declaration below refers to this
// existing C-linkage declaration instead of introducing a conflicting
// C++-linkage espp::espp_usb_device_event_cb.
extern "C" void espp_usb_device_event_cb(tinyusb_event_t *event, void *arg);

class UsbDevice : public BaseComponent {
public:
/**
Expand All @@ -64,6 +72,10 @@ class UsbDevice : public BaseComponent {
*/
using receive_callback_fn = std::function<void(std::span<const uint8_t> data)>;

/// @brief Callback for a device lifecycle event (mount / unmount). Invoked in
/// the TinyUSB device-task context.
using event_callback_fn = std::function<void()>;

/**
* @brief CDC-ACM (virtual serial port) function.
*
Expand Down Expand Up @@ -287,6 +299,19 @@ class UsbDevice : public BaseComponent {
/// @brief Set or replace the vendor receive callback (nullptr to detach).
void set_vendor_receive_callback(const receive_callback_fn &cb);

/// @brief Register a callback invoked when the device is mounted (the host has
/// configured it). Runs in the TinyUSB device-task context; nullptr
/// detaches. esp_tinyusb owns the raw tud_mount_cb, so applications
/// should register here rather than defining that callback themselves.
void set_mount_callback(const event_callback_fn &cb);
Comment thread
finger563 marked this conversation as resolved.

/// @brief Register a callback invoked when the device is unmounted (detached /
/// re-enumerated). The component clears the vendor + CDC TX FIFOs before
/// invoking it. Runs in the TinyUSB device-task context; nullptr
/// detaches. Register here instead of defining tud_umount_cb
/// (esp_tinyusb already defines it).
void set_unmount_callback(const event_callback_fn &cb);

/// @brief Whether initialize() has completed successfully.
bool is_initialized() const;

Expand Down Expand Up @@ -332,6 +357,15 @@ class UsbDevice : public BaseComponent {
static UsbDevice *instance();

private:
// Trampoline registered as tinyusb_config_t::event_cb; routes
// TINYUSB_EVENT_ATTACHED/DETACHED to the private handlers below.
friend void espp_usb_device_event_cb(tinyusb_event_t *event, void *arg);

/// @brief Internal: mount / unmount handling driven by esp_tinyusb's event_cb
/// (clears the TX FIFOs on unmount, then invokes the app callback).
void handle_usb_mount();
void handle_usb_unmount();

struct Impl; // holds TinyUSB descriptors, kept alive for driver lifetime
std::unique_ptr<Impl> impl_;

Expand All @@ -341,6 +375,8 @@ class UsbDevice : public BaseComponent {
std::mutex cb_mutex_;
receive_callback_fn on_cdc_receive_;
receive_callback_fn on_vendor_receive_;
event_callback_fn on_mount_;
event_callback_fn on_unmount_;

// Preallocated RX scratch buffers (sized in initialize()) so the TinyUSB-task
// RX handlers stay allocation-free (no heap churn on the hot path).
Expand Down
101 changes: 88 additions & 13 deletions components/usb_device/src/usb_device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -209,19 +209,30 @@ bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage,

#endif // CFG_TUD_VENDOR > 0

// Device unmount: drop any bytes still queued in the TX FIFOs. A host that goes
// away (cable pull / re-enumeration / suspend) leaves its unread backlog in the
// software FIFO; clearing it here means the next host to mount starts from an
// empty pipe and cannot mis-parse a stale frame as the reply to its first
// command. (An abrupt tab close does NOT unmount, so it does not reach here --
// that path relies on the streaming producer's own backpressure handling.)
void tud_umount_cb(void) {
#if (CFG_TUD_VENDOR > 0)
tud_vendor_write_clear();
#endif
#if (CFG_TUD_CDC > 0)
tud_cdc_n_write_clear(kCdcPort);
#endif
// NOTE: the TinyUSB device lifecycle callbacks (tud_mount_cb / tud_umount_cb /
// tud_suspend_cb / tud_resume_cb) are defined by esp_tinyusb itself, which
// forwards them to the tinyusb_config_t::event_cb we register in initialize().
Comment on lines +212 to +214
// Do NOT define tud_umount_cb here -- it would be a duplicate symbol. The
// unmount TX-FIFO clear + the app mount/unmount hooks live in the handlers
// below, driven by this event callback.
// cppcheck-suppress constParameterCallback // signature must match tinyusb_event_cb_t
extern "C" void espp_usb_device_event_cb(tinyusb_event_t *event, void *arg) {
// Runs in the TinyUSB device-task context: record it so a mount/unmount
// callback that calls write_cdc()/write_vendor() takes the non-blocking
// fail-fast TX path instead of vTaskDelay()-ing inside the TinyUSB task
// (which would deadlock USB servicing).
note_tinyusb_task();
// Load the teardown-guarded singleton (not event_arg): a destructor that has
// atomically detached the instance during teardown then yields nullptr here,
// matching the other tud_*_cb trampolines.
(void)arg;
auto *dev = s_device.load();
if (!dev || !event)
return;
if (event->id == TINYUSB_EVENT_ATTACHED)
dev->handle_usb_mount();
else if (event->id == TINYUSB_EVENT_DETACHED)
dev->handle_usb_unmount();
Comment on lines +232 to +235
}

#if (CFG_TUD_HID > 0)
Expand Down Expand Up @@ -289,6 +300,7 @@ const uint8_t *UsbDevice::hid_report_descriptor() const {
// ---------------------------------------------------------------------------

void UsbDevice::handle_cdc_rx() {
#if (CFG_TUD_CDC > 0)
receive_callback_fn cb;
{
std::scoped_lock lk(cb_mutex_);
Expand All @@ -310,6 +322,7 @@ void UsbDevice::handle_cdc_rx() {
if (rx_size > 0 && cb)
cb(std::span<const uint8_t>(buf.data(), rx_size));
} while (rx_size == buf.size());
#endif
}

void UsbDevice::handle_vendor_rx(const uint8_t *buffer, size_t bufsize) {
Expand Down Expand Up @@ -834,6 +847,12 @@ bool UsbDevice::initialize(std::error_code &ec) {
tusb_cfg.descriptor.qualifier = &impl_->qualifier_desc;
#endif

// Route esp_tinyusb's device lifecycle events (mount / unmount) to us so we
// can clear the TX FIFOs on unmount and invoke any app-registered callbacks.
// The callback loads the teardown-guarded s_device singleton itself, so no
// event_arg is needed.
tusb_cfg.event_cb = espp_usb_device_event_cb;
Comment on lines +850 to +854

// Register before installing so the BOS / vendor callbacks can find us.
// Claim the singleton slot ATOMICALLY: the null check at the top of
// initialize() is only a fast-fail, so two threads (or two instances) that
Expand Down Expand Up @@ -1143,18 +1162,68 @@ void UsbDevice::set_vendor_receive_callback(const receive_callback_fn &cb) {
on_vendor_receive_ = cb;
}

void UsbDevice::set_mount_callback(const event_callback_fn &cb) {
std::scoped_lock lk(cb_mutex_);
on_mount_ = cb;
}

void UsbDevice::set_unmount_callback(const event_callback_fn &cb) {
std::scoped_lock lk(cb_mutex_);
on_unmount_ = cb;
}

void UsbDevice::handle_usb_mount() {
event_callback_fn cb;
{
std::scoped_lock lk(cb_mutex_);
cb = on_mount_;
}
if (cb)
cb(); // runs in the TinyUSB task context
}

void UsbDevice::handle_usb_unmount() {
// Drop any bytes still queued in the TX FIFOs so the next host to mount starts
// from an empty pipe (a departed host's unread backlog otherwise lingers in
// the software FIFO and can be mis-parsed as a reply to the next host's first
// command).
#if (CFG_TUD_VENDOR > 0)
if (config_.vendor)
tud_vendor_write_clear();
#endif
#if (CFG_TUD_CDC > 0)
if (config_.cdc)
tud_cdc_n_write_clear(kCdcPort);
#endif
event_callback_fn cb;
{
std::scoped_lock lk(cb_mutex_);
cb = on_unmount_;
}
if (cb)
cb(); // runs in the TinyUSB task context
}

bool UsbDevice::is_initialized() const { return initialized_; }

bool UsbDevice::is_cdc_connected() const {
#if (CFG_TUD_CDC > 0)
if (!initialized_ || !config_.cdc)
return false;
return tud_cdc_n_connected(kCdcPort);
#else
return false;
#endif
}

bool UsbDevice::is_vendor_connected() const {
#if (CFG_TUD_VENDOR > 0)
if (!initialized_ || !config_.vendor)
return false;
return tud_mounted();
#else
return false;
#endif
}

size_t UsbDevice::vendor_write_available() const {
Expand All @@ -1168,9 +1237,13 @@ size_t UsbDevice::vendor_write_available() const {
}

size_t UsbDevice::cdc_write_available() const {
#if (CFG_TUD_CDC > 0)
if (!initialized_ || !config_.cdc || !tud_mounted())
return 0;
return tud_cdc_n_write_available(kCdcPort);
#else
return 0;
#endif
}

void UsbDevice::vendor_write_clear() {
Expand All @@ -1181,8 +1254,10 @@ void UsbDevice::vendor_write_clear() {
}

void UsbDevice::cdc_write_clear() {
#if (CFG_TUD_CDC > 0)
if (initialized_ && config_.cdc)
tud_cdc_n_write_clear(kCdcPort);
#endif
}

bool UsbDevice::is_hid_ready() const {
Expand Down
7 changes: 7 additions & 0 deletions doc/en/buses/usb_cdc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,13 @@ Notes
(the TinyUSB stack and the BOS / vendor control callbacks are global).
- The receive callbacks run in the TinyUSB device task; keep them short and
non-blocking. It is safe to call the matching ``write_*()`` from within them.
- The TinyUSB device lifecycle callbacks (``tud_mount_cb`` / ``tud_umount_cb`` /
``tud_suspend_cb`` / ``tud_resume_cb``) are owned by ``esp_tinyusb``. Register
mount / unmount handlers via ``set_mount_callback()`` / ``set_unmount_callback()``
rather than defining those callbacks yourself (which would be a duplicate
symbol). On unmount the component clears the vendor + CDC TX FIFOs — so a
Comment on lines +192 to +196
departed host's queued backlog is not delivered to the next host that mounts —
before invoking your callback; both handlers run in the TinyUSB device task.
- The WebUSB landing-page URL is configured *without* a scheme; the scheme is
encoded separately via ``VendorFunction::url_scheme`` (0 = http, 1 = https).

Expand Down
Loading