Cómo compilar un dispositivo para WebUSB

Compila un dispositivo para aprovechar al máximo la API de WebUSB.

En este artículo, se explica cómo compilar un dispositivo para aprovechar al máximo la API de WebUSB. Para obtener una breve introducción a la API, consulta Cómo acceder a dispositivos USB en la Web.

Segundo plano

El bus universal en serie (USB) se convirtió en la interfaz física más común para conectar periféricos a dispositivos de procesamiento de computadoras de escritorio y móviles. Además de definir las características eléctricas del bus y un modelo general para comunicarse con un dispositivo, las especificaciones de USB incluyen un conjunto de especificaciones de clase de dispositivo. Estos son modelos generales para clases particulares de dispositivos, como almacenamiento, audio, video, redes, etc., que los fabricantes de dispositivos pueden implementar. La ventaja de estas especificaciones de clase de dispositivo es que un proveedor de sistemas operativos puede implementar un solo controlador basado en la especificación de clase (un "controlador de clase") y se admitirá cualquier dispositivo que implemente esa clase. Esta fue una gran mejora en comparación con cada fabricante que necesitaba escribir sus propios controladores de dispositivos.

Sin embargo, algunos dispositivos no se ajustan a una de estas clases de dispositivos estandarizados. En su lugar, un fabricante puede optar por etiquetar su dispositivo como implementador de la clase específica del proveedor. En este caso, el sistema operativo elige qué controlador de dispositivo cargar en función de la información proporcionada en el paquete de controladores del proveedor, por lo general, un conjunto de IDs de proveedor y producto que se sabe que implementan un protocolo específico del proveedor.

Otra característica del USB es que los dispositivos pueden proporcionar varias interfaces al host al que están conectados. Cada interfaz puede implementar una clase estandarizada o ser específica del proveedor. Cuando un sistema operativo elige los controladores correctos para controlar el dispositivo, cada interfaz puede ser reclamada por un controlador diferente. Por ejemplo, una cámara web USB suele proporcionar dos interfaces, una que implementa la clase de video USB (para la cámara) y otra que implementa la clase de audio USB (para el micrófono). El sistema operativo no carga un solo “controlador de cámara web”, sino que carga controladores independientes de clase de audio y video que se responsabilizan de las funciones independientes del dispositivo. Esta composición de clases de interfaz proporciona una mayor flexibilidad.

Conceptos básicos de API

Muchas de las clases USB estándar tienen las APIs web correspondientes. Por ejemplo, una página puede capturar video de un dispositivo de clase de video con getUserMedia() o recibir eventos de entrada de un dispositivo de clase de interfaz humana (HID) escuchando KeyboardEvents o PointerEvents, o con el Gamepad o la API de WebHID. Así como no todos los dispositivos implementan una definición de clase estandarizada, tampoco todos implementan funciones que correspondan a las APIs de plataformas web existentes. Cuando este es el caso, la API de WebUSB puede llenar esa brecha, ya que proporciona una forma para que los sitios reclamen una interfaz específica del proveedor y la implementen directamente desde su página.

Los requisitos específicos para que se pueda acceder a un dispositivo a través de WebUSB varían ligeramente de una plataforma a otra debido a las diferencias en la forma en que los sistemas operativos administran los dispositivos USB, pero el requisito básico es que un dispositivo no debería tener un controlador que reclame la interfaz que la página desea controlar. Puede ser un controlador de clase genérico que proporciona el proveedor del SO o un controlador de dispositivo que proporciona el proveedor. Como los dispositivos USB pueden proporcionar varias interfaces, cada una de las cuales puede tener su propio controlador, es posible compilar un dispositivo para el que un controlador reclame algunas interfaces y otras queden accesibles para el navegador.

Por ejemplo, un teclado USB de alta gama puede proporcionar una interfaz de clase HID que reclamará el subsistema de entrada del sistema operativo y una interfaz específica del proveedor que permanecerá disponible para WebUSB para que la use una herramienta de configuración. Esta herramienta se puede entregar en el sitio web del fabricante, lo que permite al usuario cambiar aspectos del comportamiento del dispositivo, como las teclas de macro y los efectos de iluminación, sin instalar ningún software específico de la plataforma. El descriptor de configuración de un dispositivo de este tipo se vería de la siguiente manera:

Valor Campo Descripción
Descriptor de configuración
0x09 bLength Tamaño de este descriptor
0x02 bDescriptorType Descriptor de configuración
0x0039 wTotalLength Es la longitud total de esta serie de descriptores.
0x02 bNumInterfaces Cantidad de interfaces
0x01 bConfigurationValue Configuration 1
0x00 iConfiguration Nombre de la configuración (ninguno)
0b1010000 bmAttributes Dispositivo con alimentación propia con activación remota
0x32 bMaxPower La potencia máxima se expresa en incrementos de 2 mA.
Descriptor de interfaz
0x09 bLength Tamaño de este descriptor
0x04 bDescriptorType Descriptor de interfaz
0x00 bInterfaceNumber Interfaz 0
0x00 bAlternateSetting Configuración alternativa 0 (predeterminada)
0x01 bNumEndpoints 1 extremo
0x03 bInterfaceClass Clase de interfaz HID
0x01 bInterfaceSubClass Subclase de la interfaz de inicio
0x01 bInterfaceProtocol Teclado
0x00 iInterface Nombre de la interfaz (ninguno)
Descriptor HID
0x09 bLength Tamaño de este descriptor
0x21 bDescriptorType Descriptor HID
0x0101 bcdHID HID versión 1.1
0x00 bCountryCode País de destino del hardware
0x01 bNumDescriptors Cantidad de descriptores de clase HID que se deben seguir
0x22 bDescriptorType Tipo de descriptor de informe
0x003F wDescriptorLength Es la longitud total del descriptor de informe.
Descriptor de extremo
0x07 bLength Tamaño de este descriptor
0x05 bDescriptorType Descriptor de extremo
0b10000001 bEndpointAddress Extremo 1 (IN)
0b00000011 bmAttributes Interrumpir
0x0008 wMaxPacketSize Paquetes de 8 bytes
0x0A bInterval Intervalo de 10 ms
Descriptor de interfaz
0x09 bLength Tamaño de este descriptor
0x04 bDescriptorType Descriptor de interfaz
0x01 bInterfaceNumber Interfaz 1
0x00 bAlternateSetting Configuración alternativa 0 (predeterminada)
0x02 bNumEndpoints 2 extremos
0xFF bInterfaceClass Clase de interfaz específica del proveedor
0x00 bInterfaceSubClass
0x00 bInterfaceProtocol
0x00 iInterface Nombre de la interfaz (ninguno)
Descriptor de extremo
0x07 bLength Tamaño de este descriptor
0x05 bDescriptorType Descriptor de extremo
0b10000010 bEndpointAddress Extremo 1 (IN)
0b00000010 bmAttributes Masiva
0x0040 wMaxPacketSize Paquetes de 64 bytes
0x00 bInterval N/A para extremos masivos
Descriptor de extremo
0x07 bLength Tamaño de este descriptor
0x05 bDescriptorType Descriptor de extremo
0b00000011 bEndpointAddress Extremo 3 (SALIDA)
0b00000010 bmAttributes Masiva
0x0040 wMaxPacketSize Paquetes de 64 bytes
0x00 bInterval N/A para los extremos masivos

El descriptor de configuración consta de varios descriptores concatenados juntos. Cada uno comienza con los campos bLength y bDescriptorType para que se puedan identificar. La primera interfaz es una interfaz HID con un descriptor HID asociado y un solo extremo que se usa para entregar eventos de entrada al sistema operativo. La segunda interfaz es una interfaz específica del proveedor con dos extremos que se pueden usar para enviar comandos al dispositivo y recibir respuestas a cambio.

Descriptores de WebUSB

Si bien WebUSB puede funcionar con muchos dispositivos sin modificaciones de firmware, se habilita la funcionalidad adicional marcando el dispositivo con descriptores específicos que indican compatibilidad con WebUSB. Por ejemplo, puedes especificar una URL de página de destino a la que el navegador pueda dirigir al usuario cuando el dispositivo esté conectado.

Captura de pantalla de la notificación de WebUSB en Chrome
Notificación de WebUSB.

El almacén de objetos de dispositivos binarios (BOS) es un concepto que se introdujo en USB 3.0, pero también se retroportó a dispositivos USB 2.0 como parte de la versión 2.1. La declaración de compatibilidad con WebUSB comienza con la inclusión del siguiente descriptor de capacidades de la plataforma en el descriptor de BOS:

Valor Campo Descripción
Descriptor de Object Store del dispositivo binario
0x05 bLength Tamaño de este descriptor
0x0F bDescriptorType Descriptor de Object Store del dispositivo binario
0x001D wTotalLength Es la longitud total de esta serie de descriptores.
0x01 bNumDeviceCaps Cantidad de descriptores de capacidades del dispositivo en el BOS
Descriptor de funciones de la plataforma WebUSB
0x18 bLength Tamaño de este descriptor
0x10 bDescriptorType Descriptor de capacidades del dispositivo
0x05 bDevCapabilityType Descriptor de funciones de la plataforma
0x00 bReserved
{0x38, 0xB6, 0x08, 0x34, 0xA9, 0x09, 0xA0, 0x47, 0x8B, 0xFD, 0xA0, 0x76, 0x88, 0x15, 0xB6, 0x65} PlatformCapablityUUID GUID del descriptor de capacidades de la plataforma WebUSB en formato little-endian
0x0100 bcdVersion Descriptor de WebUSB versión 1.0
0x01 bVendorCode Valor de bRequest para WebUSB
0x01 iLandingPage URL de la página de destino

El UUID de la capability de la plataforma lo identifica como un descriptor de capability de la plataforma WebUSB, que proporciona información básica sobre el dispositivo. Para que el navegador recupere más información sobre el dispositivo, usa el valor bVendorCode para enviar solicitudes adicionales al dispositivo. La única solicitud especificada actualmente es GET_URL, que muestra un descriptor de URL. Son similares a los descriptores de cadenas, pero están diseñados para codificar URLs en la menor cantidad de bytes posible. Un descriptor de URL para "https://google.com" se vería de la siguiente manera:

Valor Campo Descripción
Descriptor de URL
0x0D bLength Tamaño de este descriptor
0x03 bDescriptorType Descriptor de URL
0x01 bScheme https://
"google.com" URL Contenido de la URL codificado en UTF-8

Cuando el dispositivo se conecta por primera vez, el navegador lee el descriptor de BOS emitiendo esta transferencia de control GET_DESCRIPTOR estándar:

bmRequestType bRequest wValue wIndex wLength Datos (respuesta)
0b10000000 0x06 0x0F00 0x0000 * El descriptor de BOS

Por lo general, esta solicitud se realiza dos veces: la primera vez con un wLength lo suficientemente grande para que el host descubra el valor del campo wTotalLength sin confirmar una transferencia grande y, luego, otra vez cuando se conozca la longitud completa del descriptor.

Si el descriptor de capacidades de la plataforma WebUSB tiene el campo iLandingPage configurado en un valor distinto de cero, el navegador realiza una solicitud GET_URL específica de WebUSB mediante la emisión de una transferencia de control con bRequest establecido en el valor bVendorCode del descriptor de capacidades de la plataforma y wValue establecido en el valor iLandingPage. El código de solicitud de GET_URL (0x02) se coloca en wIndex:

bmRequestType bRequest wValue wIndex wLength Datos (respuesta)
0b11000000 0x01 0x0001 0x0002 * El descriptor de URL

Una vez más, esta solicitud se puede emitir dos veces para sondear primero la longitud del descriptor que se está leyendo.

Consideraciones específicas de la plataforma

Si bien la API de WebUSB intenta proporcionar una interfaz coherente para acceder a los dispositivos USB, los desarrolladores deben conocer los requisitos que se imponen a las aplicaciones, como los requisitos de los navegadores web para acceder a los dispositivos.

macOS

No es necesario hacer nada especial en macOS. Un sitio web que usa WebUSB puede conectarse al dispositivo y reclamar cualquier interfaz que no reclame un controlador de kernel o otra aplicación.

Linux

Linux es como macOS, pero de forma predeterminada, la mayoría de las distribuciones no configuran cuentas de usuario con permiso para abrir dispositivos USB. Un daemon del sistema llamado udev es responsable de asignar el usuario y el grupo que pueden acceder a un dispositivo. Una regla como esta asignará la propiedad de un dispositivo que coincida con los IDs de proveedor y producto determinados al grupo plugdev, que es un grupo común para los usuarios con acceso a periféricos:

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

Reemplaza XXXX por los IDs de proveedor y producto hexadecimales de tu dispositivo. Por ejemplo, ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e11" coincidiría con un teléfono Nexus One. Para que se reconozcan correctamente, deben escribirse sin el prefijo "0x" habitual y en minúsculas. Para encontrar los IDs de tu dispositivo, ejecuta la herramienta de línea de comandos lsusb.

Esta regla debe colocarse en un archivo del directorio /etc/udev/rules.d y se aplica en cuanto se enchufa el dispositivo. No es necesario reiniciar udev.

Android

La plataforma de Android se basa en Linux, pero no requiere ninguna modificación en la configuración del sistema. De forma predeterminada, cualquier dispositivo que no tenga un controlador integrado en el sistema operativo está disponible para el navegador. Sin embargo, los desarrolladores deben tener en cuenta que los usuarios encontrarán un paso adicional cuando se conecten al dispositivo. Una vez que un usuario selecciona un dispositivo en respuesta a una llamada a requestDevice(), Android mostrará un mensaje en el que se le preguntará si permite que Chrome acceda a él. Este mensaje también vuelve a aparecer si un usuario regresa a un sitio web que ya tiene permiso para conectarse a un dispositivo y el sitio web llama a open().

Además, se podrá acceder a más dispositivos en Android que en Linux para computadoras, ya que se incluyen menos controladores de forma predeterminada. Una omisión notable, por ejemplo, es la clase USB CDC-ACM que suelen implementar los adaptadores USB a serie, ya que no hay una API en el SDK de Android para comunicarse con un dispositivo serie.

ChromeOS

ChromeOS también se basa en Linux y no requiere ninguna modificación en la configuración del sistema. El servicio permission_broker controla el acceso a los dispositivos USB y permitirá que el navegador acceda a ellos, siempre y cuando haya al menos una interfaz sin reclamar.

Windows

El modelo de controlador de Windows introduce un requisito adicional. A diferencia de las plataformas anteriores, la capacidad de abrir un dispositivo USB desde una aplicación de usuario no es la predeterminada, incluso si no se cargó ningún controlador. En su lugar, hay un controlador especial, WinUSB, que se debe cargar para proporcionar la interfaz que usan las aplicaciones para acceder al dispositivo. Esto se puede hacer con un archivo de información del controlador (INF) personalizado instalado en el sistema o modificando el firmware del dispositivo para proporcionar los descriptores de compatibilidad del SO de Microsoft durante la enumeración.

Archivo de información del controlador (INF)

Un archivo de información del controlador le indica a Windows qué hacer cuando detecta un dispositivo por primera vez. Como el sistema del usuario ya incluye el controlador WinUSB, lo único que se necesita es que el archivo INF asocie tu ID de proveedor y producto con esta nueva regla de instalación. El siguiente archivo es un ejemplo básico. Guárdalo en un archivo con la extensión .inf, cambia las secciones marcadas con una “X” y, luego, haz clic con el botón derecho en él y elige “Instalar” en el menú contextual.

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

La sección [Dev_AddReg] configura el conjunto de DeviceInterfaceGUIDs para el dispositivo. Cada interfaz de dispositivo debe tener un GUID para que una aplicación lo encuentre y se conecte a él a través de la API de Windows. Usa el cmdlet New-Guid de PowerShell o una herramienta en línea para generar un GUID aleatorio.

Para fines de desarrollo, la herramienta Zadig proporciona una interfaz sencilla para reemplazar el controlador cargado para una interfaz USB con el controlador WinUSB.

Descriptores de compatibilidad del SO de Microsoft

El enfoque del archivo INF anterior es engorroso porque requiere configurar la máquina de cada usuario con anticipación. Windows 8.1 y versiones posteriores ofrecen una alternativa a través del uso de descriptores USB personalizados. Estos descriptores proporcionan información al sistema operativo Windows cuando se conecta el dispositivo por primera vez, que normalmente se incluiría en el archivo INF.

Una vez que hayas configurado los descriptores de WebUSB, también es fácil agregar los descriptores de compatibilidad del SO de Microsoft. Primero, extiende el descriptor de BOS con este descriptor de capacidades de plataforma adicional. Asegúrate de actualizar wTotalLength y bNumDeviceCaps para tener en cuenta este cambio.

Valor Campo Descripción
Descriptor de capacidades de la plataforma del SO 2.0 de Microsoft
0x1C bLength Tamaño de este descriptor
0x10 bDescriptorType Descriptor de capacidades del dispositivo
0x05 bDevCapabilityType Descriptor de funciones de la plataforma
0x00 bReserved
{0xDF, 0x60, 0xDD, 0xD8, 0x89, 0x45, 0xC7, 0x4C, 0x9C, 0xD2, 0x65, 0x9D, 0x9E, 0x64, 0x8A, 0x9F} PlatformCapablityUUID GUID del descriptor de compatibilidad de la plataforma del SO 2.0 de Microsoft en formato little-endian
0x06030000 dwWindowsVersion Versión mínima de Windows compatible (Windows 8.1)
0x00B2 wMSOSDescriptorSetTotalLength Es la longitud total del conjunto de descriptores.
0x02 bMS_VendorCode Valor de bRequest para recuperar más descriptores de Microsoft
0x00 bAltEnumCode El dispositivo no admite la enumeración alternativa.

Al igual que con los descriptores de WebUSB, debes elegir un valor bRequest que usarán las transferencias de control relacionadas con estos descriptores. En este ejemplo, elegí 0x02. 0x07, que se coloca en wIndex, es el comando para recuperar el conjunto de descriptores del SO 2.0 de Microsoft del dispositivo.

bmRequestType bRequest wValue wIndex wLength Datos (respuesta)
0b11000000 0x02 0x0000 0x0007 * Conjunto de descriptores de SO de Microsoft 2.0

Un dispositivo USB puede tener varias funciones, por lo que la primera parte del conjunto de descriptores describe con qué función están asociadas las propiedades que siguen. En el siguiente ejemplo, se configura la interfaz 1 de un dispositivo compuesto. El descriptor le brinda al SO dos datos importantes sobre esta interfaz. El descriptor de ID compatible le indica a Windows que este dispositivo es compatible con el controlador de WinUSB. El descriptor de propiedades del registro funciona de manera similar a la sección [Dev_AddReg] del ejemplo de INF anterior, ya que establece una propiedad del registro para asignarle a esta función un GUID de interfaz de dispositivo.

Valor Campo Descripción
Encabezado del conjunto de descriptores del SO 2.0 de Microsoft
0x000A wLength Tamaño de este descriptor
0x0000 wDescriptorType Descriptor de encabezado del conjunto de descriptores
0x06030000 dwWindowsVersion Versión mínima compatible de Windows (Windows 8.1)
0x00B2 wTotalLength Es la longitud total del conjunto de descriptores.
Encabezado del subconjunto de configuración del SO 2.0 de Microsoft
0x0008 wLength Tamaño de este descriptor
0x0001 wDescriptorType Descripción del encabezado del subconjunto de configuración
0x00 bConfigurationValue Se aplica a la configuración 1 (indexada desde 0 a pesar de que las configuraciones se indexan normalmente desde 1).
0x00 bReserved Se debe establecer en 0.
0x00A8 wTotalLength Es la longitud total del subconjunto, incluido este encabezado.
Encabezado del subconjunto de funciones del SO 2.0 de Microsoft
0x0008 wLength Tamaño de este descriptor
0x0002 wDescriptorType Descriptor de encabezado del subconjunto de funciones
0x01 bFirstInterface Primera interfaz de la función
0x00 bReserved Se debe establecer en 0.
0x00A0 wSubsetLength Es la longitud total del subconjunto, incluido este encabezado.
Descriptor de ID compatible con el SO 2.0 de Microsoft
0x0014 wLength Tamaño de este descriptor
0x0003 wDescriptorType Descriptor de ID compatible
"WINUSB\0\0" CompatibileID Cadena ASCII rellena a 8 bytes
"\0\0\0\0\0\0\0\0" SubCompatibleID Cadena ASCII rellenada a 8 bytes
Descriptor de propiedades del registro del SO 2.0 de Microsoft
0x0084 wLength Tamaño de este descriptor
0x0004 wDescriptorType Descriptor de propiedades del registro
0x0007 wPropertyDataType REG_MULTI_SZ
0x002A wPropertyNameLength Longitud del nombre de la propiedad
"DeviceInterfaceGUIDs\0" PropertyName Nombre de propiedad con terminador nulo codificado en UTF-16LE
0x0050 wPropertyDataLength Longitud del valor de la propiedad
"{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}\0\0" PropertyData GUID más dos terminadores nulos codificados en UTF-16LE

Windows solo consultará esta información al dispositivo una vez. Si el dispositivo no responde con descriptores válidos, no volverá a preguntar la próxima vez que se conecte. Microsoft proporcionó una lista de entradas del registro del dispositivo USB que describe las entradas del registro creadas cuando se enumera un dispositivo. Cuando realices pruebas, borra las entradas creadas para un dispositivo para forzar a Windows a intentar leer los descriptores nuevamente.

Para obtener más información, consulta la entrada de blog de Microsoft sobre cómo usar estos descriptores.

Ejemplos

En estos proyectos, puedes encontrar código de ejemplo que implementa dispositivos compatibles con WebUSB que incluyen descriptores de WebUSB y descriptores del SO de Microsoft: