mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 22:11:12 +00:00
823c0f1b2e
Ship the two Layer-1 cross-cutting Protocols replay mode needs to leave production C1-C5 components mode-agnostic (Invariant 1) and replay- deterministic (Invariant 2). Live + replay binaries see the same interfaces; only the strategy differs. * Clock Protocol (monotonic_ns / time_ns / sleep_until_ns) + WallClock (live + REALTIME replay) + TlogDerivedClock (ASAP replay; advance-on-call; non-monotonic source → ClockOrderingError). * FrameSource Protocol (next_frame -> NavCameraFrame | None / close) + LiveCameraFrameSource (cv2.VideoCapture device index) + VideoFileFrameSource (cv2.VideoCapture file). * Build-flag gating: BUILD_VIDEO_FILE_FRAME_SOURCE, BUILD_LIVE_CAMERA_FRAME_SOURCE (constructor-time check; Tier-0 OFF refuses construction with FrameSourceConfigError). * Composition-root factories: build_clock + build_frame_source. * Injected Clock across every component that previously called time.monotonic_ns() / time.sleep() directly: c5_state (estimator, ESKF, fallback watcher, source-label SM, isam2 handle), c8_fc_adapter (inbound MAVLink + MSP2, AP outbound, iNav outbound, QGC GCS), c13_fdr writer, c12_operator_tooling httpx flights client. All constructors default to WallClock() so existing call sites keep live-binary behaviour without a wiring change. * AC-4 CI guard (tests/_meta/test_no_direct_time_in_components.py) AST-scans components/**/*.py for direct time.monotonic_ns / time.time_ns / time.sleep references and fails loudly with file:line. * Conformance + factory tests: tests/unit/clock + tests/unit/frame_source. * Test fixture updates: FallbackWatcher / SourceLabelStateMachine clock_ns is now required (removed time.monotonic_ns default); test_az388 patches estimator._clock instead of a module-level time; test_az393 ardupilot adapter uses a _FixedClock test double. Excluded per the task spec: TlogReplayFcAdapter (AZ-399), ReplaySink (AZ-400), compose_replay (AZ-401), CLI (AZ-402), Docker/CI (AZ-403), E2E fixture (AZ-404), IMU auto-sync (AZ-405). Co-authored-by: Cursor <cursoragent@cursor.com>
71 lines
2.7 KiB
Python
71 lines
2.7 KiB
Python
"""``Clock`` Protocol — replay/live-agnostic monotonic + wall-clock time.
|
|
|
|
Frozen at AZ-398 v1.0.0 per the replay contract:
|
|
``_docs/02_document/contracts/replay/replay_protocol.md``.
|
|
|
|
The Protocol is Layer 1 cross-cutting per ``module-layout.md`` — every
|
|
component that previously called :func:`time.monotonic_ns`,
|
|
:func:`time.time_ns`, or :func:`time.sleep` MUST consume an injected
|
|
:class:`Clock` instead (Invariant 2). The strategy is selected exactly
|
|
once at composition time (Invariant — Single Clock per process):
|
|
|
|
- **Live / research / operator** binaries inject :class:`WallClock`.
|
|
- **Replay** binary injects :class:`TlogDerivedClock` (ASAP) or
|
|
:class:`WallClock` (REALTIME pace).
|
|
|
|
Mode-specific behaviour lives in the strategy; consumers see only the
|
|
``Clock`` interface (R-DEMO-4 mitigation).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Protocol, runtime_checkable
|
|
|
|
|
|
@runtime_checkable
|
|
class Clock(Protocol):
|
|
"""Monotonic + wall-clock + sleep-until abstraction (AZ-398 v1.0.0).
|
|
|
|
All three methods are non-blocking except :meth:`sleep_until_ns`,
|
|
which honours the configured replay pace:
|
|
|
|
- ``WallClock.sleep_until_ns(t)`` blocks until ``time.monotonic_ns()``
|
|
catches up to ``t`` (live + REALTIME replay).
|
|
- ``TlogDerivedClock.sleep_until_ns(t)`` is a no-op (ASAP replay).
|
|
|
|
Strategies MUST guarantee :meth:`monotonic_ns` is non-decreasing
|
|
across calls within the same process (Invariant 3 spirit).
|
|
"""
|
|
|
|
def monotonic_ns(self) -> int:
|
|
"""Return the strategy's monotonic time in nanoseconds.
|
|
|
|
For :class:`WallClock` this delegates to
|
|
:func:`time.monotonic_ns`. For :class:`TlogDerivedClock` this
|
|
returns the most recently advanced tlog timestamp (advance-on-
|
|
call semantics — see AC-6).
|
|
"""
|
|
...
|
|
|
|
def time_ns(self) -> int:
|
|
"""Return the strategy's UTC wall-clock time in nanoseconds.
|
|
|
|
Used for log timestamps that need calendar alignment (FDR
|
|
records, STATUSTEXT). For :class:`WallClock` this is
|
|
:func:`time.time_ns`; for :class:`TlogDerivedClock` this is the
|
|
tlog message's wall-clock timestamp (the ``time_unix_usec`` /
|
|
``time_boot_ms`` field, normalised to ns).
|
|
"""
|
|
...
|
|
|
|
def sleep_until_ns(self, target_ns: int) -> None:
|
|
"""Block until :meth:`monotonic_ns` would return ``target_ns``.
|
|
|
|
Honours ``pace=REALTIME`` by sleeping the wall-clock delta; honours
|
|
``pace=ASAP`` by no-op'ing. ``target_ns`` already in the past is a
|
|
no-op (no exception, no negative sleep). The Protocol does not
|
|
prescribe spurious-wakeup behaviour; strategies SHOULD use
|
|
:func:`time.sleep` (which retries internally on POSIX).
|
|
"""
|
|
...
|