mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-04 03:24:06 +08:00
Volt One-Pedal
This commit is contained in:
@@ -16,6 +16,8 @@ GM_BASE_GATEWAY_IDS = frozenset((CAR.GMC_ACADIA, CAR.BUICK_LACROSSE, CAR.CADILLA
|
||||
CAR.CADILLAC_ESCALADE_ESV, CAR.CADILLAC_ESCALADE_ESV_2019, CAR.CHEVROLET_SUBURBAN))
|
||||
GM_AOL_WORDS = frozenset((
|
||||
0, 0x80,
|
||||
0xD100, 0xD101, 0xD102, 0xD103, 0xD104, 0xD105, 0xD106, 0xD107, 0xD108, 0xD109, 0xD10A,
|
||||
0xD110, 0xD111, 0xD112, 0xD113, 0xD114, 0xD115, 0xD116, 0xD117, 0xD118, 0xD119, 0xD11A,
|
||||
0x1001, 0x1401, 0x3001, 0x3401, 0x1003, 0x1403,
|
||||
0x201, 0x601, 0xA01, 0xE01, 0x203, 0x603, 0xA03, 0xE03,
|
||||
5, 7, 20, 0xBD, 0x9D, 0x19D, 0x1CD,
|
||||
|
||||
@@ -18,7 +18,10 @@ class AutoHoldConfig:
|
||||
|
||||
def config_for(cp) -> AutoHoldConfig:
|
||||
from opendbc.car.gm.values import is_gm_auto_hold, is_volt_sdgm_profile
|
||||
return AutoHoldConfig(100, .25) if is_gm_auto_hold(cp) and is_volt_sdgm_profile(cp, longitudinal=True) else AutoHoldConfig()
|
||||
if is_gm_auto_hold(cp) and is_volt_sdgm_profile(cp, longitudinal=True):
|
||||
solo = 0xD100 <= int(cp.safetyConfigs[0].safetyParam) <= 0xD10A
|
||||
return AutoHoldConfig(100, .02 if solo else .25)
|
||||
return AutoHoldConfig()
|
||||
|
||||
|
||||
def stopped_for_hold(cs, config: AutoHoldConfig, sent_hold: bool) -> bool:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from opendbc.car.gm.one_pedal import VoltOnePedal
|
||||
from opendbc.car.gm.values import gm_control_word, is_volt_one_pedal
|
||||
from opendbc.car.gm.longitudinal import volt_sng_release
|
||||
from opendbc.car.gm.auto_hold import AutoHold, config_for as auto_hold_config_for
|
||||
from opendbc.car.gm.auto_hold import AutoHold, config_for as auto_hold_config_for, hold_brake as estimate_hold_brake
|
||||
from opendbc.car.gm.values import is_gm_auto_hold
|
||||
from opendbc.car.gm.ordinary import demands as ascm_demands
|
||||
import math
|
||||
@@ -225,6 +227,8 @@ class CarController(CarControllerBase):
|
||||
self.pedal_active_last = False
|
||||
self.volt_sng = False
|
||||
self.volt_sng_plan_input = None
|
||||
self.volt_one_pedal = False
|
||||
self.volt_one_pedal_input = None
|
||||
self.gm_auto_hold = False
|
||||
self.gm_auto_hold_input = None
|
||||
self.gm_auto_hold_state = AutoHold(config=auto_hold_config_for(CP))
|
||||
@@ -242,6 +246,7 @@ class CarController(CarControllerBase):
|
||||
self.lka_icon_status_last = (False, False)
|
||||
|
||||
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)
|
||||
self.volt_gateway_long = is_volt_gateway_longitudinal(self.CP)
|
||||
self.volt_ascm_long = is_volt_ascm_longitudinal(self.CP)
|
||||
@@ -348,16 +353,36 @@ class CarController(CarControllerBase):
|
||||
def update(self, CC, CS, now_nanos):
|
||||
# Sample physical hold dwell at parser/controller cadence, before current brake demands.
|
||||
hold_brake = None
|
||||
one_pedal_enabled = (self.volt_one_pedal and is_volt_one_pedal(self.CP) and self.volt_one_pedal_input is not None and
|
||||
self.volt_one_pedal_input.update(now_nanos))
|
||||
one_pedal_active = False
|
||||
if is_gm_auto_hold(self.CP):
|
||||
hold_enabled = self.gm_auto_hold and self.gm_auto_hold_input is not None and self.gm_auto_hold_input.update(now_nanos)
|
||||
physical_current = (bool(CS.gm_auto_hold_sources) and
|
||||
all(0 < stamp <= now_nanos and now_nanos - stamp <= limit for stamp, limit in CS.gm_auto_hold_sources) and
|
||||
not CS.gm_auto_hold_unavailable)
|
||||
one_pedal_ready = (one_pedal_enabled and physical_current and CS.volt_one_pedal_mode and
|
||||
0 < CS.volt_one_pedal_mode_ns <= now_nanos and now_nanos - CS.volt_one_pedal_mode_ns <= 300_000_000 and
|
||||
CS.out.cruiseState.available and CS.gm_auto_hold_forward and
|
||||
not CS.out.gasPressed and not CS.out.brakePressed and not CS.out.regenBraking)
|
||||
hold_brake = self.gm_auto_hold_state.update(
|
||||
CS.out, enabled=hold_enabled, sources_current=all(0 < stamp <= now_nanos and now_nanos - stamp <= limit
|
||||
CS.out, enabled=hold_enabled or one_pedal_enabled, sources_current=all(0 < stamp <= now_nanos and now_nanos - stamp <= limit
|
||||
for stamp, limit in CS.gm_auto_hold_sources) and bool(CS.gm_auto_hold_sources) and
|
||||
not CS.gm_auto_hold_unavailable, long_active=CC.longActive, driver_brake=CS.gm_auto_hold_brake,
|
||||
controller_brake=self.apply_brake, wheel_ns=CS.gm_auto_hold_wheel_ns, now_ns=now_nanos,
|
||||
physical_forward=CS.gm_auto_hold_forward, moving=CS.gm_auto_hold_moving, sent_hold=CS.gm_auto_hold_engaged)
|
||||
if hold_brake is None:
|
||||
if not hold_enabled:
|
||||
hold_brake = None
|
||||
one_pedal_active = (one_pedal_ready and self.gm_auto_hold_state.drive_ns >= 3_000_000_000 and
|
||||
CS.out.canValid and not CS.out.canTimeout and not CC.longActive and
|
||||
(CS.volt_one_pedal_moving or CS.volt_one_pedal_stopped))
|
||||
if hold_brake is None and not (one_pedal_active and CS.volt_one_pedal_stopped and
|
||||
(CS.out.standstill or CS.out.vEgo < .02)):
|
||||
CS.gm_auto_hold_engaged = False
|
||||
if self.volt_one_pedal_state is not None:
|
||||
self.volt_one_pedal_state.observe(eligible=one_pedal_ready and self.gm_auto_hold_state.drive_ns >= 3_000_000_000 and
|
||||
not CC.longActive, gas_pressed=CS.out.gasPressed,
|
||||
speed=CS.out.vEgo)
|
||||
actuators = CC.actuators
|
||||
hud_control = CC.hudControl
|
||||
hud_alert = hud_control.visualAlert
|
||||
@@ -630,6 +655,25 @@ class CarController(CarControllerBase):
|
||||
self.ordinary_camera_long and not self.ordinary_camera_removed):
|
||||
can_sends.append(gmcan.create_acc_2cd_command(CanBus.POWERTRAIN, idx))
|
||||
|
||||
if self.volt_one_pedal_state is not None:
|
||||
one_pedal_brake = self.volt_one_pedal_state.update(
|
||||
eligible=one_pedal_active, speed=CS.out.vEgo, acceleration=CS.out.aEgo,
|
||||
steering_angle=CS.out.steeringAngleDeg, orientation=CC.orientationNED)
|
||||
if one_pedal_active:
|
||||
one_pedal_stop = CS.volt_one_pedal_stopped and (CS.out.standstill or CS.out.vEgo < .02)
|
||||
if hold_brake is None and one_pedal_stop and one_pedal_brake > 0:
|
||||
hold_brake = max(one_pedal_brake, self.gm_auto_hold_state.brake or estimate_hold_brake(
|
||||
0., one_pedal_brake, self.gm_auto_hold_state.config.minimum_brake))
|
||||
elif hold_brake is not None:
|
||||
hold_brake = max(hold_brake, one_pedal_brake)
|
||||
self.apply_gas = self.params.INACTIVE_REGEN
|
||||
self.apply_brake = max(self.apply_brake, one_pedal_brake)
|
||||
|
||||
one_pedal_braking = one_pedal_active and self.volt_one_pedal_state is not None and self.volt_one_pedal_state.brake > 0
|
||||
if one_pedal_braking:
|
||||
at_full_stop = at_full_stop or CS.out.cruiseState.standstill
|
||||
near_stop = near_stop or CS.out.vEgo < self.params.NEAR_STOP_BRAKE_PHASE
|
||||
|
||||
# Resume releases only the gas/regen ACC-state bit; friction remains separately owned.
|
||||
acc_engaged = CC.enabled and not volt_sng_release(
|
||||
self.CP, self.volt_sng, CC, CS, now_nanos,
|
||||
@@ -641,7 +685,8 @@ class CarController(CarControllerBase):
|
||||
at_full_stop = CS.out.cruiseState.standstill
|
||||
near_stop = CS.out.vEgo < self.params.NEAR_STOP_BRAKE_PHASE
|
||||
can_sends.append(gmcan.create_friction_brake_command(self.packer_ch, friction_brake_bus, self.apply_brake,
|
||||
idx, CC.enabled, near_stop, at_full_stop, self.CP, auto_hold=hold_brake is not None))
|
||||
idx, CC.enabled, near_stop, at_full_stop, self.CP,
|
||||
auto_hold=hold_brake is not None or one_pedal_braking))
|
||||
CS.gm_auto_hold_engaged = hold_brake is not None
|
||||
|
||||
if self.bolt_euv_long:
|
||||
@@ -708,7 +753,7 @@ class CarController(CarControllerBase):
|
||||
if self.cancel_counter > CAMERA_CANCEL_DELAY_FRAMES and (not cc_gateway or
|
||||
(button_fresh and stock_cruise_fresh and CS.out.cruiseState.available and CS.out.cruiseState.enabled)):
|
||||
self.last_button_frame = self.frame
|
||||
cancel_bus = (CanBus.POWERTRAIN if cc_gateway or self.CP.safetyConfigs[0].safetyParam & GMSafetyFlags.SDGM_CANCEL_PT.value
|
||||
cancel_bus = (CanBus.POWERTRAIN if cc_gateway or gm_control_word(self.CP) & GMSafetyFlags.SDGM_CANCEL_PT.value
|
||||
else CanBus.CAMERA)
|
||||
can_sends.append(gmcan.create_buttons(self.packer_pt, cancel_bus, CS.buttons_counter, CruiseButtons.CANCEL))
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from opendbc.car.gm.values import gm_control_word, is_volt_one_pedal
|
||||
from opendbc.car.gm.values import is_volt_longitudinal, is_gm_auto_hold
|
||||
from opendbc.car.gm.auto_hold import config_for as auto_hold_config_for, stopped_for_hold
|
||||
import copy
|
||||
@@ -66,6 +67,10 @@ class CarState(CarStateBase):
|
||||
self.gm_auto_hold_wheel_ns = 0
|
||||
self.gm_auto_hold_unavailable = True
|
||||
self.gm_auto_hold_engaged = False
|
||||
self.volt_one_pedal_mode = False
|
||||
self.volt_one_pedal_mode_ns = 0
|
||||
self.volt_one_pedal_moving = False
|
||||
self.volt_one_pedal_stopped = False
|
||||
self.cc_gateway_cruise_ts_nanos = 0
|
||||
self.cc_gateway_buttons_ts_nanos = 0
|
||||
self.camera_stock_status_ts_nanos = 0
|
||||
@@ -200,8 +205,8 @@ class CarState(CarStateBase):
|
||||
alternate = is_volt_gateway_alternate_brake(self.CP) or (
|
||||
is_volt_camera_removed(self.CP) and bool(self.CP.flags & GMFlags.NO_ACCELERATOR_POS_MSG))
|
||||
c9 = self.CP.networkLocation == NetworkLocation.fwdCamera and not (
|
||||
self.CP.safetyConfigs[0].safetyParam & (GMSafetyFlags.ASCM_INTERCEPT | GMSafetyFlags.SDGM) and
|
||||
not self.CP.safetyConfigs[0].safetyParam & GMSafetyFlags.BRAKE_C9)
|
||||
gm_control_word(self.CP) & (GMSafetyFlags.ASCM_INTERCEPT | GMSafetyFlags.SDGM) and
|
||||
not gm_control_word(self.CP) & GMSafetyFlags.BRAKE_C9)
|
||||
brake = (("EBCMBrakePedalPosition", "BrakePedalPosition") if alternate else
|
||||
("ECMEngineStatus", "BrakePressed") if c9 else ("ECMAcceleratorPos", "BrakePedalPos"))
|
||||
names = (("AcceleratorPedal2", "CruiseState", 100_000_000),
|
||||
@@ -219,8 +224,8 @@ class CarState(CarStateBase):
|
||||
alternate = is_volt_gateway_alternate_brake(self.CP)
|
||||
alternate_force = alternate or (is_volt_camera_removed(self.CP) and bool(self.CP.flags & GMFlags.NO_ACCELERATOR_POS_MSG))
|
||||
c9 = self.CP.networkLocation == NetworkLocation.fwdCamera and (
|
||||
not self.CP.safetyConfigs[0].safetyParam & (GMSafetyFlags.ASCM_INTERCEPT | GMSafetyFlags.SDGM) or
|
||||
bool(self.CP.safetyConfigs[0].safetyParam & GMSafetyFlags.BRAKE_C9))
|
||||
not gm_control_word(self.CP) & (GMSafetyFlags.ASCM_INTERCEPT | GMSafetyFlags.SDGM) or
|
||||
bool(gm_control_word(self.CP) & GMSafetyFlags.BRAKE_C9))
|
||||
brake_name, brake_signal = (("EBCMBrakePedalPosition", "BrakePedalPosition") if alternate else
|
||||
("ECMEngineStatus", "BrakePressed") if c9 else ("ECMAcceleratorPos", "BrakePedalPos"))
|
||||
names = (("ECMEngineStatus", "CruiseMainOn"), ("ECMPRDNL2", "PRNDL2"), (brake_name, brake_signal),
|
||||
@@ -232,7 +237,7 @@ class CarState(CarStateBase):
|
||||
pt_cp.vl[name]
|
||||
self.gm_auto_hold_sources = tuple((pt_cp.ts_nanos[name][signal], 300_000_000) for name, signal in names)
|
||||
# C9 is pressed authority, never analog force; missing-BE profiles retain minimum hold.
|
||||
absent_be = c9 and bool(self.CP.safetyConfigs[0].safetyParam & (GMSafetyFlags.ASCM_INTERCEPT | GMSafetyFlags.SDGM))
|
||||
absent_be = c9 and bool(gm_control_word(self.CP) & (GMSafetyFlags.ASCM_INTERCEPT | GMSafetyFlags.SDGM))
|
||||
if alternate_force:
|
||||
self.gm_auto_hold_brake = pt_cp.vl["EBCMBrakePedalPosition"]["BrakePedalPosition"] / 208.
|
||||
if c9:
|
||||
@@ -247,6 +252,16 @@ class CarState(CarStateBase):
|
||||
self.gm_auto_hold_moving = wheels["RLWheelSpd"] >= 12 * .0311 and wheels["RRWheelSpd"] >= 12 * .0311
|
||||
self.gm_auto_hold_wheel_ns = pt_cp.ts_nanos["EBCMWheelSpdRear"]["RLWheelSpd"]
|
||||
self.gm_auto_hold_unavailable = bool(pt_cp.vl["EBCMFrictionBrakeStatus"]["FrictionBrakeUnavailable"])
|
||||
if is_volt_one_pedal(self.CP):
|
||||
gear = pt_cp.vl["ECMPRDNL2"]
|
||||
low = gear["PRNDL2"] == 6 and not gear["ManualMode"]
|
||||
mode_ns = pt_cp.ts_nanos["EVDriveMode"]["SinglePedalModeActive"]
|
||||
mode_current = 0 < mode_ns <= pt_cp._last_update_nanos and pt_cp._last_update_nanos - mode_ns <= 300_000_000
|
||||
self.volt_one_pedal_mode_ns = pt_cp.ts_nanos["ECMPRDNL2"]["PRNDL2"] if low else mode_ns
|
||||
self.volt_one_pedal_mode = low or (mode_current and bool(pt_cp.vl["EVDriveMode"]["SinglePedalModeActive"]))
|
||||
rear = pt_cp.vl["EBCMWheelSpdRear"]
|
||||
self.volt_one_pedal_moving = rear["RLWheelSpd"] > 10 * .0311 and rear["RRWheelSpd"] > 10 * .0311
|
||||
self.volt_one_pedal_stopped = rear["RLWheelSpd"] <= 10 * .0311 and rear["RRWheelSpd"] <= 10 * .0311
|
||||
self.pscm_status = copy.copy(pt_cp.vl["PSCMStatus"])
|
||||
|
||||
# Variables used for avoiding LKAS faults
|
||||
@@ -283,9 +298,9 @@ class CarState(CarStateBase):
|
||||
else:
|
||||
ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(pt_cp.vl["ECMPRDNL2"]["PRNDL2"], None))
|
||||
|
||||
source_be_brake = (self.CP.safetyConfigs[0].safetyParam &
|
||||
source_be_brake = (gm_control_word(self.CP) &
|
||||
(GMSafetyFlags.ASCM_INTERCEPT | GMSafetyFlags.SDGM).value and
|
||||
not self.CP.safetyConfigs[0].safetyParam & GMSafetyFlags.BRAKE_C9.value)
|
||||
not gm_control_word(self.CP) & GMSafetyFlags.BRAKE_C9.value)
|
||||
if is_conventional_cc_pedal_profile(self.CP) and self.CP.carFingerprint == CAR.CHEVROLET_MALIBU_CC:
|
||||
ret.brakePressed = (pt_cp.vl["EBCMBrakePedalPosition"]["BrakePedalPosition"] / 0xD0 >= .10
|
||||
if self.CP.flags & GMFlags.NO_ACCELERATOR_POS_MSG else
|
||||
@@ -451,6 +466,8 @@ class CarState(CarStateBase):
|
||||
hold_sources_current and not self.gm_auto_hold_unavailable and self.gm_auto_hold_forward and
|
||||
ret.cruiseState.available and hold_stopped and
|
||||
not ret.gasPressed and not ret.regenBraking)
|
||||
if is_volt_one_pedal(self.CP) and int(self.CP.safetyConfigs[0].safetyParam) < 0xD110:
|
||||
hold_current = hold_current and self.volt_one_pedal_mode and self.volt_one_pedal_stopped and not ret.brakePressed
|
||||
if not hold_current:
|
||||
self.gm_auto_hold_engaged = False
|
||||
ret.brakeHoldActive = bool(hold_current and self.gm_auto_hold_engaged and ret.standstill)
|
||||
@@ -468,6 +485,8 @@ class CarState(CarStateBase):
|
||||
pt_messages.append(("ECMAcceleratorPos", 10))
|
||||
if is_volt_gateway_alternate_brake(CP):
|
||||
pt_messages.append(("EBCMBrakePedalPosition", 100))
|
||||
if is_volt_one_pedal(CP):
|
||||
pt_messages.append(("EVDriveMode", float('nan')))
|
||||
if CP.carFingerprint in VOLT_BSM_CAR and CP.flags & GMFlags.HAS_BSM.value:
|
||||
pt_messages.append(("BCMBlindSpotMonitor", float('nan')))
|
||||
if CP.flags & GMFlags.PEDAL_LONG.value:
|
||||
@@ -490,7 +509,7 @@ class CarState(CarStateBase):
|
||||
("BCMDoorBeltStatus", 10), ("BCMGeneralPlatformStatus", 10),
|
||||
("ASCMSteeringButton", 33),
|
||||
]
|
||||
if not CP.safetyConfigs[0].safetyParam & GMSafetyFlags.BRAKE_C9.value:
|
||||
if not gm_control_word(CP) & GMSafetyFlags.BRAKE_C9.value:
|
||||
pt_messages.append(("ECMAcceleratorPos", 80))
|
||||
if (CP.carFingerprint in CC_GATEWAY_STOCK_CAR or CP.carFingerprint == CAR.CHEVROLET_VOLT_CC):
|
||||
# No camera or ACC status dependency on this gateway conventional-cruise path.
|
||||
@@ -538,7 +557,7 @@ class CarState(CarStateBase):
|
||||
("ECMEngineStatus", 100), ("AcceleratorPedal2", 33), ("ECMPRDNL2", 10),
|
||||
("EBCMWheelSpdRear", 20), ("EBCMRegenPaddle", 40)]
|
||||
|
||||
if CP.carFingerprint == CAR.CHEVROLET_VOLT_2019 and CP.safetyConfigs[0].safetyParam & GMSafetyFlags.BRAKE_C9.value:
|
||||
if CP.carFingerprint == CAR.CHEVROLET_VOLT_2019 and gm_control_word(CP) & GMSafetyFlags.BRAKE_C9.value:
|
||||
pt_messages = [(name, frequency) for name, frequency in pt_messages if name != "ECMAcceleratorPos"]
|
||||
|
||||
if is_conventional_cc_pedal_profile(CP):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from opendbc.car.gm.values import gm_control_word
|
||||
"""Exact finalized GM configurations for shared lateral preferences."""
|
||||
import math
|
||||
from opendbc.car.structs import CarParams
|
||||
@@ -35,7 +36,7 @@ def lane_centering_supported(cp) -> bool:
|
||||
if (len(cp.safetyConfigs) != 1 or cp.safetyConfigs[0].safetyModel != CarParams.SafetyModel.gm or
|
||||
cp.networkLocation != CarParams.NetworkLocation.fwdCamera or control_flags(cp) != 0):
|
||||
return False
|
||||
word = int(cp.safetyConfigs[0].safetyParam)
|
||||
word = gm_control_word(cp)
|
||||
if cp.carFingerprint == CAR.CHEVROLET_VOLT_CAMERA:
|
||||
return ((word == 5 and cp.pcmCruise and not cp.openpilotLongitudinalControl) or
|
||||
(word in (0x4007, 0x4087) and not cp.pcmCruise and cp.openpilotLongitudinalControl and cp.alphaLongitudinalAvailable))
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Volt moving one-pedal braking with vehicle-owned physical admission."""
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
|
||||
from opendbc.car import ACCELERATION_DUE_TO_GRAVITY
|
||||
from opendbc.car.common.conversions import Conversions as CV
|
||||
from opendbc.car.gm.values import CAR
|
||||
|
||||
|
||||
class _VoltPid:
|
||||
# Original openpilot/common/pid.py arithmetic, with unused derivative/override API removed.
|
||||
def __init__(self, kp, ki):
|
||||
self.kp, self.ki = kp, ki
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.i = 0.0
|
||||
|
||||
def update(self, error, *, speed, feedforward):
|
||||
proportional = float(np.interp(speed, *self.kp)) * error
|
||||
integral = self.i + float(np.interp(speed, *self.ki)) * 0.04 * error
|
||||
candidate = proportional + integral + feedforward
|
||||
upper = self.i if candidate > 0.0 else 0.0
|
||||
lower = self.i if candidate < -3.5 else -3.5
|
||||
self.i = float(np.clip(integral, lower, upper))
|
||||
return float(np.clip(proportional + self.i + feedforward, -3.5, 0.0))
|
||||
|
||||
|
||||
class VoltOnePedal:
|
||||
def __init__(self, cp, params):
|
||||
self.params = params
|
||||
self.stopping_speed = .75 if cp.carFingerprint == CAR.CHEVROLET_VOLT else .25
|
||||
self.pid = _VoltPid(((0.,), (0.,)),
|
||||
(cp.longitudinalTuning.kiBP, cp.longitudinalTuning.kiV))
|
||||
self.decel = 0.0
|
||||
self.brake = 0
|
||||
self.lift_frames = 0
|
||||
self.gas_last = False
|
||||
|
||||
def observe(self, *, eligible, gas_pressed, speed):
|
||||
# Called at controller cadence; preserve a gas-release edge until the next 25 Hz sender.
|
||||
if eligible and self.gas_last and not gas_pressed and speed < 2.0 * CV.MPH_TO_MS:
|
||||
self.lift_frames = 8
|
||||
elif gas_pressed or not eligible:
|
||||
self.lift_frames = 0
|
||||
self.gas_last = gas_pressed
|
||||
|
||||
def update(self, *, eligible, speed, acceleration, steering_angle, orientation):
|
||||
# Called only at the existing gas/friction sender cadence, with qualified physical inputs.
|
||||
finite = all(math.isfinite(value) for value in (speed, acceleration, steering_angle, *orientation))
|
||||
if not eligible or not finite or speed < 0:
|
||||
self.pid.reset()
|
||||
self.decel = min(0.0, acceleration) if math.isfinite(acceleration) else 0.0
|
||||
self.brake = 0
|
||||
self.lift_frames = 0
|
||||
return 0
|
||||
pitch_accel = 0.0
|
||||
if len(orientation) == 3 and speed > self.stopping_speed:
|
||||
pitch_accel = math.sin(orientation[1]) * ACCELERATION_DUE_TO_GRAVITY
|
||||
pitch_values = [0.4, 1.0] if pitch_accel <= 0 else [0.2, 1.0]
|
||||
pitch_accel *= float(np.interp(speed, [4.0, 8.0], pitch_values))
|
||||
target = float(np.interp(speed, [0.5 * CV.MPH_TO_MS, 6.0 * CV.MPH_TO_MS], [-1.0, -1.1]))
|
||||
measured = min(0.0, acceleration + pitch_accel)
|
||||
error = (target - measured) * float(np.interp(speed, [1.5, 20.0], [0.4, 0.2]))
|
||||
raw = float(self.pid.update(error, speed=speed, feedforward=target))
|
||||
factor = min(float(np.interp(speed, [0.0, 10.0 * CV.MPH_TO_MS], [0.2, 1.0])),
|
||||
float(np.interp(abs(steering_angle), [20.0, 120.0], [1.0, 0.2])))
|
||||
lower = min(self.decel, measured) - 0.032 * factor
|
||||
# Preserve the original asymmetric upper expression; this is not a tuning change.
|
||||
upper = max(self.decel, measured) + 0.032 + factor
|
||||
self.decel = max(float(np.clip(raw, lower, upper)), -2.1)
|
||||
self.brake = int(round(np.clip(np.interp(self.decel, [self.params.ACCEL_MIN, 0.],
|
||||
[self.params.MAX_BRAKE, 0.]), 0, self.params.MAX_BRAKE)))
|
||||
if self.lift_frames > 0:
|
||||
lift = 0 if speed > 2.0 * CV.MPH_TO_MS else int(round(np.interp(
|
||||
speed, [0.0, self.params.NEAR_STOP_BRAKE_PHASE, 2.0 * CV.MPH_TO_MS], [80.0, 80.0, 20.0])))
|
||||
self.brake = max(self.brake, lift)
|
||||
self.lift_frames -= 1
|
||||
return self.brake
|
||||
@@ -1,3 +1,4 @@
|
||||
from opendbc.car.gm.values import gm_control_word, is_volt_one_pedal
|
||||
"""Finalized GM speed-control choices; existing native envelopes are retained."""
|
||||
|
||||
from opendbc.car.gm.values import (is_volt_gateway_profile, is_volt_cc_profile, is_ordinary_cc_profile,
|
||||
@@ -14,6 +15,8 @@ def disable_long_supported(cp) -> bool:
|
||||
|
||||
|
||||
def prepare_disable_longitudinal(cp, requested: bool) -> None:
|
||||
if requested and is_volt_one_pedal(cp):
|
||||
cp.safetyConfigs[0].safetyParam = gm_control_word(cp)
|
||||
if requested and is_volt_camera_removed(cp, longitudinal=True):
|
||||
cp.safetyConfigs[0].safetyParam = 0xC150
|
||||
cp.openpilotLongitudinalControl = False
|
||||
|
||||
@@ -11,7 +11,201 @@ from opendbc.car.gm.tests.test_bolt_volt_configurations import controller_messag
|
||||
from opendbc.car.gm.values import CAR, DBC, GMSafetyFlags, is_volt_gateway_alternate_brake
|
||||
|
||||
|
||||
def one_pedal_profiles(*, release=False):
|
||||
from opendbc.car.gm.tests.test_ascm_intercept import params as ascm_params
|
||||
from opendbc.car.gm.tests.test_volt_camera_control import camera_params
|
||||
from opendbc.car.gm.tests.test_volt_sdgm_control import sdgm_params
|
||||
from opendbc.car.gm.tests.test_volt_camera_removed import removed_params
|
||||
for be in (True, False):
|
||||
yield f'gateway-{int(be)}', ordinary_params(CAR.CHEVROLET_VOLT, radar=True, accelerator=be), be
|
||||
for be in (True, False):
|
||||
for radar in (False, True):
|
||||
yield (f'ascm-{int(be)}-{int(radar)}', ascm_params(CAR.CHEVROLET_VOLT_ASCM, sascm=True, alpha=True,
|
||||
accelerator=be, radar=radar, release=release), be)
|
||||
yield 'camera', camera_params(release=release), True
|
||||
for c9 in (False, True):
|
||||
yield f'sdgm-{int(c9)}', sdgm_params(brake_c9=c9, radar=True, release=release), True
|
||||
for alternate in (False, True):
|
||||
yield f'removed-{int(alternate)}', removed_params(alternate=alternate, release=release), not alternate
|
||||
|
||||
|
||||
class TestVoltAlternateBrake(unittest.TestCase):
|
||||
def test_one_pedal_matches_pinned_original_scalar_histories(self):
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from opendbc.car.gm.one_pedal import VoltOnePedal, _VoltPid
|
||||
reference = json.loads(Path(__file__).with_name('volt_one_pedal_original_oracle.json').read_text())
|
||||
params = SimpleNamespace(**reference['params'])
|
||||
for history in reference['histories']:
|
||||
cp = SimpleNamespace(carFingerprint=CAR.CHEVROLET_VOLT_CAMERA, longitudinalTuning=SimpleNamespace(
|
||||
kpBP=history['kp'][0], kpV=history['kp'][1], kiBP=history['ki'][0], kiV=history['ki'][1]))
|
||||
controller = VoltOnePedal(cp, params)
|
||||
controller.pid = _VoltPid(history['kp'], history['ki'])
|
||||
controller.lift_frames = history['initial_lift_frames']
|
||||
for tick, step in enumerate(history['steps']):
|
||||
with self.subTest(history=history['name'], tick=tick):
|
||||
result = controller.update(**step['input'])
|
||||
self.assertEqual(result, step['expected']['brake'])
|
||||
self.assertAlmostEqual(controller.pid.i, step['expected']['integral'], places=12)
|
||||
self.assertAlmostEqual(controller.decel, step['expected']['decel'], places=12)
|
||||
self.assertEqual(controller.lift_frames, step['expected']['lift_frames'])
|
||||
|
||||
def test_one_pedal_default_off_preserves_committed_sender_bytes(self):
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.gm.interface import CarInterface
|
||||
from opendbc.car.gm.tests.test_bolt_cc import feed, control
|
||||
from opendbc.car.gm.values import is_volt_camera_removed
|
||||
reference = json.loads(Path(__file__).with_name('volt_one_pedal_default_off.json').read_text())
|
||||
cases = list(one_pedal_profiles())
|
||||
self.assertEqual(len(cases), len(reference['profiles']))
|
||||
for (name, cp, be), baseline in zip(cases, reference['profiles'], strict=True):
|
||||
with self.subTest(profile=name):
|
||||
self.assertEqual((name, cp.carFingerprint, cp.safetyConfigs[0].safetyParam),
|
||||
(baseline['name'], baseline['identity'], baseline['word']))
|
||||
ci = CarInterface(cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for tick, expected in enumerate(baseline['tx']):
|
||||
now = 1_000_000_000 + tick * 10_000_000
|
||||
speed = 20. if tick < 40 or tick >= 100 else 0. if tick < 80 else 6.
|
||||
active, brake, gas = tick < 60 or tick >= 90, 60 <= tick < 65, 65 <= tick < 70
|
||||
_, packets = feed(SimpleNamespace(update=lambda _: None), packer, now, counter=tick % 4, speed=speed,
|
||||
brake=brake, gas=gas, camera=cp.networkLocation == structs.CarParams.NetworkLocation.fwdCamera and
|
||||
not is_volt_camera_removed(cp))
|
||||
packets = [packet for packet in packets if packet[0] not in (0xBE, 0xF1, 0x1C4)]
|
||||
packets.append(packer.make_can_msg('AcceleratorPedal2', 0, {'CruiseState': 2 if active else 0,
|
||||
'AcceleratorPedal2': 30 if gas else 0}))
|
||||
if be:
|
||||
packets.append(packer.make_can_msg('ECMAcceleratorPos', 0, {'BrakePedalPos': 20 if brake else 0}))
|
||||
else:
|
||||
packets.append(packer.make_can_msg('EBCMBrakePedalPosition', 0, {'BrakePedalPosition': 104 if brake else 0}))
|
||||
if cp.networkLocation == structs.CarParams.NetworkLocation.fwdCamera and not is_volt_camera_removed(cp):
|
||||
packets.append(packer.make_can_msg('ASCMActiveCruiseControlStatus', 2, {'ACCCruiseState': 3, 'ACCSpeedSetpoint': 60}))
|
||||
ci.update([(now - 1_000_000, packets)])
|
||||
self.assertTrue(ci.update([(now, packets)]).canValid)
|
||||
command = control(enabled=active, long_active=active)
|
||||
command.latActive = active
|
||||
command.actuators.accel = -.5 if tick < 40 else 1.
|
||||
command.actuators.torque = .02
|
||||
_, messages = ci.apply(command.as_reader(), now)
|
||||
self.assertEqual([[address, data.hex(), bus] for address, data, bus in messages], expected, (name, tick))
|
||||
|
||||
def test_one_pedal_actual_parser_controller_all_tx(self):
|
||||
from tempfile import TemporaryDirectory
|
||||
from types import SimpleNamespace
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.gm.interface import CarInterface
|
||||
from opendbc.car.gm.tests.test_bolt_cc import feed, setup, native
|
||||
from opendbc.car.gm.values import gm_control_word, is_volt_camera_removed
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.starpilot.controller_extensions import configure_controller
|
||||
from openpilot.starpilot.vehicle_preferences import VehicleStartupPreferences
|
||||
release = libsafety_py.libsafety.set_safety_hooks(structs.CarParams.SafetyModel.allOutput, 0) != 0
|
||||
indexes = {'gateway-1': 0, 'gateway-0': 1, 'ascm-1-0': 2, 'ascm-0-0': 3,
|
||||
'ascm-1-1': 4, 'ascm-0-1': 5, 'camera': 6, 'sdgm-0': 7,
|
||||
'sdgm-1': 8, 'removed-0': 9, 'removed-1': 10}
|
||||
for name, original, be in one_pedal_profiles(release=release):
|
||||
for paired in (False, True):
|
||||
with self.subTest(profile=name, paired=paired), TemporaryDirectory() as directory:
|
||||
cp = original.as_reader().as_builder()
|
||||
params = Params(directory)
|
||||
for key, value in [('OpenpilotEnabledToggle', True), ('VoltOnePedalMode', True), ('GMAutoHold', paired)]:
|
||||
params.put_bool(key, value, block=True)
|
||||
preferences = VehicleStartupPreferences.read(params, enabled=True)
|
||||
preferences.prepare(cp)
|
||||
preferences.finalize(cp)
|
||||
if release and indexes[name] >= 2:
|
||||
self.assertEqual(cp.safetyConfigs[0].safetyParam, original.safetyConfigs[0].safetyParam)
|
||||
self._hold_release_factory(cp, camera=False, alternative=0)
|
||||
continue
|
||||
self.assertEqual(cp.safetyConfigs[0].safetyParam, 0xD100 + indexes[name] + (0x10 if paired else 0))
|
||||
self.assertNotEqual(gm_control_word(cp), cp.safetyConfigs[0].safetyParam)
|
||||
ci = CarInterface(cp)
|
||||
preferences.configure_controller(ci)
|
||||
configure_controller(ci, params)
|
||||
self.assertTrue(ci.CC.volt_one_pedal)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
setup(cp)
|
||||
self.assertEqual(libsafety_py.libsafety.set_safety_hooks(int(structs.CarParams.SafetyModel.gm), cp.safetyConfigs[0].safetyParam), 0)
|
||||
removed = is_volt_camera_removed(cp)
|
||||
positive = stopped_positive = immediate_release = False
|
||||
for tick in range(2680 if name == 'gateway-1' and not paired else 940):
|
||||
now = 1_000_000_000 + tick * 10_000_000
|
||||
stopped = 360 <= tick < 410 or 440 <= tick < 490 or 550 <= tick < 660
|
||||
regen = 410 <= tick < 420
|
||||
mode_off = 490 <= tick < 550 or 610 <= tick < 660 or 730 <= tick < 940
|
||||
manual = 760 <= tick < 800
|
||||
fault = 800 <= tick < 820
|
||||
physical_brake = 820 <= tick < 840
|
||||
unknown_gear = 840 <= tick < 860
|
||||
main_off = 860 <= tick < 880
|
||||
stale_status = 880 <= tick < 940
|
||||
live_changes = {1280: ('VoltOnePedalMode', False), 1320: ('VoltOnePedalMode', True),
|
||||
1720: ('OpenpilotEnabledToggle', False), 1760: ('OpenpilotEnabledToggle', True),
|
||||
2160: ('SafeMode', True), 2200: ('SafeMode', False),
|
||||
2600: ('DisableOpenpilotLongitudinal', True)}
|
||||
if tick in live_changes:
|
||||
key, value = live_changes[tick]
|
||||
params.put_bool(key, value, block=True)
|
||||
live_withdrawn = 1305 <= tick < 1320 or 1745 <= tick < 1760 or 2185 <= tick < 2200 or tick >= 2625
|
||||
gas = 310 <= tick < 320
|
||||
speed = 0. if stopped else .15 if 420 <= tick < 440 else .5 if tick >= 310 else 2.
|
||||
_, rx = feed(SimpleNamespace(update=lambda _: None), packer, now, speed=speed, gas=gas, regen=regen,
|
||||
camera=cp.networkLocation == structs.CarParams.NetworkLocation.fwdCamera and not removed)
|
||||
rx = [message for message in rx if message[0] not in (0xBE, 0xF1, 0x1C4, 0xC9, 0x1F5, 0x232)]
|
||||
brake_name, signal = ('ECMAcceleratorPos', 'BrakePedalPos') if be else ('EBCMBrakePedalPosition', 'BrakePedalPosition')
|
||||
rx += [packer.make_can_msg(brake_name, 0, {signal: 20 if physical_brake else 0}),
|
||||
packer.make_can_msg('AcceleratorPedal2', 0, {'CruiseState': 4 if stopped else 2, 'AcceleratorPedal2': 30 if gas else 0}),
|
||||
packer.make_can_msg('ECMEngineStatus', 0, {'CruiseMainOn': int(not main_off), 'BrakePressed': int(physical_brake)}),
|
||||
packer.make_can_msg('ECMPRDNL2', 0, {'PRNDL2': 8 if unknown_gear else
|
||||
6 if manual or not mode_off and (tick < 660 or tick >= 940) else 4, 'ManualMode': int(manual)}),
|
||||
packer.make_can_msg('EBCMFrictionBrakeStatus', 0, {'FrictionBrakeUnavailable': int(fault)})]
|
||||
if 660 <= tick < 700:
|
||||
rx.append(packer.make_can_msg('EVDriveMode', 0, {'SinglePedalModeActive': 1}))
|
||||
if stale_status:
|
||||
rx = [message for message in rx if message[0] != 0x232]
|
||||
if cp.networkLocation == structs.CarParams.NetworkLocation.fwdCamera and not removed:
|
||||
rx.append(packer.make_can_msg('ASCMActiveCruiseControlStatus', 2, {'ACCCruiseState': 3, 'ACCSpeedSetpoint': 60}))
|
||||
ci.update([(now - 1_000_000, rx)])
|
||||
out = ci.update([(now, rx)])
|
||||
if not stale_status:
|
||||
self.assertTrue(out.canValid, (name, tick))
|
||||
self.assertEqual(ci.CS.volt_one_pedal_mode, not mode_off, tick)
|
||||
for message in rx:
|
||||
native('rx', message, now // 1000)
|
||||
libsafety_py.libsafety.safety_tick()
|
||||
_, commands = ci.apply(structs.CarControl(enabled=False, longActive=False).as_reader(), now)
|
||||
for message in commands:
|
||||
self.assertTrue(native('tx', message, now // 1000), (name, paired, tick, message))
|
||||
brakes = [message for message in commands if message[0] == 0x315]
|
||||
self.assertLessEqual(len(brakes), 1)
|
||||
if not brakes:
|
||||
continue
|
||||
data = brakes[0][1]
|
||||
self.assertEqual(int.from_bytes(data[2:4], 'big'),
|
||||
(0x10000 - int.from_bytes(data[:2], 'big') - (data[4] & 3)) & 0xFFFF)
|
||||
amount = (0x1000 - (((data[0] & 15) << 8) | data[1])) & 0xFFF
|
||||
if (gas or regen or tick < 300 or mode_off and not paired or fault or physical_brake or
|
||||
unknown_gear or main_off or 910 <= tick < 940 or live_withdrawn):
|
||||
self.assertEqual(amount, 0, (name, paired, tick))
|
||||
if tick in (1260, 1716, 2156, 2596):
|
||||
self.assertGreater(amount, 0, (name, paired, tick))
|
||||
if 330 <= tick < 360 and amount:
|
||||
positive = True
|
||||
if 390 <= tick < 410 and amount:
|
||||
stopped_positive = True
|
||||
self.assertEqual(data[0] >> 4, 0xD)
|
||||
self.assertFalse(any(message[0] == 0x2CB for message in commands))
|
||||
if 420 <= tick < 440 and amount:
|
||||
immediate_release = True
|
||||
if out.vEgo < ci.CC.params.NEAR_STOP_BRAKE_PHASE:
|
||||
self.assertEqual(data[0] >> 4, 0xB)
|
||||
self.assertTrue(positive and stopped_positive and immediate_release, (name, paired))
|
||||
|
||||
def test_auto_hold_actual_gateway_parser_controller_native(self):
|
||||
from itertools import product
|
||||
for accelerator, alternative in product((True, False), (0, 32)):
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -136,6 +136,43 @@ class GMFlags(IntFlag):
|
||||
VOLT_CAMERA_NO_ACCEL_POS = NO_ACCELERATOR_POS_MSG
|
||||
|
||||
|
||||
VOLT_ONE_PEDAL_BASE_WORDS = (0x4084, 0xC084, 0x4287, 0x4687, 0x4A87, 0x4E87, 0x4087, 0x5087, 0x5487, 0xC1D1, 0xC1D3)
|
||||
VOLT_ONE_PEDAL_WORDS = {start + index: word for start in (0xD100, 0xD110)
|
||||
for index, word in enumerate(VOLT_ONE_PEDAL_BASE_WORDS)}
|
||||
|
||||
|
||||
def gm_control_word(cp: CarParams) -> int:
|
||||
word = int(cp.safetyConfigs[0].safetyParam)
|
||||
if word not in VOLT_ONE_PEDAL_WORDS:
|
||||
return word
|
||||
index = word - (0xD110 if word >= 0xD110 else 0xD100)
|
||||
identity = (CAR.CHEVROLET_VOLT if index < 2 else CAR.CHEVROLET_VOLT_ASCM if index < 6 else
|
||||
CAR.CHEVROLET_VOLT_2019 if index in (7, 8) else CAR.CHEVROLET_VOLT_CAMERA)
|
||||
network = CarParams.NetworkLocation.gateway if index < 2 else CarParams.NetworkLocation.fwdCamera
|
||||
if (cp.brand == 'gm' and cp.transmissionType == CarParams.TransmissionType.direct and
|
||||
cp.carFingerprint == identity and cp.networkLocation == network):
|
||||
return VOLT_ONE_PEDAL_WORDS[word]
|
||||
return word
|
||||
|
||||
|
||||
def is_volt_one_pedal(cp: CarParams) -> bool:
|
||||
try:
|
||||
return (int(cp.safetyConfigs[0].safetyParam) in VOLT_ONE_PEDAL_WORDS and
|
||||
cp.transmissionType == CarParams.TransmissionType.direct and is_volt_longitudinal(cp) and is_gm_auto_hold(cp))
|
||||
except (AttributeError, IndexError, TypeError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def apply_volt_one_pedal(cp: CarParams, enabled: bool, auto_hold_enabled: bool) -> None:
|
||||
if not is_volt_longitudinal(cp) or cp.transmissionType != CarParams.TransmissionType.direct:
|
||||
return
|
||||
cp.safetyConfigs[0].safetyParam = gm_control_word(cp)
|
||||
apply_gm_auto_hold(cp, enabled or auto_hold_enabled)
|
||||
word = int(cp.safetyConfigs[0].safetyParam)
|
||||
if enabled and word in VOLT_ONE_PEDAL_BASE_WORDS:
|
||||
cp.safetyConfigs[0].safetyParam = (0xD110 if auto_hold_enabled else 0xD100) + VOLT_ONE_PEDAL_BASE_WORDS.index(word)
|
||||
|
||||
|
||||
def control_flags(cp: CarParams) -> int:
|
||||
"""Exclude only the informational BSM bit from exact control-profile admission."""
|
||||
return int(cp.flags) & ~int(GMFlags.HAS_BSM)
|
||||
@@ -175,7 +212,7 @@ def is_volt_gateway_profile(cp: CarParams) -> bool:
|
||||
not cp.passive and not cp.dashcamOnly and not cp.notCar and not cp.radarUnavailable and
|
||||
control_flags(cp) == 0 and
|
||||
len(cp.safetyConfigs) == 1 and cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.gm and
|
||||
int(cp.safetyConfigs[0].safetyParam) in (
|
||||
gm_control_word(cp) in (
|
||||
int(GMSafetyFlags.EV | GMSafetyFlags.VOLT_GATEWAY_LONG),
|
||||
int(GMSafetyFlags.EV | GMSafetyFlags.VOLT_GATEWAY_LONG | GMSafetyFlags.VOLT_GATEWAY_ALT_BRAKE),
|
||||
0x4084, 0xC084))
|
||||
@@ -195,7 +232,7 @@ def is_lacrosse_gateway_profile(cp: CarParams) -> bool:
|
||||
cp.transmissionType == CarParams.TransmissionType.automatic and
|
||||
not cp.passive and not cp.dashcamOnly and not cp.notCar and not cp.radarUnavailable and control_flags(cp) == 0 and
|
||||
len(cp.safetyConfigs) == 1 and cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.gm and
|
||||
cp.safetyConfigs[0].safetyParam in (0, 0x80))
|
||||
gm_control_word(cp) in (0, 0x80))
|
||||
except (AttributeError, IndexError, TypeError, ValueError):
|
||||
return False
|
||||
|
||||
@@ -204,7 +241,7 @@ def is_gm_auto_hold(cp: CarParams) -> bool:
|
||||
return ((is_volt_gateway_longitudinal(cp) or is_volt_ascm_longitudinal(cp) or is_volt_camera_longitudinal(cp) or
|
||||
is_volt_sdgm_profile(cp, longitudinal=True) or is_volt_camera_removed(cp, longitudinal=True) or
|
||||
is_lacrosse_gateway_profile(cp) and cp.openpilotLongitudinalControl) and
|
||||
int(cp.safetyConfigs[0].safetyParam) in (0x4084, 0xC084, 0x4287, 0x4687, 0x4A87, 0x4E87, 0x4087, 0x5087, 0x5487,
|
||||
gm_control_word(cp) in (0x4084, 0xC084, 0x4287, 0x4687, 0x4A87, 0x4E87, 0x4087, 0x5087, 0x5487,
|
||||
0xC1D1, 0xC1D3, 0x80))
|
||||
|
||||
|
||||
@@ -214,7 +251,7 @@ def apply_gm_auto_hold(cp: CarParams, enabled: bool) -> None:
|
||||
return
|
||||
if (is_volt_gateway_profile(cp) or is_volt_ascm_longitudinal(cp) or is_volt_camera_longitudinal(cp) or
|
||||
is_volt_sdgm_profile(cp, longitudinal=True) or is_lacrosse_gateway_profile(cp)):
|
||||
word = int(cp.safetyConfigs[0].safetyParam)
|
||||
word = gm_control_word(cp)
|
||||
cp.safetyConfigs[0].safetyParam = (word | int(GMSafetyFlags.VOLT_AUTO_HOLD)) if enabled and cp.openpilotLongitudinalControl else (
|
||||
word & ~int(GMSafetyFlags.VOLT_AUTO_HOLD))
|
||||
|
||||
@@ -227,7 +264,7 @@ def is_volt_ascm_longitudinal(cp: CarParams) -> bool:
|
||||
optional = int(GMSafetyFlags.ASCM_BRAKE_C9 | GMSafetyFlags.ASCM_RADAR)
|
||||
if len(cp.safetyConfigs) != 1:
|
||||
return False
|
||||
flags = int(cp.safetyConfigs[0].safetyParam)
|
||||
flags = gm_control_word(cp)
|
||||
if flags in (0x4287, 0x4687, 0x4A87, 0x4E87):
|
||||
flags &= ~int(GMSafetyFlags.VOLT_AUTO_HOLD)
|
||||
return (cp.brand == 'gm' and cp.carFingerprint == CAR.CHEVROLET_VOLT_ASCM and
|
||||
@@ -253,8 +290,8 @@ def is_volt_camera_removed(cp: CarParams, *, longitudinal=None) -> bool:
|
||||
(not is_long or cp.alphaLongitudinalAvailable) and
|
||||
not cp.passive and not cp.dashcamOnly and not cp.notCar and len(cp.safetyConfigs) == 1 and
|
||||
cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.gm and
|
||||
(cp.safetyConfigs[0].safetyParam == (0xC151 if is_long else 0xC150) or
|
||||
is_long and cp.safetyConfigs[0].safetyParam == (0xC1D3 if cp.flags & GMFlags.NO_ACCELERATOR_POS_MSG else 0xC1D1)))
|
||||
(gm_control_word(cp) == (0xC151 if is_long else 0xC150) or
|
||||
is_long and gm_control_word(cp) == (0xC1D3 if cp.flags & GMFlags.NO_ACCELERATOR_POS_MSG else 0xC1D1)))
|
||||
except (AttributeError, IndexError, TypeError, ValueError):
|
||||
return False
|
||||
|
||||
@@ -266,7 +303,7 @@ def is_volt_camera_stock(cp: CarParams) -> bool:
|
||||
cp.networkLocation == CarParams.NetworkLocation.fwdCamera and cp.pcmCruise and
|
||||
not cp.openpilotLongitudinalControl and not cp.passive and not cp.dashcamOnly and
|
||||
not cp.notCar and control_flags(cp) == 0 and len(cp.safetyConfigs) == 1 and
|
||||
cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.gm and cp.safetyConfigs[0].safetyParam == 5)
|
||||
cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.gm and gm_control_word(cp) == 5)
|
||||
except (AttributeError, IndexError, TypeError, ValueError):
|
||||
return False
|
||||
|
||||
@@ -279,7 +316,7 @@ def is_volt_camera_longitudinal(cp: CarParams) -> bool:
|
||||
cp.openpilotLongitudinalControl and not cp.pcmCruise and not cp.passive and not cp.dashcamOnly and
|
||||
not cp.notCar and control_flags(cp) == 0 and len(cp.safetyConfigs) == 1 and
|
||||
cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.gm and
|
||||
cp.safetyConfigs[0].safetyParam in (0x4007, 0x4087))
|
||||
gm_control_word(cp) in (0x4007, 0x4087))
|
||||
except (AttributeError, IndexError, TypeError, ValueError):
|
||||
return False
|
||||
|
||||
@@ -292,7 +329,7 @@ def is_volt_sdgm_profile(cp: CarParams, *, longitudinal=False) -> bool:
|
||||
not cp.notCar and control_flags(cp) == 0 and cp.openpilotLongitudinalControl == longitudinal and
|
||||
cp.pcmCruise != longitudinal and (not longitudinal or cp.alphaLongitudinalAvailable) and
|
||||
len(cp.safetyConfigs) == 1 and cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.gm and
|
||||
cp.safetyConfigs[0].safetyParam in words)
|
||||
gm_control_word(cp) in words)
|
||||
except (AttributeError, IndexError, TypeError, ValueError):
|
||||
return False
|
||||
|
||||
@@ -304,7 +341,7 @@ def is_volt_longitudinal(cp: CarParams) -> bool:
|
||||
|
||||
def is_volt_gateway_alternate_brake(cp: CarParams) -> bool:
|
||||
"""The exact gateway Volt profile using EBCM pedal input and PT friction output."""
|
||||
return is_volt_gateway_profile(cp) and bool(cp.safetyConfigs[0].safetyParam & GMSafetyFlags.VOLT_GATEWAY_ALT_BRAKE)
|
||||
return is_volt_gateway_profile(cp) and bool(gm_control_word(cp) & GMSafetyFlags.VOLT_GATEWAY_ALT_BRAKE)
|
||||
|
||||
|
||||
class Footnote(Enum):
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "opendbc/safety/declarations.h"
|
||||
#include "opendbc/safety/can_tx.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"
|
||||
@@ -405,6 +406,7 @@ static void gm_rx_hook(const CANPacket_t *msg) {
|
||||
}
|
||||
|
||||
gm_cc_pedal_rx(msg);
|
||||
gm_one_pedal_observe(msg);
|
||||
gm_hold_rx(msg);
|
||||
}
|
||||
|
||||
@@ -714,7 +716,7 @@ static bool gm_fwd_hook(int bus_num, int addr) {
|
||||
}
|
||||
|
||||
static safety_config gm_init(uint16_t safety_param) {
|
||||
uint16_t param = safety_param;
|
||||
uint16_t param = gm_one_pedal_reset(safety_param);
|
||||
gm_cc_pedal_silverado = (safety_param == 0xC182U) || (safety_param == 0xC183U) ||
|
||||
(safety_param == 0xC184U) || (safety_param == 0xC185U);
|
||||
gm_cc_pedal_ordinary_stock = (safety_param == 0xC186U) || (safety_param == 0xC187U);
|
||||
|
||||
@@ -72,6 +72,7 @@ static void gm_aol_initialize(bool profile_supported) {
|
||||
static bool gm_aol_profile_word(uint16_t word) {
|
||||
bool supported = false;
|
||||
switch (word) {
|
||||
case 0xD100U: case 0xD101U: case 0xD110U: case 0xD111U:
|
||||
case 0U: case 0x80U:
|
||||
case 0x201U: case 0x601U: case 0xA01U: case 0xE01U:
|
||||
case 0xC171U: case 0xC172U: case 5U:
|
||||
@@ -87,6 +88,11 @@ static bool gm_aol_profile_word(uint16_t word) {
|
||||
case 0xC130U: case 0xC131U: case 0xC140U: case 0xC141U:
|
||||
case 0xC150U:
|
||||
#ifdef ALLOW_DEBUG
|
||||
case 0xD102U: case 0xD103U: case 0xD104U: case 0xD105U:
|
||||
case 0xD106U: case 0xD107U: case 0xD108U: case 0xD109U:
|
||||
case 0xD10AU: case 0xD112U: case 0xD113U: case 0xD114U:
|
||||
case 0xD115U: case 0xD116U: case 0xD117U: case 0xD118U:
|
||||
case 0xD119U: case 0xD11AU:
|
||||
case 0x1003U: case 0x1403U:
|
||||
case 0x203U: case 0x603U: case 0xA03U: case 0xE03U:
|
||||
case 0xC170U: case 0xC173U:
|
||||
|
||||
@@ -211,6 +211,7 @@ 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);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ static bool gm_hold_regen;
|
||||
static bool gm_hold_stopped;
|
||||
static bool gm_hold_armed;
|
||||
static bool gm_hold_moving;
|
||||
static bool gm_hold_one_pedal_moving;
|
||||
static uint8_t gm_hold_acc;
|
||||
static uint32_t gm_hold_drive_us;
|
||||
static uint32_t gm_hold_regen_release_us;
|
||||
@@ -29,6 +30,10 @@ static bool gm_hold_regen_released;
|
||||
static bool gm_hold_counter_seen;
|
||||
static uint8_t gm_hold_counter;
|
||||
|
||||
static void gm_one_pedal_withdraw(void) {
|
||||
if (!gm_one_pedal_config.paired_auto_hold) { gm_hold_accepted = false; }
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
bool enabled;
|
||||
bool alternate;
|
||||
@@ -53,7 +58,7 @@ static void gm_hold_reset(const GMHoldConfig *config) {
|
||||
for (uint8_t i = 0U; i < 7U; i++) { gm_hold_seen[i] = false; gm_hold_us[i] = 0U; }
|
||||
gm_hold_brake_unavailable = true;
|
||||
gm_hold_main = false; gm_hold_forward = false; gm_hold_gas = false;
|
||||
gm_hold_brake = false; gm_hold_regen = false; gm_hold_stopped = false; gm_hold_armed = false; gm_hold_moving = false;
|
||||
gm_hold_brake = false; gm_hold_regen = false; gm_hold_stopped = false; gm_hold_armed = false; gm_hold_moving = false; gm_hold_one_pedal_moving = false;
|
||||
gm_hold_acc = 0U; gm_hold_drive_us = 0U; gm_hold_regen_release_us = 0U;
|
||||
gm_hold_regen_released = false; gm_hold_counter_seen = false; gm_hold_counter = 0U;
|
||||
}
|
||||
@@ -68,13 +73,20 @@ static bool gm_hold_sources_current(uint32_t now) {
|
||||
return current;
|
||||
}
|
||||
|
||||
static bool gm_hold_ready(uint32_t now) {
|
||||
static bool gm_hold_physical_ready(uint32_t now) {
|
||||
if (gm_hold_regen_released && (safety_get_ts_elapsed(now, gm_hold_regen_release_us) >= 1000000U)) {
|
||||
gm_hold_regen_released = false;
|
||||
}
|
||||
const bool cooldown = gm_hold_regen_released;
|
||||
return !safety_rx_checks_invalid && !relay_malfunction && gm_hold_sources_current(now) && gm_hold_main && gm_hold_forward && !gm_hold_gas &&
|
||||
!gm_hold_regen && !gm_hold_brake_unavailable && !cooldown && (gm_hold_drive_us >= 3000000U);
|
||||
!gm_hold_regen && !gm_hold_brake_unavailable && (gm_hold_drive_us >= 3000000U);
|
||||
}
|
||||
|
||||
static bool gm_hold_ready(uint32_t now) {
|
||||
return gm_hold_physical_ready(now) && !gm_hold_regen_released;
|
||||
}
|
||||
|
||||
static bool gm_one_pedal_physical_ready(uint32_t now) {
|
||||
return gm_one_pedal_mode_qualified(now) && !gm_hold_brake && gm_hold_physical_ready(now);
|
||||
}
|
||||
|
||||
static void gm_hold_rx(const CANPacket_t *msg) {
|
||||
@@ -106,6 +118,7 @@ static void gm_hold_rx(const CANPacket_t *msg) {
|
||||
gm_hold_stopped = (left <= 10U) && (right <= 10U);
|
||||
gm_hold_near_stop = (left <= 28U) && (right <= 28U);
|
||||
if (!gm_hold_near_stop) { gm_hold_accepted = false; }
|
||||
gm_hold_one_pedal_moving = (left > 10U) && (right > 10U);
|
||||
gm_hold_moving = (left > 0U) || (right > 0U);
|
||||
const uint32_t elapsed = safety_get_ts_elapsed(now, gm_hold_us[4]);
|
||||
// Raw wheel units are .0311 km/h; twelve units exceed .1 m/s.
|
||||
@@ -123,10 +136,14 @@ static void gm_hold_rx(const CANPacket_t *msg) {
|
||||
// Unrelated or malformed traffic cannot refresh physical authority.
|
||||
}
|
||||
if (source >= 0) { gm_hold_seen[source] = true; gm_hold_us[source] = now; }
|
||||
if (!gm_hold_ready(now)) { gm_hold_armed = false; gm_hold_accepted = false; }
|
||||
if (!gm_hold_ready(now)) {
|
||||
gm_hold_armed = false;
|
||||
if (!gm_one_pedal_physical_ready(now)) { gm_hold_accepted = false; }
|
||||
}
|
||||
else if (gm_hold_moving || gm_hold_brake) { gm_hold_armed = true; }
|
||||
else { /* Retain a previously armed stopped hold. */ }
|
||||
if (get_longitudinal_allowed()) { gm_hold_accepted = false; }
|
||||
if (get_longitudinal_allowed() || (gm_one_pedal_config.enabled && !gm_one_pedal_config.paired_auto_hold &&
|
||||
(!gm_one_pedal_mode_qualified(now) || gm_hold_brake))) { gm_hold_accepted = false; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,15 +159,27 @@ static bool gm_hold_brake_tx(const CANPacket_t *msg, int brake) {
|
||||
(checksum == expected) && ((msg->data[4] & 0xFCU) == 0U) &&
|
||||
(release || !gm_hold_counter_seen || (counter == ((gm_hold_counter + 1U) & 3U)));
|
||||
const bool hold_mode = (((mode == 0xAU) || (mode == 0xBU)) && (gm_hold_acc != 4U)) || ((mode == 0xDU) && (gm_hold_acc == 4U));
|
||||
if (!gm_hold_ready(now)) { gm_hold_armed = false; gm_hold_accepted = false; }
|
||||
const bool stationary = gm_hold_stopped || (gm_hold_sdgm && gm_hold_accepted && gm_hold_near_stop);
|
||||
if (!gm_hold_ready(now)) {
|
||||
gm_hold_armed = false;
|
||||
if (!gm_one_pedal_physical_ready(now)) { gm_hold_accepted = false; }
|
||||
}
|
||||
const int minimum = gm_hold_sdgm ? 100 : 80;
|
||||
const bool hold = gm_hold_armed && gm_hold_ready(now) && stationary &&
|
||||
const bool one_pedal_mode = gm_one_pedal_mode_qualified(now);
|
||||
if (gm_one_pedal_config.enabled && !gm_one_pedal_config.paired_auto_hold && (!one_pedal_mode || gm_hold_brake)) { gm_hold_accepted = false; }
|
||||
const bool stationary = gm_hold_stopped || (gm_hold_sdgm && gm_hold_accepted && gm_hold_near_stop);
|
||||
const bool auto_hold_owner = (!gm_one_pedal_config.enabled || gm_one_pedal_config.paired_auto_hold) &&
|
||||
gm_hold_armed && gm_hold_ready(now);
|
||||
const bool one_pedal_owner = gm_one_pedal_config.enabled && one_pedal_mode && !gm_hold_brake && gm_hold_physical_ready(now);
|
||||
const bool hold = auto_hold_owner && stationary &&
|
||||
(brake >= minimum) && (brake <= 240) && hold_mode;
|
||||
const bool permitted = packet_valid && (release || hold);
|
||||
const bool one_pedal_brake = one_pedal_owner && hold_mode && (brake <= 400) &&
|
||||
((gm_hold_stopped && (brake >= minimum)) || (gm_hold_one_pedal_moving && (brake >= 0)));
|
||||
const bool permitted = packet_valid && (release || hold || one_pedal_brake);
|
||||
if (permitted) {
|
||||
gm_hold_counter_seen = true; gm_hold_counter = counter;
|
||||
gm_hold_accepted = !release;
|
||||
if (release || !hold) { gm_hold_accepted = false; }
|
||||
else if (gm_hold_stopped) { gm_hold_accepted = true; }
|
||||
else { /* Retain credit only from an accepted stationary hold. */ }
|
||||
}
|
||||
return permitted;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#define GM_ONE_PEDAL_MODE_TIMEOUT_US 300000U
|
||||
|
||||
typedef struct {
|
||||
bool enabled;
|
||||
bool paired_auto_hold;
|
||||
uint16_t canonical_hold_word;
|
||||
} GMOnePedalConfig;
|
||||
|
||||
static GMOnePedalConfig gm_one_pedal_config = {0};
|
||||
static bool gm_one_pedal_low;
|
||||
static bool gm_one_pedal_mode;
|
||||
static bool gm_one_pedal_mode_seen;
|
||||
static uint32_t gm_one_pedal_mode_us;
|
||||
|
||||
static uint16_t gm_one_pedal_reset(uint16_t word) {
|
||||
gm_one_pedal_config = (GMOnePedalConfig){0};
|
||||
gm_one_pedal_low = false;
|
||||
gm_one_pedal_mode = false;
|
||||
gm_one_pedal_mode_seen = false;
|
||||
gm_one_pedal_mode_us = 0U;
|
||||
switch (word) {
|
||||
case 0xD100U: case 0xD110U:
|
||||
gm_one_pedal_config.canonical_hold_word = 0x4084U;
|
||||
gm_one_pedal_config.paired_auto_hold = word == 0xD110U;
|
||||
break;
|
||||
case 0xD101U: case 0xD111U:
|
||||
gm_one_pedal_config.canonical_hold_word = 0xC084U;
|
||||
gm_one_pedal_config.paired_auto_hold = word == 0xD111U;
|
||||
break;
|
||||
#ifdef ALLOW_DEBUG
|
||||
case 0xD102U: case 0xD112U:
|
||||
gm_one_pedal_config.canonical_hold_word = 0x4287U;
|
||||
gm_one_pedal_config.paired_auto_hold = word == 0xD112U;
|
||||
break;
|
||||
case 0xD103U: case 0xD113U:
|
||||
gm_one_pedal_config.canonical_hold_word = 0x4687U;
|
||||
gm_one_pedal_config.paired_auto_hold = word == 0xD113U;
|
||||
break;
|
||||
case 0xD104U: case 0xD114U:
|
||||
gm_one_pedal_config.canonical_hold_word = 0x4A87U;
|
||||
gm_one_pedal_config.paired_auto_hold = word == 0xD114U;
|
||||
break;
|
||||
case 0xD105U: case 0xD115U:
|
||||
gm_one_pedal_config.canonical_hold_word = 0x4E87U;
|
||||
gm_one_pedal_config.paired_auto_hold = word == 0xD115U;
|
||||
break;
|
||||
case 0xD106U: case 0xD116U:
|
||||
gm_one_pedal_config.canonical_hold_word = 0x4087U;
|
||||
gm_one_pedal_config.paired_auto_hold = word == 0xD116U;
|
||||
break;
|
||||
case 0xD107U: case 0xD117U:
|
||||
gm_one_pedal_config.canonical_hold_word = 0x5087U;
|
||||
gm_one_pedal_config.paired_auto_hold = word == 0xD117U;
|
||||
break;
|
||||
case 0xD108U: case 0xD118U:
|
||||
gm_one_pedal_config.canonical_hold_word = 0x5487U;
|
||||
gm_one_pedal_config.paired_auto_hold = word == 0xD118U;
|
||||
break;
|
||||
case 0xD109U: case 0xD119U:
|
||||
gm_one_pedal_config.canonical_hold_word = 0xC1D1U;
|
||||
gm_one_pedal_config.paired_auto_hold = word == 0xD119U;
|
||||
break;
|
||||
case 0xD10AU: case 0xD11AU:
|
||||
gm_one_pedal_config.canonical_hold_word = 0xC1D3U;
|
||||
gm_one_pedal_config.paired_auto_hold = word == 0xD11AU;
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
gm_one_pedal_config.enabled = gm_one_pedal_config.canonical_hold_word != 0U;
|
||||
return gm_one_pedal_config.enabled ? gm_one_pedal_config.canonical_hold_word : word;
|
||||
}
|
||||
|
||||
static bool gm_one_pedal_mode_qualified(uint32_t now);
|
||||
static void gm_one_pedal_withdraw(void);
|
||||
|
||||
static void gm_one_pedal_observe(const CANPacket_t *msg) {
|
||||
if (gm_one_pedal_config.enabled && (msg->bus == 0U)) {
|
||||
if ((msg->addr == 0x1F5U) && (GET_LEN(msg) == 8U)) {
|
||||
gm_one_pedal_low = ((msg->data[3] & 0xFU) == 6U) && !GET_BIT(msg, 41U);
|
||||
} else if ((msg->addr == 0x3C7U) && (GET_LEN(msg) == 4U)) {
|
||||
gm_one_pedal_mode = GET_BIT(msg, 7U);
|
||||
gm_one_pedal_mode_seen = true;
|
||||
gm_one_pedal_mode_us = microsecond_timer_get();
|
||||
} else {
|
||||
// Other traffic cannot refresh mode or physical Low.
|
||||
}
|
||||
if (!gm_one_pedal_mode_qualified(microsecond_timer_get())) { gm_one_pedal_withdraw(); }
|
||||
}
|
||||
}
|
||||
|
||||
static bool gm_one_pedal_mode_qualified(uint32_t now) {
|
||||
// Cadence is unknown: this is a bounded observation, not a periodic RX claim.
|
||||
if (gm_one_pedal_mode_seen && (safety_get_ts_elapsed(now, gm_one_pedal_mode_us) > GM_ONE_PEDAL_MODE_TIMEOUT_US)) {
|
||||
gm_one_pedal_mode_seen = false;
|
||||
}
|
||||
return gm_one_pedal_config.enabled && (gm_one_pedal_low || (gm_one_pedal_mode_seen && gm_one_pedal_mode));
|
||||
}
|
||||
@@ -11,9 +11,10 @@ from opendbc.safety.tests.libsafety import libsafety_py
|
||||
|
||||
|
||||
class TestVoltAutoHold(unittest.TestCase):
|
||||
def init(self, alternate=False, extra=0, alternative=0, word=None):
|
||||
def init(self, alternate=False, extra=0, alternative=0, word=None, physical_word=None):
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.alternate = alternate or word == 0xC1D3
|
||||
physical_word = word if physical_word is None else physical_word
|
||||
self.alternate = alternate or physical_word in (0xC084, 0xC1D3)
|
||||
self.safety.set_alternative_experience(alternative)
|
||||
word = int(GMSafetyFlags.EV | GMSafetyFlags.VOLT_GATEWAY_LONG | GMSafetyFlags.VOLT_AUTO_HOLD) if word is None else word
|
||||
if alternate:
|
||||
@@ -23,8 +24,9 @@ class TestVoltAutoHold(unittest.TestCase):
|
||||
self.time = 1
|
||||
self.counter = 0
|
||||
self.packer = CANPacker(DBC[CAR.CHEVROLET_VOLT][Bus.pt])
|
||||
self.c9_brake = word in (0x4687, 0x4E87, 0x4087, 0x5487, 0xC1D1, 0xC1D3)
|
||||
self.tx_bus = 0 if word in (0x4287, 0x4687, 0x4A87, 0x4E87, 0x4087, 0xC1D1, 0xC1D3) or alternate else 2
|
||||
physical_word = word if physical_word is None else physical_word
|
||||
self.c9_brake = physical_word in (0x4687, 0x4E87, 0x4087, 0x5487, 0xC1D1, 0xC1D3)
|
||||
self.tx_bus = 0 if physical_word in (0x4287, 0x4687, 0x4A87, 0x4E87, 0x4087, 0xC1D1, 0xC1D3) or self.alternate else 2
|
||||
|
||||
def tearDown(self):
|
||||
libsafety_py.libsafety.set_alternative_experience(0)
|
||||
|
||||
@@ -43,7 +43,10 @@ class TestGmVoltGatewayMapping(unittest.TestCase):
|
||||
expected = [EXACT, EXACT | int(GMSafetyFlags.VOLT_GATEWAY_ALT_BRAKE),
|
||||
EXACT | int(GMSafetyFlags.VOLT_AUTO_HOLD),
|
||||
EXACT | int(GMSafetyFlags.VOLT_GATEWAY_ALT_BRAKE | GMSafetyFlags.VOLT_AUTO_HOLD)]
|
||||
expected += [0xD100, 0xD101, 0xD110, 0xD111]
|
||||
if not release:
|
||||
expected += [0xD102, 0xD103, 0xD104, 0xD105, 0xD106, 0xD107, 0xD108, 0xD109, 0xD10A,
|
||||
0xD112, 0xD113, 0xD114, 0xD115, 0xD116, 0xD117, 0xD118, 0xD119, 0xD11A]
|
||||
expected += [7, 515, 1539, 2563, 3587, 4099, 5123, 16391, 16903, 17927, 18951, 19975,
|
||||
20487, 21511, 49489, 49520, 49523, 0x4287, 0x4687, 0x4A87, 0x4E87, 0x4087, 0x5087, 0x5487, 0xC1D1, 0xC1D3]
|
||||
self.assertEqual(accepted, sorted(expected))
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
"""Exact Volt inactive OnePedal source, routing and stopped-owner contracts."""
|
||||
import unittest
|
||||
|
||||
from opendbc.car.gm import gmcan
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
from opendbc.safety.tests import test_gm_volt_auto_hold as hold_fixture
|
||||
|
||||
CANONICAL = (0x4084, 0xC084, 0x4287, 0x4687, 0x4A87, 0x4E87, 0x4087, 0x5087, 0x5487, 0xC1D1, 0xC1D3)
|
||||
|
||||
|
||||
class TestVoltOnePedal(unittest.TestCase):
|
||||
def fixture(self, index, paired=False, alternative=0):
|
||||
self.release = libsafety_py.libsafety.set_safety_hooks(CarParams.SafetyModel.allOutput, 0) != 0
|
||||
f = hold_fixture.TestVoltAutoHold()
|
||||
f.init(word=0xD100 + index + (0x10 if paired else 0), physical_word=CANONICAL[index], alternative=alternative)
|
||||
return f
|
||||
|
||||
def tearDown(self):
|
||||
libsafety_py.libsafety.set_alternative_experience(0)
|
||||
|
||||
def mode(self, f, active=True, bus=0, length=4):
|
||||
frame = f.packer.make_can_msg('EVDriveMode', bus, {'SinglePedalModeActive': int(active)})
|
||||
self.assertEqual(frame[1][0] & 0x80, 0x80 if active else 0)
|
||||
f.rx(frame[0], frame[1][:length], bus)
|
||||
|
||||
def test_all_exact_profiles_moving_and_stopped_bounds(self):
|
||||
for paired in (False, True):
|
||||
for index in range(11):
|
||||
with self.subTest(index=index, paired=paired):
|
||||
f = self.fixture(index, paired)
|
||||
denied = self.release and index >= 2
|
||||
for _ in range(32):
|
||||
f.observations(gear=6, healthy=not denied)
|
||||
self.assertEqual(f.tx(400), not denied)
|
||||
self.assertFalse(f.tx(401))
|
||||
self.assertFalse(f.safety.safety_tx_hook(f.frame(bus=2 if f.tx_bus == 0 else 0)))
|
||||
f.observations(speed=0, gear=6, healthy=not denied)
|
||||
minimum = 100 if index in (7, 8) else 80
|
||||
self.assertFalse(f.tx(minimum - 1))
|
||||
self.assertEqual(f.tx(minimum), not denied)
|
||||
self.assertEqual(f.tx(240), not denied)
|
||||
self.assertEqual(f.tx(400), not denied)
|
||||
self.assertFalse(f.tx(401))
|
||||
self.assertEqual(f.tx(0, 1), not denied)
|
||||
|
||||
def test_solo_mode_and_paired_hold_are_distinct(self):
|
||||
for index in range(11):
|
||||
if self.release_mode() and index >= 2:
|
||||
continue
|
||||
for paired in (False, True):
|
||||
f = self.fixture(index, paired)
|
||||
f.dwell()
|
||||
self.assertEqual(f.tx(), paired)
|
||||
self.mode(f)
|
||||
self.assertTrue(f.tx())
|
||||
f.observations(speed=0, brake=True)
|
||||
self.assertEqual(f.tx(), paired)
|
||||
f.observations(speed=0)
|
||||
self.mode(f, False)
|
||||
self.assertEqual(f.tx(), paired)
|
||||
|
||||
@staticmethod
|
||||
def release_mode():
|
||||
return libsafety_py.libsafety.set_safety_hooks(CarParams.SafetyModel.allOutput, 0) != 0
|
||||
|
||||
def test_mode_current_observation_low_and_physical_withdrawal(self):
|
||||
for paired in (False, True):
|
||||
f = self.fixture(0, paired)
|
||||
f.dwell()
|
||||
f.observations(speed=100)
|
||||
self.mode(f, bus=2)
|
||||
self.assertFalse(f.tx(400))
|
||||
self.mode(f, length=3)
|
||||
self.assertFalse(f.tx(400))
|
||||
self.mode(f)
|
||||
self.assertTrue(f.tx(400))
|
||||
for _ in range(4):
|
||||
f.observations(speed=100)
|
||||
self.assertFalse(f.tx(400))
|
||||
f.time = 0xFFFFFF00 - 100000
|
||||
f.observations(speed=100)
|
||||
self.assertFalse(f.tx(400))
|
||||
f.time = 0
|
||||
f.observations(speed=100)
|
||||
self.assertFalse(f.tx(400)) # Expired mode cannot revive at a 32-bit timer wrap.
|
||||
f.observations(speed=100, gear=6)
|
||||
self.assertTrue(f.tx(400)) # Fresh Low needs no drive-mode frame.
|
||||
f.observations(speed=100, gear=6, manual=True)
|
||||
self.assertFalse(f.tx(400))
|
||||
for changed in ({'main': False}, {'gas': True}, {'brake': True}, {'regen': True}, {'gear': 2}, {'brake_unavailable': True}):
|
||||
f = self.fixture(0, paired)
|
||||
for _ in range(32):
|
||||
f.observations(gear=6)
|
||||
self.assertTrue(f.tx(400))
|
||||
f.observations(gear=6, **changed) if 'gear' not in changed else f.observations(**changed)
|
||||
self.assertFalse(f.tx(400))
|
||||
self.assertTrue(f.tx(0, 1))
|
||||
|
||||
def test_moving_does_not_seed_sdgm_hold_credit_or_mixed_wheels(self):
|
||||
for index in (7, 8):
|
||||
f = self.fixture(index)
|
||||
if self.release:
|
||||
self.assertFalse(f.tx())
|
||||
continue
|
||||
for _ in range(32):
|
||||
f.observations(gear=6)
|
||||
self.assertTrue(f.tx(400))
|
||||
f.observations(speed=20, gear=4)
|
||||
self.assertFalse(f.tx()) # No accepted stationary hold; mode withdrawn.
|
||||
self.mode(f)
|
||||
self.assertTrue(f.tx(400))
|
||||
f.observations(speed=0)
|
||||
self.mode(f)
|
||||
self.assertTrue(f.tx(100))
|
||||
f.observations(speed=20)
|
||||
self.mode(f)
|
||||
self.assertTrue(f.tx(240))
|
||||
self.assertTrue(f.tx(400))
|
||||
self.mode(f, False)
|
||||
self.mode(f)
|
||||
self.assertTrue(f.tx(400)) # Mode withdrawal cleared old stop credit before reactivation.
|
||||
self.assertTrue(f.tx(0, 1))
|
||||
self.assertTrue(f.tx(400)) # Explicitly moving after released stop credit.
|
||||
f = self.fixture(0)
|
||||
for _ in range(32):
|
||||
f.observations(gear=6)
|
||||
f.rx(0x34A, [0, 20, 0, 0, 0])
|
||||
self.assertFalse(f.tx(400))
|
||||
|
||||
def test_no_positive_gas_and_unknown_tags_reset(self):
|
||||
f = self.fixture(0, alternative=32)
|
||||
for _ in range(32):
|
||||
f.observations(gear=6)
|
||||
f.safety.set_aol_test_heartbeat(True)
|
||||
f.safety.aol_set_host_request(1)
|
||||
self.assertTrue(f.tx(400))
|
||||
for gas, enabled, accepted in ((-650, False, True), (-649, False, False), (1, False, False), (-650, True, False)):
|
||||
frame = gmcan.create_gas_regen_command(f.packer, 0, gas, 0, enabled, False)
|
||||
self.assertEqual(f.safety.safety_tx_hook(libsafety_py.make_CANPacket(frame[0], frame[2], frame[1])), accepted)
|
||||
for word in (0xD0FF, 0xD10B, 0xD10F, 0xD11B, 0xD11F, 0xD120):
|
||||
f.init(word=word)
|
||||
for _ in range(32):
|
||||
f.observations(gear=6, healthy=False)
|
||||
self.assertFalse(f.tx(400))
|
||||
f.init()
|
||||
for _ in range(32):
|
||||
f.observations(gear=6)
|
||||
self.assertFalse(f.tx(400))
|
||||
f.observations(speed=0)
|
||||
self.assertTrue(f.tx(80))
|
||||
|
||||
def test_one_pedal_release_is_immediate_but_independent_hold_keeps_cooldown(self):
|
||||
for paired in (False, True):
|
||||
for stopped in (False, True):
|
||||
f = self.fixture(0, paired)
|
||||
for _ in range(32):
|
||||
f.observations(gear=6)
|
||||
speed = 0 if stopped else 100
|
||||
f.observations(speed=speed, gear=6, regen=True)
|
||||
self.assertFalse(f.tx(100))
|
||||
f.observations(speed=speed, gear=6)
|
||||
self.assertTrue(f.tx(100))
|
||||
f = self.fixture(0, paired=True)
|
||||
f.dwell()
|
||||
f.observations(speed=0, brake=True, regen=True)
|
||||
f.observations(speed=0, brake=True)
|
||||
self.assertFalse(f.tx())
|
||||
for _ in range(10):
|
||||
f.observations(speed=0, brake=True)
|
||||
self.assertTrue(f.tx())
|
||||
|
||||
def test_independent_paired_hold_keeps_240_and_sdgm_credit(self):
|
||||
for index in (0, 7, 8):
|
||||
f = self.fixture(index, paired=True)
|
||||
if self.release and index >= 2:
|
||||
self.assertFalse(f.tx())
|
||||
continue
|
||||
f.dwell()
|
||||
self.assertTrue(f.tx(240))
|
||||
self.assertFalse(f.tx(241))
|
||||
if index in (7, 8):
|
||||
f.observations(speed=20)
|
||||
self.assertTrue(f.tx(240))
|
||||
self.assertFalse(f.tx(241))
|
||||
self.assertTrue(f.tx(0, 1))
|
||||
self.assertFalse(f.tx(240))
|
||||
f.observations(speed=0, gear=6)
|
||||
self.assertTrue(f.tx(400))
|
||||
f.observations(speed=0, gear=6, brake=True)
|
||||
self.assertTrue(f.tx(240))
|
||||
self.assertFalse(f.tx(241))
|
||||
|
||||
def test_only_independent_auto_hold_seeds_sdgm_mixed_wheel_credit(self):
|
||||
for index in (7, 8):
|
||||
for paired in (False, True):
|
||||
with self.subTest(index=index, paired=paired):
|
||||
f = self.fixture(index, paired)
|
||||
if self.release:
|
||||
self.assertFalse(f.tx(200))
|
||||
continue
|
||||
for _ in range(32):
|
||||
f.observations(gear=6)
|
||||
f.observations(speed=0, gear=6)
|
||||
self.assertTrue(f.tx(200))
|
||||
f.rx(0x34A, [0, 20, 0, 0, 0])
|
||||
self.assertEqual(f.tx(200), paired)
|
||||
self.assertTrue(f.tx(0, 1))
|
||||
self.assertFalse(f.tx(200))
|
||||
@@ -24,6 +24,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"ToyotaAutoHold", {PERSISTENT, BOOL, "0"}},
|
||||
{"VoltSNG", {PERSISTENT, BOOL, "0"}},
|
||||
{"GMAutoHold", {PERSISTENT, BOOL, "0"}},
|
||||
{"VoltOnePedalMode", {PERSISTENT, BOOL, "0"}},
|
||||
{"HondaBoschARadar", {PERSISTENT, BOOL, "1"}},
|
||||
{"HondaGasFactorParams", {PERSISTENT, FLOAT, "1.0"}},
|
||||
{"HondaWindFactorParams", {PERSISTENT, FLOAT, "1.0"}},
|
||||
|
||||
@@ -180,7 +180,11 @@ int main() {
|
||||
forte.safety_mode = HYUNDAI_AOL_PROFILE.mode;
|
||||
assert(!aol_capable(forte, HYUNDAI_AOL_PROFILE.mode, VEHICLE_REGISTRY));
|
||||
}
|
||||
for (const uint16_t word : {0U, 0x80U}) {
|
||||
for (const uint16_t word : {0U, 0x80U,
|
||||
0xD100U, 0xD101U, 0xD102U, 0xD103U, 0xD104U, 0xD105U,
|
||||
0xD106U, 0xD107U, 0xD108U, 0xD109U, 0xD10AU,
|
||||
0xD110U, 0xD111U, 0xD112U, 0xD113U, 0xD114U, 0xD115U,
|
||||
0xD116U, 0xD117U, 0xD118U, 0xD119U, 0xD11AU}) {
|
||||
AolAxisNegotiator negotiator(VEHICLE_REGISTRY);
|
||||
FakeTransport transport;
|
||||
auto gateway = status();
|
||||
@@ -190,20 +194,26 @@ int main() {
|
||||
queue(transport, gateway, gateway);
|
||||
const auto first = run(negotiator, transport, axis("gateway"), NOW, GM_AOL_PROFILE.mode);
|
||||
assert(first.plan.capable && first.plan.request_mask == 0U && first.outcome.compatible);
|
||||
assert(first.outcome.status && first.outcome.status->safety_param == word);
|
||||
auto active = gateway;
|
||||
active.request_mask = 3U;
|
||||
active.permission_mask = 1U;
|
||||
queue(transport, gateway, active);
|
||||
const auto second = run(negotiator, transport, axis("gateway", true, true), NOW, GM_AOL_PROFILE.mode);
|
||||
assert(second.plan.capable && second.plan.request_mask == 3U && second.outcome.compatible);
|
||||
assert(second.outcome.status && second.outcome.status->permission_mask == 1U);
|
||||
assert(second.outcome.status && second.outcome.status->permission_mask == 1U && second.outcome.status->safety_param == word);
|
||||
}
|
||||
for (const uint16_t word : {0x40U, 0x81U, 0x82U, 0x84U, 0x180U}) {
|
||||
for (const uint16_t word : {0x40U, 0x81U, 0x82U, 0x84U, 0x180U, 0xD0FFU, 0xD10BU, 0xD10FU, 0xD11BU, 0xD11FU, 0xD120U}) {
|
||||
assert(!gm_aol_param(word));
|
||||
auto unknown = status();
|
||||
unknown.safety_mode = GM_AOL_PROFILE.mode;
|
||||
unknown.safety_param = word;
|
||||
assert(!aol_capable(unknown, GM_AOL_PROFILE.mode, VEHICLE_REGISTRY));
|
||||
AolAxisNegotiator negotiator(VEHICLE_REGISTRY);
|
||||
FakeTransport transport;
|
||||
queue(transport, unknown, unknown);
|
||||
const auto rejected = run(negotiator, transport, axis(), NOW, GM_AOL_PROFILE.mode);
|
||||
assert(!rejected.plan.capable && rejected.plan.request_mask == 0U && !rejected.outcome.compatible);
|
||||
}
|
||||
for (uint32_t word = 0U; word <= 65535U; word++) {
|
||||
auto gm = status();
|
||||
|
||||
@@ -16,7 +16,7 @@ from opendbc.car.gm.tests.test_bolt_volt_configurations import ordinary_params
|
||||
from opendbc.car.gm.tests.test_volt_camera_control import camera_params
|
||||
from opendbc.car.gm.tests.test_volt_camera_removed import removed_params
|
||||
from opendbc.car.gm.tests.test_volt_sdgm_control import sdgm_params
|
||||
from opendbc.car.gm.values import CAR, DBC, ORDINARY_ASCM_CAR, ORDINARY_SDGM_CAR, ORDINARY_CAMERA_CAR, ORDINARY_CC_CAR
|
||||
from opendbc.car.gm.values import CAR, DBC, ORDINARY_ASCM_CAR, ORDINARY_SDGM_CAR, ORDINARY_CAMERA_CAR, ORDINARY_CC_CAR, is_volt_one_pedal
|
||||
from openpilot.cereal import messaging
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
@@ -106,6 +106,11 @@ def configurations():
|
||||
VehicleStartupPreferences(gm_auto_hold=True).prepare(selected)
|
||||
if selected.safetyConfigs[0].safetyParam != cp.safetyConfigs[0].safetyParam:
|
||||
yield selected
|
||||
for paired in (False, True):
|
||||
selected = cp.as_reader().as_builder()
|
||||
VehicleStartupPreferences(volt_one_pedal=True, gm_auto_hold=paired).prepare(selected)
|
||||
if is_volt_one_pedal(selected):
|
||||
yield selected
|
||||
|
||||
|
||||
class TestGmAol(unittest.TestCase):
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
|
||||
inline bool gm_aol_param(uint16_t param) {
|
||||
switch (param) {
|
||||
case 0xD100U: case 0xD101U: case 0xD102U: case 0xD103U:
|
||||
case 0xD104U: case 0xD105U: case 0xD106U: case 0xD107U:
|
||||
case 0xD108U: case 0xD109U: case 0xD10AU: case 0xD110U:
|
||||
case 0xD111U: case 0xD112U: case 0xD113U: case 0xD114U:
|
||||
case 0xD115U: case 0xD116U: case 0xD117U: case 0xD118U:
|
||||
case 0xD119U: case 0xD11AU:
|
||||
case 0U: case 0x80U:
|
||||
case 0x201U: case 0x601U: case 0xA01U: case 0xE01U:
|
||||
case 0x203U: case 0x603U: case 0xA03U: case 0xE03U:
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
from opendbc.car.gm.values import is_gm_auto_hold
|
||||
from opendbc.car.gm.values import is_gm_auto_hold, is_volt_one_pedal
|
||||
from openpilot.starpilot.saved_source import read_saved
|
||||
|
||||
|
||||
class AutoHoldPreference:
|
||||
def __init__(self, cp, params):
|
||||
self.cp, self.params = cp, params
|
||||
def __init__(self, cp, params, key="GMAutoHold"):
|
||||
if key not in ("GMAutoHold", "VoltOnePedalMode"):
|
||||
raise ValueError("Unsupported GM stop preference")
|
||||
self.cp, self.params, self.key = cp, params, key
|
||||
self.supported = is_volt_one_pedal if key == "VoltOnePedalMode" else is_gm_auto_hold
|
||||
self.read_ns = 0
|
||||
self.enabled = False
|
||||
|
||||
def update(self, now_ns):
|
||||
if type(now_ns) is not int or now_ns <= 0 or not is_gm_auto_hold(self.cp):
|
||||
if type(now_ns) is not int or now_ns <= 0 or not self.supported(self.cp):
|
||||
self.enabled = False
|
||||
return False
|
||||
if now_ns < self.read_ns:
|
||||
@@ -19,7 +22,7 @@ class AutoHoldPreference:
|
||||
if self.read_ns == 0 or now_ns - self.read_ns >= 250_000_000:
|
||||
self.read_ns = now_ns
|
||||
try:
|
||||
choice = read_saved(self.params, "GMAutoHold", 8)
|
||||
choice = read_saved(self.params, self.key, 8)
|
||||
safe, safe_valid = read_saved(self.params, "SafeMode", 8)
|
||||
longitudinal, long_valid = read_saved(self.params, "DisableOpenpilotLongitudinal", 8)
|
||||
master = read_saved(self.params, "OpenpilotEnabledToggle", 8)
|
||||
|
||||
@@ -110,10 +110,13 @@ class ResumePlanInputs:
|
||||
def configure_controller(CI, params):
|
||||
cp = CI.CP
|
||||
controller = CI.CC
|
||||
from opendbc.car.gm.values import is_gm_auto_hold
|
||||
from opendbc.car.gm.values import is_gm_auto_hold, is_volt_one_pedal
|
||||
if controller is not None and is_gm_auto_hold(cp) and controller.gm_auto_hold:
|
||||
from openpilot.starpilot.car.gm.auto_hold import AutoHoldPreference
|
||||
controller.gm_auto_hold_input = AutoHoldPreference(cp, params)
|
||||
if controller is not None and is_volt_one_pedal(cp) and controller.volt_one_pedal:
|
||||
from openpilot.starpilot.car.gm.auto_hold import AutoHoldPreference
|
||||
controller.volt_one_pedal_input = AutoHoldPreference(cp, params, "VoltOnePedalMode")
|
||||
from opendbc.car.gm.values import is_volt_longitudinal
|
||||
if controller is not None and is_volt_longitudinal(cp) and controller.volt_sng:
|
||||
try:
|
||||
|
||||
@@ -5,7 +5,7 @@ from openpilot.starpilot.vehicle_selection import read_selection
|
||||
|
||||
|
||||
STARTUP_KEYS = ('AlphaLongitudinalEnabled', 'IsReleaseBranch', 'OpenpilotEnabledToggle', 'SafeMode',
|
||||
'DisableOpenpilotLongitudinal', 'ToyotaAutoHold', 'VoltSNG', 'GMAutoHold', 'TurnAssist', 'LongPitch',
|
||||
'DisableOpenpilotLongitudinal', 'ToyotaAutoHold', 'VoltSNG', 'GMAutoHold', 'VoltOnePedalMode', 'TurnAssist', 'LongPitch',
|
||||
'HondaBoschARadar', 'NAPPedalEnabled', 'NAPRadarEnabled', 'NAPRadarBehindNosecone')
|
||||
|
||||
|
||||
|
||||
@@ -75,7 +75,10 @@ class TestGMPitchStartupAndSettings(unittest.TestCase):
|
||||
from opendbc.car.gm.interface import CarInterface
|
||||
from openpilot.starpilot.vehicle_selection import encode
|
||||
from openpilot.starpilot.ui.feature_settings_state import FeaturePage, row_change, row_default
|
||||
settings = (("VoltSNG", "volt_sng", (CAR.CHEVROLET_VOLT, CAR.CHEVROLET_VOLT_ASCM,
|
||||
from openpilot.starpilot.ui.vehicle_bool import confirmation_question
|
||||
settings = (("VoltOnePedalMode", "volt_one_pedal", (CAR.CHEVROLET_VOLT, CAR.CHEVROLET_VOLT_ASCM,
|
||||
CAR.CHEVROLET_VOLT_CAMERA, CAR.CHEVROLET_VOLT_2019)),
|
||||
("VoltSNG", "volt_sng", (CAR.CHEVROLET_VOLT, CAR.CHEVROLET_VOLT_ASCM,
|
||||
CAR.CHEVROLET_VOLT_CAMERA, CAR.CHEVROLET_VOLT_2019)),
|
||||
("GMAutoHold", "gm_auto_hold", (CAR.CHEVROLET_VOLT, CAR.CHEVROLET_VOLT_ASCM,
|
||||
CAR.CHEVROLET_VOLT_CAMERA, CAR.CHEVROLET_VOLT_2019, CAR.BUICK_LACROSSE)))
|
||||
@@ -95,11 +98,16 @@ class TestGMPitchStartupAndSettings(unittest.TestCase):
|
||||
self.assertEqual("regen paddle" in row().reason, identity != CAR.BUICK_LACROSSE)
|
||||
change = row_change(row())
|
||||
assert change is not None
|
||||
question = confirmation_question(change)
|
||||
if key == "GMAutoHold":
|
||||
self.assertEqual("regen paddle" in question, identity != CAR.BUICK_LACROSSE)
|
||||
elif key == "VoltOnePedalMode":
|
||||
self.assertIn("Lifting off the accelerator can apply the brakes", question)
|
||||
self.assertTrue(owner.apply(replace(change, confirmation=True)))
|
||||
self.assertEqual(self.saved.get_bool(key), change.value == 'On')
|
||||
self.assertTrue(owner.apply(row_default(row())))
|
||||
self.assertFalse(self.saved.get_bool(key))
|
||||
controller = SimpleNamespace(volt_sng=False, gm_auto_hold=False, long_pitch=True)
|
||||
controller = SimpleNamespace(volt_sng=False, gm_auto_hold=False, volt_one_pedal=False, long_pitch=True)
|
||||
VehicleStartupPreferences(**{preference: True}).configure_controller(SimpleNamespace(CP=cp, CC=controller))
|
||||
self.assertFalse(getattr(controller, preference))
|
||||
fresh = row()
|
||||
|
||||
@@ -27,7 +27,7 @@ class TestVehicleStartupPreferences(unittest.TestCase):
|
||||
|
||||
def test_gm_stop_preferences_require_strict_saved_opt_in(self):
|
||||
from openpilot.common.params import ParamKeyType
|
||||
for key, name in (("VoltSNG", "volt_sng"), ("GMAutoHold", "gm_auto_hold")):
|
||||
for key, name in (("VoltSNG", "volt_sng"), ("GMAutoHold", "gm_auto_hold"), ("VoltOnePedalMode", "volt_one_pedal")):
|
||||
self.raw("SafeMode", None)
|
||||
self.assertEqual(self.params.get_type(key), ParamKeyType.BOOL)
|
||||
self.assertIs(self.params.get_default_value(key), False)
|
||||
@@ -43,29 +43,31 @@ class TestVehicleStartupPreferences(unittest.TestCase):
|
||||
from opendbc.car.gm.tests.test_bolt_volt_configurations import ordinary_params
|
||||
from opendbc.car.gm.values import CAR as GM_CAR
|
||||
from openpilot.starpilot.car.gm.auto_hold import AutoHoldPreference
|
||||
cp = ordinary_params(GM_CAR.CHEVROLET_VOLT, radar=True)
|
||||
VehicleStartupPreferences(gm_auto_hold=True).prepare(cp)
|
||||
now = 1_000_000_000
|
||||
for key in ("GMAutoHold", "OpenpilotEnabledToggle", "SafeMode", "DisableOpenpilotLongitudinal"):
|
||||
for raw in (b"0", b"1", b"", b"true", None):
|
||||
for clean_key, value in (("GMAutoHold", b"1"), ("OpenpilotEnabledToggle", b"1"),
|
||||
("SafeMode", b"0"), ("DisableOpenpilotLongitudinal", b"0")):
|
||||
self.raw(clean_key, value)
|
||||
owner = AutoHoldPreference(cp, self.params)
|
||||
self.assertTrue(owner.update(now))
|
||||
self.raw(key, raw)
|
||||
now += 250_000_000
|
||||
expected = raw == b"1" if key in ("GMAutoHold", "OpenpilotEnabledToggle") else raw in (None, b"0")
|
||||
self.assertEqual(owner.update(now), expected, (key, raw))
|
||||
self.assertFalse(owner.update(now - 1))
|
||||
cp.passive = True
|
||||
self.assertFalse(owner.update(now))
|
||||
cp.passive = False
|
||||
preferences = VehicleStartupPreferences(gm_auto_hold=True, disable_bolt_long=True)
|
||||
preferences.prepare(cp)
|
||||
preferences.finalize(cp)
|
||||
self.assertFalse(cp.openpilotLongitudinalControl)
|
||||
self.assertEqual(cp.safetyConfigs[0].safetyParam, 0x4004)
|
||||
for preference in ("GMAutoHold", "VoltOnePedalMode"):
|
||||
cp = ordinary_params(GM_CAR.CHEVROLET_VOLT, radar=True)
|
||||
VehicleStartupPreferences(gm_auto_hold=preference == "GMAutoHold",
|
||||
volt_one_pedal=preference == "VoltOnePedalMode").prepare(cp)
|
||||
now = 1_000_000_000
|
||||
for key in (preference, "OpenpilotEnabledToggle", "SafeMode", "DisableOpenpilotLongitudinal"):
|
||||
for raw in (b"0", b"1", b"", b"true", None):
|
||||
for clean_key, value in ((preference, b"1"), ("OpenpilotEnabledToggle", b"1"),
|
||||
("SafeMode", b"0"), ("DisableOpenpilotLongitudinal", b"0")):
|
||||
self.raw(clean_key, value)
|
||||
owner = AutoHoldPreference(cp, self.params, preference)
|
||||
self.assertTrue(owner.update(now))
|
||||
self.raw(key, raw)
|
||||
now += 250_000_000
|
||||
expected = raw == b"1" if key in (preference, "OpenpilotEnabledToggle") else raw in (None, b"0")
|
||||
self.assertEqual(owner.update(now), expected, (preference, key, raw))
|
||||
self.assertFalse(owner.update(now - 1))
|
||||
cp.passive = True
|
||||
self.assertFalse(owner.update(now))
|
||||
cp.passive = False
|
||||
preferences = VehicleStartupPreferences(gm_auto_hold=True, volt_one_pedal=True, disable_bolt_long=True)
|
||||
preferences.prepare(cp)
|
||||
preferences.finalize(cp)
|
||||
self.assertFalse(cp.openpilotLongitudinalControl)
|
||||
self.assertEqual(cp.safetyConfigs[0].safetyParam, 0x4004)
|
||||
|
||||
def test_suburban_saved_long_pitch_configures_exact_controller(self):
|
||||
from opendbc.car.gm.carcontroller import CarController
|
||||
@@ -156,6 +158,54 @@ class TestVehicleStartupPreferences(unittest.TestCase):
|
||||
else:
|
||||
self.assertEqual(published.safetyConfigs[0].safetyModel, structs.CarParams.SafetyModel.noOutput)
|
||||
|
||||
def test_volt_one_pedal_card_retains_exact_owner_and_withdrawal(self):
|
||||
from opendbc.car.gm.values import CAR as GM_CAR, is_volt_one_pedal, gm_control_word
|
||||
profiles = [
|
||||
(GM_CAR.CHEVROLET_VOLT, {0xBE: 6}, {0x460: 8}, {}, 0xD100),
|
||||
(GM_CAR.CHEVROLET_VOLT, {}, {0x460: 8}, {}, 0xD101),
|
||||
(GM_CAR.CHEVROLET_VOLT_ASCM, {0x2FF: 8, 0xBE: 6}, {}, {}, 0xD102),
|
||||
(GM_CAR.CHEVROLET_VOLT_ASCM, {0x2FF: 8}, {}, {}, 0xD103),
|
||||
(GM_CAR.CHEVROLET_VOLT_ASCM, {0x2FF: 8, 0xBE: 6}, {0x460: 8}, {}, 0xD104),
|
||||
(GM_CAR.CHEVROLET_VOLT_ASCM, {0x2FF: 8}, {0x460: 8}, {}, 0xD105),
|
||||
(GM_CAR.CHEVROLET_VOLT_CAMERA, {}, {}, {0x320: 6}, 0xD106),
|
||||
(GM_CAR.CHEVROLET_VOLT_2019, {0x2FF: 8, 0xBE: 6}, {}, {}, 0xD107),
|
||||
(GM_CAR.CHEVROLET_VOLT_2019, {0x2FF: 8}, {}, {}, 0xD108),
|
||||
]
|
||||
for alternate in (False, True):
|
||||
observed = {0x184: 8, 0x34A: 5, 0x1C4: 8, 0xC9: 8, 0x1E1: 7, 0xF1 if alternate else 0xBE: 6}
|
||||
profiles.append((GM_CAR.CHEVROLET_VOLT_CAMERA, observed, {}, {}, 0xD109 + alternate))
|
||||
for candidate, pt, radar, camera, solo_word in profiles:
|
||||
for paired in (False, True):
|
||||
for state in ('on', 'off', 'live_off', 'master_off', 'safe', 'disable_long', 'release'):
|
||||
with self.subTest(candidate=candidate, word=hex(solo_word), paired=paired, state=state):
|
||||
observed = gen_empty_fingerprint()
|
||||
observed[0], observed[1], observed[2] = dict(pt), dict(radar), dict(camera)
|
||||
self.params.put_bool('AlphaLongitudinalEnabled', True, block=True)
|
||||
self.params.put_bool('GMAutoHold', paired, block=True)
|
||||
self.params.put_bool('SafeMode', state == 'safe', block=True)
|
||||
self.params.put_bool('DisableOpenpilotLongitudinal', state == 'disable_long', block=True)
|
||||
self.params.put_bool('IsReleaseBranch', state == 'release', block=True)
|
||||
host, constructed, published = self.start(candidate, key='VoltOnePedalMode', observed=observed,
|
||||
capture=lambda ci: int(ci.CS.CP.safetyConfigs[0].safetyParam), enabled=state != 'master_off',
|
||||
requested=state != 'off', change_saved=state == 'live_off')
|
||||
admitted = state in ('on', 'live_off') or state == 'release' and candidate == GM_CAR.CHEVROLET_VOLT
|
||||
self.assertEqual(is_volt_one_pedal(published), admitted)
|
||||
if admitted:
|
||||
self.assertEqual(constructed, [solo_word + (0x10 if paired else 0)])
|
||||
self.assertEqual(published.safetyConfigs[0].safetyParam, constructed[0])
|
||||
self.assertTrue(host.CI.CC.volt_one_pedal)
|
||||
self.assertEqual(host.CI.CC.volt_one_pedal_input.update(1_000_000_000), state != 'live_off')
|
||||
self.assertEqual(host.CI.CC.gm_auto_hold, paired)
|
||||
else:
|
||||
self.assertFalse(host.CI.CC.volt_one_pedal)
|
||||
self.assertIsNone(host.CI.CC.volt_one_pedal_input)
|
||||
self.assertEqual(gm_control_word(host.CI.CP), host.CI.CP.safetyConfigs[0].safetyParam)
|
||||
if state == 'master_off':
|
||||
self.assertEqual(published.safetyConfigs[0].safetyModel, structs.CarParams.SafetyModel.noOutput)
|
||||
if state == 'disable_long':
|
||||
self.assertFalse(published.openpilotLongitudinalControl)
|
||||
self.assertEqual(self.params.get_bool('VoltOnePedalMode'), state not in ('off', 'live_off'))
|
||||
|
||||
def test_camera_volt_hold_startup_retains_release_and_disable_longitudinal_owners(self):
|
||||
from opendbc.car.gm.values import CAR as GM_CAR, is_gm_auto_hold
|
||||
for candidate in (GM_CAR.CHEVROLET_VOLT_ASCM, GM_CAR.CHEVROLET_VOLT_CAMERA, GM_CAR.CHEVROLET_VOLT_2019):
|
||||
|
||||
@@ -65,7 +65,8 @@ from openpilot.starpilot.controllers.toyota_cruise import capability as toyota_c
|
||||
|
||||
BOOL_DEFAULTS = {
|
||||
"GMPedalLongitudinal": False, "ForceStops": False, "AlwaysAllowUploads": False, "TurnAssist": True,
|
||||
"ReverseCruise": False, "ToyotaAutoHold": False, "VoltSNG": False, "GMAutoHold": False, "LongPitch": True, "DisableOpenpilotLongitudinal": False,
|
||||
"ReverseCruise": False, "ToyotaAutoHold": False, "VoltSNG": False, "GMAutoHold": False, "VoltOnePedalMode": False,
|
||||
"LongPitch": True, "DisableOpenpilotLongitudinal": False,
|
||||
"SpeedLimitController": False, "ShowSpeedLimits": False,
|
||||
"SLCConfirmation": False, "SLCConfirmationHigher": False, "SLCConfirmationLower": False,
|
||||
"LaneCentering": False, "LaneCenteringPauseOnSignal": True,
|
||||
@@ -255,7 +256,7 @@ class FeatureSettingsOwner:
|
||||
identities = {CAR.CHEVROLET_VOLT, CAR.CHEVROLET_VOLT_ASCM, CAR.CHEVROLET_VOLT_CAMERA, CAR.CHEVROLET_VOLT_2019}
|
||||
if key == "GMAutoHold":
|
||||
identities.add(CAR.BUICK_LACROSSE)
|
||||
if key not in ("VoltSNG", "GMAutoHold") or cp is None or cp.brand != "gm" or cp.carFingerprint not in identities:
|
||||
if key not in ("VoltSNG", "GMAutoHold", "VoltOnePedalMode") or cp is None or cp.brand != "gm" or cp.carFingerprint not in identities:
|
||||
return None
|
||||
selection = read_selection(self.params)
|
||||
if not selection.readable or not selection.valid:
|
||||
@@ -613,6 +614,12 @@ class FeatureSettingsOwner:
|
||||
row = self._bool_row("VoltSNG", "Volt Stop-and-Go Assistance", parked and self.authority("parked_preferences"),
|
||||
"Assists automatic resume from a cruise-controlled stop. Requires StarPilot speed control; applies after the next startup")
|
||||
rows.append(replace(row, available=row.available and self._readable("VoltSNG"), capability=sng_capability))
|
||||
one_pedal_capability = self._gm_stop_capability("VoltOnePedalMode")
|
||||
if one_pedal_capability is not None:
|
||||
row = self._bool_row("VoltOnePedalMode", "Volt One-Pedal Driving", parked and self.authority("parked_preferences"),
|
||||
"With cruise main on and cruise disengaged, slows and holds when you lift off the accelerator in Low or single-pedal mode. " +
|
||||
"Requires StarPilot speed control; applies after the next startup")
|
||||
rows.append(replace(row, available=row.available and self._readable("VoltOnePedalMode"), capability=one_pedal_capability))
|
||||
gm_hold_capability = self._gm_stop_capability("GMAutoHold")
|
||||
if gm_hold_capability is not None:
|
||||
from opendbc.car.gm.values import CAR
|
||||
@@ -993,7 +1000,7 @@ class FeatureSettingsOwner:
|
||||
return self._apply_pedal_setup(request)
|
||||
if key == "DisableOpenpilotLongitudinal":
|
||||
return self._apply_bolt_disable(request)
|
||||
if key in ("VoltSNG", "GMAutoHold"):
|
||||
if key in ("VoltSNG", "GMAutoHold", "VoltOnePedalMode"):
|
||||
return self._apply_gm_stop(request)
|
||||
if key == "LongPitch":
|
||||
return self._apply_long_pitch(request)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Confirmation text for parked vehicle preferences."""
|
||||
|
||||
VEHICLE_BOOL_KEYS = frozenset(("GMPedalLongitudinal", "DisableOpenpilotLongitudinal", "LongPitch", "VoltSNG", "GMAutoHold"))
|
||||
VEHICLE_BOOL_KEYS = frozenset(("GMPedalLongitudinal", "DisableOpenpilotLongitudinal", "LongPitch", "VoltSNG", "GMAutoHold", "VoltOnePedalMode"))
|
||||
|
||||
|
||||
def confirmation_question(request):
|
||||
@@ -16,7 +16,11 @@ def confirmation_question(request):
|
||||
if request.key == "VoltSNG":
|
||||
return f"{verb} Volt stop-and-go assistance after the next startup?"
|
||||
if request.key == "GMAutoHold":
|
||||
return f"{verb} Automatic Brake Hold after the next startup? Press the gas or regen paddle to release a hold."
|
||||
from opendbc.car.gm.values import CAR
|
||||
release = "gas" if request.vehicle_fingerprint == CAR.BUICK_LACROSSE else "gas or regen paddle"
|
||||
return f"{verb} Automatic Brake Hold after the next startup? Press the {release} to release a hold."
|
||||
if request.key == "VoltOnePedalMode":
|
||||
return f"{verb} Volt one-pedal driving after the next startup? Lifting off the accelerator can apply the brakes with cruise disengaged and cruise main on."
|
||||
if request.key == "LongPitch":
|
||||
return f"{verb} Grade Compensation after the next startup?"
|
||||
raise ValueError("Unsupported vehicle preference")
|
||||
|
||||
@@ -24,6 +24,7 @@ class VehicleStartupPreferences:
|
||||
tesla_preap_stock: bool = True
|
||||
volt_sng: bool = False
|
||||
gm_auto_hold: bool = False
|
||||
volt_one_pedal: bool = False
|
||||
|
||||
@classmethod
|
||||
def read(cls, params, *, enabled: bool):
|
||||
@@ -47,6 +48,8 @@ class VehicleStartupPreferences:
|
||||
readable and safe in (None, b"0"))
|
||||
gm_auto_hold = bool(enabled and not disable_bolt and read_saved(params, "GMAutoHold", 8) == (b"1", True) and
|
||||
readable and safe in (None, b"0"))
|
||||
volt_one_pedal = bool(enabled and not disable_bolt and read_saved(params, "VoltOnePedalMode", 8) == (b"1", True) and
|
||||
readable and safe in (None, b"0"))
|
||||
except (OSError, TypeError, ValueError):
|
||||
return cls(disable_bolt_long=disable_bolt, honda_bosch_a_radar=honda_radar, tesla_preap_stock=preap_stock)
|
||||
try:
|
||||
@@ -59,7 +62,7 @@ class VehicleStartupPreferences:
|
||||
assist = assist_readable and assist_raw in (None, b"1")
|
||||
except (OSError, TypeError, ValueError):
|
||||
assist = False
|
||||
return cls(toyota_auto_hold=toyota, volt_sng=volt_sng, gm_auto_hold=gm_auto_hold,
|
||||
return cls(toyota_auto_hold=toyota, volt_sng=volt_sng, gm_auto_hold=gm_auto_hold, volt_one_pedal=volt_one_pedal,
|
||||
turn_assist=bool(enabled and assist and readable and safe in (None, b"0")),
|
||||
gm_long_pitch=pitch_enabled, disable_bolt_long=disable_bolt, honda_bosch_a_radar=honda_radar, tesla_preap_stock=preap_stock)
|
||||
|
||||
@@ -97,35 +100,40 @@ class VehicleStartupPreferences:
|
||||
cp.dashcamOnly = True
|
||||
|
||||
def prepare(self, cp, *, fingerprints=None):
|
||||
from opendbc.car.gm.values import apply_gm_auto_hold
|
||||
from opendbc.car.gm.values import apply_gm_auto_hold, apply_volt_one_pedal
|
||||
from openpilot.starpilot.car.tesla.preap_preferences import prepare_stock
|
||||
prepare_stock(cp, self.tesla_preap_stock)
|
||||
self._prepare_honda_radar(cp)
|
||||
self._prepare_bolt(cp, fingerprints)
|
||||
prepare_disable_longitudinal(cp, self.disable_bolt_long)
|
||||
apply_gm_auto_hold(cp, self.gm_auto_hold)
|
||||
apply_volt_one_pedal(cp, self.volt_one_pedal and not self.disable_bolt_long, self.gm_auto_hold)
|
||||
if cp.brand == "toyota":
|
||||
apply_toyota_auto_hold(cp, self.toyota_auto_hold)
|
||||
return cp
|
||||
|
||||
def finalize(self, cp) -> None:
|
||||
from opendbc.car.gm.values import apply_gm_auto_hold, is_gm_auto_hold
|
||||
from opendbc.car.gm.values import apply_gm_auto_hold, is_gm_auto_hold, apply_volt_one_pedal, is_volt_one_pedal
|
||||
admitted_one_pedal = is_volt_one_pedal(cp)
|
||||
admitted_hold = is_gm_auto_hold(cp)
|
||||
from openpilot.starpilot.car.tesla.preap_preferences import prepare_stock
|
||||
prepare_stock(cp, self.tesla_preap_stock)
|
||||
self._prepare_honda_radar(cp)
|
||||
self._prepare_bolt(cp)
|
||||
prepare_disable_longitudinal(cp, self.disable_bolt_long)
|
||||
apply_gm_auto_hold(cp, self.gm_auto_hold and is_gm_auto_hold(cp))
|
||||
apply_gm_auto_hold(cp, self.gm_auto_hold and admitted_hold)
|
||||
apply_volt_one_pedal(cp, self.volt_one_pedal and admitted_one_pedal and not self.disable_bolt_long, self.gm_auto_hold and admitted_hold)
|
||||
if cp.brand == "toyota":
|
||||
admitted = bool(cp.flags & ToyotaFlags.AUTO_BRAKE_HOLD)
|
||||
apply_toyota_auto_hold(cp, self.toyota_auto_hold and admitted)
|
||||
|
||||
def configure_controller(self, ci) -> None:
|
||||
from opendbc.car.gm.values import CAR, is_bolt_euv_longitudinal, is_volt_longitudinal, is_gm_auto_hold
|
||||
from opendbc.car.gm.values import CAR, is_bolt_euv_longitudinal, is_volt_longitudinal, is_gm_auto_hold, is_volt_one_pedal
|
||||
cp = ci.CP
|
||||
if ci.CC is not None and is_volt_longitudinal(cp):
|
||||
ci.CC.volt_sng = self.volt_sng
|
||||
if ci.CC is not None and cp.brand == "gm":
|
||||
ci.CC.gm_auto_hold = self.gm_auto_hold and is_gm_auto_hold(cp)
|
||||
ci.CC.volt_one_pedal = self.volt_one_pedal and is_volt_one_pedal(cp)
|
||||
if ci.CC is not None and (is_bolt_euv_longitudinal(cp) or is_volt_longitudinal(cp) or cp.carFingerprint == CAR.CHEVROLET_SUBURBAN):
|
||||
ci.CC.long_pitch = self.gm_long_pitch
|
||||
|
||||
Reference in New Issue
Block a user