golang

Утро без телефона: зачем я поставил рядом с кроватью матричный принтер

  • понедельник, 5 октября 2026 г. в 00:00:06
https://habr.com/ru/articles/1090126/

Всё началось с очень простой проблемы: я хотел перестать брать телефон в руки сразу после пробуждения. Обычно я открывал его с вполне невинной целью — посмотреть данные сна с фитнес‑браслета, а потом как‑то незаметно оказывался в сообщениях, почте и новостях.

Тогда я и подумал: а что, если вообще не открывать приложение, а получать эти данные по факту пробуждения без телефона. Физически. Например, на бумаге? Так начала зарождаться идея поставить рядом с кроватью чековый принтер, который утром сделает короткий отчёт о моём сне.

Первой мыслью был обычный термопринтер. Такие сейчас стоят практически на каждой кассе: компактные, дешёвые, тихие, простые. И именно поэтому довольно скучные.

Пока я перебирал в поиске разные варианты, мне вспомнились старые добрые матричные принтеры с их характерным треском — почти идеальные будильники: они могут напечатать утренние данные и гарантированно сообщат всем вокруг, что утро наступило.

В итоге простая идея «распечатать данные здоровья утром» довольно быстро превратилась в маленькую систему из сервера, старого Mac mini, принтера и нескольких сервисов. И где‑то по дороге всё это стало достаточно странным, чтобы об этом уже хотелось написать.

Сначала — проверить идею

До всей истории с железом мне хотелось понять, смогу ли я вообще получить данные от фитнес‑браслета WHOOP. Первая версия была простой. Один сервис ходил в WHOOP API, забирал данные о сне, восстановлении (Recovery) и нагрузке (Strain) и сохранял их в PostgreSQL. Второй — брал эти данные и собирал текст будущего утреннего отчёта.

Никакого физического принтера тогда ещё не было. Результат просто выводился в консоль. Этого оказалось достаточно, чтобы проверить главную гипотезу: из данных действительно можно собрать короткий отчёт, который мне хотелось бы видеть утром.

Железо: Star SP700 и Mac mini 2010

Я начал искать старые матричные принтеры по барахолкам. Вариантов было достаточно, но почти все выглядели соответственно возрасту: непонятно, сколько аппарат уже проработал, живы ли печатающая головка и механизм протяжки бумаги и не придётся ли после покупки ещё отдельно разбираться с каким‑нибудь древним проприетарным интерфейсом.

В какой‑то момент мне попался профессиональный чековый матричный принтер Star SP700. Продавец утверждал, что он новый и фактически не использовался. На фоне остальных объявлений это выглядело как редкая удача, поэтому я заказал его почти не раздумывая.

Тот самый Star SP700
Тот самый Star SP700

Когда Star приехал, первым делом я решил проверить: может ли macOS вообще что‑нибудь на него вывести. Немного почитав про CUPS (Common UNIX Printing System), я смог напечатать первый тестовый чек через обычный lp.

Оказывается, мои воспоминания о том, насколько шумными бывают матричные принтеры, не вполне соответствовали действительности. После первой же печати стало понятно: с ролью будильника эта вундервафля справится как никто другой. Такой звук утром проигнорировать сложно, даже если очень сильно захотеть.

После удачной печати понадобилась отдельная машина, которая будет постоянно «жить» рядом с принтером и отправлять ему задания. Логичным вариантом выглядел какой‑нибудь Raspberry Pi, но его у меня не было.

Зато был Mac mini 2010 года, который уже давно лежал без дела. На нём стоит macOS High Sierra, а последняя версия Go, которая нормально на ней запускается — 1.20. Для моей задачи этого было более чем достаточно. Но оставалась одна проблема — зависимости.

Что именно потребовало свежий Go?

Дело не в том, что под капотом происходит какой‑то рокет сайнс.

Самому коду возможностей 1.20 хватает с большим запасом.
Но часть привычных мне библиотек уже требует более свежего Go:

  • gin v1.12.0 — Go 1.25

  • pgx/v5 v5.9.2 — Go 1.25+

  • viper v1.21.0 — Go 1.23

Тянуть весь проект назад по версиям ради одного старого Mac мне не хотелось, поэтому код, который работает непосредственно рядом с принтером, я в итоге отделил от основного бэкенда и собирал отдельно под Go 1.20.

С интерфейсом принтера мне при этом повезло. У Star оказался совершенно обычный USB‑B — тот самый квадратный разъём, который много лет использовался у принтеров. Для железки такого возраста всё вполне могло закончиться RS-232, но радость от нормального USB неожиданно разбилась о сам Mac mini. При прямом подключении macOS периодически сообщала, что устройство потребляет слишком много питания, после чего отключала разъём.

На другой машине такого поведения не было. Проблема решилась почти комично: я поставил между Mac и принтером обычный USB‑хаб — и с тех пор всё работает стабильно. Почему именно это помогло, я так до конца и не выяснил.

От будильника к wake_plan

В первой версии время подъёма я задавал через Telegram‑бот. Делать отдельный интерфейс ради одного поля тогда казалось немного избыточным: мне по сути нужно было сообщить системе только одно значение — во сколько завтра вставать.

После команды morningbot создаёт на сервере wake_plan и сохраняет его в PostgreSQL.

wake_plan стал центральной сущностью всей системы. Это план конкретного утра: дата и время, вокруг которых дальше строится вся остальная работа.

После этого уже не особенно важно, откуда этот план появился. Остальные сервисы видят его и начинают делать свою часть работы. Упрощённо утренний flow выглядит примерно так:

В итоге количество сервисов получилось больше, чем я изначально рассчитывал.

Что делает каждый сервис
  • morningbot (VPS) — точка входа в систему. Принимает команды из Telegram и запросы от приложения, создаёт, показывает и отменяет wake_plan.

  • whoopsync (VPS) — общается с WHOOP API, забирает данные о сне, Recovery и Strain и собирает из них единый утренний snapshot.

  • coach (VPS) — получает готовый snapshot, обычным Go‑кодом собирает фактический brief о состоянии на день, после чего локальная LLM превращает его в короткий комментарий для второго чека.

  • receiptworker (VPS) — отвечает за содержимое чеков. Заранее собирает Wake Receipt, а после появления финальных данных WHOOP — Final Report и переводит соответствующий print_job в состояние готовности к печати.

  • printergateway (VPS) — HTTP‑граница между сервером и внешними устройствами. Через него Mac mini получает задания и время следующего будильника, а iPhone читает его текущее состояние.

  • wakeplanner (Mac mini) — узнаёт время ближайшего wake_plan и через pmset программирует пробуждение самого Mac.

  • printeragent (Mac mini) — максимально тупой исполнитель: получает готовый print_job, отправляет его в CUPS и сообщает серверу результат.

  • MorningStation (iPhone) — небольшой интерфейс для установки и отмены будильника.

  • MorningStationWidget (iPhone) — самостоятельно читает состояние активного будильника и показывает его в StandBy, даже если приложение закрыто.

Со временем функционал и интерфейс Telegram‑бота начали казаться слишком утилитарными. До этого я пользовался стандартным будильником iOS: вечером ставил телефон горизонтально на беспроводную зарядку, включался StandBy режим, и ночью сразу было видно время и установленный будильник.

Терять этот сценарий не хотелось. Поэтому у проекта появилось небольшое приложение MorningStation. В нём время подъёма можно выставить привычной крутилкой, проверить соединение с сервером, увидеть активный будильник или отменить его. А для StandBy я сделал отдельный виджет, который показывает время подъёма и сколько до него осталось.

Экран установки будильника и виджет в реальном StandBy
Экран установки будильника и виджет в реальном StandBy

Telegram при этом никуда не исчез. Он остался простым резервным интерфейсом: приложение и бот в итоге делают одно и то же — создают или меняют следующий wake_plan на сервере.

Mac mini не должен знать про PostgreSQL

Первая рабочая версия была максимально прямолинейной: printeragent на Mac mini сам подключался к PostgreSQL на VPS, находил готовые задания на печать, отправлял их в CUPS, а после обновлял статус в базе. Для прототипа этого вполне хватало. Данных мало, всё под моим контролем — зачем придумывать что‑то сложнее. Но чем дольше проект жил, тем сильнее меня раздражало, что старый компьютер рядом с кроватью фактически является клиентом внутренней базы.

На Mac приходилось хранить креды PostgreSQL, серверу — разрешать внешнее подключение, а сам printeragent при этом знал слишком много о внутреннем устройстве бэка: какие есть таблицы, какие статусы бывают у заданий и что именно нужно обновить после успешной печати.

Особенно странно это выглядело с учётом режима работы самого Mac mini. Большую часть времени он спит, потом просыпается, некоторое время восстанавливает сеть и только после этого начинает работать. Давать такой машине прямой доступ к БД ради операции «дай мне следующий чек» оказалось довольно сомнительным решением. В какой‑то момент я сформулировал для себя простое правило:

Mac mini должен быть максимально тупым.

Он не должен знать, где находится PostgreSQL, как устроен wake_plan и какие переходы состояний бывают у print_job. Его задача ограничена тремя действиями: получить готовое задание, распечатать его и сообщить серверу, получилось это сделать или нет. Так между Mac mini и внутренней частью системы появился printergateway — небольшой HTTP‑сервис на VPS.

Теперь printeragent знает только адрес gateway и Bearer‑токен. Через API он получает следующее задание на печать, а после выполнения сообщает серверу об успехе или ошибке. Что именно при этом происходит в PostgreSQL — уже не его забота.

Вся работа с очередью осталась на стороне сервера. printergateway сам выбирает подходящий print_job, меняет его состояние, следит за claim и при необходимости обновляет связанный wake_plan.

Если я позже поменяю схему таблиц или внутреннюю механику очереди, старый Mac из‑за этого переписывать не придётся. Заодно исчезла необходимость выставлять PostgreSQL наружу и хранить его креды на машине рядом с принтером.

Публичный HTTPS я тоже не стал реализовывать внутри Go‑сервиса. printergateway слушает обычный HTTP внутри VPS, а снаружи его закрывает Nginx, который занимается TLS и reverse proxy. В итоге каждый делает свою работу: Go‑сервис отвечает за API и внутреннюю логику, Nginx — за TLS и внешний доступ. Из довольно локального рефакторинга в итоге получился один из основных принципов всей системы:

сервер принимает решения, а железка рядом с принтером только исполняет их.

База, конечно, но до этой базы я в итоге дошёл сам — уже после того, как сначала сделал всё наоборот.

Как разбудить компьютер, который должен разбудить меня

После решения одной задачи следом появляется новая: держать старый Mac mini постоянно включённым ради нескольких минут работы утром не было особого смысла. К тому же он стоит рядом с кроватью, и его бесконечное жужжание ночью довольно быстро стало раздражать. Казалось логичным, чтобы большую часть времени Mac спал, а перед будильником самостоятельно просыпался и печатал чек.

Очевидная проблема заключается в том, что пока компьютер спит, никакой printeragent тоже не работает. Обычный Go‑процесс сам разбудить машину тоже не может. Значит, время следующего пробуждения нужно было заранее записывать в системное расписание macOS. Для этого подошёл штатный pmset. Он умеет запланировать пробуждение компьютера на конкретное время, поэтому оставалось только вовремя сообщать ему, когда именно Mac понадобится утром.

Так появился ещё один небольшой процесс — wakeplanner. Каждую ночь в 03:55 Mac mini просыпается по постоянному расписанию pmset. В 04:00 launchd запускает wakeplanner от root. Тот обращается к printergateway, получает ближайший wake_plan, вычисляет время за двадцать минут до будильника и через pmset записывает ещё одно пробуждение — уже на утро.

После этого wakeplanner снова отправляет Mac спать и завершает работу. Утром уже сама macOS будит компьютер в заранее запрограммированное время. Двадцати минут хватает, чтобы Mac нормально поднялся, восстановил сеть и запустил всё нужное. printeragent снова начинает опрашивать gateway, а когда наступает время печати, забирает готовый print_job и отправляет его в CUPS. Получилась немного забавная цепочка:

компьютер ночью ненадолго просыпается только затем, чтобы узнать, во сколько ему нужно проснуться утром, чтобы разбудить уже меня.

launchd здесь заодно решает ещё одну задачу: мне не нужно вручную запускать процессы после перезагрузки или следить, пережили ли они очередной sleep/wake. При этом root‑права нужны только wakeplanner для управления питанием через pmset. printeragent остаётся обычным долгоживущим процессом без лишних привилегий.

Почему чеков стало два

Изначально я представлял всё проще. По задумке вечером ставится будильник, а утром принтер будит меня и сразу печатает все данные за прошедшую ночь. На практике оказалось, что утром финальные данные появляются не сразу.

В момент пробуждения браслет ещё не всегда считает сон завершённым. Ему требуется некоторое время: я встаю, начинаю двигаться, система понимает, что покою пришел конец, и только после этого появляются финальные данные сна и recovery.

Можно было бы просто открыть приложение и вручную завершить сон. Но это ровно то взаимодействие с телефоном, отказ от которого был отправной точкой для реализации всего этого проекта. Поэтому утреннюю печать пришлось разделить на два этапа.

Первый чек — Wake Receipt — печатается ровно в момент будильника. Он вообще не ждёт WHOOP и не пытается анализировать моё состояние. Его главная задача гораздо важнее — разбудить меня вовремя.

Сейчас на Wake Receipt есть время будильника, ожидаемое время Final Report, небольшой ASCII‑art и бумажный список дел на утро. Позже туда же добавился Field Note — короткая фраза от локальной LLM. Сам Wake Receipt при этом готовится заранее. Как только вечером появляется wake_plan, receiptworker собирает содержимое и сохраняет готовый print_job с временем, раньше которого печатать нельзя. Поэтому утром Mac mini уже ничего не вычисляет — ему остаётся дождаться нужного момента, забрать готовое задание и отправить его в CUPS.

Второй чек — Final Report — появляется уже тогда, когда сервера WHOOP'а закончили интерпретировать данные. Он содержит финальные данные сна, Recovery, Strain и короткий комментарий от coach. В итоге каждый чек выполняет свою роль:

Wake Receipt — это сам будильник. Он должен появиться точно в нужный момент и своей печатью разбудить меня.

Final Report — отчёт о том, как прошла ночь. Он может появиться позже, зато уже содержит финальные данные WHOOP.

И главное — первый не зависит от второго. Даже если WHOOP утром тормозит, внешний API недоступен или локальная нейронка лежит, Star всё равно должен вовремя загрохотать рядом с кроватью.

Wake Receipt и Final Report
Wake Receipt и Final Report

Очередь печати: claim, lease и не совсем exactly‑once

С самой очередью сначала тоже очень хотелось поступить максимально просто. printeragent раз в несколько секунд спрашивает сервер, есть ли готовый print_job, забирает первый найденный, печатает и после этого помечает его как printed.

В целом схема рабочая, однако между получением задания и подтверждением результата что-то может пойти не по плану. Например, агент может получить job и перезапуститься до подтверждения результата — тогда запрос повторится.

Если просто читать первое подходящее задание из базы, два процесса могут одновременно решить, что печатать нужно именно его. Для обычной фоновой задачи это могло бы закончиться просто повторной обработкой. Здесь результат гораздо нагляднее — принтер выдаст два одинаковых чека.

Поэтому получение задания я реализовал через claim. Когда агент просит следующий чек, сервер не просто читает строку из PostgreSQL. Он выбирает доступный print_job, переводит его в состояние processing и на некоторое время закрепляет за конкретным worker_id. Пока этот claim действует, другой агент то же самое задание уже не получит. Если печать прошла успешно, printeragent сообщает об этом серверу, и job переходит в printed.

Закреплять задание за одним агентом навсегда тоже нельзя. Mac mini может получить job, потерять сеть, зависнуть или выключиться до того, как отправит подтверждение. Поэтому у claim есть срок жизни — lease. Если агент так и не сообщил результат, lease истекает, и задание снова становится доступным для обработки.

В итоге система даёт простую гарантию: активный claim не позволяет одновременно выдать один print_job нескольким агентам, а сбой одного из них не оставляет задание навсегда зависшим в processing.

Как устроен claim

В printeragent основная часть claim выглядит так:

WITH candidates AS (
    SELECT id
    FROM print_jobs
    WHERE user_id = $1
      AND (
          (status = 'ready' AND not_before <= $2)
          OR (
              status = 'processing'
              AND processing_until IS NOT NULL
              AND processing_until <= $2
          )
      )
    ORDER BY not_before ASC, id ASC
    LIMIT $3
    FOR UPDATE SKIP LOCKED
)
UPDATE print_jobs pj
SET status = 'processing',
    claimed_by = $4,
    processing_until = $5
WHERE pj.id IN (
    SELECT id
    FROM candidates
);

На уровне базы это хорошо защищает от параллельной печати одного задания. Но гарантировать настоящий exactly-once всё равно нельзя. Представим ситуацию: printeragent уже отправил данные в CUPS, Star начал печатать, а процесс в этот момент упал и не успел сообщить серверу об успехе. Для принтера операция уже произошла — чек физически существует. Для PostgreSQL тот же print_job всё ещё выглядит незавершённым.

Через некоторое время lease истечёт, сервер снова разрешит забрать это задание, и тот же чек теоретически может напечататься ещё раз. Обычной транзакцией этот промежуток не закрыть. Изменение в базе можно откатить, а бумагу, которая уже вышла из принтера, обратно не затолкать. Поэтому здесь меня устраивает более практичная гарантия: один print_job не должен одновременно печататься несколькими агентами, зависшее задание должно возвращаться в очередь, а сами задания не должны теряться. Для будильника редкий дубликат после аварийного сбоя для меня намного менее неприятен, чем задание, которое система потеряла и больше никогда не попробует напечатать.

Немного локальной нейронки

Когда система стала уже более‑менее стабильно будить меня в заданное время, захотелось добавить в неё жизни и, так скажем, черт характера. На том же VPS у меня от другого проекта уже работала Ollama с локальной моделью. Сразу доверять ей что‑то важное не хотелось, поэтому первое применение получилось максимально безобидным: пусть иногда пишет короткую фразу на утро.

Так в Wake Receipt появился Field Note — одна небольшая реплика под списком дел. Практической пользы в ней почти никакой, но чек перестал быть простым набором служебных строк. Иногда модель пишет что‑то действительно забавное, иногда несёт ерунду, а во время экспериментов с промптом периодически начинала довольно жёстко меня буллить. Позже для той же модели нашлась уже более полезная задача. Когда WHOOP заканчивает обработку ночи, сервис coach получает готовый snapshot с данными сна, Recovery и Strain.

При этом мне не хотелось просто отдавать модели пачку чисел и просить её самой решать, что они означают. Поэтому основная интерпретация сначала происходит обычным Go‑кодом. Из метрик собирается структурированная сводка: уровень Recovery, количество сна, Strain и факты, которые вообще стоит упомянуть в отчёте. И только после этого LLM получает уже подготовленную фактическую основу и превращает её в несколько нормальных человеческих строк для Final Report.

То есть модель здесь скорее редактор, нежели источник истины. При этом я специально оставил её вне критического пути. Если утром Ollama не отвечает, это никак не должно влиять на работу будильника. Для Field Note есть несколько ступеней деградации. Сначала система пытается получить фразу от Ollama — если модель недоступна или результат не проходит проверки, используется небольшой локальный генератор на основе заранее заданной грамматики. Если не сработает и он — остаётся фиксированная запасная фраза.

Новая модель не укладывалась в таймаут

Изначально на VPS у меня работала qwen2.5:7b. В какой‑то момент захотелось попробовать модель побольше — qwen3.5:9b. Сервер при этом CPU‑only, поэтому чудес производительности я не ожидал. Но после перехода coach почему‑то перестал получать готовый текст. В остальном всё выглядело нормально: контейнеры были живы, healthcheck'и проходили, Ollama отвечала, модель загружалась. А в логах coach раз за разом появлялось:

context deadline exceeded

Сначала я подозревал всё подряд: сеть между контейнерами, Ollama, неправильный запрос, саму новую модель. Корень проблемы заметил не сразу. К тому моменту Ollama уже почти сутки держала около 800% CPU — фактически занимала все восемь ядер сервера.

 Ollama под полной нагрузкой
Ollama под полной нагрузкой

Сначала это выглядело как зависшая генерация. Но график говорил об обратном: модель всё это время продолжала считать. Тогда я полез смотреть таймауты. Для запроса из coach был выставлен лимит в две минуты. qwen2.5:7b в него укладывалась, поэтому я давно перестал обращать внимание на это значение. А qwen3.5:9b на CPU за две минуты закончить генерацию просто не успевала.

В результате получался довольно бессмысленный цикл: модель две минуты занимала почти все ядра, coach отменял запрос по таймауту, затем срабатывал retry — и всё начиналось заново. Иначе говоря, система не падала, вполне исправно тратила CPU на работу, результат которой я каждый раз сам же выбрасывал, не дождавшись окончания. Я увеличил таймаут до пятнадцати минут и запустил всё ещё раз. Первая полноценная генерация заняла чуть больше семи минут и успешно завершилась.

На графике это было чётко видно: Ollama несколько минут держит все восемь ядер, затем появляется ответ — и нагрузка резко падает. Позже выяснилось, что похожая история происходила и в другом моём сервисе, который использует ту же модель для классификации новостей. Там старого таймаута тоже уже не хватало.

Для меня это оказался хороший пример того, зачем вообще собирать метрики даже в домашнем pet-проекте. До этого Grafana была скорее способом иногда открыть дашборд и посмотреть, как живут контейнеры. А здесь одного графика хватило, чтобы понять, что искать проблему нужно совсем не там, откуда я начал.

Архитектура в сборе

Если посмотреть на проект целиком, он уже довольно далеко ушёл от первоначальной идеи «забрать данные WHOOP и утром распечатать их на чеке». При этом почти каждая новая часть появилась не потому, что мне хотелось построить побольше сервисов, а как решение конкретной задачи. Вся основная логика в итоге базируется на VPS — там хранится состояние утра, синхронизируются данные WHOOP, готовятся оба чека, работает очередь печати и локальная модель.

Mac mini находится на другом конце системы — рядом с кроватью и Star. После всех переделок он знает «базовый минимум» серверной части и потому способен совершить несколько важных действий: проснуться в нужный момент, получить через printergateway готовое задание, отправить его в CUPS и сообщить результат.

Точкой входа перед сном может быть Telegram или приложение на iPhone. Для остальной системы разницы почти нет: в обоих случаях на сервере появляется wake_plan, после чего всё остальное происходит уже без моего участия.

Финальная архитектура
Финальная архитектура

Наверное, больше всего мне в этом проекте нравится именно последняя часть цепочки. В обычном бэкенде результат работы кода чаще всего остаётся где‑то в HTTP‑ответе, базе или очереди сообщений. Здесь же цепочка из Go, PostgreSQL, HTTP, старой macOS и CUPS в какой‑то момент заканчивается конкретным физическим действием: принтер протягивает бумагу, печатающая головка начинает стучать по ленте, и рядом с кроватью появляется чек. Иначе говоря, я могу по‑настоящему «пощупать» результат своей работы.

Ради этого, на мой взгляд, стоило возиться с проектом гораздо дольше, чем изначально собирался.

Несколько месяцев спустя

Эта штука будит меня уже несколько месяцев, и главное — первоначальная идея действительно сработала. Утром мне больше не нужно брать телефон только ради того, чтобы посмотреть, как прошла ночь. Сначала рядом с кроватью с характерным треском появляется Wake Receipt, а чуть позже — Final Report с данными WHOOP.

Причём с основной своей обязанностью Star пока справляется безотказно. Проспать работающий матричный принтер действительно сложно. За всё это время Final Report один раз до меня не добрался. Первый чек, который непосредственно отвечает за будильник, печатался всегда. Для системы, собранной из старого Mac mini, матричного принтера и нескольких моих сервисов, такой результат меня вполне устраивает.

 Мой любимый интерфейс всей системы
Мой любимый интерфейс всей системы

Никаких уведомлений, progress bar и отдельного приложения, которое нужно не забыть открыть. Просто кусок бумаги, который лежит перед глазами ровно столько, сколько мне нужен. При этом законченной систему я пока не считаю. Самым старым и потенциально хрупким звеном остаётся Mac mini. Когда‑нибудь я всё‑таки хочу заменить его чем‑нибудь вроде Raspberry Pi.

Есть ещё несколько вещей, которые хочется добавить. Например, чтобы вместе с будильником утром автоматически открывались шторы через Zigbee. По‑настоящему крутым этот проект для меня делает тот факт, что он в итоге вошёл в мою привычную жизнь. Это уже не сервис, который я запускаю ради эксперимента и потом забываю, а штука, которая каждое утро стоит рядом с кроватью и делает ровно то, ради чего я её собирал.

Ну и один вопрос у меня так и остался. Если кто‑нибудь знает, почему Star при прямом подключении к старому Mac mini периодически определялся как слишком прожорливое USB‑устройство, а обычный USB‑хаб полностью решил проблему, — я всё ещё буду рад наконец узнать ответ.

Весь код проекта лежит на GitHub — если захочется заглянуть внутрь.