Skip to content

fix(usb_device): don't redefine tud_umount_cb (esp_tinyusb owns it); add mount/unmount hooks - #779

Merged
finger563 merged 5 commits into
mainfrom
fix/usb-device-lifecycle-callbacks
Sep 5, 2026
Merged

fix(usb_device): don't redefine tud_umount_cb (esp_tinyusb owns it); add mount/unmount hooks#779
finger563 merged 5 commits into
mainfrom
fix/usb-device-lifecycle-callbacks

Conversation

@finger563

Copy link
Copy Markdown
Contributor

The break

#774 added a tud_umount_cb definition to usb_device.cpp, but esp_tinyusb already defines it (external/esp-usb/device/esp_tinyusb/tinyusb.c:87):

multiple definition of `tud_umount_cb';
usb_device.cpp:218: first defined here

That fails the link on every manager-off USB example (bldc_haptics, can_bridge, coredump, mcp266, ota, usb_device) — visible on #776's CI.

The fix

esp_tinyusb owns the TinyUSB device lifecycle callbacks (tud_mount_cb/tud_umount_cb/…) and forwards them to a tinyusb_config_t::event_cb. So:

  • Remove usb_device's tud_umount_cb definition.
  • Register an event_cb (event_arg = this) in initialize() and route TINYUSB_EVENT_ATTACHED/DETACHED to handle_usb_mount() / handle_usb_unmount().
  • handle_usb_unmount() clears the vendor + CDC TX FIFOs (the behavior feat(usb_device): TX FIFO space queries + clear; clear stale TX on unmount #774 intended) and then invokes an app callback.
  • Expose set_mount_callback() / set_unmount_callback() so applications register their handlers through UsbDevice instead of defining tud_*_cb (which would also collide with esp_tinyusb).

Also (requested)

CFG guards around the remaining CDC/vendor method bodies — is_cdc_connected, is_vendor_connected, cdc_write_available, cdc_write_clear, handle_cdc_rx — returning a safe default when the interface is compiled out, matching write_vendor/vendor_write_* which were already guarded.

No example changes are needed — the collision was purely usb_device ↔ esp_tinyusb.

🤖 Generated with Claude Code

…d mount/unmount hooks

#774 added a tud_umount_cb definition to usb_device.cpp, but esp_tinyusb already
defines it (tinyusb.c) -> "multiple definition of tud_umount_cb", breaking every
manager-off USB example link (bldc_haptics, can_bridge, coredump, mcp266, ota,
usb_device).

esp_tinyusb owns the TinyUSB device lifecycle callbacks and forwards them to a
tinyusb_config_t::event_cb, so:
- remove usb_device's tud_umount_cb definition;
- register an event_cb (event_arg = this) and route ATTACHED/DETACHED to
  handle_usb_mount()/handle_usb_unmount();
- handle_usb_unmount() clears the vendor + CDC TX FIFOs (the behavior #774
  intended) then invokes an app callback;
- expose set_mount_callback()/set_unmount_callback() so applications register
  their mount/unmount handlers via UsbDevice instead of defining tud_*_cb
  (which would also collide with esp_tinyusb).

Also (requested): add CFG_TUD_CDC / CFG_TUD_VENDOR guards around the remaining
CDC/vendor method bodies (is_cdc_connected, is_vendor_connected,
cdc_write_available, cdc_write_clear, handle_cdc_rx), returning a safe default
when the interface is disabled -- matching write_vendor / vendor_write_* which
were already guarded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 5, 2026 13:31
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

✅Static analysis result - no issues found! ✅

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new event_cb path doesn’t call note_tinyusb_task(), which can make write_*() mis-detect TinyUSB-task context and block inside the USB task when invoked from mount/unmount callbacks.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Fixes a link-time multiple-definition failure caused by defining tud_umount_cb inside usb_device.cpp even though esp_tinyusb already provides the TinyUSB lifecycle callbacks, and adds a supported mechanism for applications to hook mount/unmount events through UsbDevice.

Changes:

  • Remove the local tud_umount_cb implementation and route lifecycle events via tinyusb_config_t::event_cb.
  • Add set_mount_callback() / set_unmount_callback() APIs and internal mount/unmount handlers (including TX FIFO clearing on unmount).
  • Add compile-time guards for CDC/vendor helper methods so they return safe defaults when the interface is compiled out.
File summaries
File Description
components/usb_device/src/usb_device.cpp Removes duplicate TinyUSB callback, registers event_cb, adds mount/unmount handling and additional CFG guards.
components/usb_device/include/usb_device.hpp Exposes mount/unmount callback API and declares internal mount/unmount handler methods.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread components/usb_device/src/usb_device.cpp
…lback

espp_usb_device_event_cb runs in the TinyUSB device task; call note_tinyusb_task()
first (like the other tud_*_cb callbacks) so a mount/unmount callback that writes
via write_cdc()/write_vendor() takes the non-blocking fail-fast TX path rather
than vTaskDelay()-ing inside the TinyUSB task and deadlocking USB servicing.
Addresses the review note on #779.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The lifecycle trampoline bypasses teardown routing and TinyUSB-task detection, and its public documentation inaccurately includes suspension.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

components/usb_device/include/usb_device.hpp:304

  • The unmount callback is not invoked for suspension: this implementation dispatches it only for TINYUSB_EVENT_DETACHED, while esp_tinyusb exposes suspension as a distinct optional TINYUSB_EVENT_SUSPENDED. Remove “suspended” from this API contract (or add separate suspend handling rather than treating a temporary suspend as unmount).
  /// @brief Register a callback invoked when the device is unmounted (detached /
  ///        re-enumerated / suspended). 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).
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread components/usb_device/src/usb_device.cpp
Comment thread components/usb_device/include/usb_device.hpp
…he lifecycle API

Follow-ups on #779:
- espp_usb_device_event_cb now loads the teardown-guarded s_device singleton
  instead of using event_arg, so a destructor that has atomically detached the
  instance yields nullptr here (matching the other tud_*_cb trampolines);
  dropped the now-unused tusb_cfg.event_arg.
- set_unmount_callback docs no longer claim "suspended" (only ATTACHED/DETACHED
  are routed to mount/unmount).
- documented the new set_mount_callback/set_unmount_callback + vendor/cdc
  write_available/write_clear helpers and the automatic unmount TX-FIFO clear in
  the component README (Key methods, Notes) and the usb_cdc RST (Notes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@finger563
finger563 requested a balanced review from Copilot September 5, 2026 14:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

Comment thread components/usb_device/include/usb_device.hpp Outdated
Comment on lines +232 to +235
if (event->id == TINYUSB_EVENT_ATTACHED)
dev->handle_usb_mount();
else if (event->id == TINYUSB_EVENT_DETACHED)
dev->handle_usb_unmount();
Comment on lines +850 to +854
// 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;
…ampoline

Co-authored-by: finger563 <213467+finger563@users.noreply.github.com>
@finger563

Copy link
Copy Markdown
Contributor Author

@copilot fix the build failures you introduced

…nding it

Co-authored-by: finger563 <213467+finger563@users.noreply.github.com>

Copilot AI commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

@copilot fix the build failures you introduced

Fixed in a5... (see latest commit): the bare friend void espp_usb_device_event_cb(...) inside the class declared a new C++-linkage espp::espp_usb_device_event_cb, conflicting with the extern "C" definition in usb_device.cpp ("conflicting declaration ... with 'C' linkage"). Added an extern "C" forward declaration inside namespace espp before the class so the friend declaration binds to the existing C-linkage name instead. Verified the pattern compiles with a standalone repro.

@finger563
finger563 requested a balanced review from Copilot September 5, 2026 18:54
@finger563
finger563 merged commit a08fc9f into main Sep 5, 2026
157 checks passed
@finger563
finger563 deleted the fix/usb-device-lifecycle-callbacks branch September 5, 2026 18:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Mount delivery has an initialization race, and lifecycle callback ownership is documented inaccurately.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

components/usb_device/src/usb_device.cpp:854

  • esp_tinyusb starts the TinyUSB task before copying tinyusb_config_t::event_cb into its global context. With a host already connected, the task can process tud_mount_cb() during that window, so this callback is still null and the application's first mount notification is lost; on a reinstall, a stale callback can instead dispatch before CDC setup and before initialized_ is set. Please track/reconcile the mounted state and deliver the initial mount exactly once only after function initialization is complete.
  tusb_cfg.event_cb = espp_usb_device_event_cb;
  • Files reviewed: 4/4 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment on lines +205 to +209
- 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 +212 to +214
// 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 thread doc/en/buses/usb_cdc.rst
Comment on lines +192 to +196
- 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants