Accéder aux périphériques USB sur le Web

L'API WebUSB rend l'USB plus sûr et plus facile à utiliser en l'introduisant sur le Web.

François Beaufort
François Beaufort

Si je dis simplement "USB", vous penserez probablement immédiatement aux claviers, aux souris, aux périphériques audio, vidéo et de stockage. Vous avez raison, mais vous trouverez d'autres types de périphériques USB (Universal Serial Bus).

Ces appareils USB non standardisés nécessitent que les fournisseurs de matériel écrivent des pilotes et des SDK spécifiques à la plate-forme pour que vous (développeur) puissiez en profiter. Malheureusement, ce code spécifique à la plate-forme a historiquement empêché l'utilisation de ces appareils par le Web. C'est l'une des raisons pour lesquelles l'API WebUSB a été créée: pour fournir un moyen d'exposer les services de périphériques USB au Web. Grâce à cette API, les fabricants de matériel pourront créer des SDK JavaScript multiplates-formes pour leurs appareils.

Mais surtout, cela rendra l'USB plus sûr et plus facile à utiliser en l'introduisant sur le Web.

Voyons le comportement auquel vous pouvez vous attendre avec l'API WebUSB:

  1. Achetez un appareil USB.
  2. Branchez-la à votre ordinateur. Une notification s'affiche immédiatement, indiquant le site Web auquel accéder pour cet appareil.
  3. Cliquez sur la notification. Le site Web est prêt à être utilisé.
  4. Cliquez pour vous connecter. Un sélecteur d'appareils USB s'affiche dans Chrome, dans lequel vous pouvez sélectionner votre appareil.

Et voilà !

Comment se passerait cette procédure sans l'API WebUSB ?

  1. Installez une application spécifique à la plate-forme.
  2. S'il est même compatible avec mon système d'exploitation, vérifiez que j'ai bien téléchargé le bon fichier.
  3. Installez l'élément. Avec un peu de chance, aucune invite d'OS ni aucun pop-up effrayant ne s'affichera pour vous informer de l'installation de pilotes ou d'applications depuis Internet. Si vous avez de la malchance, les pilotes ou applications installés ne fonctionnent pas correctement et endommagent votre ordinateur. (N'oubliez pas que le Web est conçu pour contenir des sites Web défectueux.)
  4. Si vous n'utilisez cette fonctionnalité qu'une seule fois, le code reste sur votre ordinateur jusqu'à ce que vous pensiez à le supprimer. (Sur le Web, l'espace inutilisé est finalement récupéré.)

Avant de commencer

Cet article suppose que vous avez des connaissances de base sur le fonctionnement de la clé USB. Si ce n'est pas le cas, je vous recommande de lire USB dans une NutShell. Pour en savoir plus sur l'USB, consultez les spécifications officielles de l'USB.

L'API WebUSB est disponible dans Chrome 61.

Disponible pour les essais d'origine

Afin de recueillir autant de commentaires que possible de la part des développeurs qui utilisent l'API WebUSB sur le terrain, nous avons précédemment ajouté cette fonctionnalité dans Chrome 54 et Chrome 57 en tant que phase d'évaluation.

Le dernier test s'est bien terminé en septembre 2017.

Confidentialité et sécurité

HTTPS uniquement

En raison de la puissance de cette fonctionnalité, elle ne fonctionne que dans les contextes sécurisés. Cela signifie que vous devrez compiler en tenant compte de TLS.

Geste de l'utilisateur requis

Par mesure de sécurité, navigator.usb.requestDevice() ne peut être appelé que par un geste de l'utilisateur, tel qu'un appui ou un clic de souris.

Règles sur les autorisations

Une règle d'autorisation est un mécanisme qui permet aux développeurs d'activer et de désactiver sélectivement différentes fonctionnalités et API du navigateur. Il peut être défini via un en-tête HTTP et/ou un attribut "allow" d'un iframe.

Vous pouvez définir une stratégie d'autorisations qui contrôle si l'attribut usb est exposé sur l'objet Navigator, ou en d'autres termes si vous autorisez WebUSB.

Vous trouverez ci-dessous un exemple de stratégie d'en-tête dans laquelle WebUSB n'est pas autorisé:

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

Vous trouverez ci-dessous un autre exemple de règle de conteneur dans laquelle l'USB est autorisé:

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

Commençons à coder

L'API WebUSB s'appuie fortement sur les promesses JavaScript. Si vous ne les connaissez pas, consultez ce tutoriel sur les promesses. En outre, les () => {} sont simplement des fonctions fléchées d'ECMAScript 2015.

Accéder aux appareils USB

Vous pouvez demander à l'utilisateur de sélectionner un seul appareil USB connecté à l'aide de navigator.usb.requestDevice() ou appeler navigator.usb.getDevices() pour obtenir la liste de tous les appareils USB connectés auxquels le site Web a été autorisé.

La fonction navigator.usb.requestDevice() utilise un objet JavaScript obligatoire qui définit filters. Ces filtres permettent de faire correspondre n'importe quel appareil USB avec les identifiants de fournisseur (vendorId) et, éventuellement, de produit (productId) donnés. Les clés classCode, protocolCode, serialNumber et subclassCode peuvent également y être définies.

Capture d&#39;écran de l&#39;invite utilisateur de l&#39;appareil USB dans Chrome
Invite utilisateur pour le périphérique USB.

Par exemple, voici comment accéder à un appareil Arduino connecté et configuré pour autoriser 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); });

Avant que vous me posiez la question, je n'ai pas trouvé ce nombre hexadécimal 0x2341 par magie. J'ai simplement recherché le mot "Arduino" dans cette liste d'ID USB.

La clé USB device renvoyée dans la promesse exécutée ci-dessus contient des informations de base, mais importantes, sur l'appareil, telles que la version USB compatible, la taille maximale des paquets, le fournisseur et les ID produit, ainsi que le nombre de configurations possibles pour l'appareil. Il contient essentiellement tous les champs du descripteur USB de l'appareil.

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

À ce propos, si un périphérique USB annonce qu'il est compatible avec WebUSB et qu'il définit une URL de page de destination, Chrome affiche une notification persistante lorsque l'appareil USB est branché. Si vous cliquez sur cette notification, la page de destination s'ouvre.

Capture d&#39;écran de la notification WebUSB dans Chrome
Notification WebUSB.

Communiquer avec une carte Arduino USB

Voyons maintenant à quel point il est facile de communiquer à partir d'une carte Arduino compatible avec WebUSB via le port USB. Consultez les instructions sur la page https://github.com/webusb/arduino pour activer WebUSB dans vos esquisses.

Ne vous inquiétez pas, je vais aborder toutes les méthodes d'appareil WebUSB mentionnées ci-dessous plus tard dans cet article.

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

N'oubliez pas que la bibliothèque WebUSB que j'utilise n'implémente qu'un exemple de protocole (basé sur le protocole série USB standard) et que les fabricants peuvent créer n'importe quel ensemble et type de points de terminaison. Les transferts de contrôle sont particulièrement utiles pour les petites commandes de configuration, car ils bénéficient d'une priorité de bus et d'une structure bien définie.

Voici l'esquisse qui a été importée sur la carte 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 bibliothèque Arduino WebUSB tierce utilisée dans l'exemple de code ci-dessus effectue essentiellement deux tâches:

  • L'appareil agit comme un appareil WebUSB, ce qui permet à Chrome de lire l'URL de la page de destination.
  • Il expose une API WebUSB Serial que vous pouvez utiliser pour remplacer celle par défaut.

Examinez à nouveau le code JavaScript. Une fois que device a été sélectionné par l'utilisateur, device.open() exécute toutes les étapes spécifiques à la plate-forme pour démarrer une session avec le périphérique USB. Je dois ensuite sélectionner une configuration USB disponible avec device.selectConfiguration(). N'oubliez pas qu'une configuration spécifie le mode d'alimentation de l'appareil, sa consommation d'énergie maximale et son nombre d'interfaces. En parlant d'interfaces, je dois également demander un accès exclusif avec device.claimInterface(), car les données ne peuvent être transférées vers une interface ou des points de terminaison associés que lorsque l'interface est revendiquée. Enfin, l'appel de device.controlTransferOut() est nécessaire pour configurer l'appareil Arduino avec les commandes appropriées pour communiquer via l'API WebUSB Serial.

À partir de là, device.transferIn() effectue un transfert groupé sur l'appareil pour l'informer que l'hôte est prêt à recevoir des données groupées. Ensuite, la promesse est remplie avec un objet result contenant un data DataView qui doit être analysé de manière appropriée.

Si vous connaissez bien les USB, tout cela devrait vous sembler familier.

Je veux plus

L'API WebUSB vous permet d'interagir avec tous les types de transfert/points de terminaison USB:

  • Les transferts de contrôle, utilisés pour envoyer ou recevoir des paramètres de configuration ou de commande à un appareil USB, sont gérés avec controlTransferIn(setup, length) et controlTransferOut(setup, data).
  • Les transferts d'INTERRUPTION, utilisés pour une petite quantité de données sensibles dans le temps, sont traités avec les mêmes méthodes que les transferts BULK avec transferIn(endpointNumber, length) et transferOut(endpointNumber, data).
  • Les transferts ISOCHRONES, utilisés pour les flux de données tels que la vidéo et le son, sont gérés avec isochronousTransferIn(endpointNumber, packetLengths) et isochronousTransferOut(endpointNumber, data, packetLengths).
  • Les transferts groupés, qui permettent de transférer de grandes quantités de données non urgentes de manière fiable, sont gérés avec transferIn(endpointNumber, length) et transferOut(endpointNumber, data).

Vous pouvez également consulter le projet WebLight de Mike Tsao, qui fournit un exemple concret de création d'un appareil LED contrôlé par USB conçu pour l'API WebUSB (sans utiliser d'Arduino ici). Vous trouverez du matériel, des logiciels et des micrologiciels.

Révoquer l'accès à un périphérique USB

Le site Web peut nettoyer les autorisations d'accès à un appareil USB dont il n'a plus besoin en appelant forget() sur l'instance USBDevice. Par exemple, pour une application Web éducative utilisée sur un ordinateur partagé avec de nombreux appareils, un grand nombre d'autorisations générées par l'utilisateur nuit à l'expérience utilisateur.

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

Comme forget() est disponible dans Chrome 101 ou version ultérieure, vérifiez si cette fonctionnalité est compatible avec les éléments suivants:

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

Limites de taille de transfert

Certains systèmes d'exploitation imposent des limites à la quantité de données pouvant faire partie des transactions USB en attente. Diviser vos données en transactions plus petites et n'en soumettre que quelques-uns à la fois permet d'éviter ces limites. Il réduit également la quantité de mémoire utilisée et permet à votre application de signaler la progression à mesure que les transferts sont effectués.

Étant donné que plusieurs transferts envoyés à un point de terminaison s'exécutent toujours dans l'ordre, il est possible d'améliorer le débit en envoyant plusieurs segments mis en file d'attente pour éviter la latence entre les transferts USB. Chaque fois qu'un bloc est entièrement transmis, il avertit votre code qu'il doit fournir plus de données, comme indiqué dans l'exemple de fonction d'assistance ci-dessous.

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

Conseils

Le débogage USB dans Chrome est plus facile avec la page interne about://device-log, où vous pouvez consulter tous les événements liés aux appareils USB au même endroit.

Capture d&#39;écran de la page du journal de l&#39;appareil pour déboguer WebUSB dans Chrome
Page de journal de l'appareil dans Chrome pour déboguer l'API WebUSB.

La page interne about://usb-internals est également utile et vous permet de simuler la connexion et la déconnexion d'appareils WebUSB virtuels. Cela peut être utile pour effectuer des tests d'interface utilisateur sans matériel réel.

Capture d&#39;écran de la page interne permettant de déboguer WebUSB dans Chrome
Page interne dans Chrome pour déboguer l'API WebUSB

Sur la plupart des systèmes Linux, les périphériques USB sont mappés avec des autorisations en lecture seule par défaut. Pour autoriser Chrome à ouvrir un périphérique USB, vous devez ajouter une nouvelle règle udev. Créez un fichier à l'emplacement /etc/udev/rules.d/50-yourdevicename.rules avec le contenu suivant:

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

[yourdevicevendor] est 2341 si votre appareil est un Arduino, par exemple. Vous pouvez également ajouter ATTR{idProduct} pour une règle plus spécifique. Assurez-vous que votre user est membre du groupe plugdev. Il vous suffira ensuite de reconnecter votre appareil.

Ressources

Envoyez un tweet à @ChromiumDev en utilisant le hashtag #WebUSB et indiquez-nous où et comment vous l'utilisez.

Remerciements

Merci à Joe Medley d'avoir relu cet article.