Skip to content

USB Host: Unplugging device can break usb.core.find() #10563

@samblenny

Description

@samblenny

CircuitPython version and board name

Adafruit CircuitPython 10.0.0-beta.2 on 2025-07-30; Adafruit Fruit Jam with rp2350b

Code/REPL

import displayio
import gc
import usb
from usb.core import USBError, USBTimeoutError


def test_unplug_during_find():
    # This tests how repeated calls to usb.core.find() behave in the context of
    # unplugging a USB device. Initially, before a device is plugged in for the
    # first time, the generator produces no iterator items. When you plug in a
    # device, it will produce one item, usually with a valid descriptor. When
    # you unplug that device, find() breaks and starts always yielding one item
    # with an all-zeros descriptor. It keeps doing that if you plug the device
    # back in.
    cache = {}
    print("Finding USB devices...")
    consecutive_all_zeros = 0
    while True:
        try:
            valid_devices = 0
            for device in usb.core.find(find_all=True):
                # 1. Read device descriptor
                desc = get_desc(device, 0x01, length=18)
                # 2. Check if descriptor is all zeros. This happens when I
                #    unplug a device while find() is active. In that case, as
                #    long as no devices are plugged in, find()'s generator will
                #    always provide 1 device with an all-zero descriptor.
                if all((byte_==0 for byte_ in desc)):
                    consecutive_all_zeros += 1
                    if consecutive_all_zeros % 50 == 1:
                        print("Consecutive all zero descriptor count",
                            consecutive_all_zeros)
                    continue
                else:
                    consecutive_all_zeros = 0
                    valid_devices += 1
                # 3. If it's already in the cache, skip device
                key_ = tuple(desc)
                if key_ in cache:
                    continue
                # 3. Otherwise, print properties and cache descriptor
                cache[key_] = True
                print_descriptor_properties(device)
                print(cache)
            if valid_devices == 0 and len(cache) > 0:
                # find() found no devices, so clear cache
                print("Clearing Cache")
                cache.clear()
        except USBError as e:
            print(f"find USBError: .errno={e.errno} '{str(e)}'")
        except USBTimeoutError as e:
            print(f"find USBError: .errno={e.errno} '{str(e)}'")

def get_desc(device, desc_type, length=256):
    # Read USB descriptor of type specified by desc_type (wIndex always 0).
    data = bytearray(length)
    bmRequestType = 0x80
    wValue = desc_type << 8
    wIndex = 0
    device.ctrl_transfer(bmRequestType, 6, wValue, wIndex, data, 300)
    return data

def print_descriptor_properties(device):
    print()
    print(f"idVendor      {device.idVendor:04x}")
    print(f"idProduct     {device.idProduct:04x}")
    print(f"product       {device.product}")
    print(f"manufacturer  {device.manufacturer}")

# Run the test
displayio.release_displays()
gc.collect()
test_unplug_during_find()

Behavior

This is an example serial console log from running the code above on a Fruit Jam rev D board with an Adafruit generic SNES style gamepad. The problem is that usb.core.find() can get into a state where it always finds just one device with an invalid (all zero) descriptor.

code.py output:
Finding USB devices...

idVendor      081f
idProduct     e401
product       USB gamepad           
manufacturer  None
{(18, 1, 0, 1, 0, 0, 0, 8, 31, 8, 1, 228, 6, 1, 0, 2, 0, 1): True}
Consecutive all zero descriptor count 1
Clearing Cache
Consecutive all zero descriptor count 51
Consecutive all zero descriptor count 101
Consecutive all zero descriptor count 151
Consecutive all zero descriptor count 201
Consecutive all zero descriptor count 251
Consecutive all zero descriptor count 301
Consecutive all zero descriptor count 351
Consecutive all zero descriptor count 401
...
  1. When the code starts with no USB device plugged in, you should just see Finding USB devices.... At this point, the loop with for device in usb.core.find(find_all=True): ... is not doing anything because find() does yield any values for device. This is good, because no device is plugged in yet.
  2. When you first plug in a device, the for loop code will print that device's descriptor properties and add the descriptor to a cache. The loop checks the cache to avoid spamming the serial console with repeated prints of the same device descriptor details. Note that at this point find() is still getting called rapidly inside of the while True: ... loop as we're trying to trigger a race condition.
  3. When you unplug the gamepad, the code prints Consecutive all zero descriptor count 1 and Clearing Cache. At this point, find() is broken and it begins to always return one device with an all zero descriptor. This continues until you reload the code.
  4. If you plug the gamepad back in (or some other USB device), find() does not find it. Instead it just gives an invalid device with an all zero descriptor (that's the source of the messages like Consecutive all zero descriptor count 201, ...251, etc. I put a % 50 rate limiter on the prints to avoid spamming the serial console)

Expected Behavior:

  1. Unplugging the USB device should cause find() to begin generating an empty iterator with no items. (not a 1-item iterator with an invalid descriptor)
  2. When you plug the USB device back in, find() should find it, and the descriptor bytes should be valid.

Description

No response

Additional information

This may sound like it's related to #10552 (USB Host: Unreliable Unplug Event Detection), in that they both involve unplug behavior. But, there are two different things going on.

  1. The other issue is about a problem with the API where usb.core.USBError.errno and usb.core.USBTimeoutError.errno don't get set in a way that lets you distinguish an unplug event from other unrelated timeouts.

  2. This issue is about what looks like a race condition where usb.core.find() can get totally broken if you happen to unplug a USB device while find() is finding devices.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions