La API de Web Serial permite que los sitios web se comuniquen con dispositivos seriales.
¿Qué es la API de Web Serial?
Un puerto en serie es una interfaz de comunicación bidireccional que permite enviar y recibir datos byte por byte.
La API de Web Serial proporciona una forma para que los sitios web lean y escriban en un dispositivo serial con JavaScript. Los dispositivos seriales se conectan a través de un puerto serial en el sistema del usuario o a través de dispositivos USB y Bluetooth extraíbles que emulan un puerto serial.
En otras palabras, la API de Web Serial une la Web y el mundo físico, ya que permite que los sitios web se comuniquen con dispositivos seriales, como microcontroladores e impresoras 3D.
Esta API también es un excelente complemento para WebUSB, ya que los sistemas operativos requieren que las aplicaciones se comuniquen con algunos puertos seriales a través de su API serial de nivel superior en lugar de la API de USB de nivel inferior.
Casos de uso sugeridos
En los sectores educativo, industrial y de aficionados, los usuarios conectan dispositivos periféricos a sus computadoras. Estos dispositivos suelen controlarse con microcontroladores a través de una conexión serial que utiliza software personalizado. Algunos softwares personalizados para controlar estos dispositivos se compilan con tecnología web:
En algunos casos, los sitios web se comunican con el dispositivo a través de una aplicación de agente que los usuarios instalaron de forma manual. En otras, la aplicación se entrega en un paquete a través de un framework como Electron. En otros casos, el usuario debe realizar un paso adicional, como copiar una aplicación compilada en el dispositivo a través de una unidad flash USB.
En todos estos casos, la experiencia del usuario mejorará gracias a la comunicación directa entre el sitio web y el dispositivo que controla.
Estado actual
Paso | Estado |
---|---|
1. Crea una explicación | Completar |
2. Crea el borrador inicial de la especificación | Completar |
3. Recopila comentarios y realiza iteraciones en el diseño | Completar |
4. Prueba de origen | Completar |
5. Lanzamiento | Completar |
Usa la API de Web Serial
Detección de características
Para verificar si se admite la API de Web Serial, usa el siguiente código:
if ("serial" in navigator) {
// The Web Serial API is supported.
}
Cómo abrir un puerto en serie
La API de Web Serial está diseñada para ser asíncrona. Esto evita que la IU del sitio web se bloquee mientras espera la entrada, lo que es importante porque los datos seriales se pueden recibir en cualquier momento, lo que requiere una forma de escucharlos.
Para abrir un puerto en serie, primero accede a un objeto SerialPort
. Para ello, puedes solicitarle al usuario que seleccione un solo puerto serie llamando a navigator.serial.requestPort()
en respuesta a un gesto del usuario, como un toque o un clic del mouse, o bien elegir uno de navigator.serial.getPorts()
, que devuelve una lista de los puertos serie a los que se le otorgó acceso al sitio web.
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();
La función navigator.serial.requestPort()
toma un objeto literal opcional que define filtros. Se usan para hacer coincidir cualquier dispositivo serial conectado a través de USB con un proveedor de USB obligatorio (usbVendorId
) y con identificadores de productos USB opcionales (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();

Si llamas a requestPort()
, se le pedirá al usuario que seleccione un dispositivo y se devolverá un objeto SerialPort
. Una vez que tengas un objeto SerialPort
, llamar a port.open()
con la velocidad de transmisión deseada abrirá el puerto serie. El miembro del diccionario baudRate
especifica la velocidad con la que se envían los datos a través de una línea serial. Se expresa en unidades de bits por segundo (bps). Consulta la documentación de tu dispositivo para conocer el valor correcto, ya que todos los datos que envíes y recibas serán ilegibles si se especifica de forma incorrecta. En el caso de algunos dispositivos USB y Bluetooth que emulan un puerto serie, este valor se puede establecer de forma segura en cualquier valor, ya que la emulación lo ignora.
// 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 });
También puedes especificar cualquiera de las siguientes opciones cuando abras un puerto en serie. Estas opciones son opcionales y tienen valores predeterminados convenientes.
dataBits
: Es la cantidad de bits de datos por fotograma (7 u 8).stopBits
: Es la cantidad de bits de parada al final de un fotograma (1 o 2).parity
: Es el modo de paridad ("none"
,"even"
o"odd"
).bufferSize
: Es el tamaño de los búferes de lectura y escritura que se deben crear (debe ser inferior a 16 MB).flowControl
: Es el modo de control de flujo ("none"
o"hardware"
).
Leer desde un puerto en serie
La API de Streams controla los flujos de entrada y salida en la API de Web Serial.
En este artículo, apenas se abordan los aspectos básicos de los flujos y su procesamiento.Después de que se establece la conexión del puerto serie, las propiedades readable
y writable
del objeto SerialPort
devuelven un ReadableStream y un WritableStream. Se usarán para recibir datos del dispositivo serial y enviarlos a él. Ambos usan instancias de Uint8Array
para la transferencia de datos.
Cuando llegan datos nuevos del dispositivo serial, port.readable.getReader().read()
devuelve dos propiedades de forma asíncrona: value
y un valor booleano done
. Si done
es verdadero, el puerto serie se cerró o no hay más datos entrantes. La llamada a port.readable.getReader()
crea un lector y bloquea readable
para él. Mientras readable
está bloqueado, no se puede cerrar el puerto serie.
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);
}
En algunas condiciones, como desbordamiento de búfer, errores de encuadre o errores de paridad, pueden producirse algunos errores de lectura del puerto serie no fatales. Estas se arrojan como excepciones y se pueden detectar agregando otro bucle sobre el anterior que verifique port.readable
. Esto funciona porque, mientras los errores no sean fatales, se creará automáticamente un nuevo ReadableStream. Si se produce un error grave, como la extracción del dispositivo serial, port.readable
se convierte en nulo.
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.
}
}
Si el dispositivo serial envía texto, puedes canalizar port.readable
a través de un TextDecoderStream
como se muestra a continuación. Un TextDecoderStream
es un flujo de transformación que toma todos los fragmentos Uint8Array
y los convierte en cadenas.
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);
}
Puedes controlar cómo se asigna la memoria cuando lees desde la transmisión con un lector "Bring Your Own Buffer". Llama a port.readable.getReader({ mode: "byob" })
para obtener la interfaz ReadableStreamBYOBReader y proporciona tu propio ArrayBuffer
cuando llames a read()
. Ten en cuenta que la API de Web Serial admite esta función en Chrome 106 o versiones posteriores.
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()...
}
}
Este es un ejemplo de cómo reutilizar el búfer fuera de 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`.
}
Este es otro ejemplo de cómo leer una cantidad específica de datos de un puerto serie:
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);
Escribe en un puerto en serie
Para enviar datos a un dispositivo serial, pasa datos a port.writable.getWriter().write()
. Se requiere llamar a releaseLock()
en port.writable.getWriter()
para que el puerto en serie se cierre más adelante.
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();
Envía texto al dispositivo a través de un TextEncoderStream
canalizado a port.writable
, como se muestra a continuación.
const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
const writer = textEncoder.writable.getWriter();
await writer.write("hello");
Cierra un puerto en serie
port.close()
cierra el puerto serie si sus miembros readable
y writable
están desbloqueados, lo que significa que se llamó a releaseLock()
para su respectivo lector y escritor.
await port.close();
Sin embargo, cuando se leen datos de forma continua desde un dispositivo serial con un bucle, port.readable
siempre estará bloqueado hasta que se produzca un error. En este caso, llamar a reader.cancel()
forzará a reader.read()
a resolverse de inmediato con { value: undefined, done: true }
y, por lo tanto, permitirá que el bucle llame a 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;
});
Cerrar un puerto serie es más complicado cuando se usan transform streams. Llama a reader.cancel()
como antes.
Luego, llama a writer.close()
y port.close()
. Esto propaga los errores a través de las transmisiones de transformación al puerto en serie subyacente. Dado que la propagación de errores no se produce de inmediato, debes usar las promesas readableStreamClosed
y writableStreamClosed
creadas anteriormente para detectar cuándo se desbloquearon port.readable
y port.writable
. Si cancelas el reader
, se anulará la transmisión. Por eso, debes detectar y omitir el error resultante.
// 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();
Escucha la conexión y desconexión
Si un dispositivo USB proporciona un puerto serie, ese dispositivo se puede conectar o desconectar del sistema. Cuando se le otorga permiso al sitio web para acceder a un puerto serie, debe supervisar los eventos connect
y disconnect
.
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.
});
Cómo controlar los indicadores
Después de establecer la conexión del puerto serie, puedes consultar y configurar de forma explícita los indicadores expuestos por el puerto serie para la detección de dispositivos y el control de flujo. Estos indicadores se definen como valores booleanos. Por ejemplo, algunos dispositivos, como Arduino, entrarán en modo de programación si se activa el indicador Data Terminal Ready (DTR).
La configuración de los indicadores de salida y la obtención de los indicadores de entrada se realizan llamando a port.setSignals()
y port.getSignals()
, respectivamente. Consulta los ejemplos de uso a continuación.
// 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}`);
Transformación de transmisiones
Cuando recibas datos del dispositivo serial, no necesariamente obtendrás todos los datos a la vez. Se puede dividir en fragmentos de forma arbitraria. Para obtener más información, consulta Conceptos de la API de Streams.
Para solucionar este problema, puedes usar algunos flujos de transformación integrados, como TextDecoderStream
, o crear tu propio flujo de transformación que te permita analizar el flujo entrante y devolver datos analizados. La transmisión de transformación se encuentra entre el dispositivo serial y el bucle de lectura que consume la transmisión. Puede aplicar una transformación arbitraria antes de que se consuman los datos. Piensa en ello como una línea de ensamblaje: a medida que un widget avanza por la línea, cada paso lo modifica, de modo que, cuando llega a su destino final, es un widget completamente funcional.

Por ejemplo, considera cómo crear una clase de transmisión de transformación que consuma una transmisión y la divida en fragmentos según los saltos de línea. Se llama a su método transform()
cada vez que la transmisión recibe datos nuevos. Puede poner los datos en cola o guardarlos para más tarde. Se llama al método flush()
cuando se cierra la transmisión, y controla los datos que aún no se procesaron.
Para usar la clase de transmisión de transformación, debes canalizar una transmisión entrante a través de ella. En el tercer ejemplo de código de Read from a serial port, el flujo de entrada original solo se canalizó a través de un TextDecoderStream
, por lo que debemos llamar a pipeThrough()
para canalizarlo a través de nuestro nuevo LineBreakTransformer
.
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();
Para depurar problemas de comunicación con dispositivos seriales, usa el método tee()
de port.readable
para dividir los flujos que van hacia el dispositivo serial o desde él. Las dos transmisiones creadas se pueden consumir de forma independiente, lo que te permite imprimir una en la consola para su inspección.
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.
Cómo revocar el acceso a un puerto en serie
El sitio web puede limpiar los permisos para acceder a un puerto en serie que ya no le interesa conservar llamando a forget()
en la instancia SerialPort
. Por ejemplo, en el caso de una aplicación web educativa que se usa en una computadora compartida con muchos dispositivos, una gran cantidad de permisos acumulados generados por el usuario crea una experiencia del usuario deficiente.
// Voluntarily revoke access to this serial port.
await port.forget();
Como forget()
está disponible en Chrome 103 o versiones posteriores, verifica si esta función es compatible con lo siguiente:
if ("serial" in navigator && "forget" in SerialPort.prototype) {
// forget() is supported.
}
Sugerencias para desarrolladores
Depurar la API de Web Serial en Chrome es fácil con la página interna about://device-log
, en la que puedes ver todos los eventos relacionados con dispositivos seriales en un solo lugar.

Codelab
En el codelab de Google Developers, usarás la API de Web Serial para interactuar con una placa BBC micro:bit y mostrar imágenes en su matriz de LED de 5 x 5.
Navegadores compatibles
La API de Web Serial está disponible en todas las plataformas para computadoras (ChromeOS, Linux, macOS y Windows) en Chrome 89.
Polyfill
En Android, es posible admitir puertos en serie basados en USB con la API de WebUSB y el polyfill de la API de Serial. Este polyfill se limita al hardware y a las plataformas en los que se puede acceder al dispositivo a través de la API de WebUSB, ya que no se reclamó con un controlador de dispositivo integrado.
Seguridad y privacidad
Los autores de la especificación diseñaron y, luego, implementaron la API de Web Serial con los principios fundamentales definidos en Controlling Access to Powerful Web Platform Features, incluidos el control del usuario, la transparencia y la ergonomía. La capacidad de usar esta API se limita principalmente a un modelo de permisos que otorga acceso a un solo dispositivo serial a la vez. En respuesta a una instrucción del usuario, este debe seguir pasos activos para seleccionar un dispositivo serial en particular.
Para comprender las compensaciones de seguridad, consulta las secciones de seguridad y privacidad del documento explicativo de la API de Web Serial.
Comentarios
Al equipo de Chrome le encantaría conocer tus opiniones y experiencias con la API de Web Serial.
Cuéntanos sobre el diseño de la API
¿Hay algo en la API que no funciona como se espera? ¿O faltan métodos o propiedades que necesitas para implementar tu idea?
Informa un problema de especificación en el repositorio de GitHub de la API de Web Serial o agrega tus comentarios a un problema existente.
Informa un problema con la implementación
¿Encontraste un error en la implementación de Chrome? ¿O la implementación es diferente de las especificaciones?
Informa el error en https://new.crbug.com. Asegúrate de incluir la mayor cantidad de detalles posible, proporcionar instrucciones sencillas para reproducir el error y configurar Components como Blink>Serial
.
Mostrar apoyo
¿Planeas usar la API de Web Serial? Tu apoyo público ayuda al equipo de Chrome a priorizar funciones y muestra a otros proveedores de navegadores lo importante que es admitirlas.
Envía un tweet a @ChromiumDev con el hashtag #SerialAPI
y cuéntanos dónde y cómo lo usas.
Vínculos útiles
- Especificación
- Error de seguimiento
- Entrada de ChromeStatus.com
- Componente Blink:
Blink>Serial
Demostraciones
Agradecimientos
Gracias a Reilly Grant y Joe Medley por sus revisiones de este artículo. Foto de fábrica de aviones de Birmingham Museums Trust en Unsplash.