En este artículo quiero hablar sobre un pequeño y divertido proyecto de fin de semana para transferir archivos a través de códigos QR animados. El proyecto está escrito en Go, usando Gomobile y Gopherjs, lo último para una aplicación web para medir automáticamente las tasas de transferencia de datos. Si está interesado en la idea de transmitir datos a través de códigos visuales, desarrollar aplicaciones web que no estén en JS o en la verdadera plataforma multiplataforma Go - to Wellcome to Cat.

La idea del proyecto nació de una tarea específica para una aplicación móvil: cómo transferir una pequeña porción de datos (~ 15 KB) a otro dispositivo de la manera más simple y rápida, en condiciones de bloqueo de red. La primera idea fue usar Bluetooth, pero no es tan conveniente como parece: el proceso relativamente largo y no siempre activo de detección y emparejamiento de dispositivos es demasiado difícil para la tarea. Una buena idea sería usar NFC (Near Field Communication), pero todavía hay demasiados dispositivos en los que la compatibilidad con NFC es limitada o inexistente. Necesitábamos algo más simple y más asequible.
¿Qué pasa con los códigos QR?
Códigos QR
El código QR (respuesta rápida) es el tipo de código visual más popular del mundo. Le permite codificar hasta 3 KB de datos arbitrarios y tiene varios niveles de corrección de errores, lo que le permite leer con confianza incluso un tercio de un código cerrado o sucio.
Pero con los códigos QR hay dos problemas:
- 3 KB no es suficiente
- Cuantos más datos estén codificados, mayores serán los requisitos de calidad para escanear la imagen
Aquí está el código QR de la versión 40 (la mayor densidad de grabación) con 1276 bytes:

Para mi tarea, necesitaba aprender cómo transferir ~ 15 KB de datos en dispositivos estándar (teléfonos inteligentes / tabletas), por lo que la pregunta surgió por sí misma: ¿por qué no animar la secuencia de códigos QR y transferir los datos en pedazos?
Una búsqueda rápida de implementaciones preparadas condujo a varios proyectos de este tipo , principalmente proyectos en hackatones (aunque había una tesis de graduación ), pero todos fueron escritos en Java, Python o JavaScript, lo que, desafortunadamente, hizo que el código fuera prácticamente inportable y sin usar. Pero dada la gran popularidad de los códigos QR y la baja complejidad técnica de la idea, se decidió escribir desde cero en Go, un lenguaje multiplataforma, legible y rápido. Por lo general, multiplataforma implica la capacidad de construir código binario para Windows, Mac y Linux, pero en mi caso también fue importante construirlo para la web (gopherjs) y para sistemas móviles (iOS / Android). Go lo da todo fuera de la caja con un costo mínimo.
También consideré opciones alternativas para códigos visuales, como HCCB o JAB Code , pero para ellos tendría que escribir un escáner OpenCV, implementar un codificador / decodificador desde cero y esto fue demasiado para un proyecto para un fin de semana. Los códigos QR redondos (códigos de tiro ) y sus homólogos utilizados en Facebook, Kik y Snapchat, le permiten codificar mucha menos información, y el enfoque patentado increíblemente genial de Apple para emparejar Apple Watch y iPhone, una nube animada de partículas coloridas , también está optimizado para el efecto sorpresa, y no bajo el ancho de banda máximo. Los códigos QR están integrados en las cámaras SDK nativas del sistema operativo móvil, lo que facilita enormemente el trabajo con ellos.
TXQR
Así nació el proyecto txqr (de Tx - transmisión y QR), que implementa una biblioteca para codificar / decodificar QR en Go puro y un protocolo para transmitir datos.
La idea principal es la siguiente: un cliente selecciona un archivo o datos para enviar, el programa en el dispositivo divide el archivo en pedazos, codifica cada uno de ellos en cuadros QR y los muestra en un bucle sin fin con una velocidad de cuadro dada hasta que el destinatario reciba todos los datos. El protocolo está hecho de tal manera que el destinatario puede comenzar desde cualquier fotograma, recibir fotogramas QR en cualquier orden; esto elimina la necesidad de sincronizar la frecuencia de animación y la frecuencia de escaneo. El receptor puede ser un dispositivo antiguo, cuya potencia le permite decodificar 2 cuadros por segundo, y el remitente con un nuevo teléfono inteligente que produce animación de 120Hz, o viceversa, y esto no será un problema fundamental para el protocolo.
Esto se logra de la siguiente manera: cuando el archivo se divide en partes ( cuadros adicionales), un prefijo con información sobre el desplazamiento relativo a todos los datos y la longitud total: OFFSET/TOTAL|
(donde OFFSET y TOTAL son valores enteros de desplazamiento y longitud, respectivamente). Los datos binarios todavía están codificados en Base64, pero esto no es realmente necesario: la especificación QR permite no solo codificar datos como binarios, sino también optimizar diferentes partes de los datos para diferentes codificaciones (por ejemplo, un prefijo con cambios menores puede codificarse como alfanumérico , y el resto del contenido - como binario ), pero por simplicidad, Base64 realizó su función perfectamente.
Además, el tamaño del cuadro y la frecuencia pueden incluso cambiarse dinámicamente, adaptándose a las capacidades del destinatario.

El protocolo en sí es muy simple, y su principal inconveniente es que para archivos grandes (aunque esto está más allá del alcance de la tarea, pero aún así), un cuadro omitido durante el escaneo duplicará el tiempo de escaneo: el destinatario tendrá que esperar el ciclo completo nuevamente. En la teoría de la codificación hay soluciones para tales casos: códigos fuente , pero lo dejaré para algunos de los siguientes días libres.
El punto más interesante fue escribir una aplicación móvil que pueda usar este protocolo.
Gomobile
Si no ha escuchado sobre gomobile , entonces este es un proyecto que le permite usar bibliotecas Go en proyectos de iOS y Android y lo convierte en un procedimiento simple obsceno.
El proceso estándar es el siguiente:
- escribes código Go regular
- ejecutar
gomobile bind ...
- copie los artefactos resultantes (
yourpackage.framework.
o yourpackage.aar
) en su proyecto móvil - importa tu
yourpackage
y trabaja con él como con una biblioteca normal
Puedes probar lo fácil que es.
Por lo tanto, escribí rápidamente una aplicación en Swift que escanea códigos QR (gracias a este maravilloso artículo ) y los decodifica, los pega y, cuando se recibe el archivo completo, lo muestra en la ventana de vista previa.
Como novato en Swift (aunque leí el libro de Swift 4), hubo algunos momentos en que me quedé atrapado en algo simple, tratando de descubrir cómo hacerlo correctamente y, al final, la mejor solución fue implementar esta funcionalidad en Go y usar a través de gomobile. No me malinterpreten, Swift es en muchos sentidos un lenguaje maravilloso, pero, como la mayoría de los otros lenguajes de programación, proporciona demasiadas formas de hacer lo mismo y ya tiene un historial decente de cambios incompatibles con versiones anteriores. Por ejemplo, necesitaba hacer algo simple: medir la duración de un evento con una precisión de milisegundos. Una búsqueda en Google y StackOverflow condujo a una serie de soluciones diferentes, contradictorias y a menudo desactualizadas, ninguna de las cuales, al final, me pareció hermosa o correcta para el compilador. Después de 40 minutos de tiempo invertido, acabo de hacer otro método en el paquete Go que llamó a time.Since(start) / time.Millisecond
y usé su resultado directamente de Swift.
También escribí la utilidad de consola txqr-ascii
para pruebas rápidas de aplicaciones. Codifica el archivo y anima los códigos QR en el terminal. En conjunto, funcionó sorprendentemente bien: pude enviar una imagen pequeña en unos segundos, pero tan pronto como comencé a probar diferentes valores de la velocidad de fotogramas, el número de bytes en cada fotograma QR y el nivel de corrección de errores en el codificador QR, quedó claro que la solución del terminal no era hace frente a la alta frecuencia (más de 10) de la animación, y que probar y medir los resultados manualmente es algo desastroso.
Probador TXQR

Para encontrar la combinación óptima de velocidad de cuadros, tamaño de datos en un cuadro QR y el nivel de corrección de errores entre los límites razonables de estos valores, tuve que ejecutar más de 1000 pruebas, cambiar manualmente los parámetros, esperar un ciclo completo con el teléfono en la mano y escribir los resultados en una placa. ¡Por supuesto, esto debe ser automatizado!
Aquí surgió la idea de la próxima aplicación: txqr-tester
. Inicialmente, planeé usar x / exp / shiny , un marco de IU experimental para aplicaciones de escritorio nativas en Go, pero parece estar abandonado. Hace aproximadamente un año lo probé, y la impresión no era mala: encajaba perfectamente para cosas de bajo nivel. Pero hoy, la rama maestra ni siquiera ha sido compilada. Parece que ya no hay ningún incentivo para invertir en el desarrollo de marcos de escritorio: una tarea compleja y engorrosa, con una demanda casi nula en la actualidad, todas las soluciones de IU se han trasladado a la web hace mucho tiempo.
En la programación web, como saben, los lenguajes de programación acaban de comenzar a ingresar, gracias a WebAssembly, pero estos siguen siendo los primeros pasos para los niños. Por supuesto, también hay JavaScript y complementos, pero los amigos no permiten que sus amigos escriban aplicaciones en JavaScript, por lo que decidí usar mi descubrimiento reciente: el marco Vecty , que le permite escribir interfaces en Pure Go, que se convierten automáticamente a JavaScript utilizando un sistema muy adulto y sorprendentemente bien funcional. Proyecto GopherJS .
Vecty y GopherJS

En mi vida, no he recibido tanto placer del desarrollo de interfaces front-end.
Un poco más tarde, planeo escribir un par de artículos más sobre mi experiencia en el desarrollo de interfaces en Vecty, incluidas las aplicaciones WebGL, pero la conclusión es que después de varios proyectos en React, Angulars y Ember, escribir una interfaz en un lenguaje de programación simple y reflexivo es un soplo fresco. aire! ¡Puedo escribir frontales muy bonitos en poco tiempo sin escribir una sola línea en JavaScript!
Para empezar, así es como se inicia un nuevo proyecto en Vecty (no hay generadores de código de "proyecto inicial" que creen toneladas de archivos y carpetas), solo main.go:
ackage main import ( "github.com/gopherjs/vecty" ) func main() { app := NewApp() vecty.SetTitle("My App") vecty.AddStylesheet() vecty.RenderBody(app) }
Una aplicación, como cualquier componente de la interfaz de usuario, es solo un tipo: una estructura que incluye el tipo vecty.Core
y debe implementar la interfaz vecty.Component
(que consiste en un método Render()
). ¡Y eso es todo! Luego, trabaja con tipos, métodos, funciones, bibliotecas para trabajar con DOM, etc., sin magia oculta y nuevos términos y conceptos. Aquí está el código simplificado para la página principal:
/ App is a top-level app component. type App struct { vecty.Core session *Session settings *Settings
Probablemente esté viendo el código ahora y pensando: ¡cuánto es el trabajo infundado con el DOM! También lo pensé al principio, pero tan pronto como comencé a trabajar, me di cuenta de lo conveniente que era:
- No hay magia: cada bloque (marcado o HTML) es solo una variable del tipo deseado, con límites claros donde puede colocar algo, gracias a la escritura estática.
- No hay etiquetas de apertura / cierre que deba recordar cambiar cuando refactorice, o use el IDE que lo hace por usted.
- La estructura de repente se vuelve clara, por ejemplo, nunca entendí por qué en React hasta la 16ª versión era imposible devolver varias etiquetas de un componente, esto es "solo una cadena". Al ver cómo se hace esto en Vecty, de repente se hizo evidente dónde crecieron las raíces de esa restricción en React. De todos modos, no está claro, sin embargo, por qué después de React 16 se hizo posible, pero no necesario.
En general, tan pronto como pruebe este enfoque para trabajar con el DOM, sus ventajas serán muy obvias. También hay desventajas, por supuesto, pero después de las desventajas de los métodos habituales, son invisibles.
Vecty se llama un marco similar a React, pero esto no es del todo cierto. Hay una biblioteca nativa de GopherJS para React: myitcv.io/react , pero no creo que sea una buena idea repetir las soluciones arquitectónicas de React para Go. Cuando escribes una interfaz en Vecty, de repente se vuelve claro lo fácil que es realmente. De repente, toda esta magia oculta y los nuevos términos y conceptos que inventa cada marco de JavaScript se vuelven superfluos: solo se les agrega complejidad , nada más. Todo lo que se necesita es describir clara y claramente los componentes, su comportamiento y conectarlos entre sí: tipos, métodos y funciones, y eso es todo.
Para CSS, utilicé un marco de Bulma sorprendentemente decente: tiene un nombre de clase muy claro y una buena estructura, y el código de IU declarativo con él es muy legible.
Sin embargo, la verdadera magia comienza cuando compila el código Go en JavaScript. Suena muy intimidante, pero, de hecho, simplemente llama a gopherjs build
y en menos de un segundo, tiene un archivo JavaScript generado automáticamente listo para ser incluido en su página HTML básica (una aplicación regular consiste solo en una etiqueta de cuerpo vacía y la inclusión de este Guión JS). Cuando ejecuté este comando por primera vez, esperaba ver muchos mensajes, advertencias y errores, pero no, funciona fantásticamente de manera rápida y silenciosa, solo imprime archivos de una sola línea en el caso de errores de compilación generados por el compilador Go, por lo que son muy claros. ¡Pero fue aún más genial ver errores en la consola del navegador, con rastros de pila apuntando a archivos .go y la línea correcta! Esto es genial
Prueba de parámetros de animación QR
Durante varias horas tuve una aplicación web lista que me permitió cambiar rápidamente los parámetros para las pruebas:
- FPS - velocidad de fotogramas
- Tamaño de trama QR: cuántos bytes debe haber en cada trama
- Nivel de recuperación QR: nivel de corrección de errores QR
y ejecuta la prueba automáticamente.

La aplicación móvil, por supuesto, también tenía que ser automatizada: tenía que comprender cuándo comienza la próxima ronda con nuevos parámetros, comprender cuándo la recepción toma demasiado tiempo y romper la ronda, enviar los resultados a la aplicación, etc.
El problema es que la aplicación web, mientras se ejecuta en el entorno limitado del navegador, no puede crear nuevas conexiones, y si no me equivoco, la única posibilidad de una conexión de igual a igual real al navegador es solo a través de WebRTC (no necesito perforar a través de NAT ), pero era demasiado engorroso. La aplicación web solo podría ser un cliente.
La solución fue simple: el servicio web Go que entregó la aplicación web (y lanzó el navegador a la URL deseada) también lanzó el proxy WebSocket para dos clientes. Tan pronto como se unen dos clientes, envía mensajes transparentes de una conexión a otra, permitiendo que los clientes (aplicación web y cliente móvil) se comuniquen directamente. Deben ser para esto, en una red WIFI, por supuesto.
Hubo un problema de cómo decirle a un dispositivo móvil dónde, de hecho, conectarse, y se resolvió usando ... ¡código QR!
El proceso de prueba se ve así:
- una aplicación móvil está buscando un código QR con un marcador de inicio y un enlace a un proxy de WebSocket
- tan pronto como se lee el token, la aplicación se conecta a este proxy de WebSocket
- la aplicación web (ya conectada al proxy) entiende que la aplicación móvil está lista y muestra un código QR con el marcador "¿listo para la próxima ronda?"
- la aplicación móvil reconoce la señal, restablece el decodificador y envía un mensaje "sí" a través de WebSocket.
- la aplicación web, después de recibir la confirmación, genera una nueva animación QR y la retuerce hasta que recibe los resultados o el tiempo de espera.
- los resultados se agregan a una placa junto a la cual puede descargar inmediatamente como CSV

Como resultado, todo lo que me quedó fue simplemente poner el teléfono en un trípode, iniciar la aplicación, y luego los dos programas hicieron todo el trabajo sucio, comunicándose cortésmente a través de códigos QR y WebSocket :)

Al final, descargué el archivo CSV con los resultados, lo conduje a RStudio y Plotly Online Chart Maker y analicé los resultados.
Resultados
El ciclo de prueba completo dura aproximadamente 4 horas (desafortunadamente, la parte más difícil del proceso: generar una imagen GIF animada con marcos QR, tuvo que ejecutarse en el navegador, y dado que el código resultante todavía está en JS, solo se usa un procesador), durante lo cual, tenía que mirar para que la pantalla no se quedara en blanco de repente o alguna aplicación no cerrara la ventana con la aplicación web. Se probaron los siguientes parámetros:
- FPS: de 3 a 12
- El tamaño de la trama QR es de 100 a 1000 bytes (en incrementos de 50)
- Los 4 niveles de corrección de errores QR (bajo, medio, alto, más alto)
- Tamaño de archivo transferido: 13 KB de bytes generados aleatoriamente
Unas horas más tarde descargué CSV y comencé a analizar los resultados.
Una imagen es más importante que mil palabras, pero las visualizaciones 3D interactivas son más importantes que mil imágenes. Aquí hay una visualización de los resultados (clicable):

¡El mejor resultado fue 1.4 segundos, que es aproximadamente 9 KB / s! Este resultado se registró a una frecuencia de 11 cuadros por segundo, un tamaño de cuadro de 850 bytes y un nivel promedio de corrección de errores. Sin embargo, en la mayoría de los casos, a esta velocidad, el decodificador de la cámara se saltó algunos fotogramas y tuvo que esperar la próxima repetición del fotograma perdido, lo que tuvo un efecto muy negativo en los resultados; en lugar de dos segundos, podría resultar fácilmente 15, o un tiempo de espera establecido en 30 segundos.
Aquí están los gráficos de la dependencia de los resultados en variables variables:
Tiempo / tamaño del cuadro

Como puede ver, a valores bajos del número de bytes en cada cuadro, el exceso de codificación es demasiado grande y el tiempo de lectura total, respectivamente, también. Hay un mínimo local de 500-600 bytes por cuadro, pero los valores al lado aún conducen a cuadros perdidos. El mejor resultado se observó a 900 bytes, pero 1000 y más es casi una pérdida de trama garantizada.
Tiempo / FPS

Para mi sorpresa, el valor del número de cuadros por segundo no tuvo un efecto muy grande: los valores pequeños aumentaron demasiado el tiempo de transmisión general y los grandes aumentaron la probabilidad de un cuadro perdido. El valor óptimo, a juzgar por estas pruebas, está en la región de 6-7 cuadros por segundo para los dispositivos en los que probé.
Nivel de corrección de tiempo / error

El nivel de corrección de errores mostró una relación clara entre el tiempo de transferencia de archivos y el nivel de redundancia, lo cual no es sorprendente. El claro ganador aquí es el bajo nivel de corrección (L): cuanto menos datos redundantes, más legible es el código QR para el escáner con el mismo tamaño de datos. De hecho, no se necesita redundancia para este experimento, pero el estándar no ofrece tal opción.
Por supuesto, para obtener datos más objetivos, esta prueba debe ejecutarse cientos y miles de veces, en diferentes dispositivos y diferentes pantallas, pero para mi experimento de fin de semana, fue un resultado más que suficiente.
Conclusiones
Este divertido proyecto demostró que la transferencia de datos unidireccional a través de códigos animados es ciertamente posible, y para situaciones en las que necesita transferir una pequeña cantidad en ausencia de cualquier tipo de redes, es bastante adecuada. Aunque mi resultado máximo fue de aproximadamente 9 KB / s, en la mayoría de los casos la velocidad real fue de 1-2 KB / s.
También me gustó mucho usar Gomobile y GopherJS con Vecty como herramienta de rutina para resolver problemas. Estos son proyectos muy maduros, con excelente velocidad de trabajo y, en la mayoría de los casos, dando experiencia "simplemente funciona".
Al final, todavía admiro lo productivo que puede ser con Go cuando sabe claramente lo que quiere implementar: el ciclo extremadamente corto "cambio" - "ensamblado" - "verificación" le permite experimentar mucho y, a menudo, un código simple y la falta de una jerarquía de clases en la estructura Los programas hacen posible refactorizarlos fácilmente y sin problemas sobre la marcha, y la fantástica plataforma cruzada incrustada en el lenguaje desde el principio le permite escribir el código una vez y usarlo en el servidor, en el cliente web y en la aplicación móvil nativa. Al mismo tiempo, a pesar del rendimiento más que suficiente de fábrica, todavía hay mucho espacio para la optimización y la aceleración.
Entonces, si nunca ha probado Gomobile o GopherJS, le recomiendo que lo intente en la próxima oportunidad. Le tomará una hora de su tiempo, pero puede abrir una nueva capa de oportunidades en el desarrollo web o móvil. ¡Siéntase libre de probar!
Referencias