Виртуальный полигон для дрона
Визуализация полёта самолёта (drone_flight_demo)
WebGL-приложение показывает движение летательного аппарата в реальном времени:
модель Engee отправляет состояние по UDP, приложение Genie принимает его и отдаёт
браузеру, а страница проигрывает измерения без рывков и без движения назад.
┌──────────────────────────┐ UDP 14 × Float64 ┌──────────────────────────┐
│ drone_flight_demo.engee │ ────────────────────► │ drone_graphics_app.jl │
│ блок «3D сцена» │ 127.0.0.1:5150 │ Genie: /state, /trace │
└──────────────────────────┘ └────────────┬─────────────┘
│ HTTP
▼
┌──────────────────────────┐
│ браузер: public/flight.js│
│ + public/mesh.bin │
└──────────────────────────┘
Состав проекта
| Файл | Назначение |
|---|---|
drone_flight_demo.engee |
Модель Engee с блоком «3D сцена», который отправляет состояние аппарата по UDP. |
drone_graphics_app.jl |
Приложение Genie: UDP-приёмник, HTTP-маршруты /state, /trace, /reset, отдача страницы. |
public/flight.js |
Клиент: буфер измерений, интерполяция, камера, отрисовка WebGL. |
public/flight_orange.js |
Архив: предыдущая версия клиента с оранжевой шахматкой на земле. |
public/mesh.bin |
Бинарный меш аппарата (формат DRN1), его читает flight.js. |
drone_model.obj |
Исходный OBJ-меш (источник для mesh.bin). |
obj2bin.jl |
Упаковщик OBJ → DRN1 (mesh.bin). |
drone_virtual_polygon.ngscript |
Ноутбук проекта: описание, сборка меша, запуск и остановка приложения. |
Как работает клиент public/flight.js
Клиент не интегрирует координаты по своим стеновым часам. Он принимает поток
измерений с модельным временем t, складывает их в буфер и проигрывает буфер по
монотонным часам playT. Благодаря этому уже показанное положение никогда не
переопределяется, и аппарат не может поехать назад.
Приём данных
GET /stateопрашивается каждыеPOLL_MS = 40мс (≈25 Гц), одновременно не
болееMAX_INFLIGHT = 2запросов. Таймаут запроса — 2 с.- Каждый ответ — одна строка CSV из 19 полей (см. «Формат
/state»). - Пакет считается новым по счётчику
rx(поле 14). Повторныйrxотбрасывается. - Если между двумя ответами пропущено больше одного пакета (
rx > prevRx + 1),
клиент запрашиваетGET /trace?since=<prevRx>и добавляет пропущенные
измерения в буфер. Это закрывает паузы HTTP и потери UDP.
Буфер измерений
Каждое измерение хранит: t (модельное время), координаты, углы, линейные и
угловые скорости, rx, время приёма. Вставка отсортированная, поэтому /trace
может дослать пропущенное «в середину».
- Оценка скорости модельного времени: медиана
Δt_модели / Δt_стеныпо
последним 41 пакетам, ограничение 0.2…3.0. - История интервалов между пакетами: последние 12 значений.
- Адаптивная задержка воспроизведения:
delay = 1.6 × p90(интервалов), ограничение 0.12…1.6 с.
При 25 Гц это ≈0.12 с, при 1 Гц и секундных пробелах — до 1.6 с. - Буфер хранит
delay + 8 × интервалов + 2 систории, но не более 800 точек.
Воспроизведение
playT— модельное время того, что сейчас нарисовано; только растёт.playT += dt × rate × corr, гдеrate— оценка скорости модельного времени,
аcorr = clamp(1 + err / 0.6, 0.7, 1.4)удерживаетplayTнаdelayпозади
самых свежих данных.- Положение между двумя измерениями считается кубическим Эрмитом: узлы —
координаты, касательные — скорости из модели. Углы интерполируются по
кратчайшей дуге. - Выбег (
COAST_MAX = 1.0с модельного времени): если впереди данных нет,
аппарат продолжает движение по последней скорости, а не замирает. - Только вперёд: если после выбега очередная точка оказалась позади уже
показанной, из поправки убирается отрицательная продольная составляющая
(поперечная остаётся). Счётчик таких коррекций —backCorrect(в HUD не
выводится);uв HUD — число кадров, когда данных впереди не было. - При перезапуске модели (модельное время ушло назад больше чем на 0.5 с)
буфер и история очищаются,playTпривязывается заново. - Если вкладка была скрыта и
playTотстал сильнее, чем хранится история,
выполняется один прыжок вперёд — без обратного движения.
Управление и HUD
- Кнопка Camera переключает свободную камеру и камеру «погони».
- Мышь: перетаскивание — вращение, колесо — приближение, двойной клик —
сброс камеры. Reset UI перезагружает страницу, Reset UDP вызывает
POST /reset(перезапуск UDP-сокета). - Строка HUD:
🟢 pkts: 1895 ✕ 0 hz: 21.0 age: 8ms t: 75.76 buf: 70 d:0.13 r:0.65 u:4067
p=(x,y,z) ψ=... γ=... θ=... r=0 v=...
| Поле | Смысл |
|---|---|
| флаг | ⚪ нет пакетов, 🟠 пакеты есть, но hz ≤ 1, 🟢 норма |
pkts |
счётчик принятых UDP-пакетов rx |
✕ |
отброшенные пакеты (неверная длина/разбор) |
hz |
оценка частоты приёма за ~0.25 с |
age |
возраст последнего пакета, мс |
t |
модельное время последнего пакета |
buf |
сколько измерений в буфере |
d |
текущая задержка воспроизведения, с |
r |
оценка скорости модельного времени (1.0 = реальное время) |
u |
счётчик кадров, когда данных впереди не было (выбег) |
p, ψ, γ, θ |
последние координаты и углы |
r= |
число перезапусков приёма (restarts) |
v |
горизонтальная скорость для контроля |
Как должна выглядеть модель
В модели drone_flight_demo.engee используется блок «3D сцена» — причинный
компонент с 13 скалярными входами. Порядок входов фиксирован и совпадает с
порядком полей пакета:
| № | Метка порта | Смысл | Единицы |
|---|---|---|---|
| 1 | Xpos |
координата X | м |
| 2 | Ypos |
координата Y (вверх) | м |
| 3 | Zpos |
координата Z | м |
| 4 | ψ |
курс (рыскание), вокруг вертикали мира | рад |
| 5 | γ |
крен, вокруг оси носа | рад |
| 6 | θ |
тангаж, вокруг правого крыла | рад |
| 7 | vx |
скорость по X | м/с |
| 8 | vy |
скорость по Y | м/с |
| 9 | vz |
скорость по Z | м/с |
| 10 | dψ |
угловая скорость курса | рад/с |
| 11 | dγ |
угловая скорость крена | рад/с |
| 12 | dθ |
угловая скорость тангажа | рад/с |
| 13 | state_id |
состояние для цвета: 0 — белый, 1 — красный, 2 — зелёный, 3 — жёлтый | — |
Параметры блока:
| Параметр | Значение | Комментарий |
|---|---|---|
port |
5150 |
UDP-порт приёмника; должен совпадать с GUI_IN_PORT в drone_graphics_app.jl. |
SampleTime |
1.0/25 |
Период отправки пакетов, с. Может быть 1.0 — клиент подстроится. |
Требования к CommonCode блока:
- сигнатура вызываемого метода должна принимать 13 входов плюс
t; - в сокет отправляются 14
Float64в big-endian в порядке портов, последнее
поле —float(t); - используется
Sockets.localhostи параметрport; - при остановке модели сокет закрывается в
terminate!.
Минимальный фрагмент, который должен быть в CommonCode:
function (c::Block)(t::Real, xp, yp, zp, psi, gamma, theta,
vx, vy, vz, dpsi, dgamma, dtheta, st)
bytes = UInt8[]
for val in (xp, yp, zp, psi, gamma, theta,
vx, vy, vz, dpsi, dgamma, dtheta, st, float(t))
append!(bytes, reinterpret(UInt8, [hton(val)]))
end
send(c.udpsocket, Sockets.localhost, port, bytes)
end
Параметры решателя в файле модели: SolverName = Euler, SolverType = fixed-step, FixedStep = 0.1, StopTime = 50. StopTime задаёт длительность
прогона; SampleTime блока задаёт частоту пакетов (25 Гц). Фактическая скорость
модельного времени на поде может быть ниже реального времени (≈0.6–0.8×) — клиент
измеряет её сам и не требует точного совпадения.
Меш аппарата: локальный кадр после подготовки — +Z нос, +Y вверх, +X левое
крыло. Ориентация строится как R = Rψ · Rθ · Rγ (авиационная
последовательность 3-2-1). Размеры меша берутся в его собственных единицах и
переводятся в метры: если наибольший габарит больше MESH_MM_THRESHOLD = 100,
меш считается заданным в миллиметрах (масштаб 0.001), иначе — в метрах.
Клетка земли равна GROUND_CELL = 1.0 м, поэтому аппарат рисуется в истинном
масштабе относительно сетки. Точка вращения — pivotFrac = -0.1 от высоты.
Оформление сцены — стерильная сетка для тестирования: почти белые клетки,
тёмно-серые линии и средне-серое небо. Цвета задаются константами SKY_COLOR,
CELL_COLOR, LINE_COLOR, толщина линий — LINE_PX. Прежняя версия с
оранжевой шахматкой сохранена в public/flight_orange.js.
Передача данных
UDP-пакет модели
- Адрес:
127.0.0.1, порт5150(параметрportблока). - Длина:
14 × 8 = 112байт,Float64, big-endian (hton). - Порядок:
x, y, z, ψ, γ, θ, vx, vy, vz, dψ, dγ, dθ, state_id, t. - Пакет отправляется на каждом шаге блока;
t— модельное время в секундах.
Формат /state
Одна строка CSV, 19 полей:
| № | Поле | Источник | Смысл |
|---|---|---|---|
| 1–3 | x,y,z |
модель | координаты, м |
| 4–6 | ψ,γ,θ |
модель | курс, крен, тангаж, рад |
| 7–9 | vx,vy,vz |
модель | скорости, м/с |
| 10–12 | dψ,dγ,dθ |
модель | угловые скорости, рад/с |
| 13 | state_id |
модель | цвет аппарата |
| 14 | rx |
сервер | счётчик принятых пакетов (растёт по одному) |
| 15 | bad |
сервер | отброшенные пакеты |
| 16 | hz |
сервер | частота приёма за ~0.25 с |
| 17 | age_ms |
сервер | возраст последнего пакета, мс; -1 — пакетов ещё не было |
| 18 | restarts |
сервер | число вызовов /reset |
| 19 | t |
модель | модельное время последнего пакета, с |
Точность: координаты — 4 знака, углы и скорости — 5, t — 6 знаков после
запятой.
/trace?since=<rx>
Возвращает по строке на каждый пакет с rx > since в том же 19-полевом формате.
Кольцевой буфер хранит последние TRACE_MAX = 1200 пакетов (≈1 мин при 25 Гц,
несколько минут при 1 Гц). При перезапуске модели (модельное время ушло назад
больше чем на 0.5 с) буфер очищается.
/reset (POST)
Перезапускает UDP-сокет и увеличивает restarts. Используется кнопкой
Reset UDP, если приём «залип» или порт был занят.
Запуск
-
Собрать меш (если менялся OBJ):
;julia obj2bin.jl drone_model.obj public/mesh.bin -
Запустить приложение:
engee.genie.start(joinpath(@__DIR__, "drone_graphics_app.jl"), log_file = joinpath(@__DIR__, "log.txt"), open_url = true)Первый запуск иногда завершается ошибкой «Unknown error while starting» —
повторите через несколько секунд. -
Запустить модель
drone_flight_demoобычным образом. Пакеты пойдут на
127.0.0.1:5150, страница начнёт движение. -
Остановка:
engee.genie.stop(joinpath(@__DIR__, "drone_graphics_app.jl")); sleep(5)
Параметры клиента, которые можно менять
| Параметр | Где | Значение | Влияние |
|---|---|---|---|
POLL_MS |
flight.js |
40 |
период опроса /state; меньше — больше нагрузка |
MAX_INFLIGHT |
flight.js |
2 |
одновременные запросы /state |
COAST_MAX |
flight.js |
1.0 |
максимальный выбег по последней скорости, с модельного времени |
| границы задержки | flight.js |
0.12…1.6 |
минимум/максимум буфера воспроизведения |
pushSample → 1.6 * p90 |
flight.js |
1.6 |
запас задержки относительно интервалов пакетов |
GROUND_CELL |
flight.js |
1.0 |
сторона клетки земли, м; задаёт масштаб всей сцены |
MESH_MM_THRESHOLD |
flight.js |
100 |
наибольший габарит меша больше этого значения — меш в миллиметрах, иначе в метрах |
SKY_COLOR |
flight.js |
0.58, 0.58, 0.58 |
цвет неба, средне-серый |
CELL_COLOR |
flight.js |
250, 250, 250 |
фон клетки, почти белый |
LINE_COLOR |
flight.js |
70, 70, 90 |
цвет линий сетки, тёмно-серый |
LINE_PX |
flight.js |
2 |
толщина линии в текстуре |
pivotFrac |
flight.js |
-0.1 |
точка вращения по высоте |
MODEL_ROT_X/Y/Z_DEG |
flight.js |
90/-90/0 |
ориентация исходного OBJ в кадре рендера |
chaseDist, chaseHeight |
flight.js |
15, 5 |
камера «погони»; минимальные значения, растут с габаритом меша |
camDist, camYaw, camPitch |
flight.js |
30, -0.35, 0.20 |
свободная камера; camDist тоже растёт с габаритом |
COLORS |
flight.js |
4 цвета | цвета по state_id |
GUI_IN_PORT |
drone_graphics_app.jl |
5150 |
UDP-порт; должен совпадать с port блока |
TRACE_MAX |
drone_graphics_app.jl |
1200 |
глубина истории для /trace |
Диагностика
| Симптом | Вероятная причина | Что делать |
|---|---|---|
HUD ⚪ WAITING, age = -1 |
модель не запущена, приложение не слушает порт, порт не совпадает | проверить engee.genie и параметр port; нажать Reset UDP |
✕ bad растёт |
длина пакета не 112 байт (старый блок с 13 числами, другой отправитель) | привести CommonCode к 14 Float64 с float(t) |
HUD 🟠, hz ≤ 1 |
модель идёт медленно или сеть/под подвисает | это допустимо: клиент увеличит задержку до 1.6 с и сохранит плавность |
| Пакеты идут, но аппарат стоит | rx не меняется или t не растёт |
смотреть rx/t в HUD; проверить, что модель действительно выполняется |
Нет меша, консоль mesh.bin не загрузился |
не собран public/mesh.bin |
выполнить obj2bin.jl |
| «Откатывание назад» | в буфер попали данные с немонотонным t без перезапуска |
проверить перезапуск модели и t в пакете; клиент обязан держать playT монотонным |
| Порт 5150 занят | остался старый процесс Genie | остановить приложение, подождать 5 с, запустить снова |
Важные ограничения
- Гарантия «только вперёд» достигается ценой задержки: при 1 Гц картинка
отстаёт от реального времени до ~1.6 с. Уменьшение задержки вернёт риск
коротких остановок между редкими пакетами. - При пропуске данных длиннее
COAST_MAXаппарат останавливается в точке
выбега; после прихода данных он продолжает движение вперёд, а поперечная
ошибка исправляется. Продольного отката при этом не происходит. - Порядок полей жёстко одинаков в трёх местах: CommonCode блока, приёмник
drone_graphics_app.jl, страницаflight.js. Менять его нужно сразу везде.