You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
UVC device: an S3 host's VS_PROBE requests are stalled by a P4 webcam at full speed #116
An ESP32-P4 running usbif_webcam.py enumerates on an ESP32-S3 host (cdc+uvc, 242-byte configuration), but it stalls every class request to its video streaming interface, so uvc_display.py on the S3 stops at the first negotiation:
camera is device 1
uncompressed YUY2 only (no jpegio, or PREFER_MJPEG is False)
E (9366) USBH: Dev 1 EP 0 STALL
OSError: host_uvc_negotiate failed (-2)
-2 is the SET_CUR on VS_PROBE_CONTROL. The descriptors parse as expected: VC on interface 2, VS on interface 3 (alt 1 an isochronous IN at 0x83, 512 bytes), one YUY2 format with one 160x120 frame at a continuous 10 fps (min = max = 1000000). The probe stalls with format 1 / frame 1 / interval 1000000, with interval 0, and with format 0 / frame 0. TinyUSB's VS probe handler accepts format 0 before it validates anything, so the request seems never to reach it. Either the video driver isn't matching interface 3 to its streaming interface, or the request is refused before the class driver.
The webcam last worked against Windows on 2026-09-04, at high speed. Here the P4 runs at full speed, cabled straight into the S3's native port (ESP32-S3-Touch-LCD-7, MicroPython 1.29 with usbif 452c99f; the P4 on its 10-09 build of main). Since September the costumes have been renumbered per costume (#25), which also moves the video interfaces.
To reproduce: run usbif_webcam.py on the P4 and uvc_display.py on the S3. Checking whether the same P4 streams to Windows at high speed would split "full speed" from "a regression since September".
The same P4 didn't stream to Windows at high speed either, so this wasn't only full speed. There turned out to be two separate faults, one in the webcam's descriptor and one in how usbif's host starts a camera.
Windows, high speed: fixed in PyDevices/usbif#89. A P4 wearing cdc+uvc enumerated as a camera, but no application could open it. DirectShow said "Could not RenderStream to connect pins", and the board never saw the stream open. The cause was the per-costume endpoint numbering from PyDevices/usbif#25. It moves the video endpoint (to 0x83 in cdc+uvc), but the VideoStreaming input header still named the build-time address, 0x87, and the host opens the pipe the header names. The one costume that worked was all six functions, because that one puts the endpoint back on 0x87.
After PyDevices/usbif#89 the header moves with the endpoint. costume_selftest.py now catches a mismatch: with the fix taken out, it fails exactly the 31 camera costumes whose endpoint moves. On the P4 (Waveshare ESP32-P4-WIFI6-Touch-LCD-4B) with the fix, usbif_webcam.py in cdc+uvc streams to Windows over a high-speed link. ffmpeg's dshow input captured 323 frames in 6 s, every one new, with the colour bars 2 px further along each time.
PyDevices/usbif#90 also gives camera costumes their own product id. Before it, cdc+uvc enumerated as 0x4001, the same as bare CDC.
The S3 host's stalled probe: still open. I haven't tested this half on an S3 yet. The stall you saw fits TinyUSB's video driver: it doesn't know the streaming interface's current alternate setting until the host sends SET_INTERFACE to it. Until then, its probe handler has no streaming descriptor to check the request against, so it stalls every VS_PROBE, whatever the format and frame. That's why format 0 / frame 0 stalled too. Windows and Linux send SET_INTERFACE(VS, 0) before they probe (Linux's uvcvideo does it on purpose, for cameras that misbehave without it). host_uvc_negotiate() probes first, and only selects an alternate setting later, in host_uvc_open(). The next step is to send SET_INTERFACE(itf, 0) at the start of host_uvc_negotiate() and try uvc_display.py on the S3 against a P4 with PyDevices/usbif#89 in it.
This no longer happens, and the code it happened in is gone.
I ran the same pairing on 2026-10-20: an ESP32-S3-Touch-LCD-4.3 as the host (MicroPython 1.30 preview, usb.host.video from pydevices main) and the Waveshare P4 panel as the webcam (usb-device-video's CameraInterface, colour bars, 160x120 YUY2 at 10 fps, so the same VC on interface 2 and VS on interface 3 with the isochronous IN at 0x83). The P4 was reached through the S3's powered hub rather than straight into its port, which I didn't change. The device enumerated at full speed and negotiated on the first try:
speed 2 0x303a 0x4001 e8f60ae0f070f24f
probe fmt 1 (512, 38400) # PROBE, GET_CUR, COMMIT at alternate setting 0
probe fmt 0 (512, 38400) # format 0 / frame 0 now answers too
Camera then picked the 160x120 mode and streamed 28 frames in 3 s (9.3 fps against the 10 asked for). No SET_INTERFACE is sent before the probe; Camera.start() selects the alternate setting only after negotiating, as host_uvc_negotiate() did.
The cause was on the device side, as the earlier comment guessed. The stalls came from TinyUSB's own video class driver, which answers VS_PROBE only once the streaming interface has been opened with SET_INTERFACE. usb-device-video replaced that driver on 2026-10-16 (the UVC controls are answered in Python on machine.USBDevice, and the video class driver is no longer part of the firmware), so a probe at alternate setting 0 is accepted whatever the format and frame. Windows and Linux were never affected, because both send SET_INTERFACE(VS, 0) before they probe.
I've left the host as it is. Sending SET_INTERFACE(itf, 0) first would only help a camera that needs it, and none of the ones tried does. Reopen this if a camera stalls the probe, and the fix goes at the top of usb.host.video._negotiate.
Closing as not reproducible on current firmware. The stall came from TinyUSB's own video class driver, which only answered a probe after SET_INTERFACE. Since usb-device-video replaced it, that driver is no longer in the path: an S3 host's PROBE, GET_CUR and COMMIT at alternate setting 0 complete, and the stream runs (28 frames in 3 s). Reopen with a log if a probe stalls again.
Activity
bdbarnett commentedon Oct 13, 2026
The same P4 didn't stream to Windows at high speed either, so this wasn't only full speed. There turned out to be two separate faults, one in the webcam's descriptor and one in how usbif's host starts a camera.
Windows, high speed: fixed in PyDevices/usbif#89. A P4 wearing
cdc+uvcenumerated as a camera, but no application could open it. DirectShow said "Could not RenderStream to connect pins", and the board never saw the stream open. The cause was the per-costume endpoint numbering from PyDevices/usbif#25. It moves the video endpoint (to 0x83 incdc+uvc), but the VideoStreaming input header still named the build-time address, 0x87, and the host opens the pipe the header names. The one costume that worked was all six functions, because that one puts the endpoint back on 0x87.After PyDevices/usbif#89 the header moves with the endpoint.
costume_selftest.pynow catches a mismatch: with the fix taken out, it fails exactly the 31 camera costumes whose endpoint moves. On the P4 (Waveshare ESP32-P4-WIFI6-Touch-LCD-4B) with the fix,usbif_webcam.pyincdc+uvcstreams to Windows over a high-speed link. ffmpeg'sdshowinput captured 323 frames in 6 s, every one new, with the colour bars 2 px further along each time.PyDevices/usbif#90 also gives camera costumes their own product id. Before it,
cdc+uvcenumerated as 0x4001, the same as bare CDC.The S3 host's stalled probe: still open. I haven't tested this half on an S3 yet. The stall you saw fits TinyUSB's video driver: it doesn't know the streaming interface's current alternate setting until the host sends SET_INTERFACE to it. Until then, its probe handler has no streaming descriptor to check the request against, so it stalls every VS_PROBE, whatever the format and frame. That's why format 0 / frame 0 stalled too. Windows and Linux send SET_INTERFACE(VS, 0) before they probe (Linux's uvcvideo does it on purpose, for cameras that misbehave without it).
host_uvc_negotiate()probes first, and only selects an alternate setting later, inhost_uvc_open(). The next step is to send SET_INTERFACE(itf, 0) at the start ofhost_uvc_negotiate()and tryuvc_display.pyon the S3 against a P4 with PyDevices/usbif#89 in it.bdbarnett commentedon Oct 20, 2026
This no longer happens, and the code it happened in is gone.
I ran the same pairing on 2026-10-20: an ESP32-S3-Touch-LCD-4.3 as the host (MicroPython 1.30 preview,
usb.host.videofrom pydevices main) and the Waveshare P4 panel as the webcam (usb-device-video'sCameraInterface, colour bars, 160x120 YUY2 at 10 fps, so the same VC on interface 2 and VS on interface 3 with the isochronous IN at 0x83). The P4 was reached through the S3's powered hub rather than straight into its port, which I didn't change. The device enumerated at full speed and negotiated on the first try:Camerathen picked the 160x120 mode and streamed 28 frames in 3 s (9.3 fps against the 10 asked for). No SET_INTERFACE is sent before the probe;Camera.start()selects the alternate setting only after negotiating, ashost_uvc_negotiate()did.The cause was on the device side, as the earlier comment guessed. The stalls came from TinyUSB's own video class driver, which answers VS_PROBE only once the streaming interface has been opened with SET_INTERFACE.
usb-device-videoreplaced that driver on 2026-10-16 (the UVC controls are answered in Python onmachine.USBDevice, and the video class driver is no longer part of the firmware), so a probe at alternate setting 0 is accepted whatever the format and frame. Windows and Linux were never affected, because both send SET_INTERFACE(VS, 0) before they probe.I've left the host as it is. Sending SET_INTERFACE(itf, 0) first would only help a camera that needs it, and none of the ones tried does. Reopen this if a camera stalls the probe, and the fix goes at the top of
usb.host.video._negotiate.bdbarnett commentedon Oct 20, 2026
Closing as not reproducible on current firmware. The stall came from TinyUSB's own video class driver, which only answered a probe after SET_INTERFACE. Since
usb-device-videoreplaced it, that driver is no longer in the path: an S3 host's PROBE, GET_CUR and COMMIT at alternate setting 0 complete, and the stream runs (28 frames in 3 s). Reopen with a log if a probe stalls again.