Introdução à API Web Serial

1. Introdução

Última atualização:21/07/2020

O que você vai criar

Neste codelab, você vai criar uma página da Web que usa a API Web Serial para interagir com uma placa BBC micro:bit e mostrar imagens na matriz de LED 5x5. Você vai aprender sobre a API Web Serial e como usar streams legíveis, graváveis e de transformação para se comunicar com dispositivos seriais pelo navegador.

81167ab7c01d353d.png

O que você vai aprender

  • Como abrir e fechar uma porta serial da Web
  • Como usar um loop de leitura para processar dados de um stream de entrada.
  • Como enviar dados por meio de um fluxo de gravação

O que é necessário

Escolhemos usar o micro:bit para este codelab porque ele é acessível, oferece algumas entradas (botões) e saídas (tela LED 5x5) e pode fornecer mais entradas e saídas. Consulte a página micro:bit da BBC (em inglês) no site de Espruino para saber mais detalhes sobre o que o micro:bit é capaz.

2. Sobre a API Web Serial

A API Web Serial permite que os sites leiam e gravem em um dispositivo serial com scripts. A API conecta a Web e o mundo físico, permitindo que sites se comuniquem com dispositivos seriais, como microcontroladores e impressoras 3D.

Há muitos exemplos de software de controle sendo construído usando tecnologia web. Exemplo:

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

A experiência do usuário pode ser melhorada fornecendo uma comunicação direta entre o site e o dispositivo que ele está controlando.

3. Etapas da configuração

Buscar o código

Colocamos tudo o que você precisa para este codelab em um projeto Glitch.

  1. Abra uma nova guia do navegador e acesse https://web-serial-codelab-start.glitch.me/.
  2. Clique no link Remix Glitch para criar sua própria versão do projeto inicial.
  3. Clique no botão Mostrar e escolha Em uma nova janela para ver o código em ação.

4. Abrir uma conexão serial

Verificar se a API Web Serial é compatível

A primeira coisa a fazer é verificar se a API Web Serial é compatível com o navegador atual. Para fazer isso, verifique se serial está em navigator.

No evento DOMContentLoaded, adicione o seguinte código ao seu projeto:

script.js - DOMContentLoaded

// CODELAB: Add feature detection here.
const notSupported = document.getElementById('notSupported');
notSupported.classList.toggle('hidden', 'serial' in navigator);

Ela verifica se o serial da Web é compatível. Se for, esse código vai ocultar o banner que informa que o serial da Web não é compatível.

Faça um teste

  1. Carregue a página.
  2. Verifique se a página não mostra um banner vermelho informando que o Web Serial não é compatível.

Abrir a porta serial

Em seguida, precisamos abrir a porta serial. Como a maioria das outras APIs modernas, a API Web Serial é assíncrona. Isso evita que a IU bloqueie ao aguardar entradas, mas também é importante porque os dados seriais podem ser recebidos pela página da Web a qualquer momento, e precisamos de uma maneira de escutá-los.

Como um computador pode ter vários dispositivos seriais, quando o navegador tenta solicitar uma porta, ele pede que o usuário escolha com qual dispositivo se conectar.

Adicione o seguinte código ao seu projeto:

script.js - connect()

// CODELAB: Add code to request & open port here.
// - Request a port and open a connection.
port = await navigator.serial.requestPort();
// - Wait for the port to open.
await port.open({ baudrate: 9600 });

A chamada requestPort solicita ao usuário a qual dispositivo ele quer se conectar. Chamar port.open abre a porta. Também precisamos fornecer a velocidade com que queremos nos comunicar com o dispositivo serial. O micro:bit da BBC usa uma conexão de 9.600 Baud entre o chip USB para serial e o processador principal.

Também vamos conectar o botão de conexão e fazer com que ele chame connect() quando o usuário clicar nele.

Adicione o seguinte código ao seu projeto:

script.js - clickConnect()

// CODELAB: Add connect code here.
await connect();

Faça um teste

Agora, nosso projeto tem o mínimo necessário para começar. Clicar no botão Conectar solicita que o usuário selecione o dispositivo serial ao qual se conectar e, em seguida, se conecta ao micro:bit.

  1. Recarregue a página.
  2. Clique no botão Conectar.
  3. Na caixa de diálogo do seletor de porta serial, selecione o dispositivo micro:bit BBC e clique em Conectar.
  4. Na guia, você verá um ícone indicando que se conectou a um dispositivo serial:

d9d0d3966960aeab.png

Configurar um stream de entrada para detectar dados da porta serial

Depois que a conexão for estabelecida, precisamos configurar um fluxo de entrada e um leitor para ler os dados do dispositivo. Primeiro, receberemos o stream legível da porta chamando port.readable. Como sabemos que receberemos o texto do dispositivo, vamos encaminhá-lo por um decodificador de texto. Em seguida, obteremos um leitor e iniciaremos a repetição de leitura.

Adicione o seguinte código ao seu projeto:

script.js - connect()

// CODELAB: Add code to read the stream here.
let decoder = new TextDecoderStream();
inputDone = port.readable.pipeTo(decoder.writable);
inputStream = decoder.readable;

reader = inputStream.getReader();
readLoop();

O loop de leitura é uma função assíncrona que é executada em uma repetição e aguarda conteúdo sem bloquear a linha de execução principal. Quando novos dados chegam, o leitor retorna duas propriedades: value e um booleano done. Se done for verdadeiro, a porta foi fechada ou não há mais dados de entrada.

Adicione o seguinte código ao seu projeto:

script.js - readLoop()

// CODELAB: Add read loop here.
while (true) {
  const { value, done } = await reader.read();
  if (value) {
    log.textContent += value + '\n';
  }
  if (done) {
    console.log('[readLoop] DONE', done);
    reader.releaseLock();
    break;
  }
}

Faça um teste

Nosso projeto agora pode se conectar ao dispositivo e anexar todos os dados recebidos do dispositivo ao elemento de registro.

  1. Recarregue a página.
  2. Clique no botão Conectar.
  3. Na caixa de diálogo do seletor de porta serial, selecione o dispositivo micro:bit BBC e clique em Conectar.
  4. Você verá o logotipo da Espruino:

93494fd58ea835eb.png

Configurar um stream de saída para enviar dados para a porta serial

A comunicação serial geralmente é bidirecional. Além de receber dados da porta serial, também queremos enviar dados para a porta serial. Assim como no fluxo de entrada, o texto só será enviado pelo stream de saída para o micro:bit.

Primeiro, crie um stream de codificador de texto e canalize-o para port.writeable.

script.js - connect()

// CODELAB: Add code setup the output stream here.
const encoder = new TextEncoderStream();
outputDone = encoder.readable.pipeTo(port.writable);
outputStream = encoder.writable;

Quando conectada por serial com o firmware Espruino, a placa micro:bit da BBC funciona como um loop de leitura-avaliação-impressão (REPL, na sigla em inglês) JavaScript, semelhante ao que você recebe em um shell do Node.js. Em seguida, precisamos fornecer um método para enviar dados ao stream. O código abaixo recebe um gravador do stream de saída e usa write para enviar cada linha. Cada linha enviada inclui um caractere de nova linha (\n) para instruir o micro:bit a avaliar o comando enviado.

script.js - writeToStream()

// CODELAB: Write to output stream
const writer = outputStream.getWriter();
lines.forEach((line) => {
  console.log('[SEND]', line);
  writer.write(line + '\n');
});
writer.releaseLock();

Para colocar o sistema em um estado conhecido e impedir que ele retorne os caracteres que enviamos, precisamos enviar CTRL+C e desativar o eco.

script.js - connect()

// CODELAB: Send CTRL-C and turn off echo on REPL
writeToStream('\x03', 'echo(false);');

Faça um teste

Nosso projeto agora pode enviar e receber dados do micro:bit. Vamos verificar se podemos enviar um comando corretamente:

  1. Recarregue a página.
  2. Clique no botão Conectar.
  3. Na caixa de diálogo do seletor de porta serial, selecione o dispositivo micro:bit BBC e clique em Conectar.
  4. Abra a guia Console no Chrome DevTools e digite writeToStream('console.log("yes")');.

A página exibirá algo parecido com isto:

a13187e7e6260f7f.png

5. Controlar a matriz de LED

Criar a string de grade da matriz

Para controlar a matriz de LED no micro:bit, precisamos chamar show(). Esse método mostra gráficos na tela LED 5x5 integrada. Ela usa um número binário ou uma string.

Iteraremos as caixas de seleção e geraremos uma matriz de 1s e 0s indicando qual está marcada e qual não está. Em seguida, precisamos inverter a matriz, porque a ordem das caixas de seleção é o oposto da ordem dos LEDs na matriz. Em seguida, convertemos a matriz em uma string e criamos o comando para enviar ao micro:bit.

script.js - sendGrid()

// CODELAB: Generate the grid
const arr = [];
ledCBs.forEach((cb) => {
  arr.push(cb.checked === true ? 1 : 0);
});
writeToStream(`show(0b${arr.reverse().join('')})`);

Marque as caixas de seleção para atualizar a matriz

Em seguida, precisamos detectar as alterações nas caixas de seleção e, se elas mudarem, enviar essas informações para o micro:bit. No código de detecção de recursos (// CODELAB: Add feature detection here.), adicione a seguinte linha:

script.js - DOMContentLoaded

initCheckboxes();

Também vamos redefinir a grade quando o micro:bit for conectado pela primeira vez, para que ele mostre uma carinha feliz. A função drawGrid() já foi fornecida. Essa função funciona de maneira semelhante a sendGrid(). ele usa uma matriz de 1s e 0s e marca as caixas de seleção conforme apropriado.

script.js - clickConnect()

// CODELAB: Reset the grid on connect here.
drawGrid(GRID_HAPPY);
sendGrid();

Faça um teste

Agora, quando a página abrir uma conexão com o micro:bit, ela vai enviar uma carinha feliz. Clique nas caixas de seleção para atualizar a tela na matriz de LED.

  1. Recarregue a página.
  2. Clique no botão Conectar.
  3. Na caixa de diálogo do seletor de porta serial, selecione o dispositivo micro:bit BBC e clique em Conectar.
  4. Um sorriso será mostrado na matriz de LED micro:bit.
  5. Desenhe um padrão diferente na matriz de LED mudando as caixas de seleção.

6. Conectar os botões micro:bit

Adicionar um evento de exibição aos botões micro:bit

Existem dois botões no micro:bit, um em cada lado da matriz de LED. O Espruino fornece uma função setWatch que envia um evento/callback quando o botão é pressionado. Como queremos escutar os dois botões, tornaremos nossa função genérica e faremos com que ela mostre os detalhes do evento.

script.js - watchButton()

// CODELAB: Hook up the micro:bit buttons to print a string.
const cmd = `
  setWatch(function(e) {
    print('{"button": "${btnId}", "pressed": ' + e.state + '}');
  }, ${btnId}, {repeat:true, debounce:20, edge:"both"});
`;
writeToStream(cmd);

Em seguida, precisamos conectar os dois botões (chamados BTN1 e BTN2 na placa micro:bit) toda vez que a porta serial for conectada ao dispositivo.

script.js - clickConnect()

// CODELAB: Initialize micro:bit buttons.
watchButton('BTN1');
watchButton('BTN2');

Faça um teste

Além de mostrar um rosto feliz quando conectado, pressionar um dos botões no micro:bit adiciona um texto à página indicando qual botão foi pressionado. O mais provável é que cada caractere esteja em sua própria linha.

  1. Recarregue a página.
  2. Clique no botão Conectar.
  3. Na caixa de diálogo do seletor de porta serial, selecione o dispositivo micro:bit BBC e clique em Conectar.
  4. Um sorriso será mostrado na matriz de LED de micro:bits.
  5. Pressione os botões no micro:bit e verifique se ele anexa um novo texto à página com os detalhes do botão pressionado.

7. Usar um fluxo de transformação para analisar os dados de entrada

Processamento básico de stream

Quando um dos botões micro:bit é pressionado, o micro:bit envia dados para a porta serial por um stream. Os streams são muito úteis, mas também podem ser um desafio, porque você não terá necessariamente todos os dados de uma vez e eles podem ser arbitrariamente divididos.

No momento, o app imprime o stream de entrada assim que ele chega (em readLoop). Na maioria dos casos, cada caractere está na própria linha, mas isso não é muito útil. O ideal é que o stream seja analisado em linhas individuais, e cada mensagem mostrada como uma linha própria.

Como transformar streams com TransformStream

Para isso, podemos usar um fluxo de transformação ( TransformStream), que possibilita analisar o fluxo de entrada e retornar dados analisados. Um stream de transformação pode ficar entre a origem do stream (neste caso, o micro:bit) e o que estiver consumindo o stream (neste caso, readLoop) e aplicar uma transformação arbitrária antes de ser finalmente consumida. Pense nisso como uma linha de montagem: à medida que um widget desce da linha, cada etapa da linha modifica o widget, de modo que, no momento em que ele chega ao destino final, ele é um widget totalmente funcional.

Para mais informações, consulte os conceitos da API Streams do MDN.

Transformar o stream com LineBreakTransformer

Vamos criar uma classe LineBreakTransformer, que dividirá um stream com base nas quebras de linha (\r\n). A classe precisa de dois métodos, transform e flush. O método transform é chamado sempre que novos dados são recebidos pelo stream. Ele pode enfileirar os dados ou salvá-los para mais tarde. O método flush é chamado quando o stream é fechado e processa os dados que ainda não foram processados.

No nosso método transform, adicionaremos novos dados ao container e, em seguida, verificaremos se há quebras de linha em container. Se houver, divida-o em uma matriz e faça iterações nas linhas, chamando controller.enqueue() para enviar as linhas analisadas.

script.js - LineBreakTransformer.transform()

// CODELAB: Handle incoming chunk
this.container += chunk;
const lines = this.container.split('\r\n');
this.container = lines.pop();
lines.forEach(line => controller.enqueue(line));

Quando o stream é fechado, simplesmente limpamos todos os dados restantes no contêiner usando enqueue.

script.js - LineBreakTransformer.flush()

// CODELAB: Flush the stream.
controller.enqueue(this.container);

Por fim, precisamos canalizar o stream de entrada pelo novo LineBreakTransformer. Nosso stream de entrada original só passou por TextDecoderStream, então precisamos adicionar mais pipeThrough para canalizá-lo pelo novo LineBreakTransformer.

script.js - connect()

// CODELAB: Add code to read the stream here.
let decoder = new TextDecoderStream();
inputDone = port.readable.pipeTo(decoder.writable);
inputStream = decoder.readable
  .pipeThrough(new TransformStream(new LineBreakTransformer()));

Faça um teste

Agora, quando você pressionar um dos botões micro:bit, os dados impressos deverão ser retornados em uma única linha.

  1. Recarregue a página.
  2. Clique no botão Conectar.
  3. Na caixa de diálogo do seletor de porta serial, selecione o dispositivo micro:bit BBC e clique em Conectar.
  4. Um sorriso será mostrado na matriz de LED micro:bit.
  5. Pressione os botões no micro:bit e verifique se aparece algo parecido com o seguinte:

6c2193880c748412.png

Transformar o stream com JSONTransformer

Podemos tentar analisar a string em JSON no readLoop, mas, em vez disso, vamos criar um transformador JSON muito simples que transformará os dados em um objeto JSON. Se os dados não forem um JSON válido, basta retornar o que veio.

script.js - JSONTransformer.transform

// CODELAB: Attempt to parse JSON content
try {
  controller.enqueue(JSON.parse(chunk));
} catch (e) {
  controller.enqueue(chunk);
}

Em seguida, direcione o stream pelo JSONTransformer, depois que ele passar pelo LineBreakTransformer. Isso permite manter o JSONTransformer simples, já que sabemos que o JSON será enviado apenas em uma única linha.

script.js - connect

// CODELAB: Add code to read the stream here.
let decoder = new TextDecoderStream();
inputDone = port.readable.pipeTo(decoder.writable);
inputStream = decoder.readable
  .pipeThrough(new TransformStream(new LineBreakTransformer()))
  .pipeThrough(new TransformStream(new JSONTransformer()));

Faça um teste

Agora, quando você pressionar um dos botões micro:bit, o [object Object] vai aparecer na página.

  1. Recarregue a página.
  2. Clique no botão Conectar.
  3. Na caixa de diálogo do seletor de porta serial, selecione o dispositivo micro:bit BBC e clique em Conectar.
  4. Um sorriso será mostrado na matriz de LED micro:bit.
  5. Pressione os botões no micro:bit e verifique se aparece algo parecido com o seguinte:

Responder ao pressionamento de botão

Para responder ao pressionamento do botão micro:bit, atualize readLoop para verificar se os dados recebidos são um object com uma propriedade button. Em seguida, chame buttonPushed para processar o pressionamento do botão.

script.js - readLoop()

const { value, done } = await reader.read();
if (value && value.button) {
  buttonPushed(value);
} else {
  log.textContent += value + '\n';
}

Quando um botão micro:bit é pressionado, a tela na matriz de LED precisa mudar. Use o seguinte código para definir a matriz:

script.js - buttonPushed()

// CODELAB: micro:bit button press handler
if (butEvt.button === 'BTN1') {
  divLeftBut.classList.toggle('pressed', butEvt.pressed);
  if (butEvt.pressed) {
    drawGrid(GRID_HAPPY);
    sendGrid();
  }
  return;
}
if (butEvt.button === 'BTN2') {
  divRightBut.classList.toggle('pressed', butEvt.pressed);
  if (butEvt.pressed) {
    drawGrid(GRID_SAD);
    sendGrid();
  }
}

Faça um teste

Agora, ao pressionar um dos botões micro:bit, a matriz de LED vai mudar para um rosto feliz ou triste.

  1. Recarregue a página.
  2. Clique no botão Conectar.
  3. Na caixa de diálogo do seletor de porta serial, selecione o dispositivo micro:bit BBC e clique em Conectar.
  4. Um sorriso será exibido na matriz de LED de micro:bits.
  5. Pressione os botões no micro:bit e verifique se a matriz de LED muda.

8. Como fechar a porta serial

A etapa final é conectar a funcionalidade de desconexão para fechar a porta quando o usuário terminar de usá-la.

Fechar a porta quando o usuário clicar no botão "Conectar/desconectar"

Quando o usuário clica no botão Conectar/desconectar, é necessário encerrar a conexão. Se a porta já estiver aberta, chame disconnect() e atualize a interface para indicar que a página não está mais conectada ao dispositivo serial.

script.js - clickConnect()

// CODELAB: Add disconnect code here.
if (port) {
  await disconnect();
  toggleUIConnected(false);
  return;
}

Fechar os streams e a porta

Na função disconnect, precisamos fechar o stream de entrada e o de saída e fechar a porta. Para fechar o stream de entrada, chame reader.cancel(). A chamada para cancel é assíncrona, então precisamos usar await para aguardar a conclusão:

script.js - disconnect()

// CODELAB: Close the input stream (reader).
if (reader) {
  await reader.cancel();
  await inputDone.catch(() => {});
  reader = null;
  inputDone = null;
}

Para fechar o stream de saída, receba um writer, chame close() e aguarde o objeto outputDone ser fechado:

script.js - disconnect()

// CODELAB: Close the output stream.
if (outputStream) {
  await outputStream.getWriter().close();
  await outputDone;
  outputStream = null;
  outputDone = null;
}

Por fim, feche a porta serial e aguarde o fechamento:

script.js - disconnect()

// CODELAB: Close the port.
await port.close();
port = null;

Faça um teste

Agora, você pode abrir e fechar a porta serial à vontade.

  1. Recarregue a página.
  2. Clique no botão Conectar.
  3. Na caixa de diálogo do seletor de porta serial, selecione o dispositivo micro:bit BBC e clique em Conectar.
  4. Um sorriso será mostrado na matriz de LED micro:bit
  5. Pressione o botão Desconectar e verifique se a matriz de LED apagará e se não há erros no console.

9. Parabéns

Parabéns! Você criou seu primeiro app da Web que usa a API Web Serial.

Fique de olho em https://goo.gle/fugu-api-tracker (link em inglês) para conhecer as novidades sobre a API Web Serial e todos os outros novos recursos da Web interessantes em que a equipe do Chrome está trabalhando.