Cómo acceder a dispositivos USB en la Web

La API de WebUSB lleva el USB a la Web, lo que lo hace más seguro y fácil de usar.

François Beaufort
François Beaufort

Si dijera “USB” de forma clara y sencilla, es probable que pienses de inmediato en teclados, mouses, audio, video y dispositivos de almacenamiento. Tienes razón, pero hay otros tipos de dispositivos de bus universal en serie (USB).

Estos dispositivos USB no estandarizados requieren que los proveedores de hardware escriban controladores y SDKs específicos para cada plataforma para que tú (el desarrollador) puedas aprovecharlos. Lamentablemente, este código específico de la plataforma históricamente impidió que la Web usara estos dispositivos. Esa es una de las razones por las que se creó la API de WebUSB: para proporcionar una forma de exponer los servicios de dispositivos USB a la Web. Con esta API, los fabricantes de hardware podrán compilar SDKs de JavaScript multiplataforma para sus dispositivos.

Pero lo más importante es que llevará el USB a la Web, lo que lo hará más seguro y fácil de usar.

Veamos el comportamiento que podrías esperar con la API de WebUSB:

  1. Compra un dispositivo USB.
  2. Conéctala a tu computadora. Aparecerá una notificación de inmediato con el sitio web correcto al que debes ir para este dispositivo.
  3. Haz clic en la notificación. El sitio web está listo para usarse.
  4. Haz clic para conectarte y aparecerá un selector de dispositivos USB en Chrome en el que podrás elegir tu dispositivo.

Listo.

¿Cómo sería este procedimiento sin la API de WebUSB?

  1. Instala una aplicación específica de la plataforma.
  2. Si es compatible con mi sistema operativo, verifica que haya descargado el archivo correcto.
  3. Instala el dispositivo. Si tienes suerte, no recibirás mensajes ni ventanas emergentes del SO que te adviertan sobre la instalación de controladores o aplicaciones desde Internet. Si tienes mala suerte, los controladores o las aplicaciones instalados fallan y dañan la computadora. (recuerda que la Web está diseñada para contener sitios web que no funcionan correctamente).
  4. Si solo usas la función una vez, el código permanecerá en tu computadora hasta que decidas quitarlo. (en la Web, el espacio para los elementos que no se usan se recupera con el tiempo).

Antes de comenzar

En este artículo, se da por sentado que tienes conocimientos básicos sobre el funcionamiento de USB. De lo contrario, te recomiendo que leas USB in a NutShell. Para obtener información general sobre USB, consulta las especificaciones oficiales de USB.

La API de WebUSB está disponible en Chrome 61.

Disponible para pruebas de origen

Para obtener la mayor cantidad posible de comentarios de los desarrolladores que usan la API de WebUSB en el campo, agregamos esta función en Chrome 54 y Chrome 57 como una prueba de origen.

La prueba más reciente finalizó correctamente en septiembre de 2017.

Privacidad y seguridad

Solo HTTPS

Debido a la potencia de esta función, solo funciona en contextos seguros. Esto significa que deberás compilar teniendo en cuenta TLS.

Se requiere un gesto del usuario

Como precaución de seguridad, solo se puede llamar a navigator.usb.requestDevice() a través de un gesto del usuario, como un toque o un clic con el mouse.

Política de permisos

Una política de permisos es un mecanismo que permite a los desarrolladores habilitar y inhabilitar de forma selectiva varias funciones y APIs del navegador. Se puede definir a través de un encabezado HTTP o un atributo "allow" de iframe.

Puedes definir una política de permisos que controle si el atributo usb se expone en el objeto Navigator o, en otras palabras, si permites WebUSB.

A continuación, se muestra un ejemplo de una política de encabezado en la que no se permite WebUSB:

Feature-Policy: fullscreen "*"; usb "none"; payment "self" https://payment.example.com

A continuación, se muestra otro ejemplo de una política de contenedor en la que se permite USB:

<iframe allowpaymentrequest allow="usb; fullscreen"></iframe>

Comencemos a programar

La API de WebUSB depende en gran medida de las promesas de JavaScript. Si no estás familiarizado con ellas, consulta este excelente instructivo sobre promesas. Otra cosa, () => {} son simplemente funciones de flecha de ECMAScript 2015.

Cómo obtener acceso a dispositivos USB

Puedes pedirle al usuario que seleccione un solo dispositivo USB conectado con navigator.usb.requestDevice() o llamar a navigator.usb.getDevices() para obtener una lista de todos los dispositivos USB conectados a los que se le otorgó acceso al sitio web.

La función navigator.usb.requestDevice() toma un objeto JavaScript obligatorio que define filters. Estos filtros se usan para hacer coincidir cualquier dispositivo USB con los identificadores de proveedor (vendorId) y, de manera opcional, de producto (productId) determinados. Las claves classCode, protocolCode, serialNumber y subclassCode también se pueden definir allí.

Captura de pantalla del mensaje del usuario del dispositivo USB en Chrome
Mensaje para el usuario del dispositivo USB.

Por ejemplo, aquí se muestra cómo obtener acceso a un dispositivo Arduino conectado configurado para permitir el origen.

navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341 }] })
.then(device => {
  console.log(device.productName);      // "Arduino Micro"
  console.log(device.manufacturerName); // "Arduino LLC"
})
.catch(error => { console.error(error); });

Antes de que lo preguntes, no encontré este número hexadecimal 0x2341 por arte de magia. Solo busqué la palabra “Arduino” en esta lista de IDs de USB.

El device USB que se muestra en la promesa cumplida anterior tiene información básica, pero importante, sobre el dispositivo, como la versión de USB compatible, el tamaño máximo de paquete, el proveedor y los IDs de producto, y la cantidad de configuraciones posibles que puede tener el dispositivo. Básicamente, contiene todos los campos del descriptor USB del dispositivo.

// Get all connected USB devices the website has been granted access to.
navigator.usb.getDevices().then(devices => {
  devices.forEach(device => {
    console.log(device.productName);      // "Arduino Micro"
    console.log(device.manufacturerName); // "Arduino LLC"
  });
})

Por cierto, si un dispositivo USB anuncia su compatibilidad con WebUSB y define una URL de página de destino, Chrome mostrará una notificación persistente cuando se conecte el dispositivo USB. Si haces clic en esta notificación, se abrirá la página de destino.

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

Cómo comunicarse con una placa USB de Arduino

Bien, ahora veamos lo fácil que es comunicarse desde una placa Arduino compatible con WebUSB a través del puerto USB. Consulta las instrucciones en https://github.com/webusb/arduino para habilitar WebUSB en tus esquemas.

No te preocupes, explicaré todos los métodos de dispositivos WebUSB que se mencionan a continuación más adelante en este artículo.

let device;

navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341 }] })
.then(selectedDevice => {
    device = selectedDevice;
    return device.open(); // Begin a session.
  })
.then(() => device.selectConfiguration(1)) // Select configuration #1 for the device.
.then(() => device.claimInterface(2)) // Request exclusive control over interface #2.
.then(() => device.controlTransferOut({
    requestType: 'class',
    recipient: 'interface',
    request: 0x22,
    value: 0x01,
    index: 0x02})) // Ready to receive data
.then(() => device.transferIn(5, 64)) // Waiting for 64 bytes of data from endpoint #5.
.then(result => {
  const decoder = new TextDecoder();
  console.log('Received: ' + decoder.decode(result.data));
})
.catch(error => { console.error(error); });

Ten en cuenta que la biblioteca de WebUSB que uso solo implementa un protocolo de ejemplo (basado en el protocolo serie USB estándar) y que los fabricantes pueden crear cualquier conjunto y tipo de extremos que deseen. Las transferencias de control son especialmente útiles para comandos de configuración pequeños, ya que obtienen prioridad de bus y tienen una estructura bien definida.

Este es el boceto que se subió a la placa Arduino.

// Third-party WebUSB Arduino library
#include <WebUSB.h>

WebUSB WebUSBSerial(1 /* https:// */, "webusb.github.io/arduino/demos");

#define Serial WebUSBSerial

void setup() {
  Serial.begin(9600);
  while (!Serial) {
    ; // Wait for serial port to connect.
  }
  Serial.write("WebUSB FTW!");
  Serial.flush();
}

void loop() {
  // Nothing here for now.
}

La biblioteca de Arduino WebUSB de terceros que se usa en el código de muestra anterior hace básicamente dos cosas:

  • El dispositivo actúa como un dispositivo WebUSB que permite que Chrome lea la URL de la página de destino.
  • Expone una API de serie WebUSB que puedes usar para anular la predeterminada.

Vuelve a mirar el código JavaScript. Una vez que obtengo el device que eligió el usuario, device.open() ejecuta todos los pasos específicos de la plataforma para iniciar una sesión con el dispositivo USB. Luego, debo seleccionar una configuración de USB disponible con device.selectConfiguration(). Recuerda que una configuración especifica cómo se alimenta el dispositivo, su consumo máximo de energía y su cantidad de interfaces. A propósito de las interfaces, también debo solicitar acceso exclusivo con device.claimInterface(), ya que los datos solo se pueden transferir a una interfaz o a los extremos asociados cuando se reclama la interfaz. Por último, es necesario llamar a device.controlTransferOut() para configurar el dispositivo Arduino con los comandos adecuados para comunicarse a través de la API de serie de WebUSB.

A partir de ahí, device.transferIn() realiza una transferencia masiva al dispositivo para informarle que el host está listo para recibir datos masivos. Luego, la promesa se completa con un objeto result que contiene un data DataView que se debe analizar de forma adecuada.

Si conoces el USB, todo esto debería resultarte familiar.

Quiero más

La API de WebUSB te permite interactuar con todos los tipos de transferencia o extremo USB:

  • Las transferencias de CONTROL, que se usan para enviar o recibir parámetros de configuración o comando a un dispositivo USB, se controlan con controlTransferIn(setup, length) y controlTransferOut(setup, data).
  • Las transferencias de INTERRUPCIÓN, que se usan para una pequeña cantidad de datos sensibles al tiempo, se controlan con los mismos métodos que las transferencias masivas con transferIn(endpointNumber, length) y transferOut(endpointNumber, data).
  • Las transferencias ISOCRONAS, que se usan para transmisiones de datos, como video y sonido, se controlan con isochronousTransferIn(endpointNumber, packetLengths) y isochronousTransferOut(endpointNumber, data, packetLengths).
  • Las transferencias masivas, que se usan para transferir una gran cantidad de datos no urgentes de forma confiable, se controlan con transferIn(endpointNumber, length) y transferOut(endpointNumber, data).

También te recomendamos que mires el proyecto WebLight de Mike Tsao, que proporciona un ejemplo desde cero de cómo compilar un dispositivo LED controlado por USB diseñado para la API de WebUSB (no se usa Arduino aquí). Encontrarás hardware, software y firmware.

Cómo revocar el acceso a un dispositivo USB

El sitio web puede limpiar los permisos de acceso a un dispositivo USB que ya no necesita llamando a forget() en la instancia de USBDevice. Por ejemplo, en el caso de una aplicación web educativa que se usa en una computadora compartida con muchos dispositivos, una gran cantidad de permisos generados por el usuario acumulados crea una experiencia del usuario deficiente.

// Voluntarily revoke access to this USB device.
await device.forget();

Como forget() está disponible en Chrome 101 o versiones posteriores, verifica si esta función es compatible con lo siguiente:

if ("usb" in navigator && "forget" in USBDevice.prototype) {
  // forget() is supported.
}

Límites de tamaño de transferencia

Algunos sistemas operativos imponen límites a la cantidad de datos que pueden ser parte de las transacciones USB pendientes. Dividir los datos en transacciones más pequeñas y enviar solo algunas a la vez ayuda a evitar esas limitaciones. También reduce la cantidad de memoria utilizada y permite que tu aplicación informe el progreso a medida que se completan las transferencias.

Debido a que varias transferencias enviadas a un extremo siempre se ejecutan en orden, es posible mejorar la capacidad de procesamiento enviando varios fragmentos en fila para evitar la latencia entre las transferencias USB. Cada vez que se transmita un fragmento por completo, se notificará a tu código que debe proporcionar más datos, como se documenta en el siguiente ejemplo de la función auxiliar.

const BULK_TRANSFER_SIZE = 16 * 1024; // 16KB
const MAX_NUMBER_TRANSFERS = 3;

async function sendRawPayload(device, endpointNumber, data) {
  let i = 0;
  let pendingTransfers = [];
  let remainingBytes = data.byteLength;
  while (remainingBytes > 0) {
    const chunk = data.subarray(
      i * BULK_TRANSFER_SIZE,
      (i + 1) * BULK_TRANSFER_SIZE
    );
    // If we've reached max number of transfers, let's wait.
    if (pendingTransfers.length == MAX_NUMBER_TRANSFERS) {
      await pendingTransfers.shift();
    }
    // Submit transfers that will be executed in order.
    pendingTransfers.push(device.transferOut(endpointNumber, chunk));
    remainingBytes -= chunk.byteLength;
    i++;
  }
  // And wait for last remaining transfers to complete.
  await Promise.all(pendingTransfers);
}

Sugerencias

La depuración de USB en Chrome es más fácil con la página interna about://device-log, en la que puedes ver todos los eventos relacionados con el dispositivo USB en un solo lugar.

Captura de pantalla de la página de registro del dispositivo para depurar WebUSB en Chrome
Página del registro del dispositivo en Chrome para depurar la API de WebUSB.

La página interna about://usb-internals también es útil y te permite simular la conexión y desconexión de dispositivos WebUSB virtuales. Esto es útil para realizar pruebas de IU sin hardware real.

Captura de pantalla de la página interna para depurar WebUSB en Chrome
Página interna de Chrome para depurar la API de WebUSB.

En la mayoría de los sistemas Linux, los dispositivos USB se asignan con permisos de solo lectura de forma predeterminada. Para permitir que Chrome abra un dispositivo USB, deberás agregar una nueva regla de udev. Crea un archivo en /etc/udev/rules.d/50-yourdevicename.rules con el siguiente contenido:

SUBSYSTEM=="usb", ATTR{idVendor}=="[yourdevicevendor]", MODE="0664", GROUP="plugdev"

donde [yourdevicevendor] es 2341 si tu dispositivo es un Arduino, por ejemplo. También se puede agregar ATTR{idProduct} para una regla más específica. Asegúrate de que tu user sea miembro del grupo plugdev. Luego, vuelve a conectar el dispositivo.

Recursos

Envía un tuit a @ChromiumDev con el hashtag #WebUSB y cuéntanos dónde y cómo lo usas.

Agradecimientos

Gracias a Joe Medley por revisar este artículo.