mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 08:31:13 +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>
62 lines
2.1 KiB
Python
62 lines
2.1 KiB
Python
"""Composition-root :class:`Clock` factory (AZ-398).
|
|
|
|
Composition resolves :class:`Clock` exactly once per process per
|
|
Invariant — Single Clock per process. Live / research / operator
|
|
binaries call :func:`build_clock(kind="wall")`; the replay binary
|
|
calls :func:`build_clock(kind="tlog", source=...)` (the replay
|
|
composition root, AZ-401, wires the tlog timestamp source).
|
|
|
|
Concrete strategy modules (``wall_clock``, ``tlog_derived``) live
|
|
under :mod:`gps_denied_onboard.clock`; they are imported eagerly here
|
|
because the Clock has no Tier-specific runtime dependency and the
|
|
selection happens at startup.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable, Iterable
|
|
from typing import TYPE_CHECKING
|
|
|
|
from gps_denied_onboard.clock.tlog_derived import TlogDerivedClock
|
|
from gps_denied_onboard.clock.wall_clock import WallClock
|
|
|
|
if TYPE_CHECKING:
|
|
from gps_denied_onboard.clock import Clock
|
|
|
|
|
|
__all__ = ["build_clock"]
|
|
|
|
|
|
def build_clock(
|
|
*,
|
|
kind: str = "wall",
|
|
source: Callable[[], int] | Iterable[int] | None = None,
|
|
) -> "Clock":
|
|
"""Construct the :class:`Clock` strategy for this process.
|
|
|
|
``kind`` is one of ``"wall"`` (default) or ``"tlog"``. ``source`` is
|
|
required when ``kind == "tlog"`` (it carries the tlog parser's
|
|
timestamp stream) and forbidden otherwise.
|
|
|
|
Raises :class:`ValueError` on an unknown ``kind`` or a misconfigured
|
|
source — neither is recoverable, so failing loudly at composition
|
|
time is correct.
|
|
"""
|
|
if kind == "wall":
|
|
if source is not None:
|
|
raise ValueError(
|
|
"build_clock(kind='wall'): source must be None; "
|
|
"WallClock takes no upstream timestamp stream."
|
|
)
|
|
return WallClock()
|
|
if kind == "tlog":
|
|
if source is None:
|
|
raise ValueError(
|
|
"build_clock(kind='tlog'): source is required (the tlog "
|
|
"timestamp stream from the replay parser)."
|
|
)
|
|
return TlogDerivedClock(source)
|
|
raise ValueError(
|
|
f"build_clock: unknown kind {kind!r}; expected 'wall' or 'tlog'"
|
|
)
|