Malibu Hybrid Cruise Control

This commit is contained in:
firestar5683
2026-10-06 15:26:49 -05:00
parent fb4a28774e
commit c8de87e680
31 changed files with 1609 additions and 42 deletions
+3 -2
View File
@@ -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
+57 -4
View File
@@ -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
+64 -3
View File
@@ -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),
@@ -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
@@ -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
}],
+272
View File
@@ -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()
+31 -1
View File
@@ -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
+2 -2
View File
@@ -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)):
+1 -1
View File
@@ -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):
+2 -2
View File
@@ -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
@@ -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
@@ -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)
+49 -1
View File
@@ -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
@@ -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]
+3 -2
View File
@@ -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;
@@ -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:
+32 -16
View File
@@ -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;
}
@@ -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;
}
@@ -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];
@@ -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())
@@ -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;
+11
View File
@@ -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)
+5
View File
@@ -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
+1
View File
@@ -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:
@@ -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
+3 -1
View File
@@ -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
@@ -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))
@@ -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),
@@ -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
+2
View File
@@ -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',
+1
View File
@@ -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",