Auf USB-Geräte im Web zugreifen

Die WebUSB API macht USB sicherer und einfacher, da sie ins Web integriert wird.

François Beaufort
François Beaufort

Wenn ich einfach und einfach nur „USB“ sagen würde, werden Sie mit hoher Wahrscheinlichkeit direkt an Tastaturen, Mäuse, Audio-, Video- und Speichergeräte denken. Das stimmt, aber es gibt noch andere Arten von USB-Geräten (Universal Serial Bus).

Für diese nicht standardisierten USB-Geräte müssen Hardwareanbieter plattformspezifische Treiber und SDKs schreiben, damit Sie (der Entwickler) sie nutzen können. Dieser plattformspezifische Code hat in der Vergangenheit leider verhindert, dass diese Geräte im Web verwendet werden. Das ist einer der Gründe, warum die WebUSB API entwickelt wurde: Sie soll eine Möglichkeit bieten, Dienste von USB-Geräten im Web verfügbar zu machen. Mit dieser API können Hardwarehersteller plattformübergreifende JavaScript-SDKs für ihre Geräte erstellen.

Am wichtigsten ist jedoch, dass USB dadurch sicherer und einfacher verwendet werden kann, da sie über das Web verfügbar ist.

Sehen wir uns das Verhalten der WebUSB API an:

  1. USB-Gerät kaufen
  2. Schließen Sie ihn an Ihren Computer an. Es erscheint sofort eine Benachrichtigung mit der richtigen Website für dieses Gerät.
  3. Klicken Sie auf die Benachrichtigung. Die Website ist da und kann verwendet werden!
  4. Wenn Sie darauf klicken, wird in Chrome eine USB-Geräteauswahl angezeigt, über die Sie Ihr Gerät auswählen können.

Tada!

Wie würde dieser Vorgang ohne die WebUSB API aussehen?

  1. Plattformspezifische Anwendung installieren
  2. Falls sie von meinem Betriebssystem unterstützt wird, überprüfe, ob ich das richtige Produkt heruntergeladen habe.
  3. Installiere das Gerät. Wenn Sie Glück haben, erhalten Sie keine beängstigenden Betriebssystem-Aufforderungen oder Pop-ups, in denen Sie vor der Installation von Treibern/Anwendungen aus dem Internet gewarnt werden. Wenn Sie Pech haben, funktionieren die installierten Treiber oder Anwendungen nicht richtig und beschädigen Ihren Computer. (Denken Sie daran: Das Web enthält fehlerhafte Websites.)
  4. Wenn Sie die Funktion nur einmal verwenden, bleibt der Code auf Ihrem Computer, bis Sie ihn entfernen. Im Web wird der Platz für ungenutzte Ressourcen irgendwann freigegeben.

Bevor ich fange

In diesem Artikel wird davon ausgegangen, dass Sie Grundkenntnisse zur Funktionsweise von USB haben. Falls nicht, empfehlen wir Ihnen, USB in einer praktischen Anleitung zu lesen. Hintergrundinformationen zu USB finden Sie in den offiziellen USB-Spezifikationen.

Die WebUSB API ist in Chrome 61 verfügbar.

Verfügbar für Ursprungstests

Um möglichst viel Feedback von Entwicklern zu erhalten, die die WebUSB API verwenden, haben wir diese Funktion zuvor in Chrome 54 und Chrome 57 als Ursprungstest hinzugefügt.

Der letzte Testzeitraum ist im September 2017 abgelaufen.

Datenschutz und Sicherheit

Nur HTTPS

Aufgrund der Leistungsfähigkeit dieser Funktion funktioniert sie nur in sicheren Kontexten. Das bedeutet, dass Sie bei der Entwicklung TLS berücksichtigen müssen.

Nutzergeste erforderlich

Aus Sicherheitsgründen kann navigator.usb.requestDevice() nur durch eine Nutzergeste wie eine Berührung oder einen Mausklick aufgerufen werden.

Berechtigungsrichtlinie

Eine Berechtigungsrichtlinie ist ein Mechanismus, mit dem Entwickler verschiedene Browserfunktionen und APIs selektiv aktivieren und deaktivieren können. Sie kann über einen HTTP-Header und/oder ein iFrame-Attribut „allow“ definiert werden.

Sie können eine Berechtigungsrichtlinie definieren, die steuert, ob das Attribut usb für das Navigator-Objekt verfügbar ist, oder mit anderen Worten, ob Sie WebUSB zulassen.

Im Folgenden finden Sie ein Beispiel für eine Header-Richtlinie, in der WebUSB nicht zulässig ist:

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

Im Folgenden finden Sie ein weiteres Beispiel für eine Containerrichtlinie, in der USB zulässig ist:

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

Beginnen wir mit dem Programmieren

Die WebUSB API stützt sich stark auf JavaScript-Promise-Objekte. Falls Sie damit nicht vertraut sind, sehen Sie sich die Anleitung für Promise-Objekte an. Außerdem sind () => {} einfach die Pfeilfunktionen von ECMAScript 2015.

Zugriff auf USB-Geräte erhalten

Du kannst den Nutzer entweder mit navigator.usb.requestDevice() auffordern, ein einzelnes verbundenes USB-Gerät auszuwählen, oder navigator.usb.getDevices() aufrufen, um eine Liste aller verbundenen USB-Geräte abzurufen, auf die die Website Zugriff hat.

Für die Funktion navigator.usb.requestDevice() wird ein obligatorisches JavaScript-Objekt verwendet, das filters definiert. Diese Filter werden verwendet, um USB-Geräte der angegebenen Anbieter-ID (vendorId) und optional Produkt-IDs (productId) abzugleichen. Die Schlüssel classCode, protocolCode, serialNumber und subclassCode können auch dort definiert werden.

Screenshot der Nutzeraufforderung für USB-Geräte in Chrome
Nutzeraufforderung für USB-Gerät.

Im Folgenden erfahren Sie, wie Sie Zugriff auf ein verbundenes Arduino-Gerät erhalten, das so konfiguriert ist, dass es den Ursprung zulässt.

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

Bevor Sie fragen, habe ich mir diese 0x2341-Hexadezimalzahl nicht automatisch einfallen lassen. Ich habe in der Liste der USB-IDs einfach nach dem Wort „Arduino“ gesucht.

Der USB-device, der im obigen Erfüllungsversprechen zurückgegeben wird, enthält einige grundlegende, aber wichtige Informationen über das Gerät, z. B. die unterstützte USB-Version, die maximale Paketgröße, den Anbieter, die Produkt-IDs sowie die Anzahl der möglichen Konfigurationen des Geräts. Grundsätzlich enthält sie alle Felder im USB-Deskriptor des Geräts.

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

Übrigens: Wenn ein USB-Gerät seine Unterstützung für WebUSB ankündigt und eine Landingpage-URL definiert, zeigt Chrome eine dauerhafte Benachrichtigung an, wenn das USB-Gerät angeschlossen wird. Wenn Sie auf diese Benachrichtigung klicken, wird die Landingpage geöffnet.

Screenshot der WebUSB-Benachrichtigung in Chrome
WebUSB-Benachrichtigung.

Mit einem Arduino-USB-Board sprechen

Sehen wir uns nun an, wie einfach es ist, von einem WebUSB-kompatiblen Arduino-Board über den USB-Port zu kommunizieren. Unter https://github.com/webusb/arduino finden Sie eine Anleitung dazu, wie Sie mit WebUSB Ihre Skizzen aktivieren können.

Keine Sorge, alle unten aufgeführten WebUSB-Gerätemethoden werden später in diesem Artikel erläutert.

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

Beachten Sie, dass die von mir verwendete WebUSB-Bibliothek nur ein Beispielprotokoll implementiert (basierend auf dem standardmäßigen seriellen USB-Protokoll) und dass Hersteller beliebige Sätze und Typen von Endpunkten erstellen können. Steuerübertragungen sind besonders schön für kleine Konfigurationsbefehle, da sie Buspriorität erhalten und eine gut definierte Struktur haben.

Hier ist die Skizze, die auf das Arduino-Board hochgeladen wurde.

// 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.
}

Die im obigen Beispielcode verwendete WebUSB Arduino-Bibliothek eines Drittanbieters erfüllt im Grunde zwei Dinge:

  • Das Gerät fungiert als WebUSB-Gerät, mit dem Chrome die Landingpage-URL lesen kann.
  • Es stellt eine WebUSB Serial API bereit, mit der Sie die Standard-API überschreiben können.

Sehen Sie sich den JavaScript-Code noch einmal an. Sobald der Nutzer device ausgewählt hat, führt device.open() alle plattformspezifischen Schritte aus, um eine Sitzung mit dem USB-Gerät zu starten. Anschließend muss ich eine verfügbare USB-Konfiguration mit device.selectConfiguration() auswählen. Denken Sie daran, dass eine Konfiguration angibt, wie das Gerät betrieben wird, welchen maximalen Stromverbrauch und wie viele Schnittstellen es hat. Apropos Schnittstellen, ich muss auch exklusiven Zugriff mit device.claimInterface() anfordern, da Daten nur an eine Schnittstelle oder zugehörige Endpunkte übertragen werden können, wenn die Schnittstelle beansprucht wird. Schließlich ist der Aufruf von device.controlTransferOut() erforderlich, um das Arduino-Gerät mit den entsprechenden Befehlen für die Kommunikation über die WebUSB Serial API einzurichten.

Von dort führt device.transferIn() eine Bulk-Übertragung auf dem Gerät durch, um es darüber zu informieren, dass der Host zum Empfang von Bulk-Daten bereit ist. Dann wird das Versprechen mit einem result-Objekt erfüllt, das eine DataView-data enthält, die entsprechend geparst werden muss.

Wenn Sie bereits mit USB vertraut sind, dürfte Ihnen alles bekannt vorkommen.

Ich möchte mehr

Über die WebUSB API können Sie mit allen USB-Übertragungs-/Endpunkttypen interagieren:

  • CONTROL-Übertragungen, die zum Senden oder Empfangen von Konfigurations- oder Befehlsparametern an ein USB-Gerät verwendet werden, werden mit controlTransferIn(setup, length) und controlTransferOut(setup, data) verarbeitet.
  • INTERRUPT-Übertragungen, die für eine kleine Menge zeitkritischer Daten verwendet werden, werden mit den gleichen Methoden wie BULK-Übertragungen mit transferIn(endpointNumber, length) und transferOut(endpointNumber, data) verarbeitet.
  • ISOCHRONOUS-Übertragungen, die für Datenstreams wie Video und Ton verwendet werden, werden mit isochronousTransferIn(endpointNumber, packetLengths) und isochronousTransferOut(endpointNumber, data, packetLengths) verarbeitet.
  • BULK-Übertragungen werden zur zuverlässigen Übertragung einer großen Menge nicht zeitkritischer Daten verwendet und werden mit transferIn(endpointNumber, length) und transferOut(endpointNumber, data) verarbeitet.

Sie können sich auch das WebLight-Projekt von Mike Tsao ansehen, das ein konkretes Beispiel für den Bau eines USB-gesteuerten LED-Geräts für die WebUSB API bietet (hier kein Arduino verwendet). Sie finden Hardware, Software und Firmware.

Zugriff auf USB-Gerät widerrufen

Die Website kann Berechtigungen für den Zugriff auf ein USB-Gerät bereinigen, das sie nicht mehr benötigt, indem sie forget() auf der Instanz USBDevice aufruft. Bei einer Webanwendung für den Unterricht, die auf einem gemeinsam genutzten Computer mit vielen Geräten verwendet wird, beeinträchtigt eine große Anzahl von nutzergenerierten Berechtigungen die Nutzerfreundlichkeit.

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

Wenn forget() in Chrome 101 oder höher verfügbar ist, prüfen Sie anhand der folgenden Elemente, ob diese Funktion unterstützt wird:

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

Limits bei der Übertragungsgröße

Einige Betriebssysteme beschränken die Menge an Daten, die Teil von ausstehenden USB-Transaktionen sein können. Wenn Sie Ihre Daten in kleinere Transaktionen aufteilen und nur wenige auf einmal senden, können Sie diese Einschränkungen vermeiden. Außerdem reduziert es den verwendeten Arbeitsspeicher und Ihre Anwendung kann den Fortschritt während der Übertragung melden.

Da mehrere Übertragungen, die an einen Endpunkt gesendet werden, immer der Reihe nach ausgeführt werden, kann der Durchsatz durch Senden mehrerer Blöcke in der Warteschlange verbessert werden, um Latenzen zwischen USB-Übertragungen zu vermeiden. Jedes Mal, wenn ein Chunk vollständig übertragen wurde, wird der Code darüber informiert, dass er weitere Daten zur Verfügung stellen soll, wie im Beispiel der Hilfsfunktion unten beschrieben.

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

Tipps

Die Fehlerbehebung von USB-Geräten in Chrome ist auf der internen Seite about://device-log einfacher, auf der du alle Ereignisse zu USB-Geräten an einem einzigen Ort sehen kannst.

Screenshot der Geräteprotokollseite für das Debuggen von WebUSB in Chrome
Geräteprotokollseite in Chrome für das Debugging der WebUSB API

Die interne Seite about://usb-internals ist ebenfalls praktisch und ermöglicht es Ihnen, das Verbinden und Trennen virtueller WebUSB-Geräte zu simulieren. Dies ist nützlich, um UI-Tests ohne echte Hardware durchzuführen.

Screenshot der internen Seite zum Debuggen von WebUSB in Chrome
Interne Seite in Chrome zum Debuggen der WebUSB API.

Auf den meisten Linux-Systemen werden USB-Geräte standardmäßig mit schreibgeschützten Berechtigungen zugeordnet. Damit Chrome ein USB-Gerät öffnen kann, müssen Sie eine neue udev-Regel hinzufügen. Erstellen Sie unter /etc/udev/rules.d/50-yourdevicename.rules eine Datei mit folgendem Inhalt:

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

Dabei ist [yourdevicevendor] 2341, wenn Ihr Gerät beispielsweise ein Arduino ist. ATTR{idProduct} kann auch für eine spezifischere Regel hinzugefügt werden. Deine user muss Mitglied der Gruppe plugdev sein. Verbinde dein Gerät anschließend wieder.

Ressourcen

Sende einen Tweet mit dem Hashtag #WebUSB an @ChromiumDev und teile uns mit, wo und wie du es verwendest.

Danksagungen

Vielen Dank an Joe Medley für die Rezension dieses Artikels.