[AZ-390] [AZ-392] C8 FC/GCS adapter foundation + covariance projector

Adds the C8 foundation:
- FcAdapter / GcsAdapter / ReplaySink Protocols + contract DTOs in
  _types/fc.py (PortConfig, FcKind, FlightState, GpsStatus, Severity,
  TelemetryKind, FcTelemetryFrame, FlightStateSignal, GpsHealth,
  OperatorCommand, Subscription, Imu/Attitude samples).
- Disjoint FcAdapterError / GcsAdapterError trees with
  SourceSetSwitchNotSupportedError <: SourceSetSwitchError per AC-9.
- FcConfig + GcsConfig cross-cutting Config blocks with config-load
  validation (unknown strategy rejected at __post_init__).
- runtime_root/fc_factory.py: build_fc_adapter / build_gcs_adapter
  with BUILD_FC_*/BUILD_GCS_* flag gating + INFO log on load +
  single-writer outbound-thread binding.
- CovarianceProjector (helper, AZ-392): 6x6 -> 3x3 -> 2x2 ->
  sqrt(lambda_max) reduction; AP returns float m, iNav returns int mm
  with uint16 clamp + WARN + FDR record. Non-SPD / NaN / wrong-shape
  raise FcEmitError and emit an FDR ERROR record carrying frame_id.

Contracts:
- composition_root_protocol.md 1.1.0 -> 1.2.0 (added fc/gcs blocks +
  build_fc_adapter / build_gcs_adapter + outbound-thread binding).
- fc_adapter_protocol.md unchanged (this batch implements v1.0.0).

Tests: 410 pass / 2 skip / 0 fail (+53 new tests in batch 8).

AZ-391 (inbound subscription) deferred to batch 9 — pulls YAMSPy as
a new external dependency (iNav MSP2 decode).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 04:17:59 +03:00
parent e4ecdaf619
commit 362e93c626
22 changed files with 1909 additions and 59 deletions
+27
View File
@@ -23,7 +23,9 @@ import yaml
from gps_denied_onboard.config.schema import (
_COMPONENT_REGISTRY,
Config,
FcConfig,
FdrConfig,
GcsConfig,
LogConfig,
RequiredFieldMissingError,
RuntimeConfig,
@@ -49,6 +51,15 @@ ENV_KEY_MAP: Final[dict[str, tuple[str, str]]] = {
"LOG_SINK": ("log", "sink"),
"FDR_PATH": ("fdr", "path"),
"FDR_QUEUE_SIZE": ("fdr", "queue_size"),
# C8 FC + GCS adapter blocks (AZ-390)
"FC_ADAPTER": ("fc", "adapter"),
"FC_PORT_DEVICE": ("fc", "port_device"),
"FC_PORT_BAUD": ("fc", "port_baud"),
"FC_SIGNING_KEY_SOURCE": ("fc", "signing_key_source"),
"GCS_ADAPTER": ("gcs", "adapter"),
"GCS_PORT_DEVICE": ("gcs", "port_device"),
"GCS_PORT_BAUD": ("gcs", "port_baud"),
"GCS_SUMMARY_RATE_HZ": ("gcs", "summary_rate_hz"),
}
# Env vars that MUST resolve to a non-empty value before `load_config`
@@ -81,6 +92,12 @@ _FIELD_COERCIONS: Final[dict[str, type]] = {
"inference_backend": str,
"tile_cache_path": str,
"overrun_policy": str,
# C8 FC + GCS adapter coercions (AZ-390)
"adapter": str,
"port_device": str,
"port_baud": int,
"signing_key_source": str,
"summary_rate_hz": float,
}
@@ -160,6 +177,14 @@ def load_config(
FdrConfig(),
{k: _coerce_value(k, v) for k, v in yaml_overrides.get("fdr", {}).items()},
)
fc_block = _replace_block(
FcConfig(),
{k: _coerce_value(k, v) for k, v in yaml_overrides.get("fc", {}).items()},
)
gcs_block = _replace_block(
GcsConfig(),
{k: _coerce_value(k, v) for k, v in yaml_overrides.get("gcs", {}).items()},
)
component_blocks = _resolve_component_blocks()
for slug, dataclass_type in _COMPONENT_REGISTRY.items():
@@ -174,5 +199,7 @@ def load_config(
runtime=runtime_block,
log=log_block,
fdr=fdr_block,
fc=fc_block,
gcs=gcs_block,
components=component_blocks,
)