mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 07:41:12 +00:00
Update autodev state, architecture documentation, and glossary terms
Transitioned the autodev state to phase 21, reflecting the completion of Step 5 and the drafting of Step 6 epics. Revised the architecture documentation to clarify the roles of the Tile Manager and its components, ensuring accurate representation of the system's operational flow. Updated glossary entries for Flight State and Operator to incorporate recent changes and enhance clarity on component interactions and responsibilities.
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
# C8 — Flight-Controller Adapter
|
||||
|
||||
## 1. High-Level Overview
|
||||
|
||||
**Purpose**: own the per-FC inbound + outbound communication. Inbound: subscribe to FC IMU/attitude/GPS-health/MAV_STATE telemetry; publish `ImuWindow` / `AttitudeWindow` / `GpsHealth` / `FlightStateSignal` for upstream consumers. Outbound: encode `EstimatorOutput` into the per-FC external-position contract (`GPS_INPUT` for ArduPilot Plane, `MSP2_SENSOR_GPS` for iNav) at 5 Hz periodic with honest 6×6 → 2×2 covariance projection. Owns MAVLink 2.0 message signing on the AP wired channel (D-C8-9 = (d)) and the per-flight key rotation. Also feeds the GCS link with downsampled telemetry (1–2 Hz per AC-6.1).
|
||||
|
||||
**Architectural Pattern**: Strategy — `FcAdapter` interface with two concrete implementations: `PymavlinkArdupilotAdapter`, `Msp2InavAdapter`. Plus a `GcsAdapter` (single concrete `QgcTelemetryAdapter` today). All selected at startup by config (ADR-001), build-time gating per `BUILD_*` flags (ADR-002, both adapters typically linked into the deployment binary so a single image can target both FCs by configuration), composition-root wired (ADR-009).
|
||||
|
||||
**Upstream dependencies**:
|
||||
- C5 StateEstimator → `EstimatorOutput` (5 Hz periodic emit driver).
|
||||
- Hardware: UART/USB to FC; UART (or USB) to GCS (often shared or via FC mavlink-routing).
|
||||
|
||||
**Downstream consumers**:
|
||||
- C5 StateEstimator (consumes `ImuWindow`, `AttitudeWindow`, `GpsHealth`, `FlightStateSignal`).
|
||||
- C1 VIO (consumes `ImuWindow`).
|
||||
- C13 FDR (consumes raw inbound + emitted outbound MAVLink/MSP2 streams; signing key rotation events; spoof-promotion events).
|
||||
- C11 `TileUploader` (consumes `FlightStateSignal == ON_GROUND` confirmation; runs on a different process / image, so the signal flows out-of-band via the FDR or a small bus the operator tool subscribes to post-flight). The C11 `TileDownloader` does NOT depend on `FlightStateSignal` — it runs pre-flight when the companion is plugged into the operator workstation.
|
||||
|
||||
## 2. Internal Interfaces
|
||||
|
||||
### Interface: `FcAdapter`
|
||||
|
||||
| Method | Input | Output | Async | Error Types |
|
||||
|--------|-------|--------|-------|-------------|
|
||||
| `open` | `port_config: PortConfig, signing_key: bytes (AP only)` | `None` | No | `FcOpenError`, `SigningHandshakeError` |
|
||||
| `subscribe_telemetry` | `callback: Callable[[FcTelemetryFrame], None]` | `Subscription` | No | — |
|
||||
| `emit_external_position` | `EstimatorOutput` | `None` | No | `FcEmitError` |
|
||||
| `emit_status_text` | `string, severity: enum {INFO, WARN, ERROR}` | `None` | No | `FcEmitError` |
|
||||
| `request_source_set_switch` (AP only) | `()` | `None` | No | `SourceSetSwitchError` |
|
||||
| `current_flight_state` | `()` | `FlightStateSignal` | No | — |
|
||||
|
||||
### Interface: `GcsAdapter`
|
||||
|
||||
| Method | Input | Output | Async | Error Types |
|
||||
|--------|-------|--------|-------|-------------|
|
||||
| `emit_summary` | `EstimatorOutput` (downsampled to 1–2 Hz) | `None` | No | `GcsEmitError` |
|
||||
| `subscribe_operator_commands` | `callback` | `Subscription` | No | — |
|
||||
|
||||
**Input/Output DTOs**:
|
||||
```
|
||||
PortConfig:
|
||||
device: string (e.g. /dev/ttyTHS1)
|
||||
baud: int
|
||||
fc_kind: enum {ardupilot_plane, inav}
|
||||
|
||||
FcTelemetryFrame:
|
||||
kind: enum {imu_sample, attitude, gps_health, mav_state}
|
||||
payload: union per kind
|
||||
received_at: monotonic_ns
|
||||
signed: bool — true only for AP signed frames
|
||||
|
||||
EmittedExternalPosition:
|
||||
per_fc_payload: enum (GPS_INPUT for AP / MSP2_SENSOR_GPS for iNav)
|
||||
horiz_accuracy_m: float (AP) — projection of covariance_6x6 → 2x2 → equivalent_radius
|
||||
hPosAccuracy_mm: int (iNav) — same source, mm units (D-C8-8 = (b))
|
||||
source_label: STATUSTEXT / NAMED_VALUE_FLOAT (out-of-band)
|
||||
```
|
||||
|
||||
## 3. External API Specification
|
||||
|
||||
C8 exposes no HTTP/gRPC API. Its **external** surfaces are:
|
||||
|
||||
- **MAVLink 2.0 messages** to/from ArduPilot Plane, signed (D-C8-9 = (d)). Specifically:
|
||||
- In: `RAW_IMU` / `SCALED_IMU2`, `ATTITUDE`, `GPS_RAW_INT`, `GPS2_RAW`, `HEARTBEAT`, `STATUSTEXT`.
|
||||
- Out: `GPS_INPUT` (5 Hz), `STATUSTEXT`, `NAMED_VALUE_FLOAT` (provenance label), `COMMAND_LONG MAV_CMD_SET_EKF_SOURCE_SET` (D-C8-2 source-set switch on F7).
|
||||
- **MSP2 messages** to/from iNav over UART (unsigned, accepted residual risk):
|
||||
- In: `MSP2_INAV_ANALOG`, attitude+IMU stream.
|
||||
- Out: `MSP2_SENSOR_GPS` (5 Hz). MAVLink outbound for telemetry only (iNav speaks both).
|
||||
- **MAVLink 2.0 messages** to/from QGroundControl on the GCS link (1–2 Hz downsampled summary out, operator commands in).
|
||||
|
||||
Refer to `architecture.md` § 5 External Integrations for the per-message rate/auth/failure-mode table; this spec inherits those values.
|
||||
|
||||
## 4. Data Access Patterns
|
||||
|
||||
C8 holds no persistent storage. All communication is in-memory streaming.
|
||||
|
||||
### Caching Strategy
|
||||
|
||||
| Data | Cache Type | TTL | Invalidation |
|
||||
|------|-----------|-----|-------------|
|
||||
| Per-flight signing key (AP) | in-memory, generated at takeoff | flight lifetime | new key on next takeoff |
|
||||
| Last-emitted external position (for downsampling 5 Hz → 1–2 Hz GCS) | in-memory ring | flight lifetime | n/a |
|
||||
| FC telemetry rolling window (for `ImuWindow`, `AttitudeWindow` consumers) | in-memory bounded ring | bounded by C1/C5 consumption | drop-oldest |
|
||||
|
||||
## 5. Implementation Details
|
||||
|
||||
**Algorithmic Complexity**: per-message encode/decode is `O(message_size)`; the per-frame work is dominated by the covariance projection (3×3 sub-matrix → equivalent_radius), which is constant-time.
|
||||
|
||||
**State Management**:
|
||||
- AP path: holds the MAVLink 2.0 signing state (key, key generation timestamp, signature counter).
|
||||
- iNav path: holds the MSP2 channel state (sequence numbers).
|
||||
- Both paths: hold the bounded telemetry rings and a backpressure budget for the 5 Hz outbound emit.
|
||||
|
||||
**Key Dependencies**:
|
||||
|
||||
| Library | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| pymavlink | bundled unmodified per D-C8-3 | AP MAVLink 2.0 + signing |
|
||||
| YAMSPy | per project pin | iNav MSP2 |
|
||||
| INAV-Toolkit | per project pin | iNav MSP2 message definitions for `MSP2_SENSOR_GPS` |
|
||||
| pyserial | per project pin | UART transport |
|
||||
|
||||
**Error Handling Strategy**:
|
||||
- `FcOpenError` at takeoff: refuse takeoff with explicit error.
|
||||
- `SigningHandshakeError` at takeoff (AP): refuse takeoff. Mid-flight signing failure: FC ignores unsigned messages, AC-5.2 takes over (3 s no estimate → FC IMU-only). Companion logs the rotation/expiry event to FDR.
|
||||
- `FcEmitError` mid-flight: log + continue; the FC's own staleness detection will eventually trigger AC-5.2.
|
||||
- `SourceSetSwitchError` (AP only, F7): try once; on failure log + STATUSTEXT to GCS; the system continues to emit `GPS_INPUT` and the operator can manually switch via RC aux per D-C8-2-FALLBACK.
|
||||
- iNav unsigned: NOT an error — accepted residual risk per Mode B Source #129. Documented in `tests/security-tests.md` NFT-SEC-03.
|
||||
|
||||
**Covariance projection** (D-C8-8 = (b)):
|
||||
- Project 6×6 GTSAM `Marginals` covariance → 3×3 position sub-matrix → 2×2 horizontal sub-matrix → `equivalent_radius` per per-FC contract:
|
||||
- AP `horiz_accuracy` (m): `sqrt(0.5 * (sigma_xx + sigma_yy + sqrt((sigma_xx - sigma_yy)^2 + 4*sigma_xy^2)))` (largest eigenvalue of 2×2; documented as the "honest covariance projection" in IT-10).
|
||||
- iNav `hPosAccuracy` (mm): same value, converted to mm.
|
||||
|
||||
## 6. Extensions and Helpers
|
||||
|
||||
| Helper | Purpose | Used By |
|
||||
|--------|---------|---------|
|
||||
| `WgsConverter` | shared with C4, C5, C6 | C4, C5, C6, C8 |
|
||||
| `CovarianceProjector` | 6×6 → 2×2 → equivalent_radius — keep inside C8 (per-FC unit conversion is variant-specific per coderule SRP) | C8 only |
|
||||
|
||||
## 7. Caveats & Edge Cases
|
||||
|
||||
**Known limitations**:
|
||||
- iNav has no signing equivalent; accepted residual risk per Mode B Source #129. Plan-phase carryforward proposes an iNav firmware feature request — out of scope for this cycle.
|
||||
- D-C8-2 source-set switch is firmware-supported but not deployed-precedent; ADR-008 makes IT-3 (ArduPilot SITL validation) the lock gate.
|
||||
|
||||
**Potential race conditions**:
|
||||
- The 5 Hz outbound emit thread MUST not block on the 100–200 Hz inbound telemetry decode thread. Bounded rings + drop-oldest semantics on the inbound side; outbound-emit timer is independent.
|
||||
- The AP signing key rotation MUST happen between flights, not mid-flight. The key generation is at takeoff load (F2).
|
||||
|
||||
**Performance bottlenecks**:
|
||||
- pymavlink's signing handshake is sub-second on a wired UART/USB link. F2 budget is generous.
|
||||
|
||||
## 8. Dependency Graph
|
||||
|
||||
**Must be implemented after**: hardware UART config (operator workstation pre-flight stage).
|
||||
|
||||
**Can be implemented in parallel with**: C6, C13.
|
||||
|
||||
**Blocks**: C1 (FC IMU prior), C5 (factor inputs), F2 / F3 / F5 / F7 / F8 / F9 / F10.
|
||||
|
||||
## 9. Logging Strategy
|
||||
|
||||
| Log Level | When | Example |
|
||||
|-----------|------|---------|
|
||||
| ERROR | `FcOpenError`, `SigningHandshakeError`, persistent `FcEmitError` | `C8 AP signing handshake failed; refusing takeoff` |
|
||||
| WARN | one-off `FcEmitError`; spoofing-promotion blocked event surfaced via STATUSTEXT | `C8 GPS_INPUT emit dropped; reason=uart_busy; will retry next tick` |
|
||||
| INFO | adapter ready; key rotation event; F7 source-set switch executed | `C8 AP signing key rotated; flight_id=…; key_age_s=0` |
|
||||
| DEBUG | per-emit timing | `C8 GPS_INPUT emit frame=12345 took=2.1ms; horiz_accuracy_m=4.2` |
|
||||
|
||||
**Log format**: structured JSON.
|
||||
**Log storage**: stdout / journald / FDR via C13 (ERROR + WARN always; AP signing key rotation events ALWAYS to FDR; F7 source-set switch events ALWAYS to FDR + GCS STATUSTEXT; STATUSTEXT broadcasts ALWAYS mirrored to FDR).
|
||||
@@ -0,0 +1,224 @@
|
||||
# Test Specification — C8 FC + GCS Adapter
|
||||
|
||||
Component-scoped. Suite-level coverage in `_docs/02_document/tests/*.md`.
|
||||
|
||||
## Acceptance Criteria Traceability
|
||||
|
||||
| AC ID | Acceptance Criterion (one-line) | Test IDs | Coverage |
|
||||
|-------|---------------------------------|----------|----------|
|
||||
| AC-4.3 | FC output: GPS_INPUT (AP) + MSP2_SENSOR_GPS (iNav) with honest covariance | FT-P-03, FT-P-09-AP, FT-P-09-iNav, **C8-IT-01** | Covered |
|
||||
| AC-4.4 | Estimates streamed frame-by-frame | NFT-PERF-02, **C8-IT-02** | Covered |
|
||||
| AC-5.1 | Init from FC EKF's last valid GPS + IMU-extrapolated | FT-P-11, **C8-IT-03** | Covered |
|
||||
| AC-5.2 | On >3 s without estimate, FC IMU-only fallback; SUT logs | NFT-RES-01 | Covered (C8 emission path) |
|
||||
| AC-6.1 | GCS stream at 1–2 Hz | FT-P-12, **C8-IT-04** | Covered |
|
||||
| AC-6.2 | GCS may send commands via standard MAVLink | FT-P-13, **C8-IT-05** | Covered |
|
||||
| AC-6.3 | WGS84 output | FT-P-14, **C8-IT-06** | Covered |
|
||||
| AC-NEW-2 | Spoofing-promotion latency <3 s p95 (source-set switch) | NFT-PERF-04, **C8-IT-07** | Covered |
|
||||
| RESTRICT-COMM-2 | iNav has no MAVLink signing; documented asymmetry | NFT-SEC-03, **C8-IT-08** | Covered |
|
||||
| D-C8-9 = (d) | MAVLink 2.0 per-flight signing on AP | IT-3, **C8-ST-01** | Open (gated by IT-3 SITL pass) |
|
||||
|
||||
---
|
||||
|
||||
## Component-Internal Tests
|
||||
|
||||
### C8-IT-01: per-FC encoder produces honest 6×6 → 2×2 covariance projection
|
||||
|
||||
**Summary**: `EstimatorOutput.covariance_6x6`'s position 3×3 block projects to the FC's 2×2 horizontal covariance with the same Frobenius norm scale (no fake-confidence regression).
|
||||
|
||||
**Traces to**: AC-4.3
|
||||
|
||||
**Description**: 100 emitted `EstimatorOutput` records with varying covariances; capture the encoded `GPS_INPUT` bytes; decode via pymavlink; assert the FC-side 2×2 covariance Frobenius norm equals the source 3×3 horizontal-block norm to within 1% (small numerical tolerance for unit conversion). Same assertion against `MSP2_SENSOR_GPS` for the iNav adapter.
|
||||
|
||||
**Input data**: scripted `EstimatorOutput` sequence with controlled covariances.
|
||||
|
||||
**Expected result**: norm equality within 1% for both adapters.
|
||||
|
||||
**Max execution time**: 30 s.
|
||||
|
||||
---
|
||||
|
||||
### C8-IT-02: 5 Hz periodic emission stays within ±5% of nominal interval
|
||||
|
||||
**Summary**: `emit_external_position` runs at exactly 5 Hz with jitter ≤ ±5%.
|
||||
|
||||
**Traces to**: AC-4.4
|
||||
|
||||
**Description**: drive the emit path for 60 s; record emission timestamps; compute inter-emission interval distribution; assert mean = 200 ms ± 1 ms; std-dev ≤ 10 ms; max gap ≤ 220 ms.
|
||||
|
||||
**Input data**: scripted `EstimatorOutput` source at 5 Hz.
|
||||
|
||||
**Expected result**: jitter within bound.
|
||||
|
||||
**Max execution time**: 90 s.
|
||||
|
||||
---
|
||||
|
||||
### C8-IT-03: warm-start GPS from FC EKF feeds C5
|
||||
|
||||
**Summary**: at takeoff, the FC EKF's last-valid-GPS is read by C8 and surfaced via `current_flight_state` for C5's warm-start.
|
||||
|
||||
**Traces to**: AC-5.1
|
||||
|
||||
**Description**: stage a SITL with a known last-valid-GPS; bring up C8; call `current_flight_state`; assert `FlightStateSignal` includes the EKF's pose hint with timestamp ≤ 1 s old.
|
||||
|
||||
**Input data**: ArduPilot SITL with scripted EKF state.
|
||||
|
||||
**Expected result**: warm-start hint exposed within 1 s of C8 ready.
|
||||
|
||||
**Max execution time**: 60 s.
|
||||
|
||||
---
|
||||
|
||||
### C8-IT-04: 1–2 Hz GCS downsampling
|
||||
|
||||
**Summary**: GCS stream is downsampled from C5's 5 Hz to 1–2 Hz per AC-6.1.
|
||||
|
||||
**Traces to**: AC-6.1
|
||||
|
||||
**Description**: drive 5 Hz `EstimatorOutput`; capture GCS-side stream; assert rate is between 1 and 2 Hz with consistent downsample factor.
|
||||
|
||||
**Input data**: scripted source.
|
||||
|
||||
**Expected result**: 1–2 Hz GCS stream.
|
||||
|
||||
**Max execution time**: 60 s.
|
||||
|
||||
---
|
||||
|
||||
### C8-IT-05: GCS commands are accepted and routed
|
||||
|
||||
**Summary**: standard MAVLink commands sent from QGC reach C8 and the system handles them per AC-6.2 (e.g., parameter read, mode set).
|
||||
|
||||
**Traces to**: AC-6.2
|
||||
|
||||
**Description**: connect QGC SITL to the C8 GCS adapter; send a parameter-read; assert the response is well-formed and the parameter values match the running config.
|
||||
|
||||
**Input data**: QGC SITL + C8 adapter.
|
||||
|
||||
**Expected result**: parameter read returns values.
|
||||
|
||||
**Max execution time**: 60 s.
|
||||
|
||||
---
|
||||
|
||||
### C8-IT-06: WGS84 conversion round-trip identity
|
||||
|
||||
**Summary**: `WgsConverter` (helper) round-trips a local-tangent-plane → WGS84 → local-tangent-plane within 1 cm position residual.
|
||||
|
||||
**Traces to**: AC-6.3 (defensive — backstops the helper that C4 + C8 both rely on)
|
||||
|
||||
**Description**: 1000 random local-tangent-plane points; round-trip through the helper; assert max residual < 1 cm position; max angular residual < 0.001°.
|
||||
|
||||
**Input data**: scripted random points within ±10 km of an origin.
|
||||
|
||||
**Expected result**: round-trip residual within bound.
|
||||
|
||||
**Max execution time**: 5 s.
|
||||
|
||||
---
|
||||
|
||||
### C8-IT-07: source-set switch latency under spoof event
|
||||
|
||||
**Summary**: when the spoof gate clears, `request_source_set_switch` fires the AP `MAV_CMD_SET_EKF_SOURCE_SET` within the AC-NEW-2 3 s budget.
|
||||
|
||||
**Traces to**: AC-NEW-2
|
||||
|
||||
**Description**: scripted scenario from C5-IT-06 (spoof recovery); assert C8 issues the `MAV_CMD_SET_EKF_SOURCE_SET` within 3 s of the gate-clear event; assert SITL acknowledges the command.
|
||||
|
||||
**Input data**: ArduPilot SITL + scripted gate-clear.
|
||||
|
||||
**Expected result**: command issued + ACK within 3 s.
|
||||
|
||||
**Max execution time**: 60 s.
|
||||
|
||||
---
|
||||
|
||||
### C8-IT-08: iNav signing-asymmetry assertion
|
||||
|
||||
**Summary**: the iNav adapter never attempts MAVLink signing (iNav doesn't support it per RESTRICT-COMM-2); the AP adapter always does.
|
||||
|
||||
**Traces to**: RESTRICT-COMM-2
|
||||
|
||||
**Description**: bring up the iNav adapter; assert no signing handshake is attempted (capture wire bytes, check no MAVLink2 signed-flag set). Bring up AP adapter; assert signing handshake completes.
|
||||
|
||||
**Input data**: iNav SITL + AP SITL.
|
||||
|
||||
**Expected result**: per the assertion above.
|
||||
|
||||
**Max execution time**: 60 s.
|
||||
|
||||
---
|
||||
|
||||
## Performance Tests
|
||||
|
||||
### C8-PT-01: emission latency under load
|
||||
|
||||
**Traces to**: AC-4.4 / AC-4.1 (emission portion)
|
||||
|
||||
**Load scenario**: 5 Hz emit + 200 Hz inbound IMU subscribe + GCS at 2 Hz; 10 min replay.
|
||||
|
||||
**Expected results**:
|
||||
|
||||
| Metric | Target | Failure Threshold |
|
||||
|--------|--------|-------------------|
|
||||
| `emit_external_position` p95 | ≤ 5 ms | 15 ms |
|
||||
| Inbound IMU callback p95 | ≤ 1 ms | 5 ms |
|
||||
|
||||
---
|
||||
|
||||
## Security Tests
|
||||
|
||||
### C8-ST-01: MAVLink 2.0 per-flight signing handshake (D-C8-9 = (d), gated by IT-3)
|
||||
|
||||
**Summary**: at takeoff, C8 (AP adapter) generates a per-flight ephemeral key and completes the MAVLink 2.0 signing handshake with ArduPilot SITL.
|
||||
|
||||
**Traces to**: D-C8-9 = (d) (R03 risk)
|
||||
|
||||
**Test procedure**:
|
||||
1. Bring up SITL with no pre-shared key.
|
||||
2. C8 generates a fresh per-flight key, completes handshake.
|
||||
3. Send 100 signed messages; assert SITL accepts them.
|
||||
4. Replay one of those messages with a tampered signature; assert SITL rejects it.
|
||||
|
||||
**Pass criteria**: handshake succeeds; tampered messages rejected.
|
||||
**Fail criteria**: handshake fails (escalate to D-C8-2-FALLBACK per ADR-008) OR tampered messages accepted.
|
||||
|
||||
**Status**: gated by IT-3 SITL pass; this test IS the gate.
|
||||
|
||||
---
|
||||
|
||||
### C8-ST-02: signing key never persists across flights
|
||||
|
||||
**Summary**: per-flight ephemeral key is destroyed in memory at landing; never written to disk.
|
||||
|
||||
**Traces to**: defensive (D-C8-9 = (d))
|
||||
|
||||
**Test procedure**:
|
||||
1. Run a flight; capture the key in memory at takeoff via test harness.
|
||||
2. Trigger landing.
|
||||
3. Inspect process memory + disk for any residue of the key.
|
||||
4. Assert no on-disk persistence; in-memory residue cleared by the configured zeroisation routine.
|
||||
|
||||
**Pass criteria**: no on-disk + memory zeroised.
|
||||
**Fail criteria**: any persistence.
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Tests
|
||||
|
||||
Covered transitively via FT-P-09-AP / FT-P-09-iNav / FT-P-12 / FT-P-13 / FT-P-14.
|
||||
|
||||
---
|
||||
|
||||
## Test Data Management
|
||||
|
||||
| Data Set | Source | Size |
|
||||
|----------|--------|------|
|
||||
| ArduPilot SITL fixture (Plane) | upstream SITL Docker | image |
|
||||
| iNav SITL fixture | upstream SITL Docker | image |
|
||||
| QGC SITL | upstream Docker | image |
|
||||
| Scripted `EstimatorOutput` sequences | scripted | <5 MB each |
|
||||
|
||||
**Setup**: SITL containers must be running; C8 connects via simulated UART.
|
||||
**Teardown**: stop containers; clean ephemeral keys.
|
||||
**Data isolation**: per-test SITL container.
|
||||
Reference in New Issue
Block a user