import pytest import time import numpy as np from flaky import flaky from collections import defaultdict import cereal.messaging as messaging from cereal import log from cereal.services import SERVICE_LIST from openpilot.system.manager.process_config import managed_processes TEST_TIMESPAN = 30 LAG_FRAME_TOLERANCE = {log.FrameData.ImageSensor.ar0231: 0.5, # ARs use synced pulses for frame starts log.FrameData.ImageSensor.ox03c10: 1.1} # OXs react to out-of-sync at next frame FRAME_DELTA_TOLERANCE = {log.FrameData.ImageSensor.ar0231: 1.0, log.FrameData.ImageSensor.ox03c10: 1.0} CAMERAS = ('roadCameraState', 'driverCameraState', 'wideRoadCameraState') # TODO: this shouldn't be needed @flaky(max_runs=3) @pytest.mark.tici class TestCamerad: def setup_method(self): # run camerad and record logs managed_processes['camerad'].start() time.sleep(3) socks = {c: messaging.sub_sock(c, conflate=False, timeout=100) for c in CAMERAS} self.logs = defaultdict(list) start_time = time.monotonic() while time.monotonic()- start_time < TEST_TIMESPAN: for cam, s in socks.items(): self.logs[cam] += messaging.drain_sock(s) time.sleep(0.2) managed_processes['camerad'].stop() self.log_by_frame_id = defaultdict(list) self.sensor_type = None for cam, msgs in self.logs.items(): if self.sensor_type is None: self.sensor_type = getattr(msgs[0], msgs[0].which()).sensor.raw expected_frames = SERVICE_LIST[cam].frequency * TEST_TIMESPAN assert expected_frames*0.95 < len(msgs) < expected_frames*1.05, f"unexpected frame count {cam}: {expected_frames=}, got {len(msgs)}" dts = np.abs(np.diff([getattr(m, m.which()).timestampSof/1e6 for m in msgs]) - 1000/SERVICE_LIST[cam].frequency) assert (dts < FRAME_DELTA_TOLERANCE[self.sensor_type]).all(), f"{cam} dts(ms) out of spec: max diff {dts.max()}, 99 percentile {np.percentile(dts, 99)}" for m in msgs: self.log_by_frame_id[getattr(m, m.which()).frameId].append(m) # strip beginning and end for _ in range(3): mn, mx = min(self.log_by_frame_id.keys()), max(self.log_by_frame_id.keys()) del self.log_by_frame_id[mn] del self.log_by_frame_id[mx] def test_frame_skips(self): skips = {} frame_ids = self.log_by_frame_id.keys() for frame_id in range(min(frame_ids), max(frame_ids)): seen_cams = [msg.which() for msg in self.log_by_frame_id[frame_id]] skip_cams = set(CAMERAS) - set(seen_cams) if len(skip_cams): skips[frame_id] = skip_cams assert len(skips) == 0, f"Found frame skips, missing cameras for the following frames: {skips}" def test_frame_sync(self): frame_times = {frame_id: [getattr(m, m.which()).timestampSof for m in msgs] for frame_id, msgs in self.log_by_frame_id.items()} diffs = {frame_id: (max(ts) - min(ts))/1e6 for frame_id, ts in frame_times.items()} def get_desc(fid, diff): cam_times = [(m.which(), getattr(m, m.which()).timestampSof/1e6) for m in self.log_by_frame_id[fid]] return (diff, cam_times) laggy_frames = {k: get_desc(k, v) for k, v in diffs.items() if v > LAG_FRAME_TOLERANCE[self.sensor_type]} def in_tol(diff): return 50 - LAG_FRAME_TOLERANCE[self.sensor_type] < diff and diff < 50 + LAG_FRAME_TOLERANCE[self.sensor_type] if len(laggy_frames) != 0 and all( in_tol(laggy_frames[lf][0]) for lf in laggy_frames): print("TODO: handle camera out of sync") else: assert len(laggy_frames) == 0, f"Frames not synced properly: {laggy_frames=}"