Архитектура и расчёт изнутри¶
Аэрозвено — несколько небольших сервисов с одним правилом: план строит один сервис, а проверяет другой, независимый. Валидатор не импортирует код планировщика, у него своя геометрия, и он ничего не чинит — только пересчитывает и выносит вердикт.
Сервисы¶
flowchart LR
U[Браузер оператора] -->|HTTPS| IN[Ingress<br>TLS, доступ по паролю]
IN -->|/| FE[Фронтенд<br>nginx: shell + 3 remote]
IN -->|/api, /healthz, /status| GW[Gateway<br>NestJS + Fastify]
GW -->|gRPC| PL[Планировщик<br>Python, OR-Tools]
GW -->|gRPC| VA[Валидатор<br>Python]
GW -->|SQL| PG[(PostgreSQL<br>сценарии, планы,<br>очередь, события)]
FE -.->|сборка интерфейса| S3[(MinIO<br>бандл фронтенда,<br>подложка карт)]
GW -.->|по настройке| OM[Прогноз ветра<br>Open-Meteo]
GW -.->|по настройке| LLM[Языковая модель<br>для ассистента]
PL --- DEM[(Цифровая модель<br>рельефа)]
VA --- DEM
| Сервис | Технологии | Роль |
|---|---|---|
| Фронтенд | React 19, Vite, Module Federation, Ant Design, MapLibre + deck.gl, PMTiles | Оболочка shell и три подключаемых модуля: planner (сценарий, воздушное пространство, план, экспорт, отчёт), validator (отчёт валидатора, Парето), viewer3d (3D-пролёт) |
| Gateway | TypeScript, NestJS 10 на Fastify | Единственная внешняя точка входа: REST /api/*, очередь расчётов, поток прогресса (SSE), идемпотентность, экспорт, чат-ассистент, прогноз ветра |
| Планировщик | Python, Shapely, pyproj, numpy, OR-Tools | Строит план по сценарию; выгружает KML, GeoJSON, QGC .plan, путевые точки |
| Валидатор | Python, собственная геометрия | Независимо пересчитывает план: 39 проверок, метрики, вердикт |
| PostgreSQL | PostGIS 16 | Пользовательские сценарии, готовые планы, очередь заданий, события прогресса, ключи идемпотентности |
| MinIO | S3-совместимое хранилище | Только собранный интерфейс и файлы подложки; бэкенд к нему не обращается |
Внутри кластера gateway ходит в планировщик (порт 5001) и валидатор (порт 5002) по gRPC с сервисным ключом в заголовке. Размер gRPC-сообщения — до 64 МиБ, тела HTTP-запроса — до 25 МБ, ограничение частоты — 100 запросов в минуту с одного адреса.
Контракты между сервисами¶
| Сервис | Вызов | Назначение |
|---|---|---|
PlannerService |
Plan(PlanRequest) → stream PlanEvent |
Расчёт; в потоке — события прогресса и итоговый PlanResult |
PlannerService |
StopPlan |
Остановка расчёта оператором |
PlannerService |
ExportPlan |
Выгрузка KML / GeoJSON / QGC .plan / путевых точек |
ValidatorService |
Validate(ValidateRequest) → ValidateResponse |
Отчёт по паре «сценарий + план» |
Подробно — gRPC и схемы.
Как проходит один расчёт¶
sequenceDiagram
autonumber
actor O as Оператор
participant UI as Интерфейс
participant GW as Gateway
participant DB as PostgreSQL
participant PL as Планировщик
participant VA as Валидатор
O->>UI: «Рассчитать»
UI->>GW: POST /api/plans (сценарий, критерий, лимит, Idempotency-Key)
GW->>GW: JSON-схема сценария
GW->>DB: задание в очередь (queued)
GW-->>UI: 202 planId
UI->>GW: GET /api/plans/{id}/events (SSE)
loop каждые 500 мс
GW->>DB: взять задание (FOR UPDATE SKIP LOCKED)
end
GW->>PL: gRPC Plan (дедлайн = лимит + 30 с)
PL-->>GW: стадии прогресса, затем PlanResult
GW->>DB: план сохранён, задание done
GW-->>UI: completed
UI->>GW: GET /api/plans/{id}
UI->>GW: POST /api/validate {scenario, plan}
GW->>VA: gRPC Validate
VA-->>UI: отчёт: 39 проверок, вердикт
Очередь¶
Очередь — это таблица заданий в PostgreSQL, без отдельного брокера сообщений.
| Параметр | Значение |
|---|---|
| Опрос очереди | каждые 500 мс, захват FOR UPDATE SKIP LOCKED |
| Одновременных расчётов на экземпляр gateway | 2 (PLAN_CONCURRENCY) |
| Длина очереди | до 20 (PLAN_QUEUE_LIMIT); сверх — 429 QUEUE_FULL и Retry-After: 30 |
| Пульс задания | не реже раза в 10 с; задание без пульса дольше 60 с снимается с кодом SOLVER_INTERRUPTED |
| Идемпотентность | повтор POST с тем же Idempotency-Key возвращает тот же ответ (заголовок Idempotency-Replayed: true); ключ хранится 24 ч |
| Статусы | queued → running → completed / failed |
Прогресс¶
Поток GET /api/plans/{id}/events (Server-Sent Events) отдаёт события со стадией, долей готовности,
значением критерия и прошедшим временем; поддерживает Last-Event-ID для переподключения. Если
поток оборвался, интерфейс переходит на опрос GET /api/plans/{id} каждые 500 мс.
| Стадия | Что происходит |
|---|---|
queued |
задание ждёт в очереди |
geometry |
разбор сценария, справочник техники, погода |
coverage |
отбор бортов, высота съёмки, обрезка участков по пространству и зонам |
routing |
назначение галсов бортам и порядок галсов |
solving |
раскладка по бортам |
export |
упаковка в вылеты, перелёты, профиль, метрики |
completed / failed |
итог |
Честно о прогрессе
Сейчас планировщик накапливает события стадий и отправляет их вместе с результатом, поэтому во время долгого расчёта интерфейс видит «Очередь», затем пачку стадий в конце. Индикатор «Идёт М:СС» при этом показывает реальное время.
Конвейер планировщика¶
flowchart TD
A[1. Разбор сценария<br>схема, справочник] --> B[2. Предпроверка GSD<br>есть ли борт, способный дать GSD]
B -->|нет| X[422 GSD_UNREACHABLE]
B --> C[3. Отбор бортов<br>нагрузка, ветер, заряд, температура]
C --> D[4. Высота съёмки<br>из GSD и камеры]
D --> E[5. Обрезка участков<br>разрешённое пространство, зоны]
E --> F[6. Угол галсов<br>ветер, развороты]
F --> G[7. Полосы и галсы<br>шаг с учётом сноса]
G --> H[8. Назначение галсов бортам<br>и порядок обхода]
H --> I[9. Упаковка в вылеты<br>энергия, резерв, оборот]
I --> J[10. Перелёты<br>обход зон, рельеф, профиль]
J --> K[11. Метрики, площадки,<br>разведение бортов]
K --> L{Уточнять угол?}
L -->|да| M[12. Целые планы по<br>вариантам угла, параллельно]
M --> N[Лучший план]
L -->|нет| N
N --> O[13. Проверка фактического GSD]
| № | Стадия | Какие параметры сценария учитываются |
|---|---|---|
| 1 | Разбор сценария | весь документ; ошибки схемы → 400/422 |
| 2 | Предпроверка GSD | tasks[].survey_type, quality.gsd_cm, fleet[].model/payload/home, launch_sites[].elevation_m |
| 3 | Отбор бортов | fleet[].enabled, battery_left_frac, payload; objective.energy_reserve; weather.wind_speed_ms, temperature_c; предел ветра модели |
| 4 | Высота съёмки | quality.gsd_cm (или плотность точек, межгалсовое расстояние), фокусное расстояние и матрица камеры |
| 5 | Обрезка участков | airspace.allowed, airspace.no_fly[] на высоте съёмки, objective.safety_buffer_m, zone_vertical_margin_m |
| 6 | Угол галсов | tasks[].angle_deg (если задан), ветер, objective.turn_mode |
| 7 | Полосы и галсы | перекрытия overlap_forward_frac, overlap_side_frac, снос ветром, зоны и граница пространства |
| 8 | Назначение и порядок | objective.criterion, lambda_frac, площадки бортов |
| 9 | Упаковка в вылеты | запас хода модели, energy_reserve, температура, battery_left_frac, оборот и запасные АКБ площадок |
| 10 | Перелёты и профиль | зоны, ветер, рельеф, полосы и глиссада (при включённом расчётном профиле) |
| 11 | Метрики и площадки | service_slots и запасные АКБ площадок, разведение бортов (separation в запросе) |
| 12 | Уточнение угла | критерий, lambda_frac, лимит времени |
Детали каждой стадии и все числа — в Правилах и допущениях расчёта.
Параллельный перебор угла галсов¶
Угол галсов сильно влияет на число разворотов, подлёт и нарезку на вылеты. Поэтому после первого полного плана планировщик пересчитывает целые планы для нескольких кандидатов угла каждого участка и оставляет лучший по выбранному критерию. Кандидаты считаются параллельно в пуле процессов; при сбое пула расчёт продолжается последовательно.
| Параметр | Значение |
|---|---|
| Число процессов | PLANNER_ANGLE_WORKERS, по умолчанию min(число ядер, 8); на стенде — 8 |
| Когда включается | режим PLANNER_ANGLE_SEARCH=full, не пересчёт после выбытия борта, есть участки без заданного угла, участков не больше 4 |
Остальные пороги перебора (число кандидатов, минимальный выигрыш, остановка, доля лимита времени) — в разделе Лимит времени расчёта.
Хранение данных¶
| Таблица PostgreSQL | Что хранит |
|---|---|
scenarios |
пользовательские сценарии с версиями и источником (мастер, импорт, демо) |
plans |
готовые планы: критерий, лимит, зерно, статус решателя, документ плана, метрики, отпечаток |
plan_jobs |
очередь расчётов: статус, стадия, прогресс, лучший критерий, ошибка, пульс, запрос на остановку |
plan_events |
события прогресса для потока SSE |
idempotency_keys |
ответы на повторные запросы с тем же ключом (24 ч) |
Демо-сценарии в базе не лежат: gateway читает их из файлов и объединяет со списком пользовательских.
Справочник бортов и нагрузок подключается файлами fleet.yaml и payloads.yaml. Отчёт валидатора
сейчас в базу не сохраняется: интерфейс запрашивает его после каждого расчёта.
Развёртывание¶
Стенд работает в Kubernetes (k3s): по одному экземпляру каждого сервиса, PostgreSQL и MinIO — с
постоянными томами. Образы помечаются 8-символьным хешем коммита. Цифровая модель рельефа
подключается к планировщику и валидатору каталогом (PLANNER_DEM_DIR, VALIDATOR_DEM_DIR).
| Сервис | Ресурсы: запрос → предел |
|---|---|
| Gateway | 0,1 CPU / 256 МиБ → 1 CPU / 1 ГиБ |
| Планировщик | 2 CPU / 1 ГиБ → 16 CPU / 8 ГиБ |
| Валидатор | 0,1 CPU / 256 МиБ → 1 CPU / 2 ГиБ |
Локальный запуск — Эксплуатация.