This commit is contained in:
firestar5683
2026-10-04 21:24:41 -05:00
parent c2109496a7
commit 15c7110647
11 changed files with 566 additions and 25 deletions
@@ -26,6 +26,33 @@ ENABLE_BUTTONS = (Buttons.RES_ACCEL, Buttons.SET_DECEL, Buttons.CANCEL)
BUTTONS_DICT = {Buttons.RES_ACCEL: ButtonType.accelCruise, Buttons.SET_DECEL: ButtonType.decelCruise,
Buttons.GAP_DIST: ButtonType.gapAdjustCruise, Buttons.CANCEL: ButtonType.cancel}
class Ev6AolArmingState:
def __init__(self, lkas_on_engage: bool):
self.lkas_on_engage = lkas_on_engage
self.main_on = False
self.lkas_on = False
self.prev_main_button = False
self.prev_lkas_button = False
self.prev_cruise_button = Buttons.NONE
def update(self, main_button: bool, lkas_button: bool, cruise_button: int):
if main_button and not self.prev_main_button:
self.main_on = not self.main_on
if lkas_button and not self.prev_lkas_button:
self.lkas_on = not self.lkas_on
if (self.lkas_on_engage and cruise_button != self.prev_cruise_button and
self.prev_cruise_button in (Buttons.SET_DECEL, Buttons.RES_ACCEL)):
self.lkas_on = True
self.prev_main_button = main_button
self.prev_lkas_button = lkas_button
self.prev_cruise_button = cruise_button
@property
def authorized(self) -> bool:
return self.main_on or self.lkas_on
IONIQ_6_BLINDSPOT_RIGHT_MASK = 0x08
IONIQ_6_BLINDSPOT_LEFT_MASK = 0x10
CANFD_CAMERA_LEAD_MIN_DISTANCE = 0.1
@@ -138,6 +165,13 @@ class CarState(CarStateBase):
self.buttons_counter = 0
self.main_cruise_on = False
self.main_cruise_tracking = bool(getattr(FPCP, "flags", 0) & HyundaiStarPilotFlags.MAIN_CRUISE_STATE_TRACKING)
self.ev6_aol_arming = None
if CP.carFingerprint == CAR.KIA_EV6 and CP.openpilotLongitudinalControl and not CP.pcmCruise:
lkas_on_engage = any(
config.safetyParam & HyundaiStarPilotSafetyFlags.AOL_LKAS_ON_ENGAGE
for config in (*CP.safetyConfigs, *FPCP.safetyConfigs)
)
self.ev6_aol_arming = Ev6AolArmingState(lkas_on_engage)
if CP.carFingerprint == CAR.KIA_RAY_EV:
self.ray_pedal_state = 5
self.ray_pedal_valid = False
@@ -181,6 +215,10 @@ class CarState(CarStateBase):
# Main button also can trigger an engagement on these cars
return any(btn in ENABLE_BUTTONS for btn in self.cruise_buttons) or any(self.main_buttons)
@property
def ev6_aol_authorized(self) -> bool:
return self.ev6_aol_arming is not None and self.ev6_aol_arming.authorized
def update_main_cruise(self, ret: structs.CarState,
button_events: list[structs.CarState.ButtonEvent] | None = None) -> bool:
button_events = ret.buttonEvents if button_events is None else button_events
@@ -640,6 +678,13 @@ class CarState(CarStateBase):
*create_button_events(self.main_buttons[-1], prev_main_buttons, {1: ButtonType.mainCruise}),
*create_button_events(self.lda_button, prev_lda_button, {1: ButtonType.lkas}),
*create_button_events(self.left_paddle, prev_left_paddle, {1: ButtonType.altButton2})]
if self.ev6_aol_arming is not None:
buttons = cp.vl_all[self.cruise_btns_msg_canfd]
for main, lkas, cruise in zip(buttons["ADAPTIVE_CRUISE_MAIN_BTN"], buttons["LDA_BTN"], buttons["CRUISE_BUTTONS"], strict=True):
self.ev6_aol_arming.update(bool(main), bool(lkas), int(cruise))
if not ret.cruiseState.available:
self.ev6_aol_arming.main_on = False
if self.CP.openpilotLongitudinalControl and (self.CP.carFingerprint == CAR.KIA_EV9 or self.main_cruise_tracking):
ret.cruiseState.available = self.update_main_cruise(ret)
@@ -21,7 +21,7 @@ from opendbc.car.hyundai.carcontroller import CarController, CANCEL_BUTTON_DELAY
preserve_stock_canfd_lfa_status, \
preserve_stock_canfd_lkas_status, \
suppress_redundant_gv70_brake_cancel
from opendbc.car.hyundai.carstate import CarState, decode_canfd_camera_lead, decode_ioniq_6_blindspot_radar_state, \
from opendbc.car.hyundai.carstate import CarState, Ev6AolArmingState, 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, get_communication_control_request
from opendbc.car.hyundai import hyundaican, hyundaicanfd
@@ -130,6 +130,73 @@ def get_test_toggles() -> SimpleNamespace:
class TestHyundaiFingerprint:
@pytest.mark.parametrize("candidate, alpha_long, needs_arming", (
(CAR.KIA_EV6, True, True), (CAR.KIA_EV6, False, False),
(CAR.KIA_EV6_2025, True, False), (CAR.KIA_EV9, True, False),
(CAR.HYUNDAI_IONIQ_5, True, False), (CAR.HYUNDAI_IONIQ_6, True, False),
))
def test_ev6_aol_arming_is_vehicle_specific(self, candidate, alpha_long, needs_arming):
toggles = get_test_toggles()
CP = CarInterface.get_params(candidate, gen_empty_fingerprint(), [], alpha_long, False, False, toggles)
FPCP = CarInterface.get_starpilot_params(candidate, gen_empty_fingerprint(), [], CP, toggles)
CS = CarState(CP, FPCP)
assert (CS.ev6_aol_arming is not None) == needs_arming
assert not CS.ev6_aol_authorized
@pytest.mark.parametrize("alt_buttons", (False, True))
@pytest.mark.parametrize("lkas_on_engage", (False, True))
def test_ev6_aol_uses_all_physical_button_samples(self, alt_buttons, lkas_on_engage):
toggles = get_test_toggles()
toggles.always_on_lateral_lkas = lkas_on_engage
fingerprint = gen_empty_fingerprint()
if not alt_buttons:
fingerprint[0][0x1CF] = 8
CP = CarInterface.get_params(CAR.KIA_EV6, fingerprint, [], True, False, False, toggles)
FPCP = CarInterface.get_starpilot_params(CAR.KIA_EV6, fingerprint, [], CP, toggles)
CS = CarState(CP, FPCP)
parsers = CS.get_can_parsers(CP)
packer = CANPacker(DBC[CP.carFingerprint][Bus.pt])
buttons_msg = CS.cruise_btns_msg_canfd
frame = 0
def update(samples, acc_available=True):
nonlocal frame
frame += 1
msgs = [packer.make_can_msg(buttons_msg, CanBus(CP).ECAN, {
"ADAPTIVE_CRUISE_MAIN_BTN": main, "LDA_BTN": lkas, "CRUISE_BUTTONS": cruise,
}) for main, lkas, cruise in samples]
msgs.append(packer.make_can_msg("TCS", CanBus(CP).ECAN, {"ACCEnable": 0 if acc_available else 1}))
parsers[Bus.pt].update([(frame * 10_000_000, msgs)])
CS.update(parsers, toggles)
return CS.ev6_aol_authorized
assert not update([(0, 0, Buttons.NONE)])
assert update([(0, 1, Buttons.NONE), (0, 0, Buttons.NONE)])
assert update([])
assert not update([(0, 1, Buttons.NONE), (0, 0, Buttons.NONE)])
assert update([(1, 0, Buttons.NONE), (1, 0, Buttons.NONE)])
assert update([(0, 0, Buttons.NONE)])
assert not update([(1, 0, Buttons.NONE), (0, 0, Buttons.NONE)])
assert update([(1, 0, Buttons.NONE), (0, 0, Buttons.NONE)])
assert not update([], acc_available=False)
assert not update([])
assert not update([(0, 0, Buttons.SET_DECEL)])
assert update([(0, 0, Buttons.NONE)]) == lkas_on_engage
assert update([]) == lkas_on_engage
@pytest.mark.parametrize("button", (Buttons.SET_DECEL, Buttons.RES_ACCEL))
@pytest.mark.parametrize("lkas_on_engage", (False, True))
def test_ev6_aol_engagement_latch_matches_safety_flag(self, button, lkas_on_engage):
state = Ev6AolArmingState(lkas_on_engage)
state.update(False, False, button)
assert not state.authorized
state.update(False, False, Buttons.NONE)
assert state.authorized == lkas_on_engage
state.update(False, False, Buttons.CANCEL)
assert state.authorized == lkas_on_engage
state.update(False, True, Buttons.NONE)
assert state.authorized != lkas_on_engage
def test_egmp_communication_control_paths(self):
stock_request = bytes([0x28, 0x83, 0x01])
radar_keepalive_request = bytes([0x28, 0x01, 0x01])
@@ -281,9 +281,10 @@ class CarController(CarControllerBase):
self.doors_locked = False
self.brake_hold_active = False
self._brake_hold_counter = 0
self._auto_hold_rearm_blocked = False
def _compute_interceptor_gas_cmd(self, CC, CS):
if not (self.CP.enableGasInterceptorDEPRECATED and self.CP.openpilotLongitudinalControl and CC.longActive):
if self.brake_hold_active or not (self.CP.enableGasInterceptorDEPRECATED and self.CP.openpilotLongitudinalControl and CC.longActive):
return 0.0
if CS.out.standstill:
@@ -327,17 +328,31 @@ class CarController(CarControllerBase):
self.last_standstill = CS.out.standstill
def update_auto_hold_state(self, CS: structs.CarState, cancel_requested: bool = False,
activation_frames: int = TOYOTA_AUTO_HOLD_ACTIVATION_FRAMES):
activation_frames: int = TOYOTA_AUTO_HOLD_ACTIVATION_FRAMES, *,
long_active: bool = False, stopping: bool = False):
brake_hold_allowed = (not cancel_requested and 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 or long_active) and
CS.out.gearShifter not in (PARK, REVERSE))
if brake_hold_allowed and not self.brake_hold_active and CS.out.brakePressed:
self._brake_hold_counter += 1
self.brake_hold_active = self._brake_hold_counter > activation_frames
elif not brake_hold_allowed:
self._brake_hold_counter = 0
self.brake_hold_active = False
if not brake_hold_allowed:
# A gas tap releases this stop, even if the pedal is lifted before the
# wheels start moving. Re-arm once moving or after another brake press.
rearm_blocked = CS.out.standstill and (self._auto_hold_rearm_blocked or CS.out.gasPressed)
self.reset_auto_hold_state()
self._auto_hold_rearm_blocked = rearm_blocked
elif not self.brake_hold_active:
if CS.out.brakePressed:
self._auto_hold_rearm_blocked = False
cruise_stop = long_active and stopping and CS.out.cruiseState.enabled and not self._auto_hold_rearm_blocked
if cruise_stop:
# Latch immediately at a cruise-controlled stop; planner resume requests
# must not release the brakes until the driver presses the gas.
self.brake_hold_active = True
elif CS.out.brakePressed and not CS.out.cruiseState.enabled:
self._brake_hold_counter += 1
self.brake_hold_active = self._brake_hold_counter > activation_frames
else:
self._brake_hold_counter = 0
return self.brake_hold_active
@@ -358,8 +373,11 @@ class CarController(CarControllerBase):
return []
def reset_auto_hold_state(self):
if self.brake_hold_active and self.CP.carFingerprint not in TOYOTA_AUTO_HOLD_AEB_CARS:
self.standstill_req = False
self._brake_hold_counter = 0
self.brake_hold_active = False
self._auto_hold_rearm_blocked = False
def update(self, CC, CS, now_nanos, starpilot_toggles):
actuators = CC.actuators
@@ -460,7 +478,9 @@ class CarController(CarControllerBase):
if self.CP.carFingerprint in TOYOTA_AUTO_HOLD_AEB_CARS:
can_sends.extend(self.create_auto_brake_hold_messages(CS))
else:
self.update_auto_hold_state(CS, pcm_cancel_cmd)
self.update_auto_hold_state(CS, pcm_cancel_cmd, long_active=CC.longActive, stopping=stopping)
if self._auto_hold_rearm_blocked:
self.standstill_req = False
else:
self.reset_auto_hold_state()
@@ -794,6 +794,7 @@ class TestToyotaCarController:
controller.accel = 0.0
controller.brake_hold_active = False
controller._brake_hold_counter = 0
controller._auto_hold_rearm_blocked = False
return controller
@staticmethod
@@ -1093,6 +1094,17 @@ class TestToyotaCarController:
assert gas_cmd == 0.12
def test_interceptor_does_not_apply_gas_during_auto_hold(self):
controller = self._make_controller()
controller.CP.enableGasInterceptorDEPRECATED = True
controller.accel = 1.5
controller.brake_hold_active = True
assert controller._compute_interceptor_gas_cmd(
SimpleNamespace(longActive=True),
SimpleNamespace(out=SimpleNamespace(standstill=True, vEgo=0.0)),
) == 0.0
def test_interceptor_non_stop_and_go_scales_with_accel_request(self):
controller = self._make_controller()
controller.CP.enableGasInterceptorDEPRECATED = True
@@ -1225,6 +1237,181 @@ class TestToyotaCarController:
assert CP.stopAccel == -1.5
class TestToyotaAutoHoldCruise:
@staticmethod
def _make_car(*, candidate=CAR.TOYOTA_RAV4_TSS2, enabled=True, capability=True):
cp = CarInterface.get_non_essential_params(candidate)
cp.flags &= ~ToyotaFlags.AUTO_BRAKE_HOLD.value
if capability:
cp.flags |= ToyotaFlags.AUTO_BRAKE_HOLD.value
controller = CarController(DBC[candidate], cp)
cc = structs.CarControl(enabled=True, longActive=True)
cc.actuators.accel = -0.7
cc.actuators.longControlState = structs.CarControl.Actuators.LongControlState.stopping
cc.hudControl.leadDistanceBars = 3
cs = SimpleNamespace(
out=structs.CarState(
standstill=True,
gearShifter=structs.CarState.GearShifter.drive,
cruiseState=structs.CarState.CruiseState(available=True, enabled=True, standstill=True),
),
gvc=0.0,
acc_type=1,
pcm_acc_status=7,
pcm_follow_distance=1,
lkas_hud={},
pre_collision_2={},
)
toggles = SimpleNamespace(toyota_auto_hold=enabled, sng_hack=False, lock_doors=False, unlock_doors=False)
parser = CANParser(DBC[candidate][Bus.pt], [("ACC_CONTROL", 0)], 0)
return SimpleNamespace(controller=controller, cc=cc, cs=cs, toggles=toggles, parser=parser)
@staticmethod
def _tick(car):
# Run through a complete ACC_CONTROL send interval and decode the actual
# controller output, including the ordinary standstill and PID paths.
for _ in range(3):
now_nanos = car.controller.frame * 10_000_000
_, messages = car.controller.update(car.cc.as_reader(), car.cs, now_nanos, car.toggles)
car.parser.update([(now_nanos, messages)])
return car.parser.vl["ACC_CONTROL"]
@pytest.mark.parametrize("sng_hack", [False, True])
def test_cruise_stop_blocks_planner_resume_until_gas(self, sng_hack):
car = self._make_car()
command = self._tick(car)
assert car.controller.brake_hold_active
assert command["ACCEL_CMD"] == -1.0
# Reproduce the report: no brake-pedal input, then the planner changes
# from stopping to starting and asks to resume while still stationary.
car.cc.actuators.accel = 1.5
car.cc.actuators.longControlState = structs.CarControl.Actuators.LongControlState.starting
car.cc.cruiseControl.resume = True
car.toggles.sng_hack = sng_hack
for _ in range(10):
command = self._tick(car)
assert car.controller.brake_hold_active
assert command["ACCEL_CMD"] == -1.0
assert command["PERMIT_BRAKING"] == 1
assert command["RELEASE_STANDSTILL"] == 0
car.cs.out.gasPressed = True
car.cc.longActive = False
car.cc.actuators.accel = 0.0
command = self._tick(car)
assert not car.controller.brake_hold_active
assert command["ACCEL_CMD"] == 0.0
assert command["RELEASE_STANDSTILL"] == 1
def test_gas_tap_does_not_rehold_until_the_next_stop(self):
car = self._make_car()
self._tick(car)
assert car.controller.brake_hold_active
car.cs.out.gasPressed = True
car.cc.longActive = False
car.cc.actuators.accel = 0.0
self._tick(car)
car.cs.out.gasPressed = False
car.cc.longActive = True
car.cc.actuators.accel = -0.7
for _ in range(10):
command = self._tick(car)
assert not car.controller.brake_hold_active
assert command["RELEASE_STANDSTILL"] == 1
car.cs.out.standstill = False
car.cs.out.vEgo = car.cs.out.vEgoRaw = 1.0
car.cs.out.cruiseState.standstill = False
self._tick(car)
car.cs.out.standstill = True
car.cs.out.vEgo = car.cs.out.vEgoRaw = 0.0
car.cs.out.cruiseState.standstill = True
command = self._tick(car)
assert car.controller.brake_hold_active
assert command["ACCEL_CMD"] == -1.0
assert command["RELEASE_STANDSTILL"] == 0
def test_manual_stop_still_holds_with_only_aol_active(self):
car = self._make_car()
car.cc.enabled = False
car.cc.longActive = False
car.cc.latActive = True
car.cc.actuators.accel = 0.0
car.cs.out.cruiseState.enabled = False
car.cs.out.brakePressed = True
# Include the first ACC_CONTROL transmission after the one-second latch.
for _ in range(35):
command = self._tick(car)
assert car.controller.brake_hold_active
assert command["ACCEL_CMD"] == -1.0
assert command["RELEASE_STANDSTILL"] == 0
car.cs.out.brakePressed = False
command = self._tick(car)
assert car.controller.brake_hold_active
assert command["ACCEL_CMD"] == -1.0
car.cs.out.gasPressed = True
command = self._tick(car)
assert not car.controller.brake_hold_active
assert command["ACCEL_CMD"] == 0.0
assert command["RELEASE_STANDSTILL"] == 1
@pytest.mark.parametrize("long_state", ["off", "pid", "starting"])
def test_stationary_cruise_does_not_latch_without_a_stopping_request(self, long_state):
car = self._make_car()
car.cc.actuators.longControlState = getattr(structs.CarControl.Actuators.LongControlState, long_state)
car.cc.actuators.accel = 1.5
self._tick(car)
assert not car.controller.brake_hold_active
@pytest.mark.parametrize("enabled,capability,candidate", [
(False, True, CAR.TOYOTA_RAV4_TSS2),
(True, False, CAR.TOYOTA_RAV4_TSS2),
(True, True, CAR.TOYOTA_CAMRY_TSS2),
])
def test_cruise_hold_requires_toggle_capability_and_acc_hold_path(self, enabled, capability, candidate):
car = self._make_car(candidate=candidate, enabled=enabled, capability=capability)
self._tick(car)
assert not car.controller.brake_hold_active
car.cc.actuators.accel = 1.5
car.cc.actuators.longControlState = structs.CarControl.Actuators.LongControlState.starting
car.cc.cruiseControl.resume = True
# Allow the ordinary acceleration rate limiter to ramp through zero.
for _ in range(20):
command = self._tick(car)
assert not car.controller.brake_hold_active
assert command["ACCEL_CMD"] > 0
assert command["RELEASE_STANDSTILL"] == 1
@pytest.mark.parametrize("release", ["toggle", "cancel", "main", "park", "reverse", "moving", "long_inactive"])
def test_cruise_hold_releases_when_conditions_change(self, release):
car = self._make_car()
self._tick(car)
assert car.controller.brake_hold_active
if release == "toggle":
car.toggles.toyota_auto_hold = False
elif release == "cancel":
car.cc.cruiseControl.cancel = True
elif release == "main":
car.cs.out.cruiseState.available = False
elif release in ("park", "reverse"):
car.cs.out.gearShifter = getattr(structs.CarState.GearShifter, release)
elif release == "moving":
car.cs.out.standstill = False
else:
car.cc.longActive = False
self._tick(car)
assert not car.controller.brake_hold_active
class TestToyotaCarState:
@pytest.mark.parametrize("candidate", [CAR.TOYOTA_PRIUS, CAR.TOYOTA_PRIUS_RETROFIT])
def test_legacy_prius_distance_button_generates_events(self, candidate):
@@ -502,6 +502,18 @@ class TestFordMachEExtendedCurvatureSafety(TestFordCANFDStockSafety):
self._reset_curvature_measurement(0.02, 9.0)
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, 0.055, 0.02, 0.0)))
def test_mach_e_aol_keeps_curvature_without_path_angle_assist(self):
self.safety.set_alternative_experience(32)
self._rx(self._toggle_aol(True))
self.assertTrue(self.safety.get_aol_allowed())
self.assertFalse(self.safety.get_controls_allowed())
self.assertTrue(self._tx(self._extended_lka_msg()))
for sign in (-1, 1):
self._reset_curvature_measurement(sign * 0.02, 7.5)
self._set_prev_desired_angle(sign * 0.02)
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, sign * 0.055, sign * 0.02, 0.0)))
self.assertTrue(self._tx(self._lat_ctl_msg(True, 0.0, 0.0, sign * 0.02, 0.0)))
def test_other_canfd_fords_keep_original_error(self):
self.safety.set_safety_hooks(CarParams.SafetyModel.ford, FordSafetyFlags.CANFD)
self.safety.init_tests()
+4 -1
View File
@@ -370,7 +370,10 @@ class Car:
self.CP, self.CI.CS, self.sm['pandaStates'], self.sm.all_checks(['pandaStates']),
)
self.CI.CS.preap_lateral_authorized = preap_authorized
FPCS = self.starpilot_card.update(CS, FPCS, self.sm, self.starpilot_toggles, preap_authorized=preap_authorized)
FPCS = self.starpilot_card.update(
CS, FPCS, self.sm, self.starpilot_toggles, preap_authorized=preap_authorized,
ev6_aol_authorized=getattr(self.CI.CS, 'ev6_aol_authorized', False),
)
return CS, RD, FPCS
def state_publish(self, CS: car.CarState, RD: structs.RadarDataT | None, FPCS: custom.StarPilotCarState):
+11 -4
View File
@@ -38,8 +38,8 @@ MACH_E_TURN_IN_LOOKAHEAD_EXTRA = 0.80
MACH_E_LOW_SPEED_TURN_IN_LOOKAHEAD_EXTRA = 1.60
MACH_E_LOW_SPEED_TURN_IN_START_SPEED = 2.0
MACH_E_LOW_SPEED_TURN_IN_FULL_SPEED = 3.0
MACH_E_LOW_SPEED_TURN_IN_MAX_SPEED = 11.0
MACH_E_LOW_SPEED_TURN_IN_FADE_SPEED = 14.0
MACH_E_LOW_SPEED_TURN_IN_MAX_SPEED = 12.0
MACH_E_LOW_SPEED_TURN_IN_FADE_SPEED = 15.0
MACH_E_TURN_IN_MIN_CURVATURE = 0.002
MACH_E_TURN_IN_FULL_CURVATURE = 0.008
MACH_E_TURN_IN_LAG_CURVATURE = 0.006
@@ -491,8 +491,12 @@ class FordLateralController:
self.manual_turn_direction = 0.0
return False
if (CS.out.steeringPressed or blinker_direction != 0.0 or
abs(CS.out.steeringAngleDeg) > MANUAL_TURN_RELEASE_ANGLE_DEG):
following_next_curve = (
driver_assisting and not CS.out.leftBlinker and not CS.out.rightBlinker and
CS.out.vEgoRaw >= MACH_E_DIRECTION_CHANGE_MIN_SPEED and self.manual_turn_direction * desired < 0.0
)
if not following_next_curve and (CS.out.steeringPressed or blinker_direction != 0.0 or
abs(CS.out.steeringAngleDeg) > MANUAL_TURN_RELEASE_ANGLE_DEG):
self.manual_turn_recovery_timer = 0.0
else:
self.manual_turn_recovery_timer += STEER_DT
@@ -604,6 +608,9 @@ class FordLateralController:
applied = float(np.clip(applied, -max_curvature, max_curvature))
path_angle = self._path_angle_assist(
requested, desired, applied, current, v_ego, driver_override, self._lane_change()[0])
if path_angle != 0.0 and (not CC.enabled or CS.out.gasPressed or CS.out.brakePressed):
self.path_angle_last = 0.0
path_angle = 0.0
self.curvature_samples.append(predicted)
curvature_rate = 0.0
+124 -6
View File
@@ -33,7 +33,7 @@ def controller(monkeypatch):
def car_state(speed=15.0, accel=0.0, curvature=0.0, steering_pressed=False, steering_angle=0.0,
steering_torque=0.0, left_blinker=False, right_blinker=False):
steering_torque=0.0, left_blinker=False, right_blinker=False, gas_pressed=False, brake_pressed=False):
return SimpleNamespace(out=SimpleNamespace(
vEgoRaw=speed,
aEgo=accel,
@@ -43,6 +43,8 @@ def car_state(speed=15.0, accel=0.0, curvature=0.0, steering_pressed=False, stee
steeringTorque=steering_torque,
leftBlinker=left_blinker,
rightBlinker=right_blinker,
gasPressed=gas_pressed,
brakePressed=brake_pressed,
))
@@ -337,6 +339,37 @@ def test_mach_e_path_angle_assist_is_encoded_with_curvature(controller):
assert encoded_angle == pytest.approx(-assist)
@pytest.mark.parametrize("sign", (-1, 1))
@pytest.mark.parametrize("enabled,gas_pressed,brake_pressed", (
(False, False, False),
(False, True, False),
(True, True, False),
(True, False, True),
))
def test_mach_e_assist_falls_back_to_curvature_when_not_permitted(
controller, monkeypatch, sign, enabled, gas_pressed, brake_pressed):
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
controller.CP.flags = FordFlags.CANFD
controller.curvature_last = sign * 0.02
controller.path_angle_last = sign * 0.16
monkeypatch.setattr(controller, "_predicted_curvature", lambda *_: sign * 0.04)
state = car_state(speed=7.0, curvature=sign * 0.007,
gas_pressed=gas_pressed, brake_pressed=brake_pressed)
actuators = SimpleNamespace(curvature=sign * 0.03)
CC = SimpleNamespace(latActive=True, enabled=enabled)
for _ in range(10):
result = controller.update(CC, state, actuators)
assert result.active
assert result.curvature == pytest.approx(sign * 0.02)
assert result.path_angle == controller.path_angle_last == 0.0
CC.enabled = True
state.out.gasPressed = state.out.brakePressed = False
result = controller.update(CC, state, actuators)
assert result.curvature == pytest.approx(sign * 0.02)
assert result.path_angle == pytest.approx(sign * 0.055)
@pytest.mark.parametrize("sign", (-1, 1))
@pytest.mark.parametrize("speed,expected", ((1.9, False), (2.0, True), (3.0, True), (7.0, True),
(11.0, True), (14.9, True), (15.0, False)))
@@ -387,7 +420,7 @@ def test_mach_e_driver_assistance_handoff_and_takeover(controller, monkeypatch,
controller.CP.flags = FordFlags.CANFD
controller.curvature_last = sign * 0.020
monkeypatch.setattr(controller, "_predicted_curvature", lambda *_: sign * 0.030)
CC = SimpleNamespace(latActive=True)
CC = SimpleNamespace(latActive=True, enabled=True)
actuators = SimpleNamespace(curvature=sign * 0.022)
helping = car_state(speed=7.0, curvature=sign * 0.008, steering_pressed=True,
steering_angle=-sign * 50.0, steering_torque=-sign * 2.0,
@@ -441,7 +474,7 @@ def test_mach_e_driver_help_at_early_curve_entry(controller, monkeypatch, sign):
state = car_state(speed=2.7, curvature=sign * 0.003, steering_pressed=True,
steering_torque=-sign * 2.0, steering_angle=-sign * 15.0,
left_blinker=sign < 0, right_blinker=sign > 0)
CC = SimpleNamespace(latActive=True)
CC = SimpleNamespace(latActive=True, enabled=True)
assert controller.update(CC, state, SimpleNamespace(curvature=sign * 0.004)).active
state.out.vEgoRaw = 3.5
state.out.yawRate = -sign * 0.004 * state.out.vEgoRaw
@@ -683,10 +716,11 @@ def test_mach_e_turn_in_preview_is_not_carried_into_unwind(controller):
(9.0, 1.60),
(10.5, 1.60),
(11.0, 1.60),
(12.0, 4.0 / 3.0),
(13.0, 16.0 / 15.0),
(14.0, 0.80),
(12.0, 1.60),
(13.0, 4.0 / 3.0),
(14.0, 16.0 / 15.0),
(15.0, 0.80),
(16.0, 0.80),
))
def test_mach_e_turn_in_lookahead_extra_fades_by_speed(controller, speed, expected):
assert controller._turn_in_lookahead_extra(speed) == pytest.approx(expected)
@@ -1334,6 +1368,90 @@ def test_mach_e_manual_turn_releases_for_opposite_path_request(controller):
assert controller.update(CC, car_state(curvature=-0.001), CC.actuators).active
@pytest.mark.parametrize("sign", (-1.0, 1.0))
@pytest.mark.parametrize("speed", (9.0, 9.5, 14.99))
def test_mach_e_manual_turn_hands_off_to_driver_assisted_opposite_curve(controller, monkeypatch, sign, speed):
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
controller.CP.flags = FordFlags.CANFD
CC = SimpleNamespace(latActive=True, enabled=True)
actuators = SimpleNamespace(curvature=sign * 0.006)
turning = car_state(speed=speed, curvature=sign * 0.004, steering_pressed=True,
steering_angle=-sign * 30.0, steering_torque=-sign * 2.0,
left_blinker=sign < 0.0, right_blinker=sign > 0.0)
assert not controller.update(CC, turning, actuators).active
assert controller.manual_turn_direction == sign
monkeypatch.setattr(controller, "_predicted_curvature", lambda *_: -sign * 0.010)
actuators.curvature = -sign * 0.006
following = car_state(speed=speed, curvature=-sign * 0.004, steering_pressed=True,
steering_angle=sign * 20.0, steering_torque=sign * 2.0)
for _ in range(4):
assert not controller.update(CC, following, actuators).active
result = controller.update(CC, following, actuators)
assert result.active
assert -sign * result.curvature > 0.0
assert abs(result.curvature) <= 0.0025
assert result.path_angle == 0.0
assert not controller.manual_turn_latched
assert controller.manual_turn_direction == 0.0
assert controller.manual_turn_recovery_timer == 0.0
@pytest.mark.parametrize("sign", (-1.0, 1.0))
@pytest.mark.parametrize("speed,current,torque,preview,left,right,lane_change", (
(4.5, -0.004, 2.0, -0.010, False, False, False),
(8.99, -0.004, 2.0, -0.010, False, False, False),
(15.0, -0.004, 2.0, -0.010, False, False, False),
(9.5, 0.004, 2.0, -0.010, False, False, False),
(9.5, -0.009, 2.0, -0.010, False, False, False),
(9.5, -0.004, -2.0, -0.010, False, False, False),
(9.5, -0.004, 3.6, -0.010, False, False, False),
(9.5, -0.004, 2.0, 0.010, False, False, False),
(9.5, -0.004, 2.0, -0.007, False, False, False),
(9.5, -0.004, 2.0, -0.010, True, False, False),
(9.5, -0.004, 2.0, -0.010, False, True, False),
(9.5, -0.004, 2.0, -0.010, True, True, False),
(9.5, -0.004, 2.0, -0.010, False, False, True),
))
def test_mach_e_opposite_curve_handoff_preserves_manual_override(
controller, monkeypatch, sign, speed, current, torque, preview, left, right, lane_change):
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
controller.CP.flags = FordFlags.CANFD
controller.manual_turn_latched = True
controller.manual_turn_direction = sign
monkeypatch.setattr(controller, "_predicted_curvature", lambda *_: sign * preview)
monkeypatch.setattr(controller, "_lane_change", lambda: (lane_change, 0))
state = car_state(speed=speed, curvature=sign * current, steering_pressed=True,
steering_angle=sign * 25.0, steering_torque=sign * torque,
left_blinker=left, right_blinker=right)
for _ in range(8):
result = controller.update(SimpleNamespace(latActive=True, enabled=True), state,
SimpleNamespace(curvature=-sign * 0.006))
assert not result.active
assert result.curvature == result.path_angle == 0.0
assert controller.manual_turn_recovery_timer == 0.0
def test_mach_e_opposite_curve_handoff_requires_uninterrupted_agreement(controller, monkeypatch):
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
controller.CP.flags = FordFlags.CANFD
controller.manual_turn_latched = True
controller.manual_turn_direction = 1.0
monkeypatch.setattr(controller, "_predicted_curvature", lambda *_: -0.010)
CC = SimpleNamespace(latActive=True, enabled=True)
actuators = SimpleNamespace(curvature=-0.006)
state = car_state(speed=9.5, curvature=-0.004, steering_pressed=True,
steering_angle=20.0, steering_torque=2.0)
for _ in range(4):
assert not controller.update(CC, state, actuators).active
state.out.steeringTorque = -2.0
assert not controller.update(CC, state, actuators).active
assert controller.manual_turn_recovery_timer == 0.0
state.out.steeringTorque = 2.0
for _ in range(4):
assert not controller.update(CC, state, actuators).active
assert controller.update(CC, state, actuators).active
def test_non_mach_e_signaled_turn_does_not_latch(controller):
CC = SimpleNamespace(latActive=True)
result = controller.update(CC, car_state(
@@ -3652,8 +3652,8 @@
{
"key": "ToyotaAutoHold",
"label": "Toyota Auto Hold",
"description": "Hold the brakes at a stop on supported Toyota/Lexus vehicles when cruise main is available and cruise is not active.",
"picker_description": "Holds supported Toyota/Lexus brakes at stops when cruise is available.",
"description": "Hold the brakes after a manual stop with cruise main on. Supported models can also hold after a cruise-controlled stop until you press the gas pedal.",
"picker_description": "Holds supported Toyota/Lexus brakes at stops until you press the gas pedal.",
"data_type": "bool",
"ui_type": "toggle",
"galaxy_only": true,
+12 -1
View File
@@ -73,6 +73,11 @@ class StarPilotCard:
self.CP.brand == "hyundai" and not (hyundai_flags & HyundaiFlags.CANFD) and not hyundai_aol_before_engagement
)
self.hyundai_aol_ready = False
self.ev6_aol_needs_arming = (
getattr(self.CP, "carFingerprint", None) == HYUNDAI_CAR.KIA_EV6 and
getattr(self.CP, "openpilotLongitudinalControl", False) and not getattr(self.CP, "pcmCruise", False)
)
self.ev6_aol_authorized = False
self.g70_main_cruise_aol_pending = False
self.g70_main_cruise_aol_pending_frames = 0
self.prev_cruise_available = None
@@ -167,6 +172,8 @@ class StarPilotCard:
def _toggle_controller_aol(self, carState, starpilot_toggles, main_cruise_aol=False):
if not self.always_on_lateral_supported or not getattr(starpilot_toggles, "always_on_lateral", False):
return False
if self.ev6_aol_needs_arming and not self.ev6_aol_authorized:
return False
tesla_disengage_on_brake = (
self.CP.brand == "tesla" and
getattr(starpilot_toggles, "tesla_aol_disengage_on_brake", False)
@@ -237,7 +244,8 @@ class StarPilotCard:
else:
self.params.put_bool_nonblocking("ExperimentalMode", not sm["selfdriveState"].experimentalMode)
def update(self, carState, starpilotCarState, sm, starpilot_toggles, *, preap_authorized=False):
def update(self, carState, starpilotCarState, sm, starpilot_toggles, *, preap_authorized=False, ev6_aol_authorized=False):
self.ev6_aol_authorized = ev6_aol_authorized
self.switchback_mode_enabled = self.params_memory.get_bool("SwitchbackModeEnabled")
self._handle_favorite_traffic_mode_action(sm)
@@ -487,6 +495,9 @@ class StarPilotCard:
self._handle_controller_actions(carState, sm, starpilot_toggles, main_cruise_aol)
if self.ev6_aol_needs_arming and not ev6_aol_authorized:
self.always_on_lateral_allowed = False
self.always_on_lateral_enabled = self.always_on_lateral_allowed and self.always_on_lateral_set
if getattr(self.CP, "carFingerprint", None) == "TESLA_MODEL_S_PREAP":
self.always_on_lateral_enabled &= preap_authorized
@@ -295,6 +295,77 @@ def make_wrapped_button_event(button_type, pressed):
return SimpleNamespace(type=SimpleNamespace(raw=int(button_type)), pressed=pressed)
@pytest.mark.parametrize("fingerprint", tuple(spc.HYUNDAI_CAR))
@pytest.mark.parametrize("openpilot_long, pcm_cruise", ((True, False), (False, True), (True, True)))
def test_ev6_arming_gate_is_limited_to_ev6_openpilot_long(monkeypatch, tmp_path, fingerprint, openpilot_long, pcm_cruise):
monkeypatch.setattr(spc, "Params", FakeParams)
monkeypatch.setattr(spc, "ERROR_LOGS_PATH", tmp_path)
card = spc.StarPilotCard(
SimpleNamespace(brand="hyundai", carFingerprint=fingerprint, flags=spc.HyundaiFlags.CANFD,
openpilotLongitudinalControl=openpilot_long, pcmCruise=pcm_cruise),
SimpleNamespace(alternativeExperience=32),
)
needs_arming = fingerprint == spc.HYUNDAI_CAR.KIA_EV6 and openpilot_long and not pcm_cruise
assert card.ev6_aol_needs_arming == needs_arming
toggles = make_toggles(always_on_lateral=True, always_on_lateral_main=True)
ret = card.update(make_car_state(available=True), SimpleNamespace(distancePressed=False), make_sm(), toggles)
assert ret.alwaysOnLateralEnabled == (card.always_on_lateral_supported and not needs_arming)
@pytest.mark.parametrize("lkas_mapping", (False, True))
def test_ev6_aol_requires_physical_authorization_even_for_controller_actions(monkeypatch, tmp_path, lkas_mapping):
monkeypatch.setattr(spc, "Params", FakeParams)
monkeypatch.setattr(spc, "ERROR_LOGS_PATH", tmp_path)
card = spc.StarPilotCard(
SimpleNamespace(brand="hyundai", carFingerprint=spc.HYUNDAI_CAR.KIA_EV6, flags=spc.HyundaiFlags.CANFD,
openpilotLongitudinalControl=True, pcmCruise=False),
SimpleNamespace(alternativeExperience=32),
)
toggles = make_toggles(always_on_lateral=True, always_on_lateral_main=not lkas_mapping,
always_on_lateral_lkas=lkas_mapping)
sm = make_sm()
output = SimpleNamespace(distancePressed=False)
cs = make_car_state(available=True)
ret = card.update(cs, output, sm, toggles)
assert not ret.alwaysOnLateralAllowed
assert not ret.alwaysOnLateralEnabled
counter = spc.CONTROLLER_ACTION_COUNTERS[spc.CONTROLLER_ACTION_TOGGLE_AOL]
card.params_memory.put_int(counter, 1)
ret = card.update(cs, output, sm, toggles)
assert not ret.alwaysOnLateralAllowed
assert not ret.alwaysOnLateralEnabled
cs.buttonEvents = [make_wrapped_button_event(spc.ButtonType.lkas, True)]
ret = card.update(cs, output, sm, toggles, ev6_aol_authorized=True)
assert ret.alwaysOnLateralEnabled
cs.buttonEvents = []
ret = card.update(cs, output, sm, toggles, ev6_aol_authorized=True)
assert ret.alwaysOnLateralEnabled
ret = card.update(cs, output, sm, toggles, ev6_aol_authorized=False)
assert not ret.alwaysOnLateralAllowed
assert not ret.alwaysOnLateralEnabled
def test_ev6_lkas_experimental_mapping_still_runs_when_armed(monkeypatch, tmp_path):
monkeypatch.setattr(spc, "Params", FakeParams)
monkeypatch.setattr(spc, "ERROR_LOGS_PATH", tmp_path)
card = spc.StarPilotCard(
SimpleNamespace(brand="hyundai", carFingerprint=spc.HYUNDAI_CAR.KIA_EV6, flags=spc.HyundaiFlags.CANFD,
openpilotLongitudinalControl=True, pcmCruise=False),
SimpleNamespace(alternativeExperience=32),
)
toggles = make_toggles(always_on_lateral=True, always_on_lateral_main=True, experimental_mode_via_lkas=True,
experimental_mode_available=True)
sm = make_sm()
sm["carControl"].latActive = True
cs = make_car_state(available=True, button_events=[make_wrapped_button_event(spc.ButtonType.lkas, True)])
ret = card.update(cs, SimpleNamespace(distancePressed=False), sm, toggles, ev6_aol_authorized=True)
assert ret.alwaysOnLateralEnabled
assert card.params.get_bool("ExperimentalMode")
@pytest.mark.parametrize("pending_before_press", [False, True])
def test_slc_confirmation_release_does_not_republish_accel(monkeypatch, tmp_path, pending_before_press):
monkeypatch.setattr(spc, "Params", FakeParams)