-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Description
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
...
- When the code starts with no USB device plugged in, you should just see
Finding USB devices...
. At this point, the loop withfor device in usb.core.find(find_all=True): ...
is not doing anything becausefind()
does yield any values fordevice
. This is good, because no device is plugged in yet. - 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 pointfind()
is still getting called rapidly inside of thewhile True: ...
loop as we're trying to trigger a race condition. - When you unplug the gamepad, the code prints
Consecutive all zero descriptor count 1
andClearing 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. - 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 likeConsecutive all zero descriptor count 201
,...251
, etc. I put a% 50
rate limiter on the prints to avoid spamming the serial console)
Expected Behavior:
- 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) - 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.
-
The other issue is about a problem with the API where
usb.core.USBError.errno
andusb.core.USBTimeoutError.errno
don't get set in a way that lets you distinguish an unplug event from other unrelated timeouts. -
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 whilefind()
is finding devices.