De Web Serial API stelt websites in staat om met seriële apparaten te communiceren.
Wat is de Web Serial API?
Een seriële poort is een bidirectionele communicatie-interface waarmee gegevens byte voor byte kunnen worden verzonden en ontvangen.
De Web Serial API biedt websites de mogelijkheid om met JavaScript gegevens van en naar een serieel apparaat te lezen en te schrijven. Seriële apparaten worden aangesloten via een seriële poort op het systeem van de gebruiker of via verwijderbare USB- en Bluetooth-apparaten die een seriële poort emuleren.
Met andere woorden, de Web Serial API slaat een brug tussen het web en de fysieke wereld door websites in staat te stellen te communiceren met seriële apparaten, zoals microcontrollers en 3D-printers.
Deze API is ook een uitstekende aanvulling op WebUSB , omdat besturingssystemen vereisen dat applicaties met bepaalde seriële poorten communiceren via hun eigen, hogergelegen seriële API in plaats van de laaggelegen USB API.
Voorgestelde gebruiksscenario's
In het onderwijs, bij hobbyisten en in de industrie sluiten gebruikers randapparatuur aan op hun computers. Deze apparaten worden vaak aangestuurd door microcontrollers met een seriële verbinding die wordt gebruikt door aangepaste software. Sommige van deze aangepaste software is ontwikkeld met behulp van webtechnologie.
In sommige gevallen communiceren websites met het apparaat via een agentapplicatie die gebruikers handmatig installeren. In andere gevallen wordt de applicatie geleverd als een gecompileerde applicatie via een framework zoals Electron. En in weer andere gevallen moet de gebruiker een extra stap uitvoeren, zoals het kopiëren van een gecompileerde applicatie naar het apparaat via een USB-stick.
In al deze gevallen wordt de gebruikerservaring verbeterd door directe communicatie tussen de website en het apparaat dat ermee wordt aangestuurd.
Huidige status
| Stap | Status |
|---|---|
| 1. Maak een uitleg | Compleet |
| 2. Stel een eerste concept van de specificatie op. | Compleet |
| 3. Verzamel feedback en pas het ontwerp aan | Compleet |
| 4. Oorsprongsproef | Compleet |
| 5. Lancering | Compleet |
De Web Serial API gebruiken
Kenmerkdetectie
Om te controleren of de Web Serial API wordt ondersteund, gebruikt u:
if ("serial" in navigator) {
// The Web Serial API is supported.
}
Open een seriële poort.
De Web Serial API is van nature asynchroon. Dit voorkomt dat de gebruikersinterface van de website vastloopt in afwachting van invoer, wat belangrijk is omdat seriële data op elk moment kan worden ontvangen en er dus een manier nodig is om ernaar te luisteren.
Om een seriële poort te openen, moet u eerst een SerialPort -object benaderen. Hiervoor kunt u de gebruiker vragen een seriële poort te selecteren door navigator.serial.requestPort() aan te roepen als reactie op een gebruikersactie zoals aanraken of een muisklik, of u kunt er een kiezen uit navigator.serial.getPorts() , die een lijst retourneert van seriële poorten waartoe de website toegang heeft gekregen.
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();
De functie navigator.serial.requestPort() accepteert een optioneel object dat filters definieert. Deze filters worden gebruikt om elk serieel apparaat te matchen dat via USB is aangesloten en dat een verplichte USB-fabrikant ( usbVendorId ) en optionele USB-product-ID's ( usbProductId ) heeft.
// 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();

Door requestPort() aan te roepen, wordt de gebruiker gevraagd een apparaat te selecteren en wordt een SerialPort object geretourneerd. Zodra u een SerialPort -object hebt, opent u de seriële poort port.open() aan te roepen met de gewenste baudrate. Het dictionary-lid baudRate specificeert hoe snel gegevens over een seriële lijn worden verzonden. Dit wordt uitgedrukt in bits per seconde (bps). Raadpleeg de documentatie van uw apparaat voor de juiste waarde, aangezien alle gegevens die u verzendt en ontvangt onleesbaar zullen zijn als deze waarde onjuist is ingesteld. Voor sommige USB- en Bluetooth-apparaten die een seriële poort emuleren, kan deze waarde veilig op elke waarde worden ingesteld, omdat deze door de emulatie wordt genegeerd.
// 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 });
U kunt bij het openen van een seriële poort ook een van de onderstaande opties specificeren. Deze opties zijn optioneel en hebben handige standaardwaarden .
-
dataBits: The number of data bits per frame (either 7 or 8). -
stopBits: Het aantal stopbits aan het einde van een frame (1 of 2). -
parity: De pariteitsmodus (ofwel"none","even"of"odd"). -
bufferSize: De grootte van de lees- en schrijfbuffers die moeten worden aangemaakt (moet kleiner zijn dan 16 MB). -
flowControl: De flowcontrolmodus (ofwel"none"of"hardware").
Lezen vanaf een seriële poort
Invoer- en uitvoerstromen in de Web Serial API worden verwerkt door de Streams API.
Nadat de seriële poortverbinding tot stand is gebracht, retourneren de eigenschappen readable en writable van het SerialPort object respectievelijk een `ReadableStream` en een `WritableStream` . Deze worden gebruikt om gegevens van en naar het seriële apparaat te ontvangen en te verzenden. Beide gebruiken Uint8Array instanties voor gegevensoverdracht.
Wanneer er nieuwe gegevens binnenkomen via het seriële apparaat, port.readable.getReader().read() asynchroon twee eigenschappen: de value en een boolean die aangeeft of de done . Als done waar is, is de seriële poort gesloten of komen er geen gegevens meer binnen. Door port.readable.getReader() ` aan te roepen, wordt een lezer aangemaakt en wordt readable eraan gekoppeld. Zolang readable is gekoppeld , kan de seriële poort niet worden gesloten.
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);
}
Some non-fatal serial port read errors can happen under some conditions such as buffer overflow, framing errors, or parity errors. Those are thrown as exceptions and can be caught by adding another loop on top of the previous one that checks port.readable . This works because as long as the errors are non-fatal, a new ReadableStream is created automatically. If a fatal error occurs, such as the serial device being removed, then port.readable becomes 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.
}
}
If the serial device sends text back, you can pipe port.readable through a TextDecoderStream as shown below. A TextDecoderStream is a transform stream that grabs all Uint8Array chunks and converts them to 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);
}
Je kunt de geheugenallocatie tijdens het lezen uit de stream beheren met behulp van een "Bring Your Own Buffer"-lezer. Roep port.readable.getReader({ mode: "byob" }) aan om de `ReadableStreamBYOBReader` -interface te verkrijgen en geef je eigen ArrayBuffer op bij het aanroepen van read() . Houd er rekening mee dat de Web Serial API deze functie ondersteunt in Chrome 106 of later.
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 is een voorbeeld van hoe je de buffer uit value.buffer opnieuw kunt gebruiken:
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 is nog een voorbeeld van hoe je een specifieke hoeveelheid gegevens van een seriële poort kunt lezen:
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);
Schrijf naar een seriële poort
Om gegevens naar een serieel apparaat te verzenden, moet u de gegevens doorgeven aan port.writable.getWriter().write() . Het aanroepen releaseLock() op port.writable.getWriter() is vereist om de seriële poort later te kunnen sluiten.
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();
Stuur tekst naar het apparaat via een TextEncoderStream die is doorgestuurd naar port.writable zoals hieronder weergegeven.
const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
const writer = textEncoder.writable.getWriter();
await writer.write("hello");
Sluit een seriële poort
port.close() sluit de seriële poort als readable en writable leden ervan ontgrendeld zijn, wat betekent dat releaseLock() is aangeroepen voor de respectievelijke lezer en schrijver.
await port.close();
Wanneer er echter continu gegevens van een serieel apparaat worden gelezen met behulp van een lus, blijft port.readable altijd vergrendeld totdat er een fout optreedt. In dit geval zorgt het aanroepen reader.cancel() ervoor dat reader.read() onmiddellijk wordt opgelost met { value: undefined, done: true } , waardoor de lus reader.releaseLock() kan aanroepen.
// 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;
});
Het sluiten van een seriële poort is ingewikkelder bij gebruik van transformatiestreams . Roep reader.cancel() aan zoals eerder. Roep vervolgens writer.close() en port.close() aan. Dit verspreidt fouten via de transformatiestreams naar de onderliggende seriële poort. Omdat foutpropagatie niet onmiddellijk plaatsvindt, moet u de eerder gemaakte promises readableStreamClosed en writableStreamClosed gebruiken om te detecteren wanneer port.readable en port.writable zijn ontgrendeld. Het annuleren van de reader zorgt ervoor dat de stream wordt afgebroken; daarom moet u de resulterende fout opvangen en negeren.
// 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();
Luister naar verbinding en ontkoppeling.
Als een USB-apparaat een seriële poort beschikbaar stelt, kan dat apparaat met het systeem verbonden of losgekoppeld worden. Zodra de website toestemming heeft gekregen om toegang te krijgen tot een seriële poort, moet deze de connect en disconnect registreren.
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.
});
Verwerk signalen
Nadat de seriële poortverbinding tot stand is gebracht, kunt u expliciet signalen opvragen en instellen die door de seriële poort worden aangeboden voor apparaatdetectie en flow control. Deze signalen worden gedefinieerd als booleaanse waarden. Sommige apparaten, zoals Arduino, gaan bijvoorbeeld naar de programmeermodus als het Data Terminal Ready (DTR)-signaal wordt omgeschakeld.
Het instellen van uitgangssignalen en het ontvangen van ingangssignalen gebeurt respectievelijk door de functies port.setSignals() en port.getSignals() aan te roepen. Zie onderstaande gebruiksvoorbeelden.
// 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 transformeren
Wanneer u gegevens van het seriële apparaat ontvangt, krijgt u niet noodzakelijkerwijs alle gegevens in één keer. De gegevens kunnen willekeurig in stukken worden verzonden. Zie de concepten van de Streams API voor meer informatie.
Om dit probleem op te lossen, kunt u gebruikmaken van ingebouwde transformatiestreams zoals TextDecoderStream , of uw eigen transformatiestream creëren waarmee u de binnenkomende stream kunt parsen en geparseerde gegevens kunt retourneren. De transformatiestream bevindt zich tussen het seriële apparaat en de leeslus die de stream verwerkt. Deze kan een willekeurige transformatie toepassen voordat de gegevens worden verwerkt. Zie het als een lopende band: naarmate een component de band afgaat, wijzigt elke stap in de band de component, zodat deze, tegen de tijd dat hij zijn eindbestemming bereikt, een volledig functionerende component is.

Neem bijvoorbeeld eens een transformatieklasse voor streams die een stream consumeert en deze opdeelt in stukken op basis van regeleinden. De transform() `-methode wordt aangeroepen telkens wanneer de stream nieuwe gegevens ontvangt. Deze kan de gegevens in een wachtrij plaatsen of opslaan voor later gebruik. De ` flush() -methode wordt aangeroepen wanneer de stream wordt gesloten en verwerkt alle gegevens die nog niet zijn verwerkt.
Om de TransformStream-klasse te gebruiken, moet je een inkomende stream erdoorheen leiden. In het derde codevoorbeeld onder 'Lezen van een seriële poort' werd de oorspronkelijke invoerstream alleen door een TextDecoderStream geleid, dus we moeten pipeThrough() aanroepen om deze door onze nieuwe LineBreakTransformer te leiden.
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();
Voor het debuggen van problemen met seriële apparaatcommunicatie kunt u de ` tee() -methode van port.readable gebruiken om de datastromen van en naar het seriële apparaat te splitsen. De twee gecreëerde datastromen kunnen onafhankelijk van elkaar worden verwerkt, waardoor u er één naar de console kunt afdrukken ter inspectie.
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.
De toegang tot een seriële poort intrekken
De website kan de toegangsrechten voor een seriële poort die niet langer nodig is, opschonen door de functie forget() aan te roepen op de SerialPort instantie. Bijvoorbeeld, voor een educatieve webapplicatie die wordt gebruikt op een gedeelde computer met veel apparaten, zorgt een groot aantal verzamelde, door gebruikers gegenereerde toegangsrechten voor een slechte gebruikerservaring.
// Voluntarily revoke access to this serial port.
await port.forget();
Aangezien forget() beschikbaar is in Chrome 103 of later, kunt u controleren of deze functie wordt ondersteund met het volgende:
if ("serial" in navigator && "forget" in SerialPort.prototype) {
// forget() is supported.
}
Ontwikkelaarstips
Het debuggen van de Web Serial API in Chrome is eenvoudig met de interne pagina about://device-log waar u alle seriële apparaatgerelateerde gebeurtenissen op één plek kunt bekijken.

Codelab
In de Google Developer codelab ga je de Web Serial API gebruiken om te communiceren met een BBC micro:bit- bord en afbeeldingen weer te geven op de 5x5 LED-matrix.
Browserondersteuning
De Web Serial API is beschikbaar op alle desktopplatforms (ChromeOS, Linux, macOS en Windows) in Chrome 89.
Polyfill
Op Android is ondersteuning voor USB-gebaseerde seriële poorten mogelijk met behulp van de WebUSB API en de Serial API polyfill . Deze polyfill is beperkt tot hardware en platforms waar het apparaat toegankelijk is via de WebUSB API omdat het niet is geclaimd door een ingebouwde apparaatdriver.
Beveiliging en privacy
De auteurs van de specificaties hebben de Web Serial API ontworpen en geïmplementeerd volgens de kernprincipes die zijn vastgelegd in Controlling Access to Powerful Web Platform Features , waaronder gebruikerscontrole, transparantie en ergonomie. De mogelijkheid om deze API te gebruiken wordt voornamelijk beperkt door een permissiemodel dat slechts toegang verleent aan één serieel apparaat tegelijk. Na een prompt van de gebruiker moet de gebruiker actief een specifiek serieel apparaat selecteren.
Om de afwegingen op het gebied van beveiliging te begrijpen, raadpleegt u de secties over beveiliging en privacy in de Web Serial API Explainer.
Feedback
Het Chrome-team hoort graag uw mening en ervaringen met de Web Serial API.
Vertel ons iets over het API-ontwerp.
Is there something about the API that doesn't work as expected? Or are there missing methods or properties that you need to implement your idea?
Dien een specificatieprobleem in op de GitHub-repository van de Web Serial API of voeg je mening toe aan een bestaand probleem.
Meld een probleem met de implementatie.
Heb je een bug gevonden in de implementatie van Chrome? Of wijkt de implementatie af van de specificatie?
Meld een bug op https://new.crbug.com . Vermeld zoveel mogelijk details, geef eenvoudige instructies voor het reproduceren van de bug en stel Components in op Blink>Serial .
Toon je steun
Ben je van plan de Web Serial API te gebruiken? Jouw publieke steun helpt het Chrome-team bij het prioriteren van functies en laat andere browserleveranciers zien hoe belangrijk het is om deze te ondersteunen.
Stuur een tweet naar @ChromiumDev met de hashtag #SerialAPI en laat ons weten waar en hoe je het gebruikt.
Handige links
- Specificatie
- Het volgen van een bug
- ChromeStatus.com-item
- Blink-component:
Blink>Serial
Demo's
Dankbetuigingen
Met dank aan Reilly Grant en Joe Medley voor hun beoordelingen van dit document.