Neste artigo, quero falar sobre um projeto de fim de semana pequeno e engraçado para transferir arquivos por meio de códigos QR animados. O projeto foi escrito em Go, usando Gomobile e Gopherjs - o mais recente para um aplicativo da Web para medir automaticamente as taxas de transferência de dados. Se você estiver interessado na idéia de transmitir dados através de códigos visuais, desenvolvendo aplicativos da Web que não sejam JS ou o Go - to Wellcome to Cat.

A idéia do projeto nasceu de uma tarefa específica para um aplicativo móvel - como transferir uma pequena porção de dados (~ 15 KB) para outro dispositivo de maneira mais simples e rápida, em condições de bloqueio de rede. O primeiro pensamento foi usar o Bluetooth, mas não é tão conveniente quanto parece - o processo relativamente longo e nem sempre funcional de detectar e emparelhar dispositivos é muito difícil para a tarefa. Uma boa idéia seria usar NFC (Near Field Communication), mas ainda existem muitos dispositivos nos quais o suporte a NFC é limitado ou inexistente. Precisávamos de algo mais simples e mais acessível.
E os códigos QR?
Códigos QR
O código QR (resposta rápida) é o tipo de código visual mais popular do mundo. Ele permite codificar até 3 KB de dados arbitrários e possui vários níveis de correção de erros, permitindo a leitura confiável de até um terço de um código fechado ou sujo.
Mas com os códigos QR, existem dois problemas:
- 3KB não é suficiente
- quanto mais dados forem codificados, maiores serão os requisitos de qualidade para a imagem digitalizar
Aqui está o código QR da 40ª versão (a maior densidade de gravação) com 1276 bytes:

Para a minha tarefa, eu precisava aprender a transferir ~ 15 KB de dados em dispositivos padrão (smartphones / tablets), para que a questão surgisse por si só - por que não animar a sequência de códigos QR e transferir os dados em pedaços?
Uma rápida pesquisa de implementações prontas levou a vários desses projetos - principalmente projetos em hackathons (embora houvesse uma tese de graduação ) - mas todos eles foram escritos em Java, Python ou JavaScript, que, infelizmente, tornaram o código praticamente não portável e não utilizado. Mas, dada a grande popularidade dos códigos QR e a baixa complexidade técnica da ideia, foi decidido escrever do zero no Go - uma linguagem multiplataforma, legível e rápida. Normalmente, a plataforma cruzada implica a capacidade de criar código binário para Windows, Mac e Linux, mas no meu caso também foi importante construí-lo para a web (gopherjs) e para sistemas móveis (iOS / Android). O Go fornece tudo pronto para uso com custo mínimo.
Também considerei opções alternativas para códigos visuais - como código HCCB ou JAB , mas para eles eu teria que escrever um scanner OpenCV, implementar um codificador / decodificador do zero e isso era demais para um projeto em um fim de semana. Os códigos QR round-robin (códigos de captura ) e seus equivalentes usados no Facebook, Kik e Snapchat permitem codificar muito menos informações, e a abordagem patenteada incrivelmente legal da Apple para emparelhar Apple Watch e iPhone - uma nuvem animada de partículas coloridas - também é otimizada para o efeito uau, e não abaixo da largura de banda máxima. Os códigos QR são integrados às câmeras SDK nativas do sistema operacional móvel, o que facilita muito o trabalho com eles.
TXQR
Assim, nasceu o projeto txqr (de transmissão e QR), que implementa uma biblioteca para codificação / decodificação de QR no Go puro e um protocolo para transmissão de dados.
A idéia principal é a seguinte: um cliente seleciona um arquivo ou dados a serem enviados, o programa no dispositivo divide o arquivo em pedaços, codifica cada um deles em quadros QR e os mostra em um loop infinito com uma determinada taxa de quadros até que o destinatário receba todos os dados. O protocolo é feito de forma que o destinatário possa começar de qualquer quadro, receber quadros QR em qualquer ordem - isso evita a necessidade de sincronizar a frequência da animação e a frequência da varredura. O receptor pode ser um dispositivo antigo, cuja potência permite decodificar 2 quadros por segundo e o remetente com um novo smartphone que produz animação em 120Hz, ou vice-versa, e isso não será um problema fundamental para o protocolo.
Isso é obtido da seguinte maneira - quando o arquivo é dividido em partes ( quadros adicionais), um prefixo com informações sobre o deslocamento relativo a todos os dados e o comprimento total - OFFSET/TOTAL|
(onde OFFSET e TOTAL são valores inteiros de deslocamento e comprimento, respectivamente). Atualmente, os dados binários são codificados no Base64, mas isso não é realmente necessário - a especificação QR permite não apenas codificar dados como binários, mas também otimizar partes diferentes dos dados para codificações diferentes (por exemplo, um prefixo com pequenas alterações pode ser codificado como alfanumérico e o restante do conteúdo - como o binário ), mas por simplicidade, o Base64 executou sua função perfeitamente.
Além disso, o tamanho e a frequência do quadro podem ser alterados dinamicamente, adaptando-se aos recursos do destinatário.

O protocolo em si é muito simples, e seu principal ponto negativo é que, para arquivos grandes (embora isso esteja além do escopo da tarefa, mas ainda assim), um quadro ignorado durante a verificação dobrará o tempo de verificação - o destinatário terá que esperar pelo ciclo completo novamente. Na teoria da codificação, existem soluções para esses casos - códigos-fonte , mas deixarei isso para um próximo fim de semana gratuito.
O ponto mais interessante foi escrever um aplicativo móvel que possa usar esse protocolo.
Gomobile
Se você não ouviu falar sobre o gomobile , este é um projeto que permite o uso de bibliotecas Go em projetos iOS e Android e torna esse procedimento simples e obsceno.
O processo padrão é o seguinte:
- você escreve código Go regular
- executar
gomobile bind ...
- copie os artefatos resultantes (
yourpackage.framework.
ou yourpackage.aar
) em seu projeto móvel - importe seu
yourpackage
e trabalhe com ele como em uma biblioteca comum
Você pode tentar como é fácil.
Portanto, escrevi rapidamente um aplicativo no Swift que verifica os códigos QR (graças a este maravilhoso artigo ) e os decodifica, cola-os e, quando o arquivo inteiro é recebido, mostra-o na janela de visualização.
Como um novato na Swift (apesar de ler o livro Swift 4), houve alguns momentos em que fiquei preso em algo simples, tentando descobrir como fazê-lo corretamente e, no final, a melhor solução era implementar essa funcionalidade no Go e usar via gomobile. Não me interpretem mal, o Swift é, de muitas maneiras, uma linguagem maravilhosa, mas, como a maioria das outras linguagens de programação, oferece muitas maneiras de fazer a mesma coisa e já possui um histórico decente de alterações incompatíveis com versões anteriores. Por exemplo, eu precisava fazer uma coisa simples - medir a duração de um evento com precisão de milissegundos. Uma pesquisa no Google e no StackOverflow levou a uma série de soluções diferentes, conflitantes e frequentemente desatualizadas, nenhuma das quais, no final, parecia bonita para mim ou correta para o compilador. Após 40 minutos de tempo gasto, acabei de time.Since(start) / time.Millisecond
outro método no pacote Go que chamava time.Since(start) / time.Millisecond
e usou seu resultado diretamente do Swift.
Também escrevi o utilitário do console txqr-ascii
para teste rápido de aplicativos. Ele codifica o arquivo e anima os códigos QR no terminal. No conjunto, funcionou surpreendentemente bem - eu poderia enviar uma imagem pequena em alguns segundos, mas assim que comecei a testar diferentes valores da taxa de quadros, o número de bytes em cada quadro QR e o nível de correção de erros no codificador QR, ficou claro que a solução do terminal não estava lida com a alta frequência (mais de 10) da animação e testar e medir manualmente os resultados é uma coisa desastrosa.
TXQR Tester

Para encontrar a combinação ideal de taxa de quadros, tamanho dos dados em um quadro QR e o nível de correção de erros entre os limites razoáveis desses valores, tive que executar mais de 1000 testes, alterando manualmente os parâmetros, aguardando um ciclo completo com o telefone na mão e escrevendo os resultados em uma placa. Claro, isso deve ser automatizado!
Aqui surgiu a idéia do próximo aplicativo - txqr-tester
. Inicialmente, planejei usar x / exp / shiny - uma estrutura experimental de interface do usuário para aplicativos de desktop nativos no Go, mas parece ter sido abandonada. Há cerca de um ano, eu tentei, e a impressão não era ruim - ela se encaixava perfeitamente em coisas de baixo nível. Mas hoje, o ramo principal nem sequer foi compilado. Parece que não há mais nenhum incentivo para investir no desenvolvimento de estruturas de desktop - uma tarefa complexa e complicada, com demanda quase zero hoje em dia, todas as soluções de interface do usuário já estão na Web há muito tempo.
Na programação da Web, como você sabe, as linguagens de programação começaram a entrar, graças ao WebAssembly, mas ainda são os primeiros passos para as crianças. É claro que ainda há JavaScript e complementos, mas os amigos não permitem que os amigos escrevam aplicativos em JavaScript, então eu decidi usar minha descoberta recente - a estrutura Vecty , que permite escrever frontends no Go puro, que são convertidos automaticamente em JavaScript usando um adulto muito bom e surpreendentemente bem trabalhando. Projeto GopherJS .
Vecty e GopherJS

Na minha vida, não recebi tanto prazer com o desenvolvimento de interfaces front-end.
Um pouco mais tarde, pretendo escrever mais alguns artigos sobre minha experiência no desenvolvimento de frontends no Vecty, incluindo aplicativos WebGL, mas o ponto principal é que, depois de vários projetos no React, Angulars e Ember, escrever um frontend em uma linguagem de programação simples e atenciosa é uma lufada de ar fresco. ar! Posso escrever front-ends muito legais em pouco tempo sem escrever uma única linha em JavaScript!
Para iniciantes, é assim que você inicia um novo projeto no Vecty (sem geradores de código de "projeto inicial" que criam toneladas de arquivos e pastas) - apenas main.go:
ackage main import ( "github.com/gopherjs/vecty" ) func main() { app := NewApp() vecty.SetTitle("My App") vecty.AddStylesheet() vecty.RenderBody(app) }
Um aplicativo, como qualquer componente da interface do usuário, é apenas um tipo: uma estrutura que inclui o tipo vecty.Core
e deve implementar a interface vecty.Component
(que consiste em um método Render()
). E isso é tudo! Então você trabalha com tipos, métodos, funções, bibliotecas para trabalhar com o DOM e assim por diante - sem mágica oculta e novos termos e conceitos. Aqui está o código simplificado para a página principal:
/ App is a top-level app component. type App struct { vecty.Core session *Session settings *Settings
Você provavelmente está olhando o código agora e pensando - quanto é infundado o trabalho com o DOM! Eu também pensava assim no começo, mas assim que comecei a trabalhar, percebi como era conveniente:
- Não há mágica - cada bloco (marcação ou HTML) é apenas uma variável do tipo desejado, com limites claros onde você pode colocar algo, graças à digitação estática.
- Não há tags de abertura / fechamento que você precise lembrar de alterar ao refatorar ou use o IDE que faz isso para você.
- De repente, a estrutura fica clara - por exemplo, eu nunca entendi por que no React até a 16ª versão era impossível retornar várias tags de um componente - isso é "apenas uma string". Vendo como isso é feito no Vecty, de repente ficou claro onde as raízes dessa restrição cresceram no React. Mesmo assim, não está claro, no entanto, por que, após o React 16, isso se tornou possível, mas não necessário.
Em geral, assim que você tentar esta abordagem para trabalhar com o DOM, suas vantagens se tornarão muito óbvias. Também há desvantagens, é claro, mas após as desvantagens dos métodos usuais, elas são invisíveis.
O Vecty é chamado de estrutura do tipo React, mas isso não é totalmente verdade. Existe uma biblioteca GopherJS nativa para React - myitcv.io/react , mas não acho que seja uma boa ideia repetir as soluções arquitetônicas do React para o Go. Quando você escreve um frontend no Vecty, de repente fica claro o quão mais fácil é realmente. De repente, toda essa mágica oculta e os novos termos e conceitos que toda estrutura JavaScript inventa tornam-se supérfluos - eles são apenas mais complexidade , nada mais. Tudo o que é necessário é descrever clara e claramente os componentes, seu comportamento e conectá-los juntos - tipos, métodos e funções, e isso é tudo.
Para CSS, usei uma estrutura Bulma surpreendentemente decente - ela possui nomes de classe muito claros e uma boa estrutura, e o código declarado da interface do usuário é muito legível.
A verdadeira mágica, no entanto, começa quando você compila o código Go em JavaScript. Parece muito intimidador, mas, na verdade, você chama apenas gopherjs build
e, em menos de um segundo, você tem um arquivo JavaScript gerado automaticamente pronto para ser incluído em sua página HTML básica (um aplicativo regular consiste apenas em uma tag de corpo vazia e na inclusão deste Script JS). Quando executei este comando pela primeira vez, esperava ver muitas mensagens, avisos e erros, mas não - funciona extraordinariamente rápido e silenciosamente, só imprime uma linha em caso de erros de compilação gerados pelo compilador Go, por isso é muito claro. Mas foi ainda mais bacana ver erros no console do navegador, com rastreamentos de pilha apontando para arquivos .go e a linha correta! Isso é muito legal.
Testando parâmetros de animação QR
Por várias horas, eu tinha um aplicativo da Web pronto que me permitia alterar rapidamente os parâmetros para o teste:
- FPS - taxa de quadros
- Tamanho do quadro QR - quantos bytes devem existir em cada quadro
- QR Recovery Level - Nível de correção de erros QR
e execute o teste automaticamente.

O aplicativo móvel, é claro, também precisava ser automatizado - ele precisava entender quando a próxima rodada começa com novos parâmetros, entender quando a recepção leva muito tempo e interromper a rodada, enviar os resultados para a aplicação e assim por diante.
O problema foi que o aplicativo da Web, sendo iniciado na caixa de proteção do navegador, não pode criar novas conexões e, se não me engano, a única possibilidade de uma conexão ponto a ponto real com o navegador é apenas pelo WebRTC (não é necessário perfurar o NAT ), mas era muito complicado. O aplicativo da web pode ser apenas um cliente.
A solução foi simples - o serviço web Go que entregou o aplicativo Web (e iniciou o navegador no URL desejado) também lançou o proxy WebSocket para dois clientes. Assim que dois clientes ingressam, ele envia mensagens de forma transparente de uma conexão para outra, permitindo que os clientes (aplicativo Web e cliente móvel) se comuniquem diretamente. Eles devem ser para isso, em uma rede WIFI, é claro.
Houve um problema de como informar a um dispositivo móvel onde, de fato, conectar-se e foi resolvido usando ... Código QR!
O processo de teste é assim:
- um aplicativo móvel está procurando um código QR com um marcador de início e um link para um proxy WebSocket
- assim que o token é lido, o aplicativo se conecta a esse proxy WebSocket
- o aplicativo da Web (já conectado ao proxy) entende que o aplicativo móvel está pronto e exibe um código QR com o marcador "pronto para a próxima rodada?"
- o aplicativo móvel reconhece o sinal, redefine o decodificador e envia uma mensagem "sim" via WebSocket.
- o aplicativo Web, após receber a confirmação, gera uma nova animação QR e a torce até receber os resultados ou o tempo limite.
- os resultados são adicionados a uma placa ao lado da qual você pode fazer o download imediatamente como CSV

Como resultado, tudo o que me restou foi simplesmente colocar o telefone em um tripé, iniciar o aplicativo e, em seguida, os dois programas fizeram todo o trabalho sujo, comunicando-se educadamente através dos códigos QR e WebSocket :)

No final, baixei o arquivo CSV com os resultados, o levei ao RStudio e ao Plotly Online Chart Maker e analisei os resultados.
Resultados
O ciclo completo de testes leva cerca de 4 horas (infelizmente, a parte mais difícil do processo - gerar uma imagem GIF animada com quadros QR, teve que ser executada no navegador e, como o código resultante ainda está em JS, apenas um processador é usado) durante que era necessário observar para que a tela não ficasse repentinamente em branco ou algum aplicativo não fechasse a janela com o aplicativo da web. Os seguintes parâmetros foram testados:
- FPS - 3 a 12
- O tamanho do quadro QR é de 100 a 1000 bytes (em incrementos de 50)
- Todos os 4 níveis de correção de erro QR (baixo, médio, alto, mais alto)
- Tamanho do arquivo transferido - 13 KB de bytes gerados aleatoriamente
Algumas horas depois, baixei o CSV e comecei a analisar os resultados.
Uma imagem é mais importante que mil palavras, mas visualizações 3D interativas são mais importantes que mil imagens. Aqui está uma visualização dos resultados (clicáveis):

O melhor resultado foi 1,4 segundos, ou seja, aproximadamente 9KB / s! Esse resultado foi registrado com uma frequência de 11 quadros por segundo, um tamanho de quadro de 850 bytes e um nível médio de correção de erros. Na maioria dos casos, no entanto, a essa velocidade, o decodificador da câmera pulou alguns quadros e teve que aguardar a próxima repetição do quadro perdido, que teve um efeito muito negativo nos resultados - em vez de dois segundos, ele poderia facilmente gerar 15 ou um tempo limite definido para 30 segundos.
Aqui estão os gráficos da dependência dos resultados em variáveis variáveis:
Tempo / tamanho do quadro

Como você pode ver, em valores baixos do número de bytes em cada quadro, a codificação em excesso é muito grande e o tempo total de leitura, respectivamente. Há um mínimo local de 500 a 600 bytes por quadro, mas os valores próximos a ele ainda levam a quadros perdidos. O melhor resultado foi observado em 900 bytes, mas 1000 e acima é quase garantida a perda de quadros.
Time / FPS

O valor do número de quadros por segundo, para minha surpresa, não teve um efeito muito grande - valores pequenos aumentaram muito o tempo de transmissão geral e valores grandes aumentaram a probabilidade de um quadro perdido. O valor ideal, a julgar por esses testes, está na região de 6-7 quadros por segundo para os dispositivos nos quais eu testei.
Nível de correção de hora / erro

O nível de correção de erros mostrou uma relação clara entre o tempo de transferência do arquivo e o nível de redundância, o que não é surpreendente. O vencedor aqui é o baixo nível de correção (L) - quanto menos dados redundantes, mais legível o código QR do scanner com o mesmo tamanho de dados. De fato, a redundância não é necessária para este experimento, mas o padrão não oferece essa opção.
Obviamente, para dados mais objetivos, esse teste deve ser executado centenas e milhares de vezes, em diferentes dispositivos e telas diferentes, mas, para minha experiência de fim de semana, foi um resultado mais que suficiente.
Conclusões
Esse projeto divertido provou que a transferência de dados unidirecional por meio de códigos animados é certamente possível e, para situações em que você precisa transferir uma pequena quantidade na ausência de qualquer tipo de rede, é bastante adequado. Embora meu resultado máximo tenha sido de cerca de 9 KB / s, na maioria dos casos a velocidade real foi de 1-2 KB / s.
Também gostei muito de usar o Gomobile e o GopherJS com o Vecty como uma ferramenta rotineira de solução de problemas. São projetos muito maduros, com excelente velocidade de trabalho e, na maioria dos casos, dando experiência "simplesmente funciona".
Por fim, ainda admiro o quão produtivo você pode ser com o Go quando você sabe claramente o que deseja implementar - o ciclo extremamente curto “mudança” - “montagem” - “verificação” permite que você experimente muitas e muitas vezes código simples e a falta de hierarquia de classes na estrutura O programa torna possível refatorá-los de maneira fácil e indolor em movimento, e a fantástica plataforma cruzada incorporada ao idioma desde o início permite escrever o código uma vez e usá-lo no servidor, no cliente da Web e no aplicativo móvel nativo. Ao mesmo tempo, apesar do desempenho mais do que suficiente, ainda há muito espaço para otimização e aceleração.
Portanto, se você nunca experimentou o Gomobile ou o GopherJS - recomendo que tente na próxima oportunidade. Levará uma hora do seu tempo, mas poderá abrir uma nova camada de oportunidades no desenvolvimento para a Web ou para dispositivos móveis. Sinta-se livre para experimentar!
Referências