mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 07:51:28 +00:00
[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:
@@ -0,0 +1,71 @@
|
||||
# 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` + `GcsAdapterError` disjoint trees, including `SourceSetSwitchNotSupportedError` ⊂ `SourceSetSwitchError` per AC-9.
|
||||
- `src/gps_denied_onboard/components/c8_fc_adapter/_covariance_projector.py` — `CovarianceProjector` helper (AZ-392).
|
||||
- `src/gps_denied_onboard/runtime_root/fc_factory.py` — `build_fc_adapter` / `build_gcs_adapter` factories, strategy registries, `bind_outbound_emit_thread` single-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's `subscribe_telemetry(callback) -> Subscription` shape; `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 (only `FcAdapter`, `GcsAdapter`, `ReplaySink`, `EmittedExternalPosition` in `__all__` per AC-8).
|
||||
- `src/gps_denied_onboard/_types/emitted.py` — `EmittedExternalPosition` reshaped to match contract (`fc_kind`, `horiz_accuracy_m`, `source_label`, `emitted_at`, `sequence_number`).
|
||||
- `src/gps_denied_onboard/_types/nav.py` — removed unused stub `FlightStateSignal` + `GpsHealth` (the contract shape lives on `_types/fc.py`; no production producer/consumer used the old shape).
|
||||
- `src/gps_denied_onboard/config/schema.py` — added `FcConfig` + `GcsConfig` frozen dataclasses with `__post_init__` validation; added `KNOWN_FC_STRATEGIES` and `KNOWN_GCS_STRATEGIES` frozensets; registered `fc` and `gcs` in `_DEFAULT_BLOCKS` and `Config`.
|
||||
- `src/gps_denied_onboard/config/loader.py` — added env-key mappings for `FC_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; wired `fc` + `gcs` block resolution in `load_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-exports `fc_factory` symbols.
|
||||
- `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__.py` path.
|
||||
|
||||
## Contract changes
|
||||
|
||||
- `_docs/02_document/contracts/shared_config/composition_root_protocol.md`: bumped to **v1.2.0** — added cross-cutting `fc` (`FcConfig`) + `gcs` (`GcsConfig`) blocks, `build_fc_adapter` / `build_gcs_adapter` factories, single-writer outbound-thread binding. Backwards-compatible: default `FcConfig()` + `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) call `register_fc_adapter("ardupilot_plane", factory)` under the matching `BUILD_FC_*` flag.
|
||||
- **Config-load gate (AC-5)** lives in `FcConfig.__post_init__` / `GcsConfig.__post_init__`. Unknown strategies raise `ConfigError` synchronously 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)**: `CovarianceProjector` uses the closed-form 2×2 eigenvalue formula instead of `numpy.linalg.eigvalsh`, so AP `m` and iNav `mm` round-trip exactly (every iNav call is the same `radius_m` then `round-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 raises `OutboundThreadAlreadyBoundError`. 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
|
||||
|
||||
1. **AZ-391** (deferred to batch 9) will add the inbound `subscribe_telemetry` body for both AP (`pymavlink`) and iNav (`YAMSPy`), with the MAVLink/MSP2 decoders and Invariant 7 out-of-order drop. Pulls `YAMSPy` as a new `pyproject.toml` dependency.
|
||||
2. **AZ-393 / AZ-394 / AZ-395 / AZ-396 / AZ-397** will register concrete factories with the new `register_fc_adapter` / `register_gcs_adapter` calls; this batch provides only the registry + factory contract.
|
||||
3. **Composition-root wiring of `take_off`** to use the new `build_fc_adapter` factory is still a forward action (carried over from batch 7's PASS_WITH_INFO finding #3).
|
||||
Reference in New Issue
Block a user