mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 07:31:13 +00:00
1273ec8eaf
- Reflects Stage 2 hexagonal layout: components/, hot_types/, obs/, pipeline/ - Updated architecture diagram with ports-and-adapters structure - Correct test count: 236 passed, 8 skipped (was: 216 from Stage 1) - New sections: AC traceability, taxonomy markers, per-env config, Roadmap table - ADR links to 0001-0004 - Component table with Protocol / dev / Jetson adapter columns - Removed stale Stage 1 next steps and sprint backlog Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
326 lines
14 KiB
Markdown
326 lines
14 KiB
Markdown
# 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 м |
|
||
|
||
---
|
||
|
||
## Ліцензія
|
||
|
||
Приватний репозиторій. Усі права захищено.
|