Building a device for WebUSB

Build a device to take full advantage of the WebUSB API.

Reilly Grant
Reilly Grant

This article explains how to build a device to take full advantage of the WebUSB API. For a brief introduction to the API itself, see Access USB Devices on the Web.

Background

The Universal Serial Bus (USB) has become the most common physical interface for connecting peripherals to desktop and mobile computing devices. In addition to defining the electrical characteristics of the bus and a general model for communicating with a device, USB specifications include a set of device class specifications. These are general models for particular classes of devices such as storage, audio, video, networking, etc. that device manufacturers can implement. The advantage of these device class specifications is that an operating system vendor can implement a single driver based on the class specification (a "class driver") and any device implementing that class will be supported. This was a great improvement over every manufacturer needing to write their own device drivers.

Some devices however don't fit into one of these standardized device classes. A manufacturer may instead choose to label their device as implementing the vendor-specific class. In this case the operating system chooses which device driver to load based on information provided in the vendor's driver package, typically a set of vendor and product IDs which are known to implement a particular vendor-specific protocol.

Another feature of the USB is that devices may provide multiple interfaces to the host they are connected to. Each interface can implement either a standardized class or be vendor-specific. When an operating system chooses the right drivers to handle the device each interface can be claimed by a different driver. For example, a USB webcam typically provides two interfaces, one implementing the USB video class (for the camera) and one implementing the USB audio class (for the microphone). The operating system does not load a single "webcam driver" but instead loads independent video and audio class drivers which take responsibility for the separate functions of the device. This composition of interface classes provides for greater flexibility.

API basics

Many of the standard USB classes have corresponding web APIs. For example, a page can capture video from a video class device using getUserMedia() or receive input events from a human interface (HID) class device by listening for KeyboardEvents or PointerEvents, or by using the Gamepad or the WebHID API. Just as not all devices implement a standardized class definition, not all devices implement features that correspond to existing web platform APIs. When this is the case the WebUSB API can fill that gap by providing a way for sites to claim a vendor-specific interface and implement support for it from directly within their page.

The specific requirements for a device to be accessible via WebUSB vary slightly from platform to platform due to differences in how operating systems manage USB devices but the basic requirement is that a device should not already have a driver claiming the interface the page wants to control. This could be either a generic class driver provided by the OS vendor or a device driver provided by the vendor. As USB devices can provide multiple interfaces, each of which may have its own driver, it is possible to build a device for which some interfaces are claimed by a driver and others are left accessible to the browser.

For example, a high-end USB keyboard may provide an HID class interface that will be claimed by the operating system's input subsystem and a vendor-specific interface that remains available to WebUSB for use by a configuration tool. This tool can be served on the manufacturer's website allowing the user to change aspects of the device's behavior such as macro keys and lighting effects without installing any platform-specific software. Such a device's configuration descriptor would look something like this:

Value Field Description
Configuration descriptor
0x09 bLength Size of this descriptor
0x02 bDescriptorType Configuration descriptor
0x0039 wTotalLength Total length of this series of descriptors
0x02 bNumInterfaces Number of interfaces
0x01 bConfigurationValue Configuration 1
0x00 iConfiguration Configuration name (none)
0b1010000 bmAttributes Self-powered device with remote wakeup
0x32 bMaxPower Max Power is expressed in 2 mA increments
Interface descriptor
0x09 bLength Size of this descriptor
0x04 bDescriptorType Interface descriptor
0x00 bInterfaceNumber Interface 0
0x00 bAlternateSetting Alternate setting 0 (default)
0x01 bNumEndpoints 1 endpoint
0x03 bInterfaceClass HID interface class
0x01 bInterfaceSubClass Boot interface subclass
0x01 bInterfaceProtocol Keyboard
0x00 iInterface Interface name (none)
HID descriptor
0x09 bLength Size of this descriptor
0x21 bDescriptorType HID descriptor
0x0101 bcdHID HID version 1.1
0x00 bCountryCode Hardware target country
0x01 bNumDescriptors Number of HID class descriptors to follow
0x22 bDescriptorType Report descriptor type
0x003F wDescriptorLength Total length of the Report descriptor
Endpoint descriptor
0x07 bLength Size of this descriptor
0x05 bDescriptorType Endpoint descriptor
0b10000001 bEndpointAddress Endpoint 1 (IN)
0b00000011 bmAttributes Interrupt
0x0008 wMaxPacketSize 8 byte packets
0x0A bInterval 10ms interval
Interface descriptor
0x09 bLength Size of this descriptor
0x04 bDescriptorType Interface descriptor
0x01 bInterfaceNumber Interface 1
0x00 bAlternateSetting Alternate setting 0 (default)
0x02 bNumEndpoints 2 endpoints
0xFF bInterfaceClass Vendor-specific interface class
0x00 bInterfaceSubClass
0x00 bInterfaceProtocol
0x00 iInterface Interface name (none)
Endpoint descriptor
0x07 bLength Size of this descriptor
0x05 bDescriptorType Endpoint descriptor
0b10000010 bEndpointAddress Endpoint 1 (IN)
0b00000010 bmAttributes Bulk
0x0040 wMaxPacketSize 64 byte packets
0x00 bInterval N/A for bulk endpoints
Endpoint descriptor
0x07 bLength Size of this descriptor
0x05 bDescriptorType Endpoint descriptor
0b00000011 bEndpointAddress Endpoint 3 (OUT)
0b00000010 bmAttributes Bulk
0x0040 wMaxPacketSize 64 byte packets
0x00 bInterval N/A for bulk endpoints

The configuration descriptor consists of multiple descriptors concatenated together. Each begins with bLength and bDescriptorType fields so that they can be identified. The first interface is an HID interface with an associated HID descriptor and a single endpoint used to deliver input events to the operating system. The second interface is a vendor-specific interface with two endpoints that can be used to send commands to the device and receive responses in return.

WebUSB descriptors

While WebUSB can work with many devices without firmware modifications, additional functionality is enabled by marking the device with specific descriptors indicating support for WebUSB. For example, you can specify a landing page URL that the browser can direct the user to when your device is plugged in.

Screenshot of the WebUSB notification in Chrome
WebUSB notification.

The Binary device Object Store (BOS) is a concept introduced in USB 3.0 but has also been backported to USB 2.0 devices as part of version 2.1. Declaring support for WebUSB starts with including the following Platform Capability Descriptor in the BOS descriptor:

Value Field Description
Binary device Object Store descriptor
0x05 bLength Size of this descriptor
0x0F bDescriptorType Binary device Object Store descriptor
0x001D wTotalLength Total length of this series of descriptors
0x01 bNumDeviceCaps Number of device capability descriptors in the BOS
WebUSB platform capability descriptor
0x18 bLength Size of this descriptor
0x10 bDescriptorType Device capability descriptor
0x05 bDevCapabilityType Platform capability descriptor
0x00 bReserved
{0x38, 0xB6, 0x08, 0x34, 0xA9, 0x09, 0xA0, 0x47, 0x8B, 0xFD, 0xA0, 0x76, 0x88, 0x15, 0xB6, 0x65} PlatformCapablityUUID WebUSB platform capability descriptor GUID in little-endian format
0x0100 bcdVersion WebUSB descriptor version 1.0
0x01 bVendorCode bRequest value for WebUSB
0x01 iLandingPage URL for landing page

The platform capability UUID identifies this as a WebUSB Platform Capability descriptor, which provides basic information about the device. For the browser to fetch more information about the device it uses the bVendorCode value to issue additional requests to the device. The only request currently specified is GET_URL which returns a URL descriptor. These are similar to string descriptors but are designed to encode URLs in the fewest bytes. A URL descriptor for "https://google.com" would look like this:

Value Field Description
URL descriptor
0x0D bLength Size of this descriptor
0x03 bDescriptorType URL descriptor
0x01 bScheme https://
"google.com" URL UTF-8 encoded URL content

When your device is first plugged in the browser reads the BOS descriptor by issuing this standard GET_DESCRIPTOR control transfer:

bmRequestType bRequest wValue wIndex wLength Data (response)
0b10000000 0x06 0x0F00 0x0000 * The BOS descriptor

This request is usually made twice, the first time with a large enough wLength so that the host finds out the value of the wTotalLength field without committing to a large transfer and then again when the full descriptor length is known.

If the WebUSB Platform Capability descriptor has the iLandingPage field set to a non-zero value the browser then performs a WebUSB-specific GET_URL request by issuing a control transfer with the bRequest set to the bVendorCode value from the platform capability descriptor and wValue set to the iLandingPage value. The request code for GET_URL (0x02) goes in wIndex:

bmRequestType bRequest wValue wIndex wLength Data (response)
0b11000000 0x01 0x0001 0x0002 * The URL descriptor

Again, this request may be issued twice in order to first probe for the length of the descriptor being read.

Platform-specific considerations

While the WebUSB API attempts to provide a consistent interface for accessing USB devices developers should still be aware of requirements imposed on applications such as a web browsers requirements in order to access devices.

macOS

Nothing special is necessary for macOS. A website using WebUSB can connect to the device and claim any interfaces that aren't claimed by a kernel driver or another application.

Linux

Linux is like macOS but by default most distributions do not set up user accounts with permission to open USB devices. A system daemon called udev is responsible for assigning the user and group allowed to access a device. A rule such as this will assign ownership of a device matching the given vendor and product IDs to the plugdev group which is a common group for users with access to peripherals:

SUBSYSTEM=="usb", ATTR{idVendor}=="XXXX", ATTR{idProduct}=="XXXX", GROUP="plugdev"

Replace XXXX with the hexadecimal vendor and product IDs for your device, e.g. ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e11" would match a Nexus One phone. These must be written without the usual "0x" prefix and all lowercase to be recognized correctly. To find the IDs for your device run the command line tool lsusb.

This rule should be placed in a file in the /etc/udev/rules.d directory and takes effect as soon as the device is plugged in. There is no need to restart udev.

Android

The Android platform is based on Linux but does not require any modification to system configuration. By default any device that does not have a driver built into the operating system is available to the browser. Developers should be aware however that users will encounter an additional step when connecting to the device. Once a user selects a device in response to a call to requestDevice(), Android will display a prompt asking whether to allow Chrome to access it. This prompt also reappears if a user returns to a website that already has permission to connect to a device and the website calls open().

In addition more devices will be accessible on Android than on desktop Linux because fewer drivers are included by default. A notable omission, for example, is the USB CDC-ACM class commonly implemented by USB-to-serial adapters as there is no API in the Android SDK for communicating with a serial device.

ChromeOS

ChromeOS is based on Linux as well and also does not require any modification to system configuration. The permission_broker service controls access to USB devices and will allow the browser to access them as long as there is at least one unclaimed interface.

Windows

The Windows driver model introduces an additional requirement. Unlike the platforms above the ability to open a USB device from a user application is not the default, even if there is no driver loaded. Instead there is a special driver, WinUSB, that needs to be loaded in order to provide the interface applications use to access the device. This can be done with either a custom driver information file (INF) installed on the system or by modifying the device firmware to provide the Microsoft OS Compatibility Descriptors during enumeration.

Driver Information File (INF)

A driver information file tells Windows what to do when encountering a device for the first time. Since the user's system already includes the WinUSB driver all that's necessary is for the INF file to associate your vendor and product ID with this new installation rule. The file below is a basic example. Save it to a file with the .inf extension, change the sections marked with "X", then right click on it and choose "Install" from the context menu.

[Version]
Signature   = "$Windows NT$"
Class       = USBDevice
ClassGUID   = {88BAE032-5A81-49f0-BC3D-A4FF138216D6}
Provider    = %ManufacturerName%
CatalogFile = WinUSBInstallation.cat
DriverVer   = 09/04/2012,13.54.20.543

; ========== Manufacturer/Models sections ===========

[Manufacturer]
%ManufacturerName% = Standard,NTx86,NTia64,NTamd64

[Standard.NTx86]
%USB\MyCustomDevice.DeviceDesc% = USB_Install,USB\VID_XXXX&PID_XXXX

[Standard.NTia64]
%USB\MyCustomDevice.DeviceDesc% = USB_Install,USB\VID_XXXX&PID_XXXX

[Standard.NTamd64]
%USB\MyCustomDevice.DeviceDesc% = USB_Install,USB\VID_XXXX&PID_XXXX

; ========== Class definition ===========

[ClassInstall32]
AddReg = ClassInstall_AddReg

[ClassInstall_AddReg]
HKR,,,,%ClassName%
HKR,,NoInstallClass,,1
HKR,,IconPath,%REG_MULTI_SZ%,"%systemroot%\system32\setupapi.dll,-20"
HKR,,LowerLogoVersion,,5.2

; =================== Installation ===================

[USB_Install]
Include = winusb.inf
Needs   = WINUSB.NT

[USB_Install.Services]
Include = winusb.inf
Needs   = WINUSB.NT.Services

[USB_Install.HW]
AddReg = Dev_AddReg

[Dev_AddReg]
HKR,,DeviceInterfaceGUIDs,0x10000,"{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}"

; =================== Strings ===================

[Strings]
ManufacturerName              = "Your Company Name Here"
ClassName                     = "Your Company Devices"
USB\MyCustomDevice.DeviceDesc = "Your Device Name Here"

The [Dev_AddReg] section configures the set of DeviceInterfaceGUIDs for the device. Every device interface must have a GUID in order for an application to find and connect to it through the Windows API. Use the New-Guid PowerShell cmdlet or an online tool to generate a random GUID.

For development purposes the Zadig tool provides an easy interface for replacing the driver loaded for a USB interface with the WinUSB driver.

Microsoft OS compatibility descriptors

The INF file approach above is cumbersome because it requires configuring every user's machine ahead of time. Windows 8.1 and higher offers an alternative through the use of custom USB descriptors. These descriptors provide information to the Windows operating system when the device is first plugged in that would normally be included in the INF file.

Once you have WebUSB descriptors set up it is easy to add Microsoft's OS compatibility descriptors as well. First extend the BOS descriptor with this additional platform capability descriptor. Make sure to update wTotalLength and bNumDeviceCaps to account for it.

Value Field Description
Microsoft OS 2.0 platform capability descriptor
0x1C bLength Size of this descriptor
0x10 bDescriptorType Device capability descriptor
0x05 bDevCapabilityType Platform capability descriptor
0x00 bReserved
{0xDF, 0x60, 0xDD, 0xD8, 0x89, 0x45, 0xC7, 0x4C, 0x9C, 0xD2, 0x65, 0x9D, 0x9E, 0x64, 0x8A, 0x9F} PlatformCapablityUUID Microsoft OS 2.0 platform compatibility descriptor GUID in little-endian format
0x06030000 dwWindowsVersion Minimum compatible Windows version (Windows 8.1)
0x00B2 wMSOSDescriptorSetTotalLength Total length of the descriptor set
0x02 bMS_VendorCode bRequest value for retrieving further Microsoft descriptors
0x00 bAltEnumCode Device does not support alternate enumeration

As with the WebUSB descriptors you have to pick a bRequest value to be used by control transfers related to these descriptors. In this example I've picked 0x02. 0x07, placed in wIndex, is the command to retrieve the Microsoft OS 2.0 Descriptor Set from the device.

bmRequestType bRequest wValue wIndex wLength Data (response)
0b11000000 0x02 0x0000 0x0007 * MS OS 2.0 Descriptor Set

A USB device can have multiple functions and so the first part of the descriptor set describes which function the properties that follow are associated with. The example below configures interface 1 of a composite device. The descriptor gives the OS two important pieces of information about this interface. The compatible ID descriptor tells Windows that this device is compatible with the WinUSB driver. The registry property descriptor functions similarly to the [Dev_AddReg] section of the INF example above, setting a registry property to assign this function a device interface GUID.

Value Field Description
Microsoft OS 2.0 descriptor set header
0x000A wLength Size of this descriptor
0x0000 wDescriptorType Descriptor set header descriptor
0x06030000 dwWindowsVersion Minimum compatible Windows version (Windows 8.1)
0x00B2 wTotalLength Total length of the descriptor set
Microsoft OS 2.0 configuration subset header
0x0008 wLength Size of this descriptor
0x0001 wDescriptorType Configuration subset header desc.
0x00 bConfigurationValue Applies to configuration 1 (indexed from 0 despite configurations normally indexed from 1)
0x00 bReserved Must be set to 0
0x00A8 wTotalLength Total length of the subset including this header
Microsoft OS 2.0 function subset header
0x0008 wLength Size of this descriptor
0x0002 wDescriptorType Function subset header descriptor
0x01 bFirstInterface First interface of the function
0x00 bReserved Must be set to 0
0x00A0 wSubsetLength Total length of the subset including this header
Microsoft OS 2.0 compatible ID descriptor
0x0014 wLength Size of this descriptor
0x0003 wDescriptorType Compatible ID descriptor
"WINUSB\0\0" CompatibileID ASCII string padded to 8 bytes
"\0\0\0\0\0\0\0\0" SubCompatibleID ASCII string padded to 8 bytes
Microsoft OS 2.0 registry property descriptor
0x0084 wLength Size of this descriptor
0x0004 wDescriptorType Registry property descriptor
0x0007 wPropertyDataType REG_MULTI_SZ
0x002A wPropertyNameLength Length of the property name
"DeviceInterfaceGUIDs\0" PropertyName Property name with null terminator encoded in UTF-16LE
0x0050 wPropertyDataLength Length of the property value
"{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}\0\0" PropertyData GUID plus two null terminators encoded in UTF-16LE

Windows will only query the device for this information once. If the device does not respond with valid descriptors it will not ask again the next time the device is connected. Microsoft has provided a list of USB Device Registry Entries describing the registry entries created when enumerating a device. When testing delete the entries created for a device for force Windows to try to read the descriptors again.

For more information check out Microsoft's blog post on how to use these descriptors.

Examples

Example code implementing WebUSB-aware devices that include both WebUSB descriptors and Microsoft OS descriptors can be found in these projects: