mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
Malibu Hybrid Cruise Control
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}],
|
||||
|
||||
@@ -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()
|
||||
@@ -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,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)):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user