Cómo acceder a dispositivos USB en la Web

La API de WebUSB hace que USB sea más seguro y fácil de usar al llevarlo a la Web.

François Beaufort
François Beaufort

Si dijera de forma clara y simple "USB", es muy probable que pienses de inmediato en teclados, mouses, audio, video y dispositivos de almacenamiento. Tienes razón, pero encontrarás 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 de la plataforma para que tú (como desarrollador) los aproveches. Lamentablemente, este código específico de la plataforma impidió históricamente que la Web usara estos dispositivos. Esa es una de las razones por las que se creó la API de WebUSB: para proporcionar una manera de exponer los servicios de dispositivos USB a la Web. Con esta API, los fabricantes de hardware podrán compilar SDK multiplataforma de JavaScript para sus dispositivos.

Pero, lo más importante, esto hará que USB sea más seguro y fácil de usar si lo lleva a la Web.

Veamos el comportamiento que puedes esperar con la API de WebUSB:

  1. Compra un dispositivo USB.
  2. Conéctalo a tu computadora. Aparecerá de inmediato una notificación con el sitio web adecuado para ese dispositivo.
  3. Haz clic en la notificación. ¡El sitio web está ahí y listo para usar!
  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 incluso es compatible con mi sistema operativo, verifica que descargué el dispositivo correcto.
  3. Instala la cosa. Si tienes suerte, no recibirás mensajes del SO o ventanas emergentes aterradoras que te adviertan sobre la instalación de controladores o aplicaciones desde Internet. Si no tienes suerte, los controladores o las aplicaciones instalados no funcionan correctamente y dañan tu computadora. (Recuerda que la Web está diseñada para contener sitios web que funcionan mal).
  4. Si solo usas la función una vez, el código permanecerá en la computadora hasta que pienses en quitarlo. (en la Web, el espacio para los que no se usa se recupera con el tiempo).

Antes de empezar

En este artículo, se asume que tienes conocimientos básicos sobre el funcionamiento de USB. Si no es así, te recomendamos que leas USB en NutShell. Para obtener más información sobre la conexión 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, anteriormente agregamos esta función en Chrome 54 y Chrome 57 como 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 tener en cuenta TLS cuando compiles.

Gesto del usuario requerido

Como precaución de seguridad, solo se puede llamar a navigator.usb.requestDevice() mediante 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 les permite a los desarrolladores habilitar o inhabilitar de forma selectiva varias APIs y funciones del navegador. Se puede definir mediante 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 WebUSB no está permitido:

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 el uso de USB:

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

Comencemos a programar

La API de WebUSB se basa en gran medida en las promesas de JavaScript. Si no estás familiarizado con ellas, consulta este excelente instructivo sobre las promesas. Un dato adicional es que () => {} 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 mediante navigator.usb.requestDevice() o llamar a navigator.usb.getDevices() para obtener una lista de todos los dispositivos USB conectados a los que tiene acceso el 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 el proveedor determinado (vendorId) y, de forma opcional, con los identificadores de producto (productId). Las claves classCode, protocolCode, serialNumber y subclassCode también se pueden definir allí.

Captura de pantalla de la solicitud del usuario del dispositivo USB en Chrome
Mensaje del 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 se me ocurrió mágicamente este número hexadecimal 0x2341. Solo busqué la palabra "Arduino" en esta Lista de ID de USB.

El USB device que se muestra en la promesa entregada anterior contiene información básica, pero importante, sobre el dispositivo, como la versión USB compatible, el tamaño máximo del paquete, el proveedor y los IDs del 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 el dispositivo USB esté conectado. Si hace 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 hablar con una placa Arduino USB

De acuerdo, 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 bocetos.

No te preocupes, analizaremos todos los métodos de dispositivos WebUSB que se mencionan 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 WebUSB que estoy usando solo implementa un protocolo de ejemplo (basado en el protocolo en serie estándar USB) y que los fabricantes pueden crear cualquier conjunto y tipos de extremos que deseen. Los transbordos de control son especialmente útiles para los comandos de configuración pequeños, ya que tienen prioridad de bus y tienen una estructura bien definida.

Y este es el boceto que se cargó en el tablero de 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 WebUSB Arduino de terceros que se usa en el código de muestra anterior realiza básicamente dos acciones:

  • Funciona como un dispositivo WebUSB, lo que permite que Chrome lea la URL de la página de destino.
  • Expone una API de WebUSB Serial que puedes usar para anular la predeterminada.

Observa el código JavaScript de nuevo. Una vez que el usuario elige el device, 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 USB disponible con device.selectConfiguration(). Recuerda que una configuración especifica la manera en que se alimenta el dispositivo, su consumo máximo de energía y su cantidad de interfaces. Hablando de interfaces, también debo solicitar acceso exclusivo con device.claimInterface(), ya que los datos solo se pueden transferir a una interfaz o a extremos asociados cuando se reclama la interfaz. Por último, se necesita llamar a device.controlTransferOut() para configurar el dispositivo Arduino con los comandos adecuados y comunicarse a través de la API de WebUSB Serial.

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

Si estás familiarizado con el uso de USB, todo esto debería resultarte bastante familiar.

Quiero más

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

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

También puedes consultar el proyecto WebLight de Mike Tsao, que brinda un ejemplo inicial de cómo compilar un dispositivo LED controlado por USB diseñado para la API de WebUSB (sin usar Arduino en este caso). Encontrarás hardware, software y firmware.

Cómo revocar el acceso a un dispositivo USB

El sitio web puede liberar permisos para acceder a un dispositivo USB que ya no necesita llamando a forget() en la instancia 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 acumulados generados por el usuario crea una experiencia del usuario deficiente.

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

Dado que 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 sobre la cantidad de datos que pueden formar parte de transacciones USB pendientes. Dividir los datos en transacciones más pequeñas y enviar solo unos pocos a la vez ayuda a evitar esas limitaciones. También reduce la cantidad de memoria usada y permite que tu app informe el progreso a medida que se completan las transferencias.

Dado que las transferencias múltiples que se envían a un extremo siempre se ejecutan en orden, es posible mejorar la capacidad de procesamiento enviando varios fragmentos en cola para evitar la latencia entre transferencias USB. Cada vez que un fragmento se transmita por completo, se le notificará a tu código que debe proporcionar más datos, como se documenta en el siguiente ejemplo de 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 los dispositivos USB en un solo lugar.

Captura de pantalla de la página de registro del dispositivo para depurar WebUSB en Chrome
Página de 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 resulta ú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 en 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 regla udev nueva. Crea un archivo en /etc/udev/rules.d/50-yourdevicename.rules con el siguiente contenido:

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

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

Recursos

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

Agradecimientos

Agradecemos a Joe Medley por revisar este artículo.