Передача даних через анімовані QR на Gomobile і GopherJS

У даній статті я хочу розповісти про невеликому і цікавому проекті вихідного дня по передачі файлів через анімовані QR коди. Проект написаний на Go, з використанням Gomobile і Gopherjs – останній для веб-додатки для автоматичного виміру швидкості передачі даних. Якщо вам цікава ідея передачі даних через візуальні коди, розробка веб-додатків на PHP або справжня кросплатформеність Go — велкам під кат.

 

 

Ідея проекту народилася з конкретної задачі для мобільного застосування – як найбільш просто і швидко передати невелику порцію даних (~15КБ) в інше пристрій, в умовах блокування мережі. Першою думкою було використовувати Bluetooth, але це не так зручно, як здається – відносно довгий і не завжди працює процес виявлення і спаровування пристроїв дуже ускладнює задачу. Непогана ідея була б використовувати NFC (Near Field Communication), але досі надто багато пристроїв, яких підтримка NFC обмежена або відсутня взагалі. Потрібно було щось простіше і доступніше.

 

Як щодо QR кодів?

QR коди

QR (Quick Response) код – це найпопулярніший у світі вид візуальних кодів. Він дозволяє кодувати до 3КБ довільних даних і має різні рівні корекції помилок, дозволяючи впевнено читати навіть на третину закритий чи забруднений код.

 

Але з QR кодами дві проблеми:

  • 3КБ недостатньо
  • чим більше даних закодовано, тим вище вимоги до якості картинки для сканування

 

Ось так виглядає QR-код 40-й версії (найвища щільність запису) з 1276 байтами:

 

Для мого завдання потрібно було навчитися передавати ~15KB даних, на стандартних пристроях (смартфонах/планшетах), тому сам собою виникло питання – а чому б не анімувати послідовність QR кодів і передати дані шматками?

Швидкий пошук по вже готових реалізацій навів на кілька таких проектів – в основному проекти на хакатонах (хоча зустрілася і дипломна робота) – але всі були написані на Java, Python або JavaScript, що, на жаль, робило код практично непортируемым і невикористовуваним. Але враховуючи велику популярність QR кодів і низьку технічну складність ідеї, було вирішено написати з нуля на Go — крос-трм, читабельному та швидкому мовою. Звичайно під крос-платформенностью передбачають можливість зібрати бінарний код під Windows, Mac і Linux, але в моєму випадку тут була важлива ще й збірка під веб (gopherjs) і під мобільні системи (iOS/Android). Go дає все це з коробки з мінімальними витратами.

 

Я розглядав також альтернативні варіанти візуальних кодів – такі як HCCB або JAB Code, але для них довелося б писати OpenCV-сканер, імплементувати з нуля кодер/декодер і це було занадто для проекту на одні вихідні. Кругові QR коди (shotcodes), та їх аналоги, що використовуються в Facebook, Kik і Snapchat дозволяють закодувати набагато менше інформації, а неймовірно крутий патентований підхід Apple для спарювання Apple Watch і iPhone — анімоване хмара різнобарвних частинок — також оптимізовано під wow-ефект, а не під максимальну пропускну здатність. QR коди ж інтегровані в нативні SDK камер мобільних OS, що сильно полегшує роботу з ними.

TXQR

Так народився проект txqr (від Tx — transfer, і QR), що реалізує бібліотеку для кодування/декодування QR на чистому Go і протокол для передачі даних.

 

Основна ідея в наступному – один клієнт вибирає файл або дані для відправки, програма на пристрої розбиває файл на шматки, кодує кожен з них QR фрейми і показує їх у нескінченному циклі з заданою частотою кадрів доки одержувач не отримає всі дані. Протокол зроблений таким чином, що одержувач може почати з будь-якого кадру, отримувати QR кадри в будь-якому порядку – таким чином обходиться проблема необхідності синхронізації частоти анімації і частоти сканування. Одержувач може бути старим пристроєм, потужність якого дозволяє декодувати 2 кадри в секунду, а відправник – новим смартфоном видає 120Гц анімацію, або навпаки, і це не буде фундаментальною проблемою для протоколу.

 

Це досягається наступним чином – коли файл розбивається на шматки (фрейми далі), до кожного кадру в початок додається префікс з інформацією про зміщення відносно всіх даних і загальна довжина — OFFSET/TOTAL|(де OFFSET і TOTAL — цілочисельні значення зсуву і довжини відповідно). Двійкові дані поки що кодуються в Base64, але це насправді не обов’язково – QR специфікація дозволяє не тільки кодувати дані як бінарні, але і оптимізувати різні частини даних під різні кодування (наприклад, префікс з невеликими змінами можна закодувати як alphanumeric, а інший вміст – як binary), але для простоти Base64 відмінно виконував свою функцію.

Більш того, розмір фреймів і частоту можна навіть змінювати динамічно, підлаштовуючись під можливості одержувача.

 

 

Сам протокол дуже простий, і головний його мінус в тому, що для великих файлів (хоча це й виходить за рамки завдання, але все ж), один пропущений при скануванні кадр збільшить час сканування вдвічі – одержувачу доведеться чекати повного циклу знову. В теорії кодування є рішення для таких випадків – фонтанні коди, але це я залишу для яких-небудь наступних вільних вихідних.

Читайте також  Як зробити пошук користувачів з GitHub використовуючи React + RxJS 6 + Recompose

 

Найцікавішим моментом було написати мобільний додаток, яке може використовувати цей протокол.

 

Gomobile

 

Якщо ви не чули про gomobile, то це проект, який дозволяє використовувати Go бібліотеки в iOS і Android проектах і робить це до не пристойності простою процедурою.

 

Стандартний процес такий:

  • ви пишете звичайний Go код
  • запускаєте gomobile bind ...
  • копіюєте отримані артифакт(и) (yourpackage.framework. або yourpackage.aar) у ваш мобільний проект
  • імпортуєте yourpackage і працюєте з ним, як зі звичайною бібліотекою

 

Можете самі спробувати наскільки це просто.

Тому я досить швидко написав на додаток Swift, яке сканує QR коди (завдяки ось цій чудовій статті) і декодує їх, склеює і, коли весь файл отримано, показує у вікні попереднього перегляду.

 

Будучи новачком в Swift (хоч я і прочитав книгу по Swift 4), було чимало моментів, коли я застрявав на чомусь простому, намагаючись зрозуміти, як це правильно робити і, в підсумку, найкращим рішенням було реалізувати цей функціонал на Go і використовувати через Gomobile. Не зрозумійте мене не правильно, Swift багато в чому чудовий мову, але, як і більшість інших мов програмування, він дає занадто багато способів зробити одне і те ж, і вже має пристойну історію назад-несумісних змін. Наприклад, мені потрібно було робити просту річ – заміряти тривалість події з мілісекундної точністю. Пошук в Google і StackOverflow приводив до масі різних, суперечливих і, найчастіше, застарілих рішень, жодна з яких, в результаті не виглядало ні гарним для мене, ні коректним для компілятора. Після 40 хвилин витраченого часу, я просто зробив ще один метод Go пакеті, який викликав time.Since(start) / time.Millisecond і використовував його результат з Swift безпосередньо.

Я також написав консольну утиліту txqr-ascii для швидкого тестування програми. Вона кодує файл і анімує QR коди в терміналі. Все разом це працювало на диво добре – я міг відправити невелику картинку за кілька секунд, але, як тільки я почав тестувати різні значення частоти кадрів, кількості байт у кожному QR кадрі і рівень корекції помилок в QR кодировщике, стало зрозуміло, що термінальне рішення не сильно справляється з високою частотою (більше 10) анімації, і що тестувати і заміряти результати вручну це пропаща справа.

TXQR Tester

Щоб знайти оптимальну комбінацію частоти кадрів, розміру даних QR кадрі і рівня корекції помилок серед розумних меж цих значень, мені необхідно було подолати понад 1000 тестів, вручну змінюючи параметри, очікуючи повного циклу з телефоном в руці і записуючи результати в таблицю. Звичайно, це повинно бути автоматизовано!

Тут і з’явилася ідея наступного додатка — txqr-tester. Спочатку я планував використовувати x/exp/shiny — експериментальний UI фреймворк для нативних desktop-додатків на Go, але, схоже, він покинутий. Близько року тому я його пробував, і враження було непогане – для низькорівневих речей він підходив ідеально. Але сьогодні master-гілка навіть не скомпилировалась. Схоже, стимулів вкладати у розвиток desktop-фреймворків – складною і громіздкою завдання, з майже нульовим нині попитом – вже немає, все UI рішення давно перейшли в веб.

Веб-програмування, як відомо, мови програмування тільки-тільки почали заходити, завдяки WebAssembly, але це зовсім поки перші дитячі кроки. Звичайно, є ще JavaScript і надбудови, але друзі не дозволяють друзям писати програми на JavaScript, тому я вирішив використати своє недавнє відкриття – фреймворк Vecty, який дозволяє писати фронтенды на чистому Go, які автомагіческі конвертуються в JavaScript за допомогою дуже дорослого і дивно добре працюючого проекту GopherJS.

Vecty і GopherJS

 

Я в житті такого задоволення не отримував від розробки фронтенд інтерфейсів.

 

Трохи пізніше я планую написати ще пару статей про свій досвід розробки фронтендов на Vecty, в тому числі і WebGL додатків, але суть в тому, що після кількох проектів на React, Ангулярах і Ember, писати фронтенд на продуманому і простою мовою програмування це ковток свіжого повітря! Я можу писати досить симпатичні фронтенды за короткий час і при цьому не писати жодного рядка на JavaScript!

 

Для запалу, ось як ви починаєте новий проект на Vecty (ніяких кодогенераторов “початкового проекту”, створюють тонни файлів і папок) — просто main.go:

 

ackage main

import (
"github.com/gopherjs/vecty"
)

func main() {
 app := NewApp()

 vecty.SetTitle("My App")
 vecty.AddStylesheet(/* ... add your css... */)
vecty.RenderBody(app)
}

 

Додаток, як і будь-UI компонент — це всього лише тип: структура, яка включає тип vecty.Core і повинна реалізувати інтерфейс vecty.Component (що складається з одного методу Render()). І це все! Далі ви оперуєте з типами, методами, фунциями, бібліотеками для роботи DOM і так далі – ніякої прихованої магії і нових термінів та концепцій. Ось спрощений код головної сторінки:

Читайте також  Інформаційна безпека банківських безготівкових платежів. Частина 1 — Економічні основи

 

/ App is a top-level app component.
type App struct {
vecty.Core

 session *Session
 settings *Settings
 // any other stuff you need,
 // it's just a struct
}

// Render implements the vecty.Component interface.
func (a *App) Render() vecty.ComponentOrHTML {
 return elem.Body(
a.header(),
elem.Div(
vecty.Markup(
vecty.Class("columns"),
),
 // Left half
elem.Div(
vecty.Markup(
 vecty.Class("column", "is-half"),
),
 elem.Div(a.QR()), // QR display zone
),
 // Right half
elem.Div(
vecty.Markup(
 vecty.Class("column", "is-half"),
),
 vecty.If(!a.session.Started(), elem.Div(
a.settings,
)),
 vecty.If(a.session.Started(), elem.Div(
a.resultsTable,
)),
),
),
vecty.Markup(
event.Натискання(a.KeyListener),
),
)
}

 

Ви, напевно, зараз дивіться на код і думаєте – наскільки ж це голослівна робота з DOM! Я теж так спочатку подумав, але, як тільки почав працювати, усвідомив, наскільки це зручно:

 

  1. Немає магії – кожен блок (Markup або HTML) це лише мінлива потрібного типу, з чіткими лімітами куди можна поставити, завдяки статичної типізації.
  2. Немає відкривають/закривають тегів, які потрібно або не забувати міняти при рефакторинге, або використовувати IDE, яка робить це за вас.
  3. Структура раптом стає зрозумілою – я ніколи, наприклад, не розумів, чому в React до 16-ї версії не можна було повернути кілька тегів з компонента – це ж “просто рядок”. Побачивши, як це робиться в Vecty, раптом стало зрозуміло, звідки коріння зростали в того обмеження в React. Все одно не зрозуміло, правда, чому після React 16 стало можна, але і не потрібно.

 

Загалом, як тільки ви спробуєте такий підхід роботи з DOM, то його плюси стануть сильно очевидні. Мінуси теж є, безумовно, але після мінусів звичних методів, вони непомітні.

 

Vecty називають React-подібним фреймворком, але це не зовсім так. Для React є нативна GopherJS бібліотека – myitcv.io/react, але я не думаю, що це хороша ідея повторювати архітектурні рішення React для Go. Коли ви пишете фронтенд на Vecty, раптом стає зрозуміло, наскільки все насправді простіше. Раптом стає зайвою вся ця прихована магія і нові терміни і концепції, які кожен JavaScript фреймворк винаходить – вони просто додаткова складність, нічого більше. Все що потрібно – це ясно і чітко описувати компоненти, їх поведінку, і пов’язувати їх між собою — типи, методи і функції, от і все.

 

Для CSS я використовував на подив гідний фреймворк Bulma – у нього дуже зрозуміле іменування класів і хороша структура, і декларативний UI код з його допомогою дуже читабельний.

 

Справжня магія, втім, починається, коли компилируешь Go код JavaScript. Це дуже страшно звучить, але, насправді, ви просто викликаєте gopherjs build і менш ніж за секунду, у вас готовий автосгенерированный JavaScript файл, готовий щоб включати в вашу базову HTML сторінку (звичайна програма складається тільки з пустого body-тега і включення цього JS-скрипта). Коли я вперше запускав цю команду, то очікував бачити масу повідомлень, попереджень і помилок, але ні – вона відпрацьовує фантастично швидко і мовчки, в консоль виводить тільки однострочники у разі помилок компіляції, які згенеровані Go компілятором, тому дуже зрозумілі. Але ще крутіше було бачити помилки в консолі браузера, зі стектрейсами, що вказують на .go файли і правильну рядок! Це дуже круто.

 

Тестування параметрів QR анімації

 

За кілька годин у мене було готове веб-додаток, яке дозволяло мені швидко змінювати параметри для тестування:

  • FPS — частоту кадрів
  • QR Frame Size — скільки байт має бути у кожному кадрі
  • QR Recovery Level — рівень коригування помилок QR

 

і запускати тест автоматично.

 

 

Мобільний додаток, звичайно, теж повинно бути автоматизовано – воно повинно було розуміти, коли починається наступний раунд з новими параметрами, розуміти, коли прийом займає надто багато часу і обривати раунд, відправляти результати додатком і так далі.

 

Заковика була в тому, що веб-додаток, будучи запущеним в пісочниці браузера, не може створювати нові сполуки, і, якщо я не помиляюся, єдина можливість справжнього peer-to-peer з’єднання з браузером є тільки через WebRTC (NAT мені пробивати не треба), але це було занадто громіздко. Веб-додаток могло бути тільки клієнтом.

 

Рішення було просте – веб-сервіс на Go, який віддавав веб-додаток (і запускав браузер на потрібний URL), так само запускав WebSocket-проксі для двох клієнтів. Як тільки до нього приєднуються два клієнта – він прозоро відправляє повідомлення з одного з’єднання до іншого, дозволяючи клієнтам (веб-додатком і мобільному клієнту спілкуватися безпосередньо. Вони повинні бути для цього, в одній WIFI-мережі, звичайно ж.

 

Залишалася проблема того, як сказати мобільного пристрою, куди, власне, підключатися, і вона була вирішена з допомогою… QR коду!

 

Процес тестування виглядає так:

 

  • мобільний додаток шукає QR-код зі стартовим маркером і посиланням на WebSocket-проксі
  • як тільки маркер лічений, додаток підключається до даного WebSocket-проксі
  • веб-додаток (будучи вже підключеним до проксі) розуміє, що мобільний додаток готове і показує QR код з маркером “готовий до наступного раунду?”
  • мобільний додаток розпізнає сигнал, обнуляє декодер, і відправляє через WebSocket повідомлення “угу”.
  • веб-додаток, отримавши підтвердження, генерує нову QR анімацію і крутить її, поки не отримає результати або таймаут.
  • результати складаються в табличку поруч, яку можна відразу завантажити у вигляді файлу CSV
Читайте також  Модель розробки на прикладі Stack-based CPU

 

 

У підсумку, все що мені залишалося – просто поставити телефон на штатив, запустити додаток і далі дві програми самі робили всю брудну роботу, ввічливо спілкуючись через QR-коди і WebSocket 🙂

 

 

В кінці я скачував CSV файл з результатами, заганяв його в RStudio і в Plotly Online Chart Maker і аналізував результати.

Результати

Повний цикл тестування займає близько 4 годин (на жаль, найважча частина процесу — генерація анімованого GIF зображення з QR фреймами, повинна була працювати в браузері, і, оскільки, результуючий код все таки в JS, то використовується тільки один процесор), протягом яких, потрібно було стежити, щоб раптово не згас екран або будь-який додаток не закрив вікно з веб-додатком. Тестувалися наступні параметри:

 

  • FPS — від 3 до 12
  • Розмір QR кадру — від 100 до 1000 байт (з кроком в 50)
  • Всі 4 рівня корекції помилок QR (Low, Medium, High, Highest)
  • Розмір передаваного файлу — 13КБ рандомно згенерованих байт

 

Через кілька годин я скачав CSV і став аналізувати результати.

 

Картинка важливіше тисячі слів, але інтерактивні 3D-візуалізації важливіше тисячі картинок. Ось така візуалізація отриманих результатів (кликабельно):

Найкращий отриманий результат був 1.4 секунди, що приблизно дорівнює 9КБ/с! Цей результат був записаний на частотою 11 кадрів в секунду, розмір кадру 850 байт і середній (medium) рівні корекції помилок. У більшості випадків, щоправда, на такій швидкості декодер камери пропускав деякі кадри, і доводилося чекати наступного повтору пропущеного кадру, що сильно негативно позначалося на результатах – замість двох секунд легко могло вийти 15, або таймаут, який був виставлений в 30 секунд.

 

Ось графіки залежності результатів від застосовуваних змінних:

 

Час / розмір фрейму

 

 

Як видно, при низьких значеннях кількості байт у кожному кадрі, надлишок кодування занадто великий і загальний час зчитування, відповідно, теж. Якийсь локальний мінімум є в 500-600 байт на кадр, але значення поруч все одно призводять до втрачених кадрів. Найкращий результат спостерігався на 900 байт, але 1000 і вище це майже гарантована втрата кадрів.

 

Час / FPS

 

 

Значення кількості кадрів в секунду, на мій подив, сильно великого ефекту не мало – маленькі значення занадто збільшували час спільної передачі, а великі підвищували ймовірність пропущеного кадру. Оптимальне значення, судячи з цих тестів, знаходиться в районі 6-7 кадрів в секунду для тих пристроїв, на яких я тестував.

 

Час / Рівень корекції помилок

 

 

Рівень корекції помилок показав чітку взаємозв’язок між часом передачі файлу і рівнем надмірності, що не дивно. Однозначний переможець тут це низький (L) рівень корекції – чим менше надлишкових даних, тим більш читабельний для сканера QR-код при тому ж розмірі даних. За фактом, для цього експерименту надмірність не потрібна взагалі, але стандарт такого варіанту не пропонує.

 

Звичайно ж, для більш об’єктивних даних цей тест повинен бути запущений сотні й тисячі разів, на різних пристроях та на різних екранах, але для мого експерименту вихідного дня, це був більш ніж достатній результат.

Висновки

Цей кумедний проект довів, що одностороння передача даних через анімовані коди, безумовно, можлива, і для ситуацій, де потрібно передати невеликий об’єм при відсутності будь-яких видів мереж, цілком підходить. Хоча мій максимальний результат був близько 9КБ/с, у більшості випадків реальна швидкість становила 1-2КБ/с.

 

Я також отримав справжнє задоволення, використовуючи Gomobile і GopherJS з Vecty як вже буденного інструменту для вирішення проблем. Це дуже зрілі проекти, з відмінною швидкістю роботи, і, в більшості випадків дають досвід “воно просто працює”.

 

Наостанок, я досі захоплююся, наскільки продуктивним можна бути з Go, коли чітко знаєш, що хочеш реалізувати – екстремально короткий цикл “змінити”-“зібрати”-“перевірити” дозволяє експериментувати багато і часто, простий код і відсутність класової ієрархії в структурі програм дає можливість легко і безболісно їх рефакторіть на ходу, а фантастична кросплатформеність, закладена в мову з самого початку дозволяє написати код один раз і використовувати його на сервері, на веб-клієнті і в нативній мобільному додатку. При цьому, незважаючи на більш, ніж достатню продуктивність з коробки, як і раніше є маса простору для оптимізації і прискорення.

 

Так що якщо ви ніколи не пробували Gomobile або GopherJS – я рекомендую вам спробувати при такій можливості. Це забере годину вашого часу, але, можливо, відкриє вам цілий новий пласт можливостей в веб або мобільного розробці. Сміливо пробуйте!

Посилання

  • https://github.com/divan/txqr
  • https://github.com/divan/txqr/tree/master/cmd/txqr-tester
  • https://github.com/divan/txqr-tester-ios
  • https://github.com/divan/txqr-reader
  • https://github.com/gopherjs/vecty
  • https://github.com/golang/mobile

Степан Лютий

Обожнюю технології в сучасному світі. Хоча частенько і замислююся над тим, як далеко вони нас заведуть. Не те, щоб я прям і знаюся на ядрах, пікселях, коллайдерах і інших парсеках. Просто приходжу в захват від того, що може в творчому пориві вигадати людський розум.

You may also like...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *