Closed todbot closed 4 months ago
thanks for submitting this, @todbot I'm trying to add button presses for the ESP32-S3 reverse TFT Feather's D0, D1, D2 buttons for things like UI and to send MIDI panic, while using the USB MIDI Host function on the attached MAX3421e Feather Wing
@jedgarpark Are you using keypad
for the buttons? That will work if the MAX3241e reads don't suppress interrupts: I would assume they don't.
The problem is that code flow entirely blocks on .read(). If there’s no incoming data, the call to keys.events.get()
never gets called.
@dhalbert I was using debouncer, but didn't try keypad. I tried doing it with asyncio as well, but still was being blocked until a MIDI message came through.
Debouncer will not try to read the switch until its async task runs. keypad will read the switch transition immediately and save it in the queue, but to read the queue, you still have to wait and not be blocked.
@dhalbert ok, i've set it to use keypad. it now can show me a queued button press occurred the next time i play a MIDI note. not useable for how i'm planning to use buttons in this MIDI monitor project, but good to know keypad can queue up presses
The usb.core.Device.read()
also blocks on RP2040 PIO USB host.
Here's an example that should print out "doing UI things" every 0.1 seconds but blocks on read()
.
import time
import board
import usb.core
import usb_host
import adafruit_usb_host_midi
usb_host.Port(board.MOSI, board.MISO) # GP3,4 on QTPy RP2040; GP16,17 on RP2040 USB Host Feather
print("Looking for midi device")
raw_midi = None
while raw_midi is None:
for device in usb.core.find(find_all=True):
try:
raw_midi = adafruit_usb_host_midi.MIDI(device)
print("Found vid/pid %04x/%04x" % (device.idVendor, device.idProduct),
device.manufacturer, device.product)
except ValueError:
continue
time.sleep(0.1)
last_time = 0
while True:
midi_bytes = raw_midi.read(1) # this blocks instead of returning 0 or None
if midi_bytes:
print(time.monotonic(), "midi:",["0x%02x" % b for b in midi_bytes])
if time.monotonic() - last_time > 0.1:
last_time = time.monotonic()
print(last_time, "doing UI things...")
The read method is documented as having a timeout argument in milliseconds. Did y'all give that a try? https://docs.circuitpython.org/en/latest/shared-bindings/usb/core/index.html#usb.core.Device.read
oh I see that the midi library is an intermediate here and probably can't provide the timeout argument. well, that complicates things.
Also from the first post:
There is a non-standard timeout argument to the usb.core.Device.read() and usb.core.Device.write() methods. However, when specifying a timeout on .read() (by modifying adafruit_usb_host_midi.MIDI.read()), the .read() throws an unattributed usb.core.USBError, instead of the expected return value of None or 0. Since the exception is unattributed, there's no wayt to distinguish a read timeout from a device unplug or other USB error.
At least it looks like the timeout error should be possible to distinguish, it is of type USBTimeoutError, while all other errors are of type USBError. Both derive from OSError. It's true they don't have any relevant attributes, but an except clause should be able to catch them as different things, by having the except clause for the more specific USBTimeoutError first.
I do agree it should just maybe just return instead of raising an error.
fbofw I think this module strives for compatibility with pyusb
Unfortunately, adding a try/except for usb.core.USBTimeoutError
doesn't work. I've modified adafruit_usb_host_midi
to accept a timeout in its constructor that it passes to usb.core.Device.read()
, and the USBTimeoutError is thrown once then a USBError is thrown.
Full gist here: https://gist.github.com/todbot/07a2ef16ec9ee83eb8980f8a128d660b Tested on a Feather TFT ESP32-S2 with MAX3421E FeatherWing.
USBTimeoutError is thrown once then a USBError is thrown.
Ah, I see.
The build in #9240 may be helpful for further diagnosis. I tried to mark each site in the source that might be throwing the USBError exception with a different string. so, what string is shown with the 2nd exception that "should have been" USBTimeoutError?
The build in #9240 may be helpful for further diagnosis. I tried to mark each site in the source that might be throwing the USBError exception with a different string. so, what string is shown with the 2nd exception that "should have been" USBTimeoutError?
Thanks. I tried the artifact from PR #9240 and the result is usb.core.USBError: tuh_edpt_xfer() failed
after one usb.core.USBTimeoutError
.
I think we'll need @hathach to look into this because we try to abort the pending transfer when we reach the timeout: https://github.com/adafruit/circuitpython/blob/ca2a24e8d0f4dcc1b9e5fa28e029bf2ffad19dbc/shared-module/usb/core/Device.c#L201-L204
This should allow the subsequent call to work ok. It may be weird because our transaction is being NAKed by the device each SOF.
The USBTimeoutError is pyusb behavior it does here: https://github.com/pyusb/pyusb/blob/4a433a5fddd6dcbc632454d53f57e020b0b5513b/usb/backend/libusb0.py#L445-L446
ah, abort xfer isn't implemented on max3421 just yet, though it should work with pio-usb. Let me try to reproduce and double check with the rp2040 first.
ah, abort xfer isn't implemented on max3421 just yet, though it should work with pio-usb. Let me try to reproduce and double check with the rp2040 first.
I believe the RP2040 PIO host does implement abort xfer. At least the above CircuitPython code works as expected on rp2040 and doesn’t throw the error.
@todbot sorry, I misread your above statement with rp2040 as ""it does not work with pio-usb as well". PR https://github.com/hathach/tinyusb/pull/2646 implements abort xfer for max3421e. It should work, please try it out to see if that work for you
Hi @hathach, thanks for the patch! It fixes the issue so now max3421e
USB host mode behaves the same as usb_host.Port
PIO USB host mode on RP2040: timing out instead of erroring.
To test, I make a local build of CircuitPython for adafruit_feather_esp32s2_tft
incorporating this update to tinyusb and @jepler's labeled USBErrors. My test code now times out in the same manner as similar code using usb_host
.
I guess when CircuitPython updates TinyUSB next, this issue can be closed.
One thing I can notice now though with the max3421e
module: the minimum timeout passed to usb.core.Device.read()
seems to be 100 milliseconds. Longer timeouts work, but shorter timeouts do not. Shorter timeouts are sort of critical for doing anything useful.
What happens with shorter timeouts?
What happens with shorter timeouts?
Any timeout less than 100ms results in a 100ms timeout.
What happens with shorter timeouts?
Any timeout less than 100ms results in a 100ms timeout.
Does it work correctly on RP2-PIO USB host? The timeout code is the same (but background tasks or timekeeping could be messing us up.)
What happens with shorter timeouts? Any timeout less than 100ms results in a 100ms timeout. Does it work correctly on RP2-PIO USB host? The timeout code is the same (but background tasks or timekeeping could be messing us up.)
Ignore my reported 100ms timeout error. The error was in the todbot (me), not the code.
Both the RP2 PIO USB Host (usb_host.Port
) and the MAX3421E USB Host (max3421e
) now heed a timeout in usb.core.Device.read()
. Yay! With this and adding timeout to adafruit_usb_host_midi
to give it more typical stream read semantics, it can be used non-blocking with adafruit_midi
. Thus I think this issue is resolved. (I can submit a PR onadafruit_usb_host_midi
if you like)
btw, when specifying the minimum read timeout (1 millisecond), PIO USB does take 1ms, but with the max3421e it takes 2-3 milliseconds on average. Not deal-breaking, but something I wanted to note.
Fixed by #9252. Thanks @todbot !
glad that works out, feel free to pin me whenever you need my help
CircuitPython version
Code/REPL
Behavior
Code blocks on read(), only printing "this should print 10 times a second" when a byte is read.
Description
The code demonstrates how
device.read()
blocks infinitely, which is different from established behavior.The expected semantics of
device.read()
when using eitherbusio.UART
orusb_midi.PortIn
is that the read() does not noticably block if a smalltimeout
is given. (a timeout given explicitly in the constructor ofbusio.UART
, a common practice, and implicitly in howusb_midi
works)But for the
usb.core.Device
that is returned and used inadafruit_usb_host_midi
, thedevice.read()
blocks forever. Usingasyncio
orselect
does not help with this.There is a non-standard
timeout
argument to theusb.core.Device.read()
andusb.core.Device.write()
methods. However, when specifying a timeout on.read()
(by modifyingadafruit_usb_host_midi.MIDI.read())
, the .read() throws an unattributedusb.core.USBError
, instead of the expected return value of None or 0. Since the exception is unattributed, there's no wayt to distinguish a read timeout from a device unplug or other USB error.Additional information
Tested on USB Host Feather Wing and ESP32-S2 TFT Feather and ESP32-S3 Reverse TFT Feather. Discovered by @jedgarpark so tagging him here too.