Auf USB-Geräte im Web zugreifen

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

François Beaufort
François Beaufort

Wenn ich einfach nur „USB“ sage, denken Sie wahrscheinlich sofort an Tastaturen, Mäuse, Audio-, Video- und Speichergeräte. Das stimmt, aber es gibt auch andere Arten von Universal Serial Bus (USB)-Geräten.

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

Vor allem aber wird USB durch die Einbindung ins Web sicherer und einfacher in der Anwendung.

Sehen wir uns an, welches Verhalten Sie mit der WebUSB API erwarten können:

  1. Kaufen Sie ein USB-Gerät.
  2. Schließen Sie sie an Ihren Computer an. Daraufhin wird sofort eine Benachrichtigung mit der richtigen Website für dieses Gerät angezeigt.
  3. Klicken Sie auf die Benachrichtigung. Die Website ist da und kann verwendet werden.
  4. Klicke hier, um eine Verbindung herzustellen. Daraufhin wird in Chrome eine USB-Geräteauswahl angezeigt, in der du dein Gerät auswählen kannst.

Tada!

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

  1. Plattformspezifische Anwendung installieren
  2. Wenn es überhaupt von meinem Betriebssystem unterstützt wird, möchte ich prüfen, ob ich das richtige heruntergeladen habe.
  3. Installiere das Produkt. Wenn Sie Glück haben, werden Sie nicht von bedrohlichen Betriebssystemaufforderungen oder Pop-ups gewarnt, die Sie vor der Installation von Treibern/Anwendungen aus dem Internet warnen. Wenn Sie Pech haben, funktionieren die installierten Treiber oder Anwendungen nicht richtig und beschädigen Ihren Computer. Denken Sie daran, dass das Web so konzipiert ist, dass es nicht funktionierende Websites enthält.
  4. Wenn Sie die Funktion nur einmal verwenden, bleibt der Code auf Ihrem Computer, bis Sie ihn entfernen. Im Web wird der nicht verwendete Speicherplatz irgendwann wieder freigegeben.

Bevor ich anfange

In diesem Artikel wird davon ausgegangen, dass Sie Grundkenntnisse in der Funktionsweise von USB haben. Falls nicht, empfehle ich Ihnen, USB in einer NutShell-Dokumentation zu lesen. Weitere Informationen 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 in der Praxis verwenden, haben wir diese Funktion bereits in Chrome 54 und Chrome 57 als Ursprungstest hinzugefügt.

Der letzte Test wurde im September 2017 erfolgreich abgeschlossen.

Datenschutz und Sicherheit

Nur HTTPS

Aufgrund der Leistungsfähigkeit dieser Funktion ist sie nur in sicheren Kontexten verfügbar. Das bedeutet, dass Sie Ihre App mit TLS entwickeln müssen.

Nutzergeste erforderlich

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

Richtlinie für Berechtigungen

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

Sie können eine Berechtigungsrichtlinie definieren, mit der festgelegt wird, ob das usb-Attribut für das Navigator-Objekt freigegeben wird, also ob WebUSB zulässig ist.

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

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

Unten sehen Sie ein weiteres Beispiel für eine Containerrichtlinie, in der USB zulässig ist:

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

Los gehts mit dem Programmieren

Die WebUSB API basiert stark auf JavaScript-Promises. Wenn Sie mit Versprechen nicht vertraut sind, sehen Sie sich dieses Tutorial zu Versprechen an. Noch etwas: () => {} sind einfach ECMAScript 2015-Funktionen.

Zugriff auf USB-Geräte erhalten

Du kannst den Nutzer entweder über 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.

Die Funktion navigator.usb.requestDevice() benötigt ein obligatorisches JavaScript-Objekt, das filters definiert. Mit diesen Filtern werden alle USB-Geräte der angegebenen Anbieter-ID (vendorId) und optional der Produkt-ID (productId) zugeordnet. Die Schlüssel classCode, protocolCode, serialNumber und subclassCode können dort ebenfalls definiert werden.

Screenshot der Aufforderung an den Nutzer eines USB-Geräts in Chrome
Nutzeraufforderung für USB-Gerät.

So erhalten Sie beispielsweise Zugriff auf ein verbundenes Arduino-Gerät, das so konfiguriert ist, dass der Ursprung zugelassen wird.

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, bin ich nicht wie von Zauberhand auf diese 0x2341-Hexadezimalzahl gekommen. Ich habe einfach in dieser Liste der USB-IDs nach dem Wort „Arduino“ gesucht.

Die im obigen erfüllten Versprechen zurückgegebene USB-device enthält einige grundlegende, aber wichtige Informationen zum Gerät, z. B. die unterstützte USB-Version, die maximale Paketgröße, die Anbieter- und Produkt-IDs sowie die Anzahl der möglichen Konfigurationen des Geräts. Grundsätzlich enthält es 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"
  });
})

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 ist. Durch Klicken auf diese Benachrichtigung 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 die Kommunikation von einem WebUSB-kompatiblen Arduino-Board über den USB-Port ist. Eine Anleitung zum Aktivieren von WebUSB für Ihre Sketches finden Sie unter https://github.com/webusb/arduino.

Keine Sorge, ich werde später in diesem Artikel auf alle WebUSB-Gerätemethoden eingehen.

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 in der von mir verwendeten WebUSB-Bibliothek nur ein Beispielprotokoll implementiert ist (basierend auf dem standardmäßigen USB-Serienprotokoll) und dass Hersteller beliebige Endpunkte erstellen können. Steuerübertragungen eignen sich besonders für kleine Konfigurationsbefehle, da sie Buspriorität haben und eine gut definierte Struktur haben.

Und hier ist der Sketch, der 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 Beispielcode oben verwendete Drittanbieterbibliothek WebUSB Arduino dient im Wesentlichen zwei Zwecken:

  • Das Gerät fungiert als WebUSB-Gerät, sodass Chrome die URL der Landingpage 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 ich die vom Nutzer ausgewählte device erhalte, führt device.open() alle plattformspezifischen Schritte aus, um eine Sitzung mit dem USB-Gerät zu starten. Anschließend muss ich mit device.selectConfiguration() eine verfügbare USB-Konfiguration auswählen. Eine Konfiguration gibt an, wie das Gerät mit Strom versorgt wird, wie hoch die maximale Stromaufnahme ist und wie viele Schnittstellen es hat. Apropos Schnittstellen: Ich muss auch exklusiven Zugriff bei device.claimInterface() anfordern, da Daten nur dann an eine Schnittstelle oder zugehörigen Endpunkte übertragen werden können, wenn die Schnittstelle beansprucht wird. Schließlich muss device.controlTransferOut() aufgerufen werden, um das Arduino-Gerät mit den entsprechenden Befehlen für die Kommunikation über die WebUSB Serial API einzurichten.

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

Wenn Sie mit USB vertraut sind, sollte Ihnen das alles ziemlich bekannt vorkommen.

Ich möchte mehr

Mit der 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 denselben Methoden wie BULK-Übertragungen mit transferIn(endpointNumber, length) und transferOut(endpointNumber, data) verarbeitet.
  • ISOCHRONE Übertragungen, die für Datenstreams wie Video und Ton verwendet werden, werden mit isochronousTransferIn(endpointNumber, packetLengths) und isochronousTransferOut(endpointNumber, data, packetLengths) verarbeitet.
  • BULK-Übertragungen, mit denen eine große Menge nicht zeitkritischer Daten auf zuverlässige Weise übertragen werden kann, werden mit transferIn(endpointNumber, length) und transferOut(endpointNumber, data) verarbeitet.

Sehen Sie sich auch das WebLight-Projekt von Mike Tsao an. Es enthält ein grundlegendes Beispiel für die Herstellung eines USB-gesteuerten LED-Geräts, das für die WebUSB API entwickelt wurde (hier kein Arduino verwendet). Dort finden Sie Hardware, Software und Firmware.

Zugriff auf ein USB-Gerät widerrufen

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

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

Da forget() in Chrome 101 oder höher verfügbar ist, prüfen Sie, ob diese Funktion mit den folgenden Elementen unterstützt wird:

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

Beschränkungen der Übertragungsgröße

Einige Betriebssysteme beschränken die Menge der Daten, die Teil ausstehender USB-Transaktionen sein können. Wenn Sie Ihre Daten in kleinere Transaktionen aufteilen und nur wenige gleichzeitig einreichen, können Sie diese Einschränkungen vermeiden. Außerdem wird der Arbeitsspeicherverbrauch reduziert und Ihre Anwendung kann den Fortschritt der Übertragungen melden.

Da mehrere an einen Endpunkt gesendete Übertragungen immer in der richtigen Reihenfolge ausgeführt werden, lässt sich der Durchsatz durch das Einreichen mehrerer an der Warteschlange stehender Chunks verbessern, um Latenzen zwischen USB-Übertragungen zu vermeiden. Jedes Mal, wenn ein Chunk vollständig übertragen wird, wird Ihr Code darüber benachrichtigt, dass er mehr Daten liefern sollte, wie im folgenden Beispiel für eine Hilfsfunktion dokumentiert.

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

Das USB-Debugging in Chrome ist mit der internen Seite about://device-log einfacher. Dort finden Sie alle USB-Geräte-Ereignisse an einem Ort.

Screenshot der Seite mit Geräteprotokollen zur Fehlerbehebung bei WebUSB in Chrome
Geräteprotokollseite in Chrome zur Fehlerbehebung bei der WebUSB API.

Die interne Seite about://usb-internals ist ebenfalls praktisch und ermöglicht es Ihnen, die Verbindung und Trennung 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 Lesezugriffsberechtigungen 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 es sich beispielsweise um ein Arduino-Gerät handelt. Für eine genauere Regel kann auch ATTR{idProduct} hinzugefügt werden. Prüfen Sie, ob Ihr user ein Mitglied der Gruppe plugdev ist. Verbinden Sie das Gerät dann einfach wieder.

Ressourcen

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

Danksagungen

Vielen Dank an Joe Medley für die Überprüfung dieses Artikels.