Files
x0gp/ARCHITECTURE.md
x0gp 978d36c505 feat(server): initial implementation
This commit captures the full server code accumulated across
several development sessions. It includes the following layers,
applied in roughly this order during development:

1. Base infrastructure
   - cmd/poc-server HTTP+WebSocket server, /health, /stats
   - internal/transport wire types (Envelope + all message types)
   - internal/catalog, internal/realtime, internal/stats
   - internal/storage/postgres with migrations 001-004
   - .env, .env.example, docker-compose, Dockerfile, scripts/

2. Swagger documentation
   - go get github.com/swaggo/http-swagger
   - swag init produces docs/docs.go
   - /swagger/index.html UI, /swagger/doc.json spec
   - annotations on health/stats/ws and catalog handlers

3. Races API
   - internal/races/{store,service,keyset,types}.go
   - migration 005_races.sql: finished_races, race_plans, race_queue
   - GET /api/races with keyset pagination, status filter
   - GET /api/races/upcoming
   - POST /api/races/queue/join, GET, DELETE
   - POST/GET /api/races/plans, DELETE /{id}
   - lobby.RaceMeta simplification (no host_id/host_name)

4. Races seeder
   - internal/races/seed with deterministic generator
   - --seed-races / --reset CLI flags in main.go
   - 30 finished + 5 live + 5 plans + 4 queue entries

5. Drivers and clans
   - internal/drivers, internal/clans (CRUD, validation)
   - migration 008_drivers_clans.sql
   - /api/clans and /api/drivers endpoints
   - 3-letter uppercase nickname/tag validation
   - lobby.DriverMeta: Nickname, AvatarURL, ClanID, ClanTag

6. Podium
   - internal/transport.RacePodiumEntry
   - lobby.SetDriverProfile for in-memory metadata sync
   - migration 007_podium.sql (podium JSONB column)
   - seeder populates top-3 per finished race

7. Live races persistence
   - migration 009_live_persistence.sql: live_races,
     live_race_drivers, lobby_drivers
   - internal/races/live_store.go: LiveStore with write-side
     mirror for lobby.Service mutations
   - Service.collectLive and Upcoming read from Postgres
   - main.go RestoreFromDB rehydrates lobby on startup

8. Unify live + finished into one races table
   - migration 010_unify_races.sql: rename finished_races to
     races, expand status CHECK, merge live rows
   - PgStore now hosts both write paths; LiveStore is a
     thin facade implementing lobby.Persistence
   - seeder resetAll drops only finished/cancelled rows and
     race_plans / race_queue / lobby_drivers

Each layer is consistent on its own; cross-layer changes are
visible in the file history. A future refactor may split this
commit into the per-stage boundaries listed above.
2026-06-22 22:01:09 +04:00

11 KiB
Raw Permalink Blame History

x0gp — Architecture Overview

Корень проекта. Содержит общую картину системы, glossary и связи между подсистемами.

Дата фиксации архитектурных решений: 2026-06-18. Дата обновления (Bambu A1, своё шасси, домашний сервер): 2026-06-18. Дата обновления v2 (Orange Pi 5 Pro + Coolify, Compact 1/27): 2026-06-18.


1. Что такое x0gp

x0gp — это платформа для проведения гонок на реальных радиоуправляемых машинках через интернет. Игроки подключаются через Web (позже — мобильное приложение), платят за заезд/турнир и управляют физической машинкой в реальном времени.

Целевой пользователь: гонщик-любитель 16-45 лет, желающий участвовать в реальных гонках без необходимости ехать в хобби-клуб.


2. Целевые метрики

Метрика Цель
Round-trip команды (экран -> колёса) p50 < 100 мс
Round-trip команды p99 < 200 мс
Tick rate гонки 60 Hz
Одновременных активных заездов на Edge-инстанс 50-100
Доступность 99.5% (PoC) -> 99.9% (prod)
Конверсия "регистрация -> первый заезд" > 30%
Стоимость инфраструктуры на 1 заезд/час < 5 руб. (цель)

3. Компоненты системы

3.1. Слой клиента

  • Web client — React, Vite, TypeScript. WebSocket-клиент с client-side prediction. Canvas/WebGL-рендер трассы и машинок.
  • Mobile client (фаза 5) — React Native, общий с Web протокол.
  • Admin panel — React + TanStack Table, управление трассами, турнирами, пользователями.

3.2. Edge-сервер (Go)

  • api-gateway — stateless, REST + WS-терминация, auth, rate-limit.
  • race-engine — stateful, authoritative tick 60 Hz, fan-out.
  • agent-linker — long-lived WS к бортовым агентам в помещении.
  • indexer — пайплайн телеметрии в TimescaleDB.
  • replay-recorder — пишет replay-чанки в S3.

3.3. Локальная инфраструктура в помещении

  • Gateway-агент (мини-ПК) — Go-бинарь, держит связь с Edge-сервером и машинками, выполняет CV-инференс, буферизует команды на случай обрыва WAN.
  • Wi-Fi роутер — выделенный SSID "x0gp-track", WPA3.
  • Overhead-камеры — 1-2 шт, 1080p 60 fps global shutter.
  • IR-магнитные петли — на секторах трассы.

3.4. Борт машинки

  • ESP32-S3 агент — Wi-Fi, WebSocket к gateway, генерация команд ESC/серво.
  • Brushless ESC + мотор — 1/18 scale.
  • Mini digital servo — рулевое.
  • IMU, IR/магнитные сенсоры — телеметрия.

3.5. Хранение

  • PostgreSQL — пользователи, платежи, гонки, турниры.
  • Redis — сессии, pub/sub, лобби, rate-limit.
  • TimescaleDB — телеметрия, replay-индекс.
  • S3-совместимое — replay-чанки (видео, JSON-логи).

3.6. Observability

  • Loki — логи.
  • Prometheus + Grafana — метрики.
  • Tempo / Jaeger — трейсы.
  • Sentry — ошибки на клиенте и сервере.

4. Поток данных (упрощённый)

1. Игрок нажимает газ в браузере
   |
2. Web-client отправляет InputState{seq, tsMs, throttle: 1.0} по WS
   |
3. api-gateway принимает, валидирует, кладёт в Redis Stream "race:{id}:inputs"
   |
4. race-engine на ближайшем тике читает stream, применяет к state
   |
5. race-engine публикует RaceSnapshot в Redis pub/sub
   |
6. race-engine шлёт AgentCommand в agent-linker (WebSocket в помещение)
   |
7. agent-linker шлёт AgentCommand по WS на ESP32-S3 машинки
   |
8. ESP32-S3 выдаёт PWM на ESC и серво
   |
9. ESP32-S3 шлёт AgentTelemetry обратно (10-30 Hz)
   |
10. gateway-агент делает CV-инференс и шлёт CarFrame на сервер
   |
11. race-engine обновляет authoritative state, шлёт следующий Snapshot
   |
12. Web-client получает Snapshot, корректирует prediction, рисует кадр

Типичный round-trip: 50-120 мс (зависит от расстояния до Edge-сервера).


5. Модель доверия

Authoritative server: истиной владеет сервер. Клиент может рендерить раньше (через client-side prediction), но любое решение о кругах, победах, позициях принимается сервером на основе данных от agent-linker (телеметрия ESP32 + CV).

Anti-cheat контур: вход -> sanity check -> race-engine (state) -> agent-linker (CV) -> indexer (telemetry) -> admin (manual review).


6. Структура репозитория

x0gp/
  ARCHITECTURE.md         # этот файл
  README.md               # quickstart, how to run
  server/                 # Go-код (см. server/PLAN.md)
    PLAN.md
    cmd/
    internal/
    proto/
    migrations/
    deploy/
  hardware/               # CAD, спецификации, BOM (см. hardware/PLAN.md)
    PLAN.md
    cad/
    bom/
    photos/
  web/                    # React-клиент (отдельный пакет)
  mobile/                 # React Native (фаза 5)
  ops/                    # общие devops-скрипты, конфиги
  docs/                   # ADR, RFC, диаграммы
    adr/
    diagrams/

7. Glossary

Термин Значение
Race Одна гонка: 2-12 игроков на трассе, общий старт, финиш по N кругов или по времени
Lobby Пре-гонка: игроки подключаются, выбирают машину, ждут старта
Tick 1 шаг симуляции (16.67 мс, 60 Hz)
Snapshot Полное состояние гонки на момент тика (позиции, скорости, круги)
Prediction Локальная симуляция у клиента между Snapshot'ами
Correction Сообщение сервера о расхождении клиентского prediction
Agent Софт на борту машинки (ESP32-S3) или в помещении (мини-ПК)
Edge Сервер в регионе, ближайшем к игрокам и помещению с трассой
Gateway-агент Локальный мини-ПК, "мост" между сервером и машинками в помещении
Track Физическая трасса + логическая (layout_id)
Class Класс машинки (1/18 Formula, 1/20 Mini и т.д.)
Sector Часть круга (1/3, 2/3) — IR-магнитная метка
DNF Did Not Finish — игрок не закончил гонку
DQF Disqualified — дисквалификация за нарушение
Replay Запись гонки для просмотра и разбора

8. Ключевые ADR (Architecture Decision Records)

Подробности — в docs/adr/. Здесь — сводка:

  • ADR-001: WebSocket + protobuf как основной транспорт.
  • ADR-002: Authoritative server + client-side prediction.
  • ADR-003: ESP32-S3 как бортовая MCU, MicroPython в PoC -> ESP-IDF в проде.
  • ADR-004: Гибрид IR-магнитные секторы + overhead-CV для позиционирования.
  • ADR-005: Масштаб машинки 1/18 (длина шасси 18-22 см), ширина полосы 30-40 см.
  • ADR-006: Edge + Gateway-агент: сервер не знает о Wi-Fi трассе, агент буферизует обрыв WAN.
  • ADR-007: Биллинг через ЮKassa (РФ) + Stripe (международный).
  • ADR-008: 3D-принтер Bambu A1 как основной для производства шасси, барьеров, маунтов, кузовов. Альтернатива — Bambu P1S при необходимости enclosure для инженерных пластиков.
  • ADR-009: Шасси машинки полностью своё, спроектированное и напечатанное на 3D-принтере. Не используем покупные донорские деки.
  • ADR-010: Edge-сервер в PoC и MVP работает на домашнем сервере с доступом из интернета через Cloudflare Tunnel (default) или WireGuard. Перенос на облако/VPS — только при появлении коммерческой нагрузки и SLA обязательств.
  • ADR-011: Домашний сервер — Orange Pi 5 Pro (RK3588S, ARM64, 8 cores, 6 TOPS NPU, 2×2.5 GbE) с развёрнутым Coolify (self-hosted PaaS) для деплоя и управления сервисами. NPU Orange Pi используется для CV-инференса (YOLO/ByteTrack через RKNN Toolkit 2). LAN-порт Orange Pi изолирует сеть трассы от домашней сети. Доступ из интернета — Caddy + Let's Encrypt напрямую (статический IP от провайдера).
  • ADR-012: Масштаб машинки — Compact 1/27 как primary класс (длина шасси 13-15 см, wheelbase 100-115 мм, вес 150-220 г). Ширина полосы трассы 20-25 см, рабочая зона трассы 200×130 см (помещается в комнату 446×319 см с запасом). Альтернативный класс — Standard 1/24 (16-18 см) для тех, кому нужна чуть большая стабильность. Цель — минимизация площади трассы и стоимости при сохранении управляемости 3D-печати.

9. Что дальше

После фиксации плана — переход к PoC:

  1. Минимальный набор: 1 машинка, 1 трасса 100x60 см, 1 gateway-агент, 1 сервер.
  2. Цель: end-to-end "команда -> колёса крутятся, телеметрия возвращается".
  3. Срок: 1-2 недели.

Детали — в server/PLAN.md (фаза S0) и hardware/PLAN.md (фаза H0).