Accesso a dispositivi USB sul Web

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

François Beaufort
François Beaufort

Se ho detto in modo chiaro e semplice "USB", ci sono buone probabilità che pensi immediatamente a tastiere, mouse, audio, video e dispositivi di archiviazione. Hai ragione, ma in giro 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 della piattaforma affinché tu, in qualità di sviluppatore, possano utilizzarli. Purtroppo, da sempre questo codice specifico per la piattaforma ha impedito l'utilizzo di questi dispositivi sul web. Questo è uno dei motivi per cui è stata creata l'API WebUSB: per fornire un modo per esporre i servizi del dispositivo USB sul web. Con questa API, i produttori di hardware potranno creare SDK JavaScript multipiattaforma per i loro dispositivi.

Ma soprattutto questo rende l'uso della porta USB più facile e sicura rendendola disponibile sul web.

Vediamo il comportamento previsto dall'API WebUSB:

  1. Acquista un dispositivo USB.
  2. Collegalo al computer. Viene visualizzata subito una notifica, con il sito web giusto a cui indirizzare il dispositivo.
  3. Fai clic sulla notifica. Il sito web è presente e pronto per l'uso.
  4. Fai clic per eseguire la connessione. In Chrome viene visualizzato un selettore dispositivo USB da cui puoi selezionare il dispositivo.

Tada!

Come sarebbe questa procedura senza l'API WebUSB?

  1. Installare un'applicazione specifica per la piattaforma.
  2. Se è supportato anche dal mio sistema operativo, verifica di aver scaricato il software giusto.
  3. Installa il dispositivo. Se hai fortuna, non riceverai richieste o popup spaventosi per il sistema operativo che ti avvisano sull'installazione di driver/applicazioni da internet. Se sei sfortunato, le applicazioni o i driver installati non funzionano correttamente e danneggiano il computer. Tieni presente che il web è progettato per contenere siti web che non funzionano correttamente.
  4. Se utilizzi la funzionalità una sola volta, il codice rimane sul computer finché non pensi di rimuoverlo. (Sul web, lo spazio per gli elementi inutilizzati viene infine recuperato).

Prima di iniziare

Questo articolo presuppone che tu disponga di una conoscenza di base del funzionamento del dispositivo USB. In caso contrario, ti consiglio di leggere USB in a NutShell. Per informazioni di base sull'USB, consulta le specifiche USB ufficiali.

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 con successo a settembre 2017.

Privacy e sicurezza

Solo HTTPS

Per via della sua potenza, funziona solo in contesti sicuri. Ciò significa che dovrai creare il modello TLS.

Gesto dell'utente richiesto

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 di autorizzazione è un meccanismo che consente agli sviluppatori di abilitare e disabilitare in modo selettivo varie funzionalità del browser e API. Può essere definito tramite un'intestazione HTTP e/o un attributo "allow" iframe.

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

Di seguito è riportato un esempio di criterio dell'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 container in cui è consentito USB:

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

Inizia a programmare

L'API WebUSB fa molto affidamento sulle promesse di JavaScript. Se non li conosci, guarda questo fantastico tutorial di Promise. Un'altra cosa: () => {} sono semplicemente le funzioni a freccia di ECMAScript 2015.

Accesso 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 il sito web è autorizzato ad accedere.

La funzione navigator.usb.requestDevice() accetta un oggetto JavaScript obbligatorio che definisce filters. Questi filtri vengono utilizzati per associare qualsiasi dispositivo USB al fornitore (vendorId) e, facoltativamente, agli ID prodotto (productId). Qui puoi anche definire le chiavi classCode, protocolCode, serialNumber e subclassCode.

Screenshot della richiesta di un dispositivo USB in Chrome
Richiesta dell'utente del 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 lo chieda, non ho magicamente trovato questo numero esadecimale 0x2341. Ho semplicemente cercato la parola "Arduino" in questo elenco di ID USB.

L'elemento device USB restituito nella promessa mantenuta 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. 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 suo supporto per WebUSB e definisce un URL pagina di destinazione, Chrome mostrerà una notifica persistente quando il dispositivo USB viene collegato. Se fai clic su questa notifica, si aprirà la pagina di destinazione.

Screenshot della notifica WebUSB in Chrome
Notifica WebUSB.

Comunica con una scheda USB Arduino

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

Non preoccuparti, tratterò tutti i metodi dei dispositivi WebUSB descritti 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 set e tipo di endpoint che desiderano. I trasferimenti dei controlli sono particolarmente utili per comandi di configurazione piccoli, in quanto hanno priorità bus e hanno una struttura ben definita.

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

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

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

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

Se conosci l'USB, tutto questo dovrebbe esserti familiare.

Ne voglio altro

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

  • I trasferimenti di CONTROL, utilizzati per inviare o ricevere i parametri di configurazione o di 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 fattore tempo, vengono gestiti con gli stessi metodi dei trasferimenti BULK con transferIn(endpointNumber, length) e transferOut(endpointNumber, data).
  • I trasferimenti ISOCHRONOUS, utilizzati per flussi di dati come video e audio, sono gestiti con isochronousTransferIn(endpointNumber, packetLengths) e isochronousTransferOut(endpointNumber, data, packetLengths).
  • I trasferimenti BULK, utilizzati per trasferire in modo affidabile una grande quantità di dati non sensibili al fattore tempo, vengono gestiti con transferIn(endpointNumber, length) e transferOut(endpointNumber, data).

Puoi anche dare un'occhiata al progetto WebLight di Mike Tsao, che fornisce un esempio completo della creazione di un dispositivo LED controllato tramite USB progettato per l'API WebUSB (non utilizzando Arduino qui). Troverai hardware, software e firmware.

Revocare l'accesso a un dispositivo USB

Il sito web può liberare 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 per l'istruzione utilizzata su un computer condiviso con molti dispositivi, un numero elevato di autorizzazioni accumulate dall'utente 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:

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

Limiti relativi alle dimensioni di trasferimento

Alcuni sistemi operativi impongono limiti alla quantità di dati che possono essere inclusi nelle transazioni USB in attesa. La suddivisione dei dati in transazioni più piccole e l'invio di pochi dati alla volta consente di evitare queste limitazioni. Riduce inoltre 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 la velocità effettiva inviando più blocchi in coda per evitare la latenza tra i trasferimenti USB. Ogni volta che un blocco viene trasmesso completamente, avviserà al tuo codice che dovrebbe fornire più dati, come documentato nell'esempio di funzione helper riportata 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 dei dispositivi USB in Chrome è più semplice con la pagina interna about://device-log, in cui puoi visualizzare tutti gli eventi relativi ai dispositivi USB in un'unica posizione.

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

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

Screenshot della pagina interna per il debug di WebUSB in Chrome
Pagina interna in 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 contenuti seguenti:

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

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

Risorse

Invia un tweet a @ChromiumDev usando l'hashtag #WebUSB e facci sapere dove e come lo stai usando.

Ringraziamenti

Grazie a Joe Medley per aver letto questo articolo.