"""ArduPilot Plane / iNav SITL state-read observers. Reads what the SUT delivered to the FC over its external-positioning interface, without ever bypassing the FC's own acceptance path. This is the only legal way for blackbox tests to assert AC-4.3 (FC output contract): every assertion goes through the SITL's state machine. Public surface only; concrete pymavlink / yamspy / msp_gps_toy subprocess plumbing is owned by AZ-416 (FT-P-09-AP) and AZ-417 (FT-P-09-iNav). """ from __future__ import annotations from dataclasses import dataclass from typing import Literal, Protocol FcKind = Literal["ardupilot", "inav"] @dataclass(frozen=True) class FcGpsState: """The subset of FC state the e2e tests assert against. AP: assembled from EKF source-set + GLOBAL_POSITION_INT replay-back. iNav: assembled from MSP2 GPS-provider state + getRawGPS query. """ primary_source: str # "MAV" (AP gps_type=14) or "MSP" (iNav) last_position_lat_deg: float last_position_lon_deg: float last_position_alt_m: float fix_quality: int # 0..6 per NMEA convention horizontal_accuracy_m: float last_update_age_ms: int class FcSitlObserver(Protocol): """Common observer protocol — implemented by `ArduPilotObserver` + `InavObserver`.""" fc_kind: FcKind def read_gps_state(self) -> FcGpsState: ... def read_parameter(self, name: str) -> float | int | str | None: ... def get_observer(fc_kind: FcKind, host: str) -> FcSitlObserver: """Factory — returns the matching observer for the requested FC. AZ-416/417 own the concrete return types. AZ-406 raises until those tasks land so test authors can plumb the observer through their fixtures without yet running them. """ raise NotImplementedError( f"sitl_observer.get_observer({fc_kind=}, {host=}) is owned by " "AZ-416 (AP) / AZ-417 (iNav) — AZ-406 supplies only the contract." )