# 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
```

### Покриття тестами (195 passed / 8 skipped)

| Файл тесту | Компонент | К-сть |
|-------------|-----------|-------|
| `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 для конкретного апарату

---

## Ліцензія

Приватний репозиторій. Усі права захищено.
