Files
gps-denied-onboard/src/gps_denied/config.py
T
Yuzviak 78dcf7b4e7 fix: post-audit — runtime bugs, functional gaps, docs, hardening
Phase A — Runtime bugs:
  - SSE: add push_event() method to SSEEventStreamer (was missing, masked by mocks)
  - MAVLink: satellites_visible=10 (was 0, triggers ArduPilot failsafe)
  - MAVLink: horiz_accuracy=sqrt(P[0,0]+P[1,1]) per spec (was sqrt(avg))
  - MAVLink: MEDIUM confidence → fix_type=3 per solution.md (was 2)

Phase B — Functional gaps:
  - handle_user_fix() injects operator GPS into ESKF with noise=500m
  - app.py uses create_vo_backend() factory (was hardcoded SequentialVO)
  - ESKF: Mahalanobis gating on satellite updates (rejects outliers >5σ)
  - ESKF: public accessors (position, quaternion, covariance, last_timestamp)
  - Processor: no more private ESKF field access

Phase C — Documentation:
  - README: correct API endpoints, CLI command, 40+ env vars documented
  - Dockerfile: ENV prefixes match pydantic-settings (DB_, SATELLITE_, MAVLINK_)
  - tech_stack.md marked ARCHIVED (contradicts solution.md)

Phase D — Hardening:
  - JWT auth middleware (AUTH_ENABLED=false default, verify_token on /flights)
  - TLS config env vars (AUTH_SSL_CERTFILE, AUTH_SSL_KEYFILE)
  - SHA-256 tile manifest verification in SatelliteDataManager
  - AuthConfig, ESKFSettings, MAVLinkConfig, SatelliteConfig in config.py

Also: conftest.py shared fixtures, download_tiles.py, convert_to_trt.py scripts,
config wiring into app.py lifespan, config-driven ESKF, calculate_precise_angle fix.

Tests: 196 passed / 8 skipped. Ruff clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 18:27:35 +03:00

187 lines
5.4 KiB
Python

"""Application configuration loaded from environment / YAML."""
from __future__ import annotations
from pathlib import Path
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class DatabaseConfig(BaseSettings):
"""Database connection settings."""
model_config = SettingsConfigDict(env_prefix="DB_")
url: str = Field(
default="sqlite+aiosqlite:///./flight_data.db",
description="SQLAlchemy async connection URL",
)
echo: bool = False
class APIConfig(BaseSettings):
"""API server settings."""
model_config = SettingsConfigDict(env_prefix="API_")
host: str = "127.0.0.1"
port: int = 8000
reload: bool = True
cors_origins: list[str] = Field(default_factory=lambda: ["*"])
class TileProviderConfig(BaseSettings):
"""Satellite tile provider settings (Google Maps by default)."""
model_config = SettingsConfigDict(env_prefix="TILES_")
provider: str = "google"
api_key: str = ""
cache_dir: Path = Path("tile_cache")
zoom_level: int = 18
max_concurrent_requests: int = 4
class ModelPaths(BaseSettings):
"""Paths to ML model weights."""
model_config = SettingsConfigDict(env_prefix="MODEL_")
weights_dir: Path = Path("weights")
superpoint_path: str = "superpoint.pt"
lightglue_path: str = "lightglue.pt"
litesam_path: str = "litesam.pt"
class OperationalArea(BaseSettings):
"""Default operational boundaries."""
model_config = SettingsConfigDict(env_prefix="AREA_")
name: str = "Eastern Ukraine"
min_lat: float = 45.0
max_lat: float = 52.0
min_lon: float = 22.0
max_lon: float = 40.0
class RecoveryConfig(BaseSettings):
"""Failure recovery and progressive search settings."""
model_config = SettingsConfigDict(env_prefix="RECOVERY_")
search_grid_sizes: list[int] = Field(default_factory=lambda: [1, 4, 9, 16, 25])
min_chunk_frames: int = 5
max_chunk_frames: int = 20
user_input_threshold_tiles: int = 25
confidence_threshold_good: float = 0.7
confidence_threshold_degraded: float = 0.5
min_inlier_count_good: int = 50
min_inlier_count_tracking: int = 20
class RotationConfig(BaseSettings):
"""Image rotation and heading tracking settings."""
model_config = SettingsConfigDict(env_prefix="ROTATION_")
step_degrees: float = 30.0
litesam_max_tolerance: float = 45.0
sharp_turn_threshold: float = 45.0
heading_history_size: int = 10
confidence_threshold: float = 0.7
@property
def rotation_iterations(self) -> int:
return int(360 / self.step_degrees)
class AuthConfig(BaseSettings):
"""JWT authentication settings."""
model_config = SettingsConfigDict(env_prefix="AUTH_")
enabled: bool = False # False для dev/SITL, True для production
secret_key: str = "dev-secret-change-in-production"
algorithm: str = "HS256"
ssl_certfile: str = "" # шлях до TLS cert (порожній = без TLS)
ssl_keyfile: str = "" # шлях до TLS key
class MAVLinkConfig(BaseSettings):
"""MAVLink I/O bridge settings."""
model_config = SettingsConfigDict(env_prefix="MAVLINK_")
connection: str = Field(
default="udp:127.0.0.1:14550",
description="pymavlink connection string (serial:/dev/ttyTHS1:57600 or tcp:host:port)",
)
output_hz: float = 5.0
telemetry_hz: float = 1.0
class SatelliteConfig(BaseSettings):
"""Pre-loaded satellite tile directory settings."""
model_config = SettingsConfigDict(env_prefix="SATELLITE_")
tile_dir: str = ".satellite_tiles"
zoom_level: int = 18
class ESKFSettings(BaseSettings):
"""ESKF tuning parameters (overridable via env vars)."""
model_config = SettingsConfigDict(env_prefix="ESKF_")
accel_noise_density: float = 6.86e-4
gyro_noise_density: float = 5.24e-5
accel_random_walk: float = 2.0e-3
gyro_random_walk: float = 8.73e-7
vo_position_noise: float = 0.3
sat_noise_min: float = 5.0
sat_noise_max: float = 20.0
satellite_max_age: float = 30.0
covariance_high_threshold: float = 400.0
init_pos_var: float = 100.0
init_vel_var: float = 1.0
init_att_var: float = 0.01
init_accel_bias_var: float = 1e-4
init_gyro_bias_var: float = 1e-6
class AppSettings(BaseSettings):
"""Root settings — aggregates all sub-configs."""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
env_nested_delimiter="__",
extra="ignore",
)
db: DatabaseConfig = Field(default_factory=DatabaseConfig)
api: APIConfig = Field(default_factory=APIConfig)
tiles: TileProviderConfig = Field(default_factory=TileProviderConfig)
models: ModelPaths = Field(default_factory=ModelPaths)
area: OperationalArea = Field(default_factory=OperationalArea)
recovery: RecoveryConfig = Field(default_factory=RecoveryConfig)
rotation: RotationConfig = Field(default_factory=RotationConfig)
auth: AuthConfig = Field(default_factory=AuthConfig)
mavlink: MAVLinkConfig = Field(default_factory=MAVLinkConfig)
satellite: SatelliteConfig = Field(default_factory=SatelliteConfig)
eskf: ESKFSettings = Field(default_factory=ESKFSettings)
_settings: AppSettings | None = None
def get_settings() -> AppSettings:
"""Cached singleton for application settings."""
global _settings
if _settings is None:
_settings = AppSettings()
return _settings