Skip to content

UVC device: an S3 host's VS_PROBE requests are stalled by a P4 webcam at full speed #116

Closed as not planned

Description

@bdbarnett

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".

Activity

  1. bdbarnett commented on Oct 13, 2026

    @bdbarnett
    ContributorAuthor

    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.

  2. transferred this issue fromPyDevices/usbifon Oct 19, 2026
  3. bdbarnett commented on Oct 20, 2026

    @bdbarnett
    ContributorAuthor

    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.

  4. bdbarnett commented on Oct 20, 2026

    @bdbarnett
    ContributorAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions