A API Web Serial permite que os sites se comuniquem com dispositivos seriais.
O que é a API Web Serial?
Uma porta serial é uma interface de comunicação bidirecional que permite enviar e receber dados byte por byte.
A API Web Serial permite que os sites leiam e gravem em 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 faz a ponte entre a Web e o mundo físico, permitindo que os sites se comuniquem com dispositivos seriais, como microcontroladores e impressoras 3D.
Essa API também é uma ótima companheira do WebUSB, já que os sistemas operacionais exigem que os aplicativos se comuniquem com algumas portas seriais usando a API seriada de nível superior em vez da API USB de nível inferior.
Casos de uso sugeridos
Nos setores educacionais, de passatempos e industriais, os usuários conectam dispositivos periféricos aos computadores. Esses dispositivos geralmente são controlados por microcontroladores por uma conexão serial usada por software personalizado. Alguns softwares personalizados para controlar esses dispositivos são criados com tecnologia da Web:
- Arduino Create
- Betaflight Configurator
- Espruino Web IDE (link em inglês)
- Microsoft MakeCode
Em alguns casos, os sites se comunicam com o dispositivo por meio de um aplicativo agente que os usuários instalaram manualmente. Em outros casos, o aplicativo é entregue em um aplicativo empacotado por meio de uma estrutura, como o Electron. Em outros, o usuário precisa realizar uma etapa extra, como copiar um aplicativo compilado para o dispositivo usando um pen drive.
Em todos esses casos, a experiência do usuário será aprimorada, fornecendo uma comunicação direta entre o site e o dispositivo que ele está controlando.
Status atual
Etapa | Status |
---|---|
1. Criar uma explicação | Concluído |
2. Criar um rascunho inicial da especificação | Concluído |
3. Coletar feedback e iterar o design | Concluído |
4. Teste de origem | Concluído |
5. Lançar | Concluído |
Como usar a API Web Serial
Detecção de recursos
Para verificar se a API Web Serial tem suporte, use:
if ("serial" in navigator) {
// The Web Serial API is supported.
}
Abrir uma porta serial
A API Web Serial é assíncrona por design. Isso impede que a interface do site seja bloqueada ao aguardar a entrada, o que é importante porque os dados seriais podem ser recebidos a qualquer momento, exigindo uma maneira de ouvi-los.
Para abrir uma porta serial, primeiro acesse um objeto SerialPort
. Para isso, você pode
pedir que o usuário selecione uma única porta serial chamando
navigator.serial.requestPort()
em resposta a um gesto do usuário, como toque
ou clique do mouse, ou escolher uma de navigator.serial.getPorts()
, que retorna
uma lista de portas seriais às quais o site teve 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 a um fornecedor USB obrigatório (usbVendorId
) e identificadores de produto USB
opcionais (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();
Chamar requestPort()
solicita que o usuário selecione um dispositivo e retorna um
objeto SerialPort
. Quando você tiver um objeto SerialPort
, chamar port.open()
com a taxa de Baud desejada abrirá a porta serial. O membro do dicionário baudRate
especifica a rapidez com que os dados são enviados por uma linha serial. Ele é expresso em
unidades de bits por segundo (bps). Verifique a documentação do dispositivo para saber o
valor correto, já que todos os dados enviados e recebidos vão ficar sem sentido se forem
especificados incorretamente. Para alguns dispositivos USB e Bluetooth que emulam uma porta
serial, esse valor pode ser definido com segurança para qualquer valor, já que é ignorado pela
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. Essas opções 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 ser menor que 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 é estabelecida, as propriedades readable
e writable
do objeto SerialPort
retornam um ReadableStream e um
WritableStream. Eles serão usados para receber e enviar dados para o
dispositivo serial. Ambas usam instâncias Uint8Array
para transferência de dados.
Quando novos dados chegam do dispositivo serial, o port.readable.getReader().read()
retorna duas propriedades de forma assíncrona: o value
e um booleano done
. Se done
for verdadeiro, a porta serial foi fechada ou não há mais dados de entrada. Chamar port.readable.getReader()
cria um leitor e bloqueia readable
a
ele. 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 de leitura de porta serial não fatais podem ocorrer em algumas condições, como
overflow de buffer, erros de enquadramento ou erros de paridade. Elas são geradas como
exceções e podem ser detectadas adicionando outro loop sobre o anterior
que verifica port.readable
. Isso funciona porque, desde que os erros não sejam fatais, um novo ReadableStream é criado automaticamente. Se ocorrer um erro fatal, como a remoção do dispositivo serial, port.readable
será
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 texto, você pode canalizar port.readable
por um
TextDecoderStream
, conforme mostrado abaixo. Um TextDecoderStream
é um fluxo de transformação
que usa 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 controlar como a memória é alocada ao ler o stream usando um leitor "Traga seu próprio buffer". Chame port.readable.getReader({ mode: "byob" })
para acessar a interface ReadableStreamBYOBReader e forneça seu próprio ArrayBuffer
ao chamar read()
. A API Web Serial oferece suporte a 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 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 a um dispositivo serial, transmita os dados para
port.writable.getWriter().write()
. Chamar releaseLock()
em
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();
Envie texto para o dispositivo por meio de um TextEncoderStream
canalizado 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
forem desbloqueados, o que significa que releaseLock()
foi chamado para o respectivo
leitor e gravador.
await port.close();
No entanto, ao ler dados continuamente de um dispositivo serial usando um loop,
o port.readable
vai estar sempre bloqueado até encontrar um erro. Nesse
caso, chamar reader.cancel()
forçará reader.read()
a resolver
imediatamente com { value: undefined, done: true }
e, portanto, permitirá que o
loop chame 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;
});
Fechar 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 pelos
streams de transformação para a porta serial. Como a propagação de erro
não acontece imediatamente, é necessário usar as promessas readableStreamClosed
e
writableStreamClosed
criadas anteriormente para detectar quando port.readable
e port.writable
forem desbloqueados. Cancelar o reader
faz com que o
stream seja interrompido. Por isso, é necessário capturar 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 a conexão e a desconexão
Se uma porta serial for fornecida por um dispositivo USB, esse dispositivo poderá ser conectado
ou desconectado do sistema. Quando o site receber permissão para
acessar uma porta serial, ele 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
Depois de estabelecer a conexão da porta serial, é possível consultar e definir explicitamente sinais expostos pela porta serial para detecção de dispositivos e controle de fluxo. Esses sinais são definidos como valores booleanos. Por exemplo, alguns dispositivos, como o Arduino, entrarão em um modo de programação se o sinal "Data Terminal Ready" (DTR, na sigla em inglês) for alternado.
A configuração de sinais de saída e a obtenção de sinais 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}`);
Transformar streams
Quando você recebe dados de um dispositivo serial, não necessariamente receberá todos de uma só vez. Ele pode ser dividido de forma arbitrária. Para mais informações, consulte Conceitos da API Streams.
Para lidar com isso, é possível usar alguns fluxos de transformação integrados, como TextDecoderStream
, ou criar seu próprio fluxo de transformação que permite analisar o fluxo de entrada e retornar dados analisados. O stream de transformação fica entre o dispositivo serial e o loop de leitura que está consumindo o stream. Ele pode aplicar uma transformação arbitrária antes que os dados sejam consumidos. Pense nisso como uma
linha de montagem: à medida que um widget passa pela linha, cada etapa na linha modifica
o widget. Assim, quando ele chega ao destino final, ele está totalmente
funcionando.
Por exemplo, considere como criar uma classe de fluxo de transformação que consome um
fluxo e o divide com base em quebras de linha. O método transform()
é chamado
sempre que novos dados são recebidos pelo fluxo. Ele pode enfileirar os dados ou
salvá-los para mais tarde. O método flush()
é chamado quando o stream é fechado e
processa todos os dados que ainda não foram processados.
Para usar a classe de stream de transformação, é preciso canalizar um fluxo de entrada por ela. No terceiro exemplo de código em Ler de uma porta serial,
o stream de entrada original só passou 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 do dispositivo serial, use o método tee()
de
port.readable
para dividir os streams de ou para o dispositivo serial. Os dois
streams criados podem ser consumidos de forma independente, o que permite imprimir um
no 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 que não
interessa mais, chamando forget()
na instância SerialPort
. Por
exemplo, em um aplicativo educacional da Web usado em um computador compartilhado com muitos
dispositivos, um grande número de permissões geradas pelo usuário acumuladas cria uma experiência
de usuário ruim.
// Voluntarily revoke access to this serial port.
await port.forget();
Como forget()
está disponível no Chrome 103 ou mais recente, verifique se esse recurso tem
suporte com o seguinte:
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
, em que você pode ver todos os eventos relacionados a dispositivos seriais em um
único lugar.
Codelab
No codelab para desenvolvedores do Google, você vai usar a API Web Serial para interagir com uma placa BBC micro:bit para mostrar imagens na 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, o suporte a portas seriais baseadas em USB é possível usando a API WebUSB 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 foi reivindicado por um driver de dispositivo integrado.
Segurança e privacidade
Os autores da especificação projetaram e implementaram a API Web Serial usando os princípios principais definidos em Como controlar o acesso a recursos poderosos da plataforma da Web, incluindo controle do usuário, transparência e ergonomia. A capacidade de usar essa API é principalmente restrita por um modelo de permissão que concede acesso a apenas um dispositivo serial por vez. Em resposta a um comando do usuário, o usuário precisa realizar etapas ativas para selecionar um dispositivo serial específico.
Para entender as compensações de segurança, consulte as seções segurança e privacidade da explicação da API Web Serial.
Feedback
A equipe do Chrome quer saber sua opinião e experiência com a API Web Serial.
Conte sobre o design da API
Há algo na API que não funciona como esperado? Ou há métodos ou propriedades ausentes que você precisa implementar sua ideia?
Registre um problema de especificação no repositório da API Web Serial do GitHub (link em inglês) ou adicione suas opiniões a um problema existente.
Informar um problema com a implementação
Você encontrou um bug na implementação do Chrome? Ou a implementação é diferente da especificação?
Registre um bug em https://new.crbug.com. Inclua o máximo
de detalhes possível, forneça instruções simples para reproduzir o bug e defina
Components como Blink>Serial
. O Glitch é ótimo para
compartilhar reprosagens rápidas e fáceis.
Mostrar apoio
Você planeja usar a API Web Serial? Seu apoio público ajuda a equipe do Chrome a priorizar recursos e mostra a outros fornecedores de navegadores a importância de oferecer suporte a eles.
Envie um tweet para @ChromiumDev usando a hashtag
#SerialAPI
e nos informe onde e como você está usando.
Links úteis
- Especificação
- Rastreamento de bugs
- Entrada de ChromeStatus.com
- Componente do Blink:
Blink>Serial
Demonstrações
- Terminal serial
- WebSerial
- Espruino Web IDE (link em inglês)
Agradecimentos
Agradecemos a Reilly Grant e Joe Medley pelas revisões deste artigo. Foto de uma fábrica de avião da Birmingham Museums Trust (em inglês) no Unsplash.