Ler e gravar em uma porta serial

A API Web Serial permite que os sites se comuniquem com dispositivos seriais.

François Beaufort
François Beaufort

O que é a API Web Serial?

Uma porta serial é uma interface de comunicação bidirecional que permite o envio e que recebe dados byte por byte.

A API Web Serial permite que os sites leiam e gravem em um um dispositivo serial com JavaScript. Os dispositivos seriais são conectados por uma porta serial no sistema do usuário ou por dispositivos USB e Bluetooth removíveis que emulam uma porta serial.

Em outras palavras, a API Web Serial conecta o mundo físico e a Web, Permitir que os sites se comuniquem com dispositivos seriais, como microcontroladores e impressoras 3D.

Essa API também é um ótimo complemento para o WebUSB (link em inglês), já que os sistemas operacionais exigem se comuniquem com algumas portas seriais usando seus componentes serial em vez da API USB de baixo nível.

Casos de uso sugeridos

Nos setores educacional, amador e industrial, os usuários conectam dispositivos a seus computadores. Esses dispositivos geralmente são controlados por por uma conexão serial usada por um software personalizado. Alguns personalizados para controlar esses dispositivos foi desenvolvido com tecnologia Web:

Em alguns casos, os sites se comunicam com o dispositivo por meio de um agente aplicativo que os usuários instalaram manualmente. Em outros, o aplicativo é em um aplicativo empacotado usando um framework como o Electron. Em outros, o usuário precisa realizar uma etapa adicional, como copiar um aplicativo compilado para o dispositivo por meio de um pen drive.

Em todos esses casos, a experiência do usuário será aprimorada fornecendo comunicação entre o site e o dispositivo que ele controla.

Status atual

Etapa Status
1. Criar explicação Concluído
2. Criar um rascunho inicial da especificação Concluído
3. Colete feedback e iterar no design Concluído
4. Teste de origem Concluído
5. lançamento Concluído

Como usar a API Web Serial

Detecção de recursos

Para verificar se a API Web Serial é compatível, use:

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

Abrir uma porta serial

A API Web Serial é assíncrona por padrão. Isso impede que a interface do site o bloqueio quando se aguardam entradas, o que é importante porque os dados seriais podem ser recebidas a qualquer momento, exigindo uma maneira de ouvi-los.

Para abrir uma porta serial, primeiro acesse um objeto SerialPort. Para isso, é possível solicite que o usuário selecione uma única porta serial chamando navigator.serial.requestPort() em resposta a um gesto do usuário, como um toque ou clique do mouse, ou escolha um entre navigator.serial.getPorts() que retorna uma lista das portas seriais às quais o site recebeu acesso.

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

A função navigator.serial.requestPort() usa um literal de objeto opcional que define filtros. Eles são usados para corresponder a qualquer dispositivo serial conectado por USB com um fornecedor USB obrigatório (usbVendorId) e produto 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 tela de um prompt de porta serial em um site
Comando do usuário para selecionar um micro:bit da BBC

Chamar requestPort() solicita que o usuário selecione um dispositivo e retorna uma objeto SerialPort. Quando você tiver um objeto SerialPort, chame port.open(). com a taxa de Baud desejada abrirá a porta serial. O dicionário baudRate O membro especifica a rapidez com que os dados são enviados por uma linha serial. Ele é expresso em unidades de bits por segundo (bps). Consulte a documentação do dispositivo para o valor correto, pois todos os dados que você envia e recebe estarão sem sentido se forem especificado incorretamente. Para alguns dispositivos USB e Bluetooth que emulam um Esse valor pode ser definido com segurança para qualquer valor, já que é ignorado pelo e emulação.

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

Você também pode especificar qualquer uma das opções abaixo ao abrir uma porta serial. Esses são opcionais e têm valores padrão convenientes.

  • dataBits: o número de bits de dados por frame (7 ou 8).
  • stopBits: o número de bits de parada no final de um frame (1 ou 2).
  • parity: o modo de paridade ("none", "even" ou "odd").
  • bufferSize: o tamanho dos buffers de leitura e gravação que precisam ser criados. (precisa ter menos de 16 MB).
  • flowControl: o modo de controle de fluxo ("none" ou "hardware").

Ler de uma porta serial

Os streams de entrada e saída na API Web Serial são processados pela API Streams.

Depois que a conexão da porta serial for estabelecida, readable e writable do objeto SerialPort retornam um ReadableStream e um WritableStream. Elas serão usadas para receber e enviar dados ao dispositivo serial. Ambas usam instâncias Uint8Array para a transferência de dados.

Quando novos dados chegam do dispositivo serial, port.readable.getReader().read() retorna duas propriedades de forma assíncrona: o value e um booleano done. Se done é verdadeiro, a porta serial foi fechada ou não há mais dados chegando Chamar port.readable.getReader() cria um leitor e bloqueia readable em reimplantá-lo. Enquanto o readable estiver bloqueado, a porta serial não poderá ser fechada.

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

Alguns erros não fatais de leitura de porta serial podem ocorrer sob determinadas condições, como estouro de buffer, erros de enquadramento ou de paridade. Elas são geradas como exceções e podem ser capturadas adicionando outro loop sobre a anterior que verifica port.readable. Isso funciona porque, contanto que os erros sejam não fatal, um novo ReadableStream é criado automaticamente. Se um erro fatal ocorrer, como a remoção do dispositivo serial, então port.readable se tornará 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.
  }
}

Se o dispositivo serial retornar um texto, você pode canalizar port.readable por uma TextDecoderStream, conforme mostrado abaixo. Um TextDecoderStream é um fluxo de transformação. que pega todos os fragmentos Uint8Array e os converte em 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);
}

Você pode assumir o controle de como a memória é alocada ao ler o stream usando uma opção "Traga seu próprio buffer" leitor de tela. Chame port.readable.getReader({ mode: "byob" }) para ter a interface ReadableStreamBYOBReader e forneça seu próprio ArrayBuffer ao chamar read(). A API Web Serial é compatível com esse recurso no Chrome 106 ou mais recente.

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()...
  }
}

Confira um exemplo de como reutilizar o buffer fora 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`.
}

Confira outro exemplo de como ler uma quantidade específica de dados de uma porta serial:

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

Gravar em uma porta serial

Para enviar dados para um dispositivo serial, transmita dados para port.writable.getWriter().write(): Ligando para releaseLock() em O port.writable.getWriter() é necessário para que a porta serial seja fechada mais 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();

Enviar mensagens de texto para o dispositivo por um TextEncoderStream encaminhado para port.writable conforme mostrado abaixo.

const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);

const writer = textEncoder.writable.getWriter();

await writer.write("hello");

Fechar uma porta serial

port.close() fecha a porta serial se os membros readable e writable são desbloqueados, o que significa que releaseLock() foi chamado para os respectivos leitor e escritor.

await port.close();

No entanto, ao ler continuamente dados de um dispositivo serial usando um loop, O port.readable sempre será bloqueado até encontrar um erro. Neste caso, chamar reader.cancel() forçará reader.read() a resolver imediatamente com { value: undefined, done: true }, permitindo para chamar 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;
});

O fechamento de uma porta serial é mais complicado ao usar streams de transformação. Chame reader.cancel() como antes. Em seguida, chame writer.close() e port.close(). Isso propaga erros pela os streams de transformação para a porta serial subjacente. Devido à propagação de erros não acontecer imediatamente, será necessário usar as APIs readableStreamClosed e writableStreamClosed promessas criadas anteriormente para detectar quando port.readable e port.writable foram desbloqueados. Cancelar o reader faz com que a a ser cancelada, é por isso que você precisa detectar e ignorar o erro 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();

Ouvir conexão e desconexão

Se um dispositivo USB tiver uma porta serial, ele poderá ser conectado ou desconectados do sistema. Quando o site recebe permissão para acessar uma porta serial, ela vai monitorar os eventos connect e 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.
});

Processar indicadores

Após estabelecer a conexão da porta serial, é possível consultar e configurar sinais expostos pela porta serial para detecção de dispositivos e controle de fluxo. Esses são definidos como valores booleanos. Por exemplo, alguns dispositivos, como o Arduino, entrará em um modo de programação se o sinal "Data Terminal Ready" (DTR) estiver ativada ou não.

A configuração de sinais de saída e o recebimento de indicadores de entrada são feitas, respectivamente, chamando port.setSignals() e port.getSignals(). Confira os exemplos de uso abaixo.

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

Como transformar streams

Ao receber dados de um dispositivo serial, você não receberá necessariamente todas as os dados de uma só vez. Ele pode ser dividido arbitrariamente. Para mais informações, consulte Conceitos da API de streams.

Para lidar com isso, é possível usar alguns fluxos de transformação integrados, como TextDecoderStream ou crie seu próprio fluxo de transformação que permite analisa o fluxo de entrada e retorna os dados analisados. O fluxo de transformação fica entre o dispositivo serial e o loop de leitura que está consumindo o stream. Ela pode aplicar uma transformação arbitrária antes que os dados sejam consumidos. Pense nisso como um linha de montagem: à medida que um widget é enviado, cada etapa da linha modifica no widget, de modo que, no momento em que chega ao destino final, ele se torna que funciona melhor.

Foto de uma fábrica de aviões
Fábrica de Aeronaves de Bromwich da Segunda Guerra Mundial

Por exemplo, considere como criar uma classe de fluxo de transformação que consome uma stream e os divide com base nas quebras de linha. O método transform() é chamado sempre que novos dados são recebidos pelo fluxo. Ele pode enfileirar os dados ou salvar para mais tarde. O método flush() é chamado quando o stream é fechado. ela lida com todos os dados que ainda não foram processados.

Para usar a classe de stream de transformação, é preciso canalizar um stream de entrada reimplantá-lo. No terceiro exemplo de código, em Ler de uma porta serial, o fluxo de entrada original só passava por um TextDecoderStream, então precisamos chamar pipeThrough() para canalizá-lo pelo novo 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 comunicação de dispositivos seriais, use o método tee() de port.readable para dividir os streams de e para o dispositivo serial. Os dois streams criados podem ser consumidos de maneira independente, o que permite imprimir um o console para inspeção.

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.

Revogar acesso a uma porta serial

O site pode limpar as permissões para acessar uma porta serial em que ela não é mais interessados em manter chamando forget() na instância do SerialPort. Para exemplo, para um aplicativo da Web educacional usado em um computador compartilhado com muitos dispositivos, um grande número de permissões geradas pelo usuário gera uma baixa experiência do usuário.

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

Como o forget() está disponível no Chrome 103 ou mais recente, verifique se esse recurso está compatível com:

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

Dicas para desenvolvedores

Depurar a API Web Serial no Chrome é fácil com a página interna, about://device-log onde é possível conferir todos os eventos relacionados a dispositivos seriais em um só lugar único lugar.

Captura de tela da página interna para depuração da API Web Serial.
Página interna no Chrome para depuração da API Web Serial.

Codelab

No codelab do Google Developers, você vai usar a API Web Serial para interagir. com uma placa BBC micro:bit para mostrar imagens em uma matriz de LED 5x5.

Suporte ao navegador

A API Web Serial está disponível em todas as plataformas de computador (ChromeOS, Linux, macOS, e Windows) no Chrome 89.

Polyfill

No Android, a API WebUSB oferece suporte a portas seriais baseadas em USB. e o polyfill da API Serial. Esse polyfill é limitado a hardware e plataformas em que o dispositivo pode ser acessado pela API WebUSB porque não por um driver de dispositivo integrado.

Segurança e privacidade

Os autores das especificações projetaram e implementaram a API Web Serial usando o núcleo definidos em Como controlar o acesso a recursos avançados da Web Platform, incluindo controle do usuário, transparência e ergonomia. A possibilidade de usar A API é controlada principalmente por um modelo de permissão que concede acesso a apenas um dispositivo serial por vez. Em resposta a um prompt do usuário, o usuário precisa ativar para selecionar um dispositivo serial específico.

Para entender os prós e contras da segurança, consulte a seção sobre segurança e privacidade da explicação da API Web Serial.

Feedback

A equipe do Chrome adoraria saber o que você acha e tem experiência com o a API Web Serial.

Fale sobre o design da API

Alguma coisa na API não funciona como esperado? Ou há métodos ou propriedades ausentes de que precisa para implementar sua ideia?

Registre um problema de especificação no repositório da API Web Serial do GitHub (em inglês) ou adicione sua pensamentos para um problema existente.

Informar um problema com a implementação

Você encontrou um bug na implementação do Chrome? Ou a implementação diferente das especificações?

Registre um bug em https://new.crbug.com. Não deixe de incluir todos os detalhes possíveis, fornecer instruções simples para reproduzir o bug e A opção Componentes foi definida como Blink>Serial. O Glitch funciona muito bem para o compartilhamento de reproduções rápidas e fáceis.

Mostrar apoio

Você planeja usar a API Web Serial? Seu apoio público ajuda o Chrome de segurança da nuvem a priorizar recursos e mostrar a outros fornecedores de navegadores apoiá-los.

Envie um tweet para @ChromiumDev usando a hashtag #SerialAPI e informe onde e como você o utiliza.

Links úteis

Demonstrações

Agradecimentos

Agradecemos a Reilly Grant e Joe Medley pelas avaliações deles deste artigo. Foto de uma fábrica de avião da Birmingham Museums Trust (em inglês) no Unsplash.