#!/usr/bin/env python3 import numpy as np from collections import deque import cereal.messaging as messaging from cereal import car from openpilot.common.params import Params from openpilot.common.realtime import config_realtime_process, DT_CTRL from openpilot.selfdrive.locationd.helpers import ParameterEstimator, PoseCalibrator, Pose MIN_LAG_VEL = 15.0 MAX_SANE_LAG = 3.0 MIN_ABS_YAW_RATE_DEG = 1 MAX_LAG_HIST_LEN_SEC = 600 MOVING_CORR_WINDOW = 300 MIN_OKAY_WINDOW = 60 class BaseLagEstimator: def __init__(self, CP, dt, moving_corr_window, min_okay_window): self.dt = dt self.window_len = int(moving_corr_window / self.dt) self.min_okay_window_len = int(min_okay_window / self.dt) self.initial_lag = CP.steerActuatorDelay self.calibrator = PoseCalibrator() lag_limit = int(moving_corr_window / (self.dt * 25)) self.lags = deque(maxlen=lag_limit) self.correlations = deque(maxlen=lag_limit) self.times = deque(maxlen=int(moving_corr_window / self.dt)) self.curvature = deque(maxlen=int(moving_corr_window / self.dt)) self.desired_curvature = deque(maxlen=int(moving_corr_window / self.dt)) self.okay = deque(maxlen=int(moving_corr_window / self.dt)) def actuator_delay(self, expected_sig, actual_sig, is_okay, dt, max_lag): raise NotImplementedError def handle_log(self, t, which, msg) -> None: if which == "carControl": self.lat_active = msg.latActive elif which == "carState": self.steering_pressed = msg.steeringPressed self.v_ego = msg.vEgo elif which == "controlsState": curvature = msg.curvature desired_curvature = msg.desiredCurvature okay = self.lat_active and not self.steering_pressed and self.v_ego > MIN_LAG_VEL and abs(self.yaw_rate) > np.radians(MIN_ABS_YAW_RATE_DEG) self.times.append(t) self.okay.append(okay) self.curvature.append(curvature) self.desired_curvature.append(desired_curvature) elif which == "livePose": device_pose = Pose.from_live_pose(msg) calibrated_pose = self.calibrator.build_calibrated_pose(device_pose) self.yaw_rate = calibrated_pose.angular_velocity.z elif which == 'liveCalibration': self.calibrator.feed_live_calib(msg) def get_msg(self, valid: bool, with_points: bool): okay_count = np.count_nonzero(self.okay) if len(self.curvature) >= self.window_len and okay_count >= self.min_okay_window_len: curvature = np.array(self.curvature) desired_curvature = np.array(self.desired_curvature) okay = np.array(self.okay) try: delay_curvature, correlation = self.actuator_delay(desired_curvature, curvature, okay, self.dt, MAX_SANE_LAG) self.lags.append(delay_curvature) self.correlations.append(correlation) except ValueError: pass if len(self.lags) > 0: steer_actuation_delay = np.mean(self.lags) steer_correlation = np.mean(self.correlations) is_estimated = True else: steer_actuation_delay = self.initial_lag steer_correlation = np.nan is_estimated = False msg = messaging.new_message('liveActuatorDelay') msg.valid = valid liveActuatorDelay = msg.liveActuatorDelay liveActuatorDelay.steerActuatorDelay = steer_actuation_delay liveActuatorDelay.totalPoints = len(self.curvature) liveActuatorDelay.isEstimated = is_estimated if with_points: liveActuatorDelay.points = [p for p in zip(self.curvature, self.desired_curvature)] return steer_actuation_delay, steer_correlation, okay_count, is_estimated class LagEstimator(ParameterEstimator): def correlation_lags(self, sig_len, dt): return np.arange(0, sig_len) * dt def actuator_delay(self, expected_sig, actual_sig, is_okay, dt, max_lag): from skimage.registration._masked_phase_cross_correlation import cross_correlate_masked # masked (gated) normalized cross-correlation # normalized, can be used for anything, like comparsion assert len(expected_sig) == len(actual_sig) xcorr = cross_correlate_masked(actual_sig, expected_sig, is_okay, is_okay, axes=tuple(range(actual_sig.ndim)),) lags = self.correlation_lags(len(expected_sig), dt) n_frames_max_delay = int(max_lag / dt) xcorr = xcorr[len(expected_sig) - 1: len(expected_sig) - 1 + n_frames_max_delay] lags = lags[:n_frames_max_delay] max_corr_index = np.argmax(xcorr) lag, corr = lags[max_corr_index], xcorr[max_corr_index] return lag, corr def main(): config_realtime_process([0, 1, 2, 3], 5) pm = messaging.PubMaster(['liveActuatorDelay', 'alertDebug']) sm = messaging.SubMaster(['livePose', 'liveCalibration', 'carControl', 'carState', 'controlsState'], poll='controlsState') params = Params() CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) estimator = LagEstimator(CP, DT_CTRL, MOVING_CORR_WINDOW, MIN_OKAY_WINDOW) while True: sm.update() if sm.all_checks(): for which in sm.services: if sm.updated[which]: t = sm.logMonoTime[which] * 1e-9 estimator.handle_log(t, which, sm[which]) if sm.frame % 25 == 0: msg = estimator.get_msg(sm.all_checks(), with_points=True) alert_msg = messaging.new_message('alertDebug') alert_msg.alertDebug.alertText1 = f"Lag estimate (fixed: {CP.steerActuatorDelay:.2f} s)" alert_msg.alertDebug.alertText2 = f"{msg.liveActuatorDelay.steerActuatorDelay:.2f} s" pm.send('liveActuatorDelay', msg) pm.send('alertDebug', alert_msg) if __name__ == "__main__": main()