# GPS-Denied Onboard Бортова система GPS-denied навігації для фіксованого крила БПЛА на Jetson Orin Nano Super. Замінює GPS-сигнал власною оцінкою позиції на основі відеопотоку (cuVSLAM), IMU та супутникових знімків. Позиція подається у польотний контролер ArduPilot у форматі `GPS_INPUT` через MAVLink при 5–10 Гц. --- ## Архітектура ``` IMU (MAVLink RAW_IMU) ──────────────────────────────────────────▶ ESKF.predict() │ ADTI 20L V1 ──▶ ImageInputPipeline ──▶ ImageRotationManager │ │ │ ┌───────────────┼───────────────┐ │ ▼ ▼ ▼ │ cuVSLAM/ORB VO GlobalPlaceRecog SatelliteData │ (F07) (F08/Faiss) (F04) │ │ │ │ │ ▼ ▼ ▼ │ ESKF.update_vo() GSD norm MetricRefinement│ │ (F09) │ └──────────────────────▶ ESKF.update_sat()│ │ ESKF state ◀──┘ │ ┌───────────────┼──────────────┐ ▼ ▼ ▼ MAVLinkBridge FactorGraph SSE Stream GPS_INPUT 5-10Hz (GTSAM ISAM2) → Ground Station → ArduPilot FC ``` **State Machine** (`process_frame`): ``` NORMAL ──(VO fail)──▶ LOST ──▶ RECOVERY ──(GPR+Metric ok)──▶ NORMAL ``` --- ## Стек | Підсистема | Dev/CI | Jetson (production) | |-----------|--------|---------------------| | **Visual Odometry** | ORBVisualOdometry (OpenCV) | CuVSLAMVisualOdometry (PyCuVSLAM v15) | | **AI Inference** | MockInferenceEngine | TRTInferenceEngine (TensorRT FP16) | | **Place Recognition** | numpy L2 fallback | Faiss GPU index | | **MAVLink** | MockMAVConnection | pymavlink over UART | | **ESKF** | numpy (15-state) | numpy (15-state) | | **Factor Graph** | Mock poses | GTSAM 4.3 ISAM2 | | **API** | FastAPI + Pydantic v2 + SSE | FastAPI + Pydantic v2 + SSE | | **БД** | SQLite + SQLAlchemy 2 async | SQLite | | **Тести** | pytest + pytest-asyncio | — | --- ## Швидкий старт ### Вимоги - Python ≥ 3.11 - ~500 MB дискового простору (GTSAM wheel) ### Встановлення ```bash git clone https://github.com/azaion/gps-denied-onboard.git cd gps-denied-onboard git checkout stage1 python3 -m venv .venv source .venv/bin/activate pip install -e ".[dev]" ``` ### Запуск ```bash # Прямий запуск uvicorn gps_denied.app:app --host 0.0.0.0 --port 8000 # Docker docker compose up --build ``` Сервер: `http://127.0.0.1:8000` ### Змінні середовища ```env # Основні DB_URL=sqlite+aiosqlite:///./flight_data.db SATELLITE_TILE_DIR=.satellite_tiles MAVLINK_CONNECTION=serial:/dev/ttyTHS1:57600 # або tcp:host:port MAVLINK_OUTPUT_HZ=5.0 MAVLINK_TELEMETRY_HZ=1.0 # ESKF тюнінг (опціонально) ESKF_VO_POSITION_NOISE=0.3 ESKF_SATELLITE_MAX_AGE=30.0 ESKF_MAHALANOBIS_THRESHOLD=16.27 # API API_HOST=127.0.0.1 API_PORT=8000 # Моделі MODEL_WEIGHTS_DIR=weights ``` Повний список: `src/gps_denied/config.py` (40+ параметрів з prefix `DB_`, `API_`, `TILES_`, `MODEL_`, `MAVLINK_`, `SATELLITE_`, `ESKF_`, `RECOVERY_`, `ROTATION_`). --- ## API | Endpoint | Метод | Опис | |----------|-------|------| | `/health` | GET | Health check | | `/flights` | POST | Створити політ | | `/flights/{id}` | GET | Деталі польоту | | `/flights/{flight_id}` | DELETE | Видалити політ | | `/flights/{flight_id}/images/batch` | POST | Батч зображень | | `/flights/{flight_id}/user-fix` | POST | GPS-якір від оператора → ESKF update | | `/flights/{flight_id}/status` | GET | Статус обробки | | `/flights/{flight_id}/stream` | GET | SSE стрім (позиція + confidence) | | `/flights/{flight_id}/frames/{frame_id}/object-to-gps` | POST | Pixel → GPS (ray-ground проекція) | | `/flights/{flight_id}/waypoints/{waypoint_id}` | PUT | Оновити waypoint | | `/flights/{flight_id}/waypoints/batch` | PUT | Batch update waypoints | --- ## Тести ```bash # Всі тести python -m pytest -q # Конкретний модуль python -m pytest tests/test_eskf.py -v python -m pytest tests/test_mavlink.py -v python -m pytest tests/test_accuracy.py -v # SITL (потребує ArduPilot SITL) docker compose -f docker-compose.sitl.yml up -d ARDUPILOT_SITL_HOST=localhost pytest tests/test_sitl_integration.py -v # E2E пайплайн на публічних UAV-датасетах (EuRoC / VPAIR / MARS-LVIG) pytest tests/e2e/ -q # unit + skip-when-absent (швидко) pytest tests/e2e/ -m "e2e and not e2e_slow" -v # CI-tier з завантаженим датасетом pytest tests/e2e/ -m e2e_slow -v # nightly-tier (VPAIR sample, MARS-LVIG stress) # Завантажити датасет (EuRoC MH_01 — CI-tier; URL у `src/gps_denied/testing/download.py`) python scripts/download_dataset.py euroc_mh01 # у datasets/euroc/MH_01/ # VPAIR sample (fixed-wing, downward, 300-400 м) — form-gated на Zenodo # Розпакувати так, щоб datasets/vpair/sample/poses_query.txt існував # SHA256 зашитий у DATASET_REGISTRY для верифікації відомого артефакту ``` E2E-харнес гонить `FlightProcessor` як black-box через спільний `DatasetAdapter` (`src/gps_denied/testing/`). Датасети лежать у `./datasets/` (gitignored), тести пропускаються (не фейляться) коли датасету немає. Детально — у локальному design doc `.planning/brainstorms/2026-04-16-e2e-datasets-design.md` та плані `2026-04-16-e2e-datasets-plan.md`. **Поточний статус реальних прогонів:** - **VPAIR sample** (200 кадрів fixed-wing 300-400 м над Bonn/Eifel): пайплайн завершується без падінь, але ATE RMSE ~1770 км → xfail. Причина: VO без IMU/супутникового anchoring розходиться на fixed-wing траєкторії. Очікувано до тюнінгу VO+GPR під nadir-знімки високої висоти. - **EuRoC MH_01**, **MARS-LVIG** — тести скіпаються (датасети не завантажені локально). ### Покриття тестами (195 passed / 8 skipped — unit/component; e2e — окремо) | Файл тесту | Компонент | К-сть | |-------------|-----------|-------| | `test_schemas.py` | Pydantic схеми | 12 | | `test_database.py` | SQLAlchemy CRUD | 9 | | `test_api_flights.py` | REST endpoints | 5 | | `test_health.py` | Health check | 1 | | `test_eskf.py` | ESKF 15-state | 17 | | `test_coordinates.py` | ENU/GPS/pixel | 4 | | `test_satellite.py` | Тайли + Mercator | 8 | | `test_pipeline.py` | Image queue | 5 | | `test_rotation.py` | 360° ротації | 4 | | `test_models.py` | Model Manager + TRT | 6 | | `test_vo.py` | VO (ORB + cuVSLAM) | 8 | | `test_gpr.py` | Place Recognition (Faiss) | 7 | | `test_metric.py` | Metric Refinement + GSD | 6 | | `test_graph.py` | Factor Graph (GTSAM) | 4 | | `test_chunk_manager.py` | Chunk lifecycle | 3 | | `test_recovery.py` | Recovery coordinator | 2 | | `test_processor_full.py` | State Machine | 4 | | `test_processor_pipe.py` | PIPE wiring (Phase 5) | 13 | | `test_mavlink.py` | MAVLink I/O bridge | 19 | | `test_acceptance.py` | AC сценарії + perf | 6 | | `test_accuracy.py` | Accuracy validation | 23 | | `test_sitl_integration.py` | SITL (skip без ArduPilot) | 8 | | | **Всього** | **195+8** | --- ## Benchmark валідації (Phase 7) ```bash python scripts/benchmark_accuracy.py --frames 50 ``` Результати на синтетичній траєкторії (20 м/с, 0.7 fps, шум VO 0.3 м, супутник кожні 5 кадрів): | Критерій | Результат | Ліміт | |---------|-----------|-------| | 80% кадрів ≤ 50 м | ✅ 100% | ≥ 80% | | 60% кадрів ≤ 20 м | ✅ 100% | ≥ 60% | | p95 затримка | ✅ ~9 мс | < 400 мс | | VO дрейф за 1 км | ✅ ~11 м | < 100 м | --- ## Структура проєкту ``` gps-denied-onboard/ ├── src/gps_denied/ │ ├── app.py # FastAPI factory + lifespan │ ├── config.py # Pydantic Settings │ ├── api/routers/flights.py # REST + SSE endpoints │ ├── core/ │ │ ├── eskf.py # 15-state ESKF (IMU+VO+satellite fusion) │ │ ├── processor.py # FlightProcessor + process_frame │ │ ├── vo.py # ORBVisualOdometry / CuVSLAMVisualOdometry │ │ ├── mavlink.py # MAVLinkBridge → GPS_INPUT → ArduPilot │ │ ├── satellite.py # SatelliteDataManager (local z/x/y tiles) │ │ ├── gpr.py # GlobalPlaceRecognition (Faiss/numpy) │ │ ├── metric.py # MetricRefinement (LiteSAM/XFeat + GSD) │ │ ├── graph.py # FactorGraphOptimizer (GTSAM ISAM2) │ │ ├── coordinates.py # CoordinateTransformer (ENU↔GPS↔pixel) │ │ ├── models.py # ModelManager + TRTInferenceEngine │ │ ├── benchmark.py # AccuracyBenchmark + SyntheticTrajectory │ │ ├── pipeline.py # ImageInputPipeline │ │ ├── rotation.py # ImageRotationManager │ │ ├── recovery.py # FailureRecoveryCoordinator │ │ └── chunk_manager.py # RouteChunkManager │ ├── schemas/ # Pydantic схеми (eskf, mavlink, vo, ...) │ ├── db/ # SQLAlchemy ORM + async repository │ └── utils/mercator.py # Web Mercator tile utilities ├── tests/ # 22 test модулі ├── scripts/ │ └── benchmark_accuracy.py # CLI валідація точності ├── Dockerfile # Multi-stage Python 3.11 image ├── docker-compose.yml # Local dev ├── docker-compose.sitl.yml # ArduPilot SITL harness ├── .github/workflows/ │ ├── ci.yml # lint + pytest + docker smoke (кожен push) │ └── sitl.yml # SITL integration (нічний / ручний) └── pyproject.toml ``` --- ## Компоненти | ID | Назва | Файл | Dev | Jetson | |----|-------|------|-----|--------| | F04 | Satellite Data Manager | `core/satellite.py` | local tiles | local tiles | | F05 | Image Input Pipeline | `core/pipeline.py` | ✅ | ✅ | | F06 | Image Rotation Manager | `core/rotation.py` | ✅ | ✅ | | F07 | Sequential Visual Odometry | `core/vo.py` | ORB | cuVSLAM | | F08 | Global Place Recognition | `core/gpr.py` | numpy | Faiss GPU | | F09 | Metric Refinement | `core/metric.py` | Mock | LiteSAM/XFeat TRT | | F10 | Factor Graph Optimizer | `core/graph.py` | Mock | GTSAM ISAM2 | | F11 | Failure Recovery | `core/recovery.py` | ✅ | ✅ | | F12 | Route Chunk Manager | `core/chunk_manager.py` | ✅ | ✅ | | F13 | Coordinate Transformer | `core/coordinates.py` | ✅ | ✅ | | F16 | Model Manager | `core/models.py` | Mock | TRT engines | | F17 | ESKF Sensor Fusion | `core/eskf.py` | ✅ | ✅ | | F18 | MAVLink I/O Bridge | `core/mavlink.py` | Mock | pymavlink | --- ## Що залишилось (on-device) 1. Офлайн завантаження тайлів для зони місії → `{tile_dir}/z/x/y.png` 2. Конвертація моделей: LiteSAM/XFeat PyTorch → ONNX → TRT FP16 3. Запуск SITL: `docker compose -f docker-compose.sitl.yml up` 4. Польотні дані: записати GPS + відео → порівняти ESKF-траєкторію з ground truth 5. Калібрування: camera intrinsics + IMU noise density для конкретного апарату --- ## Ліцензія Приватний репозиторій. Усі права захищено.