Mit der Web Serial API können Websites mit seriellen Geräten kommunizieren.
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.
Mit der Web Serial API können Websites mit JavaScript Daten von einem seriellen Gerät lesen und darauf schreiben. Serielle Geräte werden entweder über einen seriellen Port auf dem System des Nutzers oder über austauschbare USB- und Bluetooth-Geräte verbunden, die einen seriellen Port emulieren.
Mit anderen Worten: Die Web Serial API schlägt eine Brücke zwischen dem Web und der physischen Welt, indem sie Websites die Kommunikation mit seriellen Geräten wie Mikrocontrollern und 3D-Druckern ermöglicht.
Diese API ist auch eine gute Ergänzung zu WebUSB, da Betriebssysteme erfordern, dass Anwendungen über ihre serielle API auf höherer Ebene mit einigen seriellen Ports kommunizieren und nicht über die USB API auf niedriger Ebene.
Vorgeschlagene Anwendungsfälle
Im Bildungs-, Hobby- und Industriesektor verbinden Nutzer Peripheriegeräte mit ihren Computern. Diese Geräte werden häufig von Mikrocontrollern mit einer seriellen Verbindung gesteuert, die von benutzerdefinierter Software verwendet wird. Einige benutzerdefinierte Software zur Steuerung dieser Geräte wird mit Webtechnologie erstellt:
In einigen Fällen kommunizieren Websites über eine Agent-Anwendung mit dem Gerät, die Nutzer manuell installiert haben. In anderen Fällen wird die Anwendung in einer verpackten Anwendung über ein Framework wie Electron bereitgestellt. Und in anderen Fällen muss der Nutzer einen zusätzlichen Schritt ausführen, z. B. eine kompilierte Anwendung über einen USB-Stick auf das Gerät kopieren.
In all diesen Fällen wird die Nutzerfreundlichkeit verbessert, indem eine direkte Kommunikation zwischen der Website und dem Gerät ermöglicht wird, das sie steuert.
Aktueller Status
| Schritt | Status |
|---|---|
| 1. Erklärvideo erstellen | Abgeschlossen |
| 2. Ersten Entwurf der Spezifikation erstellen | Abgeschlossen |
| 3. Feedback einholen und Design optimieren | Abgeschlossen |
| 4. Ursprungstest | Abgeschlossen |
| 5. Starten | Abgeschlossen |
Web Serial API verwenden
Funktionserkennung
Verwenden Sie Folgendes, um zu 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 von Grund auf asynchron. Dadurch wird verhindert, dass die Website-UI blockiert wird, wenn auf eine Eingabe gewartet wird. Das ist wichtig, da serielle Daten jederzeit empfangen werden können und es eine Möglichkeit geben muss, sie zu überwachen.
Um einen seriellen Port zu öffnen, greifen Sie zuerst auf ein SerialPort-Objekt zu. Dazu können Sie den Nutzer entweder auffordern, einen einzelnen seriellen Port auszuwählen, indem Sie als Reaktion auf eine Nutzeraktion wie eine Berührung oder einen Mausklick navigator.serial.requestPort() aufrufen, oder einen aus navigator.serial.getPorts() auswählen. Diese Funktion gibt eine Liste der seriellen Ports zurück, 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() verwendet ein optionales Objektliteral, das Filter definiert. Diese werden verwendet, um alle über USB verbundenen seriellen Geräte mit einer obligatorischen USB-Anbieter-ID (usbVendorId) und optionalen USB-Produkt-IDs (usbProductId) abzugleichen.
// 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();
Wenn Sie requestPort() aufrufen, wird der Nutzer aufgefordert, ein Gerät auszuwählen, und es wird ein SerialPort-Objekt zurückgegeben. Sobald Sie ein SerialPort-Objekt haben, wird der serielle Port geöffnet, wenn Sie port.open() mit der gewünschten Baudrate aufrufen. Das Wörterbuchmitglied baudRate gibt an, wie schnell Daten über eine serielle Leitung gesendet werden. Es wird in Bit pro Sekunde (Bit/s) angegeben. Den korrekten Wert finden Sie in der Dokumentation Ihres Geräts. Wenn er falsch angegeben ist, sind alle gesendeten und empfangenen Daten unverständlich. 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 beim Öffnen eines seriellen Ports auch eine der folgenden Optionen angeben. 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 Schreibpuffer, die erstellt werden sollen (muss weniger als 16 MB betragen).flowControl: Der Modus für die Flusssteuerung (entweder"none"oder"hardware").
Von einem seriellen Port lesen
Eingabe- und Ausgabestreams in der Web Serial API werden von der Streams API verarbeitet.
Nachdem die serielle Portverbindung hergestellt wurde, geben die readable und writable
Eigenschaften des SerialPort Objekts einen ReadableStream und einen
WritableStream zurück. Diese werden verwendet, um Daten vom seriellen Gerät zu empfangen und Daten an das serielle Gerät zu senden. 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: den value und einen booleschen Wert done. Wenn done „true“ ist, wurde der serielle Port geschlossen oder es kommen keine weiteren Daten an. Wenn Sie port.readable.getReader() aufrufen, wird ein Reader erstellt und readable wird für
ihn 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);
}
Unter bestimmten Bedingungen können einige nicht schwerwiegende Lesefehler am seriellen Port auftreten, z. B. bei einem Pufferüberlauf, Framing-Fehlern oder Paritätsfehlern. Diese werden als Ausnahmen ausgelöst und können abgefangen werden, indem Sie eine weitere Schleife über der vorherigen hinzufügen, die port.readable prüft. Das funktioniert, weil so lange die Fehler nicht schwerwiegend sind, automatisch ein neuer ReadableStream erstellt wird. 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 wie unten gezeigt über einen TextDecoderStream leiten. Ein TextDecoderStream ist ein Transformationsstream
, der alle Uint8Array-Chunks erfasst 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);
}
Sie können die Speicherzuweisung steuern, wenn Sie mit einem „Bring Your Own Buffer“-Reader aus dem Stream lesen. Rufen Sie port.readable.getReader({ mode: "byob" }) auf, um die ReadableStreamBYOBReader-Schnittstelle zu erhalten, und geben Sie beim Aufrufen von read() einen eigenen ArrayBuffer an. Diese Funktion wird in der Web Serial API 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 Sie den Puffer aus value.buffer wiederverwenden können:
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, wie Sie eine bestimmte Datenmenge von einem seriellen Port lesen können:
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);
Auf einen seriellen Port schreiben
Wenn Sie Daten an ein serielles Gerät senden möchten, übergeben Sie die Daten an port.writable.getWriter().write(). Wenn der serielle Port später geschlossen werden soll, müssen Sie releaseLock() für port.writable.getWriter() aufrufen.
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 einen TextEncoderStream, der an port.writable weitergeleitet wird, wie unten gezeigt, an das Gerät.
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 Member
entsperrt sind. Das bedeutet, dass releaseLock() für den jeweiligen
Reader und Writer aufgerufen wurde.
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 erzwingt der Aufruf von reader.cancel(), dass reader.read() sofort mit { value: undefined, done: true } aufgelöst wird, 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 komplizierter, wenn Transformationsstreams verwendet werden. Rufen Sie wie zuvor reader.cancel() auf.
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 Promises readableStreamClosed und writableStreamClosed verwenden, um zu erkennen, wann port.readable und port.writable entsperrt wurden. Wenn Sie reader abbrechen, wird der Stream abgebrochen. Daher 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();
Verbindungen und Trennungen überwachen
Wenn ein serieller Port von einem USB-Gerät bereitgestellt wird, kann die Verbindung zu diesem Gerät getrennt oder wiederhergestellt 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 die serielle Portverbindung hergestellt wurde, können Sie explizit Signale abfragen und festlegen, die vom seriellen Port für die Geräteerkennung und die Flusssteuerung bereitgestellt werden. Diese Signale werden als boolesche Werte definiert. Bei einigen Geräten wie Arduino wird beispielsweise ein Programmiermodus aktiviert, wenn das DTR-Signal (Data Terminal Ready) umgeschaltet wird.
Ausgabesignale werden mit port.setSignals() und Eingabesignale mit port.getSignals() festgelegt bzw. abgerufen. Unten finden Sie Anwendungsbeispiele.
// 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 können beliebig in Chunks aufgeteilt sein. Weitere Informationen finden Sie unter Konzepte der Streams API.
Um dieses Problem zu beheben, können Sie 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 verarbeitet. Er kann eine beliebige Transformation anwenden, bevor die Daten verarbeitet werden. Stellen Sie sich das wie ein Fließband vor: Wenn ein Widget das Band entlangkommt, wird es in jedem Schritt verändert, sodass es am Ende ein voll funktionsfähiges Widget ist.
Beispiel: Erstellen Sie eine Transformationsstreamklasse, die einen Stream verarbeitet und ihn anhand von Zeilenumbrüchen in Chunks aufteilt. Die Methode transform() wird jedes Mal aufgerufen, wenn neue Daten im Stream eingehen. Sie kann die Daten entweder in die Warteschlange stellen oder für später speichern. Die Methode flush() wird aufgerufen, wenn der Stream geschlossen wird, und verarbeitet alle Daten, die noch nicht verarbeitet wurden.
Wenn Sie die Transformationsstreamklasse verwenden möchten, müssen Sie einen eingehenden Stream durch sie leiten. Im dritten Codebeispiel unter Von einem seriellen Port lesen,
wurde der ursprüngliche Eingabestream nur durch einen TextDecoderStream geleitet. Daher müssen wir pipeThrough() aufrufen, um ihn durch unseren neuen 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 für die Fehlerbehebung bei Problemen mit der Kommunikation mit seriellen Geräten die Methode tee() von port.readable, um die Streams zu teilen, die zum oder vom seriellen Gerät gesendet werden. Die beiden erstellten Streams können unabhängig voneinander verarbeitet werden. So können Sie einen zur Überprü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 entfernen, an dem sie nicht mehr interessiert ist, indem sie forget() für die SerialPort-Instanz aufruft. Bei einer Webanwendung für Bildungszwecke, die auf einem gemeinsam genutzten Computer mit vielen Geräten verwendet wird, führt eine große Anzahl von vom Nutzer erstellten Berechtigungen zu einer schlechten Nutzererfahrung.
// Voluntarily revoke access to this serial port.
await port.forget();
Da forget() ab Chrome 103 verfügbar ist, prüfen Sie mit dem folgenden Code, ob diese Funktion unterstützt wird:
if ("serial" in navigator && "forget" in SerialPort.prototype) {
// forget() is supported.
}
Tipps für Entwickler
Die Fehlerbehebung für die Web Serial API in Chrome ist mit der internen Seite about://device-log ganz einfach. Dort finden Sie alle Ereignisse im Zusammenhang mit seriellen Geräten an einem Ort.
Codelab
Im Codelab für Google-Entwickler 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.
Polyesterfaser
Unter Android ist die Unterstützung für USB-basierte serielle Ports mit der WebUSB API und dem Serial API-Polyfill 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 von einem integrierten Gerätetreiber beansprucht wurde.
Sicherheit und Datenschutz
Die Autoren der Spezifikation haben die Web Serial API nach den Grund prinzipien entwickelt und implementiert, die unter Zugriff auf leistungsstarke Webplattformfunktionen steuern definiert sind, darunter Nutzerkontrolle, Transparenz und Ergonomie. Die Verwendung dieser API ist in erster Linie 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 aktiv ein bestimmtes serielles Gerät auswählen.
Informationen zu den Sicherheitsrisiken finden Sie in den Abschnitten zu Sicherheit und Datenschutz im Web Serial API Explainer.
Feedback
Das Chrome-Team freut sich über Ihr Feedback zur Web Serial API.
Feedback zum API-Design geben
Funktioniert etwas an der API nicht wie erwartet? Oder fehlen Methoden oder Eigenschaften, die Sie zur Umsetzung Ihrer Idee benötigen?
Erstellen Sie ein Spezifikationsproblem im GitHub-Repository der Web Serial API oder fügen Sie Ihre Gedanken 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 einen Fehler unter https://new.crbug.com. Geben Sie so viele
Details wie möglich an, stellen Sie eine einfache Anleitung zur Reproduktion des Fehlers bereit und legen Sie für
Components fest auf Blink>Serial.
Unterstützung zeigen
Planen 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, sie zu unterstützen.
Senden Sie einen Tweet an @ChromiumDev mit dem Hashtag
#SerialAPI
und teilen Sie uns mit, wo und wie Sie die API verwenden.
Nützliche Links
- Spezifikation
- Tracking-Fehler
- ChromeStatus.com-Eintrag
- Blink-Komponente:
Blink>Serial
Demos
Danksagungen
Vielen Dank an Reilly Grant und Joe Medley für die Überprüfung dieses Dokuments.