--- phase: "01" plan: "06" subsystem: mavlink_io tags: [refactor, hexagonal, mavlink, shim] dependency_graph: requires: [01-05] provides: [components/mavlink_io] affects: [core/mavlink.py] tech_stack: added: [] patterns: [shim-re-export, circular-import-avoidance-via-local-imports] key_files: created: - src/gps_denied/components/mavlink_io/pymavlink_bridge.py - src/gps_denied/components/mavlink_io/mock_mavlink.py modified: - src/gps_denied/components/mavlink_io/__init__.py - src/gps_denied/core/mavlink.py decisions: - "Used local (deferred) imports of MockMAVConnection inside MAVLinkBridge methods to avoid circular import between pymavlink_bridge and mock_mavlink" - "Shim re-exports all seven names including three private helpers (_confidence_to_fix_type, _eskf_to_gps_input, _unix_to_gps_time) per CRITICAL warning" metrics: duration: "~5 minutes" completed: "2026-05-11" tasks: 5 files: 4 --- # Phase 01 Plan 06: MAVLink I/O Split Summary Split `core/mavlink.py` (483 LOC) into component modules under `components/mavlink_io/`, replacing the original with a shim that re-exports all public and private names verbatim. ## LOC Distribution | File | LOC | Contents | |------|-----|----------| | `components/mavlink_io/pymavlink_bridge.py` | 455 | MAVLinkBridge class + 3 module-level helpers + pymavlink conditional import | | `components/mavlink_io/mock_mavlink.py` | 30 | MockMAVConnection (no-op for dev/CI) | | `core/mavlink.py` (shim) | 29 | Re-export shim only | | `components/mavlink_io/__init__.py` | 20 | Package surface | Original: 483 LOC → split across 4 files (514 total including shim and `__init__.py`). ## Test Counts | Test file | Result | |-----------|--------| | `tests/test_mavlink.py` | 32 passed | | `tests/test_gps_input_encoding.py` | included in the 32 above | | Full regression (`tests/`) | 216 passed, 8 skipped | Regression floor: 216 passed (baseline). Status: MAINTAINED. ## Private Helper Verification The three underscore names required by `test_mavlink.py` and `test_gps_input_encoding.py` resolve correctly via the shim: ``` python -c "from gps_denied.core.mavlink import _confidence_to_fix_type, _eskf_to_gps_input, _unix_to_gps_time; print('private helpers ok')" # Output: private helpers ok ``` All three are exported from `core/mavlink.py` → `components/mavlink_io/pymavlink_bridge.py` → callable. ## `_PYMAVLINK_AVAILABLE` on Dev Machine ``` _PYMAVLINK_AVAILABLE = True ``` pymavlink is installed on this development machine, so `MAVLinkBridge._open_connection()` will attempt a real connection before falling back to `MockMAVConnection`. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 3 - Blocking] Circular import between pymavlink_bridge and mock_mavlink** - **Found during:** Task 1 (design review before writing) - **Issue:** `pymavlink_bridge.py` (MAVLinkBridge) uses `isinstance(self._conn, MockMAVConnection)` checks. If `mock_mavlink.py` were imported at module level in `pymavlink_bridge.py`, and `__init__.py` imports both, a circular import chain would form. - **Fix:** Used local (deferred) imports of `MockMAVConnection` inside each method that references it (`_send_gps_input`, `_recv_imu`, `_send_reloc_request`, `_send_telemetry`, `_open_connection`). This matches the pattern used in similar hexagonal refactors in this codebase. - **Files modified:** `pymavlink_bridge.py` - **Commit:** f965ac7 ## Known Stubs None. All data paths are wired: bridge reads from live ESKFState, writes to real or mock MAVLink connection. ## Threat Flags None. No new network endpoints or auth paths introduced. The MAVLink UART connection was already present in the original `core/mavlink.py`. ## Self-Check: PASSED - `src/gps_denied/components/mavlink_io/pymavlink_bridge.py` — FOUND - `src/gps_denied/components/mavlink_io/mock_mavlink.py` — FOUND - `src/gps_denied/components/mavlink_io/__init__.py` — FOUND (updated) - `src/gps_denied/core/mavlink.py` — FOUND (shim, 29 LOC) - Commit f965ac7 — FOUND - 216 tests passed — CONFIRMED