Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

rianadon
Copy link

This is a module wrapping TinyUSB's class/cdc/cdc_host library into a pyserial-like API so that a CircuitPython board with a USB Host can talk to other devices with CDC interfaces over USB.

It is possible to talk CDC using only the usb.core.Device.read/.write methods. However, this only works in blocking mode. I tried setting up asynchronous communication by supplying a timeout to .read(), but when I tested my code I missed data.
As I understand, CDC reads are performed by sending an IN packet, then waiting for the device to eventually send a DATA packet back. This transaction cannot be cancelled halfway through, so even if CircuitPython times out and moves onto other things, the data transfer is going to happen regardless.

I think the only way to build an asynchronous API around sending data through the usb.core library is to use FIFOs so that even if CircuitPython times out and moves on, the data is still appended to the FIFO and can be read back later. This is how TinyUSB's Endpoint Stream API works. The class/cdc/cdc_host library I am wrapping internally uses Endpoint Streams for the rx and tx endpoints.

I was debating building a module around the cdc host or endpoint streams and decided against endpoint streams since they are only exposed in TinyUSB's private headers. They don't require a significant amount of code to implement though, so if you think that it is better to build a lower-level library than a higher-level library I could submit that as a PR instead. I have most of the code written, but it does not work yet.

Copy link
Member

@tannewt tannewt left a comment

Choose a reason for hiding this comment

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

I really like the pyserial approach! I'd go 100% into it and match APIs exactly and then call it serial. That way code will just work in CPython too.

You'll want to conditionalize enabling it so that builds without USB Host don't enable it. That should fix the CI.

msgstr ""

#: shared-bindings/usb/cdc_host/__init__.c
msgid "device must be a usb.core.Device object"
Copy link
Member

Choose a reason for hiding this comment

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

Instead of adding a new message, reuse "%q must be of type %q, not %q". (I'd search for where it is used to copy it's example.)

Comment on lines +788 to +789
usb/cdc_host/Serial.c \
usb/cdc_host/__init__.c \
Copy link
Member

Choose a reason for hiding this comment

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

Likely needs to match the other whitespace.

static const mp_rom_map_elem_t usb_cdc_host_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_usb_dot_cdc_host) },
{ MP_ROM_QSTR(MP_QSTR_Serial), MP_ROM_PTR(&usb_cdc_host_serial_type) },
{ MP_ROM_QSTR(MP_QSTR_find), MP_ROM_PTR(&usb_cdc_host_find_obj) },
Copy link
Member

Choose a reason for hiding this comment

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

Don't add a find. Instead, match pyserial exactly with: https://pyserial.readthedocs.io/en/latest/tools.html#serial.tools.list_ports.comports

@rianadon
Copy link
Author

Thanks for the feedback!

I was thinking some more about the tradeoffs between implementing the low level endpoint API vs high level serial API, and I changed my mind and am leaning more towards the EndpointStream for a few reasons:

  • An API that dynamically allocates a fifo for reading from an endpoint would support infinite endpoints. However, with TinyUSB's CDC class, the number of USB communication ports supported on the host depends on CFG_TUH_CDC. You can increase it to deal with devices that spawn many CDC ports, but this comes at the expense of memory.
  • EndpointStream solves non-blocking USB for any device, not just serial. This might be useful for keyboards, midi, etc. The pyserial-compatible API could be provided by a Python module wrapping the EndpointStream.
  • It requires fewer functions to implement.

Curious to hear your thoughts. I can close this and open a new PR.

@tannewt
Copy link
Member

tannewt commented Apr 28, 2025

It's up to you what you'd like to implement.

I'd lean against EndpointStream because it'd be a new unique API. This higher level has the advantage of matching the existing pyserial api. The low level core APIs match pyusb in CPython.

Is there a pyusb version of EndpointStream?

@rianadon
Copy link
Author

Good point. There's nothing existing, and it seems pyusb is blocking only. You'd need to use threads to do USB communication in the background. There is a PR to pyusb to make it async and add a submit_read/submit_write that return asyncio.Future objects, but it's not very far along. It seems by far the simplest and more Pythonic, but from my understanding there's very few modules in CircuitPython leveraging asyncio, so maybe it's not a good fit here.

I can continue to work on the pyserial approach.

@tannewt
Copy link
Member

tannewt commented Apr 29, 2025

Thanks! I think pyserial is best. It feels like the defacto way to do host serial.

@dhalbert
Copy link
Collaborator

Worth looking at #9365 and #10019. pyserial has some idiosyncrasies in its API which are not the greatest, so I ended up doing something slightly different for usb_cdc.

@jramboz
Copy link

jramboz commented Oct 9, 2025

Forgive me if this is a silly question, but does this enable USB Host support, or does it still require the usb_host module?

@samblenny
Copy link

Forgive me if this is a silly question, but does this enable USB Host support, or does it still require the usb_host module?

This PR is about adding better support for USB serial devices when they are connected to the USB host port of a CircuitPython board with USB host capability. The existing way of doing USB host stuff is to use the usb module, which mostly follows the PyUSB API. There are also some higher level CircuitPython helper modules that make it easier to use keyboards, mice, gamepads, and such.

Despite the name which suggests otherwise, the usb_host module is mostly not important for interacting with USB host devices in CircuitPython. Its purpose is for changing USB host settings that usually don't need to be changed.

If you want to learn about writing CircuitPython code for USB host stuff, the best places to ask questions are the Adafruit Forums or the #help-with-circuitPython channel on Adafruit's Discord. You could also search for "USB host" on the Adafruit Learning System or the Adafruit Playground.

@jramboz
Copy link

jramboz commented Oct 10, 2025

Thank you for the response! I've asked on the Discord and hopefully I'll get a response. I won't derail the thread here, but essentially I'm trying to use a Seeed XIAO ESP32S3 as a USB host to connect to a device that uses serial communication. And I'm having a horrible time trying to figure out how to do it (every time I try to do usb.core.find(find_all=True), I get RuntimeError: No usb host port initialized).

Something like you've proposed here would probably make my life much easier once I can actually communicate with the device :)

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.

5 participants