Accesso a dispositivi USB sul Web

L'API WebUSB rende l'USB più sicura e facile da usare portandola sul web.

François Beaufort
François Beaufort

Se dicessi semplicemente "USB", è molto probabile che tu penserebbe immediatamente a tastiere, mouse, audio, video e dispositivi di archiviazione. È vero, ma in questo punto ci sono altri tipi di dispositivi USB (Universal Serial Bus).

Questi dispositivi USB non standardizzati richiedono ai fornitori di hardware di scrivere driver e SDK specifici per la piattaforma affinché tu (lo sviluppatore) possa utilizzarli. Purtroppo, questo codice specifico della piattaforma ha impedito storicamente l'utilizzo di questi dispositivi da parte del web. Ed è uno dei motivi per cui è stata creata l'API WebUSB: fornire un modo per esporre i servizi dei dispositivi USB al web. Con questa API, i produttori di hardware potranno creare SDK JavaScript multipiattaforma per i propri dispositivi.

Ma, soprattutto, renderà la tecnologia USB più sicura e facile da usare portandola sul web.

Vediamo il comportamento che potresti aspettarti con l'API WebUSB:

  1. Acquista un dispositivo USB.
  2. Collegalo al computer. Viene visualizzata immediatamente una notifica con il sito web corretto da visitare per questo dispositivo.
  3. Fai clic sulla notifica. Il sito web è pronto per essere utilizzato.
  4. Fai clic per connetterti e in Chrome viene visualizzato un selettore di dispositivi USB in cui puoi scegliere il tuo dispositivo.

Ecco fatto!

Come sarebbe questa procedura senza l'API WebUSB?

  1. Installa un'applicazione specifica per la piattaforma.
  2. Se è supportato anche sul mio sistema operativo, verifica che abbia scaricato la versione corretta.
  3. Installa il dispositivo. Se sei fortunato, non riceverai richieste o popup del sistema operativo che ti avvisano di installare driver/app da internet. Se hai sfortuna, i driver o le applicazioni installati funzionano male e danneggiano il computer. Ricorda che il web è progettato per includere siti web con malfunzionamenti.
  4. Se utilizzi la funzionalità una sola volta, il codice rimane sul computer finché non lo rimuovi. Sul web, lo spazio per gli elementi inutilizzati viene eventualmente recuperato.

Prima di iniziare

Questo articolo presuppone che tu abbia alcune conoscenze di base sul funzionamento del connettore USB. In caso contrario, ti consiglio di leggere USB in a NutShell. Per informazioni generali sull'USB, consulta le specifiche ufficiali dell'USB.

L'API WebUSB è disponibile in Chrome 61.

Disponibile per le prove dell'origine

Per ricevere il maggior numero possibile di feedback dagli sviluppatori che utilizzano l'API WebUSB sul campo, in precedenza abbiamo aggiunto questa funzionalità in Chrome 54 e Chrome 57 come prova dell'origine.

L'ultima prova è terminata correttamente a settembre 2017.

Privacy e sicurezza

Solo HTTPS

A causa della sua potenza, questa funzionalità funziona solo in contesti sicuri. Ciò significa che dovresti eseguire la compilazione tenendo presente TLS.

Gesto dell'utente obbligatorio

Per motivi di sicurezza, navigator.usb.requestDevice() può essere chiamato solo tramite un gesto dell'utente, ad esempio un tocco o un clic del mouse.

Norme relative alle autorizzazioni

Un criterio relativo alle autorizzazioni è un meccanismo che consente agli sviluppatori di attivare e disattivare in modo selettivo varie funzionalità e API del browser. Può essere definito tramite un header HTTP e/o un attributo "allow" iframe.

Puoi definire un criterio di autorizzazione che controlla se l'attributo usb è esposto nell'oggetto Navigator o, in altre parole, se consenti WebUSB.

Di seguito è riportato un esempio di criterio di intestazione in cui WebUSB non è consentito:

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

Di seguito è riportato un altro esempio di criterio del contenitore in cui è consentita la porta USB:

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

Iniziamo a programmare

L'API WebUSB si basa molto sulle promise di JavaScript. Se non le conosci, consulta questo fantastico tutorial sulle promesse. Un'altra cosa: () => {} sono semplicemente funzioni Arrow di ECMAScript 2015.

Accedere ai dispositivi USB

Puoi chiedere all'utente di selezionare un singolo dispositivo USB connesso utilizzando navigator.usb.requestDevice() o chiamare il numero navigator.usb.getDevices() per ottenere un elenco di tutti i dispositivi USB connessi a cui è stato concesso l'accesso al sito web.

La funzione navigator.usb.requestDevice() accetta un oggetto JavaScript obbligatorio che definisce filters. Questi filtri vengono utilizzati per abbinare qualsiasi dispositivo USB agli identificatori del fornitore (vendorId) e, facoltativamente, degli identificatori del prodotto (productId). Anche le chiavi classCode, protocolCode, serialNumber e subclassCode possono essere definite qui.

Screenshot della richiesta utente del dispositivo USB in Chrome
Richiesta all'utente per il dispositivo USB.

Ad esempio, ecco come ottenere l'accesso a un dispositivo Arduino connesso configurato per consentire l'origine.

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); });

Prima che tu me lo chieda, non ho inventato questo numero esadecimale 0x2341. Ho semplicemente cercato la parola "Arduino" in questo elenco di ID USB.

L'oggetto USB device restituito nella promessa soddisfatta sopra contiene alcune informazioni di base, ma importanti, sul dispositivo, come la versione USB supportata, la dimensione massima del pacchetto, il fornitore e gli ID prodotto, il numero di possibili configurazioni che il dispositivo può avere. Fondamentalmente, contiene tutti i campi del descrittore 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"
  });
})

A proposito, se un dispositivo USB annuncia il proprio supporto di WebUSB, oltre a definire un URL pagina di destinazione, Chrome mostrerà una notifica persistente quando il dispositivo USB è collegato. Se fai clic su questa notifica, si aprirà la pagina di destinazione.

Screenshot della notifica WebUSB in Chrome
Notifica WebUSB
.

Parlare con una scheda Arduino USB

Bene, ora vediamo quanto è facile comunicare da una scheda Arduino compatibile con WebUSB tramite la porta USB. Consulta le istruzioni all'indirizzo https://github.com/webusb/arduino per abilitare WebUSB per i tuoi schizzi.

Non preoccuparti, tratterò tutti i metodi del dispositivo WebUSB menzionati di seguito più avanti in questo articolo.

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); });

Tieni presente che la libreria WebUSB che sto utilizzando implementa solo un protocollo di esempio (basato sul protocollo seriale USB standard) e che i produttori possono creare qualsiasi insieme e tipo di endpoint che preferiscono. I trasferimenti di controllo sono particolarmente utili per comandi di configurazione di piccole dimensioni, poiché hanno una priorità del bus e hanno una struttura ben definita.

Ed ecco lo schizzo che è stato caricato sulla lavagna 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 libreria Arduino WebUSB di terze parti utilizzata nel codice di esempio riportato sopra fa principalmente due cose:

  • Il dispositivo agisce come dispositivo WebUSB consentendo a Chrome di leggere l'URL della pagina di destinazione.
  • Espone un'API WebUSB Serial che puoi utilizzare per sostituire quella predefinita.

Esamina di nuovo il codice JavaScript. Una volta che l'utente ha scelto device, device esegue tutti i passaggi specifici della piattaforma per avviare una sessione con il dispositivo USB.device.open() Poi devo selezionare una configurazione USB disponibile con device.selectConfiguration(). Ricorda che una configurazione specifica come viene alimentato il dispositivo, il suo consumo energetico massimo e il numero di interfacce. A proposito di interfacce, devo anche richiedere l'accesso esclusivo con device.claimInterface() poiché i dati possono essere trasferiti a un'interfaccia o gli endpoint associati solo quando l'interfaccia viene rivendicata. Infine, è necessario chiamare device.controlTransferOut() per configurare il dispositivo Arduino con i comandi appropriati per comunicare tramite l'API seriale WebUSB.

Da qui, device.transferIn() esegue un trasferimento collettivo sul dispositivo per informarlo che l'host è pronto a ricevere dati collettivi. Quindi, la promessa viene soddisfatta con un oggetto result contenente un data DataView che deve essere analizzato correttamente.

Se hai dimestichezza con le porte USB, tutto questo dovrebbe sembrarti familiare.

Voglio di più

L'API WebUSB ti consente di interagire con tutti i tipi di trasferimento/endpoint USB:

  • I trasferimenti CONTROL, utilizzati per inviare o ricevere parametri di configurazione o comando a un dispositivo USB, vengono gestiti con controlTransferIn(setup, length) e controlTransferOut(setup, data).
  • I trasferimenti INTERRUPT, utilizzati per una piccola quantità di dati sensibili al tempo, vengono gestiti con gli stessi metodi dei trasferimenti BULK con transferIn(endpointNumber, length) e transferOut(endpointNumber, data).
  • I trasferimenti ISOCRONICI, utilizzati per stream di dati come video e audio, vengono gestiti con isochronousTransferIn(endpointNumber, packetLengths) e isochronousTransferOut(endpointNumber, data, packetLengths).
  • I trasferimenti BULK, utilizzati per trasferire una grande quantità di dati non sensibili al tempo in modo affidabile, vengono gestiti con transferIn(endpointNumber, length) e transferOut(endpointNumber, data).

Ti consigliamo anche di dare un'occhiata al progetto WebLight di Mike Tsao, che fornisce un esempio completo di creazione di un dispositivo LED controllato da USB progettato per l'API WebUSB (non viene utilizzato Arduino). Troverai hardware, software e firmware.

Revocare l'accesso a un dispositivo USB

Il sito web può eliminare le autorizzazioni per accedere a un dispositivo USB di cui non ha più bisogno chiamando forget() sull'istanza USBDevice. Ad esempio, per un'applicazione web educativa utilizzata su un computer condiviso con molti dispositivi, un numero elevato di autorizzazioni generate dagli utenti accumulate crea un'esperienza utente negativa.

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

Poiché forget() è disponibile in Chrome 101 o versioni successive, controlla se questa funzionalità è supportata con quanto segue:

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

Limiti di dimensione del trasferimento

Alcuni sistemi operativi impongono limiti alla quantità di dati che possono essere inclusi nelle transazioni USB in attesa. Suddividere i dati in transazioni più piccole e inviare solo pochi dati alla volta consente di evitare queste limitazioni. Inoltre, riduce la quantità di memoria utilizzata e consente all'applicazione di segnalare l'avanzamento man mano che i trasferimenti vengono completati.

Poiché più trasferimenti inviati a un endpoint vengono sempre eseguiti in ordine, è possibile migliorare il throughput inviando più chunk in coda per evitare la latenza tra i trasferimenti USB. Ogni volta che un chunk viene trasmesso completamente, il codice viene informato che deve fornire più dati, come descritto nell'esempio di funzione di supporto riportato di seguito.

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);
}

Suggerimenti

Il debug USB in Chrome è più facile con la pagina interna about://device-log dove puoi vedere tutti gli eventi relativi ai dispositivi USB in un unico posto.

Screenshot della pagina del log del dispositivo per eseguire il debug di WebUSB in Chrome
Pagina di log del dispositivo in Chrome per eseguire il debug dell'API WebUSB.

Anche la pagina interna about://usb-internals è utile e consente di simulare la connessione e la disconnessione dei dispositivi WebUSB virtuali. Questa opzione è utile per eseguire test dell'interfaccia utente senza hardware reale.

Screenshot della pagina interna per il debug di WebUSB in Chrome
Pagina interna di Chrome per il debug dell'API WebUSB.

Sulla maggior parte dei sistemi Linux, i dispositivi USB sono mappati con autorizzazioni di sola lettura per impostazione predefinita. Per consentire a Chrome di aprire un dispositivo USB, devi aggiungere una nuova regola udev. Crea un file in /etc/udev/rules.d/50-yourdevicename.rules con i seguenti contenuti:

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

dove [yourdevicevendor] è 2341 se il tuo dispositivo è ad esempio un Arduino. ATTR{idProduct} può essere aggiunto anche per una regola più specifica. Assicurati che user sia un membro del gruppo plugdev. Dopodiché, ricollega il dispositivo.

Risorse

Invia un tweet all'indirizzo @ChromiumDev utilizzando l'hashtag #WebUSB e facci sapere dove e come lo utilizzi.

Ringraziamenti

Grazie a Joe Medley per aver esaminato questo articolo.