# GPS-Denied Onboard Бортова система GPS-denied навігації для фіксованого крила БПЛА на Jetson Orin Nano Super. Замінює GPS-сигнал власною оцінкою позиції на основі відеопотоку (cuVSLAM), IMU та супутникових знімків. Позиція подається у польотний контролер ArduPilot у форматі `GPS_INPUT` через MAVLink при 5–10 Гц. **Гілка розробки:** `stage2` | **Фаза:** 2/6 (завершено) | **Тести:** 236 passed, 8 skipped --- ## Архітектура (Stage 2 — Hexagonal / Ports-and-Adapters) ``` IMU (MAVLink RAW_IMU) ──────────────────────────────────────────▶ ESKF.predict() │ ADTI 20L V1 ──▶ pipeline/image_input ──▶ ImageRotationManager │ │ │ ┌───────────────┼───────────────┐ │ ▼ ▼ ▼ │ components/vio components/gpr components/ │ cuVSLAM (Jetson) Faiss+DINOv2 satellite_ │ ORB-SLAM3 (dev) numpy (dev) matcher │ │ │ │ │ ▼ ▼ ▼ │ ESKF.update_vo() GPR norm MetricRefinement│ │ (XFeat TRT) │ └──────────────────────▶ ESKF.update_sat()│ │ ESKF state ◀──┘ │ ┌───────────────┼──────────────┐ ▼ ▼ ▼ components/mavlink_io core/factor_graph SSE stream GPS_INPUT 5-10Hz (GTSAM ISAM2) → ground station → ArduPilot FC ``` **Стейт-машина** (`FlightProcessor.process_frame`): ``` NORMAL ──(VO fail)──▶ LOST ──▶ RECOVERY ──(GPR+Metric ok)──▶ NORMAL ``` **Правило залежностей:** `pipeline/orchestrator.py` імпортує лише Protocols. Тільки `pipeline/composition.py` (`build_pipeline(env)`) знає про конкретні адаптери. --- ## Стек | Підсистема | Dev / CI | Jetson (production) | |-----------|----------|---------------------| | **Visual Odometry** | `ORBVisualOdometry` / `CuVSLAMMonoDepthVO` | `CuVSLAMMonoDepthVO` (PyCuVSLAM v15, barometer як synthetic depth) | | **AI Inference** | `MockInferenceEngine` | `TRTInferenceEngine` (TensorRT FP16) | | **Place Recognition** | numpy L2 fallback | Faiss GPU + DINOv2-VLAD TRT FP16 | | **MAVLink** | `MockMAVConnection` | pymavlink over UART | | **ESKF** | numpy 15-state | numpy 15-state | | **Factor Graph** | stub | GTSAM 4.3 ISAM2 | | **Логування** | structlog ConsoleRenderer | structlog JSON (orjson) | | **API** | FastAPI + Pydantic v2 + SSE | FastAPI + Pydantic v2 + SSE | | **БД** | SQLite + SQLAlchemy 2 async | SQLite | --- ## Швидкий старт ### Вимоги - Python ≥ 3.11 - ~500 MB дискового простору (GTSAM wheel) ### Встановлення ```bash git clone https://github.com/azaion/gps-denied-onboard.git cd gps-denied-onboard git checkout stage2 python3 -m venv .venv source .venv/bin/activate pip install -e ".[dev]" ``` ### Запуск ```bash # Прямий запуск (env=x86_dev за замовчуванням) ENV=x86_dev uvicorn gps_denied.app:app --host 0.0.0.0 --port 8000 # Docker docker compose up --build ``` Сервер: `http://127.0.0.1:8000` ### Конфігурація Вибір середовища задається змінною `ENV`: ```env ENV=x86_dev # за замовчуванням — ORB-SLAM3, моки, консольні логи ENV=jetson # cuVSLAM + TRT + pymavlink + JSON logs ENV=ci # усі моки, без hardware ENV=sitl # ArduPilot SITL ``` Кожне середовище має overlay у `config/{env}.yaml`. Усі параметри — у `src/gps_denied/config.py`. **Ключові змінні середовища:** ```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 ESKF_VO_POSITION_NOISE=0.3 ESKF_SATELLITE_MAX_AGE=30.0 MODEL_WEIGHTS_DIR=weights ``` --- ## 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) | --- ## Тести ```bash # Всі тести (236 passed, 8 skipped) python -m pytest tests/ -q --ignore=tests/e2e # За категорією (taxonomy маркери) python -m pytest -m unit -q python -m pytest -m integration -q python -m pytest -m blackbox -q # Тести прив'язані до Acceptance Criteria python -m pytest -m ac -q # тільки ac-marked тести python -m pytest -m ac --ac-dump # + таблиця покриття AC # SITL (потребує ArduPilot SITL) docker compose -f docker-compose.sitl.yml up -d pytest -m sitl tests/ -v # E2E на публічних UAV-датасетах pytest tests/e2e/ -q # skip якщо датасет відсутній pytest tests/e2e/ -m "e2e and not e2e_slow" -v # CI-tier pytest tests/e2e/ -m e2e_slow -v # nightly-tier (VPAIR, MARS-LVIG) ``` **E2E результати (EuRoC MH_01–05, indoor):** | Датасет | Кадри | ESKF ATE RMSE | Статус | |---------|-------|---------------|--------| | EuRoC MH_01 (easy) | 100 | 0.205 м | PASS | | EuRoC MH_02 (easy) | 100 | 0.131 м | PASS | | EuRoC MH_03 (medium) | 100 | 0.008 м | PASS | | EuRoC MH_04 (difficult) | 100 | 0.009 м | PASS | | EuRoC MH_05 (difficult) | 100 | 0.007 м | PASS | | VPAIR sample (fixed-wing) | 200 | — | xfail (немає raw IMU) | ### AC Traceability ```bash # Звіт: 39 AC total — 14 covered, 21 pending-phase-3+, 4 deferred-hardware python scripts/gen_ac_traceability.py # CI gate (виходить 0 якщо всі непокриті позначені pending/deferred) python scripts/gen_ac_traceability.py --check ``` Матриця: `.planning/AC-TRACEABILITY.md` --- ## Структура проєкту ``` gps-denied-onboard/ ├── src/gps_denied/ │ ├── app.py # FastAPI factory + lifespan │ ├── config.py # AppSettings / RuntimeConfig (pydantic-settings) │ ├── components/ # Hexagonal adapters (ports + impls) │ │ ├── vio/ # Visual Odometry │ │ ├── satellite_matcher/ # Tile loading + XFeat metric refinement │ │ ├── gpr/ # Global Place Recognition (Faiss/numpy) │ │ ├── mavlink_io/ # MAVLink bridge + mock │ │ ├── anchor_verifier/ # stub — Phase 3 │ │ ├── safety_state/ # stub — Phase 3 │ │ ├── flight_recorder/ # stub — Phase 4 │ │ └── coordinate_transforms/ # stub — Phase 5 │ ├── core/ # Concentrated math (без DI) │ │ ├── eskf.py # 15-state ESKF (IMU+VO+satellite fusion) │ │ ├── factor_graph.py # FactorGraphOptimizer (GTSAM ISAM2) │ │ ├── coordinates.py # ENU↔GPS↔pixel transforms │ │ ├── chunk_manager.py # RouteChunkManager │ │ ├── recovery.py # FailureRecoveryCoordinator │ │ ├── rotation.py # ImageRotationManager │ │ └── models.py # ModelManager + TRTInferenceEngine │ ├── hot_types/ # @dataclass(slots=True, frozen=True) — hot path │ ├── obs/ # Observability (Phase 2) │ │ ├── logging_config.py # configure_logging(env) — JSON/console │ │ └── log_schemas.py # Pydantic v2 boundary log event schemas │ ├── pipeline/ # Orchestration layer │ │ ├── orchestrator.py # FlightProcessor + process_frame │ │ ├── composition.py # build_pipeline(env) — composition root │ │ ├── image_input.py │ │ ├── result_manager.py │ │ └── sse_streamer.py │ ├── api/routers/flights.py # REST endpoints + SSE │ ├── schemas/ # Pydantic REST schemas + shims до hot_types │ └── db/ # SQLAlchemy ORM + async repository ├── tests/ # 37 test-модулів з pytestmark (236 passed) │ └── e2e/ # E2E на публічних UAV-датасетах ├── config/ # Per-env YAML overlays │ ├── jetson.yaml │ ├── x86_dev.yaml │ ├── ci.yaml │ └── sitl.yaml ├── scripts/ │ ├── gen_ac_traceability.py # AC coverage report + CI gate │ └── benchmark_accuracy.py # Synthetic trajectory accuracy CLI ├── _docs/ │ ├── 00_problem/ │ │ └── acceptance_criteria.md # 39 AC (AC-1.1..AC-8.6 + AC-NEW-1..8) │ └── 01_solution/decisions/ # ADRs 0001–0004 ├── Dockerfile ├── docker-compose.yml ├── docker-compose.sitl.yml └── .github/workflows/ ├── ci.yml # lint → {unit, integration, blackbox, ac-gate} → docker ├── nightly.yml # sitl + e2e slow (cron 03:00 UTC) └── sitl.yml # SITL integration ``` --- ## Компоненти (Stage 2) | Компонент | Protocol | Dev adapter | Jetson adapter | |-----------|----------|-------------|----------------| | Visual Odometry | `vio/protocol.py` | `ORBVisualOdometry` | `CuVSLAMMonoDepthVO` | | Satellite Matcher | `satellite_matcher/protocol.py` | `LocalTileLoader` + `MetricRefinement` | те саме | | Place Recognition | `gpr/protocol.py` | `FaissGPR` (numpy fallback) | `FaissGPR` (GPU) | | MAVLink I/O | `mavlink_io/protocol.py` | `MockMAVConnection` | `MAVLinkBridge` (pymavlink) | | Anchor Verifier | `anchor_verifier/protocol.py` | stub | stub (Phase 3) | | Safety State | `safety_state/protocol.py` | stub | stub (Phase 3) | | Flight Recorder | `flight_recorder/protocol.py` | stub | stub (Phase 4) | | Coord. Transforms | `coordinate_transforms/protocol.py` | stub | stub (Phase 5) | --- ## Acceptance Criteria 39 AC з ідентифікаторами `AC-N.M` / `AC-NEW-N` у `_docs/00_problem/acceptance_criteria.md`. Ключові порогові значення: | Критерій | Поріг | |----------|-------| | Точність 80% кадрів | ≤ 50 м | | Точність 60% кадрів | ≤ 20 м | | End-to-end latency | < 400 мс | | Пам'ять (shared CPU/GPU) | < 8 GB | | Сторадж місії | < 64 GB | | GSD супутникового знімку | ≤ 0.5 м/px | | Час до першої позиції | ≤ 30 с | --- ## Архітектурні рішення (ADRs) | ADR | Рішення | |-----|---------| | [0001](_docs/01_solution/decisions/0001-e2e-dataset-strategy.md) | E2E dataset strategy (EuRoC / VPAIR / MARS-LVIG) | | [0002](_docs/01_solution/decisions/0002-hexagonal-architecture-stage2.md) | Hexagonal / ports-and-adapters для Stage 2 | | [0003](_docs/01_solution/decisions/0003-hot-path-dataclasses-vs-pydantic.md) | `@dataclass(slots=True, frozen=True)` на hot path; Pydantic лише на межах | | [0004](_docs/01_solution/decisions/0004-stage2-as-independent-iteration.md) | Stage 2 як незалежна ітерація зі своїми фазами 1–6 | --- ## Roadmap Stage 2 | Фаза | Назва | Статус | |------|-------|--------| | 1 | Hexagonal Refactor | done (2026-05-11) | | 2 | AC Doc + Test Taxonomy + Observability Spine | done (2026-05-11) | | 3 | Safety Anchor State Machine + Geometry-Gated Anchor Verifier | planned | | 4 | Conditional VPR + Flight Data Recorder | planned | | 5 | MAVLink source labels + dual-channel scaffold | planned | | 6 | Azaion 10.05.2026 real-flight integration fixture | planned | --- ## Benchmark ```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 latency | ~9 мс | < 400 мс | | VO дрейф за 1 км | ~11 м | < 100 м | --- ## Ліцензія Приватний репозиторій. Усі права захищено.