Aus einem seriellen Port lesen und darauf schreiben

Mit der Web Serial API können Websites mit seriellen Geräten kommunizieren.

François Beaufort
François Beaufort

Was ist die Web Serial API?

Ein serieller Port ist eine bidirektionale Kommunikationsschnittstelle, die das Senden und Empfangen von Daten Byte für Byte ermöglicht.

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

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

Diese API eignet sich auch hervorragend für WebUSB, da Betriebssysteme von Anwendungen verlangen, dass sie mit einigen seriellen Ports über die serielle API auf höherer Ebene und nicht über die Low-Level-USB-API kommunizieren.

Empfohlene Anwendungsfälle

Im Bildungs-, Hobby- und Industriebereich verbinden Nutzer Peripheriegeräte mit ihren Computern. Diese Geräte werden häufig über eine serielle Verbindung, die von benutzerdefinierter Software verwendet wird, von Mikrocontrollern gesteuert. Einige benutzerdefinierte Software zur Steuerung dieser Geräte wird mit Webtechnologie erstellt:

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 einer paketierten Anwendung über ein Framework wie Electron 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 all 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

Schritt Status
1. Erläuternde Mitteilung 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 prüfen Sie, 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. So wird verhindert, dass die Benutzeroberfläche der Website blockiert, wenn auf Eingabe gewartet wird. Das ist wichtig, da serielle Daten jederzeit empfangen werden können und eine Möglichkeit zum Abhören erforderlich ist.

Wenn Sie eine serielle Schnittstelle öffnen möchten, greifen Sie zuerst auf ein SerialPort-Objekt zu. Dazu können Sie den Nutzer entweder auffordern, einen einzelnen seriellen Anschluss auszuwählen, indem Sie navigator.serial.requestPort() als Reaktion auf eine Nutzergeste wie Tippen oder Mausklick aufrufen, oder einen aus navigator.serial.getPorts() auswählen, wodurch eine Liste der seriellen Anschlüsse zurückgegeben wird, auf die die Website Zugriff hat.

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

Die Funktion navigator.serial.requestPort() nimmt ein optionales Objektliteral an, das Filter definiert. Damit werden alle seriellen Geräte, die über USB verbunden sind, mit einem obligatorischen USB-Anbieter (usbVendorId) und optionalen USB-Produkt-IDs (usbProductId) abgeglichen.

// 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 einer Aufforderung für den seriellen Port auf einer Website
Aufforderung für Nutzer zur Auswahl einer BBC micro:bit

Wenn requestPort() aufgerufen wird, wird der Nutzer aufgefordert, ein Gerät auszuwählen. Es wird dann 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 Dictionary-Element baudRate gibt an, wie schnell Daten über eine serielle Schnittstelle gesendet werden. Sie wird in Bit/s (Bits pro Sekunde) angegeben. Sehen Sie in der Dokumentation Ihres Geräts nach, ob der Wert korrekt ist, da alle Daten, die Sie senden und empfangen, unsinnig sind, wenn diese Angabe falsch ist. Bei einigen USB- und Bluetooth-Geräten, die einen seriellen Port emulieren, kann dieser Wert sicher auf einen beliebigen Wert gesetzt 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 });

Sie können auch eine der folgenden Optionen angeben, wenn Sie einen seriellen Port öffnen. Diese Optionen sind optional und haben praktische Standardwerte.

  • dataBits: Die Anzahl der Datenbits pro Frame (entweder 7 oder 8).
  • stopBits: Die Anzahl der Stoppbits am Ende eines Frames (entweder 1 oder 2).
  • parity: Der Paritätsmodus (entweder "none", "even" oder "odd").
  • bufferSize: Die Größe der Lese- und Schreib-Caches, 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 zum seriellen Anschluss hergestellt wurde, geben die Eigenschaften readable und writable des Objekts SerialPort einen ReadableStream und einen WritableStream zurück. Über diese werden Daten vom seriellen Gerät empfangen und an das Gerät gesendet. Beide verwenden Uint8Array-Instanzen für die Datenübertragung.

Wenn neue Daten vom seriellen Gerät eingehen, gibt port.readable.getReader().read() asynchron zwei Eigenschaften zurück: value und ein boolescher Wert done. Wenn done wahr ist, wurde der serielle Anschluss geschlossen oder es werden keine Daten mehr empfangen. Durch Aufrufen von port.readable.getReader() wird ein Leser erstellt und readable wird daran angehängt. Während 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);
}

Unter bestimmten Bedingungen können nicht schwerwiegende Lesefehler am seriellen Anschluss auftreten, z. B. Pufferüberlauf, Framing-Fehler oder Paritätsfehler. Diese werden als Ausnahmen geworfen und können abgefangen werden, indem Sie der vorherigen Schleife eine weitere hinzufügen, die port.readable prüft. Das funktioniert, weil so lange die Fehler nicht fatal sind, automatisch ein neuer ReadableStream erstellt wird. Wenn ein schwerwiegender Fehler auftritt, z. B. wenn das serielle Gerät entfernt wird, wird port.readable zu null.

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 wie unten gezeigt über ein TextDecoderStream-Element weiterleiten. Ein TextDecoderStream ist ein Transformationsstream, der alle Uint8Array-Chunks abruft und in Strings konvertiert.

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 Buffer“-Reader können Sie steuern, wie der Arbeitsspeicher zugewiesen wird, wenn Sie Daten aus dem Stream lesen. Rufen Sie port.readable.getReader({ mode: "byob" }) auf, um die ReadableStreamBYOBReader-Schnittstelle zu erhalten, und geben Sie Ihre eigene ArrayBuffer an, wenn Sie read() aufrufen. Die Web Serial API unterstützt diese Funktion ab Chrome 106.

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 für die Wiederverwendung des value.buffer-Buffers:

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 ein weiteres Beispiel zum Lesen einer bestimmten Datenmenge von einem seriellen Anschluss:

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

Wenn Sie Daten an ein serielles Gerät senden möchten, übergeben Sie sie an port.writable.getWriter().write(). Der Aufruf von releaseLock() für 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 an das Gerät über eine TextEncoderStream, die an port.writable weitergeleitet wird, 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 Anschluss, wenn seine Mitglieder readable und writable entriegelt sind, d. h. releaseLock() wurde für den jeweiligen Leser und Schreiber aufgerufen.

await port.close();

Wenn Sie jedoch Daten kontinuierlich über eine Schleife von einem seriellen Gerät lesen, ist port.readable immer gesperrt, bis ein Fehler auftritt. In diesem Fall wird durch den Aufruf von reader.cancel() reader.read() gezwungen, sofort mit { value: undefined, done: true } aufgelöst zu werden, sodass die Schleife reader.releaseLock() aufrufen kann.

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

Das Schließen eines seriellen Ports ist bei Verwendung von Transformstreams komplizierter. Rufen Sie reader.cancel() wie gewohnt 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 Fehlerweitergabe nicht sofort erfolgt, müssen Sie die zuvor erstellten Promis readableStreamClosed und writableStreamClosed verwenden, um festzustellen, wann port.readable und port.writable entsperrt wurden. Ein Abbruch des reader führt dazu, dass der Stream abgebrochen wird. 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();

Auf Verbindung und Trennung achten

Wenn ein serielle Port von einem USB-Gerät bereitgestellt wird, kann dieses Gerät mit dem System verbunden oder getrennt sein. Wenn der Website die Berechtigung zum Zugriff auf einen seriellen Port gewährt wurde, sollte sie die connect- und disconnect-Ereignisse überwachen.

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 serielle Portverbindung hergestellt haben, können Sie die vom seriellen Port für die Geräteerkennung und die Ablaufsteuerung freigegebenen Signale explizit abfragen und festlegen. Diese Signale sind als boolesche Werte definiert. Einige Geräte wie Arduino wechseln beispielsweise in den Programmiermodus, wenn das DTR-Signal (Data Terminal Ready) umgeschaltet wird.

Ausgabesignale werden über port.setSignals() und Eingabesignale über port.getSignals() festgelegt. 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. Sie kann beliebig in kleinere Blöcke aufgeteilt werden. Weitere Informationen finden Sie unter Streams API-Konzepte.

Sie können dazu integrierte Transformationsstreams wie TextDecoderStream verwenden oder einen eigenen Transformationsstream erstellen, mit dem Sie den eingehenden Stream analysieren und die analysierten Daten zurückgeben können. Der Transformationsstream befindet sich zwischen dem seriellen Gerät und der Leseschleife, die den Stream nutzt. Es kann eine beliebige Transformation anwenden, bevor die Daten verwendet werden. Stellen Sie sich das wie eine Fließbandproduktion vor: Wenn ein Widget die Produktionslinie durchläuft, wird es in jedem Schritt der Linie modifiziert, sodass es am Ende ein voll funktionsfähiges Widget ist.

Foto einer Flugzeugfabrik
Castle Bromwich Aeroplane Factory im Zweiten Weltkrieg

Angenommen, Sie möchten eine Transform-Stream-Klasse erstellen, die einen Stream konsumiert und anhand von Zeilenwechseln in Chunks aufteilt. Die transform()-Methode wird jedes Mal aufgerufen, wenn neue Daten vom Stream empfangen werden. Sie können die Daten entweder in die Warteschlange stellen oder für später speichern. Die Methode flush() wird aufgerufen, wenn der Stream geschlossen wird. Sie verarbeitet alle Daten, die noch nicht verarbeitet wurden.

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

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 Problemen mit der Kommunikation mit seriellen Geräten die tee()-Methode von port.readable, um die Streams aufzuteilen, die zum seriellen Gerät oder von ihm ausgehen. Die beiden erstellten Streams können unabhängig voneinander verwendet werden. So können Sie einen zur Prüfung in die 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 die Berechtigungen für den Zugriff auf einen seriellen Port, der nicht mehr beibehalten werden soll, bereinigen, indem sie forget() auf der Instanz SerialPort aufruft. Bei einer Bildungs-Webanwendung, die auf einem gemeinsam genutzten Computer mit vielen Geräten verwendet wird, führt eine große Anzahl von nutzergenerierten Berechtigungen zu einer schlechten Nutzererfahrung.

// Voluntarily revoke access to this serial port.
await port.forget();

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

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

Entwicklertipps

Fehler an der Web Serial API lassen sich in Chrome ganz einfach über die interne Seite about://device-log beheben. Dort finden Sie alle Ereignisse in Verbindung mit seriellen Geräten an einem einzigen Ort.

Screenshot der internen Seite zum Debuggen der Web Serial API
Interne Seite in Chrome zum Debuggen der Web Serial API.

Codelab

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

Unterstützte Browser

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

Polyfill

Unter Android ist die Unterstützung von USB-basierten seriellen Ports mit der WebUSB API und der Serial API polyfill möglich. Dieser Polyfill ist auf Hardware und Plattformen beschränkt, auf denen das Gerät über die WebUSB API zugänglich ist, da es nicht von einem integrierten Gerätetreiber beansprucht wurde.

Sicherheit und Datenschutz

Die Autoren der Spezifikation haben die Web Serial API anhand der in Zugriff auf leistungsstarke Funktionen der Webplattform steuern definierten Grundprinzipien entworfen und implementiert, einschließlich Nutzersteuerung, Transparenz und Ergonomie. Die Verwendung dieser API ist hauptsächlich durch ein Berechtigungsmodell eingeschränkt, das jeweils nur Zugriff auf ein einzelnes serielles Gerät gewährt. Als Reaktion auf eine Nutzeraufforderung muss der Nutzer aktive Schritte ausführen, um ein bestimmtes serielles Gerät auszuwählen.

Informationen zu den Sicherheitsabwägungen finden Sie in den Abschnitten Sicherheit und Datenschutz der Erläuterung der Web Serial API.

Feedback

Das Chrome-Team würde sich über Ihre Meinung und Erfahrungen mit der Web Serial API freuen.

Informationen zum API-Design

Funktioniert die API nicht wie erwartet? Oder fehlen Methoden oder Eigenschaften, die Sie für die Implementierung Ihrer Idee benötigen?

Melde ein Problem mit der Spezifikation im GitHub-Repository der Web Serial API oder füge deine Meinung zu einem vorhandenen Problem hinzu.

Problem mit der Implementierung melden

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

Melden Sie den Fehler unter https://new.crbug.com. Geben Sie so viele Details wie möglich an, machen Sie eine einfache Anleitung zum Nachstellen des Fehlers und setzen Sie Components auf Blink>Serial. Glitch eignet sich hervorragend, um schnell und einfach Reproduktionen zu teilen.

Unterstützung zeigen

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

Senden Sie einen Tweet an @ChromiumDev mit dem Hashtag #SerialAPI und teilen Sie uns mit, wo und wie Sie ihn verwenden.

Nützliche Links

Demos

Danksagungen

Vielen Dank an Reilly Grant und Joe Medley für die Überprüfung dieses Artikels. Foto einer Flugzeugfabrik von Birmingham Museums Trust auf Unsplash