Aus einem seriellen Port lesen und darauf schreiben

Die Web Serial API ermöglicht Websites die Kommunikation mit seriellen Geräten.

François Beaufort
François Beaufort

Was ist die Web Serial API?

Ein serieller Port ist eine bidirektionale Kommunikationsschnittstelle, über die Daten Byte für Byte gesendet und empfangen werden können.

Die Web Serial API bietet Websites die Möglichkeit, mit JavaScript auf einem seriellen Gerät zu lesen und zu schreiben. Serielle Geräte werden entweder über einen seriellen Port im System des Nutzers oder über USB- und Bluetooth-Geräte verbunden, die einen seriellen Port emulieren.

Mit anderen Worten, die Web Serial API verbindet das Web und die physische Welt, indem Websites die Kommunikation mit seriellen Geräten wie Mikrocontrollern und 3D-Druckern ermöglicht.

Diese API ist auch eine hervorragende Ergänzung zu WebUSB, da Anwendungen die Kommunikation mit einigen seriellen Ports über ihre höherwertige serielle API statt über die Low-Level-USB API erfordern.

Empfohlene Anwendungsfälle

Im Bildungs-, Hobby- und Industriesektor verbinden Nutzer Peripheriegeräte mit ihren Computern. Diese Geräte werden oft von Mikrocontrollern über eine serielle Verbindung gesteuert, die von kundenspezifischer Software verwendet wird. Einige benutzerdefinierte Software zur Steuerung dieser Geräte basiert auf Webtechnologie:

In einigen Fällen kommunizieren Websites mit dem Gerät über eine Agent-Anwendung, die Nutzer manuell installiert haben. In anderen Fällen wird die Anwendung in einem Anwendungspaket über ein Framework wie Elektron bereitgestellt. In anderen Fällen muss der Nutzer einen zusätzlichen Schritt ausführen, z. B. eine kompilierte Anwendung über einen USB-Speicher auf das Gerät kopieren.

In allen diesen Fällen wird die User Experience verbessert, indem eine direkte Kommunikation zwischen der Website und dem von ihr gesteuerten Gerät ermöglicht wird.

Aktueller Status

Step Status
1. Erklärende Erklärung erstellen Abschließen
2. Ersten Entwurf der Spezifikation erstellen Abschließen
3. Feedback einholen und Design iterieren Abschließen
4. Ursprungstest Abschließen
5. Starten Abschließen

Web Serial API verwenden

Funktionserkennung

So kannst du prüfen, ob die Web Serial API unterstützt wird:

if ("serial" in navigator) {
  // The Web Serial API is supported.
}

Seriellen Port öffnen

Die Web Serial API ist standardmäßig asynchron. Dadurch wird verhindert, dass die Website-UI blockiert wird, wenn sie auf eine Eingabe wartet. Dies ist wichtig, da serielle Daten jederzeit empfangen werden können und eine Möglichkeit zum Abhören erforderlich ist.

Rufen Sie zum Öffnen eines seriellen Ports zuerst ein SerialPort-Objekt auf. Dazu können Sie den Nutzer entweder auffordern, einen einzelnen seriellen Port auszuwählen, indem Sie navigator.serial.requestPort() als Reaktion auf eine Nutzergeste wie Berührung oder Mausklick aufrufen, oder Sie wählen eine aus navigator.serial.getPorts() aus. Daraufhin wird eine Liste der seriellen Ports zurückgegeben, auf die der Website Zugriff gewährt wurde.

document.querySelector('button').addEventListener('click', async () => {
  // Prompt user to select any serial port.
  const port = await navigator.serial.requestPort();
});
// Get all serial ports the user has previously granted the website access to.
const ports = await navigator.serial.getPorts();

Für die Funktion navigator.serial.requestPort() wird ein optionales Objektliteral verwendet, das Filter definiert. Sie werden verwendet, um alle seriellen Geräte abzugleichen, die über USB mit einem obligatorischen USB-Anbieter (usbVendorId) und optionalen USB-Produkt-IDs (usbProductId) verbunden sind.

// Filter on devices with the Arduino Uno USB Vendor/Product IDs.
const filters = [
  { usbVendorId: 0x2341, usbProductId: 0x0043 },
  { usbVendorId: 0x2341, usbProductId: 0x0001 }
];

// Prompt user to select an Arduino Uno device.
const port = await navigator.serial.requestPort({ filters });

const { usbProductId, usbVendorId } = port.getInfo();
Screenshot der Aufforderung für einen seriellen Port auf einer Website
Nutzeraufforderung zur Auswahl eines BBC-micro:bit

Beim Aufrufen von requestPort() wird der Nutzer zur Auswahl eines Geräts aufgefordert, und es wird ein SerialPort-Objekt zurückgegeben. Sobald Sie ein SerialPort-Objekt haben, wird durch Aufrufen von port.open() mit der gewünschten Baudrate der serielle Port geöffnet. Das Wörterbuchmitglied baudRate gibt an, wie schnell Daten über eine serielle Zeile gesendet werden. Sie wird in Bit-pro-Sekunde (bps) angegeben. Suchen Sie in der Dokumentation Ihres Geräts nach dem korrekten Wert, da alle Daten, die Sie senden und empfangen, unsinnige Daten sind, wenn diese falsch angegeben sind. Bei einigen USB- und Bluetooth-Geräten, die einen seriellen Port emulieren, kann dieser Wert sicher auf einen beliebigen Wert festgelegt werden, da er von der Emulation ignoriert wird.

// Prompt user to select any serial port.
const port = await navigator.serial.requestPort();

// Wait for the serial port to open.
await port.open({ baudRate: 9600 });

Beim Öffnen eines seriellen Ports können Sie auch eine der folgenden Optionen angeben. Diese Optionen sind optional und haben praktische Standardwerte.

  • dataBits: Die Anzahl der Datenbit pro Frame (7 oder 8).
  • stopBits: Die Anzahl der Stopp-Bits am Ende eines Frames (entweder 1 oder 2).
  • parity: Der Paritätsmodus ("none", "even" oder "odd").
  • bufferSize: Die Größe der Lese- und Schreibpuffer, die erstellt werden sollen (muss kleiner als 16 MB sein).
  • flowControl: Der Ablaufsteuerungsmodus (entweder "none" oder "hardware").

Aus einem seriellen Port lesen

Eingabe- und Ausgabestreams in der Web Serial API werden von der Streams API verarbeitet.

Nachdem die Verbindung mit dem seriellen Port hergestellt wurde, geben die Attribute readable und writable aus dem Objekt SerialPort einen ReadableStream und einen WritableStream zurück. Diese werden verwendet, um Daten vom seriellen Gerät zu empfangen und an dieses zu senden. Beide verwenden Uint8Array-Instanzen für die Datenübertragung.

Wenn neue Daten vom seriellen Gerät eintreffen, gibt port.readable.getReader().read() asynchron zwei Attribute zurück: den booleschen Wert value und einen booleschen done-Wert. Wenn done auf „true“ gesetzt ist, wurde der serielle Port geschlossen oder es gehen keine Daten mehr ein. Durch das Aufrufen von port.readable.getReader() wird ein Leser erstellt und readable daran gesperrt. Solange readable gesperrt ist, kann der serielle Port nicht geschlossen werden.

const reader = port.readable.getReader();

// Listen to data coming from the serial device.
while (true) {
  const { value, done } = await reader.read();
  if (done) {
    // Allow the serial port to be closed later.
    reader.releaseLock();
    break;
  }
  // value is a Uint8Array.
  console.log(value);
}

Einige nicht schwerwiegende Fehler beim Lesen des seriellen Ports können unter bestimmten Bedingungen wie Pufferüberlauf, Framing-Fehler oder Paritätsfehler auftreten. Diese werden als Ausnahmen ausgegeben und können abgefangen werden, indem eine weitere Schleife über der vorherigen Schleife hinzugefügt wird, die port.readable prüft. Dies funktioniert, denn solange die Fehler nicht schwerwiegende Fehler sind, wird automatisch ein neuer ReadableStream erstellt. Wenn ein schwerwiegender Fehler auftritt, z. B. wenn das serielle Gerät entfernt wird, wird port.readable auf null gesetzt.

while (port.readable) {
  const reader = port.readable.getReader();

  try {
    while (true) {
      const { value, done } = await reader.read();
      if (done) {
        // Allow the serial port to be closed later.
        reader.releaseLock();
        break;
      }
      if (value) {
        console.log(value);
      }
    }
  } catch (error) {
    // TODO: Handle non-fatal read error.
  }
}

Wenn das serielle Gerät Text zurücksendet, können Sie port.readable über eine TextDecoderStream einfügen, wie unten gezeigt. Ein TextDecoderStream ist ein Transformationsstream, der alle Uint8Array-Chunks abruft und in Strings umwandelt.

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

// Listen to data coming from the serial device.
while (true) {
  const { value, done } = await reader.read();
  if (done) {
    // Allow the serial port to be closed later.
    reader.releaseLock();
    break;
  }
  // value is a string.
  console.log(value);
}

Mit einem „Bring Your Own Puffer“-Reader können Sie steuern, wie Arbeitsspeicher zugewiesen wird, wenn Sie aus dem Stream lesen. Rufe port.readable.getReader({ mode: "byob" }) auf, um die ReadableStreamBYOBReader-Schnittstelle zu erhalten, und gib beim Aufrufen von read() deine eigene ArrayBuffer an. Beachte, dass die Web Serial API diese Funktion ab Chrome 106 unterstützt.

try {
  const reader = port.readable.getReader({ mode: "byob" });
  // Call reader.read() to read data into a buffer...
} catch (error) {
  if (error instanceof TypeError) {
    // BYOB readers are not supported.
    // Fallback to port.readable.getReader()...
  }
}

Hier ein Beispiel, wie der Zwischenspeicher von value.buffer wiederverwendet werden kann:

const bufferSize = 1024; // 1kB
let buffer = new ArrayBuffer(bufferSize);

// Set `bufferSize` on open() to at least the size of the buffer.
await port.open({ baudRate: 9600, bufferSize });

const reader = port.readable.getReader({ mode: "byob" });
while (true) {
  const { value, done } = await reader.read(new Uint8Array(buffer));
  if (done) {
    break;
  }
  buffer = value.buffer;
  // Handle `value`.
}

Hier ist ein weiteres Beispiel dafür, wie eine bestimmte Datenmenge aus einem seriellen Port gelesen wird:

async function readInto(reader, buffer) {
  let offset = 0;
  while (offset < buffer.byteLength) {
    const { value, done } = await reader.read(
      new Uint8Array(buffer, offset)
    );
    if (done) {
      break;
    }
    buffer = value.buffer;
    offset += value.byteLength;
  }
  return buffer;
}

const reader = port.readable.getReader({ mode: "byob" });
let buffer = new ArrayBuffer(512);
// Read the first 512 bytes.
buffer = await readInto(reader, buffer);
// Then read the next 512 bytes.
buffer = await readInto(reader, buffer);

In einen seriellen Port schreiben

Übergeben Sie Daten an port.writable.getWriter().write(), um Daten an ein serielles Gerät zu senden. Das Aufrufen von releaseLock() auf port.writable.getWriter() ist erforderlich, damit der serielle Port später geschlossen werden kann.

const writer = port.writable.getWriter();

const data = new Uint8Array([104, 101, 108, 108, 111]); // hello
await writer.write(data);


// Allow the serial port to be closed later.
writer.releaseLock();

Senden Sie Text über eine TextEncoderStream, die an port.writable weitergeleitet wird, an das Gerät, wie unten dargestellt.

const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);

const writer = textEncoder.writable.getWriter();

await writer.write("hello");

Seriellen Port schließen

port.close() schließt den seriellen Port, wenn die Mitglieder readable und writable entsperrt sind, d. h. releaseLock() für ihren jeweiligen Leser und Autor aufgerufen wurde.

await port.close();

Wenn jedoch kontinuierlich Daten von einem seriellen Gerät in einer Schleife gelesen werden, wird port.readable immer gesperrt, bis ein Fehler auftritt. In diesem Fall zwingt das Aufrufen von reader.cancel() dazu, dass reader.read() sofort mit { value: undefined, done: true } aufgelöst wird, sodass die Schleife reader.releaseLock() aufruft.

// Without transform streams.

let keepReading = true;
let reader;

async function readUntilClosed() {
  while (port.readable && keepReading) {
    reader = port.readable.getReader();
    try {
      while (true) {
        const { value, done } = await reader.read();
        if (done) {
          // reader.cancel() has been called.
          break;
        }
        // value is a Uint8Array.
        console.log(value);
      }
    } catch (error) {
      // Handle error...
    } finally {
      // Allow the serial port to be closed later.
      reader.releaseLock();
    }
  }

  await port.close();
}

const closedPromise = readUntilClosed();

document.querySelector('button').addEventListener('click', async () => {
  // User clicked a button to close the serial port.
  keepReading = false;
  // Force reader.read() to resolve immediately and subsequently
  // call reader.releaseLock() in the loop example above.
  reader.cancel();
  await closedPromise;
});

Bei Verwendung von Transformationsstreams ist das Schließen eines seriellen Ports komplizierter. Rufen Sie reader.cancel() wie zuvor an. Rufen Sie dann writer.close() und port.close() auf. Dadurch werden Fehler über die Transformationsstreams an den zugrunde liegenden seriellen Port weitergegeben. Da die Fehlerverteilung nicht sofort erfolgt, müssen Sie die zuvor erstellten Propaganda readableStreamClosed und writableStreamClosed verwenden, um zu erkennen, wann port.readable und port.writable entsperrt wurden. Wenn Sie den reader abbrechen, wird der Stream abgebrochen. Aus diesem Grund müssen Sie den resultierenden Fehler abfangen und ignorieren.

// With transform streams.

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

// Listen to data coming from the serial device.
while (true) {
  const { value, done } = await reader.read();
  if (done) {
    reader.releaseLock();
    break;
  }
  // value is a string.
  console.log(value);
}

const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);

reader.cancel();
await readableStreamClosed.catch(() => { /* Ignore the error */ });

writer.close();
await writableStreamClosed;

await port.close();

Hören, wenn Verbindung unterbrochen wird oder getrennt wird

Wenn ein serieller Port von einem USB-Gerät bereitgestellt wird, kann dieses Gerät mit dem System verbunden oder von diesem getrennt werden. Wenn der Website die Berechtigung für den Zugriff auf einen seriellen Port gewährt wurde, sollten die Ereignisse connect und disconnect überwacht werden.

navigator.serial.addEventListener("connect", (event) => {
  // TODO: Automatically open event.target or warn user a port is available.
});

navigator.serial.addEventListener("disconnect", (event) => {
  // TODO: Remove |event.target| from the UI.
  // If the serial port was opened, a stream error would be observed as well.
});

Signale verarbeiten

Nachdem Sie die Verbindung mit dem seriellen Port hergestellt haben, können Sie Signale, die vom seriellen Port bereitgestellt werden, zur Geräteerkennung und Ablaufsteuerung explizit abfragen und festlegen. Diese Signale sind als boolesche Werte definiert. Einige Geräte wie Arduino wechseln beispielsweise in einen Programmiermodus, wenn das DTR-Signal (Data Terminal Ready) umgeschaltet wird.

Das Festlegen von Ausgabesignalen und das Abrufen von Eingabesignalen erfolgen durch Aufrufen von port.setSignals() und port.getSignals(). Anwendungsbeispiele finden Sie unten.

// Turn off Serial Break signal.
await port.setSignals({ break: false });

// Turn on Data Terminal Ready (DTR) signal.
await port.setSignals({ dataTerminalReady: true });

// Turn off Request To Send (RTS) signal.
await port.setSignals({ requestToSend: false });
const signals = await port.getSignals();
console.log(`Clear To Send:       ${signals.clearToSend}`);
console.log(`Data Carrier Detect: ${signals.dataCarrierDetect}`);
console.log(`Data Set Ready:      ${signals.dataSetReady}`);
console.log(`Ring Indicator:      ${signals.ringIndicator}`);

Streams transformieren

Wenn Sie Daten vom seriellen Gerät empfangen, erhalten Sie nicht unbedingt alle Daten auf einmal. Er kann beliebig aufgeteilt werden. Weitere Informationen finden Sie unter Streams API-Konzepte.

Sie können einige integrierte Transformationsstreams wie TextDecoderStream verwenden oder einen eigenen Transformationsstream erstellen, mit dem Sie den eingehenden Stream parsen und geparste Daten zurückgeben können. Der Transformationsstream befindet sich zwischen dem seriellen Gerät und der Leseschleife, die den Stream aufnimmt. Sie kann eine beliebige Transformation anwenden, bevor die Daten verarbeitet werden. Stellen Sie sich das wie ein Fließband vor: Wie ein Widget weiterläuft, ändert jeder Schritt in der Linie das Widget, sodass es bis zum endgültigen Ziel automatisch ein voll funktionsfähiges Widget ist.

Foto einer Flugzeugfabrik
Flugzeugwerk Bromwich Castle Bromwich aus dem 2. Weltkrieg

So können Sie beispielsweise eine Transformationsstreamklasse erstellen, die einen Stream aufnimmt und anhand von Zeilenumbrüchen aufteilt. Die Methode transform() wird jedes Mal aufgerufen, wenn der Stream neue Daten empfängt. Die Daten können entweder in eine Warteschlange eingereiht oder für später gespeichert werden. Die Methode flush() wird beim Schließen des Streams aufgerufen und verarbeitet alle Daten, die noch nicht verarbeitet wurden.

Um die Transform-Stream-Klasse zu verwenden, müssen Sie einen eingehenden Stream über sie leiten. Im dritten Codebeispiel unter Aus einem seriellen Port lesen wurde der ursprüngliche Eingabestream nur über eine TextDecoderStream-Leitung geleitet. Daher müssen wir pipeThrough() aufrufen, um ihn durch unsere neue LineBreakTransformer zu leiten.

class LineBreakTransformer {
  constructor() {
    // A container for holding stream data until a new line.
    this.chunks = "";
  }

  transform(chunk, controller) {
    // Append new chunks to existing chunks.
    this.chunks += chunk;
    // For each line breaks in chunks, send the parsed lines out.
    const lines = this.chunks.split("\r\n");
    this.chunks = lines.pop();
    lines.forEach((line) => controller.enqueue(line));
  }

  flush(controller) {
    // When the stream is closed, flush any remaining chunks out.
    controller.enqueue(this.chunks);
  }
}
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable
  .pipeThrough(new TransformStream(new LineBreakTransformer()))
  .getReader();

Verwenden Sie zum Beheben von Kommunikationsproblemen mit seriellen Geräten die Methode tee() von port.readable, um die Streams aufzuteilen, die zum oder vom seriellen Gerät führen. Die beiden erstellten Streams können unabhängig voneinander verarbeitet werden und Sie können einen Stream zur Prüfung in der Konsole ausgeben.

const [appReadable, devReadable] = port.readable.tee();

// You may want to update UI with incoming data from appReadable
// and log incoming data in JS console for inspection from devReadable.

Zugriff auf einen seriellen Port widerrufen

Die Website kann Berechtigungen für den Zugriff auf einen seriellen Port bereinigen, den sie nicht mehr beibehalten möchte. Dazu ruft sie forget() auf der Instanz SerialPort auf. Bei einer Webanwendung für Bildungseinrichtungen, 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 serial port.
await port.forget();

Sobald forget() in Chrome 103 oder höher verfügbar ist, sollten Sie Folgendes prüfen:

if ("serial" in navigator && "forget" in SerialPort.prototype) {
  // forget() is supported.
}

Entwicklertipps

Das Debugging der Web Serial API in Chrome ist auf der internen Seite about://device-log ganz einfach. Dort kannst du alle Ereignisse im Zusammenhang mit seriellen Geräten an einem einzigen Ort sehen.

Screenshot der internen Seite für das Debugging der Web Serial API.
Interne Seite in Chrome zum Debugging der Web Serial API.

Codelab

Im Google Developer-Codelab nutzen Sie die Web Serial API, um mit einem BBC-micro:bit-Board zu interagieren, um Bilder auf der 5x5-LED-Matrix anzuzeigen.

Unterstützte Browser

Die Web Serial API ist auf allen Desktopplattformen (ChromeOS, Linux, macOS und Windows) in Chrome 89 verfügbar.

Polyfill

Unter Android ist die Unterstützung für USB-basierte serielle Ports über die WebUSB API und das Polyfill für die serielle API möglich. Dieses Polyfill ist auf Hardware und Plattformen beschränkt, auf denen das Gerät über die WebUSB API zugänglich ist, da es nicht durch einen integrierten Gerätetreiber beansprucht wurde.

Sicherheit und Datenschutz

Die Spezifikationsautoren haben die Web Serial API nach den unter Zugriff auf leistungsstarke Webplattformfunktionen steuern definierten Kernprinzipien, einschließlich Nutzersteuerung, Transparenz und Ergonomie, entworfen und implementiert. Diese API wird hauptsächlich durch ein Berechtigungsmodell gesteuert, das jeweils nur einem einzigen seriellen Gerät Zugriff gewährt. Auf eine Nutzeraufforderung muss der Nutzer aktive Schritte unternehmen, um ein bestimmtes serielles Gerät auszuwählen.

Informationen zu den Vor- und Nachteilen der Sicherheit finden Sie in den Abschnitten Sicherheit und Datenschutz in der Erläuterung zur Web Serial API.

Feedback

Das Chrome-Team würde gerne Ihre Meinung zur Web Serial API hören.

Informationen zum API-Design

Gibt es etwas an der API, das nicht wie erwartet funktioniert? Oder fehlen Methoden oder Eigenschaften, die Sie zur Implementierung Ihrer Idee benötigen?

Melden Sie ein Spezifikationsproblem im GitHub-Repository für die Web Serial API oder fügen Sie Ihre Gedanken einem bestehenden Problem hinzu.

Problem mit der Implementierung melden

Haben Sie einen Fehler bei der Implementierung in Chrome gefunden? Oder unterscheidet sich die Implementierung von der Spezifikation?

Melde einen Fehler unter https://new.crbug.com. Gib so viele Details wie möglich an, stelle eine einfache Anleitung zum Reproduzieren des Fehlers bereit und setze Komponenten auf Blink>Serial. Mit Glitch lassen sich schnelle und einfache Reproduktionen teilen.

Support zeigen

Möchtest du die Web Serial API verwenden? Ihre öffentliche Unterstützung hilft dem Chrome-Team, Funktionen zu priorisieren, und zeigt anderen Browseranbietern, wie wichtig es ist, sie zu unterstützen.

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

Nützliche Links

Demos

Danksagungen

Wir danken Reilly Grant und Joe Medley für ihre Rezensionen zu diesem Artikel. Foto einer Flugzeugfabrik vom Birmingham Museums Trust auf Unsplash