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 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:
- Arduino Create (em inglês)
- Configurador do Betaflight
- Ambiente de desenvolvimento integrado da Web do Espruino
- MakeCode da Microsoft (em inglês)
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();
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.
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.
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
- Especificação
- Rastreamento de bug
- Entrada de ChromeStatus.com
- Componente Blink:
Blink>Serial
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.