mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 10:41:14 +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>
43 lines
1.2 KiB
Python
43 lines
1.2 KiB
Python
"""``WallClock`` strategy (AZ-398) — live + REALTIME replay.
|
|
|
|
Thin :class:`Clock` adapter over the standard-library :mod:`time`
|
|
module. Owned by ``clock/`` so the AC-4 AST scan over ``components/``
|
|
remains clean: components MUST NOT call :func:`time.monotonic_ns`
|
|
directly; they call :meth:`WallClock.monotonic_ns` via injection.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
|
|
|
|
class WallClock:
|
|
"""Default :class:`Clock` strategy backed by :mod:`time`.
|
|
|
|
Stateless; constructable without arguments. All three methods are
|
|
trivially Liskov-clean over the Protocol.
|
|
"""
|
|
|
|
__slots__ = ()
|
|
|
|
def monotonic_ns(self) -> int:
|
|
return time.monotonic_ns()
|
|
|
|
def time_ns(self) -> int:
|
|
return time.time_ns()
|
|
|
|
def sleep_until_ns(self, target_ns: int) -> None:
|
|
"""Block until ``time.monotonic_ns() >= target_ns``.
|
|
|
|
A target already in the past is a no-op. Sub-millisecond
|
|
oversleep is accepted (AC-5: ≤ 5 ms drift on a 100 ms sleep).
|
|
"""
|
|
now = time.monotonic_ns()
|
|
delta_ns = target_ns - now
|
|
if delta_ns <= 0:
|
|
return
|
|
time.sleep(delta_ns / 1_000_000_000.0)
|
|
|
|
|
|
__all__ = ["WallClock"]
|