"""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