mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 17:11:14 +00:00
275f18d0e3
- Add env: Literal["jetson", "x86_dev", "ci", "sitl"] = "x86_dev" to AppSettings
- Expose RuntimeConfig = AppSettings alias for pipeline consumers
- Implement settings_customise_sources for YamlConfigSettingsSource overlay
- Create config/{x86_dev,jetson,ci,sitl}.yaml with env-specific defaults
- Add pyyaml>=6.0 to pyproject.toml dependencies
220 lines
6.6 KiB
Python
220 lines
6.6 KiB
Python
"""Application configuration loaded from environment / YAML."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Literal
|
|
|
|
from pydantic import Field
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict, YamlConfigSettingsSource
|
|
|
|
|
|
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",
|
|
)
|
|
|
|
env: Literal["jetson", "x86_dev", "ci", "sitl"] = "x86_dev"
|
|
|
|
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)
|
|
|
|
@classmethod
|
|
def settings_customise_sources(cls, settings_cls, **kwargs):
|
|
"""Load YAML overlay from config/{env}.yaml when present."""
|
|
import os
|
|
import yaml
|
|
|
|
init_kwargs = kwargs.get("init_settings")
|
|
env_settings = kwargs.get("env_settings")
|
|
dotenv = kwargs.get("dotenv_settings")
|
|
file_secret = kwargs.get("file_secret_settings")
|
|
|
|
# Determine which env we're in (check ENV env-var before loading YAML)
|
|
current_env = os.environ.get("ENV", "x86_dev")
|
|
yaml_path = Path(f"config/{current_env}.yaml")
|
|
|
|
yaml_source = None
|
|
if yaml_path.exists():
|
|
try:
|
|
yaml_source = YamlConfigSettingsSource(settings_cls, yaml_file=yaml_path)
|
|
except Exception:
|
|
yaml_source = None
|
|
|
|
sources = [s for s in [init_kwargs, env_settings, dotenv, file_secret] if s is not None]
|
|
if yaml_source is not None:
|
|
sources.append(yaml_source)
|
|
return tuple(sources)
|
|
|
|
|
|
# Alias for external consumers that expect RuntimeConfig
|
|
RuntimeConfig = AppSettings
|
|
|
|
_settings: AppSettings | None = None
|
|
|
|
|
|
def get_settings() -> AppSettings:
|
|
"""Cached singleton for application settings."""
|
|
global _settings
|
|
if _settings is None:
|
|
_settings = AppSettings()
|
|
return _settings
|