Aus einem seriellen Port lesen und darauf schreiben

Über die 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 Byte für Byte empfangen.

Mit der Web Serial API können Websites Daten aus einem serielles Gerät mit JavaScript. Serielle Geräte sind entweder über eine seriellen Port am System des Nutzers oder über abnehmbare USB- und Bluetooth-Geräte die einen seriellen Port emulieren.

Mit anderen Worten, die Web Serial API verbindet das Web und die physische Welt Websites können so mit seriellen Geräten wie Mikrocontrollern kommunizieren und 3D-Drucker.

Diese API ist auch eine gute Ergänzung zu WebUSB, da Betriebssysteme Anwendungen mit ihren übergeordneten Ports mit einigen seriellen Ports serielle API anstelle der Low-Level-USB-API.

Empfohlene Anwendungsfälle

Im Bildungs-, Hobby- und Industriebereich verbinden Nutzende Peripheriegeräte miteinander. auf ihren Computern. Diese Geräte werden oft von Mikrocontroller über eine serielle Verbindung, die von benutzerdefinierter Software verwendet wird. Einige benutzerdefinierte Software zur Steuerung dieser Geräte wird mit Webtechnologie entwickelt:

In einigen Fällen kommunizieren Websites über einen Agenten mit dem Gerät. die Nutzer manuell installiert haben. In anderen Fällen ist die Anwendung in einer gepackten Anwendung über ein Framework wie Electron geliefert. In anderen Fällen muss der Nutzer einen zusätzlichen Schritt ausführen, z. B. Kopieren einer kompilierten App über einen USB-Speicher auf das Gerät

In all diesen Fällen wird die User Experience verbessert, indem direkte Kommunikation zwischen der Website und dem Gerät, das sie steuern.

Aktueller Status

Schritt Status
1. Erklärende 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. Einführung 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. Dadurch wird verhindert, beim Warten auf eine Eingabe blockiert wird. Dies ist wichtig, weil serielle Daten jederzeit empfangen werden, sodass eine Möglichkeit erforderlich ist, sie anzuhören.

Greifen Sie zum Öffnen eines seriellen Ports zuerst auf ein SerialPort-Objekt zu. Dazu können Sie Sie fordern den Nutzer entweder auf, einen einzelnen seriellen Port auszuwählen, indem er Folgendes aufruft: navigator.serial.requestPort() als Reaktion auf eine Nutzergeste, z. B. eine Berührung oder Mausklick aus. Sie können auch einen Wert aus navigator.serial.getPorts() auswählen, Eine Liste der seriellen Ports, 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();

Die Funktion navigator.serial.requestPort() verwendet ein optionales Objektliteral. der Filter definiert. Diese werden verwendet, um alle seriellen Geräte zuzuordnen, die über USB mit obligatorischem USB-Anbieter (usbVendorId) und optionalem USB-Produkt Kennungen (usbProductId)

// 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();
<ph type="x-smartling-placeholder">
</ph> Screenshot einer Aufforderung für den seriellen Port auf einer Website
Nutzeraufforderung zur Auswahl eines BBC-micro:bit

Beim Aufrufen von requestPort() wird der Nutzer aufgefordert, ein Gerät auszuwählen, und es wird eine SerialPort-Objekt. Sobald Sie ein SerialPort-Objekt haben, rufen Sie port.open() auf. mit der gewünschten Baudrate, wird der serielle Port geöffnet. Das Wörterbuch baudRate member gibt an, wie schnell Daten über eine serielle Leitung gesendet werden. Ausgedrückt in Einheiten von Bits-per-Second (Bit/s). Lesen Sie in der Dokumentation Ihres Geräts nach den richtigen Wert, da alle gesendeten und empfangenen Daten unsinnig sind, wenn dies falsch angegeben ist. Bei einigen USB- und Bluetooth-Geräten, die eine serielle Port kann dieser Wert auf einen beliebigen Wert gesetzt werden, da er vom Emulation verwenden.

// 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 kannst du auch eine der folgenden Optionen festlegen. Diese Optionen sind optional und haben praktische Standardwerte.

  • dataBits: Die Anzahl der Datenbits pro Frame (entweder 7 oder 8).
  • stopBits: Die Anzahl der Stopp-Bits am Ende eines Frames (entweder 1 oder 2).
  • parity: Der Paritätsmodus (entweder "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, werden readable und writable aus dem SerialPort-Objekt geben einen ReadableStream und einen WritableStream Diese dienen zum Empfangen und Senden von Daten an den seriellen Gerät. Beide verwenden Uint8Array-Instanzen für die Datenübertragung.

Wenn neue Daten vom seriellen Gerät eingehen, port.readable.getReader().read() gibt asynchron zwei Eigenschaften zurück: den booleschen Wert value und den booleschen Wert done. Wenn done ist „true“, der serielle Port wurde geschlossen oder es gehen keine Daten mehr ein . Durch das Aufrufen von port.readable.getReader() wird ein Lesegerät erstellt und readable wird für . 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);
}

Nicht schwerwiegende Fehler beim Lesen des seriellen Ports können unter folgenden Bedingungen auftreten: Pufferüberlauf, Framing- oder Paritätsfehler. Diese werden als Ausnahmen und kann durch Hinzufügen einer weiteren Schleife über der vorherigen Schleife abgefangen werden. das port.readable überprüft. Das funktioniert, denn solange die Fehler nicht schwerwiegenden Fehlers, wird automatisch ein neuer ReadableStream erstellt. Schwerwiegender Fehler erfolgt, 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 eine Nachricht sendet, können Sie port.readable über einen TextDecoderStream, wie unten gezeigt. Ein TextDecoderStream ist ein Transformationsstream. erfasst alle Uint8Array-Blöcke und konvertiert sie in Strings.

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 der Funktion „Bring Your Own Buffer“ können Sie festlegen, wie Arbeitsspeicher zugeordnet wird, wenn Sie Daten aus dem Stream lesen. Leser. 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 Zwischenspeichers von value.buffer:

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, wie eine bestimmte Datenmenge von 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

Um Daten an ein serielles Gerät zu senden, port.writable.getWriter().write() releaseLock() wird angerufen 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();

SMS über eine TextEncoderStream-Leitung an port.writable an das Gerät senden 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 seine readable- und writable-Mitglieder entsperrt sind, d. h. releaseLock() für ihren jeweiligen lesen und schreiben.

await port.close();

Beim fortlaufenden Lesen von Daten von einem seriellen Gerät mithilfe einer Schleife port.readable ist so lange gesperrt, bis ein Fehler auftritt. In dieser Wenn reader.cancel() aufgerufen wird, wird reader.read() gezwungen, sofort mit { value: undefined, done: true } und kann daher Schleife zum Aufrufen von reader.releaseLock().

// 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 der Verwendung von Transformationsstreams komplizierter. Rufen Sie reader.cancel() wie zuvor auf. Rufen Sie dann writer.close() und port.close() auf. Dadurch werden Fehler an den zugrunde liegenden seriellen Port streamt. Weil Fehlerpropagierung nicht sofort auftritt, müssen Sie readableStreamClosed und writableStreamClosed hat früher erstellt, um zu erkennen, wann port.readable und port.writable wurden freigeschaltet. Das Abbrechen von reader führt dazu, dass Stream abgebrochen werden soll; Deshalb 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();

Informationen zu Verbindungs- und Verbindungsabbrüchen anhören

Wenn ein USB-Gerät einen seriellen Port bereitstellt, ist dieses Gerät möglicherweise verbunden oder vom System getrennt. Wenn der Website die Berechtigung erteilt wurde, auf einen seriellen Port zugreifen, 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 die Abfrage explizit abfragen und Signale, die der serielle Port zur Geräteerkennung und Ablaufsteuerung zur Verfügung stellt. Diese Signale sind als boolesche Werte definiert. Einige Geräte wie Arduino wechselt in einen Programmiermodus, wenn das DTR-Signal (Data Terminal Ready) umgeschaltet.

Das Festlegen von Ausgabesignalen und das Abrufen von Eingabesignalen erfolgen jeweils durch port.setSignals() und port.getSignals() werden aufgerufen. Siehe Anwendungsbeispiele weiter 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 umwandeln

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

Um dies zu vermeiden, können Sie einige integrierte Transformationsstreams verwenden, z. B. TextDecoderStream oder erstellen Sie Ihren eigenen Transformationsstream, mit dem Sie parsen Sie den eingehenden Stream und geben geparste Daten zurück. Der Transformationsstream zwischen dem seriellen Gerät und der Leseschleife, die den Stream nutzt. Es kann eine beliebige Transformation anwenden, bevor die Daten verarbeitet werden. Stellen Sie sich das als Fließband: Da ein Widget die Linie herunterfährt, verändert sich jeder Schritt und bei der Ankunft am Ziel ist es bereits funktionierenden Widgets.

<ph type="x-smartling-placeholder">
</ph> Foto einer Flugzeugfabrik
Burg Bromwich Aeroplane Factory

Überlegen Sie z. B., wie Sie eine Transform-Stream-Klasse erstellen, die einen und teilt sie entsprechend den Zeilenumbrüchen auf. Die transform()-Methode wird aufgerufen jedes Mal, wenn der Stream neue Daten empfängt. Er kann die Daten entweder in die Warteschlange stellen oder für später speichern. Die Methode flush() wird aufgerufen, wenn der Stream geschlossen ist, und verarbeitet sie alle Daten, die noch nicht verarbeitet wurden.

Zur Verwendung der Umwandlungs-Stream-Klasse müssen Sie einen eingehenden Stream über eine Pipeline . Im dritten Codebeispiel unter Von einem seriellen Port lesen Der ursprüngliche Eingabestream wurde nur über TextDecoderStream geleitet. müssen pipeThrough() aufrufen, um sie über 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();

Verwende zum Beheben von Problemen bei der Kommunikation serieller Geräte die Methode tee() oder port.readable, um die Streams zum oder vom seriellen Gerät aufzuteilen. Die beiden Erstellte Streams können unabhängig verarbeitet werden. So können Sie einen Stream zur Inspektion an die Konsole senden.

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 nicht mehr vorhandenen seriellen Port bereinigen die Sie behalten möchten, indem Sie forget() für die SerialPort-Instanz aufrufen. Für Bei einer Webanwendung für den Bildungsbereich, die auf einem gemeinsam genutzten Computer mit vielen kann eine große Zahl angesammelter benutzergenerierter Berechtigungen einen schlechten User Experience aus.

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

forget() ist in Chrome 103 oder höher verfügbar. Prüfen Sie daher, ob diese Funktion unterstützt durch Folgendes:

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

Entwicklertipps

Das Debugging der Web Serial API in Chrome ist einfach über die interne Seite. about://device-log, wo Sie alle Ereignisse in Verbindung mit seriellen Geräten in einem einzigen Tab sehen können an einem Ort.

<ph type="x-smartling-placeholder">
</ph> Screenshot der internen Seite zum Debuggen der Web Serial API.
Interne Seite in Chrome zur Fehlerbehebung bei der Web Serial API.

Codelab

Im Google Developer-Codelab nutzen Sie für die Interaktion die Web Serial API. BBC micro:Bit-Board, um Bilder auf der 5x5-LED-Matrix zu zeigen.

Unterstützte Browser

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

Polyfill

Auf Android-Geräten können mit der WebUSB API USB-basierte serielle Ports unterstützt werden und dem Serial API-Polyfill. Dieses Polyfill ist auf Hardware und Plattformen, bei denen das Gerät über die WebUSB API zugänglich ist, durch einen integrierten Gerätetreiber beansprucht.

Sicherheit und Datenschutz

Die Entwickler der Spezifikationen haben die Web Serial API mithilfe des Kerns die unter Zugriff auf leistungsstarke Webplattform-Funktionen steuern erläutert wird, einschließlich Nutzersteuerung, Transparenz und Ergonomie. Die Möglichkeit, diese Die API wird hauptsächlich durch ein Berechtigungsmodell gesteuert, das nur einem einzelnen Nutzer eines seriellen Geräts. Als Reaktion auf eine Nutzeraufforderung muss der Nutzer um ein bestimmtes serielles Gerät auszuwählen.

Weitere Informationen zu den Vor- und Nachteilen der Sicherheit finden Sie in den Artikeln zu Sicherheit und Datenschutz. der Web Serial API Explainer.

Feedback

Das Chrome-Team würde gern Ihre Meinung und Ihre Erfahrungen mit der Web Serial API

Informationen zum API-Design

Gibt es etwas an der API, das nicht wie erwartet funktioniert? Oder gibt es fehlende Methoden oder Eigenschaften, die Sie benötigen, um Ihre Idee umzusetzen?

Reichen Sie ein Spezifikationsproblem im GitHub-Repository für die Web Serial API ein oder fügen Sie die Gedanken zu einem bestehenden Problem machen.

Problem mit der Implementierung melden

Haben Sie bei der Implementierung von Chrome einen Fehler gefunden? Oder ist die Implementierung von der Spezifikation abweichen?

Melden Sie den Fehler unter https://new.crbug.com. Achten Sie darauf, so viele wie möglich, eine einfache Anleitung zum Reproduzieren des Fehlers geben und Components wurden auf Blink>Serial festgelegt. Glitch eignet sich hervorragend für schnelle und einfache Reproduktionen.

Support anzeigen

Möchten Sie die Web Serial API verwenden? Ihre öffentliche Unterstützung hilft der Chrome- priorisiert und zeigt anderen Browseranbietern, wie wichtig es ist, sie zu unterstützen.

Sende einen Tweet mit dem Hashtag an @ChromiumDev #SerialAPI und teilen Sie uns mit, wo und wie Sie sie nutzen.

Nützliche Links

Demos

Danksagungen

Vielen Dank an Reilly Grant und Joe Medley für die Rezensionen zu diesem Artikel. Foto der Flugzeugfabrik von Birmingham Museums Trust auf Unsplash