Leer y escribir en un puerto en serie

La API de Web Serial permite que los sitios web se comuniquen con dispositivos en serie.

François Beaufort
François Beaufort

¿Qué es la API de Web Serial?

Un puerto en serie es una interfaz de comunicación bidireccional que permite enviar y byte por byte de datos.

La API de Web Serial proporciona una forma para que los sitios web lean y escriban en un dispositivo en serie con JavaScript. Los dispositivos en serie se conectan a través de un puerto en serie en el sistema del usuario o mediante dispositivos USB y Bluetooth extraíbles que emulan un puerto en serie.

En otras palabras, la API de Web Serial conecta la Web y el mundo físico al permitir a los sitios web comunicarse con dispositivos en serie, 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 en serie usando sus en lugar de la API de USB de bajo nivel.

Casos de uso sugeridos

En los sectores industrial, educativo y aficionado, los usuarios conectan los dispositivos periféricos dispositivos a sus computadoras. A menudo, estos dispositivos son controlados por y microcontroladores mediante una conexión en serie que utilice un software personalizado. Algo personalizado software para controlar estos dispositivos se crea con tecnología web:

En algunos casos, los sitios web se comunican con el dispositivo a través de un agente que los usuarios instalaron manualmente. En otros, la aplicación es y se entregan en una aplicación empaquetada a través de un framework como Electron. En otros, el usuario debe realizar un paso adicional, como copiar una aplicación compilada al dispositivo a través de una unidad flash USB.

En todos estos casos, la experiencia del usuario mejorará al brindar la comunicación entre el sitio web y el dispositivo que controla.

Estado actual

Paso Estado
1. Crear explicación Completo
2. Crea el borrador inicial de la especificación Completo
3. Recopila comentarios y iterar en el diseño Completo
4. Prueba de origen Completo
5. Lanzamiento Completo

Usa la API de Web Serial

Detección de funciones

Para verificar si la API de Web Serial es compatible, usa lo siguiente:

if ("serial" in navigator) {
  // The Web Serial API is supported.
}

Abre un puerto en serie

La API de Web Serial es asíncrona por diseño. Esto evita que la IU del sitio web el bloqueo cuando se espera una entrada, lo cual es importante, ya que los datos en serie pueden en cualquier momento y requieren una forma de escucharlo.

Para abrir un puerto en serie, primero accede a un objeto SerialPort. Para ello, puedes puedes pedirle al usuario que seleccione un solo puerto en serie llamando navigator.serial.requestPort() en respuesta a un gesto del usuario, como el toque o haz clic con el mouse, o elige uno de navigator.serial.getPorts() que te muestre una lista de puertos en serie a los que tiene acceso el 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 literal de objeto opcional que define los filtros. Se usan para hacer coincidir cualquier dispositivo en serie conectado a USB con un proveedor de USB obligatorio (usbVendorId) y un producto USB opcional identificadores (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();
Captura de pantalla de una solicitud de puerto en serie en un sitio web
Mensaje del usuario para seleccionar un micro:bit de la BBC

Si llamas a requestPort(), se le pide al usuario que seleccione un dispositivo y se muestra un SerialPort. Una vez que tengas un objeto SerialPort, llamar a port.open() con la tasa de baudios deseada abrirá el puerto en serie. El diccionario baudRate miembro especifica la rapidez con la que se envían los datos a través de una línea en serie. Se expresa en de bits por segundo (bps). Consulta la documentación de tu dispositivo para obtener valor correcto, ya que todos los datos que envíes y recibas serán incoherentes si esto es se especificó de forma incorrecta. Para algunos dispositivos USB y Bluetooth que emulan un número de serie este valor se puede configurar de forma segura en cualquier valor, ya que es ignorado por la por lotes y por lotes.

// 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. Estos estas opciones son opcionales y tienen valores predeterminados convenientes.

  • dataBits: Es la cantidad de bits de datos por trama (7 o, también, 8).
  • stopBits: Es la cantidad de bits de parada al final de una trama (1 o 2).
  • parity: Es el modo de paridad (ya sea "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").

Lee desde un puerto en serie

La API de Streams controla las transmisiones de entrada y salida en la API de Web Serial.

Después de que se establece la conexión del puerto en serie, los readable y writable Las propiedades del objeto SerialPort muestran un ReadableStream y un WritableStream. Estos se usarán para recibir datos y enviarlos a la dispositivo en serie. Ambas usan instancias de Uint8Array para la transferencia de datos.

Cuando llegan datos nuevos del dispositivo en serie, port.readable.getReader().read() muestra dos propiedades de forma asíncrona: value y un booleano done. Si done es verdadero. El puerto en serie se cerró o no hay más datos entrantes. en el que te etiquetaron. Llamar a port.readable.getReader() crea un lector y bloquea a readable para que la modifica. Mientras readable esté bloqueado, el puerto en serie no se puede cerrar.

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);
}

Algunos errores de lectura de puertos en serie no fatales pueden ocurrir en algunas condiciones, como las siguientes: desbordamiento de búfer, errores de enmarcado o errores de paridad. Se arrojan como excepciones y se pueden capturar si agregas otro bucle sobre el anterior que verifica port.readable. Esto funciona porque siempre que los errores recuperable, se crea automáticamente un nuevo ReadableStream. Si se produce un error grave por ejemplo, cuando se quita el dispositivo en serie, se convierte port.readable 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.
  }
}

Si el dispositivo en serie envía texto de vuelta, puedes canalizar port.readable a través de un TextDecoderStream, como se muestra a continuación. Una TextDecoderStream es una transmisión 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 la manera en que se asigna la memoria cuando lees de la transmisión usando un “trae tu propio búfer” de lectura. 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 volver a usar el búfer 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`.
}

A continuación, se muestra otro ejemplo de cómo leer una cantidad específica de datos desde un puerto en 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);

Escribir en un puerto en serie

Para enviar datos a un dispositivo en serie, pasa los datos a port.writable.getWriter().write() Se llama a releaseLock() en Se requiere port.writable.getWriter() para que el puerto en serie se cierre más tarde.

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");

Cómo cerrar un puerto en serie

port.close() cierra el puerto en serie si sus miembros readable y writable están desbloqueadas, lo que significa que se llamó a releaseLock() para sus respectivas lector y escritor.

await port.close();

Sin embargo, cuando se leen datos de forma continua desde un dispositivo en serie usando un bucle, port.readable siempre estará bloqueado hasta que encuentre un error. En este caso, llamar a reader.cancel() forzará a reader.read() a resolver el problema inmediatamente con { value: undefined, done: true } y, por lo tanto, se permite bucle para llamar 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 en serie es más complicado cuando se usan transmisiones de transformación. Llama a reader.cancel() como lo hiciste antes. Luego, llama a writer.close() y port.close(). Esto propaga los errores a través de la transformación transmite al puerto en serie subyacente. Debido a que la propagación de errores no se produce de inmediato, debes usar readableStreamClosed y Las promesas writableStreamClosed se crearon antes para detectar cuándo port.readable y port.writable. Cancelar reader provoca que que se anule la transmisión; y es por eso que debe capturar e ignorar 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();

Escuchar conexión y desconexión

Si un dispositivo USB proporciona un puerto en serie, es posible que ese dispositivo esté conectado o desconectado del sistema. Cuando el sitio web obtiene permiso para acceder a un puerto en 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 indicadores

Luego de establecer la conexión del puerto en serie, puedes consultar y configurar que expone el puerto en serie para la detección de dispositivos y el control de flujo. Estos se definen como valores booleanos. Por ejemplo, algunos dispositivos, como Arduino ingresará en un modo de programación si la señal de terminal de datos lista (DTR) está activada.

La configuración de los indicadores de salida y la obtención de indicadores de entrada se realizan, respectivamente, de llamando a port.setSignals() y port.getSignals(). 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}`);

Cómo transformar transmisiones

Cuando recibas datos del dispositivo en serie, no necesariamente obtendrás los datos a la vez. Se puede fragmentar de forma arbitraria. Para obtener más información, consulta Conceptos de la API de transmisión.

Para lidiar con esto, puedes usar algunas transmisiones de transformaciones integradas, como TextDecoderStream o crea tu propia transmisión de transformación, que te permita realizar las siguientes acciones: analizar la transmisión entrante y mostrar los datos analizados. La transmisión de transformación se encuentra entre el dispositivo en serie y el bucle de lectura que consume la transmisión. Puede aplicar una transformación arbitraria antes de que los datos se consuman. Piénsalo como un línea de montaje: a medida que un widget desciende en la línea, cada paso en la línea modifica el widget, de modo que, cuando llegue a su destino final, será un widget en funcionamiento.

Foto de una fábrica de aviones
Fábrica de aviones del castillo de Bromwich de la Segunda Guerra Mundial

Por ejemplo, considera cómo crear una clase de transmisión de transformación que consuma un y lo fragmenta en función de los saltos de línea. Se llama al método transform() cada vez que la transmisión recibe datos nuevos. Puedes poner los datos en cola guardarlo para más tarde. Se llama al método flush() cuando se cierra la transmisión. se encarga de los datos que no se procesaron aún.

Para usar la clase de transmisión de transformación, debes canalizar una transmisión entrante a través de que la modifica. En el tercer ejemplo de código, en Leer desde un puerto en serie, la transmisión 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 de dispositivos en serie, usa el método tee() de port.readable para dividir las transmisiones desde o hacia el dispositivo en serie. Los dos las transmisiones creadas se pueden consumir de forma independiente, lo que permite imprimir a 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.

Revoca el acceso a un puerto en serie

El sitio web puede limpiar los permisos para acceder a un puerto en serie que ya no es interesado en retener llamando a forget() en la instancia SerialPort. Para ejemplo, para una aplicación web educativa que se utiliza en una computadora compartida con muchos una gran cantidad de permisos acumulados generados por el usuario crea la experiencia del usuario.

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

Dado que forget() está disponible en Chrome 103 o versiones posteriores, verifica si esta función está disponible compatibles con lo siguiente:

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

Sugerencias para desarrolladores

La depuración de la API de Web Serial en Chrome es fácil desde la página interna. about://device-log, donde puedes ver todos los eventos relacionados con el dispositivo en serie en uno solo en un solo lugar.

Captura de pantalla de la página interna para depurar la API de Web Serial.
Página interna en Chrome para depurar la API de Web Serial.

Codelab

En el codelab de Google Developers, usarás la API de Web Serial para interactuar con una placa micro:bit de la BBC para mostrar imágenes en su matriz LED de 5 × 5.

Navegadores compatibles

La API de Web Serial está disponible en todas las plataformas de escritorio (ChromeOS, Linux, macOS, y Windows) en Chrome 89.

Polyfill

En Android, los puertos en serie basados en USB son compatibles con la API de WebUSB y el polyfill de la API de Serial. Este polyfill se limita a hardware y plataformas en las que se puede acceder al dispositivo a través de la API de WebUSB porque no ha que un controlador de dispositivo integrado haya reclamado.

Seguridad y privacidad

Los autores de las especificaciones diseñaron e implementaron la API de Web Serial con la API de que se definen en Controla el acceso a las funciones potentes de la plataforma web, incluidos el control de usuario, la transparencia y la ergonomía. La capacidad de usar La API está restringida principalmente por un modelo de permisos que otorga acceso a un solo un dispositivo en serie a la vez. En respuesta a la solicitud del usuario, este debe tomar pasos para seleccionar un dispositivo en serie en particular.

Para comprender las compensaciones de seguridad, consulta las páginas de seguridad y privacidad. de la Explicación de la API de Web Serial.

Comentarios

Al equipo de Chrome le gustaría conocer tus opiniones y experiencias con el API de Web Serial.

Cuéntanos sobre el diseño de la API

¿Existe algún aspecto de la API que no funcione según lo esperado? ¿O hay o propiedades faltantes que necesites para implementar tu idea?

Informa un problema de especificaciones en el repositorio de GitHub de la API de Web Serial o agrega tu ideas a un problema existente.

Informar un problema con la implementación

¿Encontraste un error en la implementación de Chrome? ¿O la implementación diferente de la especificación?

Informa un error en https://new.crbug.com. Asegúrate de incluir tantos detalles como puedas, proporcionar instrucciones sencillas para reproducir el error y contar con Componentes establecidos en Blink>Serial. Glitch funciona muy bien para para compartir repros rápidos y fáciles.

Demostrar apoyo

¿Planeas usar la API de Web Serial? Tu apoyo público ayuda a Chrome prioriza funciones y muestra a otros proveedores de navegadores la importancia de respaldarlos.

Envía un tweet a @ChromiumDev con el hashtag #SerialAPI y cuéntanos dónde y cómo la utilizas.

Vínculos útiles

Demostraciones

Agradecimientos

Agradecemos a Reilly Grant y Joe Medley por sus opiniones sobre este artículo. Foto de la fábrica de aviones de Birmingham Museums Trust en Unsplash.