Собственный продукт • Embedded • B2B

🔫 Phygital Лазертаг

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

В разработке · собственный продукт
82 комплекта
парк, на котором симулятор гоняет нагрузочный прогон: бой, хаос связи, массовое возвращение в сеть
Бит в бит
Python и C++ сверяются одними golden-векторами, разъехаться незаметно нельзя
До 128
игроков в матче и до 4 команд — потолок разрядности протокола MilesTag II

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

Задача

Лазертаг-парку, который сдаёт оборудование в аренду корпоративным заказчикам, нужна своя система: тагеры, повязки, хаб и панель инструктора. Игра не должна останавливаться, когда за бетонными стенами пропадает связь. Урон и хитмаркер не должны зависеть от связи с хабом.

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

Решение

Комплект программ и прошивок для ESP32 и Raspberry Pi 5. Что реализовано и подтверждено кодом и тестами репозитория:

  • Тагер на ESP32-S3 и повязка на ESP32-C3; ИК-протокол MilesTag II с кодеком на Python и C++.
  • Урон считается на краю сети: повязка сама вычитает HP и сама шлёт стрелку хитмаркер по ESP-NOW, а хаб только собирает статистику.
  • Хаб на Raspberry Pi 5: приём событий по MQTT, PostgreSQL, Redis, WebSocket, статистика.
  • Панель инструктора и экран для зала на React и TypeScript, вход по логину и паролю, две роли.
  • Один файл protocol.yaml как единственный источник правды и кодогенерация констант для Python и C++.
  • Golden-векторы: одни и те же контрольные примеры проверяют Python и C++, проверка идёт в CI на каждый коммит.
  • Брокер Mosquitto без анонимных подключений, права на топики генерируются из контракта.
  • Прошивка на весь парк одна: номер комплекта привязывается в панели по MAC-адресу и уезжает в NVS.
  • Обновление прошивок по воздуху с раздачей образов хабом.
  • Симулятор комплектов, нагрузочный прогон и демонстрационные сценарии.
  • Сторожевой таймер на контроллерах, кириллица на экране тагера, схема соединений и смета закупки.

Как это устроено

Урон на краю сети

«Край сети» — само устройство, а не сервер. Тагер шлёт ИК-пакет, повязка принимает его, вычитает HP и отвечает тагеру хитмаркером по ESP-NOW (прямой радиоканал между ESP32 без роутера). Хаб в боевой цикл не входит, он агрегирует события и считает статистику. Поэтому игра не ломается, когда связь с хабом пропала: события копятся в журнале устройства и досылаются позже.

protocol.yaml: один источник правды и кодогенерация

Весь протокол описан в одном файле protocol/protocol.yaml, вся распиновка — в hardware/pinout.yaml. Оттуда генерируются константы для Python и заголовок для C++, а также схема соединений и чертежи узлов. Ни симулятор, ни бэкенд, ни прошивка не хранят протокольных констант у себя. Генератор распиновки заодно отказывается собирать заведомо нерабочую карту: например, аналоговый вход на ADC2, который не работает при включённом Wi-Fi.

Golden-векторы: Python и C++ бит в бит

Golden-вектор — контрольный пример «входные данные и ожидаемый результат», записанный один раз. Логика упаковки битов написана руками, но Python-код и C++ проверяются одними и теми же векторами. Python — эталон, C++ обязан совпадать с ним бит в бит и микросекунда в микросекунду. Первый из четырёх прогонов CI следит, чтобы сгенерированное не разъехалось с protocol.yaml и pinout.yaml.

ИК MilesTag II на несущей 56 кГц

MilesTag II — ИК-протокол лазертага: 14 бит на посылку, несущая 56 кГц. Посылка длится от 19,8 до 28,2 мс, отсюда физический потолок скорострельности около 35 выстрелов в секунду; он зафиксирован тестом. В матче до 128 игроков и до 4 команд. CHANGELOG объясняет выбор так: версия v1 (MilesTag II) активна ради совместимости с оборудованием Laserwar и Forpost, а собственная 32-битная версия v2 спроектирована, но выключена. На настоящем оборудовании совместимость ещё не проверялась: конвенция бита чётности ждёт проверки на повязке.

MQTT-брокер: права на топики из контракта

Mosquitto поднимается с паролями и правами, анонимных подключений нет. Учётные записи две: hub пишет команды, device только отчитывается о себе. Права на топики генерируются из контракта, руками их не правят. Отдельно у брокера выключен алгоритм Нейгла: с ним два ИК-луча с разницей в 50 мс доезжали до повязки одним куском.

Одна прошивка на парк и номер комплекта по MAC

Номер комплекта в прошивку не зашит. Чистая плата приходит к хабу со своим MAC-адресом, инструктор в панели привязывает адрес к слоту, номер уезжает в NVS (энергонезависимая память контроллера). Сгоревшую плату меняют без пересборки. Плата без номера не стреляет и не считает попадания, а сменить номер можно только в первые 10 минут после включения, чтобы поддельный ответ не увёл работающие комплекты в перезагрузку посреди матча.

Обновление прошивок по воздуху

Образ заливается на хаб и объявляется парку отдельным действием: положить прошивку можно заранее, а раздать — когда в зале никого. Комплект берёт образ, только если совпала роль, версия отличается от своей, не идёт матч и заряда больше 40 процентов. Хеш сверяется до переключения загрузочного раздела, поэтому оборванная закачка оставляет исправную старую прошивку.

LoRa 433 МГц для аварийных команд

Аварийный канал предусмотрен в архитектуре: LoRa на частоте 433 МГц, только команды, не более 10 мВт, в обход Wi-Fi. Модули для него пока не закуплены, а подпись команд и защита от повтора не начаты.

Симулятор парка и нагрузочный прогон

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

Отчёты для инструктора и для клиента

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

Результат

  • Вся игровая логика, протокол и бэкенд отлажены на симуляторе до сборки физических прототипов.
  • Полный прогон тестов на поднятом стенде проходил пять раз подряд: 284–285 тестов по записи журнала от 22 сентября 2026 года.
  • Регрессия «склеенные лучи» найдена и закрыта: в 40 повторах сценария после правок потерь ноль.
  • Нагрузочный прогон на 82 комплекта сверяет то, что считают устройства, с тем, что доехало до PostgreSQL.
  • Смета закупки и схема соединений собраны и сверены между собой.

Технологии: что и зачем

  • ESP32-S3 и ESP32-C3, C++17 (PlatformIO) — тагер и повязка; переносимая логика проверяется на хосте, а не отладочной печатью.
  • ESP-NOW — радиоканал повязка — тагер без роутера.
  • MQTT, Mosquitto 2 — связь устройств с хабом.
  • Python, FastAPI, PostgreSQL, Redis — хаб: приём событий, статистика, состояние матча.
  • React, TypeScript, Vite — панель инструктора и экран для зала.
  • LoRa 433 МГц — аварийные команды в обход Wi-Fi.
  • Docker Compose, GitHub Actions — стенд и четыре прогона CI на каждый коммит.

Статус

В разработке, собственный продукт. Этап — виртуальный стенд и симулятор; пакеты хаба и симулятора имеют версию 0.1.0. Последняя запись журнала разработки — 22 сентября 2026 года. Физических прототипов нет: дальность ИК, допуск приёмника TSOP4856, энергобюджет и поправка часов ждут замеров.

Открытые вопросы по README: аварийный канал LoRa не закуплен; TLS на площадке не настроен; нет журнала действий инструктора и резервного копирования истории матчей; повязка не обновляется по воздуху; миграций базы пока нет. Дальше — сборка прототипов и замеры по чек-листу.

Интересно под свой парк?

Расскажите про площадку и оборудование — прикинем, что нужно для запуска.