Compare commits

..

2 Commits

Author SHA1 Message Date
firestar5683 79b791a26b EV6 Hotfix 2026-09-10 11:30:48 -05:00
firestar5683 1c35e376e9 uno mas lil dip 2026-09-08 21:15:28 -05:00
103 changed files with 253 additions and 4455 deletions
+1 -1
View File
@@ -194,7 +194,7 @@ def get_checksum_state(dbc_name: str) -> ChecksumState | None:
return ChecksumState(4, 2, 3, 5, False, SignalType.HONDA_CHECKSUM, honda_checksum) return ChecksumState(4, 2, 3, 5, False, SignalType.HONDA_CHECKSUM, honda_checksum)
elif dbc_name.startswith(("toyota_", "lexus_")): elif dbc_name.startswith(("toyota_", "lexus_")):
return ChecksumState(8, -1, 7, -1, False, SignalType.TOYOTA_CHECKSUM, toyota_checksum) return ChecksumState(8, -1, 7, -1, False, SignalType.TOYOTA_CHECKSUM, toyota_checksum)
elif dbc_name.startswith(("hyundai_canfd_generated", "hyundai_radar_210_21f_generated")): elif dbc_name.startswith("hyundai_canfd_generated"):
return ChecksumState(16, -1, 0, -1, True, SignalType.HKG_CAN_FD_CHECKSUM, hkg_can_fd_checksum) return ChecksumState(16, -1, 0, -1, True, SignalType.HKG_CAN_FD_CHECKSUM, hkg_can_fd_checksum)
elif dbc_name.startswith(("vw_mqb", "vw_mqbevo", "vw_meb")): elif dbc_name.startswith(("vw_mqb", "vw_mqbevo", "vw_meb")):
return ChecksumState(8, 4, 0, 0, True, SignalType.VOLKSWAGEN_MQB_MEB_CHECKSUM, volkswagen_mqb_meb_checksum) return ChecksumState(8, 4, 0, 0, True, SignalType.VOLKSWAGEN_MQB_MEB_CHECKSUM, volkswagen_mqb_meb_checksum)
+2 -2
View File
@@ -6,7 +6,7 @@ from opendbc.car.lateral import apply_driver_steer_torque_limits
from opendbc.car.gm import gmcan from opendbc.car.gm import gmcan
from opendbc.car.common.conversions import Conversions as CV from opendbc.car.common.conversions import Conversions as CV
from opendbc.car.gm.values import ( from opendbc.car.gm.values import (
ASCM_INT, CAMERA_ACC_CAR, CAR, CC_ONLY_CAR, CC_REGEN_PADDLE_CAR, DBC, EV_CAR, GM_AUTO_HOLD_CARS, SDGM_CAR, AccState, CanBus, CarControllerParams, ASCM_INT, CAMERA_ACC_CAR, CAR, CC_ONLY_CAR, CC_REGEN_PADDLE_CAR, DBC, EV_CAR, SDGM_CAR, AccState, CanBus, CarControllerParams,
CruiseButtons, GMFlags, GMSafetyFlags, CruiseButtons, GMFlags, GMSafetyFlags,
) )
from opendbc.car.interfaces import CarControllerBase from opendbc.car.interfaces import CarControllerBase
@@ -309,7 +309,7 @@ def supports_volt_auto_hold(CP, auto_hold_enabled: bool):
auto_hold_enabled and auto_hold_enabled and
getattr(CP, "openpilotLongitudinalControl", False) and getattr(CP, "openpilotLongitudinalControl", False) and
stock_hold_safety_ready and stock_hold_safety_ready and
CP.carFingerprint in GM_AUTO_HOLD_CARS CP.carFingerprint in AUTO_HOLD_VOLT_CARS
) )
+2 -33
View File
@@ -32,7 +32,6 @@ STANDSTILL_THRESHOLD = 10 * 0.0311
VOLT_EBCM_BRAKE_PRESSED_THRESHOLD = 6 / 0xd0 VOLT_EBCM_BRAKE_PRESSED_THRESHOLD = 6 / 0xd0
AUTO_HOLD_MIN_DRIVE_TIME_S = 3.0 AUTO_HOLD_MIN_DRIVE_TIME_S = 3.0
AUTO_HOLD_REGEN_RELEASE_COOLDOWN_S = 1.0 AUTO_HOLD_REGEN_RELEASE_COOLDOWN_S = 1.0
ACC_STARTUP_FAULT_GRACE_PERIOD_S = 5.0
BUTTONS_DICT = {CruiseButtons.RES_ACCEL: ButtonType.accelCruise, CruiseButtons.DECEL_SET: ButtonType.decelCruise, BUTTONS_DICT = {CruiseButtons.RES_ACCEL: ButtonType.accelCruise, CruiseButtons.DECEL_SET: ButtonType.decelCruise,
CruiseButtons.MAIN: ButtonType.mainCruise, CruiseButtons.CANCEL: ButtonType.cancel} CruiseButtons.MAIN: ButtonType.mainCruise, CruiseButtons.CANCEL: ButtonType.cancel}
@@ -68,24 +67,6 @@ def update_auto_hold_drive_timers(in_drive_for_hold: bool, moving_for_hold: bool
return auto_hold_drive_time, one_pedal_drive_time return auto_hold_drive_time, one_pedal_drive_time
def update_startup_acc_fault_suppression(car_fingerprint: str, system_power_mode: int,
previous_system_power_mode: int, timer: float,
acc_state: int, friction_brake_unavailable: bool) -> tuple[float, bool]:
if car_fingerprint != CAR.BUICK_LACROSSE:
return 0.0, False
if system_power_mode == 2 and previous_system_power_mode != 2:
timer = ACC_STARTUP_FAULT_GRACE_PERIOD_S
elif system_power_mode != 2:
timer = 0.0
if timer <= 0.0 or acc_state != AccState.FAULTED:
return 0.0, False
timer = max(timer - DT_CTRL, 0.0)
return timer, timer > 0.0 and not friction_brake_unavailable
class CarState(CarStateBase): class CarState(CarStateBase):
def __init__(self, CP, FPCP): def __init__(self, CP, FPCP):
super().__init__(CP, FPCP) super().__init__(CP, FPCP)
@@ -124,8 +105,6 @@ class CarState(CarStateBase):
self.lkas_previously_enabled = 0 self.lkas_previously_enabled = 0
self.lkas_enabled = 0 self.lkas_enabled = 0
self.pcm_acc_status = AccState.OFF self.pcm_acc_status = AccState.OFF
self.system_power_mode = 0
self.startup_acc_fault_suppression_timer = 0.0
self.stock_fcw_alert = 0 self.stock_fcw_alert = 0
self.car_gps_config = get_car_gps_config(CP) self.car_gps_config = get_car_gps_config(CP)
self.car_gps_supported = self.car_gps_config is not None self.car_gps_supported = self.car_gps_config is not None
@@ -371,18 +350,8 @@ class CarState(CarStateBase):
ret.cruiseState.available = pt_cp.vl["ECMEngineStatus"]["CruiseMainOn"] != 0 ret.cruiseState.available = pt_cp.vl["ECMEngineStatus"]["CruiseMainOn"] != 0
ret.espDisabled = pt_cp.vl["ESPStatus"]["TractionControlOn"] != 1 ret.espDisabled = pt_cp.vl["ESPStatus"]["TractionControlOn"] != 1
acc_state = pt_cp.vl["AcceleratorPedal2"]["CruiseState"] ret.accFaulted = (pt_cp.vl["AcceleratorPedal2"]["CruiseState"] == AccState.FAULTED or
friction_brake_unavailable = pt_cp.vl["EBCMFrictionBrakeStatus"]["FrictionBrakeUnavailable"] == 1 pt_cp.vl["EBCMFrictionBrakeStatus"]["FrictionBrakeUnavailable"] == 1)
self.startup_acc_fault_suppression_timer, suppress_startup_acc_fault = update_startup_acc_fault_suppression(
self.CP.carFingerprint,
int(pt_cp.vl["BCMGeneralPlatformStatus"]["SystemPowerMode"]),
self.system_power_mode,
self.startup_acc_fault_suppression_timer,
acc_state,
friction_brake_unavailable,
)
self.system_power_mode = int(pt_cp.vl["BCMGeneralPlatformStatus"]["SystemPowerMode"])
ret.accFaulted = (acc_state == AccState.FAULTED and not suppress_startup_acc_fault) or friction_brake_unavailable
ret.cruiseState.enabled = pt_cp.vl["AcceleratorPedal2"]["CruiseState"] != AccState.OFF ret.cruiseState.enabled = pt_cp.vl["AcceleratorPedal2"]["CruiseState"] != AccState.OFF
ret.cruiseState.standstill = pt_cp.vl["AcceleratorPedal2"]["CruiseState"] == AccState.STANDSTILL ret.cruiseState.standstill = pt_cp.vl["AcceleratorPedal2"]["CruiseState"] == AccState.STANDSTILL
+1 -11
View File
@@ -31,8 +31,6 @@ BOLT_CC_DIRECTION_MEMORY_S = 1.5
VOLT_CC_CARS = { VOLT_CC_CARS = {
CAR.CHEVROLET_VOLT_CC, CAR.CHEVROLET_VOLT_CC,
} }
VOLT_CC_TARGET_DEADBAND_MPH = 5.0
VOLT_CC_ACCEL_DEADBAND_MS2 = 0.15
def malibu_phase_map_for_button(button): def malibu_phase_map_for_button(button):
@@ -346,15 +344,7 @@ def _create_volt_cc_spam_command(CS, actuators, ms_convert):
speed_setpoint = int(round(CS.out.cruiseState.speed * ms_convert)) speed_setpoint = int(round(CS.out.cruiseState.speed * ms_convert))
ego_speed = CS.out.vEgo * ms_convert ego_speed = CS.out.vEgo * ms_convert
v_cruise_kph = float(getattr(CS.out, "vCruise", 0.0)) if accel == 0.0:
if 0.0 < v_cruise_kph < 255.0:
is_metric = ms_convert == CV.MS_TO_KPH
target_setpoint = v_cruise_kph if is_metric else v_cruise_kph * CV.KPH_TO_MPH
target_deadband = VOLT_CC_TARGET_DEADBAND_MPH * (CV.MPH_TO_KPH if is_metric else 1.0)
if abs(target_setpoint - speed_setpoint) <= target_deadband:
return CruiseButtons.INIT, float("inf")
if abs(accel) <= VOLT_CC_ACCEL_DEADBAND_MS2:
return CruiseButtons.INIT, float("inf") return CruiseButtons.INIT, float("inf")
if accel < 0.0: if accel < 0.0:
+10 -12
View File
@@ -15,7 +15,6 @@ from opendbc.car.gm.values import (
CC_ONLY_CAR, CC_ONLY_CAR,
CC_REGEN_PADDLE_CAR, CC_REGEN_PADDLE_CAR,
EV_CAR, EV_CAR,
GM_AUTO_HOLD_CARS,
SDGM_CAR, SDGM_CAR,
CarControllerParams, CarControllerParams,
CanBus, CanBus,
@@ -711,19 +710,18 @@ class CarInterface(CarInterfaceBase):
if remote_start_boots_comma: if remote_start_boots_comma:
ret.safetyConfigs[0].safetyParam |= GMSafetyFlags.FLAG_GM_REMOTE_START_BOOTS_COMMA.value ret.safetyConfigs[0].safetyParam |= GMSafetyFlags.FLAG_GM_REMOTE_START_BOOTS_COMMA.value
gm_stock_friction_brake_safety = ( volt_stock_friction_brake_safety = (
ret.openpilotLongitudinalControl and ret.openpilotLongitudinalControl and
( (gm_auto_hold or volt_one_pedal_mode) and
(gm_auto_hold and candidate in GM_AUTO_HOLD_CARS) or candidate in {
(volt_one_pedal_mode and candidate in { CAR.CHEVROLET_VOLT,
CAR.CHEVROLET_VOLT, CAR.CHEVROLET_VOLT_2019,
CAR.CHEVROLET_VOLT_2019, CAR.CHEVROLET_VOLT_ASCM,
CAR.CHEVROLET_VOLT_ASCM, CAR.CHEVROLET_VOLT_CAMERA,
CAR.CHEVROLET_VOLT_CAMERA, }
})
)
) )
if gm_stock_friction_brake_safety: if volt_stock_friction_brake_safety:
# Reuse the paddle-scheduler safety bit as a Volt stock friction-brake
# marker on non-pedal paths. Auto hold and one-pedal can run while OP # marker on non-pedal paths. Auto hold and one-pedal can run while OP
# longitudinal is configured but not currently active, so the bit must # longitudinal is configured but not currently active, so the bit must
# be present regardless of the current long-control mode. Do not expose # be present regardless of the current long-control mode. Do not expose
@@ -431,15 +431,6 @@ def test_volt_auto_hold_requires_toggle_supported_non_cc_only_volt_and_stock_saf
), ),
True, True,
) )
assert supports_volt_auto_hold(
SimpleNamespace(
carFingerprint=CAR.BUICK_LACROSSE,
openpilotLongitudinalControl=True,
networkLocation=CarParams.NetworkLocation.gateway,
safetyConfigs=stock_safety,
),
True,
)
assert not supports_volt_auto_hold( assert not supports_volt_auto_hold(
SimpleNamespace( SimpleNamespace(
carFingerprint=CAR.CHEVROLET_VOLT, carFingerprint=CAR.CHEVROLET_VOLT,
+1 -144
View File
@@ -8,12 +8,7 @@ from opendbc.can import CANPacker, CANParser
from opendbc.car import Bus, DT_CTRL, structs from opendbc.car import Bus, DT_CTRL, structs
from opendbc.car.car_helpers import interfaces from opendbc.car.car_helpers import interfaces
from opendbc.car.gm import gmcan from opendbc.car.gm import gmcan
from opendbc.car.gm.carstate import ( from opendbc.car.gm.carstate import CarState as GMCarState, get_hard_cruise_buttons, update_auto_hold_drive_timers
CarState as GMCarState,
get_hard_cruise_buttons,
update_auto_hold_drive_timers,
update_startup_acc_fault_suppression,
)
from opendbc.car.gm.carcontroller import ( from opendbc.car.gm.carcontroller import (
VisualAlert, VisualAlert,
get_acc_dashboard_always_one, get_acc_dashboard_always_one,
@@ -209,50 +204,6 @@ class TestBoltGps:
assert all(message in parsers[Bus.pt].vl for message in CHEVROLET_BOLT_GPS_MESSAGES) assert all(message in parsers[Bus.pt].vl for message in CHEVROLET_BOLT_GPS_MESSAGES)
class TestGMCarState:
def test_lacrosse_startup_acc_fault_is_suppressed(self):
timer, suppressed = update_startup_acc_fault_suppression(
CAR.BUICK_LACROSSE, 2, 0, 0.0, 3, False,
)
assert suppressed
assert timer == pytest.approx(5.0 - DT_CTRL)
timer, suppressed = update_startup_acc_fault_suppression(
CAR.BUICK_LACROSSE, 2, 2, timer, 0, False,
)
assert timer == 0.0
assert not suppressed
def test_lacrosse_persistent_acc_fault_is_reported_after_startup(self):
timer, suppressed = update_startup_acc_fault_suppression(
CAR.BUICK_LACROSSE, 2, 0, 0.0, 3, False,
)
for _ in range(int(5.0 / DT_CTRL)):
timer, suppressed = update_startup_acc_fault_suppression(
CAR.BUICK_LACROSSE, 2, 2, timer, 3, False,
)
assert timer == 0.0
assert not suppressed
def test_lacrosse_brake_unavailable_fault_is_never_suppressed(self):
_, suppressed = update_startup_acc_fault_suppression(
CAR.BUICK_LACROSSE, 2, 0, 0.0, 3, True,
)
assert not suppressed
def test_startup_acc_fault_suppression_is_scoped_to_lacrosse(self):
_, suppressed = update_startup_acc_fault_suppression(
CAR.BUICK_REGAL, 2, 0, 0.0, 3, False,
)
assert not suppressed
class TestGMInterface: class TestGMInterface:
def test_lacrosse_obd_and_ascm_integrations_remain_separate(self): def test_lacrosse_obd_and_ascm_integrations_remain_separate(self):
obd_params = interfaces[CAR.BUICK_LACROSSE].get_params( obd_params = interfaces[CAR.BUICK_LACROSSE].get_params(
@@ -570,43 +521,6 @@ class TestGMInterface:
assert car_params.openpilotLongitudinalControl assert car_params.openpilotLongitudinalControl
assert car_params.safetyConfigs[0].safetyParam & GMSafetyFlags.FLAG_GM_PANDA_PADDLE_SCHED.value assert car_params.safetyConfigs[0].safetyParam & GMSafetyFlags.FLAG_GM_PANDA_PADDLE_SCHED.value
def test_buick_lacrosse_auto_hold_sets_stock_hold_safety_bit_with_op_long_enabled(self):
params = Params()
try:
params.put_bool("GMAutoHold", True)
car_params = interfaces[CAR.BUICK_LACROSSE].get_params(
CAR.BUICK_LACROSSE,
_empty_fingerprint(),
[],
alpha_long=False,
is_release=False,
docs=False,
starpilot_toggles=_test_starpilot_toggles(),
)
finally:
params.remove("GMAutoHold")
assert car_params.openpilotLongitudinalControl
assert car_params.safetyConfigs[0].safetyParam & GMSafetyFlags.FLAG_GM_PANDA_PADDLE_SCHED.value
def test_buick_lacrosse_auto_hold_is_off_when_toggle_is_disabled(self):
params = Params()
try:
params.put_bool("GMAutoHold", False)
car_params = interfaces[CAR.BUICK_LACROSSE].get_params(
CAR.BUICK_LACROSSE,
_empty_fingerprint(),
[],
alpha_long=False,
is_release=False,
docs=False,
starpilot_toggles=_test_starpilot_toggles(),
)
finally:
params.remove("GMAutoHold")
assert not car_params.safetyConfigs[0].safetyParam & GMSafetyFlags.FLAG_GM_PANDA_PADDLE_SCHED.value
def test_volt_auto_hold_does_not_set_stock_hold_safety_bit_with_op_long_disabled(self): def test_volt_auto_hold_does_not_set_stock_hold_safety_bit_with_op_long_disabled(self):
CarInterface = interfaces[CAR.CHEVROLET_VOLT_ASCM] CarInterface = interfaces[CAR.CHEVROLET_VOLT_ASCM]
fingerprint = _empty_fingerprint() fingerprint = _empty_fingerprint()
@@ -933,63 +847,6 @@ class TestGMCarController:
assert len(msgs) == 1 assert len(msgs) == 1
def test_volt_cc_redneck_holds_when_stock_setpoint_is_within_target_deadband(self):
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CC][Bus.pt])
controller = SimpleNamespace(frame=int(2.0 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0)
cs = SimpleNamespace(
CP=SimpleNamespace(
carFingerprint=CAR.CHEVROLET_VOLT_CC,
flags=GMFlags.NO_CAMERA.value,
networkLocation=structs.CarParams.NetworkLocation.gateway,
minEnableSpeed=0.0,
),
buttons_counter=2,
out=SimpleNamespace(
vEgo=100.0 * CV.KPH_TO_MS,
cruiseState=SimpleNamespace(speed=99.0 * CV.KPH_TO_MS),
vCruise=100.0,
),
)
msgs = gmcan.create_gm_cc_spam_command(
packer, controller, cs, SimpleNamespace(accel=0.5), SimpleNamespace(is_metric=True),
)
assert msgs == []
assert controller.apply_speed == 99
def test_volt_cc_redneck_catches_up_when_target_exceeds_deadband(self):
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CC][Bus.pt])
controller = SimpleNamespace(frame=int(0.5 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0)
cs = SimpleNamespace(
CP=SimpleNamespace(
carFingerprint=CAR.CHEVROLET_VOLT_CC,
flags=GMFlags.NO_CAMERA.value,
networkLocation=structs.CarParams.NetworkLocation.gateway,
minEnableSpeed=0.0,
),
buttons_counter=2,
out=SimpleNamespace(
vEgo=90.0 * CV.KPH_TO_MS,
cruiseState=SimpleNamespace(speed=90.0 * CV.KPH_TO_MS),
vCruise=100.0,
),
)
msgs = gmcan.create_gm_cc_spam_command(
packer, controller, cs, SimpleNamespace(accel=0.5), SimpleNamespace(is_metric=True),
)
assert msgs == []
controller.frame = int(0.7 / DT_CTRL)
msgs = gmcan.create_gm_cc_spam_command(
packer, controller, cs, SimpleNamespace(accel=0.5), SimpleNamespace(is_metric=True),
)
assert len(msgs) == 1
assert controller.apply_speed == 91
def test_volt_cc_no_camera_redneck_spam_stays_on_powertrain_bus(self): def test_volt_cc_no_camera_redneck_spam_stays_on_powertrain_bus(self):
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CC][Bus.pt]) packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CC][Bus.pt])
controller = SimpleNamespace(frame=int(0.3 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0) controller = SimpleNamespace(frame=int(0.3 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0)
-8
View File
@@ -533,14 +533,6 @@ EV_CAR = {
CAR.CHEVROLET_MALIBU_HYBRID_CC, CAR.CHEVROLET_MALIBU_HYBRID_CC,
} }
GM_AUTO_HOLD_CARS = {
CAR.CHEVROLET_VOLT,
CAR.CHEVROLET_VOLT_2019,
CAR.CHEVROLET_VOLT_ASCM,
CAR.CHEVROLET_VOLT_CAMERA,
CAR.BUICK_LACROSSE,
}
# We're integrated at the camera with VOACC on these cars (instead of ASCM w/ OBD-II harness) # We're integrated at the camera with VOACC on these cars (instead of ASCM w/ OBD-II harness)
CAMERA_ACC_CAR = { CAMERA_ACC_CAR = {
CAR.CHEVROLET_BOLT_ACC_2022_2023, CAR.CHEVROLET_BOLT_ACC_2022_2023,
@@ -11,7 +11,7 @@ from opendbc.car.common.conversions import Conversions as CV
from opendbc.car.hyundai import hyundaicanfd, hyundaican from opendbc.car.hyundai import hyundaicanfd, hyundaican
from opendbc.car.hyundai.hyundaicanfd import CanBus from opendbc.car.hyundai.hyundaicanfd import CanBus
from opendbc.car.hyundai.values import HyundaiFlags, HyundaiSafetyFlags, HyundaiStarPilotFlags, Buttons, CarControllerParams, CAR, CANFD_ANGLE_LONGITUDINAL_CAR, \ from opendbc.car.hyundai.values import HyundaiFlags, HyundaiSafetyFlags, HyundaiStarPilotFlags, Buttons, CarControllerParams, CAR, CANFD_ANGLE_LONGITUDINAL_CAR, \
CANFD_RADAR_ECU_KEEPALIVE_CAR, CANFD_ALT_BUTTONS_RESUME_CAR, kia_ev6_gt_line_longitudinal_tuning, \ CANFD_RADAR_LIVE_LONGITUDINAL_CAR, CANFD_ALT_BUTTONS_RESUME_CAR, kia_ev6_gt_line_longitudinal_tuning, \
KIA_EV6_GT_LINE_LONG_TUNING_TESTING_GROUND_ID KIA_EV6_GT_LINE_LONG_TUNING_TESTING_GROUND_ID
from opendbc.car.interfaces import CarControllerBase from opendbc.car.interfaces import CarControllerBase
from opendbc.car.vehicle_model import VehicleModel from opendbc.car.vehicle_model import VehicleModel
@@ -860,11 +860,7 @@ class CarController(CarControllerBase):
lka_steering = self.CP.flags & HyundaiFlags.CANFD_LKA_STEERING lka_steering = self.CP.flags & HyundaiFlags.CANFD_LKA_STEERING
longitudinal_active = bool(self.long_active_ecu and getattr(CC, "longActive", False)) longitudinal_active = bool(self.long_active_ecu and getattr(CC, "longActive", False))
lfa_status_cars = ( lfa_status_cars = (CAR.HYUNDAI_IONIQ_6, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, CAR.KIA_EV6)
CAR.HYUNDAI_IONIQ_6,
CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN,
CAR.KIA_EV6,
)
lfa_longitudinal_active = self.CP.openpilotLongitudinalControl \ lfa_longitudinal_active = self.CP.openpilotLongitudinalControl \
if self.CP.carFingerprint in lfa_status_cars else longitudinal_active if self.CP.carFingerprint in lfa_status_cars else longitudinal_active
lka_steering_long = lka_steering and lfa_longitudinal_active lka_steering_long = lka_steering and lfa_longitudinal_active
@@ -896,7 +892,8 @@ class CarController(CarControllerBase):
if angle_lkas_alt: if angle_lkas_alt:
steering_msg_active = bool(steering_msg_active and drive_gear) steering_msg_active = bool(steering_msg_active and drive_gear)
angle_lkas_alt_standstill_handoff = bool(getattr(CS.out, "standstill", False) and not CC.latActive) angle_lkas_alt_standstill_handoff = bool(getattr(CS.out, "standstill", False) and not CC.latActive)
forward_stock_lkas = self.CP.carFingerprint in CANFD_ANGLE_LONGITUDINAL_CAR and angle_lkas_alt and ( forward_stock_lkas = (self.CP.carFingerprint in CANFD_ANGLE_LONGITUDINAL_CAR or
self.CP.carFingerprint == CAR.KIA_SPORTAGE_HEV_2026) and angle_lkas_alt and (
angle_lkas_alt_standstill_handoff or not (drive_gear and (CC.latActive or CC.enabled)) angle_lkas_alt_standstill_handoff or not (drive_gear and (CC.latActive or CC.enabled))
) )
preserve_stock_lfa_status = preserve_stock_canfd_lfa_status(self.CP.carFingerprint) preserve_stock_lfa_status = preserve_stock_canfd_lfa_status(self.CP.carFingerprint)
@@ -998,7 +995,7 @@ class CarController(CarControllerBase):
# The front radar treats ADAS_DRV's 0x100 broadcast as its host heartbeat # The front radar treats ADAS_DRV's 0x100 broadcast as its host heartbeat
# and stops publishing object tracks when it disappears. # and stops publishing object tracks when it disappears.
radar_heartbeat_step = 1 if ccnc_angle_long else 4 radar_heartbeat_step = 1 if ccnc_angle_long else 4
if self.CP.carFingerprint in CANFD_RADAR_ECU_KEEPALIVE_CAR and self.frame % radar_heartbeat_step == 0: if self.CP.carFingerprint in CANFD_RADAR_LIVE_LONGITUDINAL_CAR and self.frame % radar_heartbeat_step == 0:
can_sends.append(hyundaicanfd.create_accelerator_brake_alt_spoof(0, self.frame // radar_heartbeat_step, can_sends.append(hyundaicanfd.create_accelerator_brake_alt_spoof(0, self.frame // radar_heartbeat_step,
CS.out.brakePressed, CS.out.gasPressed, CS.out.brakePressed, CS.out.gasPressed,
self.CP.carFingerprint)) self.CP.carFingerprint))
@@ -8,7 +8,6 @@ from opendbc.car.hyundai.values import HyundaiFlags, CAR, CarControllerParams, \
CANFD_SECURITYACCESS_CAR, \ CANFD_SECURITYACCESS_CAR, \
CANFD_ANGLE_LONGITUDINAL_CAR, \ CANFD_ANGLE_LONGITUDINAL_CAR, \
CANFD_RADAR_LIVE_LONGITUDINAL_CAR, \ CANFD_RADAR_LIVE_LONGITUDINAL_CAR, \
CANFD_RADAR_ECU_KEEPALIVE_CAR, \
RADAR_LIVE_LONGITUDINAL_CAR, \ RADAR_LIVE_LONGITUDINAL_CAR, \
UNSUPPORTED_LONGITUDINAL_CAR, HyundaiSafetyFlags, \ UNSUPPORTED_LONGITUDINAL_CAR, HyundaiSafetyFlags, \
LEGACY_LONGITUDINAL_CAR, \ LEGACY_LONGITUDINAL_CAR, \
@@ -30,7 +29,7 @@ Ecu = structs.CarParams.Ecu
def get_communication_control_request(car_fingerprint): def get_communication_control_request(car_fingerprint):
if car_fingerprint in CANFD_RADAR_ECU_KEEPALIVE_CAR: if car_fingerprint in CANFD_RADAR_LIVE_LONGITUDINAL_CAR and car_fingerprint != CAR.KIA_EV6:
return bytes([uds.SERVICE_TYPE.COMMUNICATION_CONTROL, uds.CONTROL_TYPE.ENABLE_RX_DISABLE_TX, return bytes([uds.SERVICE_TYPE.COMMUNICATION_CONTROL, uds.CONTROL_TYPE.ENABLE_RX_DISABLE_TX,
uds.MESSAGE_TYPE.NORMAL]) uds.MESSAGE_TYPE.NORMAL])
@@ -19,9 +19,6 @@ MRR30_RADAR_START_ADDR = 0x210
MRR30_RADAR_MSG_COUNT = 16 MRR30_RADAR_MSG_COUNT = 16
MRR35_RADAR_START_ADDR = 0x3A5 MRR35_RADAR_START_ADDR = 0x3A5
MRR35_RADAR_MSG_COUNT = 32 MRR35_RADAR_MSG_COUNT = 32
GV70_RADAR_START_ADDR = 0x210
GV70_RADAR_MSG_COUNT = 16
GV70_RADAR_DBC = "hyundai_radar_210_21f_generated"
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -33,7 +30,6 @@ class RadarTrackConfig:
frequency: int = 50 frequency: int = 50
parser_msg_count: int | None = None parser_msg_count: int | None = None
expected_length: int | None = None expected_length: int | None = None
dbc_name: str | None = None
@property @property
def can_parser_msg_count(self) -> int: def can_parser_msg_count(self) -> int:
@@ -51,10 +47,6 @@ RADAR_TRACK_CONFIGS = {
def get_radar_track_config(car_fingerprint, flags: int = 0) -> RadarTrackConfig | None: def get_radar_track_config(car_fingerprint, flags: int = 0) -> RadarTrackConfig | None:
if car_fingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN:
return RadarTrackConfig(GV70_RADAR_START_ADDR, GV70_RADAR_MSG_COUNT, "gv70_210", bus=0,
frequency=20, expected_length=32, dbc_name=GV70_RADAR_DBC)
radar_dbc = DBC[car_fingerprint].get(Bus.radar) radar_dbc = DBC[car_fingerprint].get(Bus.radar)
if car_fingerprint == CAR.GENESIS_G90 and radar_dbc == HYUNDAI_MANDO_FRONT_RADAR_DBC: if car_fingerprint == CAR.GENESIS_G90 and radar_dbc == HYUNDAI_MANDO_FRONT_RADAR_DBC:
return RadarTrackConfig(RADAR_START_ADDR, G90_RADAR_MSG_COUNT, "mando", parser_msg_count=RADAR_MSG_COUNT) return RadarTrackConfig(RADAR_START_ADDR, G90_RADAR_MSG_COUNT, "mando", parser_msg_count=RADAR_MSG_COUNT)
@@ -73,10 +65,6 @@ def radar_tracks_available(radar_config: RadarTrackConfig | None, fingerprint) -
if radar_config is None: if radar_config is None:
return False return False
if radar_config.radar_type == "gv70_210":
return all(fingerprint[radar_config.bus].get(addr) == radar_config.expected_length
for addr in range(radar_config.start_addr, radar_config.start_addr + radar_config.msg_count))
msg_len = fingerprint[radar_config.bus].get(radar_config.start_addr) msg_len = fingerprint[radar_config.bus].get(radar_config.start_addr)
if msg_len is None: if msg_len is None:
return False return False
@@ -90,8 +78,7 @@ def get_radar_can_parser(CP, radar_config):
messages = [(f"RADAR_TRACK_{addr:x}", radar_config.frequency) messages = [(f"RADAR_TRACK_{addr:x}", radar_config.frequency)
for addr in range(radar_config.start_addr, radar_config.start_addr + radar_config.can_parser_msg_count)] for addr in range(radar_config.start_addr, radar_config.start_addr + radar_config.can_parser_msg_count)]
dbc_name = radar_config.dbc_name or DBC[CP.carFingerprint][Bus.radar] return CANParser(DBC[CP.carFingerprint][Bus.radar], messages, radar_config.bus)
return CANParser(dbc_name, messages, radar_config.bus)
class RadarInterface(RadarInterfaceBase): class RadarInterface(RadarInterfaceBase):
@@ -236,27 +223,6 @@ class RadarInterface(RadarInterfaceBase):
del self.pts[track_key] del self.pts[track_key]
continue continue
if radar_type == "gv70_210":
for i in ("1", "2"):
track_key = addr * 2 + int(i) - 1
valid = msg[f"{i}_STATE"] in (3, 4)
if valid:
pt = self.pts.get(track_key)
if pt is None:
pt = structs.RadarData.RadarPoint()
pt.trackId = self.track_id
self.track_id += 1
self.pts[track_key] = pt
pt.measured = True
pt.dRel = msg[f"{i}_LONG_DIST"]
pt.yRel = msg[f"{i}_LAT_DIST"]
pt.vRel = msg[f"{i}_REL_SPEED"]
pt.aRel = msg[f"{i}_REL_ACCEL"]
pt.yvRel = msg[f"{i}_REL_LAT_SPEED"]
elif track_key in self.pts:
del self.pts[track_key]
continue
if radar_type == "mrrevo14f": if radar_type == "mrrevo14f":
for i in ("1", "2"): for i in ("1", "2"):
track_key = addr * 2 + int(i) - 1 track_key = addr * 2 + int(i) - 1
@@ -4,7 +4,7 @@ from types import SimpleNamespace
import pytest import pytest
from opendbc.can import CANPacker, CANParser from opendbc.can import CANPacker, CANParser
from opendbc.car import Bus, ButtonType, gen_empty_fingerprint, structs from opendbc.car import Bus, ButtonType, gen_empty_fingerprint, structs, uds
from opendbc.car.structs import CarControl, CarParams from opendbc.car.structs import CarControl, CarParams
from opendbc.car.fw_versions import build_fw_dict, match_fw_to_car from opendbc.car.fw_versions import build_fw_dict, match_fw_to_car
from opendbc.car.hyundai.carcontroller import CarController, CANCEL_BUTTON_DELAY_FRAMES, Ioniq6LongitudinalTuningState, GenesisG90LongitudinalTuningState, \ from opendbc.car.hyundai.carcontroller import CarController, CANCEL_BUTTON_DELAY_FRAMES, Ioniq6LongitudinalTuningState, GenesisG90LongitudinalTuningState, \
@@ -28,13 +28,12 @@ from opendbc.car.hyundai.interface import CarInterface, KIA_EV9_ACCEL_MAX, get_c
from opendbc.car.hyundai import hyundaican, hyundaicanfd from opendbc.car.hyundai import hyundaican, hyundaicanfd
from opendbc.car.hyundai.hyundaicanfd import CanBus, hkg_can_fd_checksum from opendbc.car.hyundai.hyundaicanfd import CanBus, hkg_can_fd_checksum
from opendbc.car.hyundai.radar_interface import MRREVO14F_RADAR_START_ADDR, MRR30_RADAR_START_ADDR, MRR35_RADAR_START_ADDR, \ from opendbc.car.hyundai.radar_interface import MRREVO14F_RADAR_START_ADDR, MRR30_RADAR_START_ADDR, MRR35_RADAR_START_ADDR, \
RADAR_START_ADDR, RadarInterface, get_radar_track_config, radar_tracks_available RADAR_START_ADDR, get_radar_track_config
from opendbc.car.hyundai.values import CAMERA_SCC_CAR, CANFD_CAR, CAN_GEARS, CAR, CHECKSUM, DATE_FW_ECUS, DATELESS_FUZZY_CARS, \ from opendbc.car.hyundai.values import CAMERA_SCC_CAR, CANFD_CAR, CAN_GEARS, CAR, CHECKSUM, DATE_FW_ECUS, DATELESS_FUZZY_CARS, \
HYBRID_CAR, EV_CAR, FW_QUERY_CONFIG, LEGACY_SAFETY_MODE_CAR, CANFD_FUZZY_WHITELIST, \ HYBRID_CAR, EV_CAR, FW_QUERY_CONFIG, LEGACY_SAFETY_MODE_CAR, CANFD_FUZZY_WHITELIST, \
UNSUPPORTED_LONGITUDINAL_CAR, PLATFORM_CODE_ECUS, HYUNDAI_VERSION_REQUEST_LONG, \ UNSUPPORTED_LONGITUDINAL_CAR, PLATFORM_CODE_ECUS, HYUNDAI_VERSION_REQUEST_LONG, \
LEGACY_LONGITUDINAL_CAR, DBC, HyundaiFlags, get_platform_codes, HyundaiSafetyFlags, \ LEGACY_LONGITUDINAL_CAR, DBC, HyundaiFlags, get_platform_codes, HyundaiSafetyFlags, \
HyundaiStarPilotFlags, HyundaiStarPilotSafetyFlags, Buttons, CarControllerParams, \ HyundaiStarPilotFlags, HyundaiStarPilotSafetyFlags, Buttons, CarControllerParams, kia_ev6_gt_line_longitudinal_tuning
CANFD_RADAR_ECU_KEEPALIVE_CAR, CANFD_RADAR_LIVE_LONGITUDINAL_CAR, kia_ev6_gt_line_longitudinal_tuning
LongCtrlState = CarControl.Actuators.LongControlState LongCtrlState = CarControl.Actuators.LongControlState
from opendbc.car.hyundai.fingerprints import FW_VERSIONS from opendbc.car.hyundai.fingerprints import FW_VERSIONS
@@ -131,11 +130,10 @@ def get_test_toggles() -> SimpleNamespace:
class TestHyundaiFingerprint: class TestHyundaiFingerprint:
def test_ev6_uses_stock_hda2_communication_control_path(self): def test_ev6_uses_stock_hda2_communication_control_path(self):
stock_request = bytes([0x28, 0x83, 0x01]) stock_request = bytes([uds.SERVICE_TYPE.COMMUNICATION_CONTROL, 0x83, uds.MESSAGE_TYPE.NORMAL])
radar_keepalive_request = bytes([0x28, 0x01, 0x01]) radar_keepalive_request = bytes([uds.SERVICE_TYPE.COMMUNICATION_CONTROL,
uds.CONTROL_TYPE.ENABLE_RX_DISABLE_TX, uds.MESSAGE_TYPE.NORMAL])
assert CAR.KIA_EV6 in CANFD_RADAR_LIVE_LONGITUDINAL_CAR
assert CAR.KIA_EV6 not in CANFD_RADAR_ECU_KEEPALIVE_CAR
assert get_communication_control_request(CAR.KIA_EV6) == stock_request assert get_communication_control_request(CAR.KIA_EV6) == stock_request
assert get_communication_control_request(CAR.HYUNDAI_IONIQ_6) == radar_keepalive_request assert get_communication_control_request(CAR.HYUNDAI_IONIQ_6) == radar_keepalive_request
@@ -436,42 +434,6 @@ class TestHyundaiFingerprint:
assert CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CANFD_LKA_STEERING assert CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CANFD_LKA_STEERING
assert CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.EV_GAS assert CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.EV_GAS
gv70_radar_config = get_radar_track_config(CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN)
assert gv70_radar_config.radar_type == "gv70_210"
for addr in range(gv70_radar_config.start_addr, gv70_radar_config.start_addr + gv70_radar_config.msg_count):
gv70_fingerprint[gv70_radar_config.bus][addr] = gv70_radar_config.expected_length
assert radar_tracks_available(gv70_radar_config, gv70_fingerprint)
CP = CarInterface.get_params(CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, gv70_fingerprint, gv70_car_fw,
True, False, False, None)
assert not CP.radarUnavailable
radar = RadarInterface(CP)
packer = CANPacker(gv70_radar_config.dbc_name)
messages = []
for addr in range(gv70_radar_config.start_addr, gv70_radar_config.start_addr + gv70_radar_config.msg_count):
message = packer.make_can_msg(f"RADAR_TRACK_{addr:x}", 0, {
"1_STATE": 3,
"1_LONG_DIST": 25.0,
"1_LAT_DIST": 0.5,
"1_REL_SPEED": -2.0,
"1_REL_LAT_SPEED": 0.1,
"1_REL_ACCEL": -0.2,
})
data = bytearray(message[1])
checksum = hkg_can_fd_checksum(addr, None, data)
data[0] = checksum & 0xff
data[1] = (checksum >> 8) & 0xff
messages.append((message[0], bytes(data), message[2]))
radar_data = radar.update([(1, messages)])
assert radar_data is not None
assert len(radar_data.points) == 16
assert radar_data.points[0].dRel == pytest.approx(25.0)
other_config = get_radar_track_config(CAR.HYUNDAI_IONIQ_5)
assert other_config.radar_type == "mrr30"
assert other_config.dbc_name is None
for candidate in HYUNDAI_NON_SCC_CARS: for candidate in HYUNDAI_NON_SCC_CARS:
CP = CarInterface.get_params(candidate, gen_empty_fingerprint(), [], True, False, False, None) CP = CarInterface.get_params(candidate, gen_empty_fingerprint(), [], True, False, False, None)
assert bool(CP.flags & HyundaiFlags.NON_SCC) assert bool(CP.flags & HyundaiFlags.NON_SCC)
@@ -2606,6 +2568,8 @@ class TestHyundaiFingerprint:
CP.openpilotLongitudinalControl = True CP.openpilotLongitudinalControl = True
controller = CarController(DBC[CP.carFingerprint], CP) controller = CarController(DBC[CP.carFingerprint], CP)
controller.frame = 1
controller.long_active_ecu = False
cc = SimpleNamespace( cc = SimpleNamespace(
enabled=False, latActive=False, longActive=False, enabled=False, latActive=False, longActive=False,
actuators=SimpleNamespace(longControlState=LongCtrlState.off), actuators=SimpleNamespace(longControlState=LongCtrlState.off),
@@ -2613,15 +2577,12 @@ class TestHyundaiFingerprint:
) )
cs = SimpleNamespace( cs = SimpleNamespace(
stock_lfa_msg=None, stock_lkas_msg=None, stock_lfa_msg=None, stock_lkas_msg=None,
left_blindspot_from_radar=False, right_blindspot_from_radar=False,
out=SimpleNamespace(gearShifter=structs.CarState.GearShifter.park), out=SimpleNamespace(gearShifter=structs.CarState.GearShifter.park),
) )
controller.frame = 1 msgs = controller.create_canfd_msgs(0, False, 0.0, 0.0, 0.0, 0.0, False,
for controller.long_active_ecu in (False, True): cc.hudControl, cs, cc, get_test_toggles(), lka_icon=1, lfa_icon=1)
msgs = controller.create_canfd_msgs(0, False, 0.0, 0.0, 0.0, 0.0, False, assert any(addr == 0x12A for addr, _, _ in msgs)
cc.hudControl, cs, cc, get_test_toggles(), lka_icon=1, lfa_icon=1)
assert any(addr == 0x12A for addr, _, _ in msgs)
def test_gv70_electrified_longitudinal_uses_hda2_scc_contract(self): def test_gv70_electrified_longitudinal_uses_hda2_scc_contract(self):
CP = CarParams.new_message() CP = CarParams.new_message()
@@ -2755,7 +2716,7 @@ class TestHyundaiFingerprint:
assert len([msg for msg in msgs if msg[0] == 0x110]) == expected_lkas_msgs assert len([msg for msg in msgs if msg[0] == 0x110]) == expected_lkas_msgs
@pytest.mark.parametrize("standstill", [False, True]) @pytest.mark.parametrize("standstill", [False, True])
def test_sportage_angle_lkas_alt_publishes_inactive_status(self, standstill): def test_sportage_angle_lkas_alt_forwards_stock_status_when_inactive(self, standstill):
CP = CarParams.new_message() CP = CarParams.new_message()
CP.carFingerprint = CAR.KIA_SPORTAGE_HEV_2026 CP.carFingerprint = CAR.KIA_SPORTAGE_HEV_2026
CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.HYBRID | HyundaiFlags.CANFD_ANGLE_STEERING | CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.HYBRID | HyundaiFlags.CANFD_ANGLE_STEERING |
@@ -2763,7 +2724,6 @@ class TestHyundaiFingerprint:
CP.openpilotLongitudinalControl = False CP.openpilotLongitudinalControl = False
controller = CarController(DBC[CP.carFingerprint], CP) controller = CarController(DBC[CP.carFingerprint], CP)
can_bus = CanBus(CP)
cc = SimpleNamespace(enabled=False, latActive=False, cc = SimpleNamespace(enabled=False, latActive=False,
actuators=SimpleNamespace(longControlState=LongCtrlState.off), actuators=SimpleNamespace(longControlState=LongCtrlState.off),
leftBlinker=False, rightBlinker=False, hudControl=SimpleNamespace()) leftBlinker=False, rightBlinker=False, hudControl=SimpleNamespace())
@@ -2773,16 +2733,7 @@ class TestHyundaiFingerprint:
msgs = controller.create_canfd_msgs(0, False, 0.0, 0.0, 0.0, 0.0, False, cc.hudControl, cs, cc, msgs = controller.create_canfd_msgs(0, False, 0.0, 0.0, 0.0, 0.0, False, cc.hudControl, cs, cc,
get_test_toggles(), lka_icon=1, lfa_icon=1) get_test_toggles(), lka_icon=1, lfa_icon=1)
lkas_msgs = [msg for msg in msgs if msg[0] == 0x110] assert not [msg for msg in msgs if msg[0] in (0x110, 0x12A)]
assert len(lkas_msgs) == 1
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS_ALT", 0)], can_bus.ACAN)
parser.update([(1, lkas_msgs)])
assert parser.can_valid
assert parser.vl["LKAS_ALT"]["LKA_ICON"] == 1
assert parser.vl["LKAS_ALT"]["LKA_SysIndReq"] == 1
assert parser.vl["LKAS_ALT"]["LKA_RcgSta"] == 0
assert parser.vl["LKAS_ALT"]["LKAS_ANGLE_ACTIVE"] == 1
def test_ev9_inactive_angle_steering_does_not_suppress_stock_lfa(self): def test_ev9_inactive_angle_steering_does_not_suppress_stock_lfa(self):
CP = CarParams.new_message() CP = CarParams.new_message()
@@ -1218,9 +1218,7 @@ CANFD_ALT_BUTTONS_RESUME_CAR = {CAR.KIA_CARNIVAL_2025, CAR.KIA_CARNIVAL_HEV_4TH_
CANFD_CORNER_RADAR_BSM_CAR = {CAR.HYUNDAI_IONIQ_6, CAR.HYUNDAI_IONIQ_5_PE, CAR.KIA_EV9} CANFD_CORNER_RADAR_BSM_CAR = {CAR.HYUNDAI_IONIQ_6, CAR.HYUNDAI_IONIQ_5_PE, CAR.KIA_EV9}
CANFD_RADAR_LIVE_LONGITUDINAL_CAR = { CANFD_RADAR_LIVE_LONGITUDINAL_CAR = {
CAR.HYUNDAI_IONIQ_5, CAR.HYUNDAI_IONIQ_5_PE, CAR.HYUNDAI_IONIQ_6, CAR.KIA_EV6, CAR.KIA_EV9, CAR.GENESIS_GV60_EV_1ST_GEN, CAR.HYUNDAI_IONIQ_5, CAR.HYUNDAI_IONIQ_5_PE, CAR.HYUNDAI_IONIQ_6, CAR.KIA_EV6, CAR.KIA_EV9, CAR.GENESIS_GV60_EV_1ST_GEN,
CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN,
} }
CANFD_RADAR_ECU_KEEPALIVE_CAR = CANFD_RADAR_LIVE_LONGITUDINAL_CAR - {CAR.KIA_EV6}
RADAR_LIVE_LONGITUDINAL_CAR = CANFD_RADAR_LIVE_LONGITUDINAL_CAR | { RADAR_LIVE_LONGITUDINAL_CAR = CANFD_RADAR_LIVE_LONGITUDINAL_CAR | {
CAR.HYUNDAI_IONIQ, CAR.HYUNDAI_IONIQ,
CAR.HYUNDAI_KONA_EV_2022, CAR.HYUNDAI_KONA_EV_2022,
@@ -302,7 +302,7 @@ class CarController(CarControllerBase):
lkas_available = CC.latActive and (not mads_only or mads_only_ok) and \ lkas_available = CC.latActive and (not mads_only or mads_only_ok) and \
CS.out.gearShifter == structs.CarState.GearShifter.drive and not CS.out.standstill CS.out.gearShifter == structs.CarState.GearShifter.drive and not CS.out.standstill
manual_handoff = self._legacy_2025_manual_handoff(CS, CC.latActive) manual_handoff = self._legacy_2025_manual_handoff(CS, lkas_available)
lkas_active = lkas_available and not manual_handoff lkas_active = lkas_available and not manual_handoff
steer_target = self._legacy_2025_reclaim_target(CC.actuators.steeringAngleDeg) if lkas_active else CC.actuators.steeringAngleDeg steer_target = self._legacy_2025_reclaim_target(CC.actuators.steeringAngleDeg) if lkas_active else CC.actuators.steeringAngleDeg
@@ -325,7 +325,7 @@ class CarController(CarControllerBase):
lkas_available = CC.latActive and (not mads_only or mads_only_ok) and \ lkas_available = CC.latActive and (not mads_only or mads_only_ok) and \
CS.out.gearShifter == structs.CarState.GearShifter.drive and not CS.out.standstill CS.out.gearShifter == structs.CarState.GearShifter.drive and not CS.out.standstill
manual_handoff = self._angle_manual_handoff(CS, CC.latActive) manual_handoff = self._angle_manual_handoff(CS, lkas_available)
lkas_active = lkas_available and not manual_handoff lkas_active = lkas_available and not manual_handoff
if lkas_active and not self.angle_lkas_active: if lkas_active and not self.angle_lkas_active:
@@ -361,7 +361,7 @@ class CarController(CarControllerBase):
lkas_available = CC.latActive and (not mads_only or mads_only_ok) and \ lkas_available = CC.latActive and (not mads_only or mads_only_ok) and \
getattr(CS.out, "gearShifter", structs.CarState.GearShifter.drive) == structs.CarState.GearShifter.drive and \ getattr(CS.out, "gearShifter", structs.CarState.GearShifter.drive) == structs.CarState.GearShifter.drive and \
not getattr(CS.out, "standstill", False) not getattr(CS.out, "standstill", False)
manual_handoff = self._angle_manual_handoff(CS, CC.latActive) manual_handoff = self._angle_manual_handoff(CS, lkas_available)
lat_active = lkas_available and not self.driver_override and not manual_handoff lat_active = lkas_available and not self.driver_override and not manual_handoff
if lat_active and not self.angle_lkas_active: if lat_active and not self.angle_lkas_active:
self.apply_steer_last = CS.out.steeringAngleDeg self.apply_steer_last = CS.out.steeringAngleDeg
+3 -3
View File
@@ -85,14 +85,14 @@ class CarState(CarStateBase):
if self.CP.flags & SubaruFlags.LKAS_ANGLE: if self.CP.flags & SubaruFlags.LKAS_ANGLE:
ret.steeringAngleDeg = cp_angle.vl["Steering_2"]["Steering_Angle"] ret.steeringAngleDeg = cp_angle.vl["Steering_2"]["Steering_Angle"]
steering_counter = cp_angle.vl["Steering_2"]["COUNTER"] steering_updated = len(cp_angle.vl_all["Steering_2"]["Steering_Angle"]) > 0
else: else:
ret.steeringAngleDeg = cp.vl["Steering_Torque"]["Steering_Angle"] ret.steeringAngleDeg = cp.vl["Steering_Torque"]["Steering_Angle"]
steering_counter = cp.vl["Steering_Torque"].get("COUNTER", 0) steering_updated = len(cp.vl_all["Steering_Torque"]["Steering_Angle"]) > 0
if not (self.CP.flags & SubaruFlags.PREGLOBAL): if not (self.CP.flags & SubaruFlags.PREGLOBAL):
# ideally we get this from the car, but unclear if it exists. diagnostic software doesn't even have it # ideally we get this from the car, but unclear if it exists. diagnostic software doesn't even have it
ret.steeringRateDeg = self.angle_rate_calulator.update(ret.steeringAngleDeg, steering_counter) ret.steeringRateDeg = self.angle_rate_calulator.update(ret.steeringAngleDeg, steering_updated)
ret.steeringTorque = cp_angle.vl["Steering_Torque"]["Steer_Torque_Sensor"] ret.steeringTorque = cp_angle.vl["Steering_Torque"]["Steer_Torque_Sensor"]
ret.steeringTorqueEps = cp_angle.vl["Steering_Torque"]["Steer_Torque_Output"] ret.steeringTorqueEps = cp_angle.vl["Steering_Torque"]["Steer_Torque_Output"]
@@ -546,25 +546,6 @@ def test_ascent_2023_uses_gen2_angle_bus_layout():
assert controller.status_bus == CanBus.main assert controller.status_bus == CanBus.main
def test_ascent_steering_rate_retains_last_can_sample():
CP = CarInterface.get_non_essential_params(CAR.SUBARU_ASCENT_2023)
car_state = CarState(CP, None)
parsers = car_state.get_can_parsers(CP)
toggles = SimpleNamespace(subaru_sng=False)
parsers[Bus.pt].vl["Steering_2"]["Steering_Angle"] = 1.0
parsers[Bus.pt].vl["Steering_2"]["COUNTER"] = 1
car_state.update(parsers, toggles)
parsers[Bus.pt].vl["Steering_2"]["Steering_Angle"] = 2.0
parsers[Bus.pt].vl["Steering_2"]["COUNTER"] = 2
state, _ = car_state.update(parsers, toggles)
assert state.steeringRateDeg == pytest.approx(50.0)
state, _ = car_state.update(parsers, toggles)
assert state.steeringRateDeg == pytest.approx(50.0)
def test_other_angle_platforms_keep_existing_bus_layout(): def test_other_angle_platforms_keep_existing_bus_layout():
CP = CarInterface.get_non_essential_params(CAR.SUBARU_CROSSTREK_2025) CP = CarInterface.get_non_essential_params(CAR.SUBARU_CROSSTREK_2025)
parsers = CarState.get_can_parsers(CP) parsers = CarState.get_can_parsers(CP)
@@ -726,20 +707,11 @@ def test_ascent_angle_controller_blocks_parking_lot_aol_engagement():
CS.out.steeringRateDeg = 0.0 CS.out.steeringRateDeg = 0.0
msg = controller.lateral_angle(CC, CS) msg = controller.lateral_angle(CC, CS)
parser.update([(2, [msg])]) parser.update([(2, [msg])])
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
for i in range(8):
msg = controller.lateral_angle(CC, CS)
parser.update([(3 + i, [msg])])
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
msg = controller.lateral_angle(CC, CS)
parser.update([(11, [msg])])
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 1 assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 1
CS.out.gearShifter = structs.CarState.GearShifter.reverse CS.out.gearShifter = structs.CarState.GearShifter.reverse
msg = controller.lateral_angle(CC, CS) msg = controller.lateral_angle(CC, CS)
parser.update([(12, [msg])]) parser.update([(3, [msg])])
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0 assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(CS.out.steeringAngleDeg) assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(CS.out.steeringAngleDeg)
-1
View File
@@ -108,7 +108,6 @@ non_tested_cars = [
TOYOTA.TOYOTA_RAV4H, TOYOTA.TOYOTA_RAV4H,
# No recorded routes yet # No recorded routes yet
VOLVO.VOLVO_V40,
VOLVO.VOLVO_XC40_RECHARGE, VOLVO.VOLVO_XC40_RECHARGE,
VOLVO.VOLVO_S60_RECHARGE, VOLVO.VOLVO_S60_RECHARGE,
VOLVO.POLESTAR_2, VOLVO.POLESTAR_2,
@@ -145,7 +145,6 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"]
"PORSCHE_MACAN_MK1" = [2.0, 2.0, 0.2] "PORSCHE_MACAN_MK1" = [2.0, 2.0, 0.2]
"VOLVO_XC40_RECHARGE" = [1.5, 1.5, 0.1] "VOLVO_XC40_RECHARGE" = [1.5, 1.5, 0.1]
"VOLVO_S60_RECHARGE" = [1.5, 1.5, 0.1] "VOLVO_S60_RECHARGE" = [1.5, 1.5, 0.1]
"VOLVO_V40" = [1.5, 1.5, 0.1]
# Dashcam or fallback configured as ideal car # Dashcam or fallback configured as ideal car
"MOCK" = [10.0, 10, 0.0] "MOCK" = [10.0, 10, 0.0]
@@ -40,8 +40,6 @@ TOYOTA_NO_LEAD_CRUISE_SIGN_FLIP_MIN_SET_SPEED_ERROR = 0.35 # m/s
TOYOTA_RAV4_LAUNCH_PEDAL_BLEND_SPEED = 5.0 # m/s TOYOTA_RAV4_LAUNCH_PEDAL_BLEND_SPEED = 5.0 # m/s
TOYOTA_RAV4_LAUNCH_PEDAL_SCALE = 0.11 TOYOTA_RAV4_LAUNCH_PEDAL_SCALE = 0.11
TOYOTA_RAV4_LOW_SPEED_PEDAL_SCALE = 0.23 TOYOTA_RAV4_LOW_SPEED_PEDAL_SCALE = 0.23
TOYOTA_AUTO_HOLD_ACCEL = -1.0
TOYOTA_AUTO_HOLD_ACTIVATION_FRAMES = 100
# LKA limits # LKA limits
# EPS faults if you apply torque while the steering rate is above 100 deg/s for too long # EPS faults if you apply torque while the steering rate is above 100 deg/s for too long
@@ -311,24 +309,23 @@ class CarController(CarControllerBase):
self.last_standstill = CS.out.standstill self.last_standstill = CS.out.standstill
def update_auto_hold_state(self, CS: structs.CarState, cancel_requested: bool = False, def create_auto_brake_hold_messages(self, CS: structs.CarState, brake_hold_allowed_timer: int = 100):
activation_frames: int = TOYOTA_AUTO_HOLD_ACTIVATION_FRAMES): can_sends = []
brake_hold_allowed = (not cancel_requested and CS.out.standstill and CS.out.cruiseState.available and brake_hold_allowed = (CS.out.standstill and CS.out.cruiseState.available and
not CS.out.gasPressed and not CS.out.cruiseState.enabled and not CS.out.gasPressed and not CS.out.cruiseState.enabled and
CS.out.gearShifter not in (PARK, REVERSE)) CS.out.gearShifter not in (PARK, REVERSE))
if brake_hold_allowed and not self.brake_hold_active and CS.out.brakePressed: if brake_hold_allowed and not self.brake_hold_active and CS.out.brakePressed:
self._brake_hold_counter += 1 self._brake_hold_counter += 1
self.brake_hold_active = self._brake_hold_counter > activation_frames self.brake_hold_active = self._brake_hold_counter > brake_hold_allowed_timer
elif not brake_hold_allowed: elif not brake_hold_allowed:
self._brake_hold_counter = 0 self._brake_hold_counter = 0
self.brake_hold_active = False self.brake_hold_active = False
return self.brake_hold_active if self.frame % 2 == 0:
can_sends.append(toyotacan.create_brake_hold_command(self.packer, self.frame, CS.pre_collision_2, self.brake_hold_active))
def reset_auto_hold_state(self): return can_sends
self._brake_hold_counter = 0
self.brake_hold_active = False
def update(self, CC, CS, now_nanos, starpilot_toggles): def update(self, CC, CS, now_nanos, starpilot_toggles):
actuators = CC.actuators actuators = CC.actuators
@@ -426,9 +423,10 @@ class CarController(CarControllerBase):
self._update_standstill_request(CC, CS, actuators, starpilot_toggles) self._update_standstill_request(CC, CS, actuators, starpilot_toggles)
if supports_toyota_auto_hold(self.CP, getattr(starpilot_toggles, "toyota_auto_hold", False)): if supports_toyota_auto_hold(self.CP, getattr(starpilot_toggles, "toyota_auto_hold", False)):
self.update_auto_hold_state(CS, pcm_cancel_cmd) can_sends.extend(self.create_auto_brake_hold_messages(CS))
else: elif self.brake_hold_active:
self.reset_auto_hold_state() self._brake_hold_counter = 0
self.brake_hold_active = False
interceptor_gas_cmd = self._compute_interceptor_gas_cmd(CC, CS) interceptor_gas_cmd = self._compute_interceptor_gas_cmd(CC, CS)
@@ -536,11 +534,6 @@ class CarController(CarControllerBase):
pcm_accel_cmd = float(np.clip(pcm_accel_cmd, self.params.ACCEL_MIN, self.params.ACCEL_MAX)) pcm_accel_cmd = float(np.clip(pcm_accel_cmd, self.params.ACCEL_MIN, self.params.ACCEL_MAX))
if self.brake_hold_active:
pcm_accel_cmd = TOYOTA_AUTO_HOLD_ACCEL
self.permit_braking = True
self.standstill_req = True
main_accel_cmd = 0. if self.CP.flags & ToyotaFlags.SECOC.value else pcm_accel_cmd main_accel_cmd = 0. if self.CP.flags & ToyotaFlags.SECOC.value else pcm_accel_cmd
can_sends.append(toyotacan.create_accel_command(self.packer, main_accel_cmd, pcm_cancel_cmd, self.permit_braking, self.standstill_req, lead, can_sends.append(toyotacan.create_accel_command(self.packer, main_accel_cmd, pcm_cancel_cmd, self.permit_braking, self.standstill_req, lead,
CS.acc_type, fcw_alert, self.distance_button, CS.acc_type, fcw_alert, self.distance_button,
@@ -90,6 +90,8 @@ class CarState(CarStateBase):
self.has_can_filter = self.FPCP.flags & ToyotaStarPilotFlags.RADAR_CAN_FILTER.value self.has_can_filter = self.FPCP.flags & ToyotaStarPilotFlags.RADAR_CAN_FILTER.value
self.has_SDSU = self.FPCP.flags & ToyotaStarPilotFlags.SMART_DSU.value self.has_SDSU = self.FPCP.flags & ToyotaStarPilotFlags.SMART_DSU.value
self.has_ZSS = self.FPCP.flags & ToyotaStarPilotFlags.ZSS.value self.has_ZSS = self.FPCP.flags & ToyotaStarPilotFlags.ZSS.value
self.auto_brake_hold = bool(self.CP.flags & ToyotaFlags.AUTO_BRAKE_HOLD.value)
self.pre_collision_2 = {}
def update(self, can_parsers, starpilot_toggles) -> structs.CarState: def update(self, can_parsers, starpilot_toggles) -> structs.CarState:
cp = can_parsers[Bus.pt] cp = can_parsers[Bus.pt]
@@ -225,6 +227,9 @@ class CarState(CarStateBase):
if self.CP.carFingerprint != CAR.TOYOTA_PRIUS_V: if self.CP.carFingerprint != CAR.TOYOTA_PRIUS_V:
self.lkas_hud = copy.copy(cp_cam.vl["LKAS_HUD"]) self.lkas_hud = copy.copy(cp_cam.vl["LKAS_HUD"])
if self.auto_brake_hold:
self.pre_collision_2 = copy.copy(cp_cam.vl["PRE_COLLISION_2"])
if self.CP.carFingerprint not in UNSUPPORTED_DSU_CAR: if self.CP.carFingerprint not in UNSUPPORTED_DSU_CAR:
self.pcm_follow_distance = cp.vl["PCM_CRUISE_2"]["PCM_FOLLOW_DISTANCE"] self.pcm_follow_distance = cp.vl["PCM_CRUISE_2"]["PCM_FOLLOW_DISTANCE"]
@@ -309,6 +314,9 @@ class CarState(CarStateBase):
if CP.carFingerprint in DISTANCE_BUTTON_CAR: if CP.carFingerprint in DISTANCE_BUTTON_CAR:
pt_messages.append(("PCM_CRUISE_4", 1)) pt_messages.append(("PCM_CRUISE_4", 1))
if CP.flags & ToyotaFlags.AUTO_BRAKE_HOLD.value:
cam_messages.append(("PRE_COLLISION_2", 50))
return { return {
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, 0), Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, 0),
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], cam_messages, 2), Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], cam_messages, 2),
+2 -2
View File
@@ -164,8 +164,8 @@ class CarInterface(CarInterfaceBase):
ret.safetyConfigs[0].safetyParam |= ToyotaSafetyFlags.GAS_INTERCEPTOR.value ret.safetyConfigs[0].safetyParam |= ToyotaSafetyFlags.GAS_INTERCEPTOR.value
toyota_auto_hold = Params(return_defaults=True).get_bool("ToyotaAutoHold") toyota_auto_hold = Params(return_defaults=True).get_bool("ToyotaAutoHold")
if toyota_auto_hold and ret.openpilotLongitudinalControl and candidate in TOYOTA_AUTO_HOLD_CARS: if toyota_auto_hold and candidate in TOYOTA_AUTO_HOLD_CARS:
ret.alternativeExperience |= ALTERNATIVE_EXPERIENCE.TOYOTA_AUTO_HOLD ret.alternativeExperience |= ALTERNATIVE_EXPERIENCE.ALLOW_AEB
ret.flags |= ToyotaFlags.AUTO_BRAKE_HOLD.value ret.flags |= ToyotaFlags.AUTO_BRAKE_HOLD.value
if not ret.openpilotLongitudinalControl: if not ret.openpilotLongitudinalControl:
@@ -196,8 +196,7 @@ class TestToyotaInterfaces:
params.put_bool("ToyotaAutoHold", True) params.put_bool("ToyotaAutoHold", True)
car_params = CarInterface.get_params( car_params = CarInterface.get_params(
candidate, candidate,
{bus: ({0x2FF: 8} if candidate in (CAR.TOYOTA_RAV4, CAR.TOYOTA_RAV4H) and bus == 0 else {}) {bus: {} for bus in range(8)},
for bus in range(8)},
[], [],
alpha_long=False, alpha_long=False,
is_release=False, is_release=False,
@@ -208,13 +207,12 @@ class TestToyotaInterfaces:
params.remove("ToyotaAutoHold") params.remove("ToyotaAutoHold")
assert car_params.flags & ToyotaFlags.AUTO_BRAKE_HOLD.value assert car_params.flags & ToyotaFlags.AUTO_BRAKE_HOLD.value
assert car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.TOYOTA_AUTO_HOLD assert car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.ALLOW_AEB
assert not car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.ALLOW_AEB
can_parsers = CarState.get_can_parsers(car_params) can_parsers = CarState.get_can_parsers(car_params)
car_state = CarState(car_params, SimpleNamespace(flags=0)) car_state = CarState(car_params, SimpleNamespace(flags=0))
car_state.update(can_parsers, SimpleNamespace(cluster_offset=1.0)) car_state.update(can_parsers, SimpleNamespace(cluster_offset=1.0))
assert "PRE_COLLISION_2" not in can_parsers[Bus.cam].vl assert "PRE_COLLISION_2" in can_parsers[Bus.cam].vl
@pytest.mark.parametrize("candidate", [CAR.TOYOTA_CAMRY_TSS2, CAR.TOYOTA_RAV4, CAR.TOYOTA_RAV4H]) @pytest.mark.parametrize("candidate", [CAR.TOYOTA_CAMRY_TSS2, CAR.TOYOTA_RAV4, CAR.TOYOTA_RAV4H])
def test_auto_hold_is_disabled_by_default(self, candidate): def test_auto_hold_is_disabled_by_default(self, candidate):
@@ -231,7 +229,7 @@ class TestToyotaInterfaces:
) )
assert not car_params.flags & ToyotaFlags.AUTO_BRAKE_HOLD.value assert not car_params.flags & ToyotaFlags.AUTO_BRAKE_HOLD.value
assert not car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.TOYOTA_AUTO_HOLD assert not car_params.alternativeExperience & ALTERNATIVE_EXPERIENCE.ALLOW_AEB
def test_prius_openpilot_long_uses_hybrid_long_defaults(self): def test_prius_openpilot_long_uses_hybrid_long_defaults(self):
car_params = CarInterface.get_params( car_params = CarInterface.get_params(
@@ -746,8 +744,6 @@ class TestToyotaCarController:
controller.standstill_req = standstill_req controller.standstill_req = standstill_req
controller.last_standstill = last_standstill controller.last_standstill = last_standstill
controller.accel = 0.0 controller.accel = 0.0
controller.brake_hold_active = False
controller._brake_hold_counter = 0
return controller return controller
@staticmethod @staticmethod
@@ -810,6 +806,9 @@ class TestToyotaCarController:
def test_toyota_auto_hold_latches_after_brake_press_until_gas(self): def test_toyota_auto_hold_latches_after_brake_press_until_gas(self):
controller = self._make_controller() controller = self._make_controller()
controller.packer = CANPacker(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt]) controller.packer = CANPacker(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt])
controller.frame = 0
controller.brake_hold_active = False
controller._brake_hold_counter = 0
cs = SimpleNamespace( cs = SimpleNamespace(
out=SimpleNamespace( out=SimpleNamespace(
@@ -819,22 +818,28 @@ class TestToyotaCarController:
brakePressed=True, brakePressed=True,
gearShifter=structs.CarState.GearShifter.drive, gearShifter=structs.CarState.GearShifter.drive,
), ),
pre_collision_2={},
) )
controller.update_auto_hold_state(cs, activation_frames=0) controller.create_auto_brake_hold_messages(cs, brake_hold_allowed_timer=0)
assert controller.brake_hold_active assert controller.brake_hold_active
cs.out.brakePressed = False cs.out.brakePressed = False
controller.update_auto_hold_state(cs, activation_frames=0) controller.frame = 2
controller.create_auto_brake_hold_messages(cs, brake_hold_allowed_timer=0)
assert controller.brake_hold_active assert controller.brake_hold_active
cs.out.gasPressed = True cs.out.gasPressed = True
controller.update_auto_hold_state(cs, activation_frames=0) controller.frame = 4
controller.create_auto_brake_hold_messages(cs, brake_hold_allowed_timer=0)
assert not controller.brake_hold_active assert not controller.brake_hold_active
def test_toyota_auto_hold_does_not_trigger_without_brake_press(self): def test_toyota_auto_hold_does_not_trigger_without_brake_press(self):
controller = self._make_controller() controller = self._make_controller()
controller.packer = CANPacker(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt]) controller.packer = CANPacker(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt])
controller.frame = 0
controller.brake_hold_active = False
controller._brake_hold_counter = 0
cs = SimpleNamespace( cs = SimpleNamespace(
out=SimpleNamespace( out=SimpleNamespace(
standstill=True, standstill=True,
@@ -843,9 +848,10 @@ class TestToyotaCarController:
brakePressed=False, brakePressed=False,
gearShifter=structs.CarState.GearShifter.drive, gearShifter=structs.CarState.GearShifter.drive,
), ),
pre_collision_2={},
) )
controller.update_auto_hold_state(cs, activation_frames=0) controller.create_auto_brake_hold_messages(cs, brake_hold_allowed_timer=0)
assert not controller.brake_hold_active assert not controller.brake_hold_active
def test_prius_resume_request_releases_standstill_latch(self): def test_prius_resume_request_releases_standstill_latch(self):
@@ -985,9 +991,12 @@ class TestToyotaCarController:
assert parser.can_valid assert parser.can_valid
assert parser.vl["ACC_CONTROL"]["ALLOW_LONG_PRESS"] == 1 assert parser.vl["ACC_CONTROL"]["ALLOW_LONG_PRESS"] == 1
def test_auto_hold_uses_acc_control_brake_path(self): def test_auto_brake_hold_sends_modified_pre_collision_after_timer(self):
controller = self._make_controller() controller = self._make_controller()
controller.packer = CANPacker(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt]) controller.packer = CANPacker(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt])
controller.frame = 0
controller.brake_hold_active = False
controller._brake_hold_counter = 0
cs = SimpleNamespace( cs = SimpleNamespace(
out=SimpleNamespace( out=SimpleNamespace(
standstill=True, standstill=True,
@@ -996,19 +1005,16 @@ class TestToyotaCarController:
brakePressed=True, brakePressed=True,
gearShifter=structs.CarState.GearShifter.drive, gearShifter=structs.CarState.GearShifter.drive,
), ),
pre_collision_2={},
) )
controller.update_auto_hold_state(cs, activation_frames=0) can_sends = controller.create_auto_brake_hold_messages(cs, brake_hold_allowed_timer=0)
can_sends = [toyotacan.create_accel_command(
controller.packer, -1.0, False, True, True, False, 1, False, 0, False,
)]
parser = CANParser(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt], [("ACC_CONTROL", 0)], 0) parser = CANParser(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt], [("PRE_COLLISION_2", 0)], 0)
parser.update([(1, can_sends)]) parser.update([(1, can_sends)])
assert controller.brake_hold_active assert controller.brake_hold_active
assert parser.vl["ACC_CONTROL"]["ACCEL_CMD"] == -1.0 assert parser.vl["PRE_COLLISION_2"]["DSS1GDRV"] == -1.0
assert parser.vl["ACC_CONTROL"]["PERMIT_BRAKING"] == 1 assert parser.vl["PRE_COLLISION_2"]["PBRTRGR"] == 1
assert parser.vl["ACC_CONTROL"]["RELEASE_STANDSTILL"] == 0
def test_interceptor_stop_and_go_holds_small_launch_at_standstill(self): def test_interceptor_stop_and_go_holds_small_launch_at_standstill(self):
controller = self._make_controller() controller = self._make_controller()
@@ -89,6 +89,38 @@ def create_pcs_commands(packer, accel, active, mass):
return [msg1, msg2] return [msg1, msg2]
def create_brake_hold_command(packer, frame, pre_collision_2, brake_hold_active):
values = {s: pre_collision_2[s] for s in [
"DSS1GDRV",
"DS1STAT2",
"DS1STBK2",
"PCSWAR",
"PCSALM",
"PCSOPR",
"PCSABK",
"PBATRGR",
"PPTRGR",
"IBTRGR",
"CLEXTRGR",
"IRLT_REQ",
"BRKHLD",
"AVSTRGR",
"VGRSTRGR",
"PREFILL",
"PBRTRGR",
"PCSDIS",
"PBPREPMP",
] if s in pre_collision_2}
if brake_hold_active:
values = {
"DSS1GDRV": 0x3FF,
"PBRTRGR": frame % 730 < 727,
}
return packer.make_can_msg("PRE_COLLISION_2", 0, values)
def create_acc_cancel_command(packer): def create_acc_cancel_command(packer):
values = { values = {
"GAS_RELEASED": 0, "GAS_RELEASED": 0,
@@ -1,5 +1,3 @@
from collections import deque
import numpy as np import numpy as np
from opendbc.can.packer import CANPacker from opendbc.can.packer import CANPacker
@@ -7,20 +5,16 @@ from opendbc.car import Bus
from opendbc.car.interfaces import CarControllerBase from opendbc.car.interfaces import CarControllerBase
from opendbc.car.lateral import apply_std_steer_angle_limits from opendbc.car.lateral import apply_std_steer_angle_limits
from opendbc.car.volvo.helpers import LCA3CounterSync from opendbc.car.volvo.helpers import LCA3CounterSync
from opendbc.car.volvo.volvocan import (create_c1_cancel, create_c1_pscm_message, create_c1_steering_control, create_lca_message, from opendbc.car.volvo.volvocan import (create_lca_message, create_pscm_message, create_lca_3_message, create_lca_2_message, create_lca_4_message,
create_pscm_message, create_lca_3_message, create_lca_2_message, create_lca_4_message, create_lca_5_message, create_lca_5_message, create_lca_6_message, create_lca_7_message, create_pscm_related_message)
create_lca_6_message, create_lca_7_message, create_pscm_related_message) from opendbc.car.volvo.values import CarControllerParams
from opendbc.car.volvo.values import CAR, CarControllerParams, VolvoC1PlatformConfig
class CarController(CarControllerBase): class CarController(CarControllerBase):
def __init__(self, dbc_names, CP): def __init__(self, dbc_names, CP):
super().__init__(dbc_names, CP) super().__init__(dbc_names, CP)
self.is_c1 = isinstance(CAR(CP.carFingerprint).config, VolvoC1PlatformConfig) self.packer = CANPacker(dbc_names[Bus.party])
self.packer = CANPacker(dbc_names[Bus.pt] if self.is_c1 else dbc_names[Bus.party])
self.apply_angle_last = 0.0 # Track last applied steering angle self.apply_angle_last = 0.0 # Track last applied steering angle
self.c1_torque_samples = deque(maxlen=CarControllerParams.C1_N_ZERO_TORQUE)
self.c1_recovery_until = -1
self.gear_acc = 60 self.gear_acc = 60
self.lca_4_acc = 0 # Bresenham accumulator for 29 Hz self.lca_4_acc = 0 # Bresenham accumulator for 29 Hz
@@ -68,9 +62,6 @@ class CarController(CarControllerBase):
self.lca_auth_drv_mag_filt = 0.0 self.lca_auth_drv_mag_filt = 0.0
def update(self, CC, CS, now_nanos, starpilot_toggles): def update(self, CC, CS, now_nanos, starpilot_toggles):
if self.is_c1:
return self._update_c1(CC, CS)
can_sends = [] can_sends = []
actuators = CC.actuators actuators = CC.actuators
@@ -294,50 +285,3 @@ class CarController(CarControllerBase):
self.frame += 1 self.frame += 1
self.last_lat_active = CC.latActive self.last_lat_active = CC.latActive
return new_actuators, can_sends return new_actuators, can_sends
def _update_c1(self, CC, CS):
can_sends = []
actuators = CC.actuators
if self.frame % 2 == 0: # stock FSM1 and PSCM1 messages are 50 Hz
requested_active = CC.latActive and CS.out.vEgo > self.CP.minSteerSpeed
recovering = requested_active and self.frame < self.c1_recovery_until
if not requested_active:
self.c1_torque_samples.clear()
self.c1_recovery_until = -1
elif recovering:
self.c1_torque_samples.clear()
else:
if self.c1_recovery_until >= 0:
self.c1_recovery_until = -1
self.c1_torque_samples.clear()
self.c1_torque_samples.append(CS.c1_lka_torque)
if (len(self.c1_torque_samples) == CarControllerParams.C1_N_ZERO_TORQUE and
all(torque == 0 for torque in self.c1_torque_samples)):
self.c1_recovery_until = self.frame + 100
self.c1_torque_samples.clear()
recovering = True
lat_active = requested_active and not recovering
desired_angle = float(np.clip(
actuators.steeringAngleDeg,
CS.out.steeringAngleDeg - CarControllerParams.C1_ANGLE_ERROR,
CS.out.steeringAngleDeg + CarControllerParams.C1_ANGLE_ERROR,
))
apply_angle = apply_std_steer_angle_limits(
desired_angle, self.apply_angle_last, CS.out.vEgoRaw,
CS.out.steeringAngleDeg, lat_active, CarControllerParams.C1_ANGLE_LIMITS,
)
can_sends.append(create_c1_pscm_message(self.packer, CS.c1_msg_pscm))
can_sends.append(create_c1_steering_control(self.packer, apply_angle, lat_active))
self.apply_angle_last = apply_angle
if CC.cruiseControl.cancel and self.frame % 10 == 0:
can_sends.append(create_c1_cancel(self.packer))
new_actuators = actuators.as_builder()
new_actuators.steeringAngleDeg = self.apply_angle_last
self.frame += 1
return new_actuators, can_sends
+2 -93
View File
@@ -1,9 +1,8 @@
from cereal import custom from cereal import custom
from opendbc.car import Bus, ButtonType, create_button_events, structs from opendbc.car import structs, Bus
from opendbc.can.parser import CANParser from opendbc.can.parser import CANParser
from opendbc.car.common.conversions import Conversions as CV from opendbc.car.volvo.values import DBC, VolvoSPAPlatformConfig, CAR
from opendbc.car.interfaces import CarStateBase from opendbc.car.interfaces import CarStateBase
from opendbc.car.volvo.values import CAR, DBC, VolvoC1PlatformConfig, VolvoSPAPlatformConfig
GearShifter = structs.CarState.GearShifter GearShifter = structs.CarState.GearShifter
TransmissionType = structs.CarParams.TransmissionType TransmissionType = structs.CarParams.TransmissionType
@@ -17,7 +16,6 @@ STEERING_PRESSED_THRESHOLD = 2
class CarState(CarStateBase): class CarState(CarStateBase):
def __init__(self, CP, FPCP): def __init__(self, CP, FPCP):
super().__init__(CP, FPCP) super().__init__(CP, FPCP)
self.is_c1 = isinstance(CAR(CP.carFingerprint).config, VolvoC1PlatformConfig)
self.is_spa = isinstance(CAR(CP.carFingerprint).config, VolvoSPAPlatformConfig) self.is_spa = isinstance(CAR(CP.carFingerprint).config, VolvoSPAPlatformConfig)
self.gas_pressed_prev = False self.gas_pressed_prev = False
self.dispatch_lca_2_msg = False self.dispatch_lca_2_msg = False
@@ -36,22 +34,8 @@ class CarState(CarStateBase):
self.msg_lca_4 = {} self.msg_lca_4 = {}
self.msg_lca_6 = {} self.msg_lca_6 = {}
self.msg_lca_7 = {} self.msg_lca_7 = {}
self.c1_msg_pscm = {}
self.c1_lka_torque = 0
self.c1_button_states = {
"ACCOnOffBtn": False,
"ACCStopBtn": False,
"ACCSetBtn": False,
"ACCResumeBtn": False,
"ACCMinusBtn": False,
"TimeGapIncreaseBtn": False,
"TimeGapDecreaseBtn": False,
}
def update(self, can_parsers, starpilot_toggles) -> structs.CarState: def update(self, can_parsers, starpilot_toggles) -> structs.CarState:
if self.is_c1:
return self._update_c1(can_parsers)
cp_main = can_parsers[Bus.main] cp_main = can_parsers[Bus.main]
cp_pt = can_parsers[Bus.pt] cp_pt = can_parsers[Bus.pt]
cp_party = can_parsers[Bus.party] cp_party = can_parsers[Bus.party]
@@ -153,83 +137,8 @@ class CarState(CarStateBase):
fp_ret = custom.StarPilotCarState.new_message() fp_ret = custom.StarPilotCarState.new_message()
return ret, fp_ret return ret, fp_ret
def _update_c1(self, can_parsers):
cp = can_parsers[Bus.pt]
cp_cam = can_parsers[Bus.cam]
ret = structs.CarState()
ret.vEgoRaw = cp.vl["VehicleSpeed1"]["VehicleSpeed"] * CV.KPH_TO_MS
ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
ret.standstill = ret.vEgoRaw < 0.1
ret.steeringAngleDeg = cp.vl["PSCM1"]["SteeringAngleServo"]
ret.steeringTorque = cp.vl["PSCM1"]["LKATorque"]
ret.steeringPressed = False
ret.gasPressed = cp.vl["PedalandBrake"]["AccPedal"] > 5.0
ret.brakePressed = bool(cp.vl["PedalandBrake"]["BrakePedalActive2"] or
cp.vl["PedalandBrake"]["BrakePedalActive"])
ret.gearShifter = {
0: GearShifter.park,
1: GearShifter.reverse,
2: GearShifter.neutral,
3: GearShifter.drive,
}.get(int(cp.vl["TCM0"]["GearShifter"]), GearShifter.unknown)
ret.cruiseState.available = bool(cp_cam.vl["FSM0"]["ACCStatusOnOff"])
ret.cruiseState.enabled = bool(cp_cam.vl["FSM0"]["ACCStatusActive"])
ret.cruiseState.speed = cp.vl["ACC"]["SpeedTargetACC"] * CV.KPH_TO_MS
ret.cruiseState.nonAdaptive = False
ret.cruiseState.standstill = ret.standstill
turn_signal = int(cp.vl["MiscCarInfo"]["TurnSignal"])
ret.leftBlinker, ret.rightBlinker = self.update_blinker_from_stalk(
50, turn_signal == 1, turn_signal == 3)
ret.doorOpen = False
ret.seatbeltUnlatched = False
button_types = {
"ACCOnOffBtn": ButtonType.mainCruise,
"ACCStopBtn": ButtonType.cancel,
"ACCSetBtn": ButtonType.setCruise,
"ACCResumeBtn": ButtonType.resumeCruise,
"ACCMinusBtn": ButtonType.decelCruise,
"TimeGapIncreaseBtn": ButtonType.gapAdjustCruise,
"TimeGapDecreaseBtn": ButtonType.gapAdjustCruise,
}
button_events = []
for signal, button_type in button_types.items():
pressed = bool(cp.vl["CCButtons"][signal])
button_events.extend(create_button_events(pressed, self.c1_button_states[signal], {True: button_type}))
self.c1_button_states[signal] = pressed
ret.buttonEvents = button_events
self.c1_msg_pscm = cp.vl["PSCM1"]
self.c1_lka_torque = int(cp.vl["PSCM1"]["LKATorque"])
fp_ret = custom.StarPilotCarState.new_message()
return ret, fp_ret
@staticmethod @staticmethod
def get_can_parsers(CP): def get_can_parsers(CP):
if isinstance(CAR(CP.carFingerprint).config, VolvoC1PlatformConfig):
return {
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], [
("VehicleSpeed1", 50),
("CCButtons", 100),
("PSCM1", 50),
("PedalandBrake", 100),
("TCM0", 10),
("ACC", 17),
("MiscCarInfo", 25),
], 0),
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.cam], [
("FSM0", 100),
("FSM1", 50),
], 2),
}
return { return {
Bus.main: CANParser(DBC[CP.carFingerprint][Bus.main], [], 0), Bus.main: CANParser(DBC[CP.carFingerprint][Bus.main], [], 0),
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], 1), Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], 1),
@@ -3,14 +3,6 @@
from opendbc.car.volvo.values import CAR from opendbc.car.volvo.values import CAR
FINGERPRINTS = { FINGERPRINTS = {
CAR.VOLVO_V40: [
# V40 2017
{8: 8, 16: 8, 48: 8, 64: 8, 85: 8, 101: 8, 112: 8, 114: 8, 117: 8, 128: 8, 176: 8, 192: 8, 208: 8, 224: 8, 240: 8, 245: 8, 256: 8, 272: 8, 288: 8, 291: 8, 293: 8, 304: 8, 325: 8, 336: 8, 352: 8, 424: 8, 432: 8, 437: 8, 464: 8, 472: 8, 480: 8, 528: 8, 608: 8, 624: 8, 640: 8, 648: 8, 652: 8, 656: 8, 657: 8, 681: 8, 693: 8, 704: 8, 707: 8, 709: 8, 816: 8, 832: 8, 848: 8, 853: 8, 864: 8, 880: 8, 912: 8, 928: 8, 943: 8, 944: 8, 968: 8, 970: 8, 976: 8, 992: 8, 997: 8, 1024: 8, 1029: 8, 1061: 8, 1072: 8, 1409: 8},
# V40 2015
{8: 8, 16: 8, 64: 8, 85: 8, 101: 8, 112: 8, 114: 8, 117: 8, 128: 8, 176: 8, 192: 8, 224: 8, 240: 8, 245: 8, 256: 8, 272: 8, 288: 8, 291: 8, 293: 8, 304: 8, 325: 8, 336: 8, 424: 8, 432: 8, 437: 8, 464: 8, 472: 8, 480: 8, 528: 8, 608: 8, 648: 8, 652: 8, 656: 8, 657: 8, 681: 8, 693: 8, 704: 8, 707: 8, 709: 8, 816: 8, 832: 8, 864: 8, 880: 8, 912: 8, 928: 8, 943: 8, 944: 8, 968: 8, 970: 8, 976: 8, 992: 8, 997: 8, 1024: 8, 1029: 8, 1061: 8, 1072: 8, 1409: 8},
# V40 2014
{8: 8, 16: 8, 64: 8, 85: 8, 101: 8, 112: 8, 114: 8, 117: 8, 128: 8, 176: 8, 192: 8, 224: 8, 240: 8, 245: 8, 256: 8, 272: 8, 288: 8, 291: 8, 293: 8, 304: 8, 325: 8, 336: 8, 424: 8, 432: 8, 437: 8, 464: 8, 472: 8, 480: 8, 528: 8, 608: 8, 648: 8, 652: 8, 657: 8, 681: 8, 693: 8, 704: 8, 707: 8, 709: 8, 816: 8, 864: 8, 880: 8, 912: 8, 928: 8, 943: 8, 944: 8, 968: 8, 970: 8, 976: 8, 992: 8, 997: 8, 1024: 8, 1029: 8, 1072: 8, 1409: 8},
],
CAR.VOLVO_XC40_RECHARGE: [{ CAR.VOLVO_XC40_RECHARGE: [{
21: 8, 22: 8, 23: 8, 26: 8, 69: 8, 85: 8, 87: 8, 88: 8, 96: 8, 103: 8, 104: 8, 105: 8, 128: 8, 144: 8, 146: 8, 147: 8, 151: 8, 304: 8, 336: 8, 339: 8, 341: 8, 394: 8, 395: 8, 640: 8, 656: 8, 666: 8, 773: 8, 778: 8, 1072: 8, 1120: 8, 1302: 8, 1336: 8, 1407: 8, 1422: 8, 1423: 8, 1424: 8, 1425: 8, 1426: 8 21: 8, 22: 8, 23: 8, 26: 8, 69: 8, 85: 8, 87: 8, 88: 8, 96: 8, 103: 8, 104: 8, 105: 8, 128: 8, 144: 8, 146: 8, 147: 8, 151: 8, 304: 8, 336: 8, 339: 8, 341: 8, 394: 8, 395: 8, 640: 8, 656: 8, 666: 8, 773: 8, 778: 8, 1072: 8, 1120: 8, 1302: 8, 1336: 8, 1407: 8, 1422: 8, 1423: 8, 1424: 8, 1425: 8, 1426: 8
}], }],
+6 -11
View File
@@ -2,10 +2,11 @@ from opendbc.car import structs, get_safety_config
from opendbc.car.interfaces import CarInterfaceBase from opendbc.car.interfaces import CarInterfaceBase
from opendbc.car.volvo.carcontroller import CarController from opendbc.car.volvo.carcontroller import CarController
from opendbc.car.volvo.carstate import CarState from opendbc.car.volvo.carstate import CarState
from opendbc.car.volvo.values import CAR, VolvoC1PlatformConfig, VolvoSafetyFlags, VolvoSPAPlatformConfig from opendbc.car.volvo.values import VolvoSPAPlatformConfig, CAR
TransmissionType = structs.CarParams.TransmissionType TransmissionType = structs.CarParams.TransmissionType
VOLVO_FLAG_SPA = 1
SAFETY_VOLVO = structs.CarParams.SafetyModel.volvo SAFETY_VOLVO = structs.CarParams.SafetyModel.volvo
@@ -17,20 +18,17 @@ class CarInterface(CarInterfaceBase):
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams: def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams:
ret.brand = 'volvo' ret.brand = 'volvo'
platform = CAR(candidate).config
safety_param = 0 safety_param = 0
if isinstance(platform, VolvoSPAPlatformConfig): if isinstance(CAR(candidate).config, VolvoSPAPlatformConfig):
safety_param = VolvoSafetyFlags.SPA.value safety_param = VOLVO_FLAG_SPA
elif isinstance(platform, VolvoC1PlatformConfig):
safety_param = VolvoSafetyFlags.C1.value
ret.safetyConfigs = [get_safety_config(SAFETY_VOLVO, safety_param)] ret.safetyConfigs = [get_safety_config(SAFETY_VOLVO, safety_param)]
#ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.noOutput)] #ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.noOutput)]
ret.dashcamOnly = False ret.dashcamOnly = False
ret.steerActuatorDelay = 0.2 if isinstance(platform, VolvoC1PlatformConfig) else 0.3 ret.steerActuatorDelay = 0.3
ret.steerLimitTimer = 0.1 ret.steerLimitTimer = 0.1
ret.steerAtStandstill = not isinstance(platform, VolvoC1PlatformConfig) ret.steerAtStandstill = True
# Use angle-based steering control for Volvo CMA platform # Use angle-based steering control for Volvo CMA platform
ret.steerControlType = structs.CarParams.SteerControlType.angle ret.steerControlType = structs.CarParams.SteerControlType.angle
@@ -41,7 +39,4 @@ class CarInterface(CarInterfaceBase):
ret.pcmCruise = True ret.pcmCruise = True
if isinstance(platform, VolvoC1PlatformConfig):
ret.transmissionType = TransmissionType.automatic
return ret return ret
@@ -1,57 +0,0 @@
import pytest
from cereal import custom
from opendbc.can.packer import CANPacker
from opendbc.car import Bus, ButtonType, CanData, structs
from opendbc.car.volvo.carstate import CarState
from opendbc.car.volvo.interface import CarInterface
from opendbc.car.volvo.values import CAR, DBC
def _can_data(msg):
address, data, bus = msg
return CanData(address, data, bus)
def test_c1_carstate_decodes_vehicle_and_cruise_signals():
cp = CarInterface.get_non_essential_params(CAR.VOLVO_V40)
cs = CarState(cp, custom.StarPilotCarParams.new_message())
parsers = CarState.get_can_parsers(cp)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
messages = [
packer.make_can_msg("VehicleSpeed1", 0, {"VehicleSpeed": 72}),
packer.make_can_msg("CCButtons", 0, {"ACCStopBtn": 1, "ACCSetBtn": 1}),
packer.make_can_msg("PSCM1", 0, {"SteeringAngleServo": -12.5, "LKATorque": 7}),
packer.make_can_msg("PedalandBrake", 0, {"AccPedal": 6, "BrakePedalActive2": 1}),
packer.make_can_msg("TCM0", 0, {"GearShifter": 3}),
packer.make_can_msg("ACC", 0, {"SpeedTargetACC": 100}),
packer.make_can_msg("MiscCarInfo", 0, {"TurnSignal": 1}),
packer.make_can_msg("FSM0", 2, {"ACCStatusOnOff": 1, "ACCStatusActive": 1}),
packer.make_can_msg("FSM1", 2, {}),
]
packets = [(1_000_000, [_can_data(msg) for msg in messages])]
for parser in parsers.values():
parser.update(packets)
ret, _ = cs.update(parsers, None)
assert ret.vEgoRaw == pytest.approx(20.0)
assert ret.steeringAngleDeg == pytest.approx(-12.5, abs=0.05)
assert ret.steeringTorque == 7
assert ret.gasPressed and ret.brakePressed
assert ret.gearShifter == structs.CarState.GearShifter.drive
assert ret.cruiseState.available and ret.cruiseState.enabled
assert ret.cruiseState.speed == pytest.approx(100 / 3.6)
assert ret.leftBlinker and not ret.rightBlinker
assert len(ret.buttonEvents) == 2
assert any(event.type == ButtonType.cancel and event.pressed for event in ret.buttonEvents)
assert any(event.type == ButtonType.setCruise and event.pressed for event in ret.buttonEvents)
release = packer.make_can_msg("CCButtons", 0, {})
packets = [(2_000_000, [_can_data(release)])]
for parser in parsers.values():
parser.update(packets)
ret, _ = cs.update(parsers, None)
assert len(ret.buttonEvents) == 2
assert any(event.type == ButtonType.cancel and not event.pressed for event in ret.buttonEvents)
assert any(event.type == ButtonType.setCruise and not event.pressed for event in ret.buttonEvents)
@@ -4,8 +4,7 @@ from types import SimpleNamespace
from opendbc.car.volvo.carcontroller import CarController from opendbc.car.volvo.carcontroller import CarController
from opendbc.car.volvo.helpers import checksum_lca_5_message from opendbc.car.volvo.helpers import checksum_lca_5_message
from opendbc.car.volvo.interface import CarInterface from opendbc.car.volvo.interface import CarInterface
from opendbc.car.volvo.values import CAR, DBC from opendbc.car.volvo.values import DBC
from opendbc.car.volvo.volvocan import create_c1_checksum
def _zero_message(): def _zero_message():
@@ -68,70 +67,3 @@ def test_controller_relays_stock_lca5_angle_when_inactive():
if raw & (1 << 14): if raw & (1 << 14):
raw -= 1 << 15 raw -= 1 << 15
assert abs(raw * 0.05596 - 12.0) < 0.1 assert abs(raw * 0.05596 - 12.0) < 0.1
def _c1_state():
return SimpleNamespace(
out=SimpleNamespace(steeringAngleDeg=10.0, vEgo=12.0, vEgoRaw=12.0),
c1_lka_torque=5,
c1_msg_pscm=_zero_message(),
)
def test_c1_controller_emits_checked_steering_and_pscm_relay():
cp = CarInterface.get_non_essential_params(CAR.VOLVO_V40)
controller = CarController(DBC[cp.carFingerprint], cp)
cc = SimpleNamespace(
latActive=True,
actuators=_Actuators(),
cruiseControl=SimpleNamespace(cancel=False),
)
actuators, can_sends = controller.update(cc, _c1_state(), 0, None)
assert [(msg[0], msg[2]) for msg in can_sends] == [(0x125, 2), (0xD0, 0)]
fsm = can_sends[1][1]
assert fsm[7] & 0x3 == 3
assert fsm[6] == create_c1_checksum(fsm)
assert 0.0 < actuators.steeringAngleDeg <= 2.0
def test_c1_controller_sends_only_cancel_button():
cp = CarInterface.get_non_essential_params(CAR.VOLVO_V40)
controller = CarController(DBC[cp.carFingerprint], cp)
cc = SimpleNamespace(
latActive=False,
actuators=_Actuators(),
cruiseControl=SimpleNamespace(cancel=True),
)
_, can_sends = controller.update(cc, _c1_state(), 0, None)
buttons = next(msg for msg in can_sends if msg[0] == 0x10)
assert buttons[2] == 0
assert buttons[1][7] == 0x10
assert buttons[1][6] == 0
def test_c1_controller_temporarily_drops_steering_on_zero_torque_fault():
cp = CarInterface.get_non_essential_params(CAR.VOLVO_V40)
controller = CarController(DBC[cp.carFingerprint], cp)
cs = _c1_state()
cs.c1_lka_torque = 0
cc = SimpleNamespace(
latActive=True,
actuators=_Actuators(),
cruiseControl=SimpleNamespace(cancel=False),
)
directions = []
for _ in range(23):
_, can_sends = controller.update(cc, cs, 0, None)
directions.extend(msg[1][7] & 0x3 for msg in can_sends if msg[0] == 0xD0)
assert directions[:-1] == [3] * 11
assert directions[-1] == 0
while controller.frame <= 122:
_, can_sends = controller.update(cc, cs, 0, None)
fsm = next(msg for msg in can_sends if msg[0] == 0xD0)
assert fsm[1][7] & 0x3 == 3
-42
View File
@@ -1,23 +1,13 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import IntFlag
from opendbc.car.structs import CarParams from opendbc.car.structs import CarParams
from opendbc.car import Bus, CarSpecs, DbcDict, PlatformConfig, Platforms from opendbc.car import Bus, CarSpecs, DbcDict, PlatformConfig, Platforms
from opendbc.car.lateral import AngleSteeringLimits from opendbc.car.lateral import AngleSteeringLimits
from opendbc.car.common.conversions import Conversions as CV
from opendbc.car.docs_definitions import CarDocs, CarHarness, CarParts from opendbc.car.docs_definitions import CarDocs, CarHarness, CarParts
from opendbc.car.fw_query_definitions import FwQueryConfig from opendbc.car.fw_query_definitions import FwQueryConfig
Ecu = CarParams.Ecu Ecu = CarParams.Ecu
# C1 support is adapted from the original dragonpilot V40 port:
# https://github.com/dragonpilot/dragonpilot/commit/773dce507082d931236b64dca8024dce9625446f
class VolvoSafetyFlags(IntFlag):
SPA = 1
C1 = 2
class CarControllerParams: class CarControllerParams:
STEER_STEP = 1 # 100 Hz LCA command frequency (controlsd runs at 100 Hz) STEER_STEP = 1 # 100 Hz LCA command frequency (controlsd runs at 100 Hz)
@@ -91,19 +81,6 @@ class CarControllerParams:
([0., 5., 25.], [5., 2., .3]), # rate down limits at different speeds ([0., 5., 25.], [5., 2., .3]), # rate down limits at different speeds
) )
C1_STEER_NO = 0
C1_STEER = 3
C1_N_ZERO_TORQUE = 12
C1_ANGLE_ERROR = 20.0
C1_ANGLE_DELTA_BP = [0., 8.33, 13.89, 19.44, 25., 30.55, 36.1]
C1_ANGLE_DELTA_UP = [2., 1.2, .25, .20, .15, .10, .10]
C1_ANGLE_DELTA_DOWN = [2., 1.2, .25, .20, .15, .10, .10]
C1_ANGLE_LIMITS: AngleSteeringLimits = AngleSteeringLimits(
359.9,
(C1_ANGLE_DELTA_BP, C1_ANGLE_DELTA_UP),
(C1_ANGLE_DELTA_BP, C1_ANGLE_DELTA_DOWN),
)
@dataclass @dataclass
class VolvoCarDocs(CarDocs): class VolvoCarDocs(CarDocs):
@@ -128,26 +105,7 @@ class VolvoSPAPlatformConfig(PlatformConfig):
}) })
@dataclass
class VolvoC1PlatformConfig(PlatformConfig):
dbc_dict: DbcDict = field(default_factory=lambda: {
Bus.pt: 'volvo_v40_2017_pt',
Bus.cam: 'volvo_v40_2017_pt',
})
class CAR(Platforms): class CAR(Platforms):
VOLVO_V40 = VolvoC1PlatformConfig(
[VolvoCarDocs("Volvo V40 2013-19")],
CarSpecs(
mass=1610,
wheelbase=2.647,
steerRatio=14.7,
centerToFrontRatio=0.44,
minSteerSpeed=1.0 * CV.KPH_TO_MS,
),
)
VOLVO_XC40_RECHARGE = VolvoCMAPlatformConfig( VOLVO_XC40_RECHARGE = VolvoCMAPlatformConfig(
[VolvoCarDocs("Volvo XC40 Recharge 2021-23")], [VolvoCarDocs("Volvo XC40 Recharge 2021-23")],
CarSpecs( CarSpecs(
@@ -2,47 +2,6 @@ from opendbc.car.volvo.helpers import (checksum_lca_2_message, checksum_2_0x69_m
checksum_2_pscm_related_message, checksum_lca_5_message) checksum_2_pscm_related_message, checksum_lca_5_message)
from opendbc.car.carlog import carlog from opendbc.car.carlog import carlog
def create_c1_pscm_message(packer, msg_pscm: dict):
values = {
"LKATorque": 0,
"SteeringAngleServo": msg_pscm["SteeringAngleServo"],
"byte0": msg_pscm["byte0"],
"byte3": msg_pscm["byte3"],
"byte4": msg_pscm["byte4"],
"byte7": msg_pscm["byte7"],
"LKAActive": int(msg_pscm["LKAActive"]) & 0xD,
}
return packer.make_can_msg("PSCM1", 2, values)
def create_c1_checksum(data: bytes) -> int:
angle_raw = ((data[4] & 0x3F) << 8) | data[5]
direction = data[7] & 0x3
checksum_sum = (data[3] + direction + angle_raw + (angle_raw >> 8)) & 0xFF
return checksum_sum ^ 0xFF
def create_c1_steering_control(packer, apply_angle: float, lat_active: bool):
values = {
"SET_X_E3": 0xE3,
"SET_X_B4": 0xB4,
"SET_X_08": 0x08,
"TrqLim": 0,
"LKAAngleReq": apply_angle,
"LKASteerDirection": 3 if lat_active else 0,
"SET_X_25": 0x25,
"SET_X_02": 0x02,
}
data = packer.make_can_msg("FSM1", 0, values)[1]
values["Checksum"] = create_c1_checksum(data)
return packer.make_can_msg("FSM1", 0, values)
def create_c1_cancel(packer):
return packer.make_can_msg("CCButtons", 0, {"ACCStopBtn": 1})
def create_lca_message(packer, lat_active: bool, apply_angle: float, msg_lca: dict, def create_lca_message(packer, lat_active: bool, apply_angle: float, msg_lca: dict,
authority_pos: int = 614, authority_neg: int = -614, authority_pos: int = 614, authority_neg: int = -614,
overrides: dict | None = None): overrides: dict | None = None):
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-1
View File
@@ -11,4 +11,3 @@ class ALTERNATIVE_EXPERIENCE:
ALWAYS_ON_LATERAL = 32 ALWAYS_ON_LATERAL = 32
GM_REMAP_CANCEL_TO_DISTANCE = 64 GM_REMAP_CANCEL_TO_DISTANCE = 64
TOYOTA_AUTO_HOLD = 128
@@ -339,7 +339,6 @@ extern bool gm_remote_start_boots_comma;
#define ALT_EXP_ALWAYS_ON_LATERAL 32 #define ALT_EXP_ALWAYS_ON_LATERAL 32
#define ALT_EXP_GM_REMAP_CANCEL_TO_DISTANCE 64 #define ALT_EXP_GM_REMAP_CANCEL_TO_DISTANCE 64
#define ALT_EXP_TOYOTA_AUTO_HOLD 128
extern int alternative_experience; extern int alternative_experience;
+17 -12
View File
@@ -2,8 +2,6 @@
#include "opendbc/safety/declarations.h" #include "opendbc/safety/declarations.h"
#define TOYOTA_AUTO_HOLD_ACCEL -1000 // -1.0 m/s^2 in ACC_CONTROL units
// Stock longitudinal // Stock longitudinal
#define TOYOTA_BASE_TX_MSGS \ #define TOYOTA_BASE_TX_MSGS \
{0x191, 0, 8, .check_relay = true}, {0x412, 0, 8, .check_relay = true}, {0x1D2, 0, 8, .check_relay = false}, /* LKAS + LTA + PCM cancel cmd */ \ {0x191, 0, 8, .check_relay = true}, {0x412, 0, 8, .check_relay = true}, {0x1D2, 0, 8, .check_relay = false}, /* LKAS + LTA + PCM cancel cmd */ \
@@ -279,15 +277,7 @@ static bool toyota_tx_hook(const CANPacket_t *msg) {
// SecOC cars move accel to 0x183. Only allow inactive accel on 0x343 to match stock behavior // SecOC cars move accel to 0x183. Only allow inactive accel on 0x343 to match stock behavior
violation = desired_accel != TOYOTA_LONG_LIMITS.inactive_accel; violation = desired_accel != TOYOTA_LONG_LIMITS.inactive_accel;
} }
violation |= longitudinal_accel_checks(desired_accel, TOYOTA_LONG_LIMITS);
bool toyota_auto_hold =
!toyota_stock_longitudinal &&
((alternative_experience & ALT_EXP_TOYOTA_AUTO_HOLD) != 0) &&
!vehicle_moving && !gas_pressed && acc_main_on &&
(desired_accel == TOYOTA_AUTO_HOLD_ACCEL) &&
GET_BIT(msg, 30U) && !GET_BIT(msg, 31U) && !GET_BIT(msg, 24U);
violation |= !toyota_auto_hold && longitudinal_accel_checks(desired_accel, TOYOTA_LONG_LIMITS);
// only ACC messages that cancel are allowed when openpilot is not controlling longitudinal // only ACC messages that cancel are allowed when openpilot is not controlling longitudinal
if (toyota_stock_longitudinal) { if (toyota_stock_longitudinal) {
@@ -404,7 +394,12 @@ static bool toyota_tx_hook(const CANPacket_t *msg) {
tx = false; tx = false;
} }
if ((msg->addr == 0x344U) && toyota_stock_longitudinal) { // Auto brake hold replaces the camera AEB message only while stopped.
if ((msg->addr == 0x344U) && ((alternative_experience & ALT_EXP_ALLOW_AEB) != 0)) {
if (vehicle_moving || gas_pressed || !acc_main_on) {
tx = false;
}
} else if ((msg->addr == 0x344U) && toyota_stock_longitudinal) {
tx = false; tx = false;
} }
} }
@@ -571,11 +566,21 @@ static safety_config toyota_init(uint16_t param) {
return ret; return ret;
} }
static bool toyota_fwd_hook(int bus_num, int addr) {
bool block_msg = false;
if (bus_num == 2) {
block_msg = (addr == 0x344) && ((alternative_experience & ALT_EXP_ALLOW_AEB) != 0) &&
!vehicle_moving && !gas_pressed && acc_main_on;
}
return block_msg;
}
const safety_hooks toyota_hooks = { const safety_hooks toyota_hooks = {
.init = toyota_init, .init = toyota_init,
.rx = toyota_rx_hook, .rx = toyota_rx_hook,
.rx_all = toyota_rx_all_hook, .rx_all = toyota_rx_all_hook,
.tx = toyota_tx_hook, .tx = toyota_tx_hook,
.fwd = toyota_fwd_hook,
.get_checksum = toyota_get_checksum, .get_checksum = toyota_get_checksum,
.compute_checksum = toyota_compute_checksum, .compute_checksum = toyota_compute_checksum,
.get_quality_flag_valid = toyota_get_quality_flag_valid, .get_quality_flag_valid = toyota_get_quality_flag_valid,
+1 -118
View File
@@ -2,11 +2,9 @@
#include "opendbc/safety/declarations.h" #include "opendbc/safety/declarations.h"
// safetyParam: 0 = CMA (XC40 Recharge), 1 = SPA (S60 Recharge, Polestar 2), // safetyParam: 0 = CMA (XC40 Recharge), 1 = SPA (S60 Recharge, Polestar 2)
// 2 = C1 (V40)
// Polestar 2 is technically CMA, but appears to use SPA DBC for CAN 1 bus // Polestar 2 is technically CMA, but appears to use SPA DBC for CAN 1 bus
#define VOLVO_FLAG_SPA 1U #define VOLVO_FLAG_SPA 1U
#define VOLVO_FLAG_C1 2U
// Volvo CAN message addresses shared between CMA and SPA // Volvo CAN message addresses shared between CMA and SPA
#define VOLVO_LCA_STEER 0x58U // TX from VCU1 to PSCM, LCA steering command (0x58) #define VOLVO_LCA_STEER 0x58U // TX from VCU1 to PSCM, LCA steering command (0x58)
@@ -26,15 +24,6 @@
#define VOLVO_LCA_6 0x97U // TX LCA_6 message #define VOLVO_LCA_6 0x97U // TX LCA_6 message
#define VOLVO_LCA_7 0x92U // TX LCA_7 message #define VOLVO_LCA_7 0x92U // TX LCA_7 message
// C1-specific addresses (V40). The V40 powertrain bus is bus 0 and its
// forward-camera bus is bus 2; bus 1 is unused by this port.
#define VOLVO_C1_BUTTONS 0x10U
#define VOLVO_C1_FSM_0 0x30U
#define VOLVO_C1_FSM_1 0xD0U
#define VOLVO_C1_PSCM_1 0x125U
#define VOLVO_C1_PEDAL_AND_BRAKE 0x55U
#define VOLVO_C1_SPEED 0x150U
// CMA-specific PT bus addresses // CMA-specific PT bus addresses
#define VOLVO_CMA_BUS1_SPEED 0x70U // RX vehicle speed #define VOLVO_CMA_BUS1_SPEED 0x70U // RX vehicle speed
#define VOLVO_CMA_ECM_1 0x250U // RX accelerator pedal position #define VOLVO_CMA_ECM_1 0x250U // RX accelerator pedal position
@@ -55,10 +44,6 @@
#define VOLVO_MAX_ANGLE_CAN 9650 #define VOLVO_MAX_ANGLE_CAN 9650
#define VOLVO_RELAY_ANGLE_TOLERANCE 54 // approximately 3 degrees #define VOLVO_RELAY_ANGLE_TOLERANCE 54 // approximately 3 degrees
#define VOLVO_C1_ANGLE_DEG_TO_CAN 22.753128f
#define VOLVO_C1_MAX_ANGLE_CAN 8189
#define VOLVO_C1_RELAY_ANGLE_TOLERANCE 2
// CAN bus definitions for Volvo // CAN bus definitions for Volvo
// Using same naming as carstate.py for consistency: main, pt, party // Using same naming as carstate.py for consistency: main, pt, party
@@ -69,7 +54,6 @@
// Runtime addresses set by volvo_init based on safetyParam // Runtime addresses set by volvo_init based on safetyParam
static uint16_t volvo_ecm_1_addr; static uint16_t volvo_ecm_1_addr;
static uint16_t volvo_bus1_cruise_control_addr; static uint16_t volvo_bus1_cruise_control_addr;
static bool volvo_c1;
static int volvo_be_15(const CANPacket_t *msg, uint8_t byte) { static int volvo_be_15(const CANPacket_t *msg, uint8_t byte) {
return (int)(((uint16_t)(msg->data[byte] & 0x7FU) << 8U) | msg->data[byte + 1U]); return (int)(((uint16_t)(msg->data[byte] & 0x7FU) << 8U) | msg->data[byte + 1U]);
@@ -83,21 +67,6 @@ static int volvo_lca_5_angle(const CANPacket_t *msg) {
return to_signed(volvo_be_15(msg, 6U), 15); return to_signed(volvo_be_15(msg, 6U), 15);
} }
static int volvo_c1_pscm_angle(const CANPacket_t *msg) {
return (int)(((uint16_t)msg->data[5] << 8U) | msg->data[6]) - 32768;
}
static int volvo_c1_fsm_angle(const CANPacket_t *msg) {
return (int)(((uint16_t)(msg->data[4] & 0x3FU) << 8U) | msg->data[5]) - 8192;
}
static uint8_t volvo_c1_fsm_checksum(const CANPacket_t *msg) {
const uint16_t angle_raw = ((uint16_t)(msg->data[4] & 0x3FU) << 8U) | msg->data[5];
const uint8_t direction = msg->data[7] & 0x3U;
const uint8_t checksum_sum = (msg->data[3] + direction + angle_raw + (angle_raw >> 8U)) & 0xFFU;
return checksum_sum ^ 0xFFU;
}
static const AngleSteeringLimits VOLVO_ANGLE_STEERING_LIMITS = { static const AngleSteeringLimits VOLVO_ANGLE_STEERING_LIMITS = {
.max_angle = VOLVO_MAX_ANGLE_CAN, .max_angle = VOLVO_MAX_ANGLE_CAN,
.angle_deg_to_can = VOLVO_ANGLE_DEG_TO_CAN, .angle_deg_to_can = VOLVO_ANGLE_DEG_TO_CAN,
@@ -112,51 +81,8 @@ static const AngleSteeringLimits VOLVO_ANGLE_STEERING_LIMITS = {
.frequency = 50U, .frequency = 50U,
}; };
static const AngleSteeringLimits VOLVO_C1_ANGLE_STEERING_LIMITS = {
.max_angle = VOLVO_C1_MAX_ANGLE_CAN,
.angle_deg_to_can = VOLVO_C1_ANGLE_DEG_TO_CAN,
.angle_rate_up_lookup = {
{7.0f, 17.0f, 36.0f},
{2.0f, 0.25f, 0.1f},
},
.angle_rate_down_lookup = {
{7.0f, 17.0f, 36.0f},
{2.0f, 0.25f, 0.1f},
},
.max_angle_error = 455, // 20 degrees
.angle_error_min_speed = 0.0f,
.frequency = 50U,
.enforce_angle_error = true,
};
static void volvo_rx_hook(const CANPacket_t *msg) { static void volvo_rx_hook(const CANPacket_t *msg) {
if (volvo_c1) {
if (msg->bus == VOLVO_MAIN_BUS) {
if (msg->addr == VOLVO_C1_PSCM_1) {
update_sample(&angle_meas, volvo_c1_pscm_angle(msg));
}
if (msg->addr == VOLVO_C1_SPEED) {
const uint16_t speed_raw = ((uint16_t)msg->data[6] << 8U) | msg->data[7];
const float speed = ((float)speed_raw * 0.01f) / 3.6f;
vehicle_moving = speed > 0.1f;
UPDATE_VEHICLE_SPEED(speed);
}
if (msg->addr == VOLVO_C1_PEDAL_AND_BRAKE) {
const uint16_t gas_raw = ((uint16_t)(msg->data[1] & 0x3U) << 8U) | msg->data[2];
gas_pressed = gas_raw > 50U; // DBC factor 0.1: greater than 5 percent
brake_pressed = GET_BIT(msg, 24U) || GET_BIT(msg, 38U);
}
}
if ((msg->bus == VOLVO_PARTY_BUS) && (msg->addr == VOLVO_C1_FSM_0)) {
pcm_cruise_check(GET_BIT(msg, 58U));
}
return;
}
// Main bus (bus 0) messages // Main bus (bus 0) messages
if (msg->bus == VOLVO_MAIN_BUS) { if (msg->bus == VOLVO_MAIN_BUS) {
// Update brake pedal and cruise state from BCM2 // Update brake pedal and cruise state from BCM2
@@ -232,33 +158,6 @@ static void volvo_rx_hook(const CANPacket_t *msg) {
static bool volvo_tx_hook(const CANPacket_t *msg) { static bool volvo_tx_hook(const CANPacket_t *msg) {
bool tx = true; bool tx = true;
if (volvo_c1) {
if (msg->addr == VOLVO_C1_FSM_1) {
const int desired_angle = volvo_c1_fsm_angle(msg);
const uint8_t direction = msg->data[7] & 0x3U;
const bool steer_control_enabled = direction != 0U;
tx &= SAFETY_ABS(desired_angle) <= VOLVO_C1_MAX_ANGLE_CAN;
tx &= !steer_angle_cmd_checks(desired_angle, steer_control_enabled, VOLVO_C1_ANGLE_STEERING_LIMITS);
tx &= (direction == 0U) || (direction == 3U);
tx &= (msg->data[0] == 0xE3U) && (msg->data[1] == 0xB4U) && (msg->data[2] == 0x08U);
tx &= (msg->data[3] == 0x80U) && ((msg->data[4] & 0xC0U) == 0x80U) && ((msg->data[7] & 0xFCU) == 0x94U);
tx &= msg->data[6] == volvo_c1_fsm_checksum(msg);
}
if (msg->addr == VOLVO_C1_PSCM_1) {
const int relayed_angle = volvo_c1_pscm_angle(msg);
const int measured_max = angle_meas.max + VOLVO_C1_RELAY_ANGLE_TOLERANCE;
const int measured_min = angle_meas.min - VOLVO_C1_RELAY_ANGLE_TOLERANCE;
tx &= !safety_max_limit_check(relayed_angle, measured_max, measured_min);
}
// Only ACC cancel (byte 7 bit 4) may be synthesized.
if (msg->addr == VOLVO_C1_BUTTONS) {
tx &= ((msg->data[7] & 0xEFU) == 0U) && (msg->data[6] == 0U);
}
return tx;
}
// LCA_5 carries the actual angle command used by the controller. The stock // LCA_5 carries the actual angle command used by the controller. The stock
// LCA frame also contains an angle-shaped field, but the imported controller // LCA frame also contains an angle-shaped field, but the imported controller
// deliberately leaves that field at the observed vehicle value. // deliberately leaves that field at the observed vehicle value.
@@ -356,22 +255,6 @@ static bool volvo_tx_hook(const CANPacket_t *msg) {
static safety_config volvo_init(uint16_t param) { static safety_config volvo_init(uint16_t param) {
bool spa = GET_FLAG(param, VOLVO_FLAG_SPA); bool spa = GET_FLAG(param, VOLVO_FLAG_SPA);
volvo_c1 = GET_FLAG(param, VOLVO_FLAG_C1);
if (volvo_c1) {
static const CanMsg VOLVO_C1_TX_MSGS[] = {
{VOLVO_C1_FSM_1, VOLVO_MAIN_BUS, 8, .check_relay = true},
{VOLVO_C1_PSCM_1, VOLVO_PARTY_BUS, 8, .check_relay = true},
{VOLVO_C1_BUTTONS, VOLVO_MAIN_BUS, 8, .check_relay = false},
};
static RxCheck volvo_c1_rx_checks[] = {
{.msg = {{VOLVO_C1_PSCM_1, VOLVO_MAIN_BUS, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_C1_FSM_0, VOLVO_PARTY_BUS, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_C1_PEDAL_AND_BRAKE, VOLVO_MAIN_BUS, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_C1_SPEED, VOLVO_MAIN_BUS, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
};
return BUILD_SAFETY_CFG(volvo_c1_rx_checks, VOLVO_C1_TX_MSGS);
}
// Set PT bus addresses based on platform // Set PT bus addresses based on platform
volvo_ecm_1_addr = spa ? VOLVO_SPA_ECM_1 : VOLVO_CMA_ECM_1; volvo_ecm_1_addr = spa ? VOLVO_SPA_ECM_1 : VOLVO_CMA_ECM_1;
@@ -97,55 +97,29 @@ class TestToyotaSafetyBase(common.CarSafetyTest, common.LongitudinalAccelSafetyT
msg = libsafety_py.make_CANPacket(0x283, 0, bytes(dat)) msg = libsafety_py.make_CANPacket(0x283, 0, bytes(dat))
self.assertEqual(not bad and not stock_longitudinal, self._tx(msg)) self.assertEqual(not bad and not stock_longitudinal, self._tx(msg))
def test_auto_hold_acc_control_is_narrowly_allowed_only_at_standstill(self): def test_auto_brake_hold_aeb_replacement_only_at_standstill(self):
if (not self.LONGITUDINAL or self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALLOW_AEB)
self.safety.get_current_safety_param() & (ToyotaSafetyFlags.STOCK_LONGITUDINAL.value | ToyotaSafetyFlags.SECOC.value)): hold_msg = libsafety_py.make_CANPacket(0x344, 0, b"\xfd\x80\x00\x00\x00\x00\x00\xcc")
raise unittest.SkipTest("Toyota Auto Hold requires non-SecOC openpilot longitudinal control")
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.TOYOTA_AUTO_HOLD)
hold_msg = self.packer.make_can_msg_safety("ACC_CONTROL", 0, {
"ACCEL_CMD": -1.0,
"PERMIT_BRAKING": 1,
"RELEASE_STANDSTILL": 0,
"CANCEL_REQ": 0,
})
self._rx(self._speed_msg(0)) self._rx(self._speed_msg(0))
self._rx(self._toggle_aol(True)) self._rx(self._toggle_aol(True))
self._rx(self._user_gas_msg(False)) self._rx(self._user_gas_msg(False))
self.safety.set_controls_allowed(False)
self.assertTrue(self._tx(hold_msg)) self.assertTrue(self._tx(hold_msg))
self.assertEqual(-1, self.safety.safety_fwd_hook(2, 0x344))
self.assertFalse(self._tx(self.packer.make_can_msg_safety("ACC_CONTROL", 0, {
"ACCEL_CMD": -1.1,
"PERMIT_BRAKING": 1,
"RELEASE_STANDSTILL": 0,
})))
self._rx(self._speed_msg(1.0)) self._rx(self._speed_msg(1.0))
self.assertFalse(self._tx(hold_msg)) self.assertFalse(self._tx(hold_msg))
self.assertEqual(0, self.safety.safety_fwd_hook(2, 0x344))
self._rx(self._speed_msg(0)) self._rx(self._speed_msg(0))
self._rx(self._user_gas_msg(True)) self._rx(self._user_gas_msg(True))
self.assertFalse(self._tx(hold_msg)) self.assertFalse(self._tx(hold_msg))
self.assertEqual(0, self.safety.safety_fwd_hook(2, 0x344))
self._rx(self._user_gas_msg(False)) self._rx(self._user_gas_msg(False))
self._rx(self._toggle_aol(False)) self._rx(self._toggle_aol(False))
self.assertFalse(self._tx(hold_msg)) self.assertFalse(self._tx(hold_msg))
self.assertEqual(0, self.safety.safety_fwd_hook(2, 0x344))
def test_auto_hold_acc_control_is_blocked_without_toyota_hold_toggle(self):
hold_msg = self.packer.make_can_msg_safety("ACC_CONTROL", 0, {
"ACCEL_CMD": -1.0,
"PERMIT_BRAKING": 1,
"RELEASE_STANDSTILL": 0,
"CANCEL_REQ": 0,
})
self._rx(self._speed_msg(0))
self._rx(self._toggle_aol(True))
self._rx(self._user_gas_msg(False))
self.safety.set_controls_allowed(False)
self.safety.set_alternative_experience(0)
self.assertFalse(self._tx(hold_msg))
# Only allow LTA msgs with no actuation # Only allow LTA msgs with no actuation
def test_lta_steer_cmd(self): def test_lta_steer_cmd(self):
+10 -131
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Safety tests for Volvo C1/CMA/SPA. Safety tests for Volvo CMA/SPA.
The safety mode lives at ``opendbc/safety/modes/volvo.h`` and is parameterized The safety mode lives at ``opendbc/safety/modes/volvo.h`` and is parameterized
by ``safetyParam``: by ``safetyParam``:
@@ -8,13 +8,14 @@ by ``safetyParam``:
- ``safetyParam == 0`` CMA platform (Volvo XC40 Recharge) - ``safetyParam == 0`` CMA platform (Volvo XC40 Recharge)
- ``safetyParam == VOLVO_FLAG_SPA`` SPA platform (Volvo S60 Recharge, - ``safetyParam == VOLVO_FLAG_SPA`` SPA platform (Volvo S60 Recharge,
Polestar 2) Polestar 2)
- ``safetyParam == VOLVO_FLAG_C1`` C1 platform (Volvo V40)
CMA and SPA share LCA/PSCM/etc. addresses on the main and party buses but use The two platforms share LCA/PSCM/etc. addresses on the main and party buses
different PT-bus addresses and signal scales. C1 uses the V40's legacy CAN but use *different* PT-bus addresses and signal scales for ECM_1 and
layout and its own safety allowlist. The tests exercise all three through the BUS1_CRUISE_CONTROL. Vehicle speed is read from main-bus SPEED on both, so it
generic ``CarSafetyTest`` harness so divergence between ``carstate.py`` and is not platform-dependent. This test file exercises both platforms through the
``volvo.h`` is caught before running in a car. same generic ``CarSafetyTest`` harness so that any future divergence between
``carstate.py`` and ``volvo.h`` e.g. a threshold drifting out of sync is
caught on a laptop instead of in the car.
Companion to: ``opendbc/car/volvo/carstate.py`` (must agree on thresholds). Companion to: ``opendbc/car/volvo/carstate.py`` (must agree on thresholds).
""" """
@@ -24,16 +25,13 @@ import re
import unittest import unittest
from opendbc.car.volvo.interface import SAFETY_VOLVO from opendbc.car.volvo.interface import SAFETY_VOLVO
from opendbc.car.volvo.values import VolvoSafetyFlags
from opendbc.car.volvo.volvocan import create_c1_checksum, create_c1_steering_control
from opendbc.safety.tests.libsafety import libsafety_py from opendbc.safety.tests.libsafety import libsafety_py
import opendbc.safety.tests.common as common import opendbc.safety.tests.common as common
from opendbc.safety.tests.common import CANPackerSafety from opendbc.safety.tests.common import CANPackerSafety
# Must match the flags in opendbc/safety/modes/volvo.h # Must match VOLVO_FLAG_SPA in opendbc/safety/modes/volvo.h
VOLVO_FLAG_SPA = VolvoSafetyFlags.SPA.value VOLVO_FLAG_SPA = 1
VOLVO_FLAG_C1 = VolvoSafetyFlags.C1.value
# Must match VOLVO_SPEED_TO_MS in volvo.h and SPEED_TO_MS in carstate.py # Must match VOLVO_SPEED_TO_MS in volvo.h and SPEED_TO_MS in carstate.py
VOLVO_SPEED_TO_MS = 0.003977 VOLVO_SPEED_TO_MS = 0.003977
@@ -334,124 +332,5 @@ class TestVolvoSPA(TestVolvoSafetyBase):
"BUS1_CRUISE_CONTROL", VOLVO_PT_BUS, values) "BUS1_CRUISE_CONTROL", VOLVO_PT_BUS, values)
class TestVolvoC1(common.CarSafetyTest, common.AngleSteeringSafetyTest):
TX_MSGS = [[0xD0, VOLVO_MAIN_BUS], [0x125, VOLVO_PARTY_BUS], [0x10, VOLVO_MAIN_BUS]]
RELAY_MALFUNCTION_ADDRS = {
VOLVO_MAIN_BUS: (0xD0,),
VOLVO_PARTY_BUS: (0x125,),
}
FWD_BLACKLISTED_ADDRS = {
VOLVO_MAIN_BUS: [0x125],
VOLVO_PARTY_BUS: [0xD0],
}
STANDSTILL_THRESHOLD = 0.1
GAS_PRESSED_THRESHOLD = 5.0
STEER_ANGLE_MAX = 359.9
STEER_ANGLE_TEST_MAX = 350.0
DEG_TO_CAN = 1 / 0.04395
ANGLE_RATE_BP = [7.0, 17.0, 36.0]
ANGLE_RATE_UP = [2.0, 0.25, 0.1]
ANGLE_RATE_DOWN = [2.0, 0.25, 0.1]
def setUp(self):
self.packer = CANPackerSafety("volvo_v40_2017_pt")
self.safety = libsafety_py.libsafety
self.safety.set_safety_hooks(SAFETY_VOLVO, VOLVO_FLAG_C1)
self.safety.init_tests()
def _angle_cmd_msg(self, angle: float, enabled: bool, increment_timer: bool = True):
values = {
"SET_X_E3": 0xE3,
"SET_X_B4": 0xB4,
"SET_X_08": 0x08,
"LKAAngleReq": angle,
"LKASteerDirection": 3 if enabled else 0,
"TrqLim": 0,
"SET_X_25": 0x25,
"SET_X_02": 0x02,
}
def fix_checksum(msg):
address, data, bus = msg
data = bytearray(data)
data[6] = create_c1_checksum(data)
return address, data, bus
return self.packer.make_can_msg_safety("FSM1", VOLVO_MAIN_BUS, values, fix_checksum)
def _angle_meas_msg(self, angle: float):
return self.packer.make_can_msg_safety(
"PSCM1", VOLVO_MAIN_BUS, {"SteeringAngleServo": angle})
def _speed_msg(self, speed):
return self.packer.make_can_msg_safety(
"VehicleSpeed1", VOLVO_MAIN_BUS, {"VehicleSpeed": speed * 3.6})
def _speed_msg_2(self, speed):
return None
def _user_brake_msg(self, brake):
return self.packer.make_can_msg_safety(
"PedalandBrake", VOLVO_MAIN_BUS, {"BrakePedalActive2": bool(brake)})
def _user_gas_msg(self, gas):
return self.packer.make_can_msg_safety(
"PedalandBrake", VOLVO_MAIN_BUS, {"AccPedal": gas})
def _pcm_status_msg(self, enable):
return self.packer.make_can_msg_safety(
"FSM0", VOLVO_PARTY_BUS, {"ACCStatusActive": bool(enable)})
def test_cancel_button_only(self):
allowed = self.packer.make_can_msg_safety(
"CCButtons", VOLVO_MAIN_BUS, {"ACCStopBtn": 1})
self.assertTrue(self._tx(allowed))
for signal in ("ACCOnOffBtn", "ACCSetBtn", "ACCResumeBtn", "ACCMinusBtn",
"TimeGapIncreaseBtn", "TimeGapDecreaseBtn"):
msg = self.packer.make_can_msg_safety("CCButtons", VOLVO_MAIN_BUS, {signal: 1})
self.assertFalse(self._tx(msg), signal)
def test_pscm_relay_cannot_invent_angle(self):
for _ in range(common.MAX_SAMPLE_VALS):
self._rx(self._angle_meas_msg(10))
valid = self.packer.make_can_msg_safety(
"PSCM1", VOLVO_PARTY_BUS, {"SteeringAngleServo": 10})
invalid = self.packer.make_can_msg_safety(
"PSCM1", VOLVO_PARTY_BUS, {"SteeringAngleServo": 20})
self.assertTrue(self._tx(valid))
self.assertFalse(self._tx(invalid))
def test_pscm_relay_preserves_full_lock_angle(self):
for angle in (-720, 500):
for _ in range(common.MAX_SAMPLE_VALS):
self._rx(self._angle_meas_msg(angle))
relayed = self.packer.make_can_msg_safety(
"PSCM1", VOLVO_PARTY_BUS, {"SteeringAngleServo": angle})
self.assertTrue(self._tx(relayed), angle)
def test_steering_static_fields_and_checksum(self):
self.safety.set_controls_allowed(True)
self._reset_angle_measurement(0)
self._reset_speed_measurement(10)
self._set_prev_desired_angle(0)
valid = self._angle_cmd_msg(0, True)
self.assertTrue(self._tx(valid))
for byte_index in (0, 1, 2, 3, 4, 6, 7):
invalid = self._angle_cmd_msg(0, True)
invalid[0].data[byte_index] ^= 0x4 if byte_index in (4, 7) else 0x1
self.assertFalse(self._tx(invalid), byte_index)
def test_controller_steering_message_is_allowed(self):
self.safety.set_controls_allowed(True)
self._reset_angle_measurement(0)
self._reset_speed_measurement(10)
self._set_prev_desired_angle(0)
address, data, bus = create_c1_steering_control(self.packer, 0, True)
self.assertTrue(self._tx(libsafety_py.make_CANPacket(address, bus, data)))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,2 +1,2 @@
extern const uint8_t gitversion[19]; extern const uint8_t gitversion[19];
const uint8_t gitversion[19] = "DEV-eedd73e5-DEBUG"; const uint8_t gitversion[19] = "DEV-c03d06b4-DEBUG";
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
DEV-eedd73e5-DEBUG DEV-c03d06b4-DEBUG
+3
View File
@@ -652,6 +652,9 @@ class Controls:
elif CC.latActive and CS.steeringPressed and CS.steeringTorque * blinker_dir < 0.0 and \ elif CC.latActive and CS.steeringPressed and CS.steeringTorque * blinker_dir < 0.0 and \
self.curvature * blinker_dir > CURVATURE_HOLD_CONFIRM_MIN and \ self.curvature * blinker_dir > CURVATURE_HOLD_CONFIRM_MIN and \
self.turn_blinker_swept < CURVATURE_HOLD_CONFIRM_SWEPT: self.turn_blinker_swept < CURVATURE_HOLD_CONFIRM_SWEPT:
# an active driver push into the signaled turn BEFORE the turn is made is fresh
# turn intent: re-arm the cycle even after a prior handoff. A long blinker-on
# approach can latch done on a trivial micro-handoff and lock out
# nudge-to-commit ten seconds later at the real turn (0000087f seg 1: +418 haul # nudge-to-commit ten seconds later at the real turn (0000087f seg 1: +418 haul
# unassisted). The swept gate keeps a light same-direction touch during the # unassisted). The swept gate keeps a light same-direction touch during the
# EXIT unwind from re-latching a large hold against the model's recentering # EXIT unwind from re-latching a large hold against the model's recentering
@@ -632,9 +632,6 @@ class LatControlTorque(LatControl):
output_torque *= get_kia_ev6_center_output_scale(setpoint, CS.vEgo) output_torque *= get_kia_ev6_center_output_scale(setpoint, CS.vEgo)
elif kia_carnival_active: elif kia_carnival_active:
output_torque *= kia_carnival_center_taper output_torque *= kia_carnival_center_taper
output_torque *= get_kia_carnival_unwind_output_scale(
setpoint, measurement, desired_lateral_jerk, CS.vEgo,
)
output_torque *= get_kia_carnival_highway_transition_output_scale(setpoint, desired_lateral_jerk, CS.vEgo) output_torque *= get_kia_carnival_highway_transition_output_scale(setpoint, desired_lateral_jerk, CS.vEgo)
elif palisade_active: elif palisade_active:
output_torque *= get_palisade_center_output_scale(setpoint, CS.vEgo) output_torque *= get_palisade_center_output_scale(setpoint, CS.vEgo)
@@ -314,7 +314,7 @@ GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_LAT = 0.14
GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_LAT_WIDTH = 0.05 GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_LAT_WIDTH = 0.05
GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_SPEED = 6.0 GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_SPEED = 6.0
GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_SPEED_WIDTH = 1.5 GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_SPEED_WIDTH = 1.5
GENESIS_G70_CURVE_UNWIND_OUTPUT_REDUCTION_MAX = 0.10 GENESIS_G70_CURVE_UNWIND_OUTPUT_REDUCTION_MAX = 0.08
GENESIS_G70_CURVE_UNWIND_SPEED = 18.0 GENESIS_G70_CURVE_UNWIND_SPEED = 18.0
GENESIS_G70_CURVE_UNWIND_SPEED_WIDTH = 3.0 GENESIS_G70_CURVE_UNWIND_SPEED_WIDTH = 3.0
GENESIS_G70_CURVE_UNWIND_LAT = 0.25 GENESIS_G70_CURVE_UNWIND_LAT = 0.25
@@ -629,15 +629,6 @@ KIA_CARNIVAL_UNWIND_FF_OVERSHOOT = 0.08
KIA_CARNIVAL_UNWIND_FF_OVERSHOOT_WIDTH = 0.06 KIA_CARNIVAL_UNWIND_FF_OVERSHOOT_WIDTH = 0.06
KIA_CARNIVAL_UNWIND_FF_JERK = 0.45 KIA_CARNIVAL_UNWIND_FF_JERK = 0.45
KIA_CARNIVAL_UNWIND_FF_JERK_WIDTH = 0.20 KIA_CARNIVAL_UNWIND_FF_JERK_WIDTH = 0.20
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_MAX = 0.28
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_SPEED = 8.0
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_SPEED_WIDTH = 2.0
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_SPEED_CUTOFF = 16.0
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_SPEED_CUTOFF_WIDTH = 2.5
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_OVERSHOOT = 0.25
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_OVERSHOOT_WIDTH = 0.15
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_JERK = 0.45
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_JERK_WIDTH = 0.20
TUCSON_4TH_GEN_CENTER_TAPER_MAX = 0.44 TUCSON_4TH_GEN_CENTER_TAPER_MAX = 0.44
TUCSON_4TH_GEN_CENTER_TAPER_LAT = 0.28 TUCSON_4TH_GEN_CENTER_TAPER_LAT = 0.28
@@ -2863,27 +2854,6 @@ def get_kia_carnival_unwind_ff_scale(setpoint: float, measured_lateral_accel: fl
return 1.0 - (KIA_CARNIVAL_UNWIND_FF_REDUCTION_MAX * speed_weight * overshoot_weight * jerk_weight) return 1.0 - (KIA_CARNIVAL_UNWIND_FF_REDUCTION_MAX * speed_weight * overshoot_weight * jerk_weight)
def get_kia_carnival_unwind_output_scale(setpoint: float, measured_lateral_accel: float,
desired_lateral_jerk: float, v_ego: float) -> float:
if (setpoint * desired_lateral_jerk >= 0.0 or
setpoint * measured_lateral_accel <= 0.0):
return 1.0
overshoot = max(abs(measured_lateral_accel) - abs(setpoint), 0.0)
if overshoot <= 0.0:
return 1.0
speed_weight = (_sigmoid((v_ego - KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_SPEED) /
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_SPEED_WIDTH) *
_sigmoid((KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_SPEED_CUTOFF - v_ego) /
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_SPEED_CUTOFF_WIDTH))
overshoot_weight = _sigmoid((overshoot - KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_OVERSHOOT) /
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_OVERSHOOT_WIDTH)
jerk_weight = _sigmoid((abs(desired_lateral_jerk) - KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_JERK) /
KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_JERK_WIDTH)
return 1.0 - (KIA_CARNIVAL_UNWIND_OUTPUT_DAMPING_MAX * speed_weight * overshoot_weight * jerk_weight)
def _tucson_4th_gen_center_weights(desired_lateral_accel: float, v_ego: float) -> tuple[float, float]: def _tucson_4th_gen_center_weights(desired_lateral_accel: float, v_ego: float) -> tuple[float, float]:
speed_weight = _sigmoid((TUCSON_4TH_GEN_CENTER_TAPER_SPEED_MAX - v_ego) / TUCSON_4TH_GEN_CENTER_TAPER_SPEED_WIDTH) speed_weight = _sigmoid((TUCSON_4TH_GEN_CENTER_TAPER_SPEED_MAX - v_ego) / TUCSON_4TH_GEN_CENTER_TAPER_SPEED_WIDTH)
center_weight = _sigmoid((TUCSON_4TH_GEN_CENTER_TAPER_LAT - abs(desired_lateral_accel)) / TUCSON_4TH_GEN_CENTER_TAPER_LAT_WIDTH) center_weight = _sigmoid((TUCSON_4TH_GEN_CENTER_TAPER_LAT - abs(desired_lateral_accel)) / TUCSON_4TH_GEN_CENTER_TAPER_LAT_WIDTH)
@@ -3156,6 +3126,8 @@ def get_genesis_gv70_unwind_ff_scale(setpoint: float, measured_lateral_accel: fl
return 1.0 return 1.0
overshoot = max(abs(measured_lateral_accel) - abs(setpoint), 0.0) overshoot = max(abs(measured_lateral_accel) - abs(setpoint), 0.0)
if overshoot <= 0.0:
return 1.0
overshoot_weight = _sigmoid((overshoot - GENESIS_GV70_UNWIND_FF_OVERSHOOT) / overshoot_weight = _sigmoid((overshoot - GENESIS_GV70_UNWIND_FF_OVERSHOOT) /
GENESIS_GV70_UNWIND_FF_OVERSHOOT_WIDTH) GENESIS_GV70_UNWIND_FF_OVERSHOOT_WIDTH)
jerk_weight = _sigmoid((abs(desired_lateral_jerk) - GENESIS_GV70_UNWIND_FF_JERK) / jerk_weight = _sigmoid((abs(desired_lateral_jerk) - GENESIS_GV70_UNWIND_FF_JERK) /
+6 -73
View File
@@ -38,10 +38,6 @@ HONDA_BOSCH_A_CHALLENGER_STALE_CYCLES = 2
HONDA_BOSCH_A_GROSS_DISTANCE_STALE_CYCLES = 3 HONDA_BOSCH_A_GROSS_DISTANCE_STALE_CYCLES = 3
HONDA_BOSCH_A_GROSS_DISTANCE_M = 25.0 HONDA_BOSCH_A_GROSS_DISTANCE_M = 25.0
POST_STANDSTILL_RADAR_LEAD_PERSISTENCE_FRAMES = 3
POST_STANDSTILL_RADAR_LEAD_URGENT_TTC = 1.5
POST_STANDSTILL_RADAR_LEAD_URGENT_DISTANCE = 1.5
def is_bosch_a_radar_car(CP) -> bool: def is_bosch_a_radar_car(CP) -> bool:
return CP.brand == "honda" and CP.carFingerprint in HONDA_BOSCH_A and not CP.radarUnavailable return CP.brand == "honda" and CP.carFingerprint in HONDA_BOSCH_A and not CP.radarUnavailable
@@ -246,17 +242,10 @@ def g90_low_speed_radar_lead_sane(track: Track, v_ego: float) -> bool:
def honda_bosch_a_low_speed_radar_lead_sane(track: Track, v_ego: float) -> bool: def honda_bosch_a_low_speed_radar_lead_sane(track: Track, v_ego: float) -> bool:
"""Require a few real Bosch sweeps before a radar-only low-speed takeover."""
return track.cnt >= HONDA_BOSCH_A_LOW_SPEED_MIN_COUNT and track.potential_low_speed_lead(v_ego) return track.cnt >= HONDA_BOSCH_A_LOW_SPEED_MIN_COUNT and track.potential_low_speed_lead(v_ego)
def post_standstill_radar_lead_is_urgent(lead: dict[str, Any]) -> bool:
d_rel = float(lead.get("dRel", math.inf))
v_rel = float(lead.get("vRel", 0.0))
closing_speed = max(-v_rel, 0.0)
ttc = d_rel / closing_speed if closing_speed > 0.1 else math.inf
return d_rel <= POST_STANDSTILL_RADAR_LEAD_URGENT_DISTANCE or ttc <= POST_STANDSTILL_RADAR_LEAD_URGENT_TTC
def track_matches_vision(track: Track, lead: capnp._DynamicStructReader, v_ego: float, *, def track_matches_vision(track: Track, lead: capnp._DynamicStructReader, v_ego: float, *,
dist_scale: float, dist_floor: float, vel_limit: float, dist_scale: float, dist_floor: float, vel_limit: float,
y_std_scale: float, y_floor: float) -> bool: y_std_scale: float, y_floor: float) -> bool:
@@ -465,11 +454,6 @@ class RadarD:
self.preferred_stale_track_ids = [-1, -1] self.preferred_stale_track_ids = [-1, -1]
self.preferred_challenger_stale_counts = [0, 0] self.preferred_challenger_stale_counts = [0, 0]
self.preferred_gross_distance_stale_counts = [0, 0] self.preferred_gross_distance_stale_counts = [0, 0]
self._was_standstill = False
self._standstill_had_lead = False
self._post_standstill_gate_active = False
self._post_standstill_candidate_id = -1
self._post_standstill_candidate_frames = 0
self.v_ego = 0.0 self.v_ego = 0.0
self.v_ego_hist = deque([0.0], maxlen=int(round(delay / DT_MDL)) + 1) self.v_ego_hist = deque([0.0], maxlen=int(round(delay / DT_MDL)) + 1)
@@ -541,57 +525,9 @@ class RadarD:
self.prev_lead_track_ids[lead_index] = -1 self.prev_lead_track_ids[lead_index] = -1
self._reset_preferred_stale_evidence(lead_index) self._reset_preferred_stale_evidence(lead_index)
def _prepare_post_standstill_gate(self, standstill: bool) -> None:
if standstill:
self._post_standstill_gate_active = False
self._post_standstill_candidate_id = -1
self._post_standstill_candidate_frames = 0
elif self._was_standstill and not self._standstill_had_lead:
self._post_standstill_gate_active = True
self._post_standstill_candidate_id = -1
self._post_standstill_candidate_frames = 0
def _filter_post_standstill_lead(self, lead: dict[str, Any]) -> dict[str, Any]:
if not self._post_standstill_gate_active:
return lead
model_lead = float(lead.get("modelProb", 0.0)) > float(
getattr(self.starpilot_toggles, "lead_detection_probability", 0.35))
radar_only = bool(lead.get("status", False) and lead.get("radar", False) and not model_lead)
if not radar_only:
if lead.get("status", False):
self._post_standstill_gate_active = False
self._post_standstill_candidate_id = -1
self._post_standstill_candidate_frames = 0
return lead
track_id = int(lead.get("radarTrackId", -1))
if track_id == self._post_standstill_candidate_id:
self._post_standstill_candidate_frames += 1
else:
self._post_standstill_candidate_id = track_id
self._post_standstill_candidate_frames = 1
persistent = self._post_standstill_candidate_frames >= POST_STANDSTILL_RADAR_LEAD_PERSISTENCE_FRAMES
if persistent or post_standstill_radar_lead_is_urgent(lead):
self._post_standstill_gate_active = False
return lead
return {"status": False}
def _remember_post_standstill_state(self, standstill: bool, lead_status: bool) -> None:
if standstill:
self._was_standstill = True
self._standstill_had_lead |= lead_status
else:
self._was_standstill = False
self._standstill_had_lead = False
def update(self, sm: messaging.SubMaster, rr: car.RadarData): def update(self, sm: messaging.SubMaster, rr: car.RadarData):
self.ready = sm.seen['modelV2'] self.ready = sm.seen['modelV2']
self.current_time = 1e-9 * max(sm.logMonoTime.values()) self.current_time = 1e-9 * max(sm.logMonoTime.values())
standstill = bool(sm['carState'].standstill)
self._prepare_post_standstill_gate(standstill)
if sm.recv_frame['carState'] != self.last_v_ego_frame: if sm.recv_frame['carState'] != self.last_v_ego_frame:
self.v_ego = sm['carState'].vEgo self.v_ego = sm['carState'].vEgo
@@ -649,12 +585,11 @@ class RadarD:
self._update_honda_bosch_a_preferred_staleness(i, leads_v3[i], self.lead_prob_filters[i].x) self._update_honda_bosch_a_preferred_staleness(i, leads_v3[i], self.lead_prob_filters[i].x)
lead_one = get_lead(self.v_ego, self.ready, self.tracks, leads_v3[0], model_v_ego, sm['modelV2'], self.radar_state.leadOne = get_lead(self.v_ego, self.ready, self.tracks, leads_v3[0], model_v_ego, sm['modelV2'],
standstill, sm['starpilotPlan'], self.starpilot_toggles, low_speed_override=True, sm['carState'].standstill, sm['starpilotPlan'], self.starpilot_toggles, low_speed_override=True,
g90_radar_filter=self.g90_radar_filter, lead_prob=self.lead_prob_filters[0].x, g90_radar_filter=self.g90_radar_filter, lead_prob=self.lead_prob_filters[0].x,
preferred_track_id=self.prev_lead_track_ids[0], preferred_track_id=self.prev_lead_track_ids[0],
honda_bosch_a_radar=self.honda_bosch_a_radar) honda_bosch_a_radar=self.honda_bosch_a_radar)
self.radar_state.leadOne = self._filter_post_standstill_lead(lead_one)
self.radar_state.leadTwo = get_lead(self.v_ego, self.ready, self.tracks, leads_v3[1], model_v_ego, sm['modelV2'], self.radar_state.leadTwo = get_lead(self.v_ego, self.ready, self.tracks, leads_v3[1], model_v_ego, sm['modelV2'],
sm['carState'].standstill, sm['starpilotPlan'], self.starpilot_toggles, low_speed_override=False, sm['carState'].standstill, sm['starpilotPlan'], self.starpilot_toggles, low_speed_override=False,
g90_radar_filter=self.g90_radar_filter, lead_prob=self.lead_prob_filters[1].x, g90_radar_filter=self.g90_radar_filter, lead_prob=self.lead_prob_filters[1].x,
@@ -681,8 +616,6 @@ class RadarD:
if self.ready: if self.ready:
self.starpilot_radar_state.adjacentStopped = get_adjacent_stopped(self.tracks, sm['modelV2']) self.starpilot_radar_state.adjacentStopped = get_adjacent_stopped(self.tracks, sm['modelV2'])
self._remember_post_standstill_state(standstill, bool(self.radar_state.leadOne.status))
self.starpilot_toggles = get_starpilot_toggles(sm) self.starpilot_toggles = get_starpilot_toggles(sm)
def publish(self, pm: messaging.PubMaster): def publish(self, pm: messaging.PubMaster):
+2 -18
View File
@@ -161,7 +161,6 @@ from openpilot.selfdrive.controls.lib.latcontrol_torque import (
get_kia_carnival_friction_threshold, get_kia_carnival_friction_threshold,
get_kia_carnival_highway_transition_output_scale, get_kia_carnival_highway_transition_output_scale,
get_kia_carnival_unwind_ff_scale, get_kia_carnival_unwind_ff_scale,
get_kia_carnival_unwind_output_scale,
get_kia_stinger_2022_center_taper_scale, get_kia_stinger_2022_center_taper_scale,
get_kia_stinger_2022_friction_threshold, get_kia_stinger_2022_friction_threshold,
get_tucson_4th_gen_center_taper_scale, get_tucson_4th_gen_center_taper_scale,
@@ -743,17 +742,6 @@ class TestLatControl:
low_speed_exit = get_kia_carnival_unwind_ff_scale(0.31, 0.43, -0.88, 11.0) low_speed_exit = get_kia_carnival_unwind_ff_scale(0.31, 0.43, -0.88, 11.0)
assert low_speed_exit < 0.90 assert low_speed_exit < 0.90
def test_kia_carnival_unwind_output_scale_is_bounded_and_phase_gated(self):
steady_turn = get_kia_carnival_unwind_output_scale(0.80, 0.90, 0.60, 11.0)
clean_unwind = get_kia_carnival_unwind_output_scale(0.20, 0.20, -1.5, 11.0)
overshooting_unwind = get_kia_carnival_unwind_output_scale(0.20, 0.90, -1.5, 11.0)
high_speed_overshoot = get_kia_carnival_unwind_output_scale(0.20, 0.90, -1.5, 25.0)
assert steady_turn == pytest.approx(1.0)
assert clean_unwind == pytest.approx(1.0)
assert 0.70 < overshooting_unwind < 1.0
assert high_speed_overshoot > overshooting_unwind
def test_genesis_g90_ff_scale_curve(self): def test_genesis_g90_ff_scale_curve(self):
assert get_genesis_g90_ff_scale(0.0, 0.0, 20.0) == 1.0 assert get_genesis_g90_ff_scale(0.0, 0.0, 20.0) == 1.0
assert get_genesis_g90_ff_scale(0.5, 0.0, 20.0) > get_genesis_g90_ff_scale(-0.5, 0.0, 20.0) assert get_genesis_g90_ff_scale(0.5, 0.0, 20.0) > get_genesis_g90_ff_scale(-0.5, 0.0, 20.0)
@@ -783,13 +771,9 @@ class TestLatControl:
assert base > left_unwind > right_unwind assert base > left_unwind > right_unwind
def test_genesis_gv70_unwind_ff_scale(self): def test_genesis_gv70_unwind_ff_scale(self):
steady_unwind = get_genesis_gv70_unwind_ff_scale(-0.3, -0.3, 0.8, 15.0) assert get_genesis_gv70_unwind_ff_scale(-0.3, -0.3, 0.8, 15.0) == 1.0
assert steady_unwind < 1.0
assert get_genesis_gv70_unwind_ff_scale(-0.3, 0.1, 0.8, 15.0) == 1.0 assert get_genesis_gv70_unwind_ff_scale(-0.3, 0.1, 0.8, 15.0) == 1.0
assert get_genesis_gv70_unwind_ff_scale(-0.3, -0.3, -0.8, 15.0) == 1.0
early_unwind = get_genesis_gv70_unwind_ff_scale(-0.7, -0.6, 0.8, 15.0)
assert early_unwind < 1.0
reduced = get_genesis_gv70_unwind_ff_scale(-0.2, -1.0, 1.0, 20.0) reduced = get_genesis_gv70_unwind_ff_scale(-0.2, -1.0, 1.0, 20.0)
assert 0.6 < reduced < 1.0 assert 0.6 < reduced < 1.0
assert get_genesis_gv70_unwind_ff_scale(-0.2, -1.0, -1.0, 20.0) == 1.0 assert get_genesis_gv70_unwind_ff_scale(-0.2, -1.0, -1.0, 20.0) == 1.0
@@ -983,7 +967,7 @@ class TestLatControl:
get_genesis_g70_high_speed_transition_scale(0.0, 0.8, 65.0 * 0.44704) get_genesis_g70_high_speed_transition_scale(0.0, 0.8, 65.0 * 0.44704)
assert get_genesis_g70_high_speed_transition_scale(0.0, 0.8, 20.0 * 0.44704) > \ assert get_genesis_g70_high_speed_transition_scale(0.0, 0.8, 20.0 * 0.44704) > \
get_genesis_g70_high_speed_transition_scale(0.0, 0.8, 65.0 * 0.44704) get_genesis_g70_high_speed_transition_scale(0.0, 0.8, 65.0 * 0.44704)
assert 0.88 < get_genesis_g70_curve_unwind_output_scale(0.7, -0.5, 25.0) < 1.0 assert 0.90 < get_genesis_g70_curve_unwind_output_scale(0.7, -0.5, 25.0) < 1.0
assert get_genesis_g70_curve_unwind_output_scale(0.7, 0.5, 25.0) == 1.0 assert get_genesis_g70_curve_unwind_output_scale(0.7, 0.5, 25.0) == 1.0
assert get_genesis_g70_angle_output_scale(55.0, 1.0) > get_genesis_g70_angle_output_scale(85.0, 1.0) assert get_genesis_g70_angle_output_scale(55.0, 1.0) > get_genesis_g70_angle_output_scale(85.0, 1.0)
assert get_genesis_g70_angle_output_scale(85.0, -1.0) == pytest.approx(1.0) assert get_genesis_g70_angle_output_scale(85.0, -1.0) == pytest.approx(1.0)
-60
View File
@@ -12,8 +12,6 @@ from openpilot.selfdrive.controls.radard import (
DT_MDL, DT_MDL,
HONDA_BOSCH_A_RADAR_TS, HONDA_BOSCH_A_RADAR_TS,
RadarD, RadarD,
POST_STANDSTILL_RADAR_LEAD_PERSISTENCE_FRAMES,
post_standstill_radar_lead_is_urgent,
g90_low_speed_radar_lead_sane, g90_low_speed_radar_lead_sane,
g90_radar_lead_lateral_sane, g90_radar_lead_lateral_sane,
has_slow_radar_tracks, has_slow_radar_tracks,
@@ -108,64 +106,6 @@ class TestLeads:
assert not has_slow_radar_tracks(normal_radar) assert not has_slow_radar_tracks(normal_radar)
assert not has_slow_radar_tracks(unavailable_radar) assert not has_slow_radar_tracks(unavailable_radar)
@staticmethod
def make_radar_only_lead(track_id: int, d_rel: float = 8.0, v_rel: float = 0.0):
return {
"status": True,
"radar": True,
"modelProb": 0.0,
"radarTrackId": track_id,
"dRel": d_rel,
"vRel": v_rel,
}
def test_post_standstill_radar_lead_requires_same_track_persistence(self):
radar = RadarD()
radar._post_standstill_gate_active = True
lead = self.make_radar_only_lead(42)
assert not radar._filter_post_standstill_lead(lead)["status"]
assert not radar._filter_post_standstill_lead(lead)["status"]
assert radar._filter_post_standstill_lead(lead)["status"]
assert radar._post_standstill_candidate_frames == POST_STANDSTILL_RADAR_LEAD_PERSISTENCE_FRAMES
def test_post_standstill_radar_lead_resets_for_new_track(self):
radar = RadarD()
radar._post_standstill_gate_active = True
assert not radar._filter_post_standstill_lead(self.make_radar_only_lead(42))["status"]
assert not radar._filter_post_standstill_lead(self.make_radar_only_lead(43))["status"]
assert radar._post_standstill_candidate_frames == 1
def test_post_standstill_gate_only_arms_after_no_lead_stop(self):
radar = RadarD()
radar._remember_post_standstill_state(standstill=True, lead_status=False)
radar._prepare_post_standstill_gate(standstill=False)
assert radar._post_standstill_gate_active
radar = RadarD()
radar._remember_post_standstill_state(standstill=True, lead_status=True)
radar._prepare_post_standstill_gate(standstill=False)
assert not radar._post_standstill_gate_active
def test_post_standstill_model_lead_bypasses_gate(self):
radar = RadarD()
radar._post_standstill_gate_active = True
lead = self.make_radar_only_lead(42)
lead["modelProb"] = 0.9
assert radar._filter_post_standstill_lead(lead)["status"]
assert not radar._post_standstill_gate_active
def test_post_standstill_urgent_radar_lead_bypasses_gate(self):
radar = RadarD()
radar._post_standstill_gate_active = True
lead = self.make_radar_only_lead(42, d_rel=3.0, v_rel=-2.1)
assert post_standstill_radar_lead_is_urgent(lead)
assert radar._filter_post_standstill_lead(lead)["status"]
assert not radar._post_standstill_gate_active
@pytest.mark.skipif(platform.system() == "Darwin", reason="SocketEventHandle requires eventfd") @pytest.mark.skipif(platform.system() == "Darwin", reason="SocketEventHandle requires eventfd")
def test_radar_fault(self): def test_radar_fault(self):
# if there's no radar-related can traffic, radard should either not respond or respond with an error # if there's no radar-related can traffic, radard should either not respond or respond with an error
@@ -1,147 +0,0 @@
from types import SimpleNamespace
from openpilot.selfdrive.ui import ui_state as ui_state_module
class FakeParams:
def __init__(self, **values):
self.values = values
def get_bool(self, key, **_kwargs):
return bool(self.values.get(key, False))
def get_int(self, key, **_kwargs):
return int(self.values.get(key, 0))
class PassthroughFilter:
def update(self, value):
return value
def make_device(monkeypatch, **overrides):
values = {
"ScreenManagement": True,
"ScreenBrightness": 35,
"ScreenBrightnessOnroad": 45,
"ScreenTimeout": 30,
"ScreenTimeoutOnroad": 10,
"StandbyMode": False,
}
values.update(overrides)
state = SimpleNamespace(
ui_params=FakeParams(**values),
status=ui_state_module.UIStatus.DISENGAGED,
started=False,
ignition=False,
light_sensor=-1.0,
sm={},
)
monkeypatch.setattr(ui_state_module, "ui_state", state)
monkeypatch.setattr(ui_state_module.gui_app, "big_ui", lambda: False)
monkeypatch.setattr(ui_state_module.gui_app, "_mouse_events", [])
device = ui_state_module.Device()
device._brightness_filter = PassthroughFilter()
return device, state
def test_manual_brightness_applies_to_current_device_state(monkeypatch):
device, state = make_device(monkeypatch)
assert device._calculate_brightness() == 35
state.started = True
assert device._calculate_brightness() == 45
def test_auto_brightness_preserves_existing_behavior(monkeypatch):
device, state = make_device(monkeypatch, ScreenBrightness=101, ScreenBrightnessOnroad=101)
assert device._calculate_brightness() == ui_state_module.BACKLIGHT_OFFROAD
state.started = True
state.light_sensor = -1.0
assert device._calculate_brightness() == ui_state_module.BACKLIGHT_OFFROAD
def test_screen_management_off_ignores_custom_values(monkeypatch):
device, state = make_device(monkeypatch, ScreenManagement=False, ScreenBrightness=10, ScreenBrightnessOnroad=20,
StandbyMode=True)
assert device._calculate_brightness() == ui_state_module.BACKLIGHT_OFFROAD
state.started = True
device._interaction_time = 0
assert device._calculate_brightness() == ui_state_module.BACKLIGHT_OFFROAD
assert device.interactive_timeout == 30
def test_screen_settings_refresh_after_external_param_change(monkeypatch):
now = 100.0
monkeypatch.setattr(ui_state_module.time, "monotonic", lambda: now)
device, state = make_device(monkeypatch)
state.started = True
state.ignition = True
device._ignition = True
device._interaction_time = now + 10
state.ui_params.values["ScreenBrightnessOnroad"] = 72
state.ui_params.values["ScreenTimeoutOnroad"] = 25
now += device.SCREEN_SETTINGS_REFRESH_INTERVAL
device._refresh_screen_settings()
assert device._calculate_brightness() == 72
assert device.interactive_timeout == 25
assert device._interaction_time == now + 25
def test_standby_blanks_after_timeout_and_touch_wakes(monkeypatch):
now = 100.0
monkeypatch.setattr(ui_state_module.time, "monotonic", lambda: now)
device, state = make_device(monkeypatch, StandbyMode=True)
state.started = True
state.ignition = True
device._ignition = True
device._interaction_time = now - 1
assert device._calculate_brightness() == 0
monkeypatch.setattr(ui_state_module.gui_app, "_mouse_events", [SimpleNamespace(left_down=True)])
device._update_wakefulness()
assert device._interaction_time == now + 10
assert device._calculate_brightness() == 45
def test_hide_ui_blanks_after_timeout_and_touch_wakes(monkeypatch):
now = 100.0
monkeypatch.setattr(ui_state_module.time, "monotonic", lambda: now)
device, state = make_device(monkeypatch, ScreenBrightnessOnroad=0)
state.started = True
state.ignition = True
device._ignition = True
device._interaction_time = now - 1
assert device._calculate_brightness() == 0
monkeypatch.setattr(ui_state_module.gui_app, "_mouse_events", [SimpleNamespace(left_down=True)])
device._update_wakefulness()
assert device._interaction_time == now + 10
assert device._calculate_brightness() == 5
def test_standby_wakes_for_visible_alert(monkeypatch):
now = 100.0
monkeypatch.setattr(ui_state_module.time, "monotonic", lambda: now)
device, state = make_device(monkeypatch, StandbyMode=True)
state.started = True
state.ignition = True
device._ignition = True
device._interaction_time = now - 1
device._visible_onroad_alert = lambda: True
device._update_wakefulness()
assert device._interaction_time == now + 10
assert device._calculate_brightness() == 45
+10 -101
View File
@@ -297,8 +297,6 @@ class UIState:
class Device: class Device:
SCREEN_SETTINGS_REFRESH_INTERVAL = 1.0
def __init__(self): def __init__(self):
self._ignition = False self._ignition = False
self._interaction_time: float = -1 self._interaction_time: float = -1
@@ -308,16 +306,6 @@ class Device:
self._awake: bool = True self._awake: bool = True
self._params = ui_state.ui_params self._params = ui_state.ui_params
self._screen_settings_refresh_time: float = 0.0
self._screen_management = False
self._screen_brightness = 101
self._screen_brightness_onroad = 101
self._screen_timeout = 30
self._screen_timeout_onroad = 30
self._standby_mode = False
self._last_status = ui_state.status
self._refresh_screen_settings(force=True)
self._offroad_brightness: int = BACKLIGHT_OFFROAD self._offroad_brightness: int = BACKLIGHT_OFFROAD
self._last_brightness: int = 0 self._last_brightness: int = 0
self._brightness_filter = FirstOrderFilter(BACKLIGHT_OFFROAD, 10.00, 1 / gui_app.target_fps) self._brightness_filter = FirstOrderFilter(BACKLIGHT_OFFROAD, 10.00, 1 / gui_app.target_fps)
@@ -337,8 +325,8 @@ class Device:
if self._override_interactive_timeout is not None: if self._override_interactive_timeout is not None:
return self._override_interactive_timeout return self._override_interactive_timeout
timeout_onroad = self._screen_timeout_onroad timeout_onroad = self._params.get_int("ScreenTimeoutOnroad", return_default=True)
timeout_offroad = self._screen_timeout timeout_offroad = self._params.get_int("ScreenTimeout", return_default=True)
if timeout_onroad <= 0: if timeout_onroad <= 0:
timeout_onroad = 10 if gui_app.big_ui() else 5 timeout_onroad = 10 if gui_app.big_ui() else 5
@@ -357,54 +345,12 @@ class Device:
self._interactive_timeout_callbacks.append(callback) self._interactive_timeout_callbacks.append(callback)
def update(self): def update(self):
self._refresh_screen_settings()
# do initial reset # do initial reset
if self._interaction_time <= 0: if self._interaction_time <= 0:
self._reset_interactive_timeout() self._reset_interactive_timeout()
self._update_wakefulness()
self._update_brightness() self._update_brightness()
self._update_wakefulness()
def _refresh_screen_settings(self, force: bool = False) -> None:
now = time.monotonic()
if not force and now - self._screen_settings_refresh_time < self.SCREEN_SETTINGS_REFRESH_INTERVAL:
return
previous = (
self._screen_management,
self._screen_brightness,
self._screen_brightness_onroad,
self._screen_timeout,
self._screen_timeout_onroad,
self._standby_mode,
)
self._screen_management = self._params.get_bool("ScreenManagement")
if self._screen_management:
self._screen_brightness = min(max(self._params.get_int("ScreenBrightness", return_default=True), 0), 101)
self._screen_brightness_onroad = min(max(self._params.get_int("ScreenBrightnessOnroad", return_default=True), 0), 101)
self._screen_timeout = self._params.get_int("ScreenTimeout", return_default=True)
self._screen_timeout_onroad = self._params.get_int("ScreenTimeoutOnroad", return_default=True)
self._standby_mode = self._params.get_bool("StandbyMode")
else:
self._screen_brightness = 101
self._screen_brightness_onroad = 101
self._screen_timeout = 30
self._screen_timeout_onroad = 30
self._standby_mode = False
self._screen_settings_refresh_time = now
current = (
self._screen_management,
self._screen_brightness,
self._screen_brightness_onroad,
self._screen_timeout,
self._screen_timeout_onroad,
self._standby_mode,
)
if previous != current and self._interaction_time > 0:
self._reset_interactive_timeout()
def set_offroad_brightness(self, brightness: int | None): def set_offroad_brightness(self, brightness: int | None):
if brightness is None: if brightness is None:
@@ -412,15 +358,6 @@ class Device:
self._offroad_brightness = min(max(brightness, 0), 100) self._offroad_brightness = min(max(brightness, 0), 100)
def _update_brightness(self): def _update_brightness(self):
brightness = self._calculate_brightness()
if brightness != self._last_brightness:
if self._brightness_thread is None or not self._brightness_thread.is_alive():
self._brightness_thread = threading.Thread(target=HARDWARE.set_screen_brightness, args=(brightness,))
self._brightness_thread.start()
self._last_brightness = brightness
def _calculate_brightness(self) -> int:
clipped_brightness = self._offroad_brightness clipped_brightness = self._offroad_brightness
if ui_state.started and ui_state.light_sensor >= 0: if ui_state.started and ui_state.light_sensor >= 0:
@@ -437,26 +374,19 @@ class Device:
brightness = round(self._brightness_filter.update(clipped_brightness)) brightness = round(self._brightness_filter.update(clipped_brightness))
if not self._awake: if not self._awake:
brightness = 0 brightness = 0
elif ui_state.started and self._standby_mode and time.monotonic() > self._interaction_time:
brightness = 0
elif ui_state.started and self._screen_brightness_onroad != 101:
brightness = max(5, self._screen_brightness_onroad) if time.monotonic() <= self._interaction_time else self._screen_brightness_onroad
elif not ui_state.started and self._screen_brightness != 101:
brightness = self._screen_brightness
return brightness if brightness != self._last_brightness:
if self._brightness_thread is None or not self._brightness_thread.is_alive():
self._brightness_thread = threading.Thread(target=HARDWARE.set_screen_brightness, args=(brightness,))
self._brightness_thread.start()
self._last_brightness = brightness
def _update_wakefulness(self): def _update_wakefulness(self):
# Handle interactive timeout # Handle interactive timeout
ignition_state_changed = ui_state.ignition != self._ignition ignition_just_turned_off = not ui_state.ignition and self._ignition
self._ignition = ui_state.ignition self._ignition = ui_state.ignition
status_changed = ui_state.status != self._last_status and ui_state.status != UIStatus.OVERRIDE if ignition_just_turned_off or any(ev.left_down for ev in gui_app.mouse_events):
self._last_status = ui_state.status
wake_for_onroad_event = (ui_state.started and self._standby_mode and self._screen_brightness_onroad != 0 and
(status_changed or self._visible_onroad_alert()))
if ignition_state_changed or any(ev.left_down for ev in gui_app.mouse_events) or wake_for_onroad_event:
self._reset_interactive_timeout() self._reset_interactive_timeout()
interaction_timeout = time.monotonic() > self._interaction_time interaction_timeout = time.monotonic() > self._interaction_time
@@ -467,27 +397,6 @@ class Device:
self._set_awake(ui_state.ignition or not interaction_timeout or PC) self._set_awake(ui_state.ignition or not interaction_timeout or PC)
@staticmethod
def _visible_onroad_alert() -> bool:
if not ui_state.started:
return False
sm = ui_state.sm
try:
selfdrive_state = sm["selfdriveState"]
if selfdrive_state.alertSize != log.SelfdriveState.AlertSize.none:
return True
if selfdrive_state.alertStatus != log.SelfdriveState.AlertStatus.normal:
return True
except Exception:
pass
try:
starpilot_state = sm["starpilotSelfdriveState"]
return getattr(starpilot_state.alertSize, "raw", 0) != 0
except Exception:
return False
def _set_awake(self, on: bool): def _set_awake(self, on: bool):
if on != self._awake: if on != self._awake:
self._awake = on self._awake = on
+5 -56
View File
@@ -37,14 +37,6 @@ CURVATURE_LOOKAHEAD_MAX = 0.40
FORD_CURVATURE_LOOKAHEAD = { FORD_CURVATURE_LOOKAHEAD = {
CAR.FORD_EXPLORER_MK6: 0.20, CAR.FORD_EXPLORER_MK6: 0.20,
} }
FORD_CONSERVATIVE_PREVIEW_CARS = frozenset({
CAR.FORD_MUSTANG_MACH_E_MK1,
})
FORD_MANUAL_TURN_LATCH_CARS = frozenset({
CAR.FORD_MUSTANG_MACH_E_MK1,
})
MANUAL_TURN_ENTRY_ANGLE_DEG = 12.0
MANUAL_TURN_RECOVERY_SECONDS = 0.25
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -109,8 +101,6 @@ class FordLateralController:
self.curvature_lane_change_factor = 0.85 self.curvature_lane_change_factor = 0.85
self.human_turn = HumanTurnDetector() self.human_turn = HumanTurnDetector()
self.manual_turn_latched = False
self.manual_turn_recovery_timer = 0.0
self.curvature_samples = deque(maxlen=max(2, round(0.3 / STEER_DT))) self.curvature_samples = deque(maxlen=max(2, round(0.3 / STEER_DT)))
self.curvature_last = 0.0 self.curvature_last = 0.0
self._frame = 0 self._frame = 0
@@ -161,13 +151,8 @@ class FordLateralController:
def _current_curvature(CS) -> float: def _current_curvature(CS) -> float:
return -CS.out.yawRate / max(CS.out.vEgoRaw, 0.1) return -CS.out.yawRate / max(CS.out.vEgoRaw, 0.1)
def _blend_and_scale(self, desired: float, predicted: float, v_ego: float, current: float = 0.0) -> tuple[float, int]: def _blend_and_scale(self, desired: float, predicted: float, v_ego: float) -> tuple[float, int]:
blend = float(np.interp(abs(desired), [0.0, 0.001], [self.curvature_blend_low, self.curvature_blend_high])) blend = float(np.interp(abs(desired), [0.0, 0.001], [self.curvature_blend_low, self.curvature_blend_high]))
if self.CP.carFingerprint in FORD_CONSERVATIVE_PREVIEW_CARS:
if desired * predicted <= 0.0:
blend = 0.0
elif current * predicted > 0.0 and abs(current) > abs(desired) and abs(predicted) > abs(desired):
blend *= abs(desired) / abs(predicted)
requested = predicted * blend + desired * (1.0 - blend) requested = predicted * blend + desired * (1.0 - blend)
lane_change, direction = self._lane_change() lane_change, direction = self._lane_change()
precision = 1 precision = 1
@@ -181,62 +166,26 @@ class FordLateralController:
def _manual_turn(self, CC, CS) -> bool: def _manual_turn(self, CC, CS) -> bool:
if not CC.latActive: if not CC.latActive:
self.human_turn.reset() self.human_turn.reset()
self.manual_turn_latched = False
self.manual_turn_recovery_timer = 0.0
return False return False
detected = self.human_turn.update( return self.human_turn.update(
self.human_turn_enabled, CS.out.steeringPressed, CS.out.steeringAngleDeg) self.human_turn_enabled, CS.out.steeringPressed, CS.out.steeringAngleDeg)
if self.CP.carFingerprint not in FORD_MANUAL_TURN_LATCH_CARS:
return detected
if not self.human_turn_enabled:
self.manual_turn_latched = False
self.manual_turn_recovery_timer = 0.0
return False
blinker_direction = float(CS.out.rightBlinker) - float(CS.out.leftBlinker)
driver_turning_with_signal = (
CS.out.steeringPressed and abs(CS.out.steeringAngleDeg) >= MANUAL_TURN_ENTRY_ANGLE_DEG and
blinker_direction != 0.0 and not self._lane_change()[0] and
CS.out.steeringTorque * blinker_direction < 0.0
)
if detected or driver_turning_with_signal:
self.manual_turn_latched = True
if not self.manual_turn_latched:
self.manual_turn_recovery_timer = 0.0
return False
if CS.out.steeringPressed or blinker_direction != 0.0:
self.manual_turn_recovery_timer = 0.0
else:
self.manual_turn_recovery_timer += STEER_DT
if self.manual_turn_recovery_timer + 1e-9 >= MANUAL_TURN_RECOVERY_SECONDS:
self.manual_turn_latched = False
self.manual_turn_recovery_timer = 0.0
return self.manual_turn_latched
def update(self, CC, CS, actuators) -> FordLateralResult: def update(self, CC, CS, actuators) -> FordLateralResult:
current = self._current_curvature(CS) current = self._current_curvature(CS)
if not CC.latActive: if not CC.latActive:
self.human_turn.reset() self.human_turn.reset()
self.manual_turn_latched = False
self.manual_turn_recovery_timer = 0.0
self.curvature_samples.clear() self.curvature_samples.clear()
self.curvature_last = 0.0 self.curvature_last = 0.0
return FordLateralResult() return FordLateralResult()
manual_turn = self._manual_turn(CC, CS) if self._manual_turn(CC, CS) or CS.out.vEgoRaw < 0.1:
if manual_turn or CS.out.vEgoRaw < 0.1:
self.curvature_samples.clear() self.curvature_samples.clear()
self.curvature_last = 0.0 self.curvature_last = 0.0
return FordLateralResult(active=not ( return FordLateralResult(active=True)
manual_turn and self.CP.carFingerprint in FORD_MANUAL_TURN_LATCH_CARS))
v_ego = float(CS.out.vEgoRaw) v_ego = float(CS.out.vEgoRaw)
predicted = self._predicted_curvature(v_ego, self._curvature_lookahead()) predicted = self._predicted_curvature(v_ego, self._curvature_lookahead())
requested, precision = self._blend_and_scale(float(actuators.curvature), predicted, v_ego, current) requested, precision = self._blend_and_scale(float(actuators.curvature), predicted, v_ego)
if v_ego > 9.0: if v_ego > 9.0:
requested = float(np.clip(requested, current - CarControllerParams.CURVATURE_ERROR, requested = float(np.clip(requested, current - CarControllerParams.CURVATURE_ERROR,
+1 -119
View File
@@ -32,16 +32,12 @@ def controller(monkeypatch):
return controller return controller
def car_state(speed=15.0, curvature=0.0, steering_pressed=False, steering_angle=0.0, def car_state(speed=15.0, curvature=0.0, steering_pressed=False, steering_angle=0.0):
steering_torque=0.0, left_blinker=False, right_blinker=False):
return SimpleNamespace(out=SimpleNamespace( return SimpleNamespace(out=SimpleNamespace(
vEgoRaw=speed, vEgoRaw=speed,
yawRate=-curvature * speed, yawRate=-curvature * speed,
steeringPressed=steering_pressed, steeringPressed=steering_pressed,
steeringAngleDeg=steering_angle, steeringAngleDeg=steering_angle,
steeringTorque=steering_torque,
leftBlinker=left_blinker,
rightBlinker=right_blinker,
)) ))
@@ -118,36 +114,6 @@ def test_curvature_strategy_uses_learned_lookahead(controller, monkeypatch):
assert lookaheads == [pytest.approx(0.38)] assert lookaheads == [pytest.approx(0.38)]
def test_mach_e_preview_does_not_override_opposite_current_path(controller):
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
requested, _ = controller._blend_and_scale(-0.0001, 0.002, 20.0)
assert requested == pytest.approx(-0.0001)
def test_mach_e_preview_is_reduced_when_ahead_of_current_path(controller):
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
requested, _ = controller._blend_and_scale(0.0005, 0.002, 20.0, current=0.0015)
assert requested == pytest.approx(0.00065)
def test_mach_e_preview_remains_available_on_curve_entry(controller):
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
requested, _ = controller._blend_and_scale(0.0005, 0.002, 20.0, current=0.0002)
assert requested == pytest.approx(0.0011)
def test_non_mach_e_preview_blend_is_unchanged(controller):
requested, _ = controller._blend_and_scale(-0.0001, 0.002, 20.0, current=0.0015)
assert requested == pytest.approx(0.00074)
def test_lane_change_accepts_capnp_enum_wrappers(controller): def test_lane_change_accepts_capnp_enum_wrappers(controller):
controller.model = SimpleNamespace(meta=SimpleNamespace( controller.model = SimpleNamespace(meta=SimpleNamespace(
laneChangeState=SimpleNamespace(raw=2), laneChangeState=SimpleNamespace(raw=2),
@@ -180,87 +146,3 @@ def test_curvature_manual_turn_keeps_session_active_with_neutral_command(control
assert result.active assert result.active
assert result.curvature == 0.0 assert result.curvature == 0.0
def test_mach_e_signaled_manual_turn_yields_until_inputs_settle(controller):
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
controller.human_turn_enabled = True
CC = SimpleNamespace(latActive=True)
actuators = SimpleNamespace(curvature=0.006)
result = controller.update(CC, car_state(
steering_pressed=True, steering_angle=-15.0, steering_torque=-2.0,
right_blinker=True), actuators)
assert not result.active
assert result.curvature == 0.0
result = controller.update(CC, car_state(
steering_pressed=True, steering_angle=5.0, steering_torque=2.0,
right_blinker=True), actuators)
assert not result.active
for _ in range(4):
result = controller.update(CC, car_state(), actuators)
assert not result.active
result = controller.update(CC, car_state(), actuators)
assert result.active
assert result.curvature > 0.0
def test_mach_e_opposite_blinker_correction_does_not_start_manual_turn(controller):
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
result = controller.update(
SimpleNamespace(latActive=True),
car_state(steering_pressed=True, steering_angle=-15.0, steering_torque=2.0,
right_blinker=True),
SimpleNamespace(curvature=0.001),
)
assert result.active
def test_mach_e_lane_change_nudge_does_not_start_manual_turn(controller):
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
controller.model = SimpleNamespace(
orientationRate=SimpleNamespace(z=[0.0] * 33),
meta=SimpleNamespace(
laneChangeState=SimpleNamespace(raw=2),
laneChangeDirection=SimpleNamespace(raw=2),
),
)
result = controller.update(
SimpleNamespace(latActive=True),
car_state(steering_pressed=True, steering_angle=-15.0, steering_torque=-2.0,
right_blinker=True),
SimpleNamespace(curvature=0.001),
)
assert result.active
def test_mach_e_small_blinker_nudge_does_not_start_manual_turn(controller):
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
result = controller.update(
SimpleNamespace(latActive=True),
car_state(steering_pressed=True, steering_angle=-5.0, steering_torque=-2.0,
right_blinker=True),
SimpleNamespace(curvature=0.001),
)
assert result.active
def test_mach_e_manual_turn_latch_resets_with_lateral_control(controller):
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
actuators = SimpleNamespace(curvature=0.001)
turning = car_state(
steering_pressed=True, steering_angle=-15.0, steering_torque=-2.0,
right_blinker=True)
assert not controller.update(SimpleNamespace(latActive=True), turning, actuators).active
assert not controller.update(SimpleNamespace(latActive=False), turning, actuators).active
assert controller.update(SimpleNamespace(latActive=True), car_state(), actuators).active
+4 -4
View File
@@ -16,7 +16,7 @@ from cereal import car, custom, log
from opendbc.car import gen_empty_fingerprint from opendbc.car import gen_empty_fingerprint
from opendbc.car.car_helpers import interfaces from opendbc.car.car_helpers import interfaces
from opendbc.car.chrysler.values import JEEPS as CHRYSLER_JEEPS from opendbc.car.chrysler.values import JEEPS as CHRYSLER_JEEPS
from opendbc.car.gm.values import CAR as GM_CAR, EV_CAR as GM_EV_CAR, GM_AUTO_HOLD_CARS, GMFlags from opendbc.car.gm.values import CAR as GM_CAR, EV_CAR as GM_EV_CAR, GMFlags
from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR, EV_CAR as HYUNDAI_EV_CAR, HyundaiFlags, HyundaiStarPilotSafetyFlags from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR, EV_CAR as HYUNDAI_EV_CAR, HyundaiFlags, HyundaiStarPilotSafetyFlags
from opendbc.car.interfaces import TORQUE_SUBSTITUTE_PATH, CarInterfaceBase, GearShifter from opendbc.car.interfaces import TORQUE_SUBSTITUTE_PATH, CarInterfaceBase, GearShifter
from opendbc.car.mock.values import CAR as MOCK from opendbc.car.mock.values import CAR as MOCK
@@ -1406,7 +1406,7 @@ class StarPilotVariables:
screen_management = self.get_value("ScreenManagement") screen_management = self.get_value("ScreenManagement")
toggle.screen_brightness = max(self.get_value("ScreenBrightness", cast=float, condition=screen_management), 1) toggle.screen_brightness = max(self.get_value("ScreenBrightness", cast=float, condition=screen_management), 1)
toggle.screen_brightness_onroad = self.get_value("ScreenBrightnessOnroad", cast=float, condition=(screen_management and not toggle.force_onroad), min=0) toggle.screen_brightness_onroad = self.get_value("ScreenBrightnessOnroad", cast=float, condition=(screen_management and not toggle.force_onroad), min=1)
toggle.screen_recorder = self.get_value("ScreenRecorder", condition=screen_management) or toggle.debug_mode toggle.screen_recorder = self.get_value("ScreenRecorder", condition=screen_management) or toggle.debug_mode
toggle.screen_timeout = self.get_value("ScreenTimeout", cast=float, condition=screen_management) toggle.screen_timeout = self.get_value("ScreenTimeout", cast=float, condition=screen_management)
toggle.screen_timeout_onroad = self.get_value("ScreenTimeoutOnroad", cast=float, condition=screen_management) toggle.screen_timeout_onroad = self.get_value("ScreenTimeoutOnroad", cast=float, condition=screen_management)
@@ -1600,9 +1600,9 @@ class StarPilotVariables:
) )
toggle.remote_start_boots_comma = self.get_value("RemoteStartBootsComma", condition=toggle.car_make == "gm") toggle.remote_start_boots_comma = self.get_value("RemoteStartBootsComma", condition=toggle.car_make == "gm")
gm_auto_hold_supported = toggle.car_model in GM_AUTO_HOLD_CARS gm_auto_hold_supported = toggle.car_model in LEGACY_VOLT_STOCK_ACC_CARS
toggle.gm_auto_hold = self.get_value("GMAutoHold", condition=gm_auto_hold_supported) toggle.gm_auto_hold = self.get_value("GMAutoHold", condition=gm_auto_hold_supported)
toggle.volt_one_pedal_mode = self.get_value("VoltOnePedalMode", condition=toggle.car_model in LEGACY_VOLT_STOCK_ACC_CARS) toggle.volt_one_pedal_mode = self.get_value("VoltOnePedalMode", condition=gm_auto_hold_supported)
toggle.volt_sng = self.get_value("VoltSNG", condition=toggle.car_model in LEGACY_VOLT_STOCK_ACC_CARS) toggle.volt_sng = self.get_value("VoltSNG", condition=toggle.car_model in LEGACY_VOLT_STOCK_ACC_CARS)
@@ -34,7 +34,7 @@ const VEHICLE_SETTING_MAKES = {
RemoteStartBootsComma: GM_MAKES, RemoteStartBootsComma: GM_MAKES,
HKGRemoteStartBootsComma: HKG_MAKES, HKGRemoteStartBootsComma: HKG_MAKES,
VoltSNG: ["Chevrolet", "Holden"], VoltSNG: ["Chevrolet", "Holden"],
GMAutoHold: ["Buick", "Chevrolet", "Holden"], GMAutoHold: ["Chevrolet", "Holden"],
VoltOnePedalMode: ["Chevrolet", "Holden"], VoltOnePedalMode: ["Chevrolet", "Holden"],
RemapCancelToDistance: ["Chevrolet", "Holden"], RemapCancelToDistance: ["Chevrolet", "Holden"],
JeepBrakeHold: ["Jeep"], JeepBrakeHold: ["Jeep"],
@@ -490,7 +490,7 @@ function numericBounds(param) {
return { min: 1, max: 101, step: 1 } return { min: 1, max: 101, step: 1 }
} }
if (param.key === "ScreenBrightnessOnroad") { if (param.key === "ScreenBrightnessOnroad") {
return { min: 0, max: 101, step: 1 } return { min: 1, max: 101, step: 1 }
} }
if (param.key === "LaneCenterOffset") { if (param.key === "LaneCenterOffset") {
@@ -36,7 +36,6 @@
--glass-border-light: rgba(255, 255, 255, 0.2); --glass-border-light: rgba(255, 255, 255, 0.2);
--glass-shadow: 0 8px 24px 0 rgba(0, 0, 0, 0.35); --glass-shadow: 0 8px 24px 0 rgba(0, 0, 0, 0.35);
--glass-blur: blur(4px); --glass-blur: blur(4px);
--card-scroll-bg: rgba(22, 18, 38, 0.95);
--glass-text: #f3f2f8; --glass-text: #f3f2f8;
--glass-accent: #9d72ff; --glass-accent: #9d72ff;
@@ -106,7 +105,6 @@
--surface-container: rgba(255, 255, 255, 0.75); --surface-container: rgba(255, 255, 255, 0.75);
--surface-container-high: rgba(255, 255, 255, 0.95); --surface-container-high: rgba(255, 255, 255, 0.95);
--surface-container-low: rgba(240, 243, 252, 0.75); --surface-container-low: rgba(240, 243, 252, 0.75);
--card-scroll-bg: rgba(245, 246, 252, 0.96);
--on-surface: #171526; --on-surface: #171526;
--on-surface-variant: #5f5b7d; --on-surface-variant: #5f5b7d;
@@ -188,8 +186,6 @@ body {
pointer-events: none; pointer-events: none;
transform: translateZ(0); transform: translateZ(0);
will-change: transform; will-change: transform;
isolation: isolate;
contain: strict;
} }
#galaxy-bg::before { #galaxy-bg::before {
@@ -208,6 +204,7 @@ body {
opacity: 0.5; opacity: 0.5;
will-change: transform; will-change: transform;
transform: translate3d(var(--gx-para-x), 0, 0); transform: translate3d(var(--gx-para-x), 0, 0);
animation: galaxy-drift-smooth 160s linear infinite;
} }
#galaxy-bg::after { #galaxy-bg::after {
@@ -224,6 +221,7 @@ body {
opacity: 0.6; opacity: 0.6;
will-change: transform; will-change: transform;
transform: translate3d(var(--gx-para-x), 0, 0); transform: translate3d(var(--gx-para-x), 0, 0);
animation: galaxy-drift-smooth 100s linear infinite;
} }
#galaxy-bg .galaxy-hero { #galaxy-bg .galaxy-hero {
@@ -236,6 +234,11 @@ body {
animation: galaxy-twinkle 6s ease-in-out infinite alternate; animation: galaxy-twinkle 6s ease-in-out infinite alternate;
} }
@keyframes galaxy-drift-smooth {
from { transform: translate3d(0, 0, 0); }
to { transform: translate3d(0, -400px, 0); }
}
@keyframes galaxy-twinkle { @keyframes galaxy-twinkle {
0%, 100% { opacity: 0.3; transform: scale(0.9); } 0%, 100% { opacity: 0.3; transform: scale(0.9); }
50% { opacity: 1; transform: scale(1.2); } 50% { opacity: 1; transform: scale(1.2); }
@@ -247,11 +250,6 @@ body {
} }
} }
/* Keep decorative animation out of modal backdrops and drawer content. */
body:has(.gx-scrim, .gx-underlay, .gx-drawer.open) #galaxy-bg .galaxy-hero {
display: none;
}
[data-theme="light"] #galaxy-bg { [data-theme="light"] #galaxy-bg {
background-color: #eef1fb; background-color: #eef1fb;
background-image: background-image:
@@ -308,8 +306,6 @@ ul { list-style: none; margin: 0; padding: 0; }
position: sticky; position: sticky;
top: 0; top: 0;
z-index: var(--z-appbar); z-index: var(--z-appbar);
transform: translateZ(0);
isolation: isolate;
} }
.gx-appbar__pill { .gx-appbar__pill {
@@ -329,8 +325,7 @@ ul { list-style: none; margin: 0; padding: 0; }
transition: background-color var(--motion-fast), border-color var(--motion-fast); transition: background-color var(--motion-fast), border-color var(--motion-fast);
} }
.gx-appbar .gx-appbar__back, .gx-appbar__back {
.gx-appbar .gx-theme-toggle {
background: var(--glass-bg); background: var(--glass-bg);
backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur); -webkit-backdrop-filter: var(--glass-blur);
@@ -344,8 +339,7 @@ ul { list-style: none; margin: 0; padding: 0; }
transition: transform var(--motion-fast), background-color var(--motion-fast); transition: transform var(--motion-fast), background-color var(--motion-fast);
} }
.gx-appbar .gx-appbar__back:active, .gx-appbar__back:active { transform: scale(0.92); }
.gx-appbar .gx-theme-toggle:active { transform: scale(0.92); }
.gx-appbar__title { .gx-appbar__title {
background: linear-gradient(135deg, #8b6cc5 0%, #5ec8c8 55%, #d4789c 100%); background: linear-gradient(135deg, #8b6cc5 0%, #5ec8c8 55%, #d4789c 100%);
@@ -364,24 +358,6 @@ ul { list-style: none; margin: 0; padding: 0; }
vertical-align: middle; vertical-align: middle;
} }
.gx-appbar__home {
display: inline-flex;
align-items: center;
cursor: pointer;
padding: 2px 8px;
border-radius: var(--radius-full);
transition: background-color var(--motion-fast), transform var(--motion-fast);
}
.gx-appbar__home:hover { background-color: var(--glass-bg-hover); }
.gx-appbar__home:active { transform: scale(0.96); }
.gx-appbar__home:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
.gx-appbar__right { .gx-appbar__right {
align-items: center; align-items: center;
display: flex; display: flex;
@@ -435,6 +411,27 @@ ul { list-style: none; margin: 0; padding: 0; }
.gx-back-btn { display: none; } .gx-back-btn { display: none; }
.gx-menu-btn { display: inline-flex; } .gx-menu-btn { display: inline-flex; }
.gx-theme-toggle {
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border: 1px solid var(--glass-border);
border-radius: 50%;
box-shadow: var(--glass-shadow);
color: var(--glass-text);
flex: none;
height: var(--touch-target);
width: var(--touch-target);
transition: transform var(--motion-fast), background-color var(--motion-fast), border-color var(--motion-fast);
}
.gx-theme-toggle:active { transform: scale(0.92); }
[data-theme="light"] .gx-theme-toggle {
border: 1.5px solid var(--primary);
box-shadow: 0 2px 8px rgba(120, 73, 232, 0.2);
}
.gx-searchwrap { .gx-searchwrap {
align-items: center; align-items: center;
display: flex; display: flex;
@@ -516,10 +513,7 @@ ul { list-style: none; margin: 0; padding: 0; }
position: fixed; position: fixed;
bottom: calc(18px + env(safe-area-inset-bottom, 0px)); bottom: calc(18px + env(safe-area-inset-bottom, 0px));
left: 50%; left: 50%;
transform: translateX(-50%) translateZ(0); transform: translateX(-50%);
will-change: transform;
isolation: isolate;
contain: layout style;
z-index: 1000; z-index: 1000;
display: flex; display: flex;
@@ -595,37 +589,7 @@ ul { list-style: none; margin: 0; padding: 0; }
box-shadow: var(--elev-1); box-shadow: var(--elev-1);
margin-bottom: var(--sp-4); margin-bottom: var(--sp-4);
overflow: hidden; overflow: hidden;
transition: transform var(--motion-fast), box-shadow var(--motion-fast), background-color 0.15s ease-out; transition: transform var(--motion-fast), box-shadow var(--motion-fast);
}
/* Surgical content-visibility for top-level non-collapsible cards */
.dh-card:not(.dh-stat),
.gx-content > div > .gx-card {
content-visibility: auto;
contain-intrinsic-size: 280px;
contain-intrinsic-size: auto 280px;
}
.dh-stat {
content-visibility: auto;
contain-intrinsic-size: 110px;
contain-intrinsic-size: auto 110px;
}
/* Disarm containment on collapsible sections and nested cards */
.gx-card:has(.gx-section__body),
.gx-card .gx-card,
.gx-tree-node .gx-card {
content-visibility: visible !important;
contain-intrinsic-size: none !important;
}
/* Velocity-Adaptive Glass: bypass blur on scrolling cards with contrast boost */
body.is-scrolling .gx-card,
body.is-scrolling .gx-tile {
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
background: var(--card-scroll-bg) !important;
} }
.gx-unit-note { .gx-unit-note {
@@ -1495,9 +1459,6 @@ input[type="color"].gx-color {
.gx-fade-enter-active, .gx-fade-leave-active { transition: opacity var(--motion-base); } .gx-fade-enter-active, .gx-fade-leave-active { transition: opacity var(--motion-base); }
.gx-fade-enter-from, .gx-fade-leave-to { opacity: 0; } .gx-fade-enter-from, .gx-fade-leave-to { opacity: 0; }
/* Keep the underlay (and star suppression) until the drawer finishes closing. */
.gx-underlay.gx-fade-leave-active { transition: opacity var(--motion-slow); }
.gx-slide-enter-active, .gx-slide-leave-active { .gx-slide-enter-active, .gx-slide-leave-active {
transition: transform var(--motion-base), opacity var(--motion-base); transition: transform var(--motion-base), opacity var(--motion-base);
} }
@@ -90,71 +90,6 @@ app.mount("#galaxy-app")
initRouter() initRouter()
// Disable card blur during document scrolling.
;(() => {
let timer = null
let scrollEnded = true
const touches = new Set()
const nativeScrollEnd = "onscrollend" in document
const body = document.body
const isModalEvent = (e) => {
const t = e.target
return t instanceof Element && t.closest(".gx-scrim, .gx-sheet, .gx-dialog, .gx-drawer") !== null
}
const setScrolling = (active) => {
if (active) {
if (!body.classList.contains("is-scrolling")) body.classList.add("is-scrolling")
} else if (!touches.size && body.classList.contains("is-scrolling")) {
body.classList.remove("is-scrolling")
}
}
const scheduleRestore = () => {
clearTimeout(timer)
const scrollY = window.scrollY
// Finger release and completion notifications can precede the last movement.
// Keep glass disabled until the scroll position has also settled.
timer = setTimeout(() => {
if (window.scrollY !== scrollY) scheduleRestore()
else setScrolling(false)
}, 120)
}
document.addEventListener("scroll", () => {
scrollEnded = false
setScrolling(true)
clearTimeout(timer)
if (!nativeScrollEnd) scheduleRestore()
}, { passive: true })
document.addEventListener("scrollend", () => {
scrollEnded = true
scheduleRestore()
}, { passive: true })
window.addEventListener("touchstart", (e) => {
if (isModalEvent(e)) return
for (const touch of e.changedTouches) touches.add(touch.identifier)
}, { passive: true })
const releaseTouches = (e) => {
for (const touch of e.changedTouches) touches.delete(touch.identifier)
if (!touches.size && (scrollEnded || !nativeScrollEnd)) scheduleRestore()
}
window.addEventListener("touchend", releaseTouches, { passive: true })
window.addEventListener("touchcancel", releaseTouches, { passive: true })
window.addEventListener("hashchange", () => {
scrollEnded = true
touches.clear()
clearTimeout(timer)
body.classList.remove("is-scrolling")
}, { passive: true })
})()
// Layer 2: Ambient Hero Stars Spawner
;(() => { ;(() => {
const bg = document.getElementById("galaxy-bg") const bg = document.getElementById("galaxy-bg")
if (!bg) return if (!bg) return
@@ -81,9 +81,6 @@ export const AppShell = {
bottomNavTo(item) { bottomNavTo(item) {
navigate(item.link) navigate(item.link)
}, },
goHome() {
navigate("/")
},
isActive(link) { isActive(link) {
return this.activePath === link || (link !== "/" && this.activePath.startsWith(link)) return this.activePath === link || (link !== "/" && this.activePath.startsWith(link))
}, },
@@ -105,10 +102,7 @@ export const AppShell = {
<button type="button" class="gx-icon-btn gx-menu-btn" aria-label="Menu" @click="store.drawerOpen = true"> <button type="button" class="gx-icon-btn gx-menu-btn" aria-label="Menu" @click="store.drawerOpen = true">
<i class="bi bi-list"></i> <i class="bi bi-list"></i>
</button> </button>
<span class="gx-appbar__home" role="button" tabindex="0" <span class="gx-appbar__title">Galaxy</span>
aria-label="Galaxy home" @click="goHome" @keydown.enter="goHome" @keydown.space.prevent="goHome">
<span class="gx-appbar__title">Galaxy</span>
</span>
<div class="gx-searchwrap"> <div class="gx-searchwrap">
<input ref="searchInput" class="gx-search gx-appbar__search" type="search" placeholder="Search toggles..." <input ref="searchInput" class="gx-search gx-appbar__search" type="search" placeholder="Search toggles..."
v-model="search" aria-label="Search toggles" /> v-model="search" aria-label="Search toggles" />
@@ -17,9 +17,9 @@ export const DevModeBanner = {
<i class="bi bi-shield-lock gx-alert__icon"></i> <i class="bi bi-shield-lock gx-alert__icon"></i>
<div class="gx-alert__body"> <div class="gx-alert__body">
<strong>{{ hiddenCount }} advanced setting{{ hiddenCount !== 1 ? "s" : "" }} hidden.</strong> <strong>{{ hiddenCount }} advanced setting{{ hiddenCount !== 1 ? "s" : "" }} hidden.</strong>
<span>Want more advanced features or missing a few toggles? Enable Developer Mode whenever youre ready.</span> <span>Advanced features are tucked away until you enable Developer Mode.</span>
</div> </div>
<button type="button" class="gx-btn gx-btn--tonal" @click="unlock">Go to Developer Tab</button> <button type="button" class="gx-btn gx-btn--tonal" @click="unlock">Enable Developer Mode</button>
</div> </div>
`, `,
} }
@@ -62,7 +62,7 @@ const VEHICLE_SETTING_MAKES = {
RemoteStartBootsComma: ["Buick", "Cadillac", "Chevrolet", "GMC", "Holden"], RemoteStartBootsComma: ["Buick", "Cadillac", "Chevrolet", "GMC", "Holden"],
HKGRemoteStartBootsComma: ["Genesis", "Hyundai", "Kia"], HKGRemoteStartBootsComma: ["Genesis", "Hyundai", "Kia"],
VoltSNG: ["Chevrolet", "Holden"], VoltSNG: ["Chevrolet", "Holden"],
GMAutoHold: ["Buick", "Chevrolet", "Holden"], GMAutoHold: ["Chevrolet", "Holden"],
VoltOnePedalMode: ["Chevrolet", "Holden"], VoltOnePedalMode: ["Chevrolet", "Holden"],
RemapCancelToDistance: ["Chevrolet", "Holden"], RemapCancelToDistance: ["Chevrolet", "Holden"],
JeepBrakeHold: ["Jeep"], JeepBrakeHold: ["Jeep"],
@@ -140,12 +140,9 @@ export function numericBounds(param, values = {}) {
const n = Number(value) const n = Number(value)
return Number.isFinite(n) ? n : null return Number.isFinite(n) ? n : null
} }
if (param.key === "ScreenBrightness") { if (param.key === "ScreenBrightness" || param.key === "ScreenBrightnessOnroad") {
return { min: 1, max: 101, step: 1 } return { min: 1, max: 101, step: 1 }
} }
if (param.key === "ScreenBrightnessOnroad") {
return { min: 0, max: 101, step: 1 }
}
if (param.key === "LaneCenterOffset") { if (param.key === "LaneCenterOffset") {
return { min: -0.3, max: 0.3, step: 0.01 } return { min: -0.3, max: 0.3, step: 0.01 }
} }

Some files were not shown because too many files have changed in this diff Show More