Back

Cosas interesantes que puedes hacer con la Web Serial API

Cosas interesantes que puedes hacer con la Web Serial API

La Web Serial API permite que una página web abra un flujo de bytes de lectura/escritura hacia un dispositivo físico que utiliza comunicación serie — un adaptador USB a serie, una placa de desarrollo para microcontroladores, una impresora 3D o un dispositivo Bluetooth Classic emparejado — sin necesidad de una aplicación nativa ni de una extensión del navegador. Se enmarca en la misma línea de APIs de acceso a hardware del navegador que MediaDevices (cámaras y micrófonos), WebUSB (interfaces USB sin procesar) y WebHID (mandos de juego y teclados): la página solicita acceso, el navegador muestra un diálogo de permisos y el usuario selecciona el dispositivo.

Antes de esbozar un proyecto de fin de semana, conviene conocer el soporte disponible, ya que este define los límites. Web Serial está soportada en escritorio en Chrome 89+, Edge 89+, Opera 75+ y Firefox 151+, según la tabla de compatibilidad de navegadores de MDN; Safari no tiene soporte anunciado. La API requiere un contexto seguro (HTTPS o localhost) y un gesto del usuario para llamar a requestPort() — no es posible solicitar un puerto al cargar la página. Este artículo describe seis categorías concretas de proyectos que puedes construir sobre esta base, cada una con el código específico de Web Serial, el emparejamiento de hardware y el error más común que suele aparecer primero.

Puntos clave

  • Web Serial está soportada en escritorio en Chrome 89+, Edge 89+, Opera 75+ y Firefox 151+; Safari no tiene soporte anunciado, y la API requiere tanto un contexto seguro como un gesto del usuario para llamar a requestPort().
  • Web Serial existe de forma independiente a WebUSB porque los controladores serie del sistema operativo se apropian de los adaptadores USB a serie antes de que WebUSB pueda acceder a ellos, por lo que los dispositivos que se enumeran como puertos COM o nodos /dev/tty necesitan Web Serial.
  • Llamar a reader.releaseLock() antes de port.close() evita los errores de “puerto ya abierto” que provocan los bloqueos de flujo no liberados al reconectar.
  • Desde Chrome 117, Web Serial puede comunicarse con dispositivos Bluetooth Classic emparejados mediante RFCOMM/SPP a través de allowedBluetoothServiceClassIds y filtros bluetoothServiceClassId — serie inalámbrica sin necesidad de una llamada separada a WebBluetooth.
  • Chrome 130 añadió SerialPort.connected, un booleano para distinguir un puerto que está físicamente presente de uno que la aplicación ha cerrado.

El patrón mínimo de conexión, lectura, escritura y limpieza

Todo proyecto con Web Serial parte de los mismos cuatro pasos: solicitar un puerto, abrirlo a una velocidad en baudios, leer o escribir a través del flujo, y limpiar liberando el bloqueo antes de cerrar. El fragmento de código a continuación es la referencia a la que apunta el resto de este artículo. Utiliza el argumento opcional filters para acotar el selector de puertos del navegador por ID de proveedor y producto USB — según la definición de SerialPortRequestOptions en la especificación Web Serial del WICG, esto excluye los puertos serie no relacionados para que el usuario solo vea los dispositivos relevantes.

// Debe ejecutarse dentro de un manejador de gesto del usuario (p. ej., un listener de clic).
const port = await navigator.serial.requestPort({
  filters: [{ usbVendorId: 0x2341 }], // opcional; omitir para "cualquier puerto"
});
await port.open({ baudRate: 115200 });

const reader = port.readable.getReader();
try {
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;          // el reader fue cancelado
    console.log(value);       // value es un fragmento Uint8Array
  }
} finally {
  reader.releaseLock();       // liberar ANTES de cerrar
  await port.close();
}

No llamar a reader.releaseLock() antes de port.close() es una causa habitual de errores de “puerto ya abierto” al reconectar — el bloqueo retiene el flujo legible, y close() es rechazado mientras el flujo sigue bloqueado, según los pasos de SerialPort.close() en la especificación. Para protocolos de texto, canaliza los flujos a través de TextEncoderStream y TextDecoderStream en lugar de decodificar manualmente los fragmentos Uint8Array sin procesar; la guía de Web Serial de MDN cubre los detalles de codificación.

Ejemplos de la Web Serial API: seis proyectos para un fin de semana

Las seis categorías de proyectos más potentes con Web Serial son: herramientas de depuración intensivas en lectura, clientes de protocolo intensivos en escritura, bucles de transmisión de larga duración, visualización en tiempo real, pantallas renderizadas desde el navegador e interfaces de configuración de dispositivos. Cada entrada a continuación aísla la lógica específica de Web Serial en lugar de recorrer una aplicación completa.

1. Un monitor serie basado en navegador

Un monitor serie es la herramienta de depuración intensiva en lectura que reemplaza el Monitor Serie del IDE de Arduino o screen /dev/ttyUSB0 con una pestaña del navegador. Abre un puerto, transmite los bytes entrantes a través de un decodificador y añade las líneas decodificadas al DOM. Es el proyecto más sencillo de esta lista y el mejor para empezar, ya que ejercita el bucle de lectura sin las complicaciones de un protocolo de escritura.

Emparejamiento de hardware/protocolo: cualquier dispositivo UART — un Arduino, un ESP32, un adaptador USB a serie — que emita texto delimitado por saltos de línea.

const port = await navigator.serial.requestPort();
await port.open({ baudRate: 9600 });

const decoder = new TextDecoderStream();
port.readable.pipeTo(decoder.writable);
const reader = decoder.readable.getReader();

let buffer = "";
while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += value;
  const lines = buffer.split("\n");
  buffer = lines.pop() ?? "";        // conservar la última línea parcial
  lines.forEach(line => appendToLog(line));
}

Error común: los datos serie llegan con límites de fragmento arbitrarios, no como líneas completas — una sola llamada a read() puede devolver media línea o tres líneas, así que acumula en el buffer hasta encontrar un delimitador antes de renderizar.

2. Un flasheador de firmware

Un flasheador de firmware escribe un binario compilado en la memoria flash de un microcontrolador a través del puerto serie, de la misma forma en que una herramienta tipo esptool en el navegador lo hace para chips ESP32. Es intensivo en escritura y está ligado a un protocolo: antes de que se transfiera cualquier carga útil, el chip debe entrar en su modo bootloader, y los datos se encapsulan según el protocolo de flasheo del chip.

Emparejamiento de hardware/protocolo: ESP32/ESP8266 a través del protocolo bootloader serie con encapsulado SLIP; la secuencia de entrada se controla mediante las señales de control DTR y RTS.

const port = await navigator.serial.requestPort();
await port.open({ baudRate: 115200 });

// Alternar las señales de control para forzar al ESP al modo de descarga.
// Consultar la secuencia de modo de arranque documentada por Espressif.
await port.setSignals({ dataTerminalReady: false, requestToSend: true });
await port.setSignals({ dataTerminalReady: true, requestToSend: false });
// ...ahora enviar los comandos del bootloader encapsulados a través de port.writable

Los flasheadores de estilo ESPTool en el navegador deben alternar las señales de control DTR y RTS en una secuencia específica para forzar al chip al modo de descarga antes de enviar cualquier comando de escritura; Espressif documenta el comportamiento de las señales del modo de arranque, y Web Serial expone los controles a través de SerialPort.setSignals().

Error común: si se omite la secuencia de bootloader con DTR/RTS, el chip permanece en su firmware de aplicación en lugar de entrar en el bootloader serie, por lo que el flasheo no puede completarse correctamente.

3. Un streamer de trabajos G-code

Un streamer de G-code envía un trabajo de impresión o corte a una impresora 3D o máquina CNC línea a línea, esperando a que el firmware confirme cada línea antes de enviar la siguiente. Es un bucle de escritura de larga duración con control de flujo, lo que lo convierte en un proyecto considerablemente más complejo que un escritor sin confirmación.

Emparejamiento de hardware/protocolo: firmware Marlin o GRBL a través de USB serie, intercambiando G-code basado en líneas con confirmaciones ok.

const encoder = new TextEncoderStream();
encoder.readable.pipeTo(port.writable);
const writer = encoder.writable.getWriter();

const decoder = new TextDecoderStream();
port.readable.pipeTo(decoder.writable);
const reader = decoder.readable.getReader();

for (const line of gcodeLines) {
  await writer.write(line + "\n");
  // Bloquear hasta que el firmware confirme que procesó la línea.
  let ack = "";
  while (!ack.includes("ok")) {
    const { value } = await reader.read();
    ack += value;
  }
}

Los streamers de G-code deben esperar la confirmación ok de la impresora después de cada línea antes de enviar la siguiente; la referencia de G-code de RepRap define este protocolo de intercambio, y saturar el buffer serie sin respetarlo desborda la cola de comandos del firmware a mitad del trabajo.

Error común: el ok puede llegar intercalado con informes de temperatura y otras líneas no solicitadas, así que busca el token en lugar de asumir que la siguiente línea leída es la confirmación.

4. Un panel de telemetría en tiempo real

Un panel de telemetría lee un flujo continuo de datos de sensores desde un puerto serie y lo renderiza como un gráfico en vivo en el navegador — temperatura, voltaje, ejes del acelerómetro, posiciones GPS. La parte de Web Serial es simplemente un bucle de lectura; el valor está en canalizar los valores decodificados directamente a una biblioteca de gráficos que se ejecuta en la misma pestaña.

Emparejamiento de hardware/protocolo: cualquier microcontrolador con sensores que emita líneas CSV, o un módulo GPS que emita sentencias NMEA 0183.

const decoder = new TextDecoderStream();
port.readable.pipeTo(decoder.writable);
const reader = decoder.readable.getReader();

let buffer = "";
while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += value;
  let nl;
  while ((nl = buffer.indexOf("\n")) >= 0) {
    const [t, v] = buffer.slice(0, nl).split(",");
    pushSample(Number(t), Number(v));   // alimentar el gráfico
    buffer = buffer.slice(nl + 1);
  }
}

Error común: un sensor rápido puede emitir líneas más rápido de lo que el gráfico puede repintarse; agrupa las muestras y actualiza en requestAnimationFrame en lugar de re-renderizar en cada read(), o el hilo principal se bloqueará.

5. Un controlador de pantalla o visual

Un controlador de pantalla renderiza píxeles o fotogramas desde el navegador y los envía a una pantalla física — una matriz de LEDs, una pantalla OLED o un panel de tinta electrónica. Aquí el navegador actúa como motor de renderizado: calculas un mapa de bits o un array de brillo en JavaScript y escribes los bytes que espera el controlador de la pantalla.

Emparejamiento de hardware/protocolo: una matriz de LEDs controlada por MAX7219, una pantalla OLED SSD1306 o un módulo de tinta electrónica detrás de un microcontrolador que acepta bytes de framebuffer a través de UART.

const writer = port.writable.getWriter();

// Fotograma 8x8 como un byte por fila (bit activado = LED encendido).
function renderFrame(rows: number[]) {
  return writer.write(Uint8Array.from(rows));
}

await renderFrame([
  0b00111100, 0b01000010, 0b10100101, 0b10000001,
  0b10100101, 0b10011001, 0b01000010, 0b00111100,
]);

Error común: las escrituras no se sincronizan automáticamente con la frecuencia de actualización de la pantalla, por lo que animar desde el navegador puede producir desgarros o pérdida de fotogramas; envía un fotograma completo por actualización y deja que el microcontrolador gestione el timing propio de la pantalla en lugar de transmitir filas parciales.

6. Una interfaz de configuración de dispositivos

Una interfaz de configuración de dispositivos lee y escribe ajustes en un dispositivo conectado por serie — parámetros del controlador de vuelo en un dron, memorias de canal en una radio de radioaficionado, valores de registros en un módulo IoT. Aquí es donde el patrón de filtro por VID/PID demuestra su utilidad: las aplicaciones de configuración apuntan a un dispositivo conocido, por lo que filtrar el selector de puertos por ID de proveedor y producto significa que el usuario solo ve su dispositivo, no todos los puertos COM de la máquina.

Emparejamiento de hardware/protocolo: un controlador de vuelo que habla MSP, el protocolo CAT/CI-V de una radio, o cualquier módulo con un conjunto de comandos de acceso a registros documentado — seleccionado con filtros en requestPort.

// Acotar el selector a un proveedor/producto conocido.
const port = await navigator.serial.requestPort({
  filters: [{ usbVendorId: 0x10c4, usbProductId: 0xea60 }], // ejemplo CP210x
});
await port.open({ baudRate: 115200 });

const writer = port.writable.getWriter();
await writer.write(buildReadConfigCommand());   // solicitar la configuración actual
// ...leer la respuesta, rellenar los campos del formulario, escribir al guardar

Pasar filters: [{ usbVendorId, usbProductId }] a requestPort() acota el diálogo a los puertos coincidentes, según la especificación de SerialPortRequestOptions. Para omitir el diálogo por completo en visitas posteriores, navigator.serial.getPorts() devuelve los puertos que el usuario ya ha aprobado.

Error común: los IDs de proveedor y producto en el selector provienen del chip puente USB a serie (FTDI, CP210x, CH340), no del dispositivo final, por lo que varios productos no relacionados pueden compartir el mismo usbVendorId — filtra tanto por usbVendorId como por usbProductId siempre que sea posible.

Cuándo Web Serial es la herramienta adecuada frente a las alternativas

Usa Web Serial cuando el dispositivo se enumera como un puerto COM o un nodo /dev/tty — es decir, cuando el sistema operativo ya ha cargado un controlador serie para él. Web Serial existe de forma independiente a WebUSB porque los controladores serie del sistema operativo se apropian de los adaptadores USB a serie antes de que WebUSB pueda acceder a ellos, una situación que el explicador de Web Serial del WICG describe directamente. Para otras clases de dispositivos, recurre a una API diferente.

APITipo de dispositivoModelo de permisosSoporte en navegadoresIdeal para
Web SerialDispositivos en un puerto serie/COM/ttySelector con gesto del usuarioChrome/Edge 89+, Opera 75+, Firefox 151+Microcontroladores, impresoras, adaptadores USB a serie
WebUSBInterfaces USB sin procesar (sin controlador del SO)Selector con gesto del usuarioBasados en ChromiumDispositivos USB personalizados sin controlador serie
WebHIDDispositivos de interfaz humanaSelector con gesto del usuarioBasados en ChromiumMandos de juego, teclados, periféricos HID personalizados
Web BluetoothBluetooth Low Energy (GATT)Selector con gesto del usuarioBasados en ChromiumSensores BLE, balizas, dispositivos wearables
WebSocket + daemon en backendCualquier dispositivoMediado por servidorTodos los navegadoresAmplio alcance entre navegadores, análisis en el servidor

Si hoy necesitas dar soporte a usuarios de Firefox y Safari y la lógica del dispositivo puede residir en el servidor, un pequeño daemon nativo que exponga un WebSocket es la alternativa portable — a costa de un paso de instalación que Web Serial evita.

Novedades recientes que merece la pena conocer

Chrome 117 añadió soporte para Bluetooth Classic RFCOMM/SPP en Web Serial, y Chrome 130 añadió el booleano SerialPort.connected para distinguir los puertos físicamente presentes de los cerrados por la aplicación. Ambas características están ausentes en la mayoría de los tutoriales existentes, por lo que vale la pena tenerlas en cuenta al diseñar nuevos proyectos.

Desde Chrome 117 en escritorio, Web Serial puede comunicarse con dispositivos Bluetooth Classic emparejados mediante RFCOMM/SPP: pasa allowedBluetoothServiceClassIds (o una entrada filters: [{ bluetoothServiceClassId }]) para que los servicios RFCOMM personalizados aparezcan en el selector de requestPort(), con la clase de servicio accesible a través de port.getInfo().bluetoothServiceClassId — y sin necesidad de una llamada separada a WebBluetooth. Esta ruta Bluetooth es exclusiva de Chromium (Chrome/Edge 117+, Opera 103+) y aún está marcada como experimental en MDN; el soporte general de Web Serial en Firefox 151 no incluye todavía las opciones de clase de servicio Bluetooth. Los detalles están en la publicación de Chrome sobre Serial over Bluetooth on the web. Esto convierte la comunicación serie inalámbrica en el mismo flujo de lectura/escritura que ya escribiste para USB.

Chrome 130 añadió SerialPort.connected, un booleano que es true cuando el puerto está físicamente presente pero no necesariamente abierto. Permite que la interfaz de reconexión distinga entre “dispositivo desconectado” y “puerto cerrado por la aplicación” — combínalo con los eventos connect y disconnect para mostrar un indicador de conexión en tiempo real sin necesidad de sondeo.

requestPort() rechaza silenciosamente cuando el usuario descarta el diálogo de permisos de Web Serial, y la mayoría de las implementaciones no muestran ese rechazo como un estado visible, por lo que el botón de conexión simplemente vuelve a su estado predeterminado y la página parece no hacer nada. Este único fallo de experiencia de usuario afecta a las seis categorías de proyectos descritas anteriormente. Las repeticiones de sesión de flujos de conexión con Web Serial suelen mostrar un patrón característico: usuarios que hacen clic en el botón de conexión dos o tres veces seguidas antes de abandonar — una señal clara de que el rechazo no se está comunicando. Captura el rechazo de requestPort() y muestra un mensaje explícito de “ningún dispositivo seleccionado” o “puerto en uso”; los puertos retenidos por otro proceso (el IDE de Arduino, screen o ModemManager) fallan de la misma forma silenciosa.

Elige una categoría, conecta el bloque mínimo de conexión, lectura, escritura y limpieza del inicio de este artículo, y verifica que la matriz de soporte coincide con tu audiencia antes de dedicarle un fin de semana. El camino más rápido hacia una compilación funcional es un monitor serie contra una placa que ya tengas — una vez que el bucle de lectura y la limpieza estén sólidos, las otras cinco categorías son variaciones sobre el mismo flujo de bytes. Web Serial es la herramienta incorrecta, sin embargo, cuando tu audiencia incluye usuarios de Safari, cuando el dispositivo no se enumera como un puerto COM o un nodo /dev/tty (en ese caso, recurre a WebUSB, WebHID o Web Bluetooth), o cuando la lógica de análisis realmente pertenece al servidor — para cualquiera de esos casos, un pequeño daemon nativo detrás de un WebSocket te da un mayor alcance a costa de un paso de instalación.

Preguntas frecuentes

Descartar el diálogo de permisos hace que la promesa de requestPort() sea rechazada en lugar de resolverse, y la mayoría de las implementaciones nunca muestran ese rechazo como un estado visible, por lo que el botón vuelve silenciosamente a su estado predeterminado. Envuelve la llamada a requestPort() en un bloque try/catch y muestra un mensaje explícito de 'ningún dispositivo seleccionado' al producirse el rechazo. Los puertos ya retenidos por otro proceso, como el IDE de Arduino, screen o ModemManager, fallan de la misma forma silenciosa.

Usa Web Serial para un Arduino porque el sistema operativo carga un controlador serie que se apropia del adaptador USB a serie, haciendo que la placa se enumere como un puerto COM o un nodo /dev/tty al que WebUSB no puede acceder. WebUSB es para interfaces USB sin procesar que no tienen controlador serie del SO, como dispositivos USB personalizados. Si un dispositivo aparece como un puerto COM, necesita Web Serial, no WebUSB.

Sí. Llama a navigator.serial.getPorts() para obtener el array de puertos que el usuario ya aprobó en sesiones anteriores y luego abre uno directamente sin solicitar permisos. Esto omite completamente el selector con gesto del usuario en visitas posteriores. En Chrome 130 y versiones posteriores, SerialPort.connected devuelve un booleano que indica si el puerto está físicamente presente, lo que permite que la interfaz de reconexión distinga un dispositivo desconectado de uno que la aplicación simplemente cerró.

Web Serial funciona en escritorio en Chrome 89+, Edge 89+, Opera 75+ y Firefox 151+, según los datos de compatibilidad de navegadores de MDN. Safari no tiene soporte anunciado ni una hoja de ruta publicada. La API también requiere un contexto seguro, es decir, HTTPS o localhost, y un gesto del usuario para llamar a requestPort(). Si hoy necesitas una cobertura amplia de Firefox y Safari, un daemon nativo que exponga un WebSocket es la alternativa portable a costa de un paso de instalación.

Un error de 'puerto ya abierto' al reconectar casi siempre significa que se ha producido una fuga de bloqueo de flujo. Si llamas a port.close() mientras un reader todavía retiene el flujo legible, el cierre es rechazado porque el flujo está bloqueado. Llama siempre a reader.releaseLock() antes de port.close(), idealmente dentro de un bloque finally, para que el bloqueo se libere independientemente de cómo salga el bucle de lectura. Lo mismo aplica a los writers obtenidos de port.writable.

Gain control over your UX

See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay. — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay