diff --git a/opendbc_repo/opendbc/car/gm/aol.py b/opendbc_repo/opendbc/car/gm/aol.py index 2d829b96eb..9ca4af1bc0 100644 --- a/opendbc_repo/opendbc/car/gm/aol.py +++ b/opendbc_repo/opendbc/car/gm/aol.py @@ -3,7 +3,7 @@ import math from opendbc.car.structs import CarParams -from opendbc.car.gm.values import (volt_cc_pedal_profile, control_flags, +from opendbc.car.gm.values import (malibu_hybrid_profile, volt_cc_pedal_profile, control_flags, camera_acc_pedal_profile, CAR, GMFlags, GMSafetyFlags, is_bolt_cc_profile, is_bolt_pedal_profile, is_ordinary_ascm_profile, is_ordinary_camera_profile, is_ordinary_sdgm_profile, is_bolt_euv_longitudinal, is_volt_gateway_profile, is_volt_cc_profile, is_silverado_cc_stock_profile, @@ -33,6 +33,7 @@ GM_AOL_WORDS = (frozenset(range(0xE600, 0xE608)) | frozenset(range(0xE610, 0xE61 0x5087, 0x5487, 0xC1D1, 0xC1D3, 0xC110, 0xC111, 0xC120, 0xC121, 0xC130, 0xC131, 0xC140, 0xC141, + 0xE800, 0xE801, 0xE802, 0xE803, 0xE804, 0xE805, 0xC150, 0xC151, 0xC160, 0xC161, 0xC162, 0xC170, 0xC171, 0xC172, 0xC173, 0xC180, 0xC181, 0xC182, 0xC183, 0xC184, 0xC185, 0xC186, 0xC187, )) @@ -68,7 +69,7 @@ def qualified_gm(cp) -> bool: cp.carFingerprint == CAR.BUICK_LACROSSE and cp.openpilotLongitudinalControl)) if is_ordinary_camera_profile(cp, longitudinal=cp.openpilotLongitudinalControl): return True - if is_silverado_cc_stock_profile(cp) or is_ordinary_cc_profile(cp) or is_conventional_cc_pedal_profile(cp): + if is_silverado_cc_stock_profile(cp) or is_ordinary_cc_profile(cp) or is_conventional_cc_pedal_profile(cp) or malibu_hybrid_profile(cp) is not None: return True if is_ordinary_sdgm_profile(cp, longitudinal=False) or is_ordinary_sdgm_profile(cp, longitudinal=True): return True diff --git a/opendbc_repo/opendbc/car/gm/carcontroller.py b/opendbc_repo/opendbc/car/gm/carcontroller.py index 0e485faa9a..5ba88fd356 100644 --- a/opendbc_repo/opendbc/car/gm/carcontroller.py +++ b/opendbc_repo/opendbc/car/gm/carcontroller.py @@ -30,6 +30,8 @@ from opendbc.car.gm.values import (DBC, CanBus, CarControllerParams, CruiseButto CC_GATEWAY_STOCK_CAR, uses_camera_stock_controls, CAR, BOLT_CC_WORDS, is_bolt_cc_profile) from opendbc.car.interfaces import CarControllerBase +from opendbc.car.gm.hybrid_cc import HybridPedalCommand, rearm_ready as hybrid_rearm_ready, lateral_ready as hybrid_lateral_ready +from opendbc.car.gm.values import malibu_hybrid_profile from opendbc.car.gm.bolt_cc import BoltCcOwner, BoltCcProfile, auxiliary_messages from opendbc.car.gm.volt_cc_pedal import VoltCcPedalCommand from opendbc.car.gm.long_tune import acc_tune_limits @@ -280,6 +282,8 @@ class CarController(CarControllerBase): self.lka_steering_cmd_counter = 0 self.lka_icon_status_last = (False, False) + self.hybrid_profile = malibu_hybrid_profile(self.CP) + self.hybrid_command = HybridPedalCommand(self.CP) if self.hybrid_profile is not None else None self.params = CarControllerParams(self.CP) self.volt_one_pedal_state = VoltOnePedal(CP, self.params) if is_volt_one_pedal(CP) else None self.volt_gateway_profile = is_volt_gateway_profile(self.CP) @@ -473,6 +477,9 @@ class CarController(CarControllerBase): (stock_cruise_fresh and CS.out.cruiseState.available and CS.out.cruiseState.enabled and not CS.out.brakePressed and not CS.out.gasPressed)) aol_lateral = aol_lateral_request(self.CP, CC) + if self.hybrid_profile is not None: + stock_steer_ready = hybrid_lateral_ready(CS, now_nanos) and (aol_lateral or + CS.out.cruiseState.enabled and not CS.out.brakePressed and not CS.out.gasPressed) if is_silverado_cc_stock_profile(self.CP): sources = CS.silverado_stock_sources stock_steer_ready = (CS.out.canValid and not CS.out.canTimeout and len(sources) == 8 and @@ -543,7 +550,8 @@ class CarController(CarControllerBase): # Steering (Active: 50Hz, inactive: 10Hz) steer_step = self.params.STEER_STEP if lat_active else self.params.INACTIVE_STEER_STEP - if (not self.bolt_pedal_removed and self.CP.networkLocation == NetworkLocation.fwdCamera and + if (not self.bolt_pedal_removed and not (self.hybrid_profile is not None and self.hybrid_profile.removed) and + self.CP.networkLocation == NetworkLocation.fwdCamera and not (self.conventional_pedal_profile and not self.silverado_cc_pedal_profile and self.CP.flags & GMFlags.NO_CAMERA)): # Also send at 50Hz: # - on startup, first few msgs are blocked @@ -561,7 +569,7 @@ class CarController(CarControllerBase): last_lka_steer_msg_ms = (now_nanos - CS.loopback_lka_steering_cmd_ts_nanos) * 1e-6 if (self.frame - self.last_steer_frame) >= steer_step and last_lka_steer_msg_ms > MIN_STEER_MSG_INTERVAL_MS: # Initialize ASCMLKASteeringCmd counter using the camera until we get a msg on the bus - if CS.loopback_lka_steering_cmd_ts_nanos == 0 and not self.bolt_pedal_removed: + if CS.loopback_lka_steering_cmd_ts_nanos == 0 and not self.bolt_pedal_removed and not (self.hybrid_profile is not None and self.hybrid_profile.removed): self.lka_steering_cmd_counter = CS.pt_lka_steering_cmd_counter + 1 if lat_active and not (self.ordinary_cc_profile and CC.enabled and not CS.out.cruiseState.enabled and not aol_lateral): @@ -575,7 +583,52 @@ class CarController(CarControllerBase): idx = self.lka_steering_cmd_counter % 4 can_sends.append(gmcan.create_steering_control(self.packer_pt, CanBus.POWERTRAIN, apply_torque, idx, lat_active)) - if self.volt_cc_pedal_profile is not None: + if self.hybrid_profile is not None: + profile = self.hybrid_profile + ready = hybrid_rearm_ready(CS, now_nanos) + sensor_ready = (not profile.pedal or CS.pedal_sensor_healthy and + 0 < CS.pedal_sensor_ts_nanos <= now_nanos <= CS.pedal_sensor_ts_nanos + 100_000_000) + if profile.pedal and profile.longitudinal and self.frame % 4 == 0: + if ready and sensor_ready and not CS.out.cruiseState.enabled: + pedal = self.hybrid_command.update( + actuators.accel, CC.enabled and CC.longActive, CS.out, + stopping=actuators.longControlState == LongCtrlState.stopping, resume=CC.cruiseControl.resume, + orientation=CC.orientationNED if self.long_pitch else None, acc_tune=self.gm_acc_tune) + else: + self.hybrid_command.withdraw() + pedal = 0. + self.apply_gas, self.apply_brake = pedal, 0 + can_sends.append(gmcan.create_pedal_command(self.packer_pt, pedal, (self.frame // 4) % 16)) + self.cancel_counter = self.cancel_counter + 1 if CC.cruiseControl.cancel else 0 + cancel = (CC.enabled and CS.out.cruiseState.enabled if profile.pedal and profile.longitudinal else + self.cancel_counter > CAMERA_CANCEL_DELAY_FRAMES) + slot = CS.hybrid_buttons.slot + if slot.credit_ns <= self.volt_cc_consumed_source_ns: + slot.credit_ns = 0 + action = None + interval_ns = 40_000_001 + authorized = hybrid_lateral_ready(CS, now_nanos) + if cancel: + action = "cancel" + elif (profile.longitudinal and not profile.pedal and CC.longActive and ready and + CS.out.vEgo >= self.CP.minEnableSpeed and self.frame % 4 == 0): + button, rate, self.apply_speed = ordinary_button_request( + CS.out.vEgo, CS.out.cruiseState.speed, actuators.accel, self.CP.minEnableSpeed, self.volt_cc_metric) + action = {CruiseButtons.RES_ACCEL: "resume", CruiseButtons.DECEL_SET: "set", CruiseButtons.CANCEL: "cancel"}.get(button) + interval_ns = int(rate * 1e9) + 1 + elif (profile.longitudinal and not profile.pedal and CC.enabled and CS.out.gasPressed and + self.frame % 52 == 0 and CC.hudControl.setSpeed > CS.out.vEgo > CS.out.cruiseState.speed and + hybrid_lateral_ready(CS, now_nanos) and not CS.out.brakePressed and not CS.out.regenBraking): + action, interval_ns = "gas_set", 520_000_000 + if action in ("cancel", "gas_set"): + authorized = authorized and CS.out.cruiseState.enabled + if action is not None: + raw = slot.expected(action, now_nanos, interval_ns=interval_ns, authorized=authorized) + if raw is not None and slot.accept(action, raw, now_nanos, interval_ns=interval_ns, authorized=authorized): + can_sends.append((0x1E1, raw, CanBus.POWERTRAIN)) + if profile.removed and profile.longitudinal and self.frame % 100 == 0: + can_sends += gmcan.create_adas_keepalive(CanBus.POWERTRAIN) + elif self.volt_cc_pedal_profile is not None: profile = self.volt_cc_pedal_profile credit = CS.conventional_cancel_credit sources = CS.volt_cc_pedal_sources @@ -970,7 +1023,7 @@ class CarController(CarControllerBase): self.volt_removed_cancel_credit_used = credit can_sends.append(gmcan.create_buttons(self.packer_pt, CanBus.CAMERA, CS.buttons_counter, CruiseButtons.CANCEL)) elif (not self.bolt_pedal_removed and self.volt_cc_pedal_profile is None and not self.volt_cc_profile and - not self.ordinary_cc_profile and not self.bolt_cc_profile and + self.hybrid_profile is None and not self.ordinary_cc_profile and not self.bolt_cc_profile and not self.volt_gateway_profile and not self.silverado_cc_pedal_profile): # While car is braking, cancel button causes ECM to enter a soft disable state with a fault status. # A delayed cancellation allows camera to cancel and avoids a fault when user depresses brake quickly diff --git a/opendbc_repo/opendbc/car/gm/carstate.py b/opendbc_repo/opendbc/car/gm/carstate.py index bf30cb72e7..d721a4c063 100644 --- a/opendbc_repo/opendbc/car/gm/carstate.py +++ b/opendbc_repo/opendbc/car/gm/carstate.py @@ -9,6 +9,8 @@ from opendbc.car.interfaces import CarStateBase from opendbc.car.gm.gmcan import pedal_crc from opendbc.car.gm.cc_longitudinal import VoltCcPhysical from opendbc.car.gm.ordinary_cc import PhysicalObservation +from opendbc.car.gm.hybrid_cc import HybridButtons +from opendbc.car.gm.values import malibu_hybrid_profile from opendbc.car.gm.conventional_pedal import CancelCredit from opendbc.car.gm.values import (DBC, AccState, CruiseButtons, STEER_THRESHOLD, SDGM_CAR, ALT_ACCS, ASCM_INTERCEPT_CAR, ORDINARY_ASCM_CAR, ORDINARY_SDGM_CAR, GMFlags, GMSafetyFlags, NO_ACC_BOLT_CAR, @@ -32,6 +34,9 @@ BUTTONS_DICT = {CruiseButtons.RES_ACCEL: ButtonType.accelCruise, CruiseButtons.D class CarState(CarStateBase): def __init__(self, CP): super().__init__(CP) + self.hybrid_profile = malibu_hybrid_profile(CP) + self.hybrid_buttons = HybridButtons() + self.hybrid_sources = () self.gm_auto_hold_config = auto_hold_config_for(CP) self.camera_pedal_profile = camera_acc_pedal_profile(CP) self.camera_pedal_sources = () @@ -166,6 +171,16 @@ class CarState(CarStateBase): self.silverado_brake_analog = pt_cp.vl[analog[0]][analog[1]] / (208. if self.CP.flags & GMFlags.NO_ACCELERATOR_POS_MSG else 1.) self.pt_lka_steering_cmd_counter = pt_cp.vl["ASCMLKASteeringCmd"]["RollingCounter"] + if self.hybrid_profile is not None: + fields = (("PSCMStatus", "LKATorqueDelivered", 300_000_000), + ("EBCMWheelSpdRear", "RLWheelSpd", 100_000_000), + ("ASCMSteeringButton", "RollingCounter", 100_000_000), + ("AcceleratorPedal2", "CruiseState", 300_000_000), + ("ECMEngineStatus", "CruiseMainOn", 300_000_000), + ("ECMCruiseControl", "CruiseActive", 300_000_000), + ("ECMPRDNL2", "PRNDL2", 1_000_000_000), + ("EBCMRegenPaddle", "RegenPaddle", 100_000_000)) + self.hybrid_sources = tuple((pt_cp.ts_nanos[name][signal], limit) for name, signal, limit in fields) prev_cruise_buttons = self.cruise_buttons prev_distance_button = self.distance_button self.cruise_buttons = pt_cp.vl["ASCMSteeringButton"]["ACCButtons"] @@ -335,7 +350,8 @@ class CarState(CarStateBase): if (self.CP.networkLocation == NetworkLocation.fwdCamera and not (is_conventional_cc_pedal_profile(self.CP) and self.CP.flags & GMFlags.NO_CAMERA) and not (self.camera_pedal_profile is not None and self.camera_pedal_profile.removed) and - not (self.volt_cc_pedal_profile is not None and self.volt_cc_pedal_profile.removed) and not removed_pedal): + not (self.volt_cc_pedal_profile is not None and self.volt_cc_pedal_profile.removed) and + not (self.hybrid_profile is not None and self.hybrid_profile.removed) and not removed_pedal): if not is_conventional_cc_pedal_profile(self.CP): self.pt_lka_steering_cmd_counter = pt_cp.vl["ASCMLKASteeringCmd"]["RollingCounter"] if not self.bolt_cc_removed and not is_volt_camera_removed(self.CP) and not is_ordinary_camera_removed(self.CP): @@ -396,7 +412,9 @@ class CarState(CarStateBase): ret.gasPressed = pt_cp.vl["AcceleratorPedal2"]["AcceleratorPedal2"] / 254. > 1e-5 - if (self.CP.flags & GMFlags.PEDAL_LONG.value and not (removed_pedal and not self.CP.openpilotLongitudinalControl) and + if (self.CP.flags & GMFlags.PEDAL_LONG.value and + not (self.hybrid_profile is not None and not self.hybrid_profile.longitudinal) and + not (removed_pedal and not self.CP.openpilotLongitudinalControl) and (self.camera_pedal_profile is None or self.camera_pedal_profile.longitudinal) and (self.volt_cc_pedal_profile is None or self.volt_cc_pedal_profile.longitudinal)): sensor = pt_cp.vl["GAS_SENSOR"] @@ -469,6 +487,13 @@ class CarState(CarStateBase): ret.cruiseState.speed = pt_cp.vl["ECMCruiseControl"]["CruiseSetSpeed"] * CV.KPH_TO_MS ret.cruiseState.nonAdaptive = not (is_silverado_cc_stock_profile(self.CP) or is_ordinary_cc_profile(self.CP) or is_conventional_cc_pedal_profile(self.CP)) + if self.hybrid_profile is not None: + ret.accFaulted = False + ret.cruiseState.enabled = bool(pt_cp.vl["ECMCruiseControl"]["CruiseActive"]) + ret.cruiseState.speed = pt_cp.vl["ECMCruiseControl"]["CruiseSetSpeed"] * CV.KPH_TO_MS + ret.cruiseState.nonAdaptive = False + if not self.hybrid_profile.removed: + ret.stockAeb = cam_cp.vl["AEBCmd"]["AEBCmdActive"] != 0 if self.CP.carFingerprint == CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL and self.CP.flags & GMFlags.PEDAL_LONG.value: self.stock_acc_status_ts_nanos = pt_cp.ts_nanos["AcceleratorPedal2"]["CruiseState"] if self.CP.carFingerprint in NO_ACC_BOLT_CAR and not self.bolt_cc_profile: @@ -482,7 +507,7 @@ class CarState(CarStateBase): pt_cp.vl["AcceleratorPedal2"]["CruiseState"] == AccState.STANDSTILL) else: ret.cruiseState.standstill = False - if (not removed_pedal and self.volt_cc_pedal_profile is None and self.CP.networkLocation == NetworkLocation.fwdCamera + if (self.hybrid_profile is None and not removed_pedal and self.volt_cc_pedal_profile is None and self.CP.networkLocation == NetworkLocation.fwdCamera and not is_volt_camera_removed(self.CP) and not is_conventional_cc_pedal_profile(self.CP) and not is_ordinary_camera_removed(self.CP) and not (self.camera_pedal_profile is not None and self.camera_pedal_profile.removed)): @@ -565,6 +590,27 @@ class CarState(CarStateBase): {1: ButtonType.gapAdjustCruise}) ] + if self.hybrid_profile is not None: + now = pt_cp._last_update_nanos + healthy = (pt_cp.can_valid and not pt_cp.bus_timeout and + (self.hybrid_profile.removed or cam_cp.can_valid and not cam_cp.bus_timeout) and + ret.cruiseState.available and + ret.gearShifter in (structs.CarState.GearShifter.drive, structs.CarState.GearShifter.low, + structs.CarState.GearShifter.manumatic) and + not ret.steerFaultTemporary and not ret.steerFaultPermanent and + all(stamp > 0 and 0 <= now - stamp <= limit for stamp, limit in self.hybrid_sources)) + if self.hybrid_profile.pedal and self.hybrid_profile.longitudinal: + healthy = (healthy and self.pedal_sensor_healthy and + 0 < self.pedal_sensor_ts_nanos <= now <= self.pedal_sensor_ts_nanos + 100_000_000) + _, semantic, edges = self.hybrid_buttons.update( + healthy, enable_ready=healthy and not ret.brakePressed and not ret.gasPressed and not ret.regenBraking) + from opendbc.car.gm.hybrid_cc import HybridPhysical + self.volt_cc_physical = HybridPhysical(now, tuple(stamp for stamp, _ in self.hybrid_sources), + self.hybrid_buttons.slot.credit_ns) + self.cruise_buttons = CruiseButtons.INIT if semantic is None else semantic + ret.buttonEvents = [event for old, new in edges for event in + create_button_events(new, old, BUTTONS_DICT, unpressed_btn=CruiseButtons.UNPRESS)] + create_button_events( + self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise}) if ret.vEgo < self.CP.minSteerSpeed: ret.lowSpeedAlert = True @@ -718,6 +764,18 @@ class CarState(CarStateBase): if profile.longitudinal: pt_messages.append(("GAS_SENSOR", 50)) + hybrid = malibu_hybrid_profile(CP) + if hybrid is not None: + pt_messages = [("PSCMStatus", 10), ("ESPStatus", 10), ("EBCMWheelSpdFront", 20), + ("EBCMWheelSpdRear", 20), ("EBCMFrictionBrakeStatus", 20), ("PSCMSteeringAngle", 100), + ("ECMPRDNL2", 10), ("AcceleratorPedal2", 33), ("ECMEngineStatus", 100), + ("BCMTurnSignals", 1), ("BCMDoorBeltStatus", 10), ("BCMGeneralPlatformStatus", 10), + ("ASCMSteeringButton", 33), ("ECMCruiseControl", 10), ("EBCMRegenPaddle", 50), + ("EBCMBrakePedalPosition", 100) if CP.flags & GMFlags.NO_ACCELERATOR_POS_MSG else ("ECMAcceleratorPos", 80)] + if hybrid.pedal and hybrid.longitudinal: + pt_messages.append(("GAS_SENSOR", 50)) + if not hybrid.removed: + pt_messages.append(("ASCMLKASteeringCmd", float("nan"))) loopback_messages = [ ("ASCMLKASteeringCmd", float('nan')), ] @@ -762,6 +820,9 @@ class CarState(CarStateBase): cam_messages = [] if cc_profile.removed else [("ASCMLKASteeringCmd", 10), ("AEBCmd", 10)] if is_bolt_pedal_removed_profile(CP) or is_bolt_pedal_removed_profile(CP, stock_only=True): pt_messages = [(name, frequency) for name, frequency in pt_messages if name != "ASCMLKASteeringCmd"] + if hybrid is not None: + cam_messages = [] if hybrid.removed else [("ASCMLKASteeringCmd", 10), ("AEBCmd", 10)] + return { Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, 0), Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], cam_messages, 2), diff --git a/opendbc_repo/opendbc/car/gm/feature_capabilities.py b/opendbc_repo/opendbc/car/gm/feature_capabilities.py index a33bb0d5fd..179a1c4373 100644 --- a/opendbc_repo/opendbc/car/gm/feature_capabilities.py +++ b/opendbc_repo/opendbc/car/gm/feature_capabilities.py @@ -2,7 +2,8 @@ from opendbc.car.gm.lateral import lane_centering_supported from opendbc.car.gm.suburban import stopping_decel_rate -from opendbc.car.gm.values import (camera_acc_pedal_profile, control_flags, CAR, GMFlags, GMSafetyFlags, is_bolt_cc_profile, is_bolt_euv_longitudinal, +from opendbc.car.gm.values import (malibu_hybrid_profile, camera_acc_pedal_profile, control_flags, CAR, GMFlags, GMSafetyFlags, + is_bolt_cc_profile, is_bolt_euv_longitudinal, is_bolt_pedal_profile, is_volt_cc_longitudinal, is_volt_longitudinal, uses_camera_stock_controls, is_ordinary_ascm_profile, is_ordinary_camera_profile, is_ordinary_sdgm_profile, is_ordinary_cc_profile, is_conventional_cc_pedal_profile, volt_cc_pedal_profile) @@ -20,7 +21,7 @@ def longitudinal_supported(cp) -> bool: if not _healthy(cp) or not cp.openpilotLongitudinalControl or cp.pcmCruise: return False return (volt_cc_pedal_profile(cp) is not None or camera_acc_pedal_profile(cp) is not None or is_ordinary_camera_profile(cp, longitudinal=True) or - is_ordinary_cc_profile(cp) or is_conventional_cc_pedal_profile(cp) or + is_ordinary_cc_profile(cp) or is_conventional_cc_pedal_profile(cp) or malibu_hybrid_profile(cp) is not None or is_ordinary_ascm_profile(cp, longitudinal=True) or is_ordinary_sdgm_profile(cp, longitudinal=True) or is_volt_longitudinal(cp) or is_volt_cc_longitudinal(cp) or is_bolt_euv_longitudinal(cp) or is_bolt_pedal_profile(cp) or diff --git a/opendbc_repo/opendbc/car/gm/fingerprints.py b/opendbc_repo/opendbc/car/gm/fingerprints.py index ca3a99f17e..03c6244153 100644 --- a/opendbc_repo/opendbc/car/gm/fingerprints.py +++ b/opendbc_repo/opendbc/car/gm/fingerprints.py @@ -7,6 +7,7 @@ from opendbc.car.gm.values import CAR FINGERPRINTS = { + CAR.CHEVROLET_MALIBU_HYBRID_CC: [{193: 8, 197: 8, 201: 8, 209: 7, 211: 2, 241: 6, 249: 8, 352: 5, 386: 8, 451: 8, 452: 8, 453: 6, 481: 7, 485: 8, 489: 8, 493: 8, 500: 6, 560: 8, 562: 8, 566: 6, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 707: 8, 717: 5, 761: 7, 810: 8, 840: 5, 842: 5, 844: 8, 869: 4}], CAR.HOLDEN_ASTRA: [{ 190: 8, 193: 8, 197: 8, 199: 4, 201: 8, 209: 7, 211: 8, 241: 6, 249: 8, 288: 5, 298: 8, 304: 1, 309: 8, 311: 8, 313: 8, 320: 3, 328: 1, 352: 5, 381: 6, 384: 4, 386: 8, 388: 8, 393: 8, 398: 8, 401: 8, 413: 8, 417: 8, 419: 8, 422: 1, 426: 7, 431: 8, 442: 8, 451: 8, 452: 8, 453: 8, 455: 7, 456: 8, 458: 5, 479: 8, 481: 7, 485: 8, 489: 8, 497: 8, 499: 3, 500: 8, 501: 8, 508: 8, 528: 5, 532: 6, 554: 3, 560: 8, 562: 8, 563: 5, 564: 5, 565: 5, 567: 5, 647: 5, 707: 8, 715: 8, 723: 8, 753: 5, 761: 7, 806: 1, 810: 8, 840: 5, 842: 5, 844: 8, 866: 4, 961: 8, 969: 8, 977: 8, 979: 8, 985: 5, 1001: 8, 1009: 8, 1011: 6, 1017: 8, 1019: 3, 1020: 8, 1105: 6, 1217: 8, 1221: 5, 1225: 8, 1233: 8, 1249: 8, 1257: 6, 1259: 8, 1261: 7, 1263: 4, 1265: 8, 1267: 8, 1280: 4, 1300: 8, 1328: 4, 1417: 8, 1906: 7, 1907: 7, 1908: 7, 1912: 7, 1919: 7 }], diff --git a/opendbc_repo/opendbc/car/gm/hybrid_cc.py b/opendbc_repo/opendbc/car/gm/hybrid_cc.py new file mode 100644 index 0000000000..f31e4f254e --- /dev/null +++ b/opendbc_repo/opendbc/car/gm/hybrid_cc.py @@ -0,0 +1,272 @@ +"""Exact Hybrid button bytes; caller owns physical and axis authorization.""" +from dataclasses import dataclass + +NEUTRAL = (0x10FF, 0x15EE, 0x1ADD, 0x1FCC) +RESUME = (0x55AE, 0x5F8C, 0x6F7C, 0x659E) +CANCEL = (0x60AF, 0x659E, 0x6A8D, 0x6F7C) +UNIQUE_NEUTRAL = {0x15EE: 1, 0x1FCC: 3} +TTL_NS = 100_000_000 + + +def custom_bytes(action: str, phase: int, prefix: int) -> bytes: + if action not in ('resume', 'cancel') or not 0 <= phase < 4 or prefix not in (1, 0x41): + raise ValueError('Invalid Hybrid button tuple') + word = (RESUME if action == 'resume' else CANCEL)[phase] + return bytes((0, 0, 0, prefix, 0, word >> 8, word & 255)) + + +def standard_set_bytes(counter: int) -> bytes: + if not 0 <= counter < 4: + raise ValueError('Invalid standard counter') + checksum = 0xFF + counter * 0x4EF - 0x20 + return bytes((0, 0, 0, 1, counter, 0x30 | (checksum >> 8), checksum & 255)) + + +@dataclass +class PhysicalSlot: + phase: int | None = None + last_admitted_phase: int | None = None + needs_requalification: bool = False + prefix: int = 1 + counter: int = 0 + observed_ns: int = 0 + credit_ns: int = 0 + last_tx_ns: int = 0 + + def clear(self): + self.phase = None + self.credit_ns = 0 + + def reset(self): + self.clear() + self.last_admitted_phase = None + self.needs_requalification = False + self.observed_ns = 0 + self.last_tx_ns = 0 + + def requalify(self, *, physical_ready: bool) -> bool: + if not self.needs_requalification or not physical_ready: + return False + self.clear() + self.last_admitted_phase = None + self.needs_requalification = False + # Keep observation monotonicity and accepted TX cadence across source recovery. + return True + + def observe(self, stamp: int, raw: bytes, *, bus: int): + if stamp <= 0 or stamp <= self.observed_ns: + return + if self.observed_ns and stamp - self.observed_ns > TTL_NS: + self.needs_requalification = True + self.observed_ns = stamp + self.clear() + if self.needs_requalification: + return + if bus != 0 or len(raw) != 7 or raw[:3] != bytes(3) or raw[3] not in (1, 0x41) or raw[4] > 3: + return + phase = UNIQUE_NEUTRAL.get(raw[5] * 256 + raw[6]) + if phase is None or phase == self.last_admitted_phase: + return + self.last_admitted_phase = phase + self.phase, self.prefix, self.counter = phase, raw[3], raw[4] + self.credit_ns = stamp + + def expected(self, action: str, now: int, *, interval_ns: int, authorized: bool) -> bytes | None: + if self.credit_ns > 0 and now - self.credit_ns > TTL_NS: + self.clear() + self.needs_requalification = True + if (not authorized or self.needs_requalification or self.phase is None or self.credit_ns <= 0 or + not 0 <= now - self.credit_ns <= TTL_NS or interval_ns < 0 or + self.last_tx_ns and now - self.last_tx_ns < interval_ns): + return None + if action == 'resume': + return custom_bytes(action, self.phase, self.prefix) + if action == 'cancel': + return custom_bytes(action, (self.phase + 1) % 4, self.prefix) + if action == 'set': + return standard_set_bytes((self.counter + 1) % 4) + if action == 'gas_set': + return standard_set_bytes(0) + return None + + def accept(self, action: str, raw: bytes, now: int, *, interval_ns: int, authorized: bool) -> bool: + expected = self.expected(action, now, interval_ns=interval_ns, authorized=authorized) + if expected is None or raw != expected: + return False + self.credit_ns = 0 + self.last_tx_ns = now + return True + + +def physical_semantic(raw: bytes, *, bus: int) -> str | None: + """Exact owner decoding; custom packets never fall through generic high-nibble buttons.""" + if bus != 0 or len(raw) != 7 or raw[:3] != bytes(3) or raw[3] not in (1, 0x41) or raw[4] > 3: + return None + word = int.from_bytes(raw[5:7], 'big') + unique = {0x15EE: 'neutral', 0x1FCC: 'neutral', 0x55AE: 'resume', 0x5F8C: 'resume', + 0x2ACD: 'main', 0x20EF: 'main', 0x60AF: 'cancel', 0x6A8D: 'cancel'} + if word in unique: + return unique[word] + if raw == standard_set_bytes(raw[4]): + return 'set' + return None + + +@dataclass(frozen=True) +class HybridPhysical: + observed_ns: int + source_ns: tuple[int, ...] + button_credit_ns: int + + +class HybridButtons: + def __init__(self): + self.slot = PhysicalSlot() + self.semantic = None + self.reset_pending = False + self.packets = [] + + def observe_packets(self, packets): + self.packets = [(stamp, bytes(raw), bus) for stamp, frames in packets + for address, raw, bus in frames if address == 0x1E1] + + def update(self, physical_ready, *, enable_ready=None): + if enable_ready is None: + enable_ready = physical_ready + mapping = {'neutral': 1, 'resume': 2, 'set': 3, 'main': 5, 'cancel': 6} + previous = self.semantic + edges = [] + if not physical_ready: + self.slot.clear() + self.slot.needs_requalification = True + self.semantic = None + self.reset_pending = True + elif not enable_ready: + self.semantic = None + self.reset_pending = True + for stamp, raw, bus in self.packets: + old_stamp = self.slot.observed_ns + self.slot.observe(stamp, raw, bus=bus) + if stamp <= old_stamp or bus != 0: + continue + if self.slot.needs_requalification: + self.semantic = None + self.reset_pending = True + if physical_ready and enable_ready: + self.slot.requalify(physical_ready=True) + continue + if not physical_ready or not enable_ready: + continue + semantic = physical_semantic(raw, bus=bus) + if semantic is None: + continue + value = mapping[semantic] + if self.semantic is None: + self.semantic = value + self.reset_pending = value != 1 + continue + if self.reset_pending: + if value == 1: + self.semantic = 1 + self.reset_pending = False + continue + if value != self.semantic: + edges.append((self.semantic, value)) + self.semantic = value + self.packets = [] + return previous, self.semantic, edges + + +def sources_current(cs, now_ns): + sources = cs.hybrid_sources + profile = cs.hybrid_profile + sensor_current = (not (profile.pedal and profile.longitudinal) or + cs.pedal_sensor_healthy and 0 < cs.pedal_sensor_ts_nanos <= now_ns <= cs.pedal_sensor_ts_nanos + TTL_NS) + return bool(sensor_current and cs.out.canValid and not cs.out.canTimeout and sources and + all(stamp > 0 and 0 <= now_ns - stamp <= limit for stamp, limit in sources)) + + +def lateral_ready(cs, now_ns): + from opendbc.car.structs import CarState + return (sources_current(cs, now_ns) and cs.out.cruiseState.available and + cs.out.gearShifter in (CarState.GearShifter.drive, CarState.GearShifter.low, CarState.GearShifter.manumatic) and + not cs.out.steerFaultTemporary and not cs.out.steerFaultPermanent) + + +def rearm_ready(cs, now_ns): + return (lateral_ready(cs, now_ns) and not cs.out.brakePressed and + not cs.out.gasPressed and not cs.out.regenBraking) + +class HybridPedalCommand: + def __init__(self, cp): + self.cp = cp + self.steady = 0. + self.active_last = False + self.recovery = False + + def withdraw(self): + self.steady = 0. + self.active_last = True + self.recovery = True + + def update(self, accel, active, cs, *, stopping, resume, orientation, acc_tune=False): + import math + import numpy as np + from opendbc.car import ACCELERATION_DUE_TO_GRAVITY + from opendbc.car.gm.silverado_cc import pedal_fraction, pedal_slew + from opendbc.car.gm.long_tune import acc_tune_limits + if not active: + self.steady = 0. + self.active_last = False + self.recovery = False + return 0. + if cs.vEgo < .25 and stopping and not resume: + return 0. # Original outer branch skips calc and retains its state. + target = pedal_fraction(accel, cs.vEgo) + self.steady = pedal_slew(target, self.steady, accel, cs.vEgo) if self.active_last else target + self.active_last = True + pitch = 0. + if orientation is not None and len(orientation) == 3 and cs.vEgo > .25 and math.isfinite(orientation[1]): + pitch = math.sin(orientation[1]) * ACCELERATION_DUE_TO_GRAVITY + pitch = 0. if pitch > 0. and accel > 0. else min(pitch, .20) + radius = .075 * self.cp.wheelbase + .1453 + drag = .5 * .30 * (1.05 * self.cp.wheelbase + .0679) * 1.225 * cs.vEgo ** 2 + switch = float(np.interp(cs.vEgo, [.5, 10.], [6150, 5500])) + maximum = 2. + if acc_tune: + maximum, switch = acc_tune_limits(cs.vEgo, maximum, switch, 6150) + scaled = radius * (self.cp.mass * float(np.clip(accel + pitch, -4., maximum)) + drag) + 6150 + gas = int(round(np.clip(scaled, 5500, 8191))) + brake = int(round(np.interp(min((scaled - switch) / (radius * self.cp.mass), 0.), [-4., -1.], [400., 0.]))) + if brake > 0 or stopping: + gas = 5500 + command = self.steady + if gas > 5500 and cs.cruiseState.standstill and (cs.standstill or cs.vEgo < .3): + command = 18. / 255. + if self.recovery: + command = min(command, pedal_slew(command, 0., accel, cs.vEgo)) + self.steady = command + self.recovery = False + return command + + +def policy_for(cp): + from opendbc.car.gm.values import malibu_hybrid_profile + from opendbc.car.gm.longitudinal import GMOrdinaryLongitudinalPolicy, _GMDefaultStopPolicy + from opendbc.car.gm.cc_longitudinal import VoltCcEvidence + import numpy as np + profile = malibu_hybrid_profile(cp) + if profile is None or not profile.longitudinal: + return None + + class HybridPolicy(GMOrdinaryLongitudinalPolicy): + stopping_decel_rate = float(np.float32(.8)) if profile.pedal else 1. + ignore_cruise_standstill = profile.pedal + kp = ((0., 5., 15., 35.), tuple(float(np.float32(v)) for v in (.095, .085, .065, .05))) if profile.pedal else ((0.,), (0.,)) + + def stop_policy(self): + return _GMDefaultStopPolicy(.25, VoltCcEvidence) + + def feedforward(self, target, speed, last_output): + return target * float(np.float32(.2) if profile.pedal else 1.) + return HybridPolicy() diff --git a/opendbc_repo/opendbc/car/gm/interface.py b/opendbc_repo/opendbc/car/gm/interface.py index b4d3b1b880..e423676b2c 100755 --- a/opendbc_repo/opendbc/car/gm/interface.py +++ b/opendbc_repo/opendbc/car/gm/interface.py @@ -17,6 +17,7 @@ from opendbc.car.gm.values import (volt_cc_pedal_profile, CAR, CarControllerPara is_silverado_cc_pedal_profile, is_conventional_cc_pedal_profile, CAMERA_STOCK_CAR, ORDINARY_CAMERA_CAR, ORDINARY_CAMERA_ALPHA_CAR, camera_acc_pedal_profile, VOLT_BSM_CAR, BOLT_CC_WORDS, is_bolt_cc_profile, BOLT_PEDAL_REMOVED_WORDS, is_bolt_pedal_removed_profile) +from opendbc.car.gm.values import malibu_hybrid_profile, MALIBU_HYBRID_SOURCES from opendbc.car.interfaces import CarInterfaceBase, TorqueFromLateralAccelCallbackType, LateralAccelFromTorqueCallbackType TransmissionType = structs.CarParams.TransmissionType @@ -42,6 +43,8 @@ class CarInterface(CarInterfaceBase): return super().get_params(pedal_candidate(candidate, fingerprint), fingerprint, car_fw, alpha_long, is_release, docs) def update(self, can_packets): + if malibu_hybrid_profile(self.CP) is not None: + self.CS.hybrid_buttons.observe_packets(can_packets) if (is_bolt_pedal_removed_profile(self.CP) or is_bolt_pedal_removed_profile(self.CP, stock_only=True)): self.CS.conventional_cancel_credit.observe( can_packets, clear_on_main_off=True, @@ -123,7 +126,8 @@ class CarInterface(CarInterfaceBase): if (profile is not None and profile.longitudinal) or (volt_cc_pedal_profile(CP) is not None and CP.openpilotLongitudinalControl): return (float(np.interp(current_speed, [0., 1.5, 4., 8., 15., 30.], [-.95, -1.3, -1.85, -2.3, -2.6, -2.8])), float(np.interp(current_speed, [0., 1.5, 4., 8., 15.], [.60, .85, 1.15, 1.60, 2.]))) - if is_silverado_cc_pedal_profile(CP): + hybrid = malibu_hybrid_profile(CP) + if is_silverado_cc_pedal_profile(CP) or hybrid is not None and hybrid.pedal and hybrid.longitudinal: return (float(np.interp(current_speed, [0., 1.5, 4., 8., 15., 30.], [-.95, -1.3, -1.85, -2.3, -2.6, -2.8])), float(np.interp(current_speed, [0., 1.5, 4., 8., 15.], [.60, .85, 1.15, 1.60, 2.]))) if CP.flags & GMFlags.PEDAL_LONG.value: @@ -816,6 +820,32 @@ class CarInterface(CarInterfaceBase): ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.gm if admitted else structs.CarParams.SafetyModel.noOutput)] if admitted: ret.safetyConfigs[0].safetyParam = MALIBU_CC_F1_WORD + if candidate == CAR.CHEVROLET_MALIBU_HYBRID_CC: + pt, cam = fingerprint[CanBus.POWERTRAIN], fingerprint[CanBus.CAMERA] + pedal, removed, alternate = 0x201 in pt, 0x320 not in cam, 0xBE not in pt + admitted = (not docs and all(pt.get(address) == length for address, length in MALIBU_HYBRID_SOURCES.items()) and + (pt.get(0xF1) == 6 if alternate else pt.get(0xBE) in (6, 7, 8)) and + (not pedal or pt.get(0x201) == 6) and (removed or cam.get(0x180) == 4 and cam.get(0x320) == 6)) + ret.networkLocation = NetworkLocation.fwdCamera + ret.transmissionType = TransmissionType.direct + ret.alphaLongitudinalAvailable = False + ret.radarUnavailable = not pedal or RADAR_HEADER_MSG not in fingerprint[CanBus.OBSTACLE] + ret.flags = int(GMFlags.CC_LONG | (GMFlags.PEDAL_LONG if pedal else 0) | + (GMFlags.NO_CAMERA if removed else 0) | (GMFlags.NO_ACCELERATOR_POS_MSG if alternate else 0)) + ret.openpilotLongitudinalControl, ret.pcmCruise = admitted, not admitted + ret.dashcamOnly = not admitted + ret.autoResumeSng = pedal and admitted + ret.minEnableSpeed = -1. if pedal else 24 * CV.MPH_TO_MS + ret.minSteerSpeed = 10 * CV.KPH_TO_MS + ret.steerActuatorDelay = .2 + ret.longitudinalActuatorDelay = .5 + ret.stopAccel = -.25 + ret.longitudinalTuning.kiBP, ret.longitudinalTuning.kiV = [5., 35.], [.5, .5] + if pedal: + ret.longitudinalTuning.kiBP, ret.longitudinalTuning.kiV = [0., 3., 6., 35.], [.07, .10, .15, .24] + CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) + ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.gm if admitted else structs.CarParams.SafetyModel.noOutput, + 0xE800 + 2 * int(pedal) + int(removed) if admitted else 0)] if 0x142 in fingerprint[CanBus.POWERTRAIN] or candidate in VOLT_BSM_CAR: ret.flags |= GMFlags.HAS_BSM.value return ret diff --git a/opendbc_repo/opendbc/car/gm/lateral.py b/opendbc_repo/opendbc/car/gm/lateral.py index 108249790f..73f9d152fa 100644 --- a/opendbc_repo/opendbc/car/gm/lateral.py +++ b/opendbc_repo/opendbc/car/gm/lateral.py @@ -2,7 +2,7 @@ from opendbc.car.gm.values import gm_control_word, camera_acc_pedal_profile, vol """Exact finalized GM configurations for shared lateral preferences.""" import math from opendbc.car.structs import CarParams -from opendbc.car.gm.values import (control_flags, CAR, GMSafetyFlags, is_volt_gateway_profile, +from opendbc.car.gm.values import (malibu_hybrid_profile, control_flags, CAR, GMSafetyFlags, is_volt_gateway_profile, is_volt_ascm_longitudinal, is_volt_cc_profile, is_volt_sdgm_profile, is_volt_camera_removed, is_ordinary_ascm_profile, is_ordinary_camera_profile, is_ordinary_sdgm_profile, is_ordinary_cc_profile, is_conventional_cc_pedal_profile) @@ -24,7 +24,7 @@ def lane_centering_supported(cp) -> bool: return False if camera_acc_pedal_profile(cp) is not None or is_ordinary_camera_profile(cp, longitudinal=cp.openpilotLongitudinalControl): return True - if is_ordinary_cc_profile(cp) or is_conventional_cc_pedal_profile(cp): + if is_ordinary_cc_profile(cp) or is_conventional_cc_pedal_profile(cp) or malibu_hybrid_profile(cp) is not None: return True if (is_ordinary_ascm_profile(cp, longitudinal=cp.openpilotLongitudinalControl) or is_ordinary_sdgm_profile(cp, longitudinal=cp.openpilotLongitudinalControl)): diff --git a/opendbc_repo/opendbc/car/gm/long_tune.py b/opendbc_repo/opendbc/car/gm/long_tune.py index 53afd85f40..b51f16fa7d 100644 --- a/opendbc_repo/opendbc/car/gm/long_tune.py +++ b/opendbc_repo/opendbc/car/gm/long_tune.py @@ -14,7 +14,7 @@ VOLT_TUNE_CARS = frozenset((CAR.CHEVROLET_VOLT, CAR.CHEVROLET_VOLT_2019, CONFIGURABLE_ACC_TUNE_CARS = (ORDINARY_ASCM_CAR | ORDINARY_CAMERA_CAR | ORDINARY_SDGM_CAR | GM_BASE_GATEWAY_IDS | NO_ACC_BOLT_CAR | frozenset((CAR.CHEVROLET_BOLT_EUV, - CAR.CHEVROLET_BOLT_ACC_2022_2023, CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL))) + CAR.CHEVROLET_BOLT_ACC_2022_2023, CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL, CAR.CHEVROLET_MALIBU_HYBRID_CC))) def tune_options(cp): if not longitudinal_supported(cp): diff --git a/opendbc_repo/opendbc/car/gm/ordinary_cc.py b/opendbc_repo/opendbc/car/gm/ordinary_cc.py index 62f6ffe8db..abbcb0c5f5 100644 --- a/opendbc_repo/opendbc/car/gm/ordinary_cc.py +++ b/opendbc_repo/opendbc/car/gm/ordinary_cc.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from opendbc.car.common.conversions import Conversions as CV -from opendbc.car.gm.values import CAR, CruiseButtons, is_ordinary_cc_profile, is_volt_cc_longitudinal +from opendbc.car.gm.values import CAR, CruiseButtons, is_ordinary_cc_profile, is_volt_cc_longitudinal, malibu_hybrid_profile from opendbc.car.gm.longitudinal import GMOrdinaryLongitudinalPolicy, _GMDefaultStopPolicy from opendbc.car.gm.cc_longitudinal import VoltCcEvidence @@ -99,4 +99,4 @@ def policy_for(cp): def control_transport_required(cp): - return is_ordinary_cc_profile(cp) or is_volt_cc_longitudinal(cp) + return is_ordinary_cc_profile(cp) or is_volt_cc_longitudinal(cp) or malibu_hybrid_profile(cp) is not None diff --git a/opendbc_repo/opendbc/car/gm/startup_preferences.py b/opendbc_repo/opendbc/car/gm/startup_preferences.py index a9fc66b3aa..bd6b6f13aa 100644 --- a/opendbc_repo/opendbc/car/gm/startup_preferences.py +++ b/opendbc_repo/opendbc/car/gm/startup_preferences.py @@ -9,6 +9,9 @@ from opendbc.car.gm.values import (is_volt_ascm_longitudinal, is_volt_camera_lon def disable_long_supported(cp) -> bool: + from opendbc.car.gm.values import malibu_hybrid_profile + if malibu_hybrid_profile(cp) is not None: + return True return (volt_cc_pedal_profile(cp) is not None or camera_acc_pedal_profile(cp) is not None or is_conventional_cc_pedal_profile(cp) or is_ordinary_cc_profile(cp) or is_volt_gateway_profile(cp) or is_volt_cc_profile(cp) or is_volt_ascm_longitudinal(cp) or is_volt_camera_longitudinal(cp) or is_volt_sdgm_profile(cp, longitudinal=True) or @@ -16,6 +19,14 @@ def disable_long_supported(cp) -> bool: def prepare_disable_longitudinal(cp, requested: bool) -> None: + from opendbc.car.gm.values import malibu_hybrid_profile + hybrid = malibu_hybrid_profile(cp) + if requested and hybrid is not None: + cp.openpilotLongitudinalControl = False + cp.pcmCruise = True + cp.autoResumeSng = False + cp.safetyConfigs[0].safetyParam = 0xE804 + int(hybrid.removed) + return if requested and is_malibu_cc_f1_profile(cp): cp.openpilotLongitudinalControl = False cp.pcmCruise = True diff --git a/opendbc_repo/opendbc/car/gm/tests/test_ordinary_cc.py b/opendbc_repo/opendbc/car/gm/tests/test_ordinary_cc.py index 89399fd553..f0e1a18949 100644 --- a/opendbc_repo/opendbc/car/gm/tests/test_ordinary_cc.py +++ b/opendbc_repo/opendbc/car/gm/tests/test_ordinary_cc.py @@ -1,4 +1,5 @@ import unittest +import numpy as np from opendbc.car.gm.tests.test_ascm_intercept import params from opendbc.car.gm.values import CAR, ORDINARY_CC_CAR, is_ordinary_cc_profile, CruiseButtons, GMFlags @@ -293,3 +294,247 @@ class TestOrdinaryCc(unittest.TestCase): with patch.object(card, 'volt_cc_control_current', return_value=True): card.controls_update(structs.CarState(canValid=False), structs.CarControl()) self.assertTrue(ci.CC.volt_cc_metric) + + +def malibu_hybrid_params(*, pedal=False, removed=False, alternate=False, radar=False, release=False): + from opendbc.car import gen_empty_fingerprint + from opendbc.car.gm.interface import CarInterface + from opendbc.car.gm.values import MALIBU_HYBRID_SOURCES + from opendbc.car.gm.radar_interface import RADAR_HEADER_MSG + fingerprint = gen_empty_fingerprint() + fingerprint[0].update(MALIBU_HYBRID_SOURCES) + fingerprint[0][0xF1 if alternate else 0xBE] = 6 + if pedal: + fingerprint[0][0x201] = 6 + if not removed: + fingerprint[2].update({0x320: 6, 0x180: 4}) + if radar: + fingerprint[1][RADAR_HEADER_MSG] = 8 + return CarInterface.get_params(CAR.CHEVROLET_MALIBU_HYBRID_CC, fingerprint, [], False, release, False) + + +class TestMalibuHybridCc(unittest.TestCase): + def test_reached_factory_and_stock_reduction(self): + from opendbc.car.gm.values import malibu_hybrid_profile + from opendbc.car.gm.startup_preferences import prepare_disable_longitudinal + from opendbc.car.gm.hybrid_cc import policy_for as hybrid_policy + for release in (False, True): + for pedal in (False, True): + for removed in (False, True): + for alternate in (False, True): + for radar in ((False, True) if pedal else (False,)): + cp = malibu_hybrid_params(pedal=pedal, removed=removed, alternate=alternate, radar=radar, release=release) + profile = malibu_hybrid_profile(cp) + self.assertIsNotNone(profile) + self.assertEqual(cp.safetyConfigs[0].safetyParam, 0xE800 + 2 * int(pedal) + int(removed)) + self.assertTrue(cp.openpilotLongitudinalControl) + self.assertFalse(cp.pcmCruise) + self.assertEqual(cp.radarUnavailable, not radar) + self.assertAlmostEqual(cp.steerActuatorDelay, .2) + self.assertEqual(cp.steerRatio, float(np.float32(15.8))) + self.assertEqual(lateral_policy_for(cp), 'ordinary_cc') + self.assertTrue(qualified_gm(cp)) + self.assertTrue(longitudinal_supported(cp)) + self.assertTrue(lane_centering_supported(cp)) + policy = hybrid_policy(cp) + self.assertIsNotNone(policy) + self.assertEqual(policy.kp, ((0., 5., 15., 35.), tuple(float(np.float32(v)) + for v in (.095, .085, .065, .05))) if pedal else ((0.,), (0.,))) + from openpilot.selfdrive.controls.lib.longcontrol import LongControl + control = LongControl(cp) + self.assertEqual(control.extension.kp, policy.kp) + self.assertEqual(control.stopping_decel_rate, float(np.float32(.8)) if pedal else 1.) + self.assertEqual(bool(policy.ignore_cruise_standstill), pedal) + self.assertAlmostEqual(policy.feedforward(.5, 10., 0.), .1 if pedal else .5) + prepare_disable_longitudinal(cp, True) + self.assertEqual(cp.safetyConfigs[0].safetyParam, 0xE804 + int(removed)) + self.assertTrue(cp.pcmCruise) + self.assertFalse(cp.openpilotLongitudinalControl) + self.assertFalse(cp.autoResumeSng) + self.assertIsNotNone(malibu_hybrid_profile(cp)) + self.assertTrue(qualified_gm(cp)) + self.assertFalse(longitudinal_supported(cp)) + self.assertIsNone(hybrid_policy(cp)) + prepare_disable_longitudinal(cp, False) + self.assertEqual(cp.safetyConfigs[0].safetyParam, 0xE804 + int(removed)) + + def test_semantic_unknown_packets_do_not_release_a_physical_press(self): + from opendbc.car.gm.hybrid_cc import HybridButtons, custom_bytes, standard_set_bytes + buttons = HybridButtons() + + def receive(stamp, raw): + buttons.observe_packets([(stamp, [(0x1E1, raw, 0)])]) + return buttons.update(True)[2] + self.assertEqual(receive(1, bytes.fromhex('000000410015ee')), []) + self.assertEqual(receive(2, custom_bytes('resume', 0, 0x41)), [(1, 2)]) + for stamp, word in enumerate(('10ff', '1add', '659e', '6f7c'), 3): + self.assertEqual(receive(stamp, bytes.fromhex('0000004100' + word)), []) + self.assertEqual(receive(8, bytes.fromhex('00000041001fcc')), [(2, 1)]) + self.assertEqual(receive(9, standard_set_bytes(2)), [(1, 3)]) + self.assertEqual(receive(10, bytes.fromhex('000000410015ee')), [(3, 1)]) + self.assertEqual(receive(200_000_000, bytes.fromhex('00000041001fcc')), []) + self.assertEqual(receive(200_000_001, bytes.fromhex('000000410015ee')), []) + + def test_phase_credit_replay_expiry_and_requalification(self): + from opendbc.car.gm.hybrid_cc import PhysicalSlot, custom_bytes + slot = PhysicalSlot() + neutral = bytes.fromhex('000000410015ee') + slot.observe(1, neutral, bus=0) + self.assertFalse(slot.requalify(physical_ready=True)) + packet = custom_bytes('resume', 1, 0x41) + self.assertTrue(slot.accept('resume', packet, 2, interval_ns=200_000_001, authorized=True)) + slot.observe(3, bytes.fromhex('000000410315ee'), bus=0) + self.assertIsNone(slot.expected('resume', 3, interval_ns=0, authorized=True)) + slot.observe(4, bytes.fromhex('000000410010ff'), bus=0) + slot.observe(5, neutral, bus=0) + self.assertIsNone(slot.expected('resume', 5, interval_ns=0, authorized=True)) + slot.observe(100_000_006, bytes.fromhex('00000041001fcc'), bus=0) + self.assertFalse(slot.requalify(physical_ready=False)) + self.assertTrue(slot.requalify(physical_ready=True)) + slot.observe(100_000_007, neutral, bus=0) + self.assertEqual(slot.last_tx_ns, 2) + self.assertIsNone(slot.expected('resume', 100_000_007, interval_ns=200_000_001, authorized=True)) + + def test_fixed_near_stop_preserves_pedal_memory_and_release_slew(self): + from types import SimpleNamespace + from opendbc.car.gm.hybrid_cc import HybridPedalCommand + cp = malibu_hybrid_params(pedal=True) + command = HybridPedalCommand(cp) + cs = SimpleNamespace(vEgo=2., standstill=False, cruiseState=SimpleNamespace(standstill=False)) + first = command.update(.5, True, cs, stopping=False, resume=False, orientation=None) + self.assertGreater(first, 0.) + saved = command.steady, command.active_last + cs.vEgo = .1 + self.assertEqual(command.update(-.5, True, cs, stopping=True, resume=False, orientation=None), 0.) + self.assertEqual((command.steady, command.active_last), saved) + resumed = command.update(.5, True, cs, stopping=False, resume=True, orientation=None) + from opendbc.car.gm.silverado_cc import pedal_fraction, pedal_slew + self.assertEqual(resumed, pedal_slew(pedal_fraction(.5, .1), saved[0], .5, .1)) + + def test_healthy_inactive_resets_only_calc_memory(self): + from types import SimpleNamespace + from opendbc.car.gm.hybrid_cc import HybridPedalCommand + command = HybridPedalCommand(malibu_hybrid_params(pedal=True)) + cs = SimpleNamespace(vEgo=2., standstill=False, cruiseState=SimpleNamespace(standstill=False)) + self.assertGreater(command.update(.5, True, cs, stopping=False, resume=False, orientation=None), 0.) + self.assertEqual(command.update(.5, False, cs, stopping=False, resume=False, orientation=None), 0.) + self.assertEqual(command.steady, 0.) + self.assertFalse(command.active_last) + + def test_final_factory_denies_malformed_required_camera(self): + from opendbc.car import gen_empty_fingerprint, structs + from opendbc.car.gm.interface import CarInterface + from opendbc.car.gm.values import MALIBU_HYBRID_SOURCES, malibu_hybrid_profile + for address, wrong_length in ((0x180, 8), (0x320, 8)): + fingerprint = gen_empty_fingerprint() + fingerprint[0].update(MALIBU_HYBRID_SOURCES) + fingerprint[0][0xBE] = 6 + fingerprint[2].update({0x180: 4, 0x320: 6}) + fingerprint[2][address] = wrong_length + cp = CarInterface.get_params(CAR.CHEVROLET_MALIBU_HYBRID_CC, fingerprint, [], False, False, False) + self.assertTrue(cp.dashcamOnly) + self.assertIsNone(malibu_hybrid_profile(cp)) + self.assertEqual(cp.safetyConfigs[0].safetyModel, structs.CarParams.SafetyModel.noOutput) + + def test_actual_stock_parser_uses_c9_brake_and_pt_cruise_without_lazy_camera(self): + from opendbc.can import CANPacker + from opendbc.car import Bus + from opendbc.car.gm.interface import CarInterface + from opendbc.car.gm.values import DBC + from opendbc.car.gm.startup_preferences import prepare_disable_longitudinal + from opendbc.car.gm.tests.test_cc_gateway_stock import pt_frames + for alternate, removed in ((False, False), (True, False), (False, True), (True, True)): + cp = malibu_hybrid_params(pedal=True, removed=removed, alternate=alternate) + prepare_disable_longitudinal(cp, True) + ci = CarInterface(cp) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + for tick in range(80): + pressed = tick % 4 >= 2 + frames = [frame for frame in pt_frames(packer, acc_cruise=4, cruise=True) + if frame[0] not in (0xBE, 0xC9, 0x1E1)] + frames += [packer.make_can_msg('ECMEngineStatus', 0, {'CruiseMainOn': 1, 'BrakePressed': int(pressed)}), + packer.make_can_msg('EBCMRegenPaddle', 0, {}), + packer.make_can_msg('EBCMBrakePedalPosition', 0, {'BrakePedalPosition': 200, 'BrakePressed': 1}) + if alternate else packer.make_can_msg('ECMAcceleratorPos', 0, {'BrakePedalPos': 100}), + (0x1E1, bytes.fromhex('000000010015ee' if tick % 2 else '00000001001fcc'), 0)] + if not removed: + frames += [packer.make_can_msg('ASCMLKASteeringCmd', 2, {}), packer.make_can_msg('AEBCmd', 2, {})] + out = ci.update([(1_000_000_000 + tick * 10_000_000, frames)]) + self.assertEqual(out.brakePressed, pressed) + self.assertTrue(out.cruiseState.enabled) + self.assertTrue(out.cruiseState.standstill) + self.assertFalse(out.cruiseState.nonAdaptive) + self.assertTrue(out.canValid) + self.assertNotIn('GAS_SENSOR', ci.can_parsers[Bus.pt].vl) + if removed: + self.assertNotIn('ASCMLKASteeringCmd', ci.can_parsers[Bus.pt].vl) + self.assertNotIn('ASCMLKASteeringCmd', ci.can_parsers[Bus.cam].vl) + self.assertNotIn('ASCMActiveCruiseControlStatus', ci.can_parsers[Bus.cam].vl) + + def test_driver_override_preserves_credit_without_semantic_enable(self): + from opendbc.car.gm.hybrid_cc import HybridButtons, standard_set_bytes + buttons = HybridButtons() + buttons.observe_packets([(1_000_000_000, [(0x1E1, bytes.fromhex('000000010015ee'), 0)])]) + _, _, edges = buttons.update(True, enable_ready=False) + self.assertEqual(edges, []) + self.assertIsNone(buttons.semantic) + self.assertEqual(buttons.slot.expected('gas_set', 1_000_000_000, + interval_ns=520_000_000, authorized=True), standard_set_bytes(0)) + buttons.observe_packets([(1_030_000_000, [(0x1E1, bytes.fromhex('00000001001fcc'), 0)])]) + self.assertEqual(buttons.update(True, enable_ready=True)[2], []) + self.assertEqual(buttons.semantic, 1) + self.assertFalse(buttons.reset_pending) + + def test_real_sensor_invalidity_withdraws_hybrid_commands_and_recovers_without_edge(self): + from opendbc.can import CANPacker + from opendbc.car import Bus, structs + from opendbc.car.gm.interface import CarInterface + from opendbc.car.gm.values import DBC + from opendbc.car.gm.gmcan import pedal_crc + from opendbc.car.gm.hybrid_cc import sources_current + from opendbc.car.gm.tests.test_cc_gateway_stock import pt_frames + cp = malibu_hybrid_params(pedal=True, removed=True) + ci = CarInterface(cp) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + control = structs.CarControl.new_message() + control.enabled = control.latActive = control.longActive = True + control.actuators.torque, control.actuators.accel = .1, .5 + control.actuators.longControlState = structs.CarControl.Actuators.LongControlState.pid + positive_pedal = positive_steer = cancelled = recovered = False + echo = [] + for tick in range(160): + now = 1_000_000_000 + tick * 10_000_000 + stock_active = tick >= 80 + frames = [frame for frame in pt_frames(packer, cruise=stock_active) if frame[0] != 0x1E1] + frames += [packer.make_can_msg('EBCMRegenPaddle', 0, {}), + (0x1E1, bytes.fromhex('000000010015ee' if tick % 2 else '00000001001fcc'), 0), *echo] + if not 108 <= tick < 132: + first = 4096 if 96 <= tick < 104 else 612 + sensor = bytearray(first.to_bytes(2, 'big') + (285).to_bytes(2, 'big') + bytes((tick % 16, 0))) + sensor[-1] = pedal_crc(sensor) + if 104 <= tick < 108: + sensor[-1] ^= 1 + frames.append((0x201, bytes(sensor), 0)) + state = ci.update([(now, frames)]) + _, sent = ci.CC.update(control.as_reader(), ci.CS, now) + echo = [(address, data, 128) for address, data, bus in sent if address == 0x180 and bus == 0] + pedal = any(address == 0x200 and data[4] & 0x80 for address, data, _ in sent) + steering = any(address == 0x180 and ((data[0] & 7) or data[1]) for address, data, _ in sent) + cancel = any(address == 0x1E1 for address, _, _ in sent) + if 60 <= tick < 80: + positive_pedal |= pedal + if 80 <= tick < 96: + positive_steer |= steering + cancelled |= cancel + if 96 <= tick < 108 or 119 <= tick < 132: + self.assertFalse(sources_current(ci.CS, now)) + self.assertFalse(pedal) + self.assertFalse(steering) + self.assertFalse(cancel) + if tick >= 132: + self.assertFalse(state.buttonEvents) + recovered |= sources_current(ci.CS, now) and cancel + self.assertTrue(positive_pedal) + self.assertTrue(positive_steer) + self.assertTrue(cancelled) + self.assertTrue(recovered) diff --git a/opendbc_repo/opendbc/car/gm/values.py b/opendbc_repo/opendbc/car/gm/values.py index 2be2d3cc9f..d9a038e90a 100644 --- a/opendbc_repo/opendbc/car/gm/values.py +++ b/opendbc_repo/opendbc/car/gm/values.py @@ -143,6 +143,9 @@ VOLT_ONE_PEDAL_WORDS = {start + index: word for start in (0xD100, 0xD110) def gm_control_word(cp: CarParams) -> int: word = int(cp.safetyConfigs[0].safetyParam) + hybrid = malibu_hybrid_profile(cp) + if hybrid is not None: + return 5 | (8 if hybrid.pedal and hybrid.longitudinal else 0) if is_bolt_pedal_removed_profile(cp) or is_bolt_pedal_removed_profile(cp, stock_only=True): return int(BOLT_PEDAL_STOCK_WORDS[cp.carFingerprint] if not cp.openpilotLongitudinalControl else BOLT_PEDAL_WORDS[cp.carFingerprint]) if word in VOLT_CC_PEDAL_PROFILES and volt_cc_pedal_profile(cp) is not None: @@ -230,6 +233,9 @@ def uses_camera_stock_controls(cp: CarParams) -> bool: def requires_camera_state_sources(cp: CarParams) -> bool: + hybrid = malibu_hybrid_profile(cp) + if hybrid is not None: + return False if is_bolt_pedal_removed_profile(cp) or is_bolt_pedal_removed_profile(cp, stock_only=True): return False profile = camera_acc_pedal_profile(cp) @@ -634,6 +640,7 @@ class CAR(Platforms): CADILLAC_XT4_CC = GMCCGatewayPlatformConfig([], CADILLAC_XT4.specs) CADILLAC_XT5_CC = GMCCGatewayPlatformConfig([], replace(CADILLAC_XT5.specs, tireStiffnessFactor=1.0)) CHEVROLET_EQUINOX_CC = GMCCGatewayPlatformConfig([], CHEVROLET_EQUINOX.specs) + CHEVROLET_MALIBU_HYBRID_CC = GMPlatformConfig([], GMCarSpecs(mass=1450, wheelbase=2.8, steerRatio=15.8, centerToFrontRatio=0.4)) CHEVROLET_MALIBU_CC = GMCCGatewayPlatformConfig([], GMCarSpecs(mass=1450, wheelbase=2.8, steerRatio=18.25, centerToFrontRatio=0.4, tireStiffnessFactor=0.997)) CHEVROLET_SILVERADO_CC = GMCCGatewayPlatformConfig([], GMCarSpecs(mass=2994, wheelbase=3.75, steerRatio=16.3, tireStiffnessFactor=1.0)) @@ -727,7 +734,7 @@ PEDAL_BOLT_CAR = { NO_ACC_BOLT_CAR = PEDAL_BOLT_CAR - {CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL} VOLT_BSM_CAR = {CAR.CHEVROLET_VOLT, CAR.CHEVROLET_VOLT_ASCM, CAR.CHEVROLET_VOLT_2019, CAR.CHEVROLET_VOLT_CAMERA} -EV_CAR = {CAR.CHEVROLET_VOLT_CC, CAR.CHEVROLET_VOLT, CAR.CHEVROLET_VOLT_ASCM, CAR.CHEVROLET_VOLT_2019, +EV_CAR = {CAR.CHEVROLET_MALIBU_HYBRID_CC, CAR.CHEVROLET_VOLT_CC, CAR.CHEVROLET_VOLT, CAR.CHEVROLET_VOLT_ASCM, CAR.CHEVROLET_VOLT_2019, CAR.CHEVROLET_VOLT_CAMERA, CAR.CHEVROLET_BOLT_EUV, CAR.CHEVROLET_BOLT_ACC_2022_2023} | PEDAL_BOLT_CAR ASCM_INTERCEPT_CAR = { @@ -1156,3 +1163,44 @@ def volt_cc_pedal_profile(cp): except (AttributeError, IndexError, TypeError, ValueError): pass return None + + +@dataclass(frozen=True) +class MalibuHybridProfile: + pedal: bool + longitudinal: bool + removed: bool + + +MALIBU_HYBRID_PROFILES = MappingProxyType({ + 0xE800 + index: MalibuHybridProfile(index in (2, 3), index < 4, bool(index & 1)) + for index in range(6) +}) +MALIBU_HYBRID_SOURCES = {0x184: 8, 0x34A: 5, 0x348: 5, 0x1E1: 7, 0x1C4: 8, + 0xC9: 8, 0x3D1: 8, 0x1F5: 8, 0x232: 8, 0x1E5: 8, 0x140: 3, + 0x12A: 8, 0x1F1: 8, 0x17D: 6, 0xBD: 7} + + +def malibu_hybrid_profile(cp): + try: + if len(cp.safetyConfigs) != 1: + return None + profile = MALIBU_HYBRID_PROFILES.get(int(cp.safetyConfigs[0].safetyParam)) + flags = int(GMFlags.CC_LONG | (GMFlags.NO_CAMERA if profile and profile.removed else 0)) + if cp.flags & GMFlags.PEDAL_LONG: + flags |= int(GMFlags.PEDAL_LONG) + if cp.flags & GMFlags.NO_ACCELERATOR_POS_MSG: + flags |= int(GMFlags.NO_ACCELERATOR_POS_MSG) + if (profile is not None and cp.brand == 'gm' and cp.carFingerprint == CAR.CHEVROLET_MALIBU_HYBRID_CC and + cp.networkLocation == CarParams.NetworkLocation.fwdCamera and + cp.transmissionType == CarParams.TransmissionType.direct and not cp.alphaLongitudinalAvailable and + cp.steerControlType == CarParams.SteerControlType.torque and cp.lateralTuning.which() == 'torque' and + not cp.passive and not cp.dashcamOnly and not cp.notCar and control_flags(cp) == flags and + cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.gm and + cp.openpilotLongitudinalControl == profile.longitudinal and cp.pcmCruise == (not profile.longitudinal) and + (not profile.longitudinal or bool(cp.flags & GMFlags.PEDAL_LONG) == profile.pedal) and + (bool(cp.flags & GMFlags.PEDAL_LONG) or cp.radarUnavailable)): + return profile + except (AttributeError, IndexError, TypeError, ValueError): + pass + return None diff --git a/opendbc_repo/opendbc/car/torque_data/params.toml b/opendbc_repo/opendbc/car/torque_data/params.toml index 1fbabf6f76..49b881b759 100644 --- a/opendbc_repo/opendbc/car/torque_data/params.toml +++ b/opendbc_repo/opendbc/car/torque_data/params.toml @@ -79,3 +79,5 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"] "VOLKSWAGEN_JETTA_MK7" = [1.2271623034089392, 1.216955117387, 0.19437384688370712] "VOLKSWAGEN_PASSAT_MK8" = [1.3432120736752917, 1.7087275587362314, 0.19444383787326647] "VOLKSWAGEN_TIGUAN_MK2" = [0.9711965500094828, 1.0001565939459098, 0.1465626137072916] + +"CHEVROLET_MALIBU_HYBRID_CC" = [1.5961527626411784, 1.8422651988094612, 0.1572393918005158] diff --git a/opendbc_repo/opendbc/safety/modes/gm.h b/opendbc_repo/opendbc/safety/modes/gm.h index 096213da6d..e9c3a6fa04 100644 --- a/opendbc_repo/opendbc/safety/modes/gm.h +++ b/opendbc_repo/opendbc/safety/modes/gm.h @@ -2,12 +2,14 @@ #include "opendbc/safety/declarations.h" #include "opendbc/safety/can_tx.h" +#include "gm_rx.h" #include "gm_aol.h" #include "gm_volt_one_pedal.h" -#include "gm_bolt_cc.h" #include "gm_cc_pedal.h" #include "gm_volt_auto_hold.h" #include "gm_camera_acc_pedal.h" +#include "gm_hybrid_cc.h" +#include "gm_bolt_cc.h" // TODO: do checksum and counter checks. Add correct timestep, 0.1s for now. #define GM_COMMON_RX_CHECKS \ @@ -1054,7 +1056,6 @@ static safety_config gm_init(uint16_t safety_param) { }; #endif - static RxCheck gm_extended_rx_checks[10]; for (uint8_t i = 0U; i < 10U; i++) { gm_extended_rx_checks[i] = (RxCheck){0}; } for (uint8_t i = 0U; i < 6U; i++) { gm_extended_rx_checks[i] = gm_ordinary_camera_rx_template[i]; } gm_extended_rx_checks[3].msg[0].addr = (gm_camera_pedal && !gm_camera_pedal_f1) ? 0xBEU : 0xF1U; diff --git a/opendbc_repo/opendbc/safety/modes/gm_aol.h b/opendbc_repo/opendbc/safety/modes/gm_aol.h index 1a6c6770a5..94dea12a21 100644 --- a/opendbc_repo/opendbc/safety/modes/gm_aol.h +++ b/opendbc_repo/opendbc/safety/modes/gm_aol.h @@ -111,6 +111,7 @@ static bool gm_aol_profile_word(uint16_t word) { case 0xC171U: case 0xC172U: case 5U: case 0xBDU: case 0x9DU: case 0x19DU: case 0x1CDU: case 0xE710U: case 0xE711U: case 0xE712U: case 0xE713U: + case 0xE800U: case 0xE801U: case 0xE802U: case 0xE803U: case 0xE804U: case 0xE805U: case 0xE700U: case 0xE701U: case 0xE702U: case 0xE703U: case 0x205U: case 0x605U: case 0xA05U: case 0xE05U: case 16U: case 0xC160U: case 0xC161U: case 0xC162U: case 0xC180U: case 0xC181U: diff --git a/opendbc_repo/opendbc/safety/modes/gm_bolt_cc.h b/opendbc_repo/opendbc/safety/modes/gm_bolt_cc.h index f9219721d0..9d4ce7df69 100644 --- a/opendbc_repo/opendbc/safety/modes/gm_bolt_cc.h +++ b/opendbc_repo/opendbc/safety/modes/gm_bolt_cc.h @@ -2,9 +2,7 @@ #include "gm_bolt_cc_gate.h" -static safety_config gm_init(uint16_t safety_param); static void gm_rx_hook(const CANPacket_t *msg); -static bool gm_tx_hook(const CANPacket_t *msg); static bool gm_fwd_hook(int bus_num, int addr); static bool gm_aol_mode_valid(void); @@ -215,19 +213,30 @@ static unsigned int gm_bolt_cc_tag(uint16_t word) { static bool gm_bolt_cc_selected; static safety_config gm_bolt_cc_init(uint16_t word) { - gm_bolt_cc_stock_main = false; - gm_bolt_cc_stock_only = (word == 0xE710U) || (word == 0xE711U) || (word == 0xE712U) || (word == 0xE713U); - const unsigned int tag = gm_bolt_cc_tag(word); - gm_bolt_cc_selected = tag != 0U; - safety_config ret = (tag != 0U) ? gm_bolt_cc_profile_init((uint16_t)tag) : gm_init(word); - gm_aol_initialize(gm_aol_profile_word(word) && gm_aol_mode_valid()); - gm_aol_stock_only = gm_bolt_cc_stock_only || (word == 16U) || (word == 0xC162U); - gm_aol_stock_gateway = word == 16U; + gm_hybrid_rejected = ((word & 0xFF00U) == 0xE800U) && ((word > 0xE805U) || + ((alternative_experience != 0) && (alternative_experience != 32))); + gm_hybrid_selected = (word >= 0xE800U) && (word <= 0xE805U); + safety_config ret; + if (gm_hybrid_selected) { + gm_bolt_cc_selected = false; + ret = gm_hybrid_init(word); + } else { + gm_bolt_cc_stock_main = false; + gm_bolt_cc_stock_only = (word == 0xE710U) || (word == 0xE711U) || (word == 0xE712U) || (word == 0xE713U); + const unsigned int tag = gm_bolt_cc_tag(word); + gm_bolt_cc_selected = tag != 0U; + ret = (tag != 0U) ? gm_bolt_cc_profile_init((uint16_t)tag) : gm_init(word); + gm_aol_initialize(gm_aol_profile_word(word) && gm_aol_mode_valid()); + gm_aol_stock_only = gm_bolt_cc_stock_only || (word == 16U) || (word == 0xC162U); + gm_aol_stock_gateway = word == 16U; + } return ret; } static void gm_bolt_cc_rx(const CANPacket_t *msg) { - if (gm_bolt_cc_selected) { + if (gm_hybrid_selected) { + gm_hybrid_rx(msg); + } else if (gm_bolt_cc_selected) { gm_bolt_cc_profile_rx(msg); } else { gm_rx_hook(msg); @@ -236,16 +245,23 @@ static void gm_bolt_cc_rx(const CANPacket_t *msg) { } static void gm_bolt_cc_optional_rx(const CANPacket_t *msg) { - gm_one_pedal_observe(msg); - if (gm_bolt_cc_selected && (msg->addr == 0xBDU) && (msg->bus == 0U) && (GET_LEN(msg) == 7U)) { - gm_bolt_cc_profile_rx(msg); + if (gm_hybrid_selected) { + gm_hybrid_rx(msg); + } else { + gm_one_pedal_observe(msg); + if (gm_bolt_cc_selected && (msg->addr == 0xBDU) && (msg->bus == 0U) && (GET_LEN(msg) == 7U)) { + gm_bolt_cc_profile_rx(msg); + } } } static bool gm_bolt_cc_tx(const CANPacket_t *msg) { - return gm_bolt_cc_selected ? gm_bolt_cc_profile_tx(msg) : gm_tx_hook(msg); + return !gm_hybrid_rejected && (gm_hybrid_selected ? gm_hybrid_tx(msg) : (gm_bolt_cc_selected ? gm_bolt_cc_profile_tx(msg) : gm_tx_hook(msg))); } static bool gm_bolt_cc_fwd(int bus, int addr) { - return gm_bolt_cc_selected ? gm_bolt_cc_profile_fwd(bus, addr) : gm_fwd_hook(bus, addr); + bool block; + if (gm_hybrid_selected) { block = ((bus == 2) && (addr == 0x180)) || ((bus == 0) && (addr == 0x184)); } + else { block = gm_bolt_cc_selected ? gm_bolt_cc_profile_fwd(bus, addr) : gm_fwd_hook(bus, addr); } + return block; } diff --git a/opendbc_repo/opendbc/safety/modes/gm_hybrid_cc.h b/opendbc_repo/opendbc/safety/modes/gm_hybrid_cc.h new file mode 100644 index 0000000000..ce578725e5 --- /dev/null +++ b/opendbc_repo/opendbc/safety/modes/gm_hybrid_cc.h @@ -0,0 +1,366 @@ +#pragma once + +static safety_config gm_init(uint16_t safety_param); +static bool gm_tx_hook(const CANPacket_t *msg); + +static bool gm_hybrid_selected; +static bool gm_hybrid_rejected; +static bool gm_hybrid_pedal; +static bool gm_hybrid_stock; +static bool gm_hybrid_removed; +static bool gm_hybrid_main; +static bool gm_hybrid_drive; +static bool gm_hybrid_stock_active; +static bool gm_hybrid_speed; +static uint32_t gm_hybrid_wheel_sum; +static uint32_t gm_hybrid_stock_speed; +static bool gm_hybrid_gas_set_seen; +static uint32_t gm_hybrid_gas_set_us; +static bool gm_hybrid_regen; +static bool gm_hybrid_eps; +static bool gm_hybrid_eps_temporary; +static bool gm_hybrid_sensor_valid; +static bool gm_hybrid_sensor_seen; +static bool gm_hybrid_sensor_gas; +static uint8_t gm_hybrid_sensor_counter; +static bool gm_hybrid_seen[11] = {false, false, false, false, false, false, false, false, false, false, false}; +static uint32_t gm_hybrid_us[11] = {0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U}; +static bool gm_hybrid_credit; +static bool gm_hybrid_phase_seen; +static uint8_t gm_hybrid_phase; +static uint8_t gm_hybrid_prefix; +static uint8_t gm_hybrid_counter; +static uint32_t gm_hybrid_credit_us; +static bool gm_hybrid_requalify; +static bool gm_hybrid_button_seen; +static uint8_t gm_hybrid_button; +static bool gm_hybrid_semantic_reset; +static bool gm_hybrid_tx_seen; +static uint32_t gm_hybrid_tx_us; +static bool gm_hybrid_pedal_tx_seen; +static uint8_t gm_hybrid_pedal_tx_counter; +static uint8_t gm_hybrid_pscm[8] = {0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U}; +static bool gm_hybrid_pscm_tx_seen; +static uint32_t gm_hybrid_pscm_tx_us; + +static void gm_hybrid_clear_credit(void) { + gm_hybrid_credit = false; + gm_hybrid_requalify = true; +} + +static void gm_hybrid_reset(void) { + gm_hybrid_main = false; gm_hybrid_drive = false; gm_hybrid_stock_active = false; + gm_hybrid_regen = false; gm_hybrid_eps = false; gm_hybrid_eps_temporary = false; gm_hybrid_sensor_valid = false; + gm_hybrid_speed = false; gm_hybrid_wheel_sum = 0U; gm_hybrid_stock_speed = 0U; + gm_hybrid_gas_set_seen = false; gm_hybrid_gas_set_us = 0U; + gm_hybrid_sensor_seen = false; gm_hybrid_sensor_gas = false; gm_hybrid_sensor_counter = 0U; + gm_hybrid_credit = false; gm_hybrid_phase_seen = false; gm_hybrid_phase = 0U; + gm_hybrid_prefix = 1U; gm_hybrid_counter = 0U; gm_hybrid_credit_us = 0U; + gm_hybrid_requalify = false; gm_hybrid_button_seen = false; gm_hybrid_button = 0U; + gm_hybrid_semantic_reset = false; gm_hybrid_tx_seen = false; gm_hybrid_tx_us = 0U; + gm_hybrid_pedal_tx_seen = false; gm_hybrid_pedal_tx_counter = 0U; + gm_hybrid_pscm_tx_seen = false; gm_hybrid_pscm_tx_us = 0U; + for (uint8_t i = 0U; i < 11U; i++) { gm_hybrid_seen[i] = false; gm_hybrid_us[i] = 0U; } + for (uint8_t i = 0U; i < 8U; i++) { gm_hybrid_pscm[i] = 0U; } +} + +static bool gm_hybrid_current(void) { + static const uint32_t limits[11] = {300000U, 100000U, 100000U, 300000U, 300000U, 300000U, + 1000000U, 100000U, 100000U, 1000000U, 1000000U}; + bool current = !safety_rx_checks_invalid && !relay_malfunction; + for (uint8_t i = 0U; i < 11U; i++) { + if (((i != 8U) || gm_hybrid_pedal) && ((i < 9U) || !gm_hybrid_removed)) { + current &= gm_hybrid_seen[i] && (safety_get_ts_elapsed(microsecond_timer_get(), gm_hybrid_us[i]) <= limits[i]); + } + } + current &= !gm_hybrid_pedal || gm_hybrid_sensor_valid; + if (!current) { gm_hybrid_clear_credit(); controls_allowed = false; } + return current; +} + +static bool gm_hybrid_ready(void) { + return gm_hybrid_current() && gm_hybrid_main && gm_hybrid_drive && gm_hybrid_eps && + !brake_pressed && !gas_pressed && !gm_hybrid_regen && (!gm_hybrid_pedal || !gm_hybrid_sensor_gas); +} + +static uint8_t gm_hybrid_request(void) { + uint8_t request = 0U; + if (heartbeat_engaged && !relay_malfunction && !safety_rx_checks_invalid && + (safety_get_ts_elapsed(microsecond_timer_get(), aol_host_request_ts) <= AOL_HOST_REQUEST_TIMEOUT_US)) { + request = aol_host_axis_mask & (gm_hybrid_stock ? 1U : 3U); + } + return request; +} + +static uint8_t gm_hybrid_permission(void) { + uint8_t permission = 0U; + if (gm_hybrid_current() && gm_hybrid_main && gm_hybrid_drive && gm_hybrid_eps) { + permission = gm_hybrid_request() & 1U; + if (!gm_hybrid_stock && controls_allowed && !brake_pressed && !gm_hybrid_regen && + (!gm_hybrid_pedal || (!gm_hybrid_stock_active && !gas_pressed && !gm_hybrid_sensor_gas))) { + permission |= gm_hybrid_request() & 2U; + } + } + return permission; +} + +static void gm_hybrid_invalid(void) { + gm_hybrid_clear_credit(); controls_allowed = false; + for (uint8_t i = 0U; i < 11U; i++) { gm_hybrid_seen[i] = false; } + aol_set_host_request(0U); +} + +static bool gm_hybrid_tuple(const CANPacket_t *msg) { + return (msg->data[0] == 0U) && (msg->data[1] == 0U) && (msg->data[2] == 0U) && + ((msg->data[3] == 1U) || (msg->data[3] == 0x41U)) && (msg->data[4] <= 3U); +} + +static uint8_t gm_hybrid_semantic(const CANPacket_t *msg) { + const uint16_t word = ((uint16_t)msg->data[5] << 8U) | msg->data[6]; + uint8_t button = 0U; + if (gm_hybrid_tuple(msg)) { + if ((word == 0x15EEU) || (word == 0x1FCCU)) { button = 1U; } + else if ((word == 0x55AEU) || (word == 0x5F8CU)) { button = 2U; } + else if ((word == 0x2ACDU) || (word == 0x20EFU)) { button = 5U; } + else if ((word == 0x60AFU) || (word == 0x6A8DU)) { button = 6U; } + else { + const unsigned int checksum = 0xDFU + ((unsigned int)msg->data[4] * 0x4EFU); + if ((msg->data[3] == 1U) && (word == (0x3000U | checksum))) { button = 3U; } + } + } + return button; +} + +static void gm_hybrid_rx(const CANPacket_t *msg) { + static const uint32_t addresses[11] = {0x184U, 0x34AU, 0x1E1U, 0x1C4U, 0xC9U, 0x3D1U, + 0x1F5U, 0xBDU, 0x201U, 0x180U, 0x320U}; + static const uint8_t lengths[11] = {8U, 5U, 7U, 8U, 8U, 8U, 8U, 7U, 6U, 4U, 6U}; + const uint32_t now = microsecond_timer_get(); + for (uint8_t i = 0U; i < 11U; i++) { + const unsigned int bus = (i >= 9U) ? 2U : 0U; + if ((msg->addr == addresses[i]) && (msg->bus == bus)) { + if (GET_LEN(msg) != lengths[i]) { gm_hybrid_seen[i] = false; gm_hybrid_clear_credit(); controls_allowed = false; } + else { + const bool gap = gm_hybrid_seen[i] && (i == 2U) && (safety_get_ts_elapsed(now, gm_hybrid_us[i]) > 100000U); + gm_hybrid_seen[i] = true; gm_hybrid_us[i] = now; + if (gap) { gm_hybrid_clear_credit(); } + if (i == 0U) { + for (uint8_t j = 0U; j < 8U; j++) { gm_hybrid_pscm[j] = msg->data[j]; } + const uint32_t torque = (((uint32_t)msg->data[6] & 7U) << 8U) | msg->data[7]; + update_sample(&torque_driver, to_signed((int)torque, 11)); + const uint8_t eps = (msg->data[0] >> 3U) & 7U; + gm_hybrid_eps = (eps != 2U) && (eps != 3U); + gm_hybrid_eps_temporary = eps == 2U; + } else if (i == 1U) { + gm_hybrid_wheel_sum = (((uint32_t)msg->data[0] << 8U) | msg->data[1]) + + (((uint32_t)msg->data[2] << 8U) | msg->data[3]); + gm_hybrid_speed = ((((uint32_t)msg->data[0] << 8U) | msg->data[1]) >= 1242U) && + ((((uint32_t)msg->data[2] << 8U) | msg->data[3]) >= 1242U) && + (((msg->data[4] >> 3U) & 7U) == 1U) && ((msg->data[4] & 7U) == 1U); + vehicle_moving = ((((unsigned int)msg->data[0] << 8U) | msg->data[1]) > 10U) || + ((((unsigned int)msg->data[2] << 8U) | msg->data[3]) > 10U); + } else if (i == 3U) { gas_pressed = msg->data[5] != 0U; } + else if (i == 4U) { gm_hybrid_main = GET_BIT(msg, 29U); brake_pressed = GET_BIT(msg, 40U); } + else if (i == 5U) { + gm_hybrid_stock_active = GET_BIT(msg, 39U); + gm_hybrid_stock_speed = (((uint32_t)msg->data[2] & 15U) << 8U) | msg->data[3]; + } + else if (i == 6U) { + const uint8_t gear = msg->data[3] & 15U; + gm_hybrid_drive = (gear == 4U) || (gear == 6U) || GET_BIT(msg, 41U); + } else if (i == 7U) { gm_hybrid_regen = (msg->data[0] >> 4U) != 0U; } + else if ((i == 8U) && gm_hybrid_pedal) { + const uint32_t track1 = ((uint32_t)msg->data[0] << 8U) | msg->data[1]; + const uint32_t track2 = ((uint32_t)msg->data[2] << 8U) | msg->data[3]; + const uint8_t counter = msg->data[4] & 15U; + gm_hybrid_sensor_valid = ((msg->data[4] >> 4U) == 0U) && (track1 <= 4095U) && (track2 <= 4095U) && + (gm_pedal_crc(msg) == msg->data[5]) && (!gm_hybrid_sensor_seen || (counter != gm_hybrid_sensor_counter)); + gm_hybrid_sensor_seen = true; gm_hybrid_sensor_counter = counter; + gm_hybrid_sensor_gas = ((125677U * track1) + (251976U * track2)) > (uint32_t)198510000U; + } else { + } + if (i == 2U) { + gm_hybrid_credit = false; + if (gm_hybrid_requalify) { + if (gm_hybrid_ready()) { + gm_hybrid_phase_seen = false; gm_hybrid_requalify = false; + gm_hybrid_button_seen = false; gm_hybrid_semantic_reset = true; + } + } else { + const uint16_t word = ((uint16_t)msg->data[5] << 8U) | msg->data[6]; + if (gm_hybrid_tuple(msg) && ((word == 0x15EEU) || (word == 0x1FCCU))) { + const uint8_t phase = (word == 0x15EEU) ? 1U : 3U; + if (!gm_hybrid_phase_seen || (phase != gm_hybrid_phase)) { + gm_hybrid_phase = phase; gm_hybrid_phase_seen = true; gm_hybrid_credit = true; + gm_hybrid_prefix = msg->data[3]; gm_hybrid_counter = msg->data[4]; gm_hybrid_credit_us = now; + } + } + const uint8_t button = gm_hybrid_semantic(msg); + if (button != 0U) { + if (!gm_hybrid_button_seen) { gm_hybrid_semantic_reset = button != 1U; } + else if (!gm_hybrid_stock && !gm_hybrid_semantic_reset && gm_hybrid_ready() && + (((button == 2U) && (gm_hybrid_button != 2U)) || ((button == 1U) && (gm_hybrid_button == 3U)))) { + controls_allowed = true; + } else { + } + if (gm_hybrid_semantic_reset && (button == 1U)) { gm_hybrid_semantic_reset = false; } + if ((button == 5U) || (button == 6U)) { controls_allowed = false; } + gm_hybrid_button = button; gm_hybrid_button_seen = true; + } + } + } + } + } + } + const bool current = gm_hybrid_current(); + if (gm_hybrid_stock) { + // Evaluate the physical PCM edge after all sources agree, independent of + // whether cruise or main arrived first. Faults do not reset a held edge. + if ((gm_hybrid_seen[4] && !gm_hybrid_main) || (gm_hybrid_seen[5] && !gm_hybrid_stock_active)) { + pcm_cruise_check(false); + } else if (current && gm_hybrid_main && gm_hybrid_stock_active) { + const bool previously_allowed = controls_allowed; + pcm_cruise_check(true); + // Observe the PCM edge during a temporary fault, but never arm from it. + if (gm_hybrid_eps_temporary && !previously_allowed) { controls_allowed = false; } + } else { + } + } + const bool permanent_eps_withdrawal = !gm_hybrid_eps && !gm_hybrid_eps_temporary; + if (!gm_hybrid_main || !gm_hybrid_drive || permanent_eps_withdrawal || (gm_hybrid_pedal && !gm_hybrid_sensor_valid)) { + gm_hybrid_clear_credit(); controls_allowed = false; + } + // Driver intervention withdraws actuation, not the physical cancellation slot. + if (brake_pressed || gm_hybrid_regen || ((gm_hybrid_stock || gm_hybrid_pedal) && gas_pressed) || (gm_hybrid_pedal && gm_hybrid_sensor_gas)) { + controls_allowed = false; + } +} + +static bool gm_hybrid_button_tx(const CANPacket_t *msg) { + static const uint16_t resume[4] = {0x55AEU, 0x5F8CU, 0x6F7CU, 0x659EU}; + static const uint16_t cancel[4] = {0x60AFU, 0x659EU, 0x6A8DU, 0x6F7CU}; + const uint32_t now = microsecond_timer_get(); + if (gm_hybrid_credit && (safety_get_ts_elapsed(now, gm_hybrid_credit_us) > 100000U)) { gm_hybrid_clear_credit(); } + bool allowed = gm_hybrid_current() && gm_hybrid_main && gm_hybrid_drive && gm_hybrid_eps && + gm_hybrid_credit && !gm_hybrid_requalify && gm_hybrid_tuple(msg) && + (!gm_hybrid_tx_seen || (safety_get_ts_elapsed(now, gm_hybrid_tx_us) >= 40000U)); + const uint16_t word = ((uint16_t)msg->data[5] << 8U) | msg->data[6]; + const bool longitudinal = get_longitudinal_allowed(); + bool cancel_long_request = controls_allowed; + if (alternative_experience != 0) { cancel_long_request = (gm_hybrid_request() & 2U) != 0U; } + const bool cancel_context = gm_hybrid_stock_active && (gm_hybrid_stock || !gm_hybrid_pedal || cancel_long_request); + const bool custom_cancel = cancel_context && (msg->data[3] == gm_hybrid_prefix) && (msg->data[4] == 0U) && + (word == cancel[(gm_hybrid_phase + 1U) % 4U]); + const bool custom_resume = !gm_hybrid_stock && !gm_hybrid_pedal && gm_hybrid_speed && longitudinal && !gas_pressed && !brake_pressed && + !gm_hybrid_regen && (msg->data[3] == gm_hybrid_prefix) && (msg->data[4] == 0U) && (word == resume[gm_hybrid_phase]); + const uint8_t next_counter = (gm_hybrid_counter + 1U) % 4U; + const unsigned int set_checksum = 0xDFU + ((unsigned int)msg->data[4] * 0x4EFU); + const bool gas_set = !gm_hybrid_stock && !gm_hybrid_pedal && longitudinal_controls_allowed() && + gas_pressed && gm_hybrid_stock_active && !brake_pressed && !gm_hybrid_regen && gm_hybrid_speed && + ((gm_hybrid_stock_speed * 1250U) < (gm_hybrid_wheel_sum * 311U)) && + (!gm_hybrid_gas_set_seen || (safety_get_ts_elapsed(now, gm_hybrid_gas_set_us) >= 520000U)) && + (msg->data[3] == 1U) && (msg->data[4] == 0U) && (word == 0x30DFU); + const bool standard_set = !gm_hybrid_stock && !gm_hybrid_pedal && longitudinal && !gas_pressed && + !brake_pressed && !gm_hybrid_regen && gm_hybrid_speed && (msg->data[3] == 1U) && + (word == (0x3000U | set_checksum)) && (msg->data[4] == next_counter); + allowed &= custom_cancel || custom_resume || standard_set || gas_set; + if (allowed) { + gm_hybrid_credit = false; gm_hybrid_tx_seen = true; gm_hybrid_tx_us = now; + if (gas_set) { gm_hybrid_gas_set_seen = true; gm_hybrid_gas_set_us = now; } + } + return allowed; +} + +static bool gm_hybrid_tx(const CANPacket_t *msg) { + bool allowed = false; + if ((msg->addr == 0x180U) && (msg->bus == 0U) && (GET_LEN(msg) == 4U)) { + const bool current = gm_hybrid_current(); + const bool neutral = ((msg->data[0] & 15U) == 0U) && (msg->data[1] == 0U); + allowed = gm_tx_hook(msg) && (neutral || (current && gm_hybrid_main && gm_hybrid_drive && gm_hybrid_eps)); + } else if ((msg->addr == 0x1E1U) && (msg->bus == 0U) && (GET_LEN(msg) == 7U)) { + allowed = gm_hybrid_button_tx(msg); + } else if (gm_hybrid_pedal && (msg->addr == 0x200U) && (msg->bus == 0U) && (GET_LEN(msg) == 6U)) { + const uint32_t raw_track1 = ((uint32_t)msg->data[0] << 8U) | msg->data[1]; + const uint32_t raw_track2 = ((uint32_t)msg->data[2] << 8U) | msg->data[3]; + const int track1 = (int)raw_track1; + const int track2 = (int)raw_track2; + const bool enabled = GET_BIT(msg, 39U); + const uint8_t counter = msg->data[4] & 15U; + const int difference = track1 - (2 * track2); + const bool inactive = !enabled && (track1 == 0) && (track2 == 0); + const bool active = enabled && gm_hybrid_ready() && !gm_hybrid_stock_active && get_longitudinal_allowed() && + (track1 >= 604) && (track1 <= 2633) && (track2 >= 304) && (track2 <= 1316) && + (difference >= -16) && (difference <= 16); + allowed = (inactive || active) && ((msg->data[4] & 0x70U) == 0U) && + (gm_pedal_crc(msg) == msg->data[5]) && (!gm_hybrid_pedal_tx_seen || (counter != gm_hybrid_pedal_tx_counter)); + if (allowed) { gm_hybrid_pedal_tx_seen = true; gm_hybrid_pedal_tx_counter = counter; } + } else if ((msg->addr == 0x184U) && (msg->bus == 2U) && (GET_LEN(msg) == 8U)) { + static const uint8_t masks[8] = {0x3FU, 0xFFU, 0x3FU, 0xFFU, 0x7BU, 0xFFU, 7U, 0xFFU}; + uint8_t expected[8]; + for (uint8_t i = 0U; i < 8U; i++) { expected[i] = gm_hybrid_pscm[i] & masks[i]; } + unsigned int checksum = (((unsigned int)gm_hybrid_pscm[4] & 3U) << 8U) | gm_hybrid_pscm[5]; + if ((gm_hybrid_pscm[2] & 32U) == 0U) { checksum += 32U; } + expected[2] |= 32U; + expected[4] = (uint8_t)(((unsigned int)expected[4] & 0xFCU) | ((checksum >> 8U) & 3U)); + expected[5] = (uint8_t)(checksum & 255U); + allowed = gm_hybrid_seen[0] && (safety_get_ts_elapsed(microsecond_timer_get(), gm_hybrid_us[0]) <= 300000U) && + (!gm_hybrid_pscm_tx_seen || (safety_get_ts_elapsed(microsecond_timer_get(), gm_hybrid_pscm_tx_us) >= 100000U)); + for (uint8_t i = 0U; i < 8U; i++) { allowed &= expected[i] == msg->data[i]; } + if (allowed) { gm_hybrid_pscm_tx_seen = true; gm_hybrid_pscm_tx_us = microsecond_timer_get(); } + } else if (gm_hybrid_removed && !gm_hybrid_stock && (msg->bus == 0U) && (GET_LEN(msg) == 7U) && + ((msg->addr == 0x409U) || (msg->addr == 0x40AU))) { + allowed = true; + for (uint8_t i = 0U; i < 7U; i++) { allowed &= msg->data[i] == 0U; } + } else { + } + return allowed; +} + +static safety_config gm_hybrid_init(uint16_t word) { + safety_config ret = gm_init(1U); + gm_aol_reset(); + gm_hybrid_reset(); + gm_hybrid_removed = (word & 1U) != 0U; + gm_hybrid_pedal = (word == 0xE802U) || (word == 0xE803U); + gm_hybrid_stock = (word == 0xE804U) || (word == 0xE805U); + static const RxCheck rx_template[10] = { + {.msg = {{0x184U, 0U, 8U, 10U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}, {0}}}, + {.msg = {{0x34AU, 0U, 5U, 20U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}, {0}}}, + {.msg = {{0x1E1U, 0U, 7U, 33U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}, {0}}}, + {.msg = {{0x1C4U, 0U, 8U, 33U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}, {0}}}, + {.msg = {{0xC9U, 0U, 8U, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}, {0}}}, + {.msg = {{0x3D1U, 0U, 8U, 10U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}, {0}}}, + {.msg = {{0x1F5U, 0U, 8U, 10U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}, {0}}}, + {.msg = {{0xBDU, 0U, 7U, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}, {0}}}, + {.msg = {{0x180U, 2U, 4U, 10U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}, {0}}}, + {.msg = {{0x320U, 2U, 6U, 10U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}, {0}}}, + }; + for (uint8_t i = 0U; i < 10U; i++) { gm_extended_rx_checks[i] = rx_template[i]; } + ret.rx_checks = gm_extended_rx_checks; + ret.rx_checks_len = gm_hybrid_removed ? 8 : 10; + static const CanMsg present_tx[] = { + {0x180U, 0U, 4U, .check_relay = true}, {0x1E1U, 0U, 7U, .check_relay = false}, + {0x184U, 2U, 8U, .check_relay = true}, {0x200U, 0U, 6U, .check_relay = false}, + }; + static const CanMsg removed_tx[] = { + {0x180U, 0U, 4U, .check_relay = false}, {0x1E1U, 0U, 7U, .check_relay = false}, + {0x184U, 2U, 8U, .check_relay = true}, {0x409U, 0U, 7U, .check_relay = false}, {0x40AU, 0U, 7U, .check_relay = false}, + {0x200U, 0U, 6U, .check_relay = false}, + }; + if (gm_hybrid_removed) { + ret.tx_msgs = removed_tx; + if (gm_hybrid_stock) { ret.tx_msgs_len = 3; } + else if (gm_hybrid_pedal) { ret.tx_msgs_len = 6; } + else { ret.tx_msgs_len = 5; } + } else { + ret.tx_msgs = present_tx; + if (gm_hybrid_pedal) { ret.tx_msgs_len = 4; } + else { ret.tx_msgs_len = 3; } + } + static const AolSafetyPolicy policy = { + .reset = gm_hybrid_reset, .host_request = NULL, .request_mask = gm_hybrid_request, + .permission_mask = gm_hybrid_permission, .rx_invalid = gm_hybrid_invalid, + }; + if ((unsigned int)alternative_experience == 32U) { aol_policy = &policy; } + return ret; +} diff --git a/opendbc_repo/opendbc/safety/modes/gm_rx.h b/opendbc_repo/opendbc/safety/modes/gm_rx.h new file mode 100644 index 0000000000..0c059513ab --- /dev/null +++ b/opendbc_repo/opendbc/safety/modes/gm_rx.h @@ -0,0 +1,5 @@ +#pragma once + +// Only the selected GM owner uses this storage. Each owning initializer replaces +// every entry from its immutable template, including all runtime RX status. +static RxCheck gm_extended_rx_checks[10]; diff --git a/opendbc_repo/opendbc/safety/tests/test_gm_hybrid_cc.py b/opendbc_repo/opendbc/safety/tests/test_gm_hybrid_cc.py new file mode 100644 index 0000000000..7cf34bae92 --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_gm_hybrid_cc.py @@ -0,0 +1,401 @@ +"""Hybrid-specific physical button credit, independent axes, and pedal envelope.""" +import unittest + +from opendbc.can import CANPacker +from opendbc.car.gm import gmcan +from opendbc.car.structs import CarParams +from opendbc.safety.tests.libsafety import libsafety_py + + +class TestGmHybridCc(unittest.TestCase): + def setUp(self): + self.safety = libsafety_py.libsafety + self.packer = CANPacker('gm_global_a_powertrain_generated') + self.now = 1_000_000 + self.sensor_counter = 0 + + def tearDown(self): + self.safety.set_alternative_experience(0) + + @staticmethod + def packet(frame): + return libsafety_py.make_CANPacket(frame[0], frame[2], frame[1]) + + def reset(self, word=0xE800, alternative=32): + self.word = word + self.safety.init_tests() + self.safety.set_alternative_experience(alternative) + self.assertEqual(self.safety.set_safety_hooks(CarParams.SafetyModel.gm, word), 0) + self.now = 1_000_000 + self.sensor_counter = 0 + self.safety.set_timer(self.now) + self.safety.set_aol_test_heartbeat(True) + + def rx(self, address, data, bus=0): + return self.safety.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, bytes(data))) + + def tx(self, address, data, bus=0): + return self.safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, bytes(data))) + + @staticmethod + def button(word, counter=0, prefix=1): + return bytes((0, 0, 0, prefix, counter, word >> 8, word & 255)) + + def slot(self, word=0x15EE, counter=0): + self.rx(0x1E1, self.button(word, counter)) + + def feed(self, *, main=True, stock=False, gear=4, manual=False, brake=False, gas=False, regen=False, + eps=0, tracks=(604, 304), missing=None): + values = { + 'PSCMStatus': {'LKATorqueDeliveredStatus': eps}, + 'EBCMWheelSpdRear': {'RLWheelSpd': 60, 'RRWheelSpd': 60, 'RLWheelDir': 1, 'RRWheelDir': 1}, + 'AcceleratorPedal2': {'AcceleratorPedal2': 30 if gas else 0}, + 'ECMEngineStatus': {'CruiseMainOn': main, 'BrakePressed': brake}, + 'ECMCruiseControl': {'CruiseActive': stock}, + 'ECMPRDNL2': {'PRNDL2': gear, 'ManualMode': manual}, + 'EBCMRegenPaddle': {'RegenPaddle': 2 if regen else 0}, + } + frames = [self.packer.make_can_msg(name, 0, val) for name, val in values.items()] + if self.word % 2 == 0: + frames += [self.packer.make_can_msg('ASCMLKASteeringCmd', 2, {}), self.packer.make_can_msg('AEBCmd', 2, {})] + if self.word in (0xE802, 0xE803): + a, b = tracks + sensor = bytearray((a >> 8, a & 255, b >> 8, b & 255, self.sensor_counter & 15, 0)) + sensor[5] = gmcan.pedal_crc(sensor) + self.sensor_counter += 1 + frames.append((0x201, bytes(sensor), 0)) + for frame in frames: + if frame[0] != missing: + self.safety.safety_rx_hook(self.packet(frame)) + if missing != 0x1E1: + self.slot(0x15EE) + self.safety.safety_tick() + + def warm(self, **kwargs): + self.feed(**kwargs) + self.advance(50_000) + self.feed(**kwargs) + self.slot(0x1FCC) + self.safety.aol_set_host_request(3) + + def advance(self, us): + self.now += us + self.safety.set_timer(self.now) + self.safety.set_aol_test_heartbeat(True) + self.safety.aol_set_host_request(3) + + def arm(self): + self.slot(0x55AE) + self.assertTrue(self.safety.get_controls_allowed()) + self.slot(0x15EE) + + def pedal(self, fraction=.2, counter=0): + return self.safety.safety_tx_hook(self.packet(gmcan.create_pedal_command(self.packer, fraction, counter))) + + def test_phase_qualified_collision_and_one_shot_credit(self): + for word in (0xE800, 0xE801): + self.reset(word) + self.warm() + self.arm() + # Unique phase1 means RES5F8C; collided659E cannot be interpreted as arbitrary RES/CANCEL. + self.assertFalse(self.tx(0x1E1, self.button(0x659E))) + self.assertTrue(self.tx(0x1E1, self.button(0x5F8C))) + self.assertFalse(self.tx(0x1E1, self.button(0x5F8C))) + self.advance(50_000) + self.feed(stock=True) + self.slot(0x1FCC) + self.assertTrue(self.tx(0x1E1, self.button(0x60AF))) + self.assertFalse(self.tx(0x1E1, self.button(0x60AF))) + + def test_standard_set_counter_and_gas_set_zero_are_distinct(self): + self.reset() + self.warm(stock=True) + self.arm() + self.slot(0x1FCC, counter=2) + self.assertTrue(self.safety.safety_tx_hook(self.packet(gmcan.create_buttons(self.packer, 0, 3, 3)))) + self.advance(50_000) + self.feed(stock=True, gas=True) + self.slot(0x1FCC, counter=2) + self.assertFalse(self.safety.safety_tx_hook(self.packet(gmcan.create_buttons(self.packer, 0, 3, 3)))) + self.assertTrue(self.safety.safety_tx_hook(self.packet(gmcan.create_buttons(self.packer, 0, 0, 3)))) + + def test_pedal_stock_takeover_cancel_does_not_grant_actuation(self): + for word in (0xE802, 0xE803): + self.reset(word) + self.warm() + self.arm() + self.assertTrue(self.pedal()) + self.advance(50_000) + self.feed(stock=True) + self.slot(0x1FCC) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + self.assertFalse(self.pedal(counter=1)) + self.assertTrue(self.tx(0x1E1, self.button(0x60AF))) + + def test_stock_reduction_and_exact_profile_admission(self): + for word in (0xE804, 0xE805): + self.reset(word) + self.warm(stock=True) + self.assertEqual(self.safety.aol_get_request_mask(), 1) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + self.assertFalse(self.pedal()) + self.assertFalse(self.tx(0x1E1, self.button(0x659E))) + self.assertFalse(self.safety.safety_tx_hook(self.packet(gmcan.create_buttons(self.packer, 0, 1, 3)))) + self.assertTrue(self.tx(0x1E1, self.button(0x60AF))) + for address in (0x315, 0x2CB, 0x370, 0x409, 0x40A, 0xBD, 0x1F5): + self.assertFalse(self.tx(address, bytes(7))) + for word in (0xE806, 0xE80F, 0xE8FF): + self.reset(word) + self.assertFalse(self.tx(0x180, bytes(4))) + self.assertFalse(self.tx(0x1E1, self.button(0x60AF))) + + def test_lateral_survives_driver_pedals_but_fault_and_reverse_withdraw(self): + for word in range(0xE800, 0xE806): + for physical in ({'brake': True}, {'gas': True}, {'regen': True}): + self.reset(word) + self.warm(**physical) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + self.assertFalse(self.pedal()) + for physical in ({'main': False}, {'gear': 2}, {'eps': 2}, {'eps': 3}): + self.reset(word) + self.warm(**physical) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.reset(word) + self.warm(manual=True) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + + def test_source_expiry_requalification_and_mode_reset(self): + self.reset(0xE803) + self.warm() + self.arm() + self.advance(100_001) + self.assertFalse(self.pedal()) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.feed() + self.assertFalse(self.safety.get_controls_allowed()) + self.assertFalse(self.tx(0x1E1, self.button(0x5F8C))) + self.slot(0x1FCC) + self.arm() + self.assertTrue(self.pedal(counter=1)) + self.reset(0xE805) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + + def test_independent_adc_crc_state_and_counter(self): + self.reset(0xE803) + self.warm() + self.arm() + self.assertTrue(self.pedal()) + for tracks in ((0, 4095), (4095, 0), (4096, 304)): + self.advance(50_000) + self.feed(tracks=tracks) + self.assertFalse(self.pedal(counter=1)) + sensor = bytearray((2, 92, 1, 48, 0x10, 0)) + sensor[5] = gmcan.pedal_crc(sensor) + self.rx(0x201, sensor) + self.assertFalse(self.pedal(counter=2)) + sensor[4] = 1 + sensor[5] = gmcan.pedal_crc(sensor) ^ 1 + self.rx(0x201, sensor) + self.assertFalse(self.pedal(counter=2)) + + def test_removed_keepalive_and_pscm_exact_copy(self): + for word in range(0xE800, 0xE806): + self.reset(word) + self.warm() + for address in (0x409, 0x40A): + expected = word in (0xE801, 0xE803) + self.assertEqual(self.tx(address, bytes(7)), expected) + self.assertFalse(self.tx(address, bytes((1, 0, 0, 0, 0, 0, 0)))) + copy = gmcan.create_pscm_status(self.packer, 2, dict.fromkeys(('HandsOffSWDetectionMode', 'HandsOffSWlDetectionStatus', 'LKATorqueDeliveredStatus', + 'LKADriverAppldTrq', 'LKATorqueDelivered', 'LKATotalTorqueDelivered', + 'RollingCounter', 'PSCMStatusChecksum'), 0)) + self.assertTrue(self.safety.safety_tx_hook(self.packet(copy))) + self.assertFalse(self.safety.safety_tx_hook(self.packet(copy))) + self.assertFalse(self.tx(0x184, bytes(8), 2)) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x180), -1) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x315), 0) + + def test_required_sources_bus_shape_and_individual_expiry(self): + sources = ((0x184, 8, 0, 300_000), (0x34A, 5, 0, 100_000), (0x1E1, 7, 0, 100_000), + (0x1C4, 8, 0, 300_000), (0xC9, 8, 0, 300_000), (0x3D1, 8, 0, 300_000), + (0x1F5, 8, 0, 1_000_000), (0xBD, 7, 0, 100_000), (0x201, 6, 0, 100_000), + (0x180, 4, 2, 1_000_000), (0x320, 6, 2, 1_000_000)) + for address, length, bus, limit in sources: + with self.subTest(address=hex(address)): + self.reset(0xE802) + self.warm() + self.rx(address, bytes(length - 1), bus) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.reset(0xE802) + self.warm() + saved = self.now + for _tick in range(1, limit // 50_000 + 2): + self.advance(50_000) + self.feed(missing=address) + self.assertGreater(self.now - saved, limit) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.reset(0xE802) + self.feed(missing=address) + self.rx(address, bytes(length), 1) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + + def test_malformed_buttons_cannot_consume_or_create_credit(self): + self.reset() + self.warm() + self.arm() + valid = self.button(0x5F8C) + for index in (0, 1, 2, 3, 4, 5, 6): + malformed = bytearray(valid) + malformed[index] ^= 0x80 + self.assertFalse(self.tx(0x1E1, malformed)) + self.assertFalse(self.tx(0x1E1, valid, 2)) + self.assertTrue(self.tx(0x1E1, valid)) + self.advance(50_000) + self.feed() + self.slot(0x1FCC, counter=1) + standard = bytearray(gmcan.create_buttons(self.packer, 0, 2, 3)[1]) + standard[6] ^= 1 + self.assertFalse(self.tx(0x1E1, standard)) + self.assertTrue(self.safety.safety_tx_hook(self.packet(gmcan.create_buttons(self.packer, 0, 2, 3)))) + + def test_repeated_sensor_counter_withdraws_positive_output(self): + self.reset(0xE803) + self.warm() + self.arm() + self.assertTrue(self.pedal()) + sensor = bytearray((2, 92, 1, 48, (self.sensor_counter - 1) & 15, 0)) + sensor[5] = gmcan.pedal_crc(sensor) + self.rx(0x201, sensor) + self.assertFalse(self.pedal(counter=1)) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + + def steering(self, torque=1, active=True): + return self.safety.safety_tx_hook(self.packet(gmcan.create_steering_control(self.packer, 0, torque, 0, active))) + + def stock_frame(self, active): + self.safety.safety_rx_hook(self.packet(self.packer.make_can_msg('ECMCruiseControl', 0, {'CruiseActive': active}))) + + def main_frame(self, main): + self.safety.safety_rx_hook(self.packet(self.packer.make_can_msg('ECMEngineStatus', 0, {'CruiseMainOn': main}))) + + def test_stock_pcm_reengagement_is_independent_of_packet_order(self): + for word in (0xE804, 0xE805): + for alternative in (0, 32): + for cruise_first in (False, True): + self.reset(word, alternative) + self.warm(stock=True) + self.assertTrue(self.steering()) + self.main_frame(False) + self.stock_frame(False) + self.assertFalse(self.steering()) + self.advance(10_000) + if cruise_first: + self.stock_frame(True) + self.assertFalse(self.steering()) + self.main_frame(True) + else: + self.main_frame(True) + # Typed lateral is independent of stock cruise; ordinary AE0 is not. + self.assertEqual(self.steering(), alternative == 32) + self.stock_frame(True) + self.assertTrue(self.steering()) + self.reset(word, alternative) + self.warm(stock=False) + self.slot(0x55AE) + if alternative == 0: + self.assertFalse(self.steering()) + self.assertFalse(self.safety.get_controls_allowed()) + + def test_stock_temporary_eps_suppresses_without_permanent_rearm(self): + for word in (0xE804, 0xE805): + for alternative in (0, 32): + self.reset(word, alternative) + self.warm(stock=True) + self.assertTrue(self.steering()) + self.advance(10_000) + self.feed(stock=True, eps=2) + self.assertFalse(self.steering()) + self.assertTrue(self.steering(0, False)) + self.advance(10_000) + self.feed(stock=True) + self.assertTrue(self.steering()) + self.advance(10_000) + self.feed(stock=True, eps=3) + self.assertFalse(self.steering()) + self.advance(10_000) + self.feed(stock=True) + if alternative == 0: + self.assertFalse(self.steering()) + self.main_frame(False) + self.main_frame(True) + self.assertTrue(self.steering()) + + def test_stock_held_cruise_cannot_rearm_after_driver_or_source_withdrawal(self): + for physical in ({'brake': True}, {'gas': True}, {'regen': True}, {'gear': 2}): + self.reset(0xE804, 0) + self.warm(stock=True) + self.assertTrue(self.steering()) + self.advance(10_000) + self.feed(stock=True, **physical) + self.assertFalse(self.steering()) + self.advance(10_000) + self.feed(stock=True) + self.assertFalse(self.steering()) + self.stock_frame(False) + self.stock_frame(True) + self.assertTrue(self.steering()) + self.reset(0xE804, 0) + self.warm(stock=True) + self.advance(100_001) + self.assertFalse(self.steering()) + self.feed(stock=True) + self.assertFalse(self.steering()) + self.main_frame(False) + self.main_frame(True) + self.assertTrue(self.steering()) + + def test_active_temporary_eps_preserves_physical_latch_and_suppresses_outputs(self): + for word in range(0xE800, 0xE804): + for alternative in (0, 32): + self.reset(word, alternative) + self.warm() + self.arm() + self.assertTrue(self.steering()) + self.advance(10_000) + self.feed(eps=2) + self.assertFalse(self.steering()) + self.assertTrue(self.steering(0, False)) + self.assertFalse(self.pedal()) + self.assertFalse(self.tx(0x1E1, self.button(0x5F8C))) + if alternative == 32: + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.advance(10_000) + self.feed() + self.assertTrue(self.safety.get_controls_allowed()) + self.assertTrue(self.steering()) + if word in (0xE802, 0xE803): + self.assertTrue(self.pedal(counter=1)) + self.advance(10_000) + self.feed(eps=3) + self.assertFalse(self.steering()) + self.advance(10_000) + self.feed() + self.assertFalse(self.safety.get_controls_allowed()) + if alternative == 0: + self.assertFalse(self.steering()) + + def test_temporary_eps_cannot_create_a_new_physical_engagement(self): + for word in range(0xE800, 0xE806): + self.reset(word, 0) + self.warm(stock=False) + self.advance(10_000) + self.feed(eps=2) + if word >= 0xE804: + self.stock_frame(True) + else: + self.slot(0x55AE) + self.assertFalse(self.safety.get_controls_allowed()) + self.advance(10_000) + self.feed(stock=word >= 0xE804) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertFalse(self.steering()) diff --git a/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc b/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc index 34fe2944aa..e27607695c 100644 --- a/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc +++ b/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc @@ -243,6 +243,7 @@ int main() { 0xE604U, 0xE605U, 0xE606U, 0xE607U, 0xE610U, 0xE611U, 0xE612U, 0xE613U, 0xE614U, 0xE615U, 0xE616U, 0xE617U, + 0xE800U, 0xE801U, 0xE802U, 0xE803U, 0xE804U, 0xE805U, 0xE700U, 0xE701U, 0xE702U, 0xE703U, 0xE710U, 0xE711U, 0xE712U, 0xE713U, 0xD100U, 0xD101U, 0xD102U, 0xD103U, 0xD104U, 0xD105U, @@ -274,7 +275,7 @@ int main() { 0xE3FFU, 0xE404U, 0xE414U, 0xE424U, 0xE430U, 0xE444U, 0xE450U, 0xE464U, 0xE470U, 0xE4FFU, 0xE504U, 0xE514U, 0xE524U, 0xE530U, 0xE544U, 0xE550U, 0xE564U, 0xE570U, 0xE5FFU, 0xE608U, 0xE60FU, 0xE618U, 0xE61FU, 0xE620U, - 0xE6FFU, 0xE704U, 0xE70FU, 0xE714U, 0xE71FU, 0xE720U}) { + 0xE806U, 0xE80FU, 0xE8FFU, 0xE6FFU, 0xE704U, 0xE70FU, 0xE714U, 0xE71FU, 0xE720U}) { assert(!gm_aol_param(word)); auto unknown = status(); unknown.safety_mode = GM_AOL_PROFILE.mode; diff --git a/openpilot/starpilot/aol/tests/test_gm.py b/openpilot/starpilot/aol/tests/test_gm.py index 265b7aa2fd..3f9c219aa5 100644 --- a/openpilot/starpilot/aol/tests/test_gm.py +++ b/openpilot/starpilot/aol/tests/test_gm.py @@ -36,6 +36,17 @@ BOLT_IDS = (CAR.CHEVROLET_BOLT_CC_2017, CAR.CHEVROLET_BOLT_CC_2018_2021, def _configurations(): + from opendbc.car.gm.tests.test_ordinary_cc import malibu_hybrid_params + from opendbc.car.gm.startup_preferences import prepare_disable_longitudinal + for pedal in (False, True): + for removed in (False, True): + for alternate in (False, True): + for radar in ((False, True) if pedal else (False,)): + cp = malibu_hybrid_params(pedal=pedal, removed=removed, alternate=alternate, radar=radar) + yield cp + disabled = cp.as_reader().as_builder() + prepare_disable_longitudinal(disabled, True) + yield disabled from opendbc.car.gm.tests.test_bolt_pedal import params as pedal_params for identity in BOLT_IDS: cp = pedal_params(identity, pedal=True, removed=True) diff --git a/openpilot/starpilot/car/gm/aol.py b/openpilot/starpilot/car/gm/aol.py index 9f3dfd4657..bef7dff93c 100644 --- a/openpilot/starpilot/car/gm/aol.py +++ b/openpilot/starpilot/car/gm/aol.py @@ -55,6 +55,11 @@ def create_intent(cp, settings): def configuration_settings_policy(cp): """Saving stock Silverado lateral intent does not establish its physical source graph.""" try: + if (cp.brand == 'gm' and cp.carFingerprint == CAR.CHEVROLET_MALIBU_HYBRID_CC and + not cp.notCar and not cp.passive and cp.lateralTuning.which() == 'torque' and + cp.steerControlType == CarParams.SteerControlType.torque and + int(cp.alternativeExperience) in (0, GM_AOL_ALTERNATIVE_EXPERIENCE)): + return AolVehiclePolicy(settings_supported=True) if (cp.brand != 'gm' or cp.carFingerprint != CAR.CHEVROLET_SILVERADO_CC or cp.notCar or cp.passive or cp.networkLocation != CarParams.NetworkLocation.gateway or cp.transmissionType != CarParams.TransmissionType.automatic or not cp.radarUnavailable or diff --git a/openpilot/starpilot/car/gm/aol_policy.h b/openpilot/starpilot/car/gm/aol_policy.h index e9cdcf4d88..19b896bd77 100644 --- a/openpilot/starpilot/car/gm/aol_policy.h +++ b/openpilot/starpilot/car/gm/aol_policy.h @@ -30,6 +30,7 @@ inline bool gm_aol_param(uint16_t param) { case 0xE604U: case 0xE605U: case 0xE606U: case 0xE607U: case 0xE610U: case 0xE611U: case 0xE612U: case 0xE613U: case 0xE614U: case 0xE615U: case 0xE616U: case 0xE617U: + case 0xE800U: case 0xE801U: case 0xE802U: case 0xE803U: case 0xE804U: case 0xE805U: case 0xE700U: case 0xE701U: case 0xE702U: case 0xE703U: case 0xE710U: case 0xE711U: case 0xE712U: case 0xE713U: case 0xD100U: case 0xD101U: case 0xD102U: case 0xD103U: diff --git a/openpilot/starpilot/lateral/ordinary_cc_policy.py b/openpilot/starpilot/lateral/ordinary_cc_policy.py index 871288b3f7..eef1fcc18a 100644 --- a/openpilot/starpilot/lateral/ordinary_cc_policy.py +++ b/openpilot/starpilot/lateral/ordinary_cc_policy.py @@ -3,13 +3,13 @@ import math import numpy as np -from opendbc.car.gm.values import CAR, is_ordinary_cc_profile, is_conventional_cc_pedal_profile +from opendbc.car.gm.values import CAR, is_ordinary_cc_profile, is_conventional_cc_pedal_profile, malibu_hybrid_profile from opendbc.car.structs import CarParams from openpilot.starpilot.lateral.gm_ordinary_policy import GMOrdinaryTorquePolicy def supported_cp(cp): - if (not (is_ordinary_cc_profile(cp) or is_conventional_cc_pedal_profile(cp)) or + if (not (is_ordinary_cc_profile(cp) or is_conventional_cc_pedal_profile(cp) or malibu_hybrid_profile(cp) is not None) or cp.steerControlType != CarParams.SteerControlType.torque or cp.lateralTuning.which() != 'torque'): return False tune = cp.lateralTuning.torque diff --git a/openpilot/starpilot/longitudinal/inputs.py b/openpilot/starpilot/longitudinal/inputs.py index 8b7c134ff7..544e969cf1 100644 --- a/openpilot/starpilot/longitudinal/inputs.py +++ b/openpilot/starpilot/longitudinal/inputs.py @@ -12,6 +12,7 @@ from openpilot.starpilot.longitudinal.toyota_output_policy import (CLOCK_PAIR_MA from openpilot.starpilot.longitudinal.ioniq6_start import StartEvidence, eligible as ioniq6_start_eligible from opendbc.car.gm.conventional_pedal import policy_for as conventional_pedal_policy_for from opendbc.car.gm.ordinary_cc import policy_for as ordinary_cc_policy_for +from opendbc.car.gm.hybrid_cc import policy_for as hybrid_policy_for from opendbc.car.gm.suburban import SuburbanStopEvidence from opendbc.car.gm.cc_longitudinal import VoltCcEvidence, policy_for as volt_cc_policy_for from opendbc.car.gm.longitudinal import ( @@ -80,7 +81,8 @@ class LongitudinalInputs: evidence_type = getattr(stop_policy, 'evidence_type', None) self.gm_volt_enabled = evidence_type is VoltStopEvidence self.resume_freshness = ResumeFreshness() - self.gm_cc_enabled = (conventional_pedal_policy_for(self.CP) or ordinary_cc_policy_for(self.CP) or volt_cc_policy_for(self.CP)) is not None + self.gm_cc_enabled = (conventional_pedal_policy_for(self.CP) or ordinary_cc_policy_for(self.CP) or + volt_cc_policy_for(self.CP) or hybrid_policy_for(self.CP)) is not None self.gm_cc_stop_enabled = evidence_type is VoltCcEvidence if self.gm_cc_enabled or self.gm_cc_stop_enabled: self.gm_cc_evidence = None diff --git a/openpilot/starpilot/longitudinal/vehicle_policy.py b/openpilot/starpilot/longitudinal/vehicle_policy.py index 1d2c3a1da1..4150647789 100644 --- a/openpilot/starpilot/longitudinal/vehicle_policy.py +++ b/openpilot/starpilot/longitudinal/vehicle_policy.py @@ -2,6 +2,7 @@ from opendbc.car.gm.conventional_pedal import policy_for as conventional_pedal_policy_for from opendbc.car.gm.camera import policy_for as camera_policy_for +from opendbc.car.gm.hybrid_cc import policy_for as hybrid_policy_for from opendbc.car.gm.ordinary_cc import policy_for as ordinary_cc_policy_for from opendbc.car.gm.bolt_cc import policy_for as bolt_cc_policy_for from opendbc.car.gm.longitudinal import policy_for as gm_pedal_policy_for, volt_policy_for, euv_policy_for, ascm_policy_for, sdgm_policy_for @@ -14,7 +15,7 @@ from opendbc.car.hyundai.g90_longitudinal import (stopping_decel_rate as hyundai def installed_policy_for(cp): - return (suburban_policy_for(cp) or conventional_pedal_policy_for(cp) or camera_policy_for(cp) or ordinary_cc_policy_for(cp) or + return (hybrid_policy_for(cp) or suburban_policy_for(cp) or conventional_pedal_policy_for(cp) or camera_policy_for(cp) or ordinary_cc_policy_for(cp) or sdgm_policy_for(cp) or ascm_policy_for(cp) or gm_pedal_policy_for(cp) or volt_policy_for(cp) or euv_policy_for(cp) or volt_cc_policy_for(cp) or bolt_cc_policy_for(cp)) diff --git a/openpilot/starpilot/ui/feature_settings_owner.py b/openpilot/starpilot/ui/feature_settings_owner.py index 6ae405d64c..0a4eb9555b 100644 --- a/openpilot/starpilot/ui/feature_settings_owner.py +++ b/openpilot/starpilot/ui/feature_settings_owner.py @@ -198,7 +198,8 @@ class FeatureSettingsOwner: cp = self.vehicle_params() try: if (cp is None or cp.brand != "gm" or cp.notCar or - cp.carFingerprint not in ORDINARY_CC_CAR | PEDAL_BOLT_CAR | CAMERA_ACC_PEDAL_CAR | {CAR.CHEVROLET_SILVERADO_CC, CAR.CHEVROLET_VOLT_CC}): + cp.carFingerprint not in ORDINARY_CC_CAR | PEDAL_BOLT_CAR | CAMERA_ACC_PEDAL_CAR | + {CAR.CHEVROLET_SILVERADO_CC, CAR.CHEVROLET_VOLT_CC, CAR.CHEVROLET_MALIBU_HYBRID_CC}): return None return (str(cp.carFingerprint), str(cp.brand), int(cp.flags), bool(cp.passive), bool(cp.dashcamOnly), bool(cp.openpilotLongitudinalControl), diff --git a/openpilot/starpilot/ui/tests/test_feature_settings_owner.py b/openpilot/starpilot/ui/tests/test_feature_settings_owner.py index 6148e0045f..3636c9e57f 100644 --- a/openpilot/starpilot/ui/tests/test_feature_settings_owner.py +++ b/openpilot/starpilot/ui/tests/test_feature_settings_owner.py @@ -31,6 +31,33 @@ def required_document(raw: object) -> dict: class FeatureSettingsOwnerTests(unittest.TestCase): + def test_hybrid_manual_settings_and_automatic_pedal_owner(self): + from opendbc.car import gen_empty_fingerprint + from opendbc.car.gm.interface import CarInterface + from opendbc.car.gm.values import CAR + from opendbc.car.gm.aol import qualified_gm + from opendbc.car.gm.tests.test_ordinary_cc import malibu_hybrid_params + from opendbc.car.gm.startup_preferences import prepare_disable_longitudinal + cp = CarInterface.get_params(CAR.CHEVROLET_MALIBU_HYBRID_CC, gen_empty_fingerprint(), [], False, False, False) + self.fingerprint = cp.carFingerprint + self.owner.vehicle_params = lambda: cp + self.owner.configuration_vehicle = lambda: True + original = cp.to_bytes() + row = self._row('aol', 'AlwaysOnLateral') + self.assertTrue(row.available) + self.assertTrue(self.owner.apply(required_change(row))) + self.assertFalse(qualified_gm(cp)) + self.assertEqual(cp.to_bytes(), original) + self.owner.configuration_vehicle = lambda: False + cp = malibu_hybrid_params(pedal=True, removed=True) + row = self._row('vehicle', 'GMPedalLongitudinal') + self.assertEqual(row.value, 'Automatic') + self.assertFalse(row.available) + self.assertIn('Activates automatically', row.reason) + prepare_disable_longitudinal(cp, True) + self.assertFalse(cp.openpilotLongitudinalControl) + self.assertEqual(self._row('vehicle', 'GMPedalLongitudinal').value, 'Detected, inactive') + def test_prius_retrofit_manual_settings_do_not_grant_runtime(self): from opendbc.car import gen_empty_fingerprint, structs from opendbc.car.toyota.interface import CarInterface diff --git a/tools/ci/run_local_tests.py b/tools/ci/run_local_tests.py index 15c91fffb8..50f87045c2 100755 --- a/tools/ci/run_local_tests.py +++ b/tools/ci/run_local_tests.py @@ -45,9 +45,11 @@ QUICK_TARGETS = ( 'opendbc_repo/opendbc/car/gm/tests/test_bolt_pedal.py', 'opendbc_repo/opendbc/car/gm/tests/test_camera_acc_pedal.py', 'opendbc_repo/opendbc/car/gm/tests/test_volt_transitions.py', + 'opendbc_repo/opendbc/car/gm/tests/test_ordinary_cc.py::TestMalibuHybridCc', 'opendbc_repo/opendbc/safety/tests/test_gm_bolt_pedal.py', 'opendbc_repo/opendbc/safety/tests/test_gm_camera_acc_pedal.py', 'opendbc_repo/opendbc/safety/tests/test_gm_cc_pedal.py', + 'opendbc_repo/opendbc/safety/tests/test_gm_hybrid_cc.py', 'opendbc_repo/opendbc/car/tesla/tests/test_screen_button.py', 'opendbc_repo/opendbc/safety/tests/test_tesla_screen_button.py', 'opendbc_repo/opendbc/safety/tests/test_toyota.py', diff --git a/tools/ci/run_vehicle_tests.py b/tools/ci/run_vehicle_tests.py index 014f51400b..fc367a34d9 100644 --- a/tools/ci/run_vehicle_tests.py +++ b/tools/ci/run_vehicle_tests.py @@ -102,6 +102,7 @@ RELEASE_TARGETS = ( "opendbc_repo/opendbc/safety/tests/test_gm_ordinary_ascm.py", "opendbc_repo/opendbc/safety/tests/test_gm_ordinary_sdgm.py", "opendbc_repo/opendbc/safety/tests/test_gm_ordinary_cc.py", + "opendbc_repo/opendbc/safety/tests/test_gm_hybrid_cc.py", "opendbc_repo/opendbc/car/gm/tests/test_ordinary_cc.py", "opendbc_repo/opendbc/safety/tests/test_gm_ordinary_camera.py", "opendbc_repo/opendbc/car/gm/tests/test_ordinary_camera.py",