Back

Coole Dinge, die Sie mit der Web Serial API machen können

Coole Dinge, die Sie mit der Web Serial API machen können

Die Web Serial API ermöglicht es einer Webseite, einen lese-/schreibfähigen Byte-Stream zu einem physischen Gerät zu öffnen, das über eine serielle Schnittstelle kommuniziert – einem USB-zu-Seriell-Adapter, einem Mikrocontroller-Entwicklungsboard, einem 3D-Drucker oder einem gekoppelten Bluetooth-Classic-Gerät – ganz ohne native App oder Browser-Erweiterung. Sie reiht sich in dieselbe Familie browser-basierter Hardware-Zugriffs-APIs ein wie MediaDevices (Kameras und Mikrofone), WebUSB (direkte USB-Schnittstellen) und WebHID (Gamepads und Tastaturen): Die Seite fordert Zugriff an, der Browser zeigt einen Berechtigungsdialog an, und der Benutzer wählt das Gerät aus.

Bevor Sie ein Wochenendprojekt planen, sollten Sie die Unterstützungssituation kennen, die den Rahmen vorgibt. Web Serial wird auf dem Desktop in Chrome 89+, Edge 89+, Opera 75+ und Firefox 151+ unterstützt, wie die MDN-Browser-Kompatibilitätstabelle zeigt; Safari hat keine angekündigte Unterstützung. Die API erfordert einen sicheren Kontext (HTTPS oder localhost) sowie eine Benutzeraktion, um requestPort() aufzurufen – ein Port-Dialog kann nicht beim Laden der Seite ausgelöst werden. Dieser Artikel stellt sechs konkrete Projektkategorien vor, die Sie darauf aufbauen können, jeweils mit dem Web-Serial-spezifischen Code, der Hardware-Kopplung und dem häufigsten Fallstrick.

Wichtige Erkenntnisse

  • Web Serial wird auf dem Desktop in Chrome 89+, Edge 89+, Opera 75+ und Firefox 151+ unterstützt; Safari hat keine angekündigte Unterstützung, und die API erfordert sowohl einen sicheren Kontext als auch eine Benutzeraktion für den Aufruf von requestPort().
  • Web Serial existiert getrennt von WebUSB, weil Betriebssystem-Seriell-Treiber USB-zu-Seriell-Adapter beanspruchen, bevor WebUSB sie erreichen kann – Geräte, die als COM-Ports oder /dev/tty-Knoten aufgelistet werden, benötigen daher Web Serial.
  • Der Aufruf von reader.releaseLock() vor port.close() vermeidet „Port bereits geöffnet”-Fehler, die durch verlorene Stream-Sperren bei der Wiederverbindung entstehen.
  • Seit Chrome 117 kann Web Serial mit gekoppelten Bluetooth-Classic-RFCOMM/SPP-Geräten über allowedBluetoothServiceClassIds und bluetoothServiceClassId-Filter kommunizieren – drahtlose serielle Kommunikation ohne separaten WebBluetooth-Aufruf.
  • Chrome 130 hat SerialPort.connected hinzugefügt, einen booleschen Wert, der unterscheidet, ob ein Port physisch vorhanden ist oder ob die App ihn geschlossen hat.

Das minimale Verbinden-Lesen-Schreiben-Aufräumen-Muster

Jedes Web-Serial-Projekt beginnt mit denselben vier Schritten: einen Port anfordern, ihn mit einer Baudrate öffnen, über den Stream lesen oder schreiben und aufräumen, indem die Sperre vor dem Schließen freigegeben wird. Der folgende Code-Ausschnitt ist die Referenz, auf die der Rest dieses Artikels zurückverweist. Er verwendet das optionale filters-Argument, um die Port-Auswahl des Browsers anhand der USB-Hersteller- und Produkt-ID einzugrenzen – gemäß der SerialPortRequestOptions-Definition in der WICG-Web-Serial-Spezifikation werden damit nicht relevante serielle Ports ausgeschlossen, sodass der Benutzer nur passende Geräte sieht.

// Must run inside a user-gesture handler (e.g. a click listener).
const port = await navigator.serial.requestPort({
  filters: [{ usbVendorId: 0x2341 }], // optional; omit for "any port"
});
await port.open({ baudRate: 115200 });

const reader = port.readable.getReader();
try {
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;          // reader was cancelled
    console.log(value);       // value is a Uint8Array chunk
  }
} finally {
  reader.releaseLock();       // release BEFORE closing
  await port.close();
}

Das Versäumnis, reader.releaseLock() vor port.close() aufzurufen, ist eine häufige Ursache für „Port bereits geöffnet”-Fehler bei der Wiederverbindung – die Sperre hält den lesbaren Stream fest, und close() schlägt fehl, solange ein Stream noch gesperrt ist, gemäß den SerialPort.close()-Schritten in der Spezifikation. Für textbasierte Protokolle sollten die Streams über TextEncoderStream und TextDecoderStream geleitet werden, anstatt rohe Uint8Array-Chunks manuell zu dekodieren; der Web-Serial-Leitfaden von MDN behandelt die Kodierungsdetails.

Web-Serial-API-Beispiele: sechs Projekte für ein Wochenende

Die sechs stärksten Projektkategorien für Web Serial sind leseintensive Debugging-Tools, schreibintensive Protokoll-Clients, langlebige Streaming-Schleifen, Live-Visualisierung, browser-gesteuerte Displays und Gerätekonfigurations-UIs. Jeder der folgenden Abschnitte isoliert die Web-Serial-spezifische Logik, anstatt eine vollständige Anwendung durchzugehen.

1. Ein browserbasierter serieller Monitor

Ein serieller Monitor ist das leseintensive Debugging-Tool, das den Serial Monitor der Arduino IDE oder screen /dev/ttyUSB0 durch einen Browser-Tab ersetzt. Er öffnet einen Port, streamt eingehende Bytes durch einen Decoder und hängt dekodierte Zeilen an das DOM an. Es ist das einfachste Projekt hier und der beste Einstieg, da es die Leseschleife ohne die Komplexität eines Schreibprotokolls übt.

Hardware/Protokoll-Kopplung: jedes UART-Gerät – ein Arduino, ein ESP32, ein USB-zu-Seriell-Adapter – das zeilenumbruch-getrennten Text ausgibt.

const port = await navigator.serial.requestPort();
await port.open({ baudRate: 9600 });

const decoder = new TextDecoderStream();
port.readable.pipeTo(decoder.writable);
const reader = decoder.readable.getReader();

let buffer = "";
while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += value;
  const lines = buffer.split("\n");
  buffer = lines.pop() ?? "";        // keep the partial last line
  lines.forEach(line => appendToLog(line));
}

Fallstrick: Serielle Daten kommen in beliebigen Chunk-Grenzen an, nicht als vollständige Zeilen – ein einzelnes read() kann eine halbe Zeile oder drei Zeilen zurückgeben. Daher sollte gepuffert werden, bis ein Trennzeichen gefunden wird, bevor die Ausgabe gerendert wird.

2. Ein Firmware-Flasher

Ein Firmware-Flasher schreibt ein kompiliertes Binärprogramm über die serielle Schnittstelle in den Flash-Speicher eines Mikrocontrollers, ähnlich wie ein esptool-im-Browser-Tool für ESP32-Chips. Er ist schreibintensiv und protokollgebunden: Bevor Nutzdaten übertragen werden, muss der Chip in seinen Bootloader versetzt werden, und die Daten werden gemäß dem Flashing-Protokoll des Chips gerahmt.

Hardware/Protokoll-Kopplung: ESP32/ESP8266 über das SLIP-gerahmte serielle Bootloader-Protokoll; die Eingangssequenz wird durch die DTR- und RTS-Steuersignale gesteuert.

const port = await navigator.serial.requestPort();
await port.open({ baudRate: 115200 });

// Toggle control signals to force the ESP into download mode.
// See Espressif's documented boot-mode sequence.
await port.setSignals({ dataTerminalReady: false, requestToSend: true });
await port.setSignals({ dataTerminalReady: true, requestToSend: false });
// ...now send the framed bootloader commands over port.writable

ESPTool-artige Browser-Flasher müssen die DTR- und RTS-Steuersignale in einer bestimmten Reihenfolge umschalten, um den Chip vor dem Senden von Schreibbefehlen in den Download-Modus zu versetzen; Espressif dokumentiert das Boot-Modus-Signalverhalten, und Web Serial stellt die Umschaltmöglichkeiten über SerialPort.setSignals() bereit.

Fallstrick: Wird die DTR/RTS-Bootloader-Sequenz übersprungen, verbleibt der Chip in seiner Anwendungs-Firmware, anstatt den seriellen Bootloader zu starten, sodass der Flash-Vorgang nicht korrekt durchgeführt werden kann.

3. Ein G-Code-Job-Streamer

Ein G-Code-Streamer sendet einen Druck- oder Schneidauftrag zeilenweise an einen 3D-Drucker oder eine CNC-Maschine und wartet dabei auf die Bestätigung jeder Zeile durch die Firmware, bevor die nächste gesendet wird. Es handelt sich um eine langlebige Schreibschleife mit Flusskontrolle, was dieses Projekt deutlich anspruchsvoller macht als einen einfachen Fire-and-Forget-Writer.

Hardware/Protokoll-Kopplung: Marlin- oder GRBL-Firmware über USB-Seriell, die zeilenbasiertes G-Code mit ok-Bestätigungen austauscht.

const encoder = new TextEncoderStream();
encoder.readable.pipeTo(port.writable);
const writer = encoder.writable.getWriter();

const decoder = new TextDecoderStream();
port.readable.pipeTo(decoder.writable);
const reader = decoder.readable.getReader();

for (const line of gcodeLines) {
  await writer.write(line + "\n");
  // Block until the firmware reports it consumed the line.
  let ack = "";
  while (!ack.includes("ok")) {
    const { value } = await reader.read();
    ack += value;
  }
}

G-Code-Streamer müssen nach jeder Zeile auf die ok-Bestätigung des Druckers warten, bevor die nächste gesendet wird; die RepRap-G-Code-Referenz definiert diesen Handshake, und das Überschwemmen des seriellen Puffers ohne diesen Mechanismus überlastet die Befehlswarteschlange der Firmware mitten im Auftrag.

Fallstrick: Das ok kann zusammen mit Temperaturberichten und anderen unaufgeforderten Zeilen eintreffen. Daher sollte auf das Token geprüft werden, anstatt davon auszugehen, dass die nächste gelesene Zeile die Bestätigung ist.

4. Ein Live-Telemetrie-Dashboard

Ein Telemetrie-Dashboard liest einen kontinuierlichen Sensor-Stream von einem seriellen Port und stellt ihn als Live-Diagramm im Browser dar – Temperatur, Spannung, Beschleunigungsmesser-Achsen, GPS-Fixes. Der Web-Serial-Anteil ist lediglich eine Leseschleife; der Mehrwert liegt darin, dekodierte Werte direkt in eine Diagrammbibliothek zu leiten, die im selben Tab läuft.

Hardware/Protokoll-Kopplung: jeder mit Sensoren ausgestattete Mikrocontroller, der CSV-Zeilen ausgibt, oder ein GPS-Modul, das NMEA-0183-Sätze sendet.

const decoder = new TextDecoderStream();
port.readable.pipeTo(decoder.writable);
const reader = decoder.readable.getReader();

let buffer = "";
while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += value;
  let nl;
  while ((nl = buffer.indexOf("\n")) >= 0) {
    const [t, v] = buffer.slice(0, nl).split(",");
    pushSample(Number(t), Number(v));   // feed your chart
    buffer = buffer.slice(nl + 1);
  }
}

Fallstrick: Ein schneller Sensor kann Zeilen schneller ausgeben, als das Diagramm neu gezeichnet werden kann. Samples sollten gebündelt und die Aktualisierung über requestAnimationFrame ausgelöst werden, anstatt bei jedem read() neu zu rendern, da sonst der Haupt-Thread blockiert wird.

5. Ein Display- oder Visualisierungs-Controller

Ein Display-Controller rendert Pixel oder Frames im Browser und überträgt sie an ein physisches Display – eine LED-Matrix, ein OLED oder ein ePaper-Panel. Hier fungiert der Browser als Rendering-Engine: Eine Bitmap oder ein Helligkeits-Array wird in JavaScript berechnet und die Bytes, die der Display-Controller erwartet, werden geschrieben.

Hardware/Protokoll-Kopplung: eine MAX7219-gesteuerte LED-Matrix, ein SSD1306-OLED oder ein ePaper-Modul hinter einem Mikrocontroller, der Framebuffer-Bytes über UART akzeptiert.

const writer = port.writable.getWriter();

// 8x8 frame as one byte per row (bit set = LED on).
function renderFrame(rows: number[]) {
  return writer.write(Uint8Array.from(rows));
}

await renderFrame([
  0b00111100, 0b01000010, 0b10100101, 0b10000001,
  0b10100101, 0b10011001, 0b01000010, 0b00111100,
]);

Fallstrick: Schreibvorgänge werden nicht automatisch an die Bildwiederholrate des Displays angepasst, sodass Animationen aus dem Browser zu Tearing oder Bildverlust führen können. Es sollte ein vollständiger Frame pro Aktualisierung gesendet werden, und der Mikrocontroller sollte das eigene Timing des Displays verwalten, anstatt partielle Zeilen zu streamen.

6. Eine Gerätekonfigurations-UI

Eine Gerätekonfigurations-UI liest und schreibt Einstellungen auf einem seriell verbundenen Gerät – Flugcontroller-Parameter einer Drohne, Kanalgedächtnisse eines Amateurfunkgeräts oder Registerwerte eines IoT-Moduls. Hier bewährt sich das VID/PID-Filtermuster besonders: Konfigurations-Apps richten sich an ein bekanntes Gerät, sodass das Filtern der Port-Auswahl nach Hersteller- und Produkt-ID sicherstellt, dass der Benutzer nur sein Gerät sieht und nicht jeden COM-Port des Systems.

Hardware/Protokoll-Kopplung: ein Flugcontroller, der MSP spricht, das CAT/CI-V-Protokoll eines Funkgeräts oder ein beliebiges Modul mit einem dokumentierten Register-Zugriffs-Befehlssatz – ausgewählt mit requestPort-Filtern.

// Narrow the picker to one known vendor/product.
const port = await navigator.serial.requestPort({
  filters: [{ usbVendorId: 0x10c4, usbProductId: 0xea60 }], // example CP210x
});
await port.open({ baudRate: 115200 });

const writer = port.writable.getWriter();
await writer.write(buildReadConfigCommand());   // request current settings
// ...read the response, populate form fields, write back on save

Das Übergeben von filters: [{ usbVendorId, usbProductId }] an requestPort() schränkt den Dialog auf passende Ports ein, gemäß der SerialPortRequestOptions-Spezifikation. Um den Dialog bei einem erneuten Besuch vollständig zu überspringen, gibt navigator.serial.getPorts() die Ports zurück, die der Benutzer bereits genehmigt hat.

Fallstrick: Die Hersteller- und Produkt-IDs in der Auswahl stammen vom USB-zu-Seriell-Bridge-Chip (FTDI, CP210x, CH340), nicht vom Endgerät. Daher können mehrere nicht verwandte Produkte dieselbe usbVendorId teilen – wenn möglich, sollte sowohl nach usbVendorId als auch nach usbProductId gefiltert werden.

Wann Web Serial das richtige Werkzeug ist – und wann nicht

Verwenden Sie Web Serial, wenn das Gerät als COM-Port oder /dev/tty-Knoten aufgelistet wird – das heißt, wenn das Betriebssystem bereits einen seriellen Treiber dafür geladen hat. Web Serial existiert getrennt von WebUSB, weil Betriebssystem-Seriell-Treiber USB-zu-Seriell-Adapter beanspruchen, bevor WebUSB sie erreichen kann – eine Lücke, die der WICG-Web-Serial-Explainer direkt beschreibt. Für andere Geräteklassen sollte eine andere API verwendet werden.

APIGerätetypBerechtigungsmodellBrowser-UnterstützungAm besten geeignet für
Web SerialGeräte an einem seriellen/COM/tty-PortBenutzeraktions-AuswahlChrome/Edge 89+, Opera 75+, Firefox 151+Mikrocontroller, Drucker, USB-Seriell-Adapter
WebUSBDirekte USB-Schnittstellen (kein OS-Treiber)Benutzeraktions-AuswahlChromium-basiertBenutzerdefinierte USB-Geräte ohne Seriell-Treiber
WebHIDHuman-Interface-GeräteBenutzeraktions-AuswahlChromium-basiertGamepads, Tastaturen, benutzerdefinierte HID-Peripheriegeräte
Web BluetoothBluetooth Low Energy (GATT)Benutzeraktions-AuswahlChromium-basiertBLE-Sensoren, Beacons, Wearables
WebSocket + Backend-DaemonBeliebigServer-vermitteltAlle BrowserBrowserübergreifende Reichweite, serverseitiges Parsing

Wenn Sie heute Firefox- und Safari-Nutzer erreichen müssen und die Gerätelogik serverseitig ausgeführt werden kann, ist ein kleiner nativer Daemon, der einen WebSocket bereitstellt, der portable Fallback – auf Kosten eines Installationsschritts, den Web Serial vermeidet.

Neuere Ergänzungen, die es wert sind, bekannt zu sein

Chrome 117 hat Bluetooth-Classic-RFCOMM/SPP-Unterstützung zu Web Serial hinzugefügt, und Chrome 130 hat den booleschen Wert SerialPort.connected eingeführt, der physisch vorhandene Ports von app-geschlossenen unterscheidet. Beide Funktionen fehlen in den meisten bestehenden Tutorials, weshalb sie sich lohnen, in neue Projekte einzubeziehen.

Seit Chrome 117 auf dem Desktop kann Web Serial mit gekoppelten Bluetooth-Classic-RFCOMM/SPP-Geräten kommunizieren: Übergeben Sie allowedBluetoothServiceClassIds (oder einen filters: [{ bluetoothServiceClassId }]-Eintrag), um benutzerdefinierte RFCOMM-Dienste in der requestPort()-Auswahl anzuzeigen, wobei die Dienstklasse über port.getInfo().bluetoothServiceClassId auslesbar ist – ohne separaten WebBluetooth-Aufruf. Dieser Bluetooth-Pfad ist nur für Chromium verfügbar (Chrome/Edge 117+, Opera 103+) und wird auf MDN noch als experimentell markiert; die allgemeine Web-Serial-Unterstützung von Firefox 151 umfasst die Bluetooth-Dienstklassen-Optionen noch nicht. Die Details finden Sie im Chrome-Beitrag Serial over Bluetooth on the web. Damit wird drahtlose serielle Kommunikation zum selben Lese-/Schreib-Stream, den Sie bereits für USB geschrieben haben.

Chrome 130 hat SerialPort.connected hinzugefügt, einen booleschen Wert, der true ist, wenn der Port physisch vorhanden, aber nicht notwendigerweise geöffnet ist. Er ermöglicht es der Wiederverbindungs-UX, zwischen „Gerät getrennt” und „Port von der App geschlossen” zu unterscheiden – kombinieren Sie ihn mit den connect- und disconnect-Events, um einen Live-Verbindungsindikator ohne Polling zu realisieren.

requestPort() schlägt stillschweigend fehl, wenn der Benutzer den Web-Serial-Berechtigungsdialog abbricht, und die meisten Implementierungen zeigen diese Ablehnung nicht als sichtbaren Zustand an, sodass der Verbindungsbutton einfach in seinen Standardzustand zurückkehrt und die Seite scheinbar nichts tut. Dieses einzelne UX-Versagen tritt in allen sechs oben genannten Projektkategorien auf. Session-Replays von Web-Serial-Verbindungsabläufen zeigen oft ein charakteristisches Muster, bei dem Benutzer den Verbindungsbutton zwei- oder dreimal hintereinander klicken, bevor sie aufgeben – ein Zeichen dafür, dass die Ablehnung nicht kommuniziert wird. Fangen Sie die requestPort()-Ablehnung ab und zeigen Sie eine explizite Meldung wie „Kein Gerät ausgewählt” oder „Port wird verwendet” an; Ports, die von einem anderen Prozess belegt werden (der Arduino IDE, screen oder ModemManager), schlagen auf dieselbe stille Weise fehl.

Wählen Sie eine Kategorie, verbinden Sie den minimalen Verbinden-Lesen-Schreiben-Aufräumen-Block vom Anfang dieses Artikels, und überprüfen Sie die Unterstützungsmatrix im Hinblick auf Ihre Zielgruppe, bevor Sie ein Wochenende dafür einplanen. Der schnellste Weg zu einem funktionierenden Build ist ein serieller Monitor gegen ein Board, das Sie bereits besitzen – sobald die Leseschleife und das Aufräumen solide sind, sind die anderen fünf Kategorien Variationen desselben Byte-Streams. Web Serial ist jedoch das falsche Werkzeug, wenn Ihre Zielgruppe Safari-Nutzer einschließt, wenn das Gerät nicht als COM- oder /dev/tty-Knoten aufgelistet wird (verwenden Sie stattdessen WebUSB, WebHID oder Web Bluetooth) oder wenn die Parsing-Logik wirklich serverseitig gehört – für all diese Fälle bietet ein kleiner nativer Daemon hinter einem WebSocket eine breitere Reichweite auf Kosten eines Installationsschritts.

Häufig gestellte Fragen

Das Schließen des Berechtigungsdialogs führt dazu, dass das requestPort()-Promise abgelehnt wird anstatt aufgelöst zu werden, und die meisten Implementierungen zeigen diese Ablehnung nie als sichtbaren Zustand an, sodass der Button stillschweigend in seinen Standardzustand zurückkehrt. Umschließen Sie den requestPort()-Aufruf mit einem try-catch und zeigen Sie bei einer Ablehnung eine explizite Meldung wie 'Kein Gerät ausgewählt' an. Ports, die bereits von einem anderen Prozess wie der Arduino IDE, screen oder ModemManager belegt werden, schlagen auf dieselbe stille Weise fehl.

Verwenden Sie Web Serial für einen Arduino, weil das Betriebssystem einen Seriell-Treiber lädt, der den USB-zu-Seriell-Adapter beansprucht und das Board als COM-Port oder /dev/tty-Knoten auflistet, den WebUSB nicht erreichen kann. WebUSB ist für direkte USB-Schnittstellen ohne OS-Seriell-Treiber gedacht, beispielsweise für benutzerdefinierte USB-Geräte. Wenn ein Gerät als COM-Port erscheint, benötigt es Web Serial, nicht WebUSB.

Ja. Rufen Sie navigator.serial.getPorts() auf, um das Array der Ports abzurufen, die der Benutzer in früheren Sitzungen bereits genehmigt hat, und öffnen Sie dann einen direkt ohne Aufforderung. Damit wird die Benutzeraktions-Auswahl bei erneuten Besuchen vollständig übersprungen. Ab Chrome 130 gibt SerialPort.connected einen booleschen Wert zurück, der angibt, ob der Port physisch vorhanden ist, sodass die Wiederverbindungs-UX zwischen einem getrennten Gerät und einem von der App geschlossenen Port unterscheiden kann.

Web Serial läuft auf dem Desktop in Chrome 89+, Edge 89+, Opera 75+ und Firefox 151+, gemäß den Browser-Kompatibilitätsdaten von MDN. Safari hat keine angekündigte Unterstützung und keine veröffentlichte Roadmap. Die API erfordert außerdem einen sicheren Kontext, also HTTPS oder localhost, sowie eine Benutzeraktion für den Aufruf von requestPort(). Wenn Sie heute eine breite Firefox- und Safari-Abdeckung benötigen, ist ein nativer Daemon, der einen WebSocket bereitstellt, der portable Fallback – auf Kosten eines Installationsschritts.

Ein 'Port bereits geöffnet'-Fehler bei der Wiederverbindung bedeutet fast immer, dass eine Stream-Sperre verloren gegangen ist. Wenn Sie port.close() aufrufen, während ein Reader noch den lesbaren Stream hält, wird das Schließen abgelehnt, weil der Stream gesperrt ist. Rufen Sie immer reader.releaseLock() vor port.close() auf, idealerweise in einem finally-Block, damit die Sperre unabhängig davon freigegeben wird, wie die Leseschleife beendet wird. Dasselbe gilt für Writer, die aus port.writable bezogen wurden.

Gain control over your UX

See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay. — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay