From e59df06eea73f104b88d20b7ef63e88a8e28cd47 Mon Sep 17 00:00:00 2001 From: jakethesake420 Date: Mon, 6 Nov 2023 16:32:31 -0600 Subject: [PATCH] mazda-9.4 --- common/params.cc | 2 + common/realtime.py | 85 +++++++++ selfdrive/assets/img_spinner_comma.png | Bin 15713 -> 37884 bytes selfdrive/assets/img_spinner_comma1.png | Bin 0 -> 15713 bytes selfdrive/boardd/boardd.cc | 1 + selfdrive/car/__init__.py | 19 ++ selfdrive/car/car_helpers.py | 13 +- selfdrive/car/ford/values.py | 21 +-- selfdrive/car/interfaces.py | 9 +- selfdrive/car/mazda/carcontroller.py | 115 ++++++++---- selfdrive/car/mazda/carstate.py | 164 ++++++++++++++--- selfdrive/car/mazda/interface.py | 81 +++++++-- selfdrive/car/mazda/mazdacan.py | 140 +++++++++----- selfdrive/car/mazda/values.py | 191 +++++++++++++++++--- selfdrive/car/tests/test_car_interfaces.py | 9 + selfdrive/car/torque_data/override.yaml | 8 + selfdrive/controls/controlsd.py | 13 +- selfdrive/controls/lib/latcontrol_torque.py | 11 +- selfdrive/global_ti.py | 8 + selfdrive/ui/qt/offroad/settings.cc | 6 + selfdrive/updated.py | 4 +- system/loggerd/loggerd.h | 2 + 22 files changed, 738 insertions(+), 164 deletions(-) create mode 100644 selfdrive/assets/img_spinner_comma1.png create mode 100644 selfdrive/global_ti.py diff --git a/common/params.cc b/common/params.cc index 9b4077347..990be48b2 100644 --- a/common/params.cc +++ b/common/params.cc @@ -180,10 +180,12 @@ std::unordered_map keys = { {"Offroad_UpdateFailed", CLEAR_ON_MANAGER_START}, {"OpenpilotEnabledToggle", PERSISTENT}, {"PandaHeartbeatLost", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, + {"OpenpilotLongitudinalControl", PERSISTENT}, {"PandaSignatures", CLEAR_ON_MANAGER_START}, {"Passive", PERSISTENT}, {"PrimeType", PERSISTENT}, {"RecordFront", PERSISTENT}, + {"RecordRoad", PERSISTENT}, {"RecordFrontLock", PERSISTENT}, // for the internal fleet {"ReplayControlsState", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"ShouldDoUpdate", CLEAR_ON_MANAGER_START}, diff --git a/common/realtime.py b/common/realtime.py index 05e19e770..5bd3c31f7 100644 --- a/common/realtime.py +++ b/common/realtime.py @@ -93,3 +93,88 @@ class Ratekeeper: self._frame += 1 self._remaining = remaining return lagged + +class DurationTimer: + def __init__(self, duration=0, step=DT_CTRL) -> None: + self.step = step + self.duration = duration + self.was_reset = False + self.timer = 0 + self.min = float("-inf") # type: float + self.max = float("inf") # type: float + + def tick_obj(self) -> None: + self.timer += self.step + # reset on overflow + self.reset() if (self.timer == (self.max or self.min)) else None + + def reset(self) -> None: + """Resets this objects timer""" + self.timer = 0 + self.was_reset = True + + def active(self) -> bool: + """Returns true if time since last reset is less than duration""" + return bool(round(self.timer,2) < self.duration) + + def adjust(self, duration) -> None: + """Adjusts the duration of the timer""" + self.duration = duration + + def once_after_reset(self) -> bool: + """Returns true only one time after calling reset()""" + ret = self.was_reset + self.was_reset = False + return ret + + @staticmethod + def interval_obj(rate, frame) -> bool: + if frame % rate == 0: # Highlighting shows "frame" in white + return True + return False + +class ModelTimer(DurationTimer): + frame = 0 # type: int + objects = [] # type: List[DurationTimer] + def __init__(self, duration=0) -> None: + self.step = DT_MDL + super().__init__(duration, self.step) + self.__class__.objects.append(self) + + @classmethod + def tick(cls) -> None: + cls.frame += 1 + for obj in cls.objects: + ModelTimer.tick_obj(obj) + + @classmethod + def reset_all(cls) -> None: + for obj in cls.objects: + obj.reset() + + @classmethod + def interval(cls, rate) -> bool: + return ModelTimer.interval_obj(rate, cls.frame) + +class ControlsTimer(DurationTimer): + frame = 0 + objects = [] # type: List[DurationTimer] + def __init__(self, duration=0) -> None: + self.step = DT_CTRL + super().__init__(duration=duration, step=self.step) + self.__class__.objects.append(self) + + @classmethod + def tick(cls) -> None: + cls.frame += 1 + for obj in cls.objects: + ControlsTimer.tick_obj(obj) + + @classmethod + def reset_all(cls) -> None: + for obj in cls.objects: + obj.reset() + + @classmethod + def interval(cls, rate) -> bool: + return ControlsTimer.interval_obj(rate, cls.frame) diff --git a/selfdrive/assets/img_spinner_comma.png b/selfdrive/assets/img_spinner_comma.png index 16109557f85911820bcafc30b6af681e3d5a1bfb..dd32c2d5d9e91be71088dcb606855f6b8335eec4 100644 GIT binary patch literal 37884 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 send_panda_states(PubMaster *pm, const std::vector ps.setSpiChecksumErrorCount(health.spi_checksum_error_count); ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f); ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f); + ps.setTorqueInterceptorDetected(health.torque_interceptor_detected_pkt); std::array cs = {ps.initCanState0(), ps.initCanState1(), ps.initCanState2()}; diff --git a/selfdrive/car/__init__.py b/selfdrive/car/__init__.py index c90ae50ab..7019f0a83 100644 --- a/selfdrive/car/__init__.py +++ b/selfdrive/car/__init__.py @@ -76,6 +76,25 @@ def scale_tire_stiffness(mass, wheelbase, center_to_front, tire_stiffness_factor def dbc_dict(pt_dbc, radar_dbc, chassis_dbc=None, body_dbc=None) -> Dict[str, str]: return {'pt': pt_dbc, 'radar': radar_dbc, 'chassis': chassis_dbc, 'body': body_dbc} +#alternate settings when using torque interceptor. May or may not be useful to some users/branches. +def apply_ti_steer_torque_limits(apply_torque, apply_torque_last, driver_torque, LIMITS): + + # limits due to driver torque + driver_max_torque = LIMITS.TI_STEER_MAX + (LIMITS.TI_STEER_DRIVER_ALLOWANCE + driver_torque * LIMITS.TI_STEER_DRIVER_FACTOR) * LIMITS.TI_STEER_DRIVER_MULTIPLIER + driver_min_torque = -LIMITS.TI_STEER_MAX + (-LIMITS.TI_STEER_DRIVER_ALLOWANCE + driver_torque * LIMITS.TI_STEER_DRIVER_FACTOR) * LIMITS.TI_STEER_DRIVER_MULTIPLIER + max_steer_allowed = max(min(LIMITS.TI_STEER_MAX, driver_max_torque), 0) + min_steer_allowed = min(max(-LIMITS.TI_STEER_MAX, driver_min_torque), 0) + apply_torque = clip(apply_torque, min_steer_allowed, max_steer_allowed) + + # slow rate if steer torque increases in magnitude + if apply_torque_last > 0: + apply_torque = clip(apply_torque, max(apply_torque_last - LIMITS.TI_STEER_DELTA_DOWN, -LIMITS.TI_STEER_DELTA_UP), + apply_torque_last + LIMITS.TI_STEER_DELTA_UP) + else: + apply_torque = clip(apply_torque, apply_torque_last - LIMITS.TI_STEER_DELTA_UP, + min(apply_torque_last + LIMITS.TI_STEER_DELTA_DOWN, LIMITS.TI_STEER_DELTA_UP)) + + return int(round(float(apply_torque))) def apply_driver_steer_torque_limits(apply_torque, apply_torque_last, driver_torque, LIMITS): diff --git a/selfdrive/car/car_helpers.py b/selfdrive/car/car_helpers.py index 5c1234258..26fc61751 100644 --- a/selfdrive/car/car_helpers.py +++ b/selfdrive/car/car_helpers.py @@ -15,6 +15,7 @@ import cereal.messaging as messaging from openpilot.selfdrive.car import gen_empty_fingerprint FRAME_FINGERPRINT = 100 # 1s +from openpilot.selfdrive.global_ti import TI EventName = car.CarEvent.EventName @@ -23,7 +24,7 @@ def get_startup_event(car_recognized, controller_available, fw_seen): if is_comma_remote() and is_tested_branch(): event = EventName.startup else: - event = EventName.startupMaster + event = EventName.startup if not car_recognized: if fw_seen: @@ -189,6 +190,8 @@ def fingerprint(logcan, sendcan, num_pandas): cloudlog.event("fingerprinted", car_fingerprint=car_fingerprint, source=source, fuzzy=not exact_match, cached=cached, fw_count=len(car_fw), ecu_responses=list(ecu_rx_addrs), vin_rx_addr=vin_rx_addr, fingerprints=finger, fw_query_time=fw_query_time, error=True) + TI.saved_candidate = car_fingerprint + TI.saved_finger = finger return car_fingerprint, finger, vin, car_fw, source, exact_match @@ -205,5 +208,13 @@ def get_car(logcan, sendcan, experimental_long_allowed, num_pandas=1): CP.carFw = car_fw CP.fingerprintSource = source CP.fuzzyFingerprint = not exact_match + + TI.saved_CarInterface = CarInterface return CarInterface(CP, CarController, CarState), CP + +def get_ti(): + print("get_ti, entering get_params") + car_params = TI.saved_CarInterface.get_params(TI.saved_candidate, TI.saved_finger, list(), False, False) + + return car_params diff --git a/selfdrive/car/ford/values.py b/selfdrive/car/ford/values.py index d11ba4ccf..435247f18 100644 --- a/selfdrive/car/ford/values.py +++ b/selfdrive/car/ford/values.py @@ -100,26 +100,7 @@ CAR_INFO: Dict[str, Union[CarInfo, List[CarInfo]]] = { } FW_QUERY_CONFIG = FwQueryConfig( - requests=[ - # CAN and CAN FD queries are combined. - # FIXME: For CAN FD, ECUs respond with frames larger than 8 bytes on the powertrain bus - # TODO: properly handle auxiliary requests to separate queries and add back whitelists - Request( - [StdQueries.TESTER_PRESENT_REQUEST, StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST], - [StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE], - # whitelist_ecus=[Ecu.engine], - ), - Request( - [StdQueries.TESTER_PRESENT_REQUEST, StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST], - [StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE], - # whitelist_ecus=[Ecu.eps, Ecu.abs, Ecu.fwdRadar, Ecu.fwdCamera, Ecu.shiftByWire], - bus=0, - auxiliary=True, - ), - ], - extra_ecus=[ - (Ecu.shiftByWire, 0x732, None), - ], + requests=[], ) FW_VERSIONS = { diff --git a/selfdrive/car/interfaces.py b/selfdrive/car/interfaces.py index 1b68b1dbc..130770e0d 100644 --- a/selfdrive/car/interfaces.py +++ b/selfdrive/car/interfaces.py @@ -19,7 +19,7 @@ from openpilot.selfdrive.controls.lib.vehicle_model import VehicleModel ButtonType = car.CarState.ButtonEvent.Type GearShifter = car.CarState.GearShifter EventName = car.CarEvent.EventName -TorqueFromLateralAccelCallbackType = Callable[[float, car.CarParams.LateralTorqueTuning, float, float, bool], float] +TorqueFromLateralAccelCallbackType = Callable[[float, car.CarParams.LateralTorqueTuning, float, float, float, float, bool], float] MAX_CTRL_SPEED = (V_CRUISE_MAX + 4) * CV.KPH_TO_MS ACCEL_MAX = 2.0 @@ -131,7 +131,8 @@ class CarInterfaceBase(ABC): return self.get_steer_feedforward_default def torque_from_lateral_accel_linear(self, lateral_accel_value: float, torque_params: car.CarParams.LateralTorqueTuning, - lateral_accel_error: float, lateral_accel_deadzone: float, friction_compensation: bool) -> float: + lateral_accel_error: float, lateral_accel_deadzone: float, + steering_angle: float, vego: float, friction_compensation: bool) -> float: # The default is a linear relationship between torque and lateral acceleration (accounting for road roll and steering friction) friction = get_friction(lateral_accel_error, lateral_accel_deadzone, FRICTION_THRESHOLD, torque_params, friction_compensation) return (lateral_accel_value / float(torque_params.latAccelFactor)) + friction @@ -175,6 +176,10 @@ class CarInterfaceBase(ABC): ret.longitudinalActuatorDelayLowerBound = 0.15 ret.longitudinalActuatorDelayUpperBound = 0.15 ret.steerLimitTimer = 1.0 + + # No Torque Interceptor by default + ret.enableTorqueInterceptor = False + return ret @staticmethod diff --git a/selfdrive/car/mazda/carcontroller.py b/selfdrive/car/mazda/carcontroller.py index 320ad19bb..d8ab5bde2 100644 --- a/selfdrive/car/mazda/carcontroller.py +++ b/selfdrive/car/mazda/carcontroller.py @@ -1,65 +1,114 @@ from cereal import car from opendbc.can.packer import CANPacker -from openpilot.selfdrive.car import apply_driver_steer_torque_limits +from openpilot.selfdrive.car import apply_driver_steer_torque_limits, apply_ti_steer_torque_limits from openpilot.selfdrive.car.mazda import mazdacan -from openpilot.selfdrive.car.mazda.values import CarControllerParams, Buttons +from openpilot.selfdrive.car.mazda.values import CarControllerParams, Buttons, GEN1 +from openpilot.common.realtime import ControlsTimer as Timer VisualAlert = car.CarControl.HUDControl.VisualAlert +LongCtrlState = car.CarControl.Actuators.LongControlState class CarController: def __init__(self, dbc_name, CP, VM): self.CP = CP self.apply_steer_last = 0 + self.ti_apply_steer_last = 0 self.packer = CANPacker(dbc_name) self.brake_counter = 0 self.frame = 0 - + self.params = CarControllerParams(CP) + self.hold_timer = Timer(6.0) + self.hold_delay = Timer(1.0) # delay before we start holding as to not hit the brakes too hard + self.resume_timer = Timer(0.5) + self.cancel_delay = Timer(0.07) # 70ms delay to try to avoid a race condition with stock system + def update(self, CC, CS, now_nanos): can_sends = [] apply_steer = 0 + ti_apply_steer = 0 if CC.latActive: # calculate steer and also set limits due to driver torque - new_steer = int(round(CC.actuators.steer * CarControllerParams.STEER_MAX)) + if CS.CP.enableTorqueInterceptor: + if CS.ti_lkas_allowed: + ti_new_steer = int(round(CC.actuators.steer * self.params.TI_STEER_MAX)) + ti_apply_steer = apply_ti_steer_torque_limits(ti_new_steer, self.ti_apply_steer_last, + CS.out.steeringTorque, self.params) + + new_steer = int(round(CC.actuators.steer * self.params.STEER_MAX)) apply_steer = apply_driver_steer_torque_limits(new_steer, self.apply_steer_last, - CS.out.steeringTorque, CarControllerParams) - - if CC.cruiseControl.cancel: - # If brake is pressed, let us wait >70ms before trying to disable crz to avoid - # a race condition with the stock system, where the second cancel from openpilot - # will disable the crz 'main on'. crz ctrl msg runs at 50hz. 70ms allows us to - # read 3 messages and most likely sync state before we attempt cancel. - self.brake_counter = self.brake_counter + 1 - if self.frame % 10 == 0 and not (CS.out.brakePressed and self.brake_counter < 7): - # Cancel Stock ACC if it's enabled while OP is disengaged - # Send at a rate of 10hz until we sync with stock ACC state - can_sends.append(mazdacan.create_button_cmd(self.packer, self.CP.carFingerprint, CS.crz_btns_counter, Buttons.CANCEL)) - else: - self.brake_counter = 0 - if CC.cruiseControl.resume and self.frame % 5 == 0: - # Mazda Stop and Go requires a RES button (or gas) press if the car stops more than 3 seconds - # Send Resume button when planner wants car to move - can_sends.append(mazdacan.create_button_cmd(self.packer, self.CP.carFingerprint, CS.crz_btns_counter, Buttons.RESUME)) - + CS.out.steeringTorque, self.params) self.apply_steer_last = apply_steer + self.ti_apply_steer_last = ti_apply_steer + + if self.CP.carFingerprint in GEN1: + if CC.cruiseControl.cancel: + # If brake is pressed, let us wait >70ms before trying to disable crz to avoid + # a race condition with the stock system, where the second cancel from openpilot + # will disable the crz 'main on'. crz ctrl msg runs at 50hz. 70ms allows us to + # read 3 messages and most likely sync state before we attempt cancel. + self.brake_counter = self.brake_counter + 1 + if self.frame % 10 == 0 and not (CS.out.brakePressed and self.brake_counter < 7): + # Cancel Stock ACC if it's enabled while OP is disengaged + # Send at a rate of 10hz until we sync with stock ACC state + can_sends.append(mazdacan.create_button_cmd(self.packer, self.CP.carFingerprint, CS.crz_btns_counter, Buttons.CANCEL)) + else: + self.brake_counter = 0 + if CC.cruiseControl.resume and self.frame % 5 == 0: + # Mazda Stop and Go requires a RES button (or gas) press if the car stops more than 3 seconds + # Send Resume button when planner wants car to move + can_sends.append(mazdacan.create_button_cmd(self.packer, self.CP.carFingerprint, CS.crz_btns_counter, Buttons.RESUME)) - # send HUD alerts - if self.frame % 50 == 0: - ldw = CC.hudControl.visualAlert == VisualAlert.ldw - steer_required = CC.hudControl.visualAlert == VisualAlert.steerRequired - # TODO: find a way to silence audible warnings so we can add more hud alerts - steer_required = steer_required and CS.lkas_allowed_speed - can_sends.append(mazdacan.create_alert_command(self.packer, CS.cam_laneinfo, ldw, steer_required)) + # send HUD alerts + if self.frame % 50 == 0: + ldw = CC.hudControl.visualAlert == VisualAlert.ldw + # steer_required = CC.hudControl.visualAlert == VisualAlert.steerRequired + # TODO: find a way to silence audible warnings so we can add more hud alerts + #steer_required = steer_required and CS.lkas_allowed_speed + steer_required = CS.out.steerFaultTemporary + can_sends.append(mazdacan.create_alert_command(self.packer, CS.cam_laneinfo, ldw, steer_required)) + + # send steering command if GEN1 + #The ti cannot be detected unless OP sends a can message to it because the ti only transmits when it + #sees the signature key in the designated address range. + can_sends.append(mazdacan.create_ti_steering_control(self.packer, self.CP.carFingerprint, + self.frame, ti_apply_steer)) + can_sends.append(mazdacan.create_steering_control(self.packer, self.CP.carFingerprint, + self.frame, apply_steer, CS.cam_lkas)) + else: + resume = False + hold = False + if Timer.interval(2): # send ACC command at 50hz + """ + Without this hold/resum logic, the car will only stop momentarily. + It will then start creeping forward again. This logic allows the car to + apply the electric brake to hold the car. The hold delay also fixes a + bug with the stock ACC where it sometimes will apply the brakes too early + when coming to a stop. + """ + if CS.out.standstill: # if we're stopped + if not self.hold_delay.active(): # and we have been stopped for more than hold_delay duration. This prevents a hard brake if we aren't fully stopped. + if (CC.cruiseControl.resume or CC.cruiseControl.override or (CC.actuators.longControlState == LongCtrlState.starting)): # and we want to resume + self.resume_timer.reset() # reset the resume timer so its active + else: # otherwise we're holding + hold = self.hold_timer.active() # hold for 6s. This allows the electric brake to hold the car. + + else: # if we're moving + self.hold_timer.reset() # reset the hold timer so its active when we stop + self.hold_delay.reset() # reset the hold delay + + resume = self.resume_timer.active() # stay on for 0.5s to release the brake. This allows the car to move. + can_sends.append(mazdacan.create_acc_cmd(self, self.packer, CS, CC, hold, resume)) - # send steering command - can_sends.append(mazdacan.create_steering_control(self.packer, self.CP.carFingerprint, + can_sends.append(mazdacan.create_steering_control(self.packer, self.CP.carFingerprint, self.frame, apply_steer, CS.cam_lkas)) new_actuators = CC.actuators.copy() - new_actuators.steer = apply_steer / CarControllerParams.STEER_MAX + new_actuators.steer = apply_steer / self.params.STEER_MAX new_actuators.steerOutputCan = apply_steer self.frame += 1 + Timer.tick() return new_actuators, can_sends diff --git a/selfdrive/car/mazda/carstate.py b/selfdrive/car/mazda/carstate.py index 1f7846ca0..bdea32f7d 100644 --- a/selfdrive/car/mazda/carstate.py +++ b/selfdrive/car/mazda/carstate.py @@ -1,9 +1,11 @@ +import copy + from cereal import car from openpilot.common.conversions import Conversions as CV from opendbc.can.can_define import CANDefine from opendbc.can.parser import CANParser from openpilot.selfdrive.car.interfaces import CarStateBase -from openpilot.selfdrive.car.mazda.values import DBC, LKAS_LIMITS, GEN1 +from openpilot.selfdrive.car.mazda.values import DBC, LKAS_LIMITS, GEN1, GEN2, TI_STATE, CAR, CarControllerParams class CarState(CarStateBase): def __init__(self, CP): @@ -17,8 +19,69 @@ class CarState(CarStateBase): self.low_speed_alert = False self.lkas_allowed_speed = False self.lkas_disabled = False + self.cam_lkas = 0 + self.params = CarControllerParams(CP) + + self.ti_ramp_down = False + self.ti_version = 1 + self.ti_state = TI_STATE.RUN + self.ti_violation = 0 + self.ti_error = 0 + self.ti_lkas_allowed = False + self.update = self.update_gen1 + if CP.carFingerprint in GEN1: + self.update = self.update_gen1 + if CP.carFingerprint in GEN2: + self.update = self.update_gen2 - def update(self, cp, cp_cam): + def update_gen2(self, cp, cp_cam, cp_body): + ret = car.CarState.new_message() + ret.wheelSpeeds = self.get_wheel_speeds( + cp_cam.vl["WHEEL_SPEEDS"]["FL"], + cp_cam.vl["WHEEL_SPEEDS"]["FR"], + cp_cam.vl["WHEEL_SPEEDS"]["RL"], + cp_cam.vl["WHEEL_SPEEDS"]["RR"], + ) + + ret.vEgoRaw = (ret.wheelSpeeds.fl + ret.wheelSpeeds.fr + ret.wheelSpeeds.rl + ret.wheelSpeeds.rr) / 4. + ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw) # Doesn't match cluster speed exactly + + ret.leftBlinker, ret.rightBlinker = self.update_blinker_from_lamp(100, cp.vl["BLINK_INFO"]["LEFT_BLINK"] == 1, + cp.vl["BLINK_INFO"]["RIGHT_BLINK"] == 1) + + ret.steeringAngleDeg = cp_cam.vl["STEER"]["STEER_ANGLE"] + + ret.steeringTorque = cp_body.vl["EPS_FEEDBACK"]["STEER_TORQUE_SENSOR"] + can_gear = int(cp_cam.vl["GEAR"]["GEAR"]) + ret.gas = cp_cam.vl["ENGINE_DATA"]["PEDAL_GAS"] + + unit_conversion = CV.MPH_TO_MS if cp.vl["SYSTEM_SETTINGS"]["IMPERIAL_UNIT"] else CV.KPH_TO_MS + + ret.steeringPressed = abs(ret.steeringTorque) > self.params.STEER_DRIVER_ALLOWANCE + ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(can_gear, None)) + ret.gasPressed = ret.gas > 0 + ret.seatbeltUnlatched = False # Cruise will not engage if seatbelt is unlatched (handled by car) + ret.doorOpen = False # Cruise will not engage if door is open (handled by car) + ret.brakePressed = cp.vl["BRAKE_PEDAL"]["BRAKE_PEDAL_PRESSED"] == 1 + ret.brake = .1 + ret.steerFaultPermanent = False # TODO locate signal. Car shows light on dash if there is a fault + ret.steerFaultTemporary = False # TODO locate signal. Car shows light on dash if there is a fault + ret.cruiseState.available = True # TODO locate signal. + ret.cruiseState.speed = cp.vl["CRUZE_STATE"]["CRZ_SPEED"] * unit_conversion + ret.cruiseState.enabled = ( (cp.vl["CRUZE_STATE"]["CRZ_ENABLED"] == 1) or (cp.vl["CRUZE_STATE"]["PRE_ENABLE"] == 1) ) + + speed_kph = cp_cam.vl["SPEED"]["SPEED"] * unit_conversion + ret.standstill = speed_kph < .1 + ret.cruiseState.standstill = False + self.cp = cp + self.cp_cam = cp_cam + self.acc = copy.copy(cp.vl["ACC"]) + + return ret + # end GEN2 + + + def update_gen1(self, cp, cp_cam, cp_body): ret = car.CarState.new_message() ret.wheelSpeeds = self.get_wheel_speeds( @@ -43,9 +106,23 @@ class CarState(CarStateBase): ret.leftBlinker, ret.rightBlinker = self.update_blinker_from_lamp(40, cp.vl["BLINK_INFO"]["LEFT_BLINK"] == 1, cp.vl["BLINK_INFO"]["RIGHT_BLINK"] == 1) - ret.steeringAngleDeg = cp.vl["STEER"]["STEER_ANGLE"] - ret.steeringTorque = cp.vl["STEER_TORQUE"]["STEER_TORQUE_SENSOR"] - ret.steeringPressed = abs(ret.steeringTorque) > LKAS_LIMITS.STEER_THRESHOLD + if self.CP.enableTorqueInterceptor: + ret.steeringTorque = cp_body.vl["TI_FEEDBACK"]["TI_TORQUE_SENSOR"] + + self.ti_version = cp_body.vl["TI_FEEDBACK"]["VERSION_NUMBER"] + self.ti_state = cp_body.vl["TI_FEEDBACK"]["STATE"] # DISCOVER = 0, OFF = 1, DRIVER_OVER = 2, RUN=3 + self.ti_violation = cp_body.vl["TI_FEEDBACK"]["VIOL"] # 0 = no violation + self.ti_error = cp_body.vl["TI_FEEDBACK"]["ERROR"] # 0 = no error + if self.ti_version > 1: + self.ti_ramp_down = (cp_body.vl["TI_FEEDBACK"]["RAMP_DOWN"] == 1) + + ret.steeringPressed = abs(ret.steeringTorque) > LKAS_LIMITS.TI_STEER_THRESHOLD + self.ti_lkas_allowed = not self.ti_ramp_down and self.ti_state == TI_STATE.RUN + else: + ret.steeringTorque = cp.vl["STEER_TORQUE"]["STEER_TORQUE_SENSOR"] + ret.steeringPressed = abs(ret.steeringTorque) > LKAS_LIMITS.STEER_THRESHOLD + + ret.steeringAngleDeg = cp.vl["STEER"]["STEER_ANGLE"] ret.steeringTorqueEps = cp.vl["STEER_TORQUE"]["STEER_TORQUE_MOTOR"] ret.steeringRateDeg = cp.vl["STEER_RATE"]["STEER_ANGLE_RATE"] @@ -82,16 +159,11 @@ class CarState(CarStateBase): ret.cruiseState.standstill = cp.vl["PEDALS"]["STANDSTILL"] == 1 ret.cruiseState.speed = cp.vl["CRZ_EVENTS"]["CRZ_SPEED"] * CV.KPH_TO_MS - if ret.cruiseState.enabled: - if not self.lkas_allowed_speed and self.acc_active_last: - self.low_speed_alert = True - else: - self.low_speed_alert = False - - # Check if LKAS is disabled due to lack of driver torque when all other states indicate - # it should be enabled (steer lockout). Don't warn until we actually get lkas active - # and lose it again, i.e, after initial lkas activation - ret.steerFaultTemporary = self.lkas_allowed_speed and lkas_blocked + # On if no driver torque the last 5 seconds + if self.CP.carFingerprint not in (CAR.CX5_2022, CAR.CX9_2021): + ret.steerFaultTemporary = cp.vl["STEER_RATE"]["HANDS_OFF_5_SECONDS"] == 1 + else: + ret.steerFaultTemporary = False self.acc_active_last = ret.cruiseState.enabled @@ -104,19 +176,39 @@ class CarState(CarStateBase): ret.steerFaultPermanent = cp_cam.vl["CAM_LKAS"]["ERR_BIT_1"] == 1 return ret - + + @staticmethod + def get_ti_messages(CP): + messages = [] + if CP.enableTorqueInterceptor and CP.carFingerprint in GEN1: + messages += [ + ("TI_FEEDBACK", 50), + ] + elif CP.carFingerprint in GEN2: + messages += [ + ("EPS_FEEDBACK", 50), + ("EPS_FEEDBACK2", 50), + ("EPS_FEEDBACK3", 50), + ] + return messages + @staticmethod def get_can_parser(CP): - messages = [ - # sig_address, frequency - ("BLINK_INFO", 10), - ("STEER", 67), - ("STEER_RATE", 83), - ("STEER_TORQUE", 83), - ("WHEEL_SPEEDS", 100), - ] + messages = [] + + if CP.carFingerprint not in GEN2: + messages += [ + # sig_address, frequency + ("BLINK_INFO", 10), + ("STEER", 67), + ("STEER_RATE", 83), + ("STEER_TORQUE", 83), + ("WHEEL_SPEEDS", 100), + ] if CP.carFingerprint in GEN1: + # get real driver torque if we are using a torque interceptor + messages += CarState.get_ti_messages(CP) messages += [ ("ENGINE_DATA", 100), ("CRZ_CTRL", 50), @@ -129,6 +221,16 @@ class CarState(CarStateBase): ("GEAR", 20), ("BSM", 10), ] + + if CP.carFingerprint in GEN2: + messages += [ + ("BRAKE_PEDAL", 20), + ("CRUZE_STATE", 10), + ("BLINK_INFO", 10), + ("ACC", 50), + ("CRZ_BTNS", 10), + ("SYSTEM_SETTINGS", 10), + ] return CANParser(DBC[CP.carFingerprint]["pt"], messages, 0) @@ -143,4 +245,18 @@ class CarState(CarStateBase): ("CAM_LKAS", 16), ] + if CP.carFingerprint in GEN2: + messages += [ + ("ENGINE_DATA", 100), + ("STEER_TORQUE", 100), + ("GEAR", 40), + ("WHEEL_SPEEDS", 100), + ("STEER", 100), + ("SPEED", 50), + ] + return CANParser(DBC[CP.carFingerprint]["pt"], messages, 2) + + @staticmethod + def get_body_can_parser(CP): + return CANParser(DBC[CP.carFingerprint]["pt"], CarState.get_ti_messages(CP), 1) diff --git a/selfdrive/car/mazda/interface.py b/selfdrive/car/mazda/interface.py index 7ac93d9de..ddfbf4677 100755 --- a/selfdrive/car/mazda/interface.py +++ b/selfdrive/car/mazda/interface.py @@ -1,24 +1,59 @@ #!/usr/bin/env python3 from cereal import car from openpilot.common.conversions import Conversions as CV -from openpilot.selfdrive.car.mazda.values import CAR, LKAS_LIMITS +from openpilot.selfdrive.car.mazda.values import CAR, LKAS_LIMITS, GEN2, GEN1 from openpilot.selfdrive.car import get_safety_config -from openpilot.selfdrive.car.interfaces import CarInterfaceBase +from openpilot.selfdrive.car.interfaces import CarInterfaceBase, TorqueFromLateralAccelCallbackType, FRICTION_THRESHOLD +from openpilot.selfdrive.controls.lib.drive_helpers import get_friction +from openpilot.selfdrive.global_ti import TI ButtonType = car.CarState.ButtonEvent.Type EventName = car.CarEvent.EventName class CarInterface(CarInterfaceBase): + + @staticmethod + def torque_from_lateral_accel_mazda(lateral_accel_value: float, torque_params: car.CarParams.LateralTorqueTuning, + lateral_accel_error: float, lateral_accel_deadzone: float, + steering_angle: float, vego: float, friction_compensation: bool) -> float: + steering_angle = abs(steering_angle) + lat_factor = torque_params.latAccelFactor * ((steering_angle * torque_params.latAngleFactor) + 1) + + friction = get_friction(lateral_accel_error, lateral_accel_deadzone, FRICTION_THRESHOLD, torque_params, friction_compensation) + return float(lateral_accel_value / lat_factor) + friction + + def torque_from_lateral_accel(self) -> TorqueFromLateralAccelCallbackType: + if self.CP.carFingerprint in GEN2: + return self.torque_from_lateral_accel_mazda + else: + return self.torque_from_lateral_accel_linear @staticmethod def _get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs): ret.carName = "mazda" - ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.mazda)] + + if candidate in GEN1: + ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.mazda)] + ret.steerActuatorDelay = 0.1 + + if candidate in GEN2: + ret.experimentalLongitudinalAvailable = True + ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.mazda2019)] + ret.openpilotLongitudinalControl = experimental_long + ret.stopAccel = -.5 + ret.vEgoStarting = .2 + ret.longitudinalTuning.kpBP = [0., 5., 35.] + ret.longitudinalTuning.kpV = [0.0, 0.0, 0.0] + ret.longitudinalTuning.kiBP = [0., 35.] + ret.longitudinalTuning.kiV = [0.1, 0.1] + ret.startingState = True + ret.steerActuatorDelay = 0.3 + ret.radarUnavailable = True - ret.dashcamOnly = candidate not in (CAR.CX5_2022, CAR.CX9_2021) + ret.dashcamOnly = False - ret.steerActuatorDelay = 0.1 + ret.steerLimitTimer = 0.8 ret.tireStiffnessFactor = 0.70 # not optimized yet @@ -40,8 +75,23 @@ class CarInterface(CarInterfaceBase): ret.mass = 3443 * CV.LB_TO_KG ret.wheelbase = 2.83 ret.steerRatio = 15.5 + elif candidate in CAR.MAZDA3_2019: + ret.mass = 3000 * CV.LB_TO_KG + ret.wheelbase = 2.725 + ret.steerRatio = 17.0 + ret.lateralTuning.torque.latAngleFactor = .14 + elif candidate in (CAR.CX_30, CAR.CX_50): + ret.mass = 3375 * CV.LB_TO_KG + ret.wheelbase = 2.814 # Measured + ret.steerRatio = 15.5 + ret.lateralTuning.torque.latAngleFactor = .14 + elif candidate in (CAR.CX_60, CAR.CX_80, CAR.CX_70, CAR.CX_90): + ret.mass = 4217 * CV.LB_TO_KG + ret.wheelbase = 3.1 + ret.steerRatio = 17.6 + ret.lateralTuning.torque.latAngleFactor = .14 - if candidate not in (CAR.CX5_2022, ): + if candidate not in (CAR.CX5_2022, CAR.MAZDA3_2019, CAR.CX_30, CAR.CX_50, CAR.CX_60, CAR.CX_70, CAR.CX_80, CAR.CX_90): ret.minSteerSpeed = LKAS_LIMITS.DISABLE_SPEED * CV.KPH_TO_MS ret.centerToFront = ret.wheelbase * 0.41 @@ -50,15 +100,24 @@ class CarInterface(CarInterfaceBase): # returns a car.CarState def _update(self, c): - ret = self.CS.update(self.cp, self.cp_cam) + if self.CP.carFingerprint in GEN1: + if self.CP.enableTorqueInterceptor and not TI.enabled: + TI.enabled = True + self.cp_body = self.CS.get_body_can_parser(self.CP) + self.can_parsers = [self.cp, self.cp_cam, self.cp_adas, self.cp_body, self.cp_loopback] + + ret = self.CS.update(self.cp, self.cp_cam, self.cp_body) # events events = self.create_common_events(ret) + if self.CP.carFingerprint in GEN1: + if self.CS.lkas_disabled: + events.add(EventName.lkasDisabled) + elif self.CS.low_speed_alert: + events.add(EventName.belowSteerSpeed) - if self.CS.lkas_disabled: - events.add(EventName.lkasDisabled) - elif self.CS.low_speed_alert: - events.add(EventName.belowSteerSpeed) + if not self.CS.acc_active_last and not self.CS.ti_lkas_allowed: + events.add(EventName.steerTempUnavailable) ret.events = events.to_msg() diff --git a/selfdrive/car/mazda/mazdacan.py b/selfdrive/car/mazda/mazdacan.py index e350c5587..12fe8be5b 100644 --- a/selfdrive/car/mazda/mazdacan.py +++ b/selfdrive/car/mazda/mazdacan.py @@ -1,51 +1,56 @@ from openpilot.selfdrive.car.mazda.values import GEN1, Buttons +from selfdrive.car.mazda.values import GEN1, GEN2, Buttons +from common.params import Params + def create_steering_control(packer, car_fingerprint, frame, apply_steer, lkas): - tmp = apply_steer + 2048 - - lo = tmp & 0xFF - hi = tmp >> 8 - - # copy values from camera - b1 = int(lkas["BIT_1"]) - er1 = int(lkas["ERR_BIT_1"]) - lnv = 0 - ldw = 0 - er2 = int(lkas["ERR_BIT_2"]) - - # Some older models do have these, newer models don't. - # Either way, they all work just fine if set to zero. - steering_angle = 0 - b2 = 0 - - tmp = steering_angle + 2048 - ahi = tmp >> 10 - amd = (tmp & 0x3FF) >> 2 - amd = (amd >> 4) | (( amd & 0xF) << 4) - alo = (tmp & 0x3) << 2 - - ctr = frame % 16 - # bytes: [ 1 ] [ 2 ] [ 3 ] [ 4 ] - csum = 249 - ctr - hi - lo - (lnv << 3) - er1 - (ldw << 7) - ( er2 << 4) - (b1 << 5) - - # bytes [ 5 ] [ 6 ] [ 7 ] - csum = csum - ahi - amd - alo - b2 - - if ahi == 1: - csum = csum + 15 - - if csum < 0: - if csum < -256: - csum = csum + 512 - else: - csum = csum + 256 - - csum = csum % 256 - - values = {} if car_fingerprint in GEN1: + tmp = apply_steer + 2048 + + lo = tmp & 0xFF + hi = tmp >> 8 + + # copy values from camera + b1 = int(lkas["BIT_1"]) + er1 = int(lkas["ERR_BIT_1"]) + lnv = 0 + ldw = 0 + er2 = int(lkas["ERR_BIT_2"]) + + # Some older models do have these, newer models don't. + # Either way, they all work just fine if set to zero. + steering_angle = 0 + b2 = 0 + + tmp = steering_angle + 2048 + ahi = tmp >> 10 + amd = (tmp & 0x3FF) >> 2 + amd = (amd >> 4) | (( amd & 0xF) << 4) + alo = (tmp & 0x3) << 2 + + ctr = frame % 16 + # bytes: [ 1 ] [ 2 ] [ 3 ] [ 4 ] + csum = 249 - ctr - hi - lo - (lnv << 3) - er1 - (ldw << 7) - ( er2 << 4) - (b1 << 5) + + # bytes [ 5 ] [ 6 ] [ 7 ] + csum = csum - ahi - amd - alo - b2 + + if ahi == 1: + csum = csum + 15 + + if csum < 0: + if csum < -256: + csum = csum + 512 + else: + csum = csum + 256 + + csum = csum % 256 + + bus = 0 + sig_name = "CAM_LKAS" + values = { "LKAS_REQUEST": apply_steer, "CTR": ctr, @@ -58,8 +63,40 @@ def create_steering_control(packer, car_fingerprint, frame, apply_steer, lkas): "ANGLE_ENABLED": b2, "CHKSUM": csum } + + elif car_fingerprint in GEN2: + bus = 1 + sig_name = "EPS_LKAS" + values = { + "LKAS_REQUEST": apply_steer, + "STEER_FEEL": 12000, + } + + return packer.make_can_msg(sig_name, bus, values) + +def create_ti_steering_control(packer, car_fingerprint, frame, apply_steer): + + key = 3294744160 + chksum = apply_steer + + if car_fingerprint in GEN1: + values = { + "LKAS_REQUEST" : apply_steer, + "CHKSUM" : chksum, + "KEY" : key + } + # TODO + # 1. Add new CAR values for MDARS Mazdas so that we can change the rate of the message. This will take some work. + # 2. Listen for reply's on both CAN buses if not MDARS version of + # Mazda (2021+ or m3 2019+) and warn the user if there is a bad connection + # but do not cause disengagment + + # Write to both buses for *future* redundancy, but we only check bus 1 for a response in carstate and safey_mazda.h for now. + # if (frame % 2 == 0): + # commands.append(packer.make_can_msg("CAM_LKAS2", 0, values)) + + return packer.make_can_msg("CAM_LKAS2", 1, values) - return packer.make_can_msg("CAM_LKAS", 0, values) def create_alert_command(packer, cam_msg: dict, ldw: bool, steer_required: bool): @@ -126,3 +163,20 @@ def create_button_cmd(packer, car_fingerprint, counter, button): } return packer.make_can_msg("CRZ_BTNS", 0, values) + +def create_acc_cmd(self, packer, CS, CC, hold, resume): + if self.CP.carFingerprint in GEN2: + values = CS.acc + msg_name = "ACC" + bus = 2 + + if (values["ACC_ENABLED"]): + if Params().get_bool("ExperimentalLongitudinalEnabled"): + values["ACCEL_CMD"] = (CC.actuators.accel * 240) + 2000 + values["HOLD"] = hold + values["RESUME"] = resume + else: + pass + + return packer.make_can_msg(msg_name, bus, values) + diff --git a/selfdrive/car/mazda/values.py b/selfdrive/car/mazda/values.py index 849cf229a..ff72f8761 100644 --- a/selfdrive/car/mazda/values.py +++ b/selfdrive/car/mazda/values.py @@ -9,23 +9,43 @@ from openpilot.selfdrive.car.fw_query_definitions import FwQueryConfig, Request, Ecu = car.CarParams.Ecu - # Steer torque limits - class CarControllerParams: - STEER_MAX = 800 # theoretical max_steer 2047 - STEER_DELTA_UP = 10 # torque increase per refresh - STEER_DELTA_DOWN = 25 # torque decrease per refresh - STEER_DRIVER_ALLOWANCE = 15 # allowed driver torque before start limiting - STEER_DRIVER_MULTIPLIER = 1 # weight driver torque - STEER_DRIVER_FACTOR = 1 # from dbc - STEER_ERROR_MAX = 350 # max delta between torque cmd and torque motor - STEER_STEP = 1 # 100 Hz - def __init__(self, CP): - pass + self.STEER_STEP = 1 # 100 Hz + if CP.carFingerprint in GEN1: + self.STEER_MAX = 600 # theoretical max_steer 2047 + self.STEER_DELTA_UP = 10 # torque increase per refresh + self.STEER_DELTA_DOWN = 25 # torque decrease per refresh + self.STEER_DRIVER_ALLOWANCE = 15 # allowed driver torque before start limiting + self.STEER_DRIVER_MULTIPLIER = 40 # weight driver torque + self.STEER_DRIVER_FACTOR = 1 # from dbc + self.STEER_ERROR_MAX = 350 # max delta between torque cmd and torque motor + + self.TI_STEER_MAX = 600 # theoretical max_steer 2047 + self.TI_STEER_DELTA_UP = 6 # torque increase per refresh + self.TI_STEER_DELTA_DOWN = 15 # torque decrease per refresh + self.TI_STEER_DRIVER_ALLOWANCE = 15 # allowed driver torque before start limiting + self.TI_STEER_DRIVER_MULTIPLIER = 40 # weight driver torque + self.TI_STEER_DRIVER_FACTOR = 1 # from dbc + self.TI_STEER_ERROR_MAX = 350 # max delta between torque cmd and torque motor + if CP.carFingerprint in GEN2: + self.STEER_MAX = 8000 + self.STEER_DELTA_UP = 45 # torque increase per refresh + self.STEER_DELTA_DOWN = 80 # torque decrease per refresh + self.STEER_DRIVER_ALLOWANCE = 1400 # allowed driver torque before start limiting + self.STEER_DRIVER_MULTIPLIER = 5 # weight driver torque + self.STEER_DRIVER_FACTOR = 1 # from dbc + self.STEER_ERROR_MAX = 3500 # max delta between torque cmd and torque motor +class TI_STATE: + DISCOVER = 0 + OFF = 1 + DRIVER_OVER = 2 + RUN = 3 + + class CAR(StrEnum): CX5 = "MAZDA CX-5" CX9 = "MAZDA CX-9" @@ -33,6 +53,13 @@ class CAR(StrEnum): MAZDA6 = "MAZDA 6" CX9_2021 = "MAZDA CX-9 2021" CX5_2022 = "MAZDA CX-5 2022" + MAZDA3_2019 = "MAZDA 3 2019" + CX_30 = "MAZDA CX-30" + CX_50 = "MAZDA CX-50" + CX_60 = "MAZDA CX-60" + CX_70 = "MAZDA CX-70" + CX_80 = "MAZDA CX-80" + CX_90 = "MAZDA CX-90" @dataclass @@ -48,13 +75,24 @@ CAR_INFO: Dict[str, Union[MazdaCarInfo, List[MazdaCarInfo]]] = { CAR.MAZDA6: MazdaCarInfo("Mazda 6 2017-20"), CAR.CX9_2021: MazdaCarInfo("Mazda CX-9 2021-23", video_link="https://youtu.be/dA3duO4a0O4"), CAR.CX5_2022: MazdaCarInfo("Mazda CX-5 2022-24"), + CAR.MAZDA3_2019: MazdaCarInfo("Mazda 3 2019-24"), + CAR.CX_30: MazdaCarInfo("Mazda CX-30 2019-24"), + CAR.CX_50: MazdaCarInfo("Mazda CX-50 2022-24"), + CAR.CX_60: MazdaCarInfo("Mazda CX-60 unreleased"), + CAR.CX_70: MazdaCarInfo("Mazda CX-70 unreleased"), + CAR.CX_80: MazdaCarInfo("Mazda CX-80 unreleased"), + CAR.CX_90: MazdaCarInfo("Mazda CX-90 2023"), } class LKAS_LIMITS: - STEER_THRESHOLD = 15 - DISABLE_SPEED = 45 # kph - ENABLE_SPEED = 52 # kph + STEER_THRESHOLD = 6 + DISABLE_SPEED = 0 # kph + ENABLE_SPEED = 0 # kph + TI_STEER_THRESHOLD = 6 + TI_DISABLE_SPEED = 0 # kph + TI_ENABLE_SPEED = 0 # kph + class Buttons: @@ -63,7 +101,7 @@ class Buttons: SET_MINUS = 2 RESUME = 3 CANCEL = 4 - + TURN_ON = 5 FW_QUERY_CONFIG = FwQueryConfig( requests=[ @@ -71,13 +109,17 @@ FW_QUERY_CONFIG = FwQueryConfig( [StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST], [StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE], ), - # Log responses on powertrain bus Request( - [StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST], - [StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE], - bus=0, - logging=True, + [StdQueries.TESTER_PRESENT_REQUEST, StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST], + [StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE], + whitelist_ecus=[Ecu.engine], ), + Request( + [StdQueries.TESTER_PRESENT_REQUEST, StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST], + [StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE], + bus=0, + whitelist_ecus=[Ecu.eps, Ecu.abs, Ecu.fwdRadar, Ecu.fwdCamera, Ecu.shiftByWire], + ) ], ) @@ -277,31 +319,37 @@ FW_VERSIONS = { b'GBEF-3210X-B-00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'GBEF-3210X-C-00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'GFBC-3210X-A-00\000\000\000\000\000\000\000\000\000', + b'GFBC-3210X-A-00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.engine, 0x7e0, None): [ b'PA34-188K2-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PX4F-188K2-D\000\000\000\000\000\000\000\000\000\000\000\000', b'PYH7-188K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PYH7-188K2-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'PANX-188K2-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.fwdRadar, 0x764, None): [ b'K131-67XK2-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'K131-67XK2-E\000\000\000\000\000\000\000\000\000\000\000\000', + b'K131-67XK2-F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.abs, 0x760, None): [ b'GBVH-437K2-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'GBVH-437K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'GDDM-437K2-A\000\000\000\000\000\000\000\000\000\000\000\000', + b'GDDM-437K2-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.fwdCamera, 0x706, None): [ b'B61L-67XK2-S\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'B61L-67XK2-T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'GSH7-67XK2-P\000\000\000\000\000\000\000\000\000\000\000\000', + b'GSH7-67XK2-T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.transmission, 0x7e1, None): [ b'PA28-21PS1-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PYH3-21PS1-D\000\000\000\000\000\000\000\000\000\000\000\000', b'PYH7-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'PA28-21PS1-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], }, @@ -334,7 +382,98 @@ FW_VERSIONS = { b'PXM4-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PXM6-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], - } + }, + + CAR.MAZDA3_2019 : { + (Ecu.eps, 0x730, None): [ + b'BDGF-3216X-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'BCKA-3216X-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'BCKA-3216X-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + #b'BDGF-3216X-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'BCKA-3216X-D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.engine, 0x7e0, None): [ + b'PA2J-188K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + #b'PX06-188K2-S\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'PX06-188K2-N\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'PX08-188K2-L\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'PX4W-188K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.fwdRadar, 0x764, None): [ + b'BDTS-67XK2-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + #b'B0N2-67XK2-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'B0N2-67XK2-D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.abs, 0x760, None): [ + b'BFVV-4300F-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'BHCB-4300F-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'BCKA-4300F-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'BCKA-4300F-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'BFVV-4300F-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.fwdCamera, 0x706, None): [ + b'BDGF-67WK2-K\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'BDGF-67WK2-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + #b'BDGF-67WK2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'DFR5-67WK2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.transmission, 0x7e1, None): [ + b'PAM6-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + #b'PX01-21PS1-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'PX03-21PS1-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'PX4K-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + }, + CAR.CX_30 : { + (Ecu.eps, 0x730, None): [ + b'DFR5-3216X-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'BDGF-3216X-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.engine, 0x7e0, None): [ + b'PX06-188K2-S\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.fwdRadar, 0x764, None): [ + b'BDTS-67XK2-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'B0N2-67XK2-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.abs, 0x760, None): [ + b'DEJW-4300F-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'BCKA-4300F-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.fwdCamera, 0x706, None): [ + b'BDGF-67WK2-K\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'BDGF-67WK2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.transmission, 0x7e1, None): [ + b'PX01-21PS1-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + }, + + CAR.CX_50 : { + (Ecu.eps, 0x730, None): [ + b'VA40-3216Y-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.engine, 0x7e0, None): [ + b'PX06-188K2-N\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'PX08-188K2-L\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'PX4W-188K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.fwdRadar, 0x764, None): [ + b'VA45-67XK2-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.abs, 0x760, None): [ + b'VA40-4300F-D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'VA40-4300F-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.fwdCamera, 0x706, None): [ + b'VA40-67WK2-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.transmission, 0x7e1, None): [ + #b'PX01-21PS1-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'PX03-21PS1-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'PX4K-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + }, } @@ -345,7 +484,15 @@ DBC = { CAR.MAZDA6: dbc_dict('mazda_2017', None), CAR.CX9_2021: dbc_dict('mazda_2017', None), CAR.CX5_2022: dbc_dict('mazda_2017', None), + CAR.MAZDA3_2019: dbc_dict('mazda_2019', None), + CAR.CX_30: dbc_dict('mazda_2019', None), + CAR.CX_50: dbc_dict('mazda_2019', None), + CAR.CX_60: dbc_dict('mazda_2019', None), + CAR.CX_70: dbc_dict('mazda_2019', None), + CAR.CX_80: dbc_dict('mazda_2019', None), + CAR.CX_90: dbc_dict('mazda_2019', None), } # Gen 1 hardware: same CAN messages and same camera GEN1 = {CAR.CX5, CAR.CX9, CAR.CX9_2021, CAR.MAZDA3, CAR.MAZDA6, CAR.CX5_2022} +GEN2 = {CAR.MAZDA3_2019, CAR.CX_30, CAR.CX_50, CAR.CX_60, CAR.CX_70, CAR.CX_80, CAR.CX_90} diff --git a/selfdrive/car/tests/test_car_interfaces.py b/selfdrive/car/tests/test_car_interfaces.py index a920bd45d..4c457951e 100755 --- a/selfdrive/car/tests/test_car_interfaces.py +++ b/selfdrive/car/tests/test_car_interfaces.py @@ -40,6 +40,8 @@ def get_fuzzy_car_interface_args(draw: DrawType) -> dict: params['car_fw'] = [car.CarParams.CarFw(ecu=fw[0], address=fw[1], subAddress=fw[2] or 0) for fw in params['car_fw']] return params +from openpilot.selfdrive.global_ti import TI +from openpilot.selfdrive.car.mazda.values import GEN1 class TestCarInterfaces(unittest.TestCase): @@ -60,6 +62,13 @@ class TestCarInterfaces(unittest.TestCase): car_params = CarInterface.get_params(car_name, args['fingerprints'], args['car_fw'], experimental_long=args['experimental_long'], docs=False) + if car_name in GEN1: + TI.saved_candidate = car_name + TI.saved_CarInterface = CarInterface + TI.saved_finger = args['fingerprints'] + car_params = TI.saved_CarInterface.get_params(TI.saved_candidate, TI.saved_finger, list(), experimental_long=False, docs=False) + car_params.enableTorqueInterceptor = True + car_interface = CarInterface(car_params, CarController, CarState) assert car_params assert car_interface diff --git a/selfdrive/car/torque_data/override.yaml b/selfdrive/car/torque_data/override.yaml index 29e616b10..0c6bb1b07 100644 --- a/selfdrive/car/torque_data/override.yaml +++ b/selfdrive/car/torque_data/override.yaml @@ -64,6 +64,14 @@ HYUNDAI AZERA HYBRID 6TH GEN: [1.8, 1.8, 0.1] KIA K8 HYBRID 1ST GEN: [2.5, 2.5, 0.1] HYUNDAI CUSTIN 1ST GEN: [2.5, 2.5, 0.1] +MAZDA 3 2019: [1.45, 3.0, .33] +MAZDA CX-30: [1.45, 3.0, .33] +MAZDA CX-50: [1.45, 3.0, .33] +MAZDA CX-60: [1.45, 3.0, .33] +MAZDA CX-70: [1.45, 3.0, .33] +MAZDA CX-80: [1.45, 3.0, .33] +MAZDA CX-90: [1.45, 3.0, .33] + # Dashcam or fallback configured as ideal car mock: [10.0, 10, 0.0] diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 19ce40793..0cca4d425 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -16,7 +16,7 @@ from panda import ALTERNATIVE_EXPERIENCE from openpilot.system.swaglog import cloudlog from openpilot.system.version import is_release_branch, get_short_branch from openpilot.selfdrive.boardd.boardd import can_list_to_can_capnp -from openpilot.selfdrive.car.car_helpers import get_car, get_startup_event, get_one_can +from openpilot.selfdrive.car.car_helpers import get_car, get_startup_event, get_one_can, get_ti from openpilot.selfdrive.controls.lib.lateral_planner import CAMERA_OFFSET from openpilot.selfdrive.controls.lib.drive_helpers import VCruiseHelper, get_lag_adjusted_curvature from openpilot.selfdrive.controls.lib.latcontrol import LatControl, MIN_LATERAL_CONTROL_SPEED @@ -103,6 +103,8 @@ class Controls: if not self.disengage_on_accelerator: self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS + self.ti_ready = False + # read params self.is_metric = self.params.get_bool("IsMetric") self.is_ldw_enabled = self.params.get_bool("IsLdwEnabled") @@ -322,6 +324,15 @@ class Controls: if log.PandaState.FaultType.relayMalfunction in pandaState.faults: self.events.add(EventName.relayMalfunction) + if pandaState.torqueInterceptorDetected and not self.ti_ready: + self.ti_ready = True + self.CP.enableTorqueInterceptor = True + #Update CP based on torque_interceptor_ready + self.CP = get_ti() + # set alternative experiences since get_ti() reset it to default. + if not self.disengage_on_accelerator: + self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS + # Handle HW and system malfunctions # Order is very intentional here. Be careful when modifying this. # All events here should at least have NO_ENTRY and SOFT_DISABLE. diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 2c7763063..2a825aaf9 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -43,11 +43,12 @@ class LatControlTorque(LatControl): output_torque = 0.0 pid_log.active = False else: + steering_angle = CS.steeringAngleDeg - params.angleOffsetDeg if self.use_steering_angle: - actual_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll) + actual_curvature = -VM.calc_curvature(math.radians(steering_angle), CS.vEgo, params.roll) curvature_deadzone = abs(VM.calc_curvature(math.radians(self.steering_angle_deadzone_deg), CS.vEgo, 0.0)) else: - actual_curvature_vm = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll) + actual_curvature_vm = -VM.calc_curvature(math.radians(steering_angle), CS.vEgo, params.roll) actual_curvature_llk = llk.angularVelocityCalibrated.value[2] / CS.vEgo actual_curvature = interp(CS.vEgo, [2.0, 5.0], [actual_curvature_vm, actual_curvature_llk]) curvature_deadzone = 0.0 @@ -63,13 +64,13 @@ class LatControlTorque(LatControl): measurement = actual_lateral_accel + low_speed_factor * actual_curvature gravity_adjusted_lateral_accel = desired_lateral_accel - params.roll * ACCELERATION_DUE_TO_GRAVITY torque_from_setpoint = self.torque_from_lateral_accel(setpoint, self.torque_params, setpoint, - lateral_accel_deadzone, friction_compensation=False) + lateral_accel_deadzone, steering_angle, CS.vEgo, friction_compensation=False) torque_from_measurement = self.torque_from_lateral_accel(measurement, self.torque_params, measurement, - lateral_accel_deadzone, friction_compensation=False) + lateral_accel_deadzone, steering_angle, CS.vEgo, friction_compensation=False) pid_log.error = torque_from_setpoint - torque_from_measurement ff = self.torque_from_lateral_accel(gravity_adjusted_lateral_accel, self.torque_params, desired_lateral_accel - actual_lateral_accel, - lateral_accel_deadzone, friction_compensation=True) + lateral_accel_deadzone,steering_angle, CS.vEgo, friction_compensation=True) freeze_integrator = steer_limited or CS.steeringPressed or CS.vEgo < 5 output_torque = self.pid.update(pid_log.error, diff --git a/selfdrive/global_ti.py b/selfdrive/global_ti.py new file mode 100644 index 000000000..20c0055f1 --- /dev/null +++ b/selfdrive/global_ti.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +from selfdrive.car import gen_empty_fingerprint + +class TI: + saved_candidate: str + saved_finger = gen_empty_fingerprint() + saved_CarInterface: object + enabled = False \ No newline at end of file diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 669de5eae..722925894 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -65,6 +65,12 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { tr("Upload data from the driver facing camera and help improve the driver monitoring algorithm."), "../assets/offroad/icon_monitoring.png", }, + { + "RecordBack", + tr("Record and Upload Road Cameras"), + tr("Upload data from the road cameras."), + "../assets/offroad/icon_monitoring.png", + }, { "IsMetric", tr("Use Metric System"), diff --git a/selfdrive/updated.py b/selfdrive/updated.py index 1e128fb11..5605c98ae 100755 --- a/selfdrive/updated.py +++ b/selfdrive/updated.py @@ -32,8 +32,8 @@ FINALIZED = os.path.join(STAGING_ROOT, "finalized") OVERLAY_INIT = Path(os.path.join(BASEDIR, ".overlay_init")) -DAYS_NO_CONNECTIVITY_MAX = 14 # do not allow to engage after this many days -DAYS_NO_CONNECTIVITY_PROMPT = 10 # send an offroad prompt after this many days +DAYS_NO_CONNECTIVITY_MAX = 9999 # do not allow to engage after this many days +DAYS_NO_CONNECTIVITY_PROMPT = 9998 # send an offroad prompt after this many days class WaitTimeHelper: def __init__(self): diff --git a/system/loggerd/loggerd.h b/system/loggerd/loggerd.h index cfc06c28d..e2674960a 100644 --- a/system/loggerd/loggerd.h +++ b/system/loggerd/loggerd.h @@ -58,12 +58,14 @@ public: const EncoderInfo main_road_encoder_info = { .publish_name = "roadEncodeData", .filename = "fcamera.hevc", + .record = Params().getBool("RecordBack"), INIT_ENCODE_FUNCTIONS(RoadEncode), }; const EncoderInfo main_wide_road_encoder_info = { .publish_name = "wideRoadEncodeData", .filename = "ecamera.hevc", + .record = Params().getBool("RecordBack"), INIT_ENCODE_FUNCTIONS(WideRoadEncode), };