mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 23:11:12 +00:00
[AZ-393] [AZ-394] [AZ-395] C8 outbound chain + AP MAVLink2 signing
AZ-393 ArduPilot outbound: PymavlinkArdupilotAdapter encodes EstimatorOutput to MAVLink2 GPS_INPUT via gps_input_send; emits NAMED_VALUE_FLOAT(name="src_lbl") every frame and STATUSTEXT on source_label transition (1 Hz per-severity cap). Smoothed-output guard (Invariant 6), single-writer thread (Invariant 8), SPD propagation. Shared helper _outbound_provenance.py owns the canonical source-label-to-float table + transition rate-limiter. AZ-394 iNav outbound: Msp2InavAdapter encodes EstimatorOutput to hand-rolled MSP2_SENSOR_GPS (0x1F03, 52-byte LE payload via _msp2_sensor_gps_encoder.py + YAMSPy send_RAW_msg). Secondary unsigned MAVLink channel for STATUSTEXT transitions. open() rejects non-None signing_key (RESTRICT-COMM-2 / Invariant 2); request_source_set_switch raises SourceSetSwitchNotSupportedError (Invariant 9 verified: never calls setup_signing on secondary). AZ-395 AP MAVLink2 signing: ephemeral per-flight 32-byte key from secrets.token_bytes; pymavlink setup_signing handshake at open(); in-place bytearray zeroisation on close(); mid-flight signing-failure detection (ERROR log + WARNING STATUSTEXT + no raise; threshold configurable). Key never logged / persisted / serialised (regex-scanned by AC-4/AC-5). BUILD_DEV_STATIC_KEY=ON enables repeatable static-key dev path; rejected at open() when the build flag is absent. Shared: EstimatorOutput.smoothed (default False) added for the Invariant 6 gate at the C8 boundary; FcConfig extended with dev_static_signing_key + signing_failure_threshold (additive defaults; cross-field validation in __post_init__). Tests: 33 new AC tests (11 + 11 + 11) covering all 30 ACs; full suite 476 passing / 2 skipped / 0 failing (was 443). Contract surfaces unchanged at fc_adapter_protocol v1.0.0 and composition_root v1.2.0. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -56,6 +56,8 @@ ENV_KEY_MAP: Final[dict[str, tuple[str, str]]] = {
|
||||
"FC_PORT_DEVICE": ("fc", "port_device"),
|
||||
"FC_PORT_BAUD": ("fc", "port_baud"),
|
||||
"FC_SIGNING_KEY_SOURCE": ("fc", "signing_key_source"),
|
||||
"FC_DEV_STATIC_SIGNING_KEY": ("fc", "dev_static_signing_key"),
|
||||
"FC_SIGNING_FAILURE_THRESHOLD": ("fc", "signing_failure_threshold"),
|
||||
"GCS_ADAPTER": ("gcs", "adapter"),
|
||||
"GCS_PORT_DEVICE": ("gcs", "port_device"),
|
||||
"GCS_PORT_BAUD": ("gcs", "port_baud"),
|
||||
@@ -97,6 +99,8 @@ _FIELD_COERCIONS: Final[dict[str, type]] = {
|
||||
"port_device": str,
|
||||
"port_baud": int,
|
||||
"signing_key_source": str,
|
||||
"dev_static_signing_key": str,
|
||||
"signing_failure_threshold": int,
|
||||
"summary_rate_hz": float,
|
||||
}
|
||||
|
||||
|
||||
@@ -207,22 +207,37 @@ class FcConfig:
|
||||
port_device: str = "/dev/ttyTHS1"
|
||||
port_baud: int = 921600
|
||||
signing_key_source: str = "ephemeral_per_flight"
|
||||
dev_static_signing_key: str = ""
|
||||
signing_failure_threshold: int = 3
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.adapter not in KNOWN_FC_STRATEGIES:
|
||||
raise ConfigError(
|
||||
f"FcConfig.adapter={self.adapter!r} not in {sorted(KNOWN_FC_STRATEGIES)}"
|
||||
)
|
||||
if self.signing_key_source not in {"none", "ephemeral_per_flight"}:
|
||||
if self.signing_key_source not in {
|
||||
"none",
|
||||
"ephemeral_per_flight",
|
||||
"dev_static",
|
||||
}:
|
||||
raise ConfigError(
|
||||
f"FcConfig.signing_key_source={self.signing_key_source!r} not in "
|
||||
f"['none', 'ephemeral_per_flight']"
|
||||
f"['none', 'ephemeral_per_flight', 'dev_static']"
|
||||
)
|
||||
if self.adapter == "inav" and self.signing_key_source != "none":
|
||||
raise ConfigError(
|
||||
"FcConfig.signing_key_source must be 'none' when adapter='inav' "
|
||||
"(RESTRICT-COMM-2 — iNav has no signing)"
|
||||
)
|
||||
if self.signing_key_source == "dev_static" and not self.dev_static_signing_key:
|
||||
raise ConfigError(
|
||||
"FcConfig.dev_static_signing_key required when signing_key_source='dev_static'"
|
||||
)
|
||||
if self.signing_failure_threshold < 1:
|
||||
raise ConfigError(
|
||||
"FcConfig.signing_failure_threshold must be >= 1; got "
|
||||
f"{self.signing_failure_threshold}"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
Reference in New Issue
Block a user