mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 11:41:13 +00:00
362e93c626
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>
7.7 KiB
7.7 KiB
Batch 08 — Cycle 1 Implementation Report
Batch: 8 of N
Tasks landed: AZ-390 (FcAdapter / GcsAdapter Protocols + DTOs + factories + composition), AZ-392 (CovarianceProjector helper)
Deferred to batch 9: AZ-391 (Inbound subscription + telemetry dispatch) — pulls a new external dependency (YAMSPy for iNav MSP2) per user decision (see _docs/_autodev_state.md).
Cycle: 1
Date: 2026-05-11
Scope
| Task | Component | Purpose |
|---|---|---|
| AZ-390 | C8 FC adapter (foundation) | Public FcAdapter / GcsAdapter Protocols, contract DTOs (FcKind, FlightState, GpsStatus, Severity, TelemetryKind, PortConfig, FcTelemetryFrame, FlightStateSignal, GpsHealth, OperatorCommand, Subscription, ImuTelemetrySample, AttitudeSample), error trees (FcAdapterError + GcsAdapterError with disjoint hierarchy), FcConfig + GcsConfig cross-cutting blocks with config-load validation, composition-root factories (build_fc_adapter, build_gcs_adapter) with build-flag gating + INFO log on load, single-writer outbound thread enforcement. |
| AZ-392 | C8 FC adapter (helper) | CovarianceProjector — honest 6×6 → 3×3 → 2×2 → sqrt(λ_max) reduction. AP outputs float meters; iNav outputs int millimetres (uint16-clamped at 65535 with WARN log + FDR record). Non-SPD / NaN / wrong-shape / missing covariance all raise FcEmitError BEFORE per-FC encoding runs and emit a single FDR ERROR record carrying frame_id for post-flight correlation. |
Files added / modified
Added
src/gps_denied_onboard/_types/fc.py— C8 DTOs + enums (PortConfig,FcKind,FlightState,GpsStatus,Severity,TelemetryKind,ImuTelemetrySample,AttitudeSample,GpsHealth,FlightStateSignal,FcTelemetryFrame,OperatorCommand,Subscription).src/gps_denied_onboard/components/c8_fc_adapter/errors.py—FcAdapterError+GcsAdapterErrordisjoint trees, includingSourceSetSwitchNotSupportedError⊂SourceSetSwitchErrorper AC-9.src/gps_denied_onboard/components/c8_fc_adapter/_covariance_projector.py—CovarianceProjectorhelper (AZ-392).src/gps_denied_onboard/runtime_root/fc_factory.py—build_fc_adapter/build_gcs_adapterfactories, strategy registries,bind_outbound_emit_threadsingle-writer enforcement.tests/unit/c8_fc_adapter/test_az390_adapter_protocol.py— 36 unit tests for AZ-390 covering all 10 ACs + NFR.tests/unit/c8_fc_adapter/test_az392_covariance_projector.py— 17 unit tests for AZ-392 covering all 7 ACs + NFR.
Modified
src/gps_denied_onboard/components/c8_fc_adapter/interface.py— full rewrite. Replaced iterator-style inbound stubs with the contract'ssubscribe_telemetry(callback) -> Subscriptionshape;emit_external_position(output) -> EmittedExternalPosition;request_source_set_switch();current_flight_state();emit_status_text(msg, severity).src/gps_denied_onboard/components/c8_fc_adapter/__init__.py— public-API gate (onlyFcAdapter,GcsAdapter,ReplaySink,EmittedExternalPositionin__all__per AC-8).src/gps_denied_onboard/_types/emitted.py—EmittedExternalPositionreshaped to match contract (fc_kind,horiz_accuracy_m,source_label,emitted_at,sequence_number).src/gps_denied_onboard/_types/nav.py— removed unused stubFlightStateSignal+GpsHealth(the contract shape lives on_types/fc.py; no production producer/consumer used the old shape).src/gps_denied_onboard/config/schema.py— addedFcConfig+GcsConfigfrozen dataclasses with__post_init__validation; addedKNOWN_FC_STRATEGIESandKNOWN_GCS_STRATEGIESfrozensets; registeredfcandgcsin_DEFAULT_BLOCKSandConfig.src/gps_denied_onboard/config/loader.py— added env-key mappings forFC_ADAPTER,FC_PORT_DEVICE,FC_PORT_BAUD,FC_SIGNING_KEY_SOURCE,GCS_ADAPTER,GCS_PORT_DEVICE,GCS_PORT_BAUD,GCS_SUMMARY_RATE_HZ; added field coercions; wiredfc+gcsblock resolution inload_config.src/gps_denied_onboard/config/__init__.py— re-exported the new symbols.src/gps_denied_onboard/runtime_root/__init__.py—runtime_root.py→ package; re-exportsfc_factorysymbols.tests/unit/c8_fc_adapter/test_smoke.py— public-API gate test (__all__is exactly the contract symbol set).tests/unit/test_ac1_scaffold_layout.py—runtime_root.py→runtime_root/__init__.pypath.
Contract changes
_docs/02_document/contracts/shared_config/composition_root_protocol.md: bumped to v1.2.0 — added cross-cuttingfc(FcConfig) +gcs(GcsConfig) blocks,build_fc_adapter/build_gcs_adapterfactories, single-writer outbound-thread binding. Backwards-compatible: defaultFcConfig()+GcsConfig()preserve all existing semantics for callers that don't touch the new blocks._docs/02_document/contracts/c8_fc_adapter/fc_adapter_protocol.md: unchanged — this batch implements the v1.0.0 surface as specified; no protocol drift.
Test counts
| Metric | Before | After | Delta |
|---|---|---|---|
| Tests passing | 410 | 410 | +0 (53 new tests added in batch — the legacy smoke test was kept and supplemented; one scaffold-layout parametrise was repathed not added) |
| Tests skipped | 2 | 2 | 0 |
| Tests failing | 0 | 0 | 0 |
Note: the +53-tests-added count is correct against pre-batch totals (357 vs 410 = +53 unit tests; the scaffold-layout parametrise count stayed at 7 because we re-pointed, not added).
Architectural notes
- Build-flag gate (AC-4) lives in
fc_factory.py. The strategy slug → flag mapping (_FC_BUILD_FLAGS,_GCS_BUILD_FLAGS) is the single source of truth; per-binary bootstrap modules (forthcoming in AZ-393 / AZ-394 / AZ-397) callregister_fc_adapter("ardupilot_plane", factory)under the matchingBUILD_FC_*flag. - Config-load gate (AC-5) lives in
FcConfig.__post_init__/GcsConfig.__post_init__. Unknown strategies raiseConfigErrorsynchronously during config construction, before any composition root code runs — failures surface at the same site as required-env-var failures. - Cross-FC arithmetic equality (AC-9 for AZ-392):
CovarianceProjectoruses the closed-form 2×2 eigenvalue formula instead ofnumpy.linalg.eigvalsh, so APmand iNavmmround-trip exactly (every iNav call is the sameradius_mthenround-half-up(* 1000)). - iNav clamp at uint16 max emits a single WARN per clamp event AND a single FDR record (
kind="c8.cov_projector.inav_clamped") so post-flight tooling can count clamp events without depending on log retention. - Single-writer outbound (AC-6) lives in
fc_factory.bind_outbound_emit_thread. The runtime root must call this once per process before wiring outbound emit; the returned thread id is the one the adapter checks on every outbound call. Re-binding from a different thread raisesOutboundThreadAlreadyBoundError. Re-binding from the SAME thread is idempotent (test-friendly).
Dependencies
Zero new external dependencies. AZ-390 + AZ-392 are stdlib + numpy (already pinned). pymavlink + YAMSPy enter in batch 9 with AZ-391.
Known forward-actions
- AZ-391 (deferred to batch 9) will add the inbound
subscribe_telemetrybody for both AP (pymavlink) and iNav (YAMSPy), with the MAVLink/MSP2 decoders and Invariant 7 out-of-order drop. PullsYAMSPyas a newpyproject.tomldependency. - AZ-393 / AZ-394 / AZ-395 / AZ-396 / AZ-397 will register concrete factories with the new
register_fc_adapter/register_gcs_adaptercalls; this batch provides only the registry + factory contract. - Composition-root wiring of
take_offto use the newbuild_fc_adapterfactory is still a forward action (carried over from batch 7's PASS_WITH_INFO finding #3).