USBIP C library 0.7.0
Virtual USB devices & host drivers over USB/IP
Loading...
Searching...
No Matches
Vendor Devices & WebUSB

Not everything is a standard class.

A vendor-specific device speaks a protocol you define: interface class 0xFF, which no operating system has a driver for. That is the point - nothing binds it, so your own program owns both ends. Most lab tools, programmers, dongles and one-off gadgets are shaped this way, and so is anything a web page drives over WebUSB.

It is also the clearest demonstration that the class layer is optional. Everything on this page is built on the device core alone - usbip-device.h, four calls, hand-written descriptors - with no class code behind it. The device classes are a convenience layer over exactly these calls, not a layer you have to go through.

A vendor bulk device

One vendor interface, one Bulk OUT, one Bulk IN, and a loopback for behaviour. This is examples/device/vendor_device.c whole - the runnable program, and the device the host walkthrough drives:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "usbip-device.h"
/* identity + wiring (a vendor device defines its own class 0xFF) */
#define VENDOR_ID 0x1209
#define PRODUCT_ID 0x0004
#define EP_BULK_OUT 0x01 /* host -> device */
#define EP_BULK_IN 0x81 /* device -> host */
#define MAX_PACKET 64
int main(int argc, char **argv)
{
int port = (argc > 1) ? atoi(argv[1]) : 3240; /* default USB/IP port */
usbip_device *dev = usbip_device_create(VENDOR_ID, PRODUCT_ID);
usbip_device_set_strings(dev, "USB over IP", "USBIP Vendor Bulk", "0004");
/* One vendor interface (class 0xFF) with a bulk OUT and a bulk IN endpoint,
* declared straight on the device core - no class layer. */
.bDescriptorType = USB_DT_INTERFACE,
.bInterfaceNumber = 0,
.bNumEndpoints = 0, /* auto-counted as endpoints are added */
.bInterfaceClass = 0xFF,
.bInterfaceSubClass = 0x00,
.bInterfaceProtocol = 0x00
});
.bDescriptorType = USB_DT_ENDPOINT,
.bEndpointAddress = EP_BULK_OUT,
.bmAttributes = USB_BULK,
.wMaxPacketSize = MAX_PACKET
});
.bDescriptorType = USB_DT_ENDPOINT,
.bEndpointAddress = EP_BULK_IN,
.bmAttributes = USB_BULK,
.wMaxPacketSize = MAX_PACKET
});
usb_transport *transport = usbip_transport(NULL, port);
int rc = usbip_device_plug(dev, transport);
if (rc != USB_SUCCESS) {
fprintf(stderr, "usbip_device_plug failed (port %d in use?)\n", port);
return 1;
}
fprintf(stderr, "[vendor] serving %04x:%04x on :%d (bulk OUT 0x%02x, bulk IN 0x%02x) - "
"echoing OUT back on IN\n", VENDOR_ID, PRODUCT_ID, port, EP_BULK_OUT, EP_BULK_IN);
/* Loopback loop: each usbip_device_read() returns one host OUT transfer; echo it back. */
uint8_t buffer[8192];
for (;;) {
int n_received = usbip_device_read(bulk_out, buffer, sizeof(buffer), 0); /* blocks until the host sends */
if (n_received <= 0)
continue;
fprintf(stderr, "[vendor] RX %d bytes -> echo\n", n_received);
usbip_device_write(bulk_in, buffer, n_received, 0); /* device -> host */
}
return 0;
}
#define USB_DT_ENDPOINT
Endpoint descriptor.
Definition usbip.h:356
#define USB_DT_INTERFACE
Interface descriptor.
Definition usbip.h:355
@ USB_BULK
Bulk - large data, reliable, no timing guarantee.
Definition usbip.h:270
void usbip_device_set_strings(usbip_device *dev, const char *mfr, const char *product, const char *serial)
Set the manufacturer / product / serial string descriptors (indices 1-3).
usbip_ep * usbip_device_add_endpoint(usbip_device *dev, const void *ep_descriptor)
Append an endpoint descriptor and return the pipe it created.
int usbip_device_write(usbip_ep *ep, const void *buf, int len, unsigned timeout_ms)
Write device-to-host (IN) data to an endpoint, blocking up to timeout_ms.
int usbip_device_plug(usbip_device *dev, usb_transport *transport)
Plug the device onto a transport and start serving it.
usbip_device * usbip_device_create(uint16_t vid, uint16_t pid)
Create a virtual device with the given vendor/product IDs.
int usbip_device_add_descriptor(usbip_device *dev, const void *descriptor)
Append a typed descriptor (interface / endpoint / HID / class-specific) to the device's configuration...
int usbip_device_read(usbip_ep *ep, void *buf, int len, unsigned timeout_ms)
Read host-to-device (OUT) data from an endpoint, blocking up to timeout_ms.
struct usbip_device usbip_device
The virtual device
Definition usbip-device.h:36
struct usbip_ep usbip_ep
An endpoint (byte pipe)
Definition usbip-device.h:37
#define USB_SUCCESS
Error/status codes - same values as libusb, so ported constants keep working.
Definition usbip.h:391
struct usb_transport usb_transport
Opaque handle to a USB/IP transport (the wire under host/device calls).
Definition usbip.h:494
usb_transport * usbip_transport(const char *host, int port)
Create a real USB/IP transport over TCP.
Standard endpoint descriptor (Sec.9.6.6).
Definition usbip.h:471
Standard interface descriptor (Sec.9.6.5).
Definition usbip.h:465
Device core API - descriptors, standard requests, endpoints, dispatch.

Descriptors are appended in wire order, exactly as the configuration blob will be read back: an interface descriptor opens a section and everything after it belongs to that interface until the next one, so a class-specific descriptor of your own is just another usbip_device_add_descriptor() in the position it should occupy. bNumEndpoints is left 0 on purpose - the core counts endpoints as they are added.

Note
bEndpointAddress is a preference, not a demand. If another interface already holds 0x81 the endpoint is relocated to the lowest free number in the same direction, so keep the usbip_ep the call returns rather than looking the endpoint up later by the address you asked for (0x00 / 0x80 mean "any"). That is what lets two independent functions land on one device without either knowing about the other.

Put the 0xFF on the interface and leave bDeviceClass at 0, as above: the per-interface form still composes with a standard-class function later (Composite devices), a device-level one does not.

usbip_device_read() returns one host OUT transfer per call and usbip_device_write() queues one IN transfer; both take a timeout in milliseconds, 0 meaning "block indefinitely". Data written before a host attaches stays queued and is delivered on attach.

Vendor control requests

Endpoints carry the data plane; a vendor protocol usually puts its commands on the control plane, which needs no endpoint and carries its arguments in the SETUP packet. The core answers every standard request itself and hands the rest to the handler registered with usbip_device_on_control():

#define VREQ_SET_LED 0x01 // OUT: wValue = on/off
#define VREQ_GET_COUNT 0x02 // IN: 4-byte counter
static int vendor_ctrl(void *ctx, const usb_setup *setup, uint8_t *buf, uint16_t len)
{
app_state *app = ctx;
return -1; // not ours -> STALL
switch (setup->bRequest) {
case VREQ_SET_LED: // no data stage
app->led = (setup->wValue != 0);
return 0;
case VREQ_GET_COUNT: // device -> host
if (len < 4)
return -1;
buf[0] = (uint8_t)(app->count );
buf[1] = (uint8_t)(app->count >> 8);
buf[2] = (uint8_t)(app->count >> 16);
buf[3] = (uint8_t)(app->count >> 24);
return 4; // bytes produced
}
return -1; // unknown request -> STALL
}
usbip_device_on_control(dev, 0, vendor_ctrl, &app); // interface 0
#define USB_REQ_TYPE(bmRequestType)
Extract the usb_req_type from a bmRequestType (bits 6:5).
Definition usbip.h:313
@ USB_VENDOR
A vendor-specific request.
Definition usbip.h:288
void usbip_device_on_control(usbip_device *dev, int ifnum, usbip_device_control_fn cb, void *ctx)
Register the class/vendor control handler for one interface.
The 8-byte SETUP packet that begins every control transfer (host byte order in the API).
Definition usbip.h:423
uint16_t wValue
Request-specific parameter (often a type/index)
Definition usbip.h:426
uint8_t bRequest
Request code (e.g.
Definition usbip.h:425
uint8_t bmRequestType
Direction | type | recipient bitmask (see above)
Definition usbip.h:424

The contract is short: on an IN request fill buf and return how many bytes you produced; on an OUT request read the len bytes in buf and return anything >= 0; return a negative value to STALL. Stalling an unrecognised request is not a failure path to avoid - it is how a control endpoint says "no such request", and the host recovers by itself. Requests reach the handler registered for their bInterfaceNumber; ifnum -1 installs the device-level fallback, which also sees device- and endpoint-recipient requests. USB_REQ_TYPE and USB_REQ_RECIP decode bmRequestType, and USB concepts explains SETUP and STALL. On a non-control endpoint, report a protocol failure by halting the pipe with usbip_ep_stall() instead.

Driving it

No driver is waiting for a vendor device, so something has to claim it. Your own host driver is the direct route - no kernel, no root, same code on all three platforms, and what examples/host/host_drive.c does to vendor_device:

usbip_host_handle *h = usbip_host_open_vid_pid(ctx, 0x1209, 0x0004);
usbip_host_control_transfer(h, 0x40, VREQ_SET_LED, 1, 0, NULL, 0, 1000); // OUT|vendor|device
int n;
usbip_host_bulk_transfer(h, 0x01, (uint8_t *)"hello", 5, &n, 1000); // OUT
uint8_t rx[64];
usbip_host_bulk_transfer(h, 0x81, rx, sizeof(rx), &n, 1000); // IN -> "hello"
int usbip_host_control_transfer(usbip_host_handle *handle, uint8_t bmRequestType, uint8_t bRequest, uint16_t wValue, uint16_t wIndex, uint8_t *data, uint16_t wLength, unsigned timeout_ms)
Perform a synchronous control transfer on endpoint 0 (libusb-shaped).
int usbip_host_bulk_transfer(usbip_host_handle *handle, uint8_t endpoint, uint8_t *data, int length, int *transferred, unsigned timeout_ms)
Perform a synchronous bulk transfer (libusb-shaped).
struct usbip_host_handle usbip_host_handle
An opened device, usable for I/O
Definition usbip-host.h:32
usbip_host_handle * usbip_host_open_vid_pid(usbip_host_context *ctx, uint16_t vid, uint16_t pid)
Convenience: enumerate, match the first device by VID:PID, and open it.

WebUSB - open it from a browser

WebUSB lets a page served over https (or http://localhost) open a vendor device directly, once the user has picked it from a chooser. Per the spec the device has to advertise a BOS platform-capability descriptor identifying itself as WebUSB and answer a vendor GET_URL request with its landing page. The BOS belongs to the device rather than to any interface, so both are core calls:

usbip_device_enable_webusb(dev, 0x22, "https://example.com/app");
usbip_device_enable_winusb(dev, NULL); // Chrome on Windows needs WinUSB bound
void usbip_device_enable_webusb(usbip_device *dev, uint8_t vendor_code, const char *url)
Advertise WebUSB so a capable browser can surface and open the device.
int usbip_device_enable_winusb(usbip_device *dev, const char *guid)
Advertise WinUSB via BOTH Microsoft OS 1.0 and Microsoft OS 2.0 descriptors.

Everything else is the vendor device from the top of this page, unchanged - examples/device/webusb_device.c is that bulk loopback plus these two lines. Three things to get right:

  • Keep the vendor code clear of 0x20/0x21, the bRequest values the Microsoft OS descriptors use, or a device advertising both answers one mechanism's request from the other's handler. The example uses 0x22.
  • A bcdUSB of 0x0210 or higher is what makes a host fetch the BOS at all; both enable_ calls bump it for you.
  • Chrome on Windows only opens a device whose interface has a driver bound, hence the WinUSB line. On Linux nothing needs to bind, but the browser still needs permission to open the device (a udev rule, or a user with access).

The page side is then plain WebUSB, with no idea the device is virtual:

const dev = await navigator.usb.requestDevice({filters: [{vendorId: 0x1209}]});
await dev.open();
await dev.selectConfiguration(1);
await dev.claimInterface(0);
await dev.transferOut(1, new TextEncoder().encode("hi"));
const r = await dev.transferIn(1, 64); // -> "HI" (echoed, case swapped)

requestDevice() has to come from a real user gesture, which is the one step that cannot be scripted in a test.