This commit is contained in:
firestar5683
2026-08-12 23:29:46 -05:00
parent 1bf2717626
commit 682b4b5c92
69 changed files with 392 additions and 211 deletions
@@ -413,6 +413,13 @@ def preserve_stock_canfd_lfa_status(car_fingerprint) -> bool:
return car_fingerprint != CAR.KIA_CARNIVAL_4TH_GEN
def suppress_redundant_gv70_brake_cancel(CP, brake_pressed: bool, lat_active: bool) -> bool:
return bool(
CP.carFingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN and
not CP.openpilotLongitudinalControl and brake_pressed and lat_active
)
class CarController(CarControllerBase):
def __init__(self, dbc_names, CP):
super().__init__(dbc_names, CP)
@@ -992,7 +999,10 @@ class CarController(CarControllerBase):
if (self.frame - self.last_button_frame) * DT_CTRL > 0.25:
# cruise cancel - suppress when stock ACC is the fallback (ECU disable failed),
# so openpilot doesn't fight/cancel the user's stock cruise
if CC.cruiseControl.cancel and not self.ecu_disable_failed:
suppress_brake_cancel = suppress_redundant_gv70_brake_cancel(
self.CP, CS.out.brakePressed, CC.latActive,
)
if CC.cruiseControl.cancel and not self.ecu_disable_failed and not suppress_brake_cancel:
if self.CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS:
can_sends.append(hyundaicanfd.create_acc_cancel(self.packer, self.CP, self.CAN, CS.cruise_info))
self.last_button_frame = self.frame
@@ -18,7 +18,8 @@ from opendbc.car.hyundai.carcontroller import CarController, Ioniq6LongitudinalT
direct_angle_request_allowed, get_angle_smoothing_alpha, \
should_use_ev6_gt_line_stop_direct_tracking, \
should_track_stop_accel_directly_for_car, \
preserve_stock_canfd_lfa_status
preserve_stock_canfd_lfa_status, \
suppress_redundant_gv70_brake_cancel
from opendbc.car.hyundai.carstate import CarState, decode_canfd_camera_lead, decode_ioniq_6_blindspot_radar_state, \
get_canfd_cruise_available
from opendbc.car.hyundai.interface import CarInterface, KIA_EV9_ACCEL_MAX
@@ -142,11 +143,18 @@ class TestHyundaiFingerprint:
lfa_msg = hyundaicanfd.create_steering_messages(packer, CP, can_bus, False, False, 0, 0.0, lfa_base)[0]
assert lfa_msg[1] == bytes.fromhex("05100002400008000000000000640000")
active_lfa_msg = hyundaicanfd.create_steering_messages(packer, CP, can_bus, True, True, 100, 0.0, None,
lka_icon=2)[0]
assert active_lfa_msg[1] == bytes.fromhex("9a17010280c818000000000000640000")
stock_cluster = {"NEW_SIGNAL_5": 1}
cluster_base = stock_cluster if preserve_stock_canfd_lfa_status(CP.carFingerprint) else None
cluster_msg = hyundaicanfd.create_lfahda_cluster(packer, can_bus, False, cluster_base)
assert cluster_msg[1] == bytes.fromhex("8e040000000000000000000000000000")
active_cluster_msg = hyundaicanfd.create_lfahda_cluster(packer, can_bus, True, None, lfa_icon=2)
assert active_cluster_msg[1] == bytes.fromhex("cdfb0180000001000000000000000000")
def test_canfd_torque_bsm_parser_registers_rear_blindspots(self):
CP = CarParams.new_message()
CP.carFingerprint = CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN
@@ -576,6 +584,21 @@ class TestHyundaiFingerprint:
palisade_2023 = CarInterface.get_params(CAR.HYUNDAI_PALISADE_2023, gen_empty_fingerprint(), [], True, False, False, None)
assert palisade_2023.safetyConfigs[-1].safetyParam & HyundaiStarPilotSafetyFlags.HAS_LDA_BUTTON
def test_carnival_lka_button_does_not_enable_angle_steering_safety(self):
fingerprint = gen_empty_fingerprint()
fingerprint[0][0x391] = 8
toggles = SimpleNamespace(always_on_lateral_lkas=True)
CP = CarInterface.get_params(CAR.KIA_CARNIVAL_4TH_GEN, fingerprint, [], True, False, False, toggles)
FPCP = CarInterface.get_starpilot_params(CAR.KIA_CARNIVAL_4TH_GEN, fingerprint, [], CP, toggles)
combined_safety_param = CP.safetyConfigs[-1].safetyParam | FPCP.safetyConfigs[-1].safetyParam
assert CP.steerControlType == CarParams.SteerControlType.torque
assert not (CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING)
assert not (combined_safety_param & HyundaiSafetyFlags.CANFD_ANGLE_STEERING)
assert combined_safety_param & HyundaiSafetyFlags.LONG
assert combined_safety_param & HyundaiStarPilotSafetyFlags.AOL_LKAS_ON_ENGAGE
def test_sonata_hybrid_aol_main_lkas_sync_is_scoped(self):
toggles = SimpleNamespace(always_on_lateral_lkas=True, main_cruise_aol_toggle=True)
@@ -2034,6 +2057,22 @@ class TestHyundaiFingerprint:
assert parser.vl["SCC_CONTROL"]["aReqValue"] == pytest.approx(-0.1)
assert parser.vl["SCC_CONTROL"]["aReqRaw"] == pytest.approx(-1.0)
def test_gv70_electrified_suppresses_only_stock_scc_brake_cancel(self):
CP = CarParams.new_message()
CP.carFingerprint = CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN
CP.openpilotLongitudinalControl = False
assert suppress_redundant_gv70_brake_cancel(CP, brake_pressed=True, lat_active=True)
assert not suppress_redundant_gv70_brake_cancel(CP, brake_pressed=False, lat_active=True)
assert not suppress_redundant_gv70_brake_cancel(CP, brake_pressed=True, lat_active=False)
CP.openpilotLongitudinalControl = True
assert not suppress_redundant_gv70_brake_cancel(CP, brake_pressed=True, lat_active=True)
CP.carFingerprint = CAR.HYUNDAI_IONIQ_6
CP.openpilotLongitudinalControl = False
assert not suppress_redundant_gv70_brake_cancel(CP, brake_pressed=True, lat_active=True)
def test_ev9_inactive_angle_steering_lets_safety_forward_stock_lkas(self):
CP = CarParams.new_message()
CP.carFingerprint = CAR.KIA_EV9
+1 -1
View File
@@ -248,7 +248,7 @@ class CarInterfaceBase(ABC):
fp_ret.pcmCruiseSpeed = False
CP.openpilotLongitudinalControl = True
hyundai_has_lda_button = (
hyundai_has_lda_button = not (CP.flags & HyundaiFlags.CANFD) and (
0x391 in fingerprint[0] or
0x50C in fingerprint[0] or
candidate in ALT_BUS_LDA_BUTTON_CARS or
@@ -226,23 +226,23 @@ def test_legacy_2025_uses_validated_angle_request_limits():
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(CS.out.steeringAngleDeg)
def test_ascent_2023_uses_d_platform_bus_layout():
def test_ascent_2023_uses_gen2_angle_bus_layout():
CP = CarInterface.get_non_essential_params(CAR.SUBARU_ASCENT_2023)
parsers = CarState.get_can_parsers(CP)
controller = CarController({}, CP)
assert CP.flags & SubaruFlags.D_PLATFORM
assert CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.D_PLATFORM
assert CP.flags & SubaruFlags.D_PLATFORM_CAMERA
assert CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.D_PLATFORM_CAMERA
assert CanBus.main_for_cp(CP) == CanBus.alt
assert CanBus.angle_for_cp(CP) == CanBus.camera
assert parsers[Bus.pt].bus == CanBus.alt
assert not (CP.flags & SubaruFlags.D_PLATFORM)
assert not (CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.D_PLATFORM)
assert not (CP.flags & SubaruFlags.D_PLATFORM_CAMERA)
assert not (CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.D_PLATFORM_CAMERA)
assert CanBus.main_for_cp(CP) == CanBus.main
assert CanBus.angle_for_cp(CP) == CanBus.main
assert parsers[Bus.pt].bus == CanBus.main
assert parsers[Bus.cam].bus == CanBus.camera
assert parsers[Bus.alt].bus == CanBus.alt
assert parsers[Bus.main].bus == CanBus.main
assert controller.angle_bus == CanBus.camera
assert controller.status_bus == CanBus.camera
assert Bus.main not in parsers
assert controller.angle_bus == CanBus.main
assert controller.status_bus == CanBus.main
def test_other_angle_platforms_keep_existing_bus_layout():
+1 -1
View File
@@ -253,7 +253,7 @@ class CAR(Platforms):
SUBARU_ASCENT_2023 = SubaruGen2PlatformConfig(
[SubaruCarDocs("Subaru Ascent 2023-25", "All", car_parts=CarParts.common([CarHarness.subaru_d]))],
SUBARU_ASCENT.specs,
flags=SubaruFlags.LKAS_ANGLE | SubaruFlags.D_PLATFORM | SubaruFlags.D_PLATFORM_CAMERA,
flags=SubaruFlags.LKAS_ANGLE,
)
SUBARU_CROSSTREK_2025 = SubaruGen2PlatformConfig(
[SubaruCarDocs("Subaru Crosstrek 2025", "All", car_parts=CarParts.common([CarHarness.subaru_d]))],
+4 -2
View File
@@ -67,8 +67,10 @@ class CarInterface(CarInterfaceBase):
# These messages are normally absent there on pre-TSS2 platforms.
camera_fingerprint = fingerprint.get(2, {})
has_dsu_bypass = 0x343 in camera_fingerprint or 0x4CB in camera_fingerprint
if candidate == CAR.LEXUS_IS:
# The IS mirrors its native buses onto camera bus during startup without a bypass adapter.
late_prius_camera = candidate == CAR.TOYOTA_PRIUS and any(
fw.ecu == Ecu.fwdCamera and bytes(fw.fwVersion).startswith(b'8646F4705') for fw in car_fw
)
if candidate == CAR.LEXUS_IS or late_prius_camera:
has_dsu_bypass = ((0x343 in camera_fingerprint and 0x343 not in fingerprint.get(1, {})) or
(0x4CB in camera_fingerprint and 0x4CB not in fingerprint.get(0, {})))
if not use_sdsu and candidate not in TSS2_CAR and has_dsu_bypass:
@@ -258,6 +258,43 @@ class TestToyotaInterfaces:
assert not car_params.safetyConfigs[0].safetyParam & ToyotaSafetyFlags.STOCK_LONGITUDINAL.value
assert not car_params.safetyConfigs[0].safetyParam & ToyotaSafetyFlags.ALT_CRUISE.value
@pytest.mark.parametrize(("native_bus", "message"), [(1, 0x343), (0, 0x4CB)])
def test_late_prius_ignores_startup_bus_mirror(self, native_bus, message):
fingerprint = {bus: {} for bus in range(8)}
fingerprint[native_bus][message] = 8
fingerprint[2][message] = 8
car_fw = [CarParams.CarFw(
ecu=Ecu.fwdCamera,
address=0x750,
subAddress=0x6D,
fwVersion=b'8646F4705200\x00\x00\x00\x00',
)]
car_params = CarInterface.get_params(
CAR.TOYOTA_PRIUS,
fingerprint,
car_fw,
alpha_long=False,
is_release=False,
docs=False,
starpilot_toggles=SimpleNamespace(),
)
assert not car_params.flags & ToyotaFlags.DSU_BYPASS.value
assert not car_params.openpilotLongitudinalControl
assert car_params.safetyConfigs[0].safetyParam & ToyotaSafetyFlags.STOCK_LONGITUDINAL.value
starpilot_params = CarInterface.get_starpilot_params(
CAR.TOYOTA_PRIUS, fingerprint, car_fw, car_params, SimpleNamespace(),
)
car_state = CarState(car_params, starpilot_params)
can_parsers = car_state.get_can_parsers(car_params)
car_state.update(can_parsers, SimpleNamespace(cluster_offset=1.0))
assert "PRE_COLLISION" in can_parsers[Bus.pt].vl
for acc_message in ("ACC_CONTROL", "PRE_COLLISION", "PCS_HUD"):
assert acc_message not in can_parsers[Bus.cam].vl
def test_dsu_bypass_does_not_change_tss2_or_smart_dsu(self):
fingerprint = {bus: {} for bus in range(8)}
fingerprint[0][0x2FF] = 8
@@ -365,6 +365,9 @@ class TestSubaruGen2Legacy2025AngleSafety(TestSubaruGen2AngleStockLongitudinalSa
ANGLE_RATE_UP = [5., .8, .15]
ANGLE_RATE_DOWN = [5., .8, .15]
def test_rt_limits(self):
raise unittest.SkipTest("Breakpoint angle limits do not enforce a real-time message frequency")
class TestSubaruDPlatformAngleSafety(TestSubaruStockLongitudinalSafetyBase, TestSubaruAngleSafetyBase):
FLAGS = SubaruSafetyFlags.GEN2 | SubaruSafetyFlags.LKAS_ANGLE | SubaruSafetyFlags.D_PLATFORM
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];
const uint8_t gitversion[19] = "DEV-187318b5-DEBUG";
const uint8_t gitversion[19] = "DEV-cbf7f35c-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-187318b5-DEBUG
DEV-cbf7f35c-DEBUG
+9 -88
View File
@@ -1,8 +1,6 @@
#!/usr/bin/env python3
import argparse
import codecs
import ctypes
import glob
import hashlib
import json
import os
@@ -47,19 +45,6 @@ MODEL_CONTEXT_FREQ = 5
REPOSITORY_FILE_LIMIT = 100_000_000
DEFAULT_MULTIPART_SIZE = 95 * 1024 * 1024
USBGPU_PROBE_ATTEMPTS = 10
USBGPU_PROBE_TIMEOUT = 2
USBDEVFS_CONTROL = 0xC0185500
USBGPU_VID_PIDS = (("add1", "0001"), ("3801", "0001"))
USBGPU_FIRMWARE_PRODUCT = "custom ed4e39b7-CLEAN"
class _UsbdevfsControl(ctypes.Structure):
_fields_ = [("request_type", ctypes.c_uint8), ("request", ctypes.c_uint8),
("value", ctypes.c_uint16), ("index", ctypes.c_uint16),
("length", ctypes.c_uint16), ("timeout", ctypes.c_uint32),
("data", ctypes.c_void_p)]
def build_compile_env(*, supercombo: bool = False) -> dict[str, str]:
env = os.environ.copy()
existing_pythonpath = env.get("PYTHONPATH", "")
@@ -85,79 +70,15 @@ def build_compile_env(*, supercombo: bool = False) -> dict[str, str]:
return env
def _probe_external_gpu_link_once() -> tuple[bool, str]:
"""Probe the bridge without initializing tinygrad or resetting the USB device."""
import fcntl
def wait_for_external_gpu() -> None:
"""Use openpilot's Chestnut link probe before starting a USB-GPU build."""
from openpilot.system.hardware.chestnut.flash import link_up
diagnostics: list[str] = []
for path in glob.glob("/sys/bus/usb/devices/*"):
try:
if not Path(path, "idVendor").is_file():
continue
vendor = Path(path, "idVendor").read_text().strip().lower()
product = Path(path, "idProduct").read_text().strip().lower()
if (vendor, product) not in USBGPU_VID_PIDS:
continue
bus = int(Path(path, "busnum").read_text())
device = int(Path(path, "devnum").read_text())
location = f"usb:{bus}-{device}"
firmware = Path(path, "product").read_text().strip()
if firmware and firmware != USBGPU_FIRMWARE_PRODUCT:
return False, f"{location}: firmware {firmware!r}, expected {USBGPU_FIRMWARE_PRODUCT!r}"
fd = os.open(f"/dev/bus/usb/{bus:03d}/{device:03d}", os.O_RDWR)
except (OSError, ValueError) as exc:
diagnostics.append(f"{path}: open failed ({exc})")
continue
try:
fcntl.ioctl(fd, USBDEVFS_CONTROL, _UsbdevfsControl(0x40, 0xF3, 1, 0, 0, USBGPU_PROBE_TIMEOUT * 1000, None))
state = (ctypes.c_ubyte * 1)()
fcntl.ioctl(fd, USBDEVFS_CONTROL, _UsbdevfsControl(0xC0, 0xE4, 0xB450, 0, 1, 1000, ctypes.cast(state, ctypes.c_void_p)))
if state[0] == 0x78:
return True, f"{location}: LTSSM=0x78"
diagnostics.append(f"{location}: LTSSM=0x{state[0]:02X}")
except OSError as exc:
diagnostics.append(f"{location}: control probe failed ({exc})")
finally:
os.close(fd)
return False, diagnostics[-1] if diagnostics else "no ASM2464PD device found"
def wait_for_external_gpu(compile_env: dict[str, str]) -> bool:
"""Wait for the USB GPU's PCIe link before starting the large model build.
The dock can enumerate on USB before its PCIe link has finished training.
Probe the bridge's control endpoint directly, like upstream openpilot. Do
not instantiate tinygrad here: opening the GPU resets/claims the USB
interface, and doing that in a probe process can leave the bridge in a state
where the authoritative compiler cannot train the link.
"""
del compile_env # retained in the public helper signature for callers/tests
diagnostics: list[str] = []
for attempt in range(USBGPU_PROBE_ATTEMPTS):
if attempt:
time.sleep(1)
try:
ready, detail = _probe_external_gpu_link_once()
except Exception as exc: # probe is advisory; compile_modeld remains authoritative
ready, detail = False, str(exc)
if ready:
return True
if "firmware" in detail and "expected" in detail:
raise RuntimeError(
f"External GPU firmware is out of date: {detail}. "
"Wait for hardwared to flash the dock, or run "
"sudo python3 system/hardware/chestnut/flash.py ed4e39b7."
)
diagnostics.append(detail)
detail = diagnostics[-1] if diagnostics else "unknown error"
print(
f"Warning: external GPU link did not become ready after {USBGPU_PROBE_ATTEMPTS} probes: {detail}\n"
" Continuing; compile_modeld will perform the authoritative link wait and initialization."
)
return False
for _ in range(USBGPU_PROBE_ATTEMPTS):
if link_up():
return
time.sleep(1)
raise RuntimeError("Chestnut not ready; external GPU PCIe link did not come up")
def parse_args() -> argparse.Namespace:
@@ -612,7 +533,7 @@ def compile_driving(
"TC_OPT": "2",
})
command.append("--out-of-band")
wait_for_external_gpu(compile_env)
wait_for_external_gpu()
subprocess.run(command, cwd=REPO_ROOT, env=compile_env, check=True)
return output_path
@@ -128,6 +128,7 @@ class LatControlTorque(LatControl):
self.is_tucson_4th_gen = CP.carFingerprint in TUCSON_4TH_GEN_CARS
self.is_civic_bosch_modified = CP.carFingerprint == HONDA_CAR.HONDA_CIVIC_BOSCH and bool(CP.flags & HondaFlags.EPS_MODIFIED)
self.is_silverado = CP.carFingerprint in SILVERADO_CARS
self.is_gmc_yukon_cc = CP.carFingerprint in GMC_YUKON_CC_CARS
self.is_ram_1500 = CP.carFingerprint in RAM_1500_CARS
self.is_gm = CP.brand == "gm"
self.is_hkg_canfd_torque = CP.brand == "hyundai" and bool(CP.flags & HyundaiFlags.CANFD)
@@ -289,6 +290,8 @@ class LatControlTorque(LatControl):
ff *= ff_scale
if self.is_ram_1500:
ff *= get_ram_1500_ff_scale(setpoint, desired_lateral_jerk, CS.vEgo)
if self.is_gmc_yukon_cc:
ff *= get_gmc_yukon_cc_ff_scale(setpoint, desired_lateral_jerk, CS.vEgo)
trailer_load_kg = float(max(getattr(starpilot_toggles, "trailer_load_kg", 0.0) or 0.0, 0.0))
bolt_2022_2023_tuned_path_active = self.is_bolt_2022_2023
bolt_2018_2021_tuned_path_active = self.is_bolt_2018_2021
@@ -526,6 +529,12 @@ class LatControlTorque(LatControl):
output_torque *= get_ioniq_6_highway_transition_output_taper_scale(setpoint, desired_lateral_jerk, CS.vEgo)
if self.is_ioniq_6_2025:
output_torque *= get_ioniq_6_2025_center_output_scale(setpoint, CS.vEgo)
low_speed_output_limit = get_ioniq_6_2025_low_speed_output_limit(setpoint, desired_lateral_jerk, CS.vEgo)
output_torque = float(np.clip(
output_torque,
-low_speed_output_limit,
low_speed_output_limit,
))
elif self.is_ram_1500 and output_torque * setpoint > 0.0:
output_torque *= get_ram_1500_transition_output_scale(setpoint, desired_lateral_jerk, CS.vEgo)
elif self.is_kona_non_scc:
@@ -563,6 +572,10 @@ class LatControlTorque(LatControl):
)
low_speed_output_limit = get_genesis_g70_low_speed_output_limit(setpoint, CS.vEgo)
output_torque = float(np.clip(output_torque, -low_speed_output_limit, low_speed_output_limit))
elif self.is_genesis_gv70:
output_torque *= get_genesis_gv70_high_speed_error_scale(
setpoint, measurement, desired_lateral_jerk, CS.vEgo,
)
elif sonata_hybrid_active:
output_torque *= sonata_hybrid_center_taper
output_torque *= sonata_hybrid_center_output_taper
@@ -84,6 +84,9 @@ SILVERADO_CARS = (
GM_CAR.CHEVROLET_SILVERADO,
GM_CAR.CHEVROLET_SILVERADO_CC,
)
GMC_YUKON_CC_CARS = (
GM_CAR.GMC_YUKON_CC,
)
GENESIS_G90_CARS = (
HYUNDAI_CAR.GENESIS_G90,
)
@@ -207,6 +210,13 @@ GENESIS_GV70_UNWIND_FF_JERK = 0.10
GENESIS_GV70_UNWIND_FF_JERK_WIDTH = 0.10
GENESIS_GV70_UNWIND_FF_SPEED = 10.0 * CV.MPH_TO_MS
GENESIS_GV70_UNWIND_FF_SPEED_WIDTH = 4.0 * CV.MPH_TO_MS
GENESIS_GV70_HIGH_SPEED_ERROR_DAMPING_MAX = 0.18
GENESIS_GV70_HIGH_SPEED_ERROR_DAMPING_SPEED = 50.0 * CV.MPH_TO_MS
GENESIS_GV70_HIGH_SPEED_ERROR_DAMPING_SPEED_WIDTH = 8.0 * CV.MPH_TO_MS
GENESIS_GV70_HIGH_SPEED_ERROR_DAMPING_ERROR = 0.18
GENESIS_GV70_HIGH_SPEED_ERROR_DAMPING_ERROR_WIDTH = 0.15
GENESIS_GV70_HIGH_SPEED_ERROR_DAMPING_JERK = 0.15
GENESIS_GV70_HIGH_SPEED_ERROR_DAMPING_JERK_WIDTH = 0.10
GENESIS_G70_FRICTION_THRESHOLD_GAIN = 0.10
GENESIS_G70_FRICTION_SPEED_ONSET = 10.0
@@ -240,7 +250,7 @@ GENESIS_G70_LOW_SPEED_ANGLE_DAMPING_ERROR_WIDTH = 3.0
GENESIS_G70_LOW_SPEED_ANGLE_DAMPING_ACTUAL = 8.0
GENESIS_G70_LOW_SPEED_ANGLE_DAMPING_ACTUAL_WIDTH = 4.0
GENESIS_G70_LOW_SPEED_ANGLE_DAMPING_BLEND = 0.50
GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_REDUCTION = 0.65
GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_REDUCTION = 0.85
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_SPEED = 6.0
@@ -394,6 +404,14 @@ SILVERADO_CENTER_TAPER_LAT_WIDTH = 0.05
SILVERADO_CENTER_TAPER_SPEED = 12.0
SILVERADO_CENTER_TAPER_SPEED_WIDTH = 2.5
GMC_YUKON_CC_PHASE_SCALE = 0.14
GMC_YUKON_CC_PHASE_SPEED_ONSET = 12.0
GMC_YUKON_CC_PHASE_SPEED_FULL = 30.0
GMC_YUKON_CC_PHASE_LAT_ONSET = 0.35
GMC_YUKON_CC_PHASE_LAT_WIDTH = 0.18
GMC_YUKON_CC_TURN_IN_FF_BOOST = 0.08
GMC_YUKON_CC_UNWIND_FF_REDUCTION = 0.12
SONATA_HYBRID_BASE_LAT_ACCEL_FACTOR_MULT = 1.05
SONATA_HYBRID_FF_REDUCTION_LEFT = 0.09
SONATA_HYBRID_FF_REDUCTION_RIGHT = 0.22
@@ -417,7 +435,7 @@ SONATA_HYBRID_LOW_SPEED_CENTER_TAPER_LAT = 0.10
SONATA_HYBRID_LOW_SPEED_CENTER_TAPER_LAT_WIDTH = 0.02
SONATA_HYBRID_LOW_SPEED_CENTER_TAPER_SPEED_MAX = 7.5
SONATA_HYBRID_LOW_SPEED_CENTER_TAPER_SPEED_WIDTH = 1.0
SONATA_HYBRID_CENTER_OUTPUT_TAPER_MAX = 0.06
SONATA_HYBRID_CENTER_OUTPUT_TAPER_MAX = 0.08
SONATA_HYBRID_CENTER_OUTPUT_TAPER_LAT = 0.18
SONATA_HYBRID_CENTER_OUTPUT_TAPER_LAT_WIDTH = 0.05
SONATA_HYBRID_CENTER_OUTPUT_TAPER_SPEED = 12.5
@@ -817,7 +835,7 @@ IONIQ_6_FRICTION_CENTER_FADE_SPEED_WIDTH = 2.5
# Newer Ioniq 6 highway center-chatter correction; activation is firmware-gated.
IONIQ_6_2025_FRICTION_SCALE_MULT = 0.80
IONIQ_6_2025_FRICTION_JERK_DEADZONE = 0.45
IONIQ_6_2025_CENTER_OUTPUT_TAPER_MAX = 0.28
IONIQ_6_2025_CENTER_OUTPUT_TAPER_MAX = 0.32
IONIQ_6_2025_CENTER_OUTPUT_TAPER_LAT = 0.35
IONIQ_6_2025_CENTER_OUTPUT_TAPER_LAT_WIDTH = 0.10
IONIQ_6_2025_CENTER_OUTPUT_TAPER_SPEED = 22.0
@@ -830,6 +848,11 @@ IONIQ_6_2025_LOW_SPEED_CENTER_LAT = 0.22
IONIQ_6_2025_LOW_SPEED_CENTER_LAT_WIDTH = 0.10
IONIQ_6_2025_LOW_SPEED_CENTER_JERK = 0.30
IONIQ_6_2025_LOW_SPEED_CENTER_JERK_WIDTH = 0.13
IONIQ_6_2025_LOW_SPEED_OUTPUT_LIMIT_BASE = 0.22
IONIQ_6_2025_LOW_SPEED_OUTPUT_LIMIT_TURN_RELIEF = 0.50
IONIQ_6_2025_LOW_SPEED_OUTPUT_LIMIT_SPEED_RELIEF = 0.20
IONIQ_6_2025_LOW_SPEED_OUTPUT_LIMIT_SPEED = 6.5
IONIQ_6_2025_LOW_SPEED_OUTPUT_LIMIT_SPEED_WIDTH = 1.5
IONIQ_6_HEAVY_DIRECTIONAL_TAPER_LAT_START = 0.90
IONIQ_6_HEAVY_DIRECTIONAL_TAPER_LAT_WIDTH = 0.18
IONIQ_6_HEAVY_DIRECTIONAL_TAPER_BASE_LEFT = 0.03
@@ -851,7 +874,7 @@ KIA_EV6_FF_CUTOFF = 1.90
KIA_EV6_FF_CUTOFF_WIDTH = 0.40
KIA_EV6_TRANSITION_SPEED = 14.5
KIA_EV6_PHASE_SCALE = 0.09
KIA_EV6_TURN_IN_BOOST_LEFT = 0.62
KIA_EV6_TURN_IN_BOOST_LEFT = 0.54
KIA_EV6_TURN_IN_BOOST_RIGHT = 0.60
KIA_EV6_UNWIND_TAPER_LEFT = 0.56
KIA_EV6_UNWIND_TAPER_RIGHT = 0.54
@@ -892,7 +915,7 @@ KIA_EV6_LOW_SPEED_CENTER_TAPER_LAT = 0.08
KIA_EV6_LOW_SPEED_CENTER_TAPER_LAT_WIDTH = 0.02
KIA_EV6_LOW_SPEED_CENTER_TAPER_SPEED_MAX = 8.5
KIA_EV6_LOW_SPEED_CENTER_TAPER_SPEED_WIDTH = 1.4
KIA_EV6_CENTER_OUTPUT_TAPER_MAX = 0.12
KIA_EV6_CENTER_OUTPUT_TAPER_MAX = 0.14
KIA_EV6_CENTER_OUTPUT_TAPER_LAT = 0.30
KIA_EV6_CENTER_OUTPUT_TAPER_LAT_WIDTH = 0.08
KIA_EV6_CENTER_OUTPUT_TAPER_SPEED = 12.0
@@ -1535,6 +1558,20 @@ def get_ram_1500_ff_scale(desired_lateral_accel: float, desired_lateral_jerk: fl
RAM_1500_UNWIND_FF_REDUCTION * unwind_weight) * speed_weight * lat_weight)
def get_gmc_yukon_cc_ff_scale(desired_lateral_accel: float, desired_lateral_jerk: float, v_ego: float) -> float:
"""Add turn-in authority and soften the high-speed unwind transient on Yukon CC."""
phase = math.tanh((desired_lateral_accel * desired_lateral_jerk) / GMC_YUKON_CC_PHASE_SCALE)
turn_in_weight = max(phase, 0.0)
unwind_weight = max(-phase, 0.0)
speed_weight = float(np.interp(v_ego,
[GMC_YUKON_CC_PHASE_SPEED_ONSET, GMC_YUKON_CC_PHASE_SPEED_FULL],
[0.0, 1.0]))
lat_weight = _sigmoid((abs(desired_lateral_accel) - GMC_YUKON_CC_PHASE_LAT_ONSET) /
GMC_YUKON_CC_PHASE_LAT_WIDTH)
return 1.0 + ((GMC_YUKON_CC_TURN_IN_FF_BOOST * turn_in_weight -
GMC_YUKON_CC_UNWIND_FF_REDUCTION * unwind_weight) * speed_weight * lat_weight)
def get_kona_non_scc_highway_transition_output_scale(desired_lateral_accel: float, desired_lateral_jerk: float,
v_ego: float) -> float:
speed_weight = float(np.interp(v_ego, [KONA_NON_SCC_TRANSITION_SPEED_ONSET, KONA_NON_SCC_TRANSITION_SPEED_FULL], [0.0, 1.0]))
@@ -2621,6 +2658,23 @@ def get_genesis_gv70_unwind_ff_scale(setpoint: float, measured_lateral_accel: fl
return 1.0 - GENESIS_GV70_UNWIND_FF_REDUCTION_MAX * overshoot_weight * jerk_weight * speed_weight
def get_genesis_gv70_high_speed_error_scale(setpoint: float, measured_lateral_accel: float,
desired_lateral_jerk: float, v_ego: float) -> float:
tracking_error = abs(measured_lateral_accel - setpoint)
if tracking_error <= 0.0:
return 1.0
speed_weight = _sigmoid((v_ego - GENESIS_GV70_HIGH_SPEED_ERROR_DAMPING_SPEED) /
GENESIS_GV70_HIGH_SPEED_ERROR_DAMPING_SPEED_WIDTH)
error_weight = _sigmoid((tracking_error - GENESIS_GV70_HIGH_SPEED_ERROR_DAMPING_ERROR) /
GENESIS_GV70_HIGH_SPEED_ERROR_DAMPING_ERROR_WIDTH)
jerk_weight = _sigmoid((abs(desired_lateral_jerk) - GENESIS_GV70_HIGH_SPEED_ERROR_DAMPING_JERK) /
GENESIS_GV70_HIGH_SPEED_ERROR_DAMPING_JERK_WIDTH)
phase_weight = 1.0 if setpoint * desired_lateral_jerk < 0.0 else 0.45
reduction = (GENESIS_GV70_HIGH_SPEED_ERROR_DAMPING_MAX * speed_weight * error_weight *
(0.35 + (0.65 * jerk_weight)) * phase_weight)
return 1.0 - reduction
def get_genesis_g70_friction_threshold(v_ego: float, desired_lateral_accel: float = 0.0,
desired_lateral_jerk: float = 0.0) -> float:
base_threshold = get_standard_friction_threshold(v_ego)
@@ -2993,6 +3047,22 @@ def get_ioniq_6_2025_center_output_scale(desired_lateral_accel: float, v_ego: fl
return 1.0 - IONIQ_6_2025_CENTER_OUTPUT_TAPER_MAX * speed_weight * center_weight
def get_ioniq_6_2025_low_speed_output_limit(desired_lateral_accel: float,
desired_lateral_jerk: float, v_ego: float) -> float:
"""Limit small-signal torque at crawl speed while leaving real turn commands open."""
speed_weight = _ioniq_6_sigmoid((IONIQ_6_2025_LOW_SPEED_OUTPUT_LIMIT_SPEED - max(v_ego, 0.0)) /
IONIQ_6_2025_LOW_SPEED_OUTPUT_LIMIT_SPEED_WIDTH)
center_weight = _ioniq_6_sigmoid((IONIQ_6_2025_LOW_SPEED_CENTER_LAT - abs(desired_lateral_accel)) /
IONIQ_6_2025_LOW_SPEED_CENTER_LAT_WIDTH)
calm_weight = _ioniq_6_sigmoid((IONIQ_6_2025_LOW_SPEED_CENTER_JERK - abs(desired_lateral_jerk)) /
IONIQ_6_2025_LOW_SPEED_CENTER_JERK_WIDTH)
center_weight *= calm_weight
limit = (IONIQ_6_2025_LOW_SPEED_OUTPUT_LIMIT_BASE +
IONIQ_6_2025_LOW_SPEED_OUTPUT_LIMIT_TURN_RELIEF * (1.0 - center_weight) +
IONIQ_6_2025_LOW_SPEED_OUTPUT_LIMIT_SPEED_RELIEF * (1.0 - speed_weight))
return float(np.clip(limit, IONIQ_6_2025_LOW_SPEED_OUTPUT_LIMIT_BASE, 1.0))
def _ioniq_6_2025_low_speed_center_envelope(desired_lateral_accel: float,
desired_lateral_jerk: float, v_ego: float) -> float:
speed_weight = _ioniq_6_sigmoid((IONIQ_6_2025_LOW_SPEED_CENTER_SPEED - max(v_ego, 0.0)) /
+3
View File
@@ -264,6 +264,9 @@ class LongControl:
if output_accel > starpilot_toggles.stopAccel:
output_accel = min(output_accel, 0.0)
output_accel -= starpilot_toggles.stoppingDecelRate * DT_CTRL
output_accel = self.vehicle_tuning.shape_stopping_accel(
output_accel, a_target, should_stop, CS.vEgo, has_lead, starpilot_toggles.stopAccel,
)
output_accel = self._apply_moving_stop_target_follow(output_accel, a_target, should_stop, CS, starpilot_toggles)
self.reset(preserve_stop_release=True)
@@ -25,7 +25,7 @@ GM_TRUCK_TARGET_FILTER_DOWN_TAU = 0.06
GM_TRUCK_TARGET_FILTER_BRAKE_BYPASS = -0.65
GM_TRUCK_TARGET_FILTER_DROP_BYPASS = 0.45
TOYOTA_SIENNA_TARGET_FILTER_MIN_SPEED = 12.0
TOYOTA_SIENNA_TARGET_FILTER_UP_TAU = 0.18
TOYOTA_SIENNA_TARGET_FILTER_UP_TAU = 0.32
TOYOTA_SIENNA_TARGET_FILTER_DOWN_TAU = 0.24
TOYOTA_SIENNA_LOW_SPEED_ACCEL_UP_TAU = 0.35
TOYOTA_SIENNA_TARGET_FILTER_BRAKE_BYPASS = -0.75
@@ -47,6 +47,7 @@ VOLT_CRUISE_INTEGRATOR_ERROR_MAX = 0.12
VOLT_CRUISE_INTEGRATOR_LEAK = 0.995
SUBARU_IMPREZA_STOP_RELEASE_TIME = 0.75
SUBARU_IMPREZA_STOP_RELEASE_MAX_ACCEL = 0.8
HYUNDAI_ELANTRA_STOPPING_HOLD_TARGET_GAP = 0.25
def get_bolt_acc_pedal_friction_bias(output_accel, a_target, v_ego):
@@ -123,6 +124,9 @@ class LongControlVehicleTuning:
CP.brand == "subaru" and
getattr(CP, "carFingerprint", None) == SUBARU_CAR.SUBARU_IMPREZA_2020
)
self.is_hyundai_elantra_2021 = bool(
CP.brand == "hyundai" and str(getattr(CP, "carFingerprint", "")) == "HYUNDAI_ELANTRA_2021"
)
self.is_bolt_acc_pedal_friction_car = bool(
CP.brand == "gm" and
CP.enableGasInterceptorDEPRECATED and
@@ -143,6 +147,16 @@ class LongControlVehicleTuning:
self.bolt_start_handoff_frames = 0
self.subaru_stop_release_frames = 0
def shape_stopping_accel(self, output_accel, a_target, should_stop, v_ego, has_lead, stop_accel):
"""Release a stale hard lead brake once the stop target has eased."""
if (
not self.is_hyundai_elantra_2021 or
not has_lead or not should_stop or v_ego > 2.0 or
a_target <= stop_accel - HYUNDAI_ELANTRA_STOPPING_HOLD_TARGET_GAP
):
return output_accel
return max(float(output_accel), float(stop_accel))
def cap_subaru_stop_release_accel(self, output_accel, stopping_handoff, should_stop):
"""Prevent an Impreza stop-sign handoff from stepping straight into full throttle."""
if not self.is_subaru_impreza_2020:
+47 -1
View File
@@ -38,6 +38,7 @@ from openpilot.selfdrive.controls.lib.latcontrol_vehicle_tunes import (
KIA_FORTE_BASE_LAT_ACCEL_FACTOR_MULT,
RAM_1500_BASE_LAT_ACCEL_FACTOR_MULT,
RAM_1500_MAX_LAT_JERK_UP,
get_gmc_yukon_cc_ff_scale,
get_ram_1500_transition_output_scale,
get_ram_1500_ff_scale,
get_subaru_impreza_pid_output_scale,
@@ -78,6 +79,7 @@ from openpilot.selfdrive.controls.lib.latcontrol_torque import (
get_genesis_g70_low_speed_angle_damping,
get_genesis_g70_low_speed_output_limit,
get_genesis_gv70_friction_threshold,
get_genesis_gv70_high_speed_error_scale,
get_genesis_gv70_unwind_ff_scale,
get_elantra_non_scc_ff_scale,
get_palisade_ff_scale,
@@ -116,6 +118,7 @@ from openpilot.selfdrive.controls.lib.latcontrol_torque import (
get_ioniq_6_friction_threshold,
get_ioniq_6_low_speed_angle_assist_torque,
get_ioniq_6_2025_center_output_scale,
get_ioniq_6_2025_low_speed_output_limit,
is_ioniq_6_2025_model,
get_kia_forte_center_taper_scale,
get_kia_forte_ff_scale,
@@ -793,6 +796,12 @@ class TestLatControl:
assert highway_turn == pytest.approx(base, rel=0.01)
assert highway_center < center
def test_genesis_gv70_high_speed_error_damping(self):
assert get_genesis_gv70_high_speed_error_scale(0.2, 0.2, 0.8, 20.0) == 1.0
assert get_genesis_gv70_high_speed_error_scale(-0.7, 0.58, -0.8, 33.5) < 1.0
assert get_genesis_gv70_high_speed_error_scale(-0.7, 0.58, -0.8, 20.0) > \
get_genesis_gv70_high_speed_error_scale(-0.7, 0.58, -0.8, 33.5)
def test_genesis_g70_center_chatter_tune(self):
base = get_standard_friction_threshold(25.0)
center = get_genesis_g70_friction_threshold(25.0, 0.0, 0.0)
@@ -808,6 +817,7 @@ class TestLatControl:
assert get_genesis_g70_center_output_scale(0.0, 0.0) < get_genesis_g70_center_output_scale(0.0, 10.0)
assert get_genesis_g70_low_speed_output_limit(0.0, 2.0) < get_genesis_g70_low_speed_output_limit(0.5, 2.0)
assert get_genesis_g70_low_speed_output_limit(0.0, 2.0) < get_genesis_g70_low_speed_output_limit(0.0, 10.0)
assert get_genesis_g70_low_speed_output_limit(0.0, 2.0) < 0.30
assert get_genesis_g70_low_speed_angle_damping(0.0, -20.0, 0.0, 2.0) < 0.0
assert get_genesis_g70_low_speed_angle_damping(0.0, 20.0, 0.0, 2.0) > 0.0
assert get_genesis_g70_curve_unwind_output_scale(0.7, -0.5, 25.0) > 1.0
@@ -958,6 +968,29 @@ class TestLatControl:
assert get_ram_1500_ff_scale(1.2, -1.1, 17.0) < 1.0
assert get_ram_1500_ff_scale(1.2, 1.1, 6.0) < get_ram_1500_ff_scale(1.2, 1.1, 17.0)
def test_gmc_yukon_cc_phase_feedforward_curve(self):
assert get_gmc_yukon_cc_ff_scale(0.0, 1.0, 30.0) == pytest.approx(1.0)
assert get_gmc_yukon_cc_ff_scale(1.2, 1.1, 30.0) > 1.0
assert get_gmc_yukon_cc_ff_scale(1.2, -1.1, 30.0) < 1.0
assert get_gmc_yukon_cc_ff_scale(1.2, 1.1, 8.0) < get_gmc_yukon_cc_ff_scale(1.2, 1.1, 30.0)
def test_gmc_yukon_cc_phase_feedforward_update_path(self, monkeypatch):
controller, VM, CS, params, starpilot_toggles = self._build_torque_controller(GM.GMC_YUKON_CC)
CS.vEgo = 25.0
base_output, _, _ = controller.update(
True, CS, VM, params, False, 0.0025, False, 0.2, None, None, starpilot_toggles,
)
monkeypatch.setattr(latcontrol_torque, "get_gmc_yukon_cc_ff_scale", lambda *_args: 0.5)
tuned_controller, tuned_VM, tuned_CS, tuned_params, tuned_toggles = self._build_torque_controller(GM.GMC_YUKON_CC)
tuned_CS.vEgo = 25.0
tuned_output, _, _ = tuned_controller.update(
True, tuned_CS, tuned_VM, tuned_params, False, 0.0025, False, 0.2, None, None, tuned_toggles,
)
assert controller.is_gmc_yukon_cc
assert tuned_output != pytest.approx(base_output)
def test_ram_1500_jerk_limit_update_path(self):
controller, VM, CS, params, starpilot_toggles = self._build_torque_controller(CHRYSLER.RAM_1500_5TH_GEN)
jerk_samples = []
@@ -1197,6 +1230,8 @@ class TestLatControl:
assert low_speed_center_error < low_speed_turn_error
assert low_speed_center_error < high_speed_center_error
assert low_speed_center_friction < 1.0
assert get_ioniq_6_2025_low_speed_output_limit(0.02, 0.05, 2.5) < get_ioniq_6_2025_low_speed_output_limit(0.60, 0.80, 2.5)
assert get_ioniq_6_2025_low_speed_output_limit(0.02, 0.05, 2.5) < get_ioniq_6_2025_low_speed_output_limit(0.02, 0.05, 8.0)
def test_ioniq_6_center_taper_curve(self):
assert get_ioniq_6_center_taper_scale(0.0, 10.0) > get_ioniq_6_center_taper_scale(0.0, 30.0)
@@ -1418,6 +1453,17 @@ class TestLatControl:
assert controller.torque_params.latAccelFactor == pytest.approx(3.0 * 1.22)
assert controller.low_speed_reset_threshold == pytest.approx(0.1 * 0.44704)
def test_ioniq_6_2025_low_speed_output_limit_update_path(self, monkeypatch):
monkeypatch.setattr(latcontrol_torque, "get_ioniq_6_2025_low_speed_output_limit", lambda *_args: 0.05)
controller, VM, CS, params, starpilot_toggles = self._build_torque_controller(HYUNDAI.HYUNDAI_IONIQ_6)
controller.is_ioniq_6_2025 = True
CS.vEgo = 3.2
output, _, lac_log = controller.update(True, CS, VM, params, False, 0.0025, False, 0.2, None, None, starpilot_toggles)
assert lac_log.active
assert abs(output) <= 0.05
def test_elantra_non_scc_default_update_path(self):
controller, VM, CS, params, starpilot_toggles = self._build_torque_controller(HYUNDAI.HYUNDAI_ELANTRA_HEV_2022_NON_SCC)
CarInterface = interfaces[HYUNDAI.HYUNDAI_ELANTRA_HEV_2022_NON_SCC]
@@ -1807,7 +1853,7 @@ class TestLatControl:
def test_kia_ev6_center_output_taper_curve(self):
assert get_kia_ev6_center_output_scale(0.0, 10.0) > get_kia_ev6_center_output_scale(0.0, 20.0)
assert get_kia_ev6_center_output_scale(0.0, 20.0) < get_kia_ev6_center_output_scale(0.5, 20.0)
assert get_kia_ev6_center_output_scale(0.0, 20.0) > 0.87
assert get_kia_ev6_center_output_scale(0.0, 20.0) > 0.86
def test_kia_ev6_center_output_taper_update_path(self, monkeypatch):
controller, VM, CS, params, starpilot_toggles = self._build_torque_controller(HYUNDAI.KIA_EV6)
@@ -755,6 +755,15 @@ def test_stopping_state_follows_stronger_moving_stop_target():
assert output_accel < -1.43
def test_elantra_lead_stop_releases_stale_hard_brake_after_target_eases():
CP = make_longcontrol_cp(brand="hyundai", carFingerprint="HYUNDAI_ELANTRA_2021")
tuning = vehicle_tunes.LongControlVehicleTuning(CP)
assert tuning.shape_stopping_accel(-1.20, -0.25, True, 1.0, True, -0.85) == pytest.approx(-0.85)
assert tuning.shape_stopping_accel(-1.20, -1.50, True, 1.0, True, -0.85) == pytest.approx(-1.20)
assert tuning.shape_stopping_accel(-1.20, -0.25, True, 1.0, False, -0.85) == pytest.approx(-1.20)
def test_volt_testing_ground_handoff_freezes_integrator(monkeypatch):
CP = car.CarParams.new_message()
CP.brand = "gm"
@@ -1177,6 +1186,16 @@ def test_toyota_sienna_target_filter_smooths_mild_high_speed_handoffs():
assert -0.20 < filtered < 0.30
def test_toyota_sienna_target_filter_unwinds_braking_before_acceleration():
CP = make_longcontrol_cp(brand="toyota", carFingerprint=TOYOTA_CAR.TOYOTA_SIENNA_4TH_GEN)
tuning = vehicle_tunes.LongControlVehicleTuning(CP)
tuning.shape_toyota_sienna_accel_target(-1.2, 20.0, False)
recovering = tuning.shape_toyota_sienna_accel_target(1.2, 20.0, False)
assert recovering == pytest.approx(-1.1272727273)
def test_toyota_sienna_target_filter_ramps_low_speed_acceleration():
CP = make_longcontrol_cp(brand="toyota", carFingerprint=TOYOTA_CAR.TOYOTA_SIENNA_4TH_GEN)
tuning = vehicle_tunes.LongControlVehicleTuning(CP)
@@ -10,6 +10,7 @@ from openpilot.starpilot.controls.lib.starpilot_vcruise import (
FORCE_STOP_TURN_VETO_STOP_SEEN_HOLD_TIME,
StarPilotVCruise,
get_active_slc_control_target,
get_lead_veto_distance,
get_slc_lead_drop_relaxed_target,
)
from types import SimpleNamespace
@@ -110,6 +111,11 @@ def test_active_slc_control_target_does_not_require_set_speed_limit():
assert target == pytest.approx((48.0 * CV.MPH_TO_MS) - 0.4)
def test_elantra_gets_lead_veto_margin_before_force_stop():
assert get_lead_veto_distance(SimpleNamespace(carFingerprint="HYUNDAI_ELANTRA_2021")) == pytest.approx(90.0)
assert get_lead_veto_distance(SimpleNamespace(carFingerprint="OTHER_CAR")) == pytest.approx(75.0)
def test_curve_speed_controller_holds_target_through_brief_detector_dropout():
planner, vcruise = make_vcruise()
sm = make_sm(standstill=False)
+6 -20
View File
@@ -23,28 +23,14 @@ def test_out_of_band_artifact_round_trip():
np.testing.assert_array_equal(restored["weights"], artifact["weights"])
def test_external_gpu_probe_retries_until_pcie_is_ready(monkeypatch):
def test_external_gpu_probe_matches_upstream_retry_loop(monkeypatch):
from openpilot.system.hardware.chestnut import flash
calls = []
probe_count = 0
def probe():
nonlocal probe_count
probe_count += 1
calls.append("probe")
return (False, "LTSSM=0x00") if probe_count < 3 else (True, "LTSSM=0x78")
monkeypatch.setattr(
model_compiler,
"_probe_external_gpu_link_once",
probe,
)
results = iter((False, False, True))
monkeypatch.setattr(flash, "link_up", lambda: calls.append("probe") or next(results))
monkeypatch.setattr(model_compiler.time, "sleep", lambda seconds: calls.append(("sleep", seconds)))
model_compiler.wait_for_external_gpu({"PYTHONPATH": "/tmp/openpilot"})
model_compiler.wait_for_external_gpu()
assert calls == ["probe", ("sleep", 1), "probe", ("sleep", 1), "probe"]
def test_external_gpu_probe_reports_failure(monkeypatch):
monkeypatch.setattr(model_compiler, "_probe_external_gpu_link_once", lambda: (False, "link unavailable"))
monkeypatch.setattr(model_compiler.time, "sleep", lambda _: None)
assert model_compiler.wait_for_external_gpu({}) is False
@@ -1,5 +1,7 @@
import importlib.util
from pathlib import Path
import re
import subprocess
import pytest
@@ -10,6 +12,19 @@ assert SPEC is not None and SPEC.loader is not None
PANDAD = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(PANDAD)
REPO_ROOT = PANDAD_PATH.parents[2]
PANDA_H7_FIRMWARE = REPO_ROOT / "panda/board/obj/panda_h7.bin.signed"
PANDA_FIRMWARE_SOURCE_PREFIXES = (
"opendbc_repo/opendbc/safety/",
"panda/board/",
"panda/crypto/",
"panda/drivers/",
)
PANDA_FIRMWARE_SOURCE_EXCLUSIONS = (
"opendbc_repo/opendbc/safety/tests/",
"panda/board/obj/",
)
class FakeParams:
def __init__(self, ignore_ignition_line):
@@ -26,3 +41,26 @@ class FakeParams:
])
def test_ignore_ignition_line_follows_toggle(enabled, expected):
assert PANDAD.get_ignore_ignition_line(FakeParams(enabled)) == expected
def test_tracked_panda_firmware_includes_current_safety_sources():
version_match = re.search(rb"DEV-([0-9a-f]{8})-DEBUG", PANDA_H7_FIRMWARE.read_bytes())
assert version_match is not None
firmware_commit = version_match.group(1).decode()
try:
changed_files = subprocess.check_output(
["git", "diff", "--name-only", f"{firmware_commit}..HEAD", "--", *PANDA_FIRMWARE_SOURCE_PREFIXES],
cwd=REPO_ROOT,
text=True,
).splitlines()
changed_files += subprocess.check_output(
["git", "diff", "--name-only", "HEAD", "--", *PANDA_FIRMWARE_SOURCE_PREFIXES],
cwd=REPO_ROOT,
text=True,
).splitlines()
except subprocess.CalledProcessError:
pytest.skip("firmware source commit is unavailable in this checkout")
stale_sources = sorted({path for path in changed_files if not path.startswith(PANDA_FIRMWARE_SOURCE_EXCLUSIONS)})
assert stale_sources == [], f"panda firmware must be rebuilt after changing: {stale_sources}"
+8 -1
View File
@@ -12,6 +12,7 @@ from msgq.visionipc import VisionIpcClient, VisionStreamType
from opendbc.car.chrysler.values import pacifica_hybrid_aol_stock_acc_mode
from opendbc.car.gm.values import GMFlags
from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR
from openpilot.common.params import Params
from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper, DT_CTRL
@@ -59,7 +60,8 @@ IGNORED_SAFETY_MODES = (SafetyModel.silent, SafetyModel.noOutput)
def commanded_torque_at_max_for_saturation(CP, output: float) -> bool:
torque_controller = (CP.steerControlType == car.CarParams.SteerControlType.torque and
CP.lateralTuning.which() == "torque")
return torque_controller and abs(output) > 0.99
has_controller_grace = CP.carFingerprint == HYUNDAI_CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN
return torque_controller and not has_controller_grace and abs(output) > 0.99
def should_loud_blindspot_alert_without_lateral(CS, sm, starpilot_toggles, combined_left_bsm=None, combined_right_bsm=None) -> bool:
@@ -94,6 +96,11 @@ def get_starpilot_alert_filters(current_alert_types: list[str], clear_event_type
starpilot_alert_types = list(current_alert_types)
starpilot_clear_event_types = set(clear_event_types)
if int(StarPilotEventName.lkasEnable) in starpilot_events.names:
if ET.WARNING not in starpilot_alert_types:
starpilot_alert_types.append(ET.WARNING)
starpilot_clear_event_types.discard(ET.WARNING)
# This alert is explicitly allowed while lateral is paused/off. The state
# machine only exposes WARNING while active/AOL, so let this warning through.
if StarPilotEventName.laneChangeBlockedLoud in starpilot_events.names:
@@ -99,6 +99,21 @@ def test_loud_blindspot_alert_survives_disabled_warning_filter():
assert alert_manager.current_alert.alert_type == "laneChangeBlockedLoud/warning"
def test_lkas_enable_sound_survives_disabled_warning_filter():
events = Events(starpilot=True)
events.add(StarPilotEventName.lkasEnable)
alert_types, clear_event_types = get_starpilot_alert_filters([ET.PERMANENT], {ET.WARNING}, events)
alerts = events.create_alerts(alert_types)
alert_manager = AlertManager()
alert_manager.add_many(0, alerts)
alert_manager.process_alerts(0, clear_event_types)
assert alert_manager.current_alert.alert_type == "lkasEnable/warning"
assert alert_manager.current_alert.audible_alert == log.SelfdriveState.AudibleAlert.engage
def test_disabled_starpilot_warnings_stay_filtered_without_blindspot_event():
events = Events(starpilot=True)
events.add(StarPilotEventName.noLaneAvailable)
@@ -1,4 +1,5 @@
from cereal import car
from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR
from openpilot.selfdrive.selfdrived.selfdrived import commanded_torque_at_max_for_saturation
@@ -17,3 +18,12 @@ def test_immediate_max_output_saturation_is_torque_controller_only():
CP.lateralTuning.init("torque")
CP.steerControlType = car.CarParams.SteerControlType.angle
assert not commanded_torque_at_max_for_saturation(CP, 1.0)
def test_gv70_uses_normal_saturation_timer_at_max_output():
CP = car.CarParams.new_message()
CP.carFingerprint = HYUNDAI_CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN
CP.steerControlType = car.CarParams.SteerControlType.torque
CP.lateralTuning.init("torque")
assert not commanded_torque_at_max_for_saturation(CP, 1.0)
+14 -1
View File
@@ -45,6 +45,9 @@ ACTIVATION_M = 100.0 # m — CEM/model path activates when model_length < t
ACTIVATION_HYSTERESIS_M = 8.0 # m — release margin; absorbs model_length jitter at the gate
LEAD_VETO_M = 75.0 # m — lead proximity that vetoes Force Stop (kept off ACTIVATION_M
# so raising activation can't silently widen the veto)
LEAD_VETO_M_OVERRIDES = {
"HYUNDAI_ELANTRA_2021": 90.0,
}
MPC_HANDOFF_M = 6.0 # m — below this, command 0 and let MPC finish the stop
FORCE_STOP_APPROACH_DECEL = 0.65 # m/s^2 — speed ceiling before commit. LOWER = more early
# braking; don't go under FORCE_STOP_MODEL_APPROACH_DECEL
@@ -70,6 +73,11 @@ OFFSET_FT_MIN = -20
OFFSET_FT_MAX = 20
def get_lead_veto_distance(car_params):
fingerprint = str(getattr(car_params, "carFingerprint", ""))
return LEAD_VETO_M_OVERRIDES.get(fingerprint, LEAD_VETO_M)
def get_active_slc_control_target(speed_limit_controller, set_speed_limit, slc_target, slc_offset, overridden_speed,
v_ego_diff, allow_lower_override=False):
# `SetSpeedLimit` only controls engage-time set-speed initialization. Ongoing
@@ -333,8 +341,13 @@ class StarPilotVCruise:
# waiting for the tracking_lead filter (~1s ramp). Without this, Force Stop can latch
# during the filter's settling window and stay committed for the whole stop.
lead = self.starpilot_planner.lead_one
try:
car_params = sm["carParams"]
except (KeyError, IndexError, TypeError, AttributeError):
car_params = None
lead_veto_m = get_lead_veto_distance(car_params)
lead_present = (bool(getattr(lead, "status", False))
and float(getattr(lead, "dRel", float("inf"))) < LEAD_VETO_M
and float(getattr(lead, "dRel", float("inf"))) < lead_veto_m
and float(getattr(lead, "vLead", float("inf"))) < v_ego + 2.0)
curved_approach_scene = (
abs(float(getattr(self.starpilot_planner, "road_curvature", 0.0))) >= FORCE_STOP_CURVE_VETO_MAX_ROAD_CURVATURE
@@ -1,54 +0,0 @@
from tinygrad.runtime.support.usb import USBMMIOInterface
class FakeUSB:
def __init__(self):
self.calls = []
def pcie_mem_req(self, address, value=None, size=4):
self.calls.append(("scalar", address, value, size))
return 0x11223344 if value is None else None
def pcie_mem_read(self, address, size):
self.calls.append(("read", address, size))
return bytes(size)
def pcie_mem_write(self, address, data):
self.calls.append(("write", address, data))
def test_scalar_mmio_uses_single_tlp():
usb = FakeUSB()
mmio = USBMMIOInterface(usb, 0x1000, 0x100, "I")
mmio[2] = 0xAABBCCDD
assert mmio[3] == 0x11223344
assert usb.calls == [
("scalar", 0x1008, 0xAABBCCDD, 4),
("scalar", 0x100C, None, 4),
]
def test_slice_mmio_keeps_streaming_path():
usb = FakeUSB()
mmio = USBMMIOInterface(usb, 0x2000, 0x100, "I")
mmio[0:2] = b"\x01\x02\x03\x04\x05\x06\x07\x08"
assert mmio[0:2] == bytes(8)
assert usb.calls == [
("write", 0x2000, b"\x01\x02\x03\x04\x05\x06\x07\x08"),
("read", 0x2000, 8),
]
def test_scalar_mmio_falls_back_to_streaming_transport():
class StreamingUSB:
def __init__(self): self.data = bytearray(4)
def pcie_mem_read(self, address, size): return self.data[address-0x3000:address-0x3000+size]
def pcie_mem_write(self, address, data): self.data[address-0x3000:address-0x3000+len(data)] = data
mmio = USBMMIOInterface(StreamingUSB(), 0x3000, 4, "I")
mmio[0] = 0xAABBCCDD
assert mmio[0] == 0xAABBCCDD
+3 -2
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
import time
START_TIME = time.perf_counter()
import os, functools, platform, re, contextlib, operator, hashlib, pickle, sqlite3, tempfile, pathlib, string, ctypes, sys, gzip, getpass, gc
import os, functools, platform, re, contextlib, operator, hashlib, pickle, sqlite3, tempfile, pathlib, string, ctypes, sys, gzip, getpass, gc, io
from collections import defaultdict
import subprocess, shutil, math, types, copyreg, inspect, importlib, decimal, itertools, difflib
from dataclasses import dataclass, field, replace
@@ -492,7 +492,8 @@ def _decompress_zstd(data:bytes) -> bytes:
from compression.zstd import decompress
return decompress(data)
from zstandard import ZstdDecompressor
return ZstdDecompressor().decompress(data)
with ZstdDecompressor().stream_reader(io.BytesIO(data)) as reader:
return reader.read()
def fetch_fw(path:str, name:str, sha256:str) -> bytes:
if (p:=pathlib.Path(f"/lib/firmware/{path}/{name}.zst")).is_file():
+1 -19
View File
@@ -135,9 +135,6 @@ class CustomASM24Controller:
address = (bus << 24) | (dev << 19) | (fn << 16) | (byte_addr & 0xfff)
return self.pcie_request(fmt_type, address, value, size)
def pcie_mem_req(self, address:int, value:int|None=None, size:int=4):
return self.pcie_request(0x60 if value is not None else 0x20, address, value, size)
def pcie_mem_write(self, address:int, data:bytes):
"""Streaming PCIe memory write via 0xF0 mode 1 + bulk OUT. Data is little-endian dwords on the wire."""
if not data: return
@@ -186,31 +183,16 @@ class USBMMIOInterface(MMIOInterface):
if isinstance(index, slice): return ((index.start or 0) * self.el_sz, ((index.stop or len(self))-(index.start or 0)) * self.el_sz)
return (index * self.el_sz, self.el_sz)
def _scalar(self, off:int, size:int, value:int|None=None):
assert size in (1, 2, 4, 8), f"invalid scalar PCIe access size {size}"
if not hasattr(self.usb, "pcie_mem_req"):
if value is not None:
self.usb.pcie_mem_write(self.addr + off, value.to_bytes(size, "little"))
return
return int.from_bytes(self.usb.pcie_mem_read(self.addr + off, size), "little")
upper = 0 if size < 8 else self.usb.pcie_mem_req(self.addr + off + 4, value if value is None else value >> 32, 4)
lower = self.usb.pcie_mem_req(self.addr + off, value if value is None else value & 0xffffffff, min(size, 4))
if value is None: return lower | (upper << 32)
def __getitem__(self, index):
off, sz = self._off_from_index(index)
if self.pcimem:
if not isinstance(index, slice): return self._scalar(off, sz)
assert sz % 4 == 0 and off % 4 == 0, f"pcie_mem_read requires 4-byte aligned access, got off={off}, sz={sz}"
data = self.usb.pcie_mem_read(self.addr + off, sz)
else: data = self.usb.scsi_read(sz) if self.addr == 0xf000 else self.usb.read(self.addr + off, sz)
return int.from_bytes(data, "little") if sz == self.el_sz else data
def __setitem__(self, index, data):
off, sz = self._off_from_index(index)
if self.pcimem and not isinstance(index, slice) and isinstance(data, int):
self._scalar(off, sz, data)
return
off, _ = self._off_from_index(index)
data = struct.pack(self.fmt, data) if isinstance(data, int) else bytes(data)
if not self.pcimem: self.usb.scsi_write(data) if self.addr == 0xf000 else self.usb.write(self.addr + off, data)
else: self.usb.pcie_mem_write(self.addr+off, data)