Files
gps-denied-onboard/src/gps_denied_onboard/runtime_root/clock_factory.py
T
Oleksandr Bezdieniezhnykh 823c0f1b2e [AZ-398] Replay: FrameSource + Clock Protocols + Clock injection
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>
2026-05-12 05:10:01 +03:00

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'"
)