from edge.app import run_once from edge.tracking import IouTracker from edge.smoothing import TrackSmootherPool from edge.detectors.fall import FallDetector from edge.detectors.immobility import ImmobilityWatchdog from edge.detectors.ghost import GhostIncidentDetector from edge.detectors.zone import ZoneIntrusionDetector from edge.engine import EventEngine from edge.snapshot import RingBuffer, FaceBlurrer, Snapshotter class PipelineRuntime: def __init__(self, config, source, pose, sink, wall_clock=None): self.config = config self.camera_id = config.camera_id # for per-camera zone routing (multi-cam) self.source = source self.pose = pose self.sink = sink # Tier-2 fallback (hardware-bringup.md): when the device cannot sustain full-rate pose # for every camera, infer on every Nth tick instead of dropping cameras. Detector timers # are wall/monotonic-based, not frame-count-based, so they stay correct at a lower # sampling rate — only the reaction granularity coarsens. self.infer_every = max(1, int(getattr(config, "infer_every", 1))) self._tick = 0 # Wall clock for Event.occurred_at only; the loop clock (step's `now`) is monotonic. None # off-daemon -> run_once stamps occurred_at with the monotonic `now`, preserving old tests. self._wall = wall_clock self.tracker = IouTracker() self.pool = TrackSmootherPool() self.zone_detector = ZoneIntrusionDetector([], config.zone) self.engine = EventEngine( FallDetector(config.fall), ImmobilityWatchdog(config.immobility), GhostIncidentDetector(config.ghost), self.zone_detector, config.engine, ) self.ring = RingBuffer() self.snapshotter = Snapshotter(FaceBlurrer()) def step(self, now: float) -> list: self._tick += 1 if self.infer_every > 1 and self._tick % self.infer_every != 0: # Drain-only tick. Pull the frame so the v4l2/CSI buffer stays current — a backed-up # buffer hands back seconds-old frames and delays every alert — then DISCARD it. # It must NOT reach the ring buffer: head boxes come from inference, so an # un-analyzed frame carries no blur regions and would ship an identifiable face in # the evidence timeline (PIPA). Returning [] (not None) keeps this a healthy tick # for the daemon's camera-health accounting; a dead camera still reports None below. ok, _ = self.source.read() return [] if ok else None return run_once(self.source, self.pose, self.tracker, self.pool, self.engine, self.ring, self.snapshotter, self.sink, now_fn=lambda: now, wall_fn=self._wall) def apply_zones(self, zones: list) -> None: self.zone_detector.zones = list(zones)