Coisas Legais que Você Pode Fazer com a Web Serial API
A Web Serial API permite que uma página web abra um fluxo de bytes de leitura/escrita para um dispositivo físico que utiliza comunicação serial — um adaptador USB-para-serial, uma placa de desenvolvimento com microcontrolador, uma impressora 3D ou um dispositivo Bluetooth Classic pareado — sem a necessidade de um aplicativo nativo ou extensão de navegador. Ela faz parte da mesma família de APIs de acesso a hardware do navegador que inclui MediaDevices (câmeras e microfones), WebUSB (interfaces USB brutas) e WebHID (gamepads e teclados): a página solicita acesso, o navegador exibe um diálogo de permissão e o usuário seleciona o dispositivo.
Antes de esboçar um projeto de fim de semana, o suporte dos navegadores define os limites. A Web Serial é compatível com desktops no Chrome 89+, Edge 89+, Opera 75+ e Firefox 151+, conforme a tabela de compatibilidade do MDN; o Safari não possui suporte anunciado. A API requer um contexto seguro (HTTPS ou localhost) e um gesto do usuário para chamar requestPort() — não é possível solicitar uma porta no carregamento da página. Este artigo mapeia seis categorias concretas de projetos que você pode construir com base nisso, cada uma com o código específico da Web Serial, o pareamento de hardware e a principal armadilha que costuma pegar os desenvolvedores de surpresa.
Principais Conclusões
- A Web Serial é compatível com desktops no Chrome 89+, Edge 89+, Opera 75+ e Firefox 151+; o Safari não possui suporte anunciado, e a API requer tanto um contexto seguro quanto um gesto do usuário para chamar
requestPort(). - A Web Serial existe separadamente da WebUSB porque os drivers seriais do sistema operacional reivindicam os adaptadores USB-para-serial antes que a WebUSB possa acessá-los, portanto dispositivos que se enumeram como portas COM ou nós
/dev/ttyprecisam da Web Serial. - Chamar
reader.releaseLock()antes deport.close()evita os erros de “porta já aberta” causados por locks de stream vazados na reconexão. - Desde o Chrome 117, a Web Serial pode se comunicar com dispositivos Bluetooth Classic RFCOMM/SPP pareados por meio de
allowedBluetoothServiceClassIdse filtrosbluetoothServiceClassId— serial sem fio sem a necessidade de uma chamada separada à WebBluetooth. - O Chrome 130 adicionou
SerialPort.connected, um booleano para distinguir uma porta fisicamente presente de uma que o aplicativo fechou.
O padrão mínimo de conectar-ler-escrever-limpar
Todo projeto com a Web Serial começa com os mesmos quatro passos: solicitar uma porta, abri-la em uma taxa de baud, ler ou escrever pelo stream e limpar, liberando o lock antes de fechar. O trecho abaixo é a referência para a qual o restante deste artigo aponta. Ele usa o argumento opcional filters para restringir o seletor de portas do navegador por ID de fornecedor e produto USB — conforme a definição de SerialPortRequestOptions na especificação WICG Web Serial, isso exclui portas seriais não relacionadas para que o usuário veja apenas os dispositivos relevantes.
// Deve ser executado dentro de um handler de gesto do usuário (ex: um listener de clique).
const port = await navigator.serial.requestPort({
filters: [{ usbVendorId: 0x2341 }], // opcional; omita para "qualquer porta"
});
await port.open({ baudRate: 115200 });
const reader = port.readable.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) break; // reader foi cancelado
console.log(value); // value é um chunk Uint8Array
}
} finally {
reader.releaseLock(); // libere ANTES de fechar
await port.close();
}
Não chamar reader.releaseLock() antes de port.close() é uma causa comum de erros de “porta já aberta” na reconexão — o lock mantém o stream legível, e close() rejeita enquanto um stream ainda está bloqueado, conforme as etapas de SerialPort.close() na especificação. Para protocolos de texto, passe os streams por TextEncoderStream e TextDecoderStream em vez de decodificar chunks brutos de Uint8Array manualmente; o guia Web Serial do MDN cobre os detalhes de codificação.
Discover how at OpenReplay.com.
Exemplos com a Web Serial API: seis projetos que valem um fim de semana
As seis categorias de projetos mais fortes da Web Serial são: ferramentas de depuração com leitura intensiva, clientes de protocolo com escrita intensiva, loops de streaming de longa duração, visualização em tempo real, displays renderizados pelo navegador e interfaces de configuração de dispositivos. Cada item abaixo isola a lógica específica da Web Serial em vez de percorrer uma aplicação completa.
1. Um monitor serial baseado em navegador
Um monitor serial é a ferramenta de depuração com leitura intensiva que substitui o Serial Monitor da Arduino IDE ou o screen /dev/ttyUSB0 por uma aba do navegador. Ele abre uma porta, transmite os bytes recebidos por um decodificador e adiciona as linhas decodificadas ao DOM. É o projeto mais simples aqui e o melhor para começar, pois exercita o loop de leitura sem as complicações de um protocolo de escrita.
Pareamento de hardware/protocolo: qualquer dispositivo UART — um Arduino, um ESP32, um adaptador USB-para-serial — emitindo texto delimitado por nova linha.
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() ?? ""; // mantém a última linha parcial
lines.forEach(line => appendToLog(line));
}
Armadilha: os dados seriais chegam em limites de chunk arbitrários, não em linhas completas — um único read() pode retornar metade de uma linha ou três linhas, portanto acumule no buffer até encontrar um delimitador antes de renderizar.
2. Um gravador de firmware
Um gravador de firmware escreve um binário compilado na memória flash de um microcontrolador via serial, da mesma forma que uma ferramenta esptool no navegador faz para chips ESP32. É intensivo em escrita e vinculado ao protocolo: antes de qualquer payload ser transferido, o chip precisa ser colocado em seu bootloader, e os dados são enquadrados de acordo com o protocolo de gravação do chip.
Pareamento de hardware/protocolo: ESP32/ESP8266 pelo protocolo de bootloader serial com frames SLIP; a sequência de entrada é controlada pelos sinais de controle DTR e RTS.
const port = await navigator.serial.requestPort();
await port.open({ baudRate: 115200 });
// Alterna os sinais de controle para forçar o ESP no modo de download.
// Consulte a sequência de modo de boot documentada pela Espressif.
await port.setSignals({ dataTerminalReady: false, requestToSend: true });
await port.setSignals({ dataTerminalReady: true, requestToSend: false });
// ...agora envie os comandos do bootloader com frames por port.writable
Gravadores no estilo ESPTool para o navegador precisam alternar os sinais de controle DTR e RTS em uma sequência específica para forçar o chip no modo de download antes que qualquer comando de escrita seja enviado; a Espressif documenta o comportamento dos sinais do modo de boot, e a Web Serial expõe os controles por meio de SerialPort.setSignals().
Armadilha: pular a sequência de bootloader DTR/RTS faz com que o chip permaneça em seu firmware de aplicação em vez de entrar no bootloader serial, impedindo que a gravação prossiga corretamente.
3. Um streamer de jobs G-code
Um streamer de G-code envia um job de impressão ou corte para uma impressora 3D ou máquina CNC uma linha por vez, aguardando o firmware confirmar cada linha antes de enviar a próxima. É um loop de escrita de longa duração com controle de fluxo, o que o torna um projeto consideravelmente mais complexo do que um escritor sem confirmação.
Pareamento de hardware/protocolo: firmware Marlin ou GRBL via USB serial, trocando G-code baseado em linhas com confirmações 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");
// Aguarda até que o firmware confirme que consumiu a linha.
let ack = "";
while (!ack.includes("ok")) {
const { value } = await reader.read();
ack += value;
}
}
Streamers de G-code precisam aguardar a confirmação ok da impressora após cada linha antes de enviar a próxima; a referência de G-code do RepRap define esse handshake, e inundar o buffer serial sem ele sobrecarrega a fila de comandos do firmware no meio do job.
Armadilha: o ok pode chegar intercalado com relatórios de temperatura e outras linhas não solicitadas, portanto faça a correspondência pelo token em vez de assumir que a próxima linha lida é a confirmação.
4. Um painel de telemetria em tempo real
Um painel de telemetria lê um stream contínuo de sensores de uma porta serial e o renderiza como um gráfico ao vivo no navegador — temperatura, tensão, eixos do acelerômetro, posições GPS. A parte da Web Serial é apenas um loop de leitura; o valor está em canalizar os valores decodificados diretamente para uma biblioteca de gráficos em execução na mesma aba.
Pareamento de hardware/protocolo: qualquer microcontrolador equipado com sensores emitindo linhas CSV, ou um módulo GPS emitindo sentenças 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)); // alimenta seu gráfico
buffer = buffer.slice(nl + 1);
}
}
Armadilha: um sensor rápido pode emitir linhas mais rápido do que o gráfico consegue redesenhar; agrupe amostras e atualize em requestAnimationFrame em vez de re-renderizar a cada read(), ou a thread principal travará.
5. Um controlador de display ou visual
Um controlador de display renderiza pixels ou frames no navegador e os envia para um display físico — uma matriz de LEDs, um OLED ou um painel de ePaper. Aqui o navegador é o motor de renderização: você computa um bitmap ou um array de brilho em JavaScript e escreve os bytes que o controlador do display espera.
Pareamento de hardware/protocolo: uma matriz de LEDs controlada por MAX7219, um OLED SSD1306 ou um módulo de ePaper atrás de um microcontrolador que aceita bytes de framebuffer via UART.
const writer = port.writable.getWriter();
// Frame 8x8 como um byte por linha (bit definido = LED aceso).
function renderFrame(rows: number[]) {
return writer.write(Uint8Array.from(rows));
}
await renderFrame([
0b00111100, 0b01000010, 0b10100101, 0b10000001,
0b10100101, 0b10011001, 0b01000010, 0b00111100,
]);
Armadilha: as escritas não são automaticamente sincronizadas com a taxa de atualização do display, portanto animar a partir do navegador pode causar tearing ou perda de frames; envie um frame completo por atualização e deixe o microcontrolador lidar com o timing do próprio display em vez de transmitir linhas parciais.
6. Uma interface de configuração de dispositivo
Uma interface de configuração de dispositivo lê e escreve configurações em um dispositivo conectado via serial — parâmetros de controlador de voo em um drone, memórias de canal em um rádio amador, valores de registrador em um módulo IoT. É aqui que o padrão de filtro por VID/PID se justifica: aplicações de configuração visam um dispositivo conhecido, portanto filtrar o seletor de portas por ID de fornecedor e produto significa que o usuário vê apenas seu dispositivo, não todas as portas COM da máquina.
Pareamento de hardware/protocolo: um controlador de voo usando MSP, o protocolo CAT/CI-V de um rádio, ou qualquer módulo com um conjunto de comandos de acesso a registradores documentado — selecionado com filtros em requestPort.
// Restringe o seletor a um fornecedor/produto conhecido.
const port = await navigator.serial.requestPort({
filters: [{ usbVendorId: 0x10c4, usbProductId: 0xea60 }], // exemplo CP210x
});
await port.open({ baudRate: 115200 });
const writer = port.writable.getWriter();
await writer.write(buildReadConfigCommand()); // solicita as configurações atuais
// ...lê a resposta, preenche os campos do formulário, escreve de volta ao salvar
Passar filters: [{ usbVendorId, usbProductId }] para requestPort() restringe o diálogo às portas correspondentes, conforme a especificação de SerialPortRequestOptions. Para pular o diálogo completamente em uma visita de retorno, navigator.serial.getPorts() retorna as portas que o usuário já aprovou.
Armadilha: os IDs de fornecedor/produto no seletor vêm do chip ponte USB-para-serial (FTDI, CP210x, CH340), não do dispositivo final, portanto vários produtos não relacionados podem compartilhar o mesmo usbVendorId — filtre por usbVendorId e usbProductId juntos sempre que possível.
Quando a Web Serial é a ferramenta certa versus as alternativas
Use a Web Serial quando o dispositivo se enumerar como uma porta COM ou nó /dev/tty — ou seja, quando o sistema operacional já carregou um driver serial para ele. A Web Serial existe separadamente da WebUSB porque os drivers seriais do sistema operacional reivindicam os adaptadores USB-para-serial antes que a WebUSB possa acessá-los, uma lacuna que o explicador WICG Web Serial descreve diretamente. Para outras classes de dispositivos, recorra a uma API diferente.
| API | Tipo de dispositivo | Modelo de permissão | Suporte de navegadores | Melhor para |
|---|---|---|---|---|
| Web Serial | Dispositivos em porta serial/COM/tty | Seletor por gesto do usuário | Chrome/Edge 89+, Opera 75+, Firefox 151+ | Microcontroladores, impressoras, dongles USB-serial |
| WebUSB | Interfaces USB brutas (sem driver do SO) | Seletor por gesto do usuário | Baseados em Chromium | Dispositivos USB personalizados sem driver serial |
| WebHID | Dispositivos de interface humana | Seletor por gesto do usuário | Baseados em Chromium | Gamepads, teclados, periféricos HID personalizados |
| Web Bluetooth | Bluetooth Low Energy (GATT) | Seletor por gesto do usuário | Baseados em Chromium | Sensores BLE, beacons, wearables |
| WebSocket + daemon backend | Qualquer coisa | Mediado pelo servidor | Todos os navegadores | Amplo alcance entre navegadores, parsing no servidor |
Se você precisar de suporte para usuários do Firefox e Safari hoje e a lógica do dispositivo puder residir no servidor, um pequeno daemon nativo expondo um WebSocket é o fallback portável — ao custo de uma etapa de instalação que a Web Serial evita.
Adições recentes que vale conhecer
O Chrome 117 adicionou suporte a Bluetooth Classic RFCOMM/SPP à Web Serial, e o Chrome 130 adicionou o booleano SerialPort.connected para distinguir portas fisicamente presentes daquelas fechadas pelo aplicativo. Ambos estão ausentes na maioria dos tutoriais existentes, portanto vale a pena considerá-los em seus projetos.
Desde o Chrome 117 no desktop, a Web Serial pode se comunicar com dispositivos Bluetooth Classic RFCOMM/SPP pareados: passe allowedBluetoothServiceClassIds (ou uma entrada filters: [{ bluetoothServiceClassId }]) para exibir serviços RFCOMM personalizados no seletor de requestPort(), com a classe de serviço legível via port.getInfo().bluetoothServiceClassId — sem necessidade de uma chamada separada à WebBluetooth. Esse caminho Bluetooth é exclusivo do Chromium (Chrome/Edge 117+, Opera 103+) e ainda está marcado como experimental no MDN; o suporte geral à Web Serial do Firefox 151 ainda não inclui as opções de classe de serviço Bluetooth. Os detalhes estão na publicação do Chrome Serial over Bluetooth on the web. Isso transforma o serial sem fio no mesmo stream de leitura/escrita que você já escreveu para USB.
O Chrome 130 adicionou SerialPort.connected, um booleano que é true quando a porta está fisicamente presente, mas não necessariamente aberta. Ele permite que a UX de reconexão distinga “dispositivo desconectado” de “porta fechada pelo aplicativo” — combine-o com os eventos connect e disconnect para acionar um indicador de conexão ao vivo sem polling.
requestPort() rejeita silenciosamente quando o usuário descarta o diálogo de permissão da Web Serial, e a maioria das implementações não renderiza essa rejeição como um estado visível, fazendo com que o botão de conexão simplesmente retorne ao seu estado padrão e a página pareça não fazer nada. Esse único ponto de falha de UX atravessa todas as seis categorias de projetos acima. Replays de sessão de fluxos de conexão com a Web Serial frequentemente revelam um padrão característico de usuários clicando no botão de conexão duas ou três vezes seguidas antes de desistir — um sinal claro de que a rejeição não está sendo comunicada. Capture a rejeição de requestPort() e exiba uma mensagem explícita de “nenhum dispositivo selecionado” ou “porta em uso”; portas mantidas por outro processo (a Arduino IDE, screen ou ModemManager) falham da mesma forma silenciosa.
Escolha uma categoria, conecte o bloco mínimo de conectar-ler-escrever-limpar do início deste artigo e confirme se a matriz de suporte corresponde ao seu público antes de dedicar um fim de semana a isso. O caminho mais rápido para uma build funcional é um monitor serial contra uma placa que você já possui — uma vez que o loop de leitura e a limpeza estejam sólidos, as outras cinco categorias são variações do mesmo stream de bytes. A Web Serial é a ferramenta errada, porém, quando seu público inclui usuários do Safari, quando o dispositivo não se enumera como uma porta COM ou nó /dev/tty (recorra à WebUSB, WebHID ou Web Bluetooth), ou quando a lógica de parsing genuinamente pertence ao servidor — para qualquer um desses casos, um pequeno daemon nativo atrás de um WebSocket oferece maior alcance ao custo de uma etapa de instalação.
Perguntas Frequentes
Descartar o diálogo de permissão faz com que a promise de requestPort() rejeite em vez de resolver, e a maioria das implementações nunca exibe essa rejeição como um estado visível, fazendo com que o botão retorne silenciosamente ao seu estado padrão. Envolva a chamada de requestPort() em um try ou catch e renderize uma mensagem explícita de 'nenhum dispositivo selecionado' na rejeição. Portas já mantidas por outro processo, como a Arduino IDE, screen ou ModemManager, falham da mesma forma silenciosa.
Use a Web Serial para um Arduino porque o sistema operacional carrega um driver serial que reivindica o adaptador USB-para-serial, fazendo com que a placa se enumere como uma porta COM ou nó /dev/tty que a WebUSB não consegue acessar. A WebUSB é para interfaces USB brutas que não possuem driver serial do SO, como dispositivos USB personalizados. Se um dispositivo aparece como uma porta COM, ele precisa da Web Serial, não da WebUSB.
Sim. Chame navigator.serial.getPorts() para recuperar o array de portas que o usuário já aprovou em sessões anteriores e, em seguida, abra uma diretamente sem solicitar permissão. Isso pula completamente o seletor por gesto do usuário para visitas de retorno. No Chrome 130 e versões posteriores, SerialPort.connected retorna um booleano indicando se a porta está fisicamente presente, permitindo que a UX de reconexão distinga um dispositivo que foi desconectado de um que o aplicativo simplesmente fechou.
A Web Serial funciona no desktop no Chrome 89+, Edge 89+, Opera 75+ e Firefox 151+, conforme os dados de compatibilidade de navegadores do MDN. O Safari não possui suporte anunciado e nenhum roadmap publicado. A API também requer um contexto seguro, ou seja, HTTPS ou localhost, e um gesto do usuário para chamar requestPort(). Se você precisar de ampla cobertura para Firefox e Safari hoje, um daemon nativo expondo um WebSocket é o fallback portável ao custo de uma etapa de instalação.
Um erro de 'porta já aberta' na reconexão quase sempre significa que um lock de stream foi vazado. Se você chamar port.close() enquanto um reader ainda mantém o stream legível, o fechamento rejeita porque o stream está bloqueado. Sempre chame reader.releaseLock() antes de port.close(), idealmente dentro de um bloco finally, para que o lock seja liberado independentemente de como o loop de leitura termina. O mesmo se aplica a writers obtidos 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.