import copy from iqdbc.car import Bus, structs from iqdbc.can.parser import CANParser from iqdbc.car.common.conversions import Conversions as CV from iqdbc.car.byd.values import DBC, CarControllerParams as CCP from iqdbc.car.interfaces import CarStateBase GearShifter = structs.CarState.GearShifter GEAR_MAP = { 1: GearShifter.park, 2: GearShifter.reverse, 3: GearShifter.neutral, 4: GearShifter.drive, } # STEERING_TORQUE low nibble, rebuilt from LKS_PREPARED + CRUISE_ACTIVATED EPS_STATE_OFF = 8 EPS_STATE_PREPARED = 9 EPS_STATE_ACTUATING = 10 EPS_STATE_LATCHED_FAULT = 11 # ACC_HUD_ADAS.CRUISE_STATE CRUISE_STATE_AVAILABLE = 1 CRUISE_STATE_ENGAGED = 2 class CarState(CarStateBase): def __init__(self, CP, CP_IQ): super().__init__(CP, CP_IQ) self.lkas_hud = {} self.acc_cmd = {} self.buttons = {} self.eps_state = EPS_STATE_OFF self.override_latched = False self.lkas_btn_prev = False def update(self, can_parsers) -> tuple[structs.CarState, structs.IQCarState]: cp = can_parsers[Bus.pt] cp_cam = can_parsers[Bus.cam] ret = structs.CarState() ret_iq = structs.IQCarState() ret.wheelSpeeds.fl = cp.vl["WHEEL_SPEEDS"]["FL"] * CV.KPH_TO_MS ret.wheelSpeeds.fr = cp.vl["WHEEL_SPEEDS"]["FR"] * CV.KPH_TO_MS ret.wheelSpeeds.rl = cp.vl["WHEEL_SPEEDS"]["RL"] * CV.KPH_TO_MS ret.wheelSpeeds.rr = cp.vl["WHEEL_SPEEDS"]["RR"] * CV.KPH_TO_MS self.parse_wheel_speeds(ret, cp.vl["WHEEL_SPEEDS"]["FL"], cp.vl["WHEEL_SPEEDS"]["FR"], cp.vl["WHEEL_SPEEDS"]["RL"], cp.vl["WHEEL_SPEEDS"]["RR"], ) ret.standstill = ret.vEgoRaw < 0.01 ret.vEgoCluster = ret.vEgo # both torques come from STEERING_TORQUE; 0x11F's 16|8 field is unsigned and reads as a # steering rate in BYD's own firmware, so it is not used for override detection ret.steeringAngleDeg = cp.vl["STEER_MODULE_2"]["STEER_ANGLE_2"] ret.steeringTorque = cp.vl["STEERING_TORQUE"]["DRIVER_TORQUE"] ret.steeringTorqueEps = cp.vl["STEERING_TORQUE"]["MAIN_TORQUE"] ret.steeringPressed = self.update_steering_pressed(abs(ret.steeringTorque) > CCP.STEER_DRIVER_OVERRIDE, 5) # Disengagement on override is handled by the latch below, which also decides when # re-engagement is allowed, so no separate hard-disengage threshold here. ret.steeringDisengage = False # state 11 is a latched dropout: the command stream stopped while the EPS was actuating. # It clears only on a STEER_REQ rising edge over a continuous stream. lks_prepared = bool(cp.vl["STEERING_TORQUE"]["LKS_PREPARED"]) cruise_activated = bool(cp.vl["STEERING_TORQUE"]["CRUISE_ACTIVATED"]) self.eps_state = EPS_STATE_OFF + int(lks_prepared) + 2 * int(cruise_activated) ret.steerFaultTemporary = self.eps_state == EPS_STATE_LATCHED_FAULT ret.steerFaultTemporary |= int(cp_cam.vl["LKAS_HUD_ADAS"]["LKAS_STATE"]) == 4 ret.steerFaultPermanent = bool(cp.vl["STEERING_TORQUE"]["TORQUE_FAILED"]) # DRIVE_STATE.RAW_THROTTLE is powertrain torque demand, not the pedal ret.gasPressed = cp.vl["PEDAL"]["GAS_PEDAL"] > 0.10 ret.brake = cp.vl["PEDAL"]["BRAKE_PEDAL"] # must stay the same bit byd_rx_hook reads, or the two engage latches desync on a light # brake graze and controlsd raises "Controls Mismatch" ret.brakePressed = bool(cp.vl["DRIVE_STATE"]["BRAKE_PRESSED"]) ret.gearShifter = GEAR_MAP.get(int(cp.vl["DRIVE_STATE"]["GEAR"]), GearShifter.unknown) ret.leftBlinker = bool(cp.vl["STALKS"]["LEFT_BLINKER"]) ret.rightBlinker = bool(cp.vl["STALKS"]["RIGHT_BLINKER"]) ret.leftBlindspot = cp.vl["BSD_RADAR"]["LEFT_APPROACH"] != 0 ret.rightBlindspot = cp.vl["BSD_RADAR"]["RIGHT_APPROACH"] != 0 ret.doorOpen = any(( cp.vl["METER_CLUSTER"]["FRONT_LEFT_DOOR"], cp.vl["METER_CLUSTER"]["FRONT_RIGHT_DOOR"], cp.vl["METER_CLUSTER"]["BACK_LEFT_DOOR"], cp.vl["METER_CLUSTER"]["BACK_RIGHT_DOOR"], )) ret.seatbeltUnlatched = not bool(cp.vl["METER_CLUSTER"]["SEATBELT_DRIVER"]) # The ADAS/ACC ECU is on the chassis bus, not behind the camera relay, so these come off # bus 0. Bus 2 carries only the camera's own frames (0x1E2, 0x316, ...). This differs from # the Atto 3, where PR #3337 reads both from the camera bus. # CRUISE_STATE: 0=off, 1=available, 2=engaged, 3=engaged and commanding accel. # Do NOT use PR #3337/#3352's ACC_STATE (19|3) - byte 2 is a constant 0x3c on this car, so # it reads 7 (ERROR) forever and engagement can never happen. ret.cruiseState.speed = cp.vl["ACC_HUD_ADAS"]["SET_SPEED"] * CV.KPH_TO_MS cruise_state = int(cp.vl["ACC_HUD_ADAS"]["CRUISE_STATE"]) ret.cruiseState.available = cruise_state >= CRUISE_STATE_AVAILABLE # A steering override fully disengages and stays disengaged. Re-arming is deliberate: # either cycle stock cruise, or press the LKAS/ICC button. Suppressing cruiseState.enabled # is what holds it off, and clearing the latch gives the rising edge that re-engages. lkas_btn = bool(cp.vl["PCM_BUTTONS"]["LKAS_ON_BTN"]) lkas_rising = lkas_btn and not self.lkas_btn_prev self.lkas_btn_prev = lkas_btn # NOTE: latch on the instantaneous torque, not the debounced steeringPressed. byd_rx_hook # latches on the same raw sample, and any skew between the two shows up as controlsMismatch. if cruise_state < CRUISE_STATE_ENGAGED or lkas_rising: self.override_latched = False elif abs(ret.steeringTorque) > CCP.STEER_DRIVER_OVERRIDE: self.override_latched = True ret.cruiseState.enabled = cruise_state >= CRUISE_STATE_ENGAGED and not self.override_latched ret.cruiseState.standstill = bool(cp.vl["ACC_CMD"]["STANDSTILL_STATE"]) self.lkas_hud = copy.copy(cp_cam.vl["LKAS_HUD_ADAS"]) self.acc_cmd = copy.copy(cp.vl["ACC_CMD"]) self.buttons = copy.copy(cp.vl["PCM_BUTTONS"]) return ret, ret_iq @staticmethod def get_can_parsers(CP, CP_IQ): return { Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], 0), Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], [], 2), }