diff --git a/common/libcommon.a b/common/libcommon.a index 86b28a2fc..35b5b51c6 100644 Binary files a/common/libcommon.a and b/common/libcommon.a differ diff --git a/common/params_keys.h b/common/params_keys.h index 82932ba5c..03dfb85d4 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -617,6 +617,8 @@ inline static std::unordered_map keys = { {"VisionSpeedLimitAutoBookmark", {PERSISTENT, BOOL, "0", "0", 0}}, {"VisionSpeedLimitAutoPreserveSegment", {PERSISTENT, BOOL, "0", "0", 0}}, {"VisionSpeedLimitDetection", {PERSISTENT, BOOL, "1", "0", 0}}, + {"VisionSpeedLimitLowLimitFilter", {PERSISTENT, BOOL, "0", "0", 0}}, + {"VisionSpeedLimitLowLimitThreshold", {PERSISTENT, INT, "25", "25", 0}}, {"VisionSpeedLimitTrainingCollector", {PERSISTENT, BOOL, "1", "1", 0}}, {"StandardFollow", {PERSISTENT, FLOAT, "1.45", "1.45", 2}}, {"StandardFollowHigh", {PERSISTENT, FLOAT, "1.2", "1.2", 2}}, diff --git a/common/params_pyx.so b/common/params_pyx.so index c405a0ef6..95f141eec 100755 Binary files a/common/params_pyx.so and b/common/params_pyx.so differ diff --git a/opendbc_repo/opendbc/car/hyundai/carstate.py b/opendbc_repo/opendbc/car/hyundai/carstate.py index 6b8e210fa..e2648949a 100644 --- a/opendbc_repo/opendbc/car/hyundai/carstate.py +++ b/opendbc_repo/opendbc/car/hyundai/carstate.py @@ -233,7 +233,9 @@ class CarState(CarStateBase): return button_events def create_lkas_button_events(self, cp: CANParser, prev_lda_button: int) -> list[structs.CarState.ButtonEvent]: - if self.CP.carFingerprint == CAR.HYUNDAI_SONATA_HYBRID: + if self.CP.carFingerprint == CAR.HYUNDAI_SONATA: + self.lda_button = int(cp.vl["BCM_PO_11"]["LDA_BTN"]) if cp.ts_nanos["BCM_PO_11"]["LDA_BTN"] > 0 else 0 + elif self.CP.carFingerprint == CAR.HYUNDAI_SONATA_HYBRID: self.lda_button = self.get_sonata_hybrid_lkas_button_state(cp) else: source_states = ( @@ -425,9 +427,6 @@ class CarState(CarStateBase): *create_button_events(self.main_buttons[-1], prev_main_buttons, {1: ButtonType.mainCruise}), *lkas_button_events] - if getattr(self.FPCP, "flags", 0) & HyundaiStarPilotFlags.MAIN_CRUISE_STATE_TRACKING: - ret.cruiseState.available = self.update_main_cruise(ret) - ret.blockPcmEnable = not self.recent_button_interaction() # low speed steer alert hysteresis logic (only for cars with steer cut off above 10 m/s) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py index 5f8f4b914..66d548278 100644 --- a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py @@ -676,20 +676,14 @@ class TestHyundaiFingerprint: ) assert not (minimal_fpcp.safetyConfigs[-1].safetyParam & HyundaiStarPilotSafetyFlags.AOL_MAIN_LKAS_SYNC) - def test_classic_hyundai_long_tracks_main_cruise_state(self): + @pytest.mark.parametrize("candidate", (CAR.HYUNDAI_ELANTRA_2021, CAR.HYUNDAI_SONATA_HYBRID)) + def test_legacy_hyundai_long_does_not_gate_availability_on_main_cruise(self, candidate): toggles = get_test_toggles() - classic_cp = CarInterface.get_params(CAR.HYUNDAI_ELANTRA_2021, gen_empty_fingerprint(), [], True, False, False, toggles) - classic_fpcp = CarInterface.get_starpilot_params( - CAR.HYUNDAI_ELANTRA_2021, gen_empty_fingerprint(), [], classic_cp, toggles, + CP = CarInterface.get_params(candidate, gen_empty_fingerprint(), [], True, False, False, toggles) + FPCP = CarInterface.get_starpilot_params( + candidate, gen_empty_fingerprint(), [], CP, toggles, ) - assert classic_fpcp.flags & HyundaiStarPilotFlags.MAIN_CRUISE_STATE_TRACKING - - car_state = CarState(classic_cp, classic_fpcp) - ret = SimpleNamespace( - cruiseState=SimpleNamespace(available=True), - buttonEvents=[structs.CarState.ButtonEvent(pressed=True, type=ButtonType.mainCruise)], - ) - assert car_state.update_main_cruise(ret) + assert not (FPCP.flags & HyundaiStarPilotFlags.MAIN_CRUISE_STATE_TRACKING) ioniq_cp = CarInterface.get_params(CAR.HYUNDAI_IONIQ_6, gen_empty_fingerprint(), [], True, False, False, toggles) ioniq_fpcp = CarInterface.get_starpilot_params( @@ -1486,7 +1480,7 @@ class TestHyundaiFingerprint: assert Bus.alt not in can_parsers - def test_sonata_alt_bus_clu13_swl_stat_lkas_button_event(self): + def test_sonata_uses_main_bus_bcm_lkas_button_event(self): toggles = get_test_toggles() fingerprint = gen_empty_fingerprint() fingerprint[0][0x391] = 8 @@ -1498,16 +1492,26 @@ class TestHyundaiFingerprint: can_parsers = car_state.get_can_parsers(CP) packer = CANPacker(DBC[CP.carFingerprint][Bus.pt]) + assert Bus.alt not in can_parsers + def update(lkas_button: int, frame: int): - msg = packer.make_can_msg("CLU13", 1, { - "CF_Clu_SWL_Stat": lkas_button, - }) - can_parsers[Bus.alt].update([(frame, [msg])]) + msgs = [ + packer.make_can_msg("CLU13", 0, { + "CF_Clu_LdwsLkasSW": 0, + "CF_Clu_SWL_Stat": 4, + }), + packer.make_can_msg("BCM_PO_11", 0, { + "LDA_BTN": lkas_button, + }), + ] + can_parsers[Bus.pt].update([(frame, msgs)]) return car_state.update(can_parsers, toggles)[0] update(0, 1) - ret = update(4, 2) + ret = update(1, 2) assert any(be.type == ButtonType.lkas and be.pressed for be in ret.buttonEvents) + + ret = update(0, 3) assert any(be.type == ButtonType.lkas and not be.pressed for be in ret.buttonEvents) def test_genesis_g90_does_not_use_alt_bus_lkas_parser(self): diff --git a/opendbc_repo/opendbc/car/hyundai/values.py b/opendbc_repo/opendbc/car/hyundai/values.py index 57456969e..585389406 100644 --- a/opendbc_repo/opendbc/car/hyundai/values.py +++ b/opendbc_repo/opendbc/car/hyundai/values.py @@ -973,18 +973,8 @@ KIA_EV6_GT_LINE_LONG_TUNING_VDS_PREFIXES = frozenset({ }) -# These classic HKG platforms publish the LKAS button on CLU13 over the alt bus. -# Keep G90 excluded until its alt-bus path is route-proven without the recent -# engage/disengage regression. -ALT_BUS_LDA_BUTTON_CARS = frozenset({ - CAR.HYUNDAI_SONATA, -}) - -# On these Sonata layouts the alt-bus LKAS button pulses through the CLU13 -# steering-wheel-status field instead of the dedicated LKAS bit. -ALT_BUS_LDA_BUTTON_SWL_STAT_CARS = frozenset({ - CAR.HYUNDAI_SONATA, -}) +ALT_BUS_LDA_BUTTON_CARS = frozenset() +ALT_BUS_LDA_BUTTON_SWL_STAT_CARS = frozenset() def hyundai_cancel_button_enables_cruise(car_fingerprint) -> bool: diff --git a/opendbc_repo/opendbc/car/interfaces.py b/opendbc_repo/opendbc/car/interfaces.py index 9eb03eb13..75b2bf275 100644 --- a/opendbc_repo/opendbc/car/interfaces.py +++ b/opendbc_repo/opendbc/car/interfaces.py @@ -232,9 +232,6 @@ class CarInterfaceBase(ABC): fp_ret.flags |= int(HondaStarPilotFlags.HAS_CAMERA_MESSAGES) elif platform in HYUNDAI: - if CP.openpilotLongitudinalControl and not (CP.flags & HyundaiFlags.CANFD): - fp_ret.flags |= HyundaiStarPilotFlags.MAIN_CRUISE_STATE_TRACKING.value - if candidate in CANFD_CAR: hda2 = Ecu.adas in [fw.ecu for fw in car_fw] CAN = CanBus(None, fingerprint, bool(CP.flags & HyundaiFlags.CANFD_LKA_STEERING)) diff --git a/opendbc_repo/opendbc/car/toyota/carcontroller.py b/opendbc_repo/opendbc/car/toyota/carcontroller.py index b58883b86..b99c54735 100644 --- a/opendbc_repo/opendbc/car/toyota/carcontroller.py +++ b/opendbc_repo/opendbc/car/toyota/carcontroller.py @@ -513,13 +513,8 @@ class CarController(CarControllerBase): pcm_accel_cmd = float(np.clip(pcm_accel_cmd, self.params.ACCEL_MIN, self.params.ACCEL_MAX)) main_accel_cmd = 0. if self.CP.flags & ToyotaFlags.SECOC.value else pcm_accel_cmd - # Toyota's physical distance-button hold can collide with StarPilot's wheel-button - # actions and trip a temporary EPS fault. Suppress native long-press handling while - # the physical gap button is held so ACC only sees the hold as a plain button press. - allow_long_press = 0 if bool(getattr(CS, "distance_button", False)) else None 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, starpilot_toggles.reverse_cruise_increase, - allow_long_press)) + CS.acc_type, fcw_alert, self.distance_button, starpilot_toggles.reverse_cruise_increase)) if self.CP.flags & ToyotaFlags.SECOC.value: acc_cmd_2 = toyotacan.create_accel_command_2(self.packer, pcm_accel_cmd) acc_cmd_2 = add_mac(self.secoc_key, @@ -538,9 +533,8 @@ class CarController(CarControllerBase): if self.CP.carFingerprint in UNSUPPORTED_DSU_CAR: can_sends.append(toyotacan.create_acc_cancel_command(self.packer)) else: - allow_long_press = 0 if bool(getattr(CS, "distance_button", False)) else None can_sends.append(toyotacan.create_accel_command(self.packer, 0, pcm_cancel_cmd, True, False, lead, CS.acc_type, False, - self.distance_button, starpilot_toggles.reverse_cruise_increase, allow_long_press)) + self.distance_button, starpilot_toggles.reverse_cruise_increase)) # *** hud ui *** if self.CP.carFingerprint != CAR.TOYOTA_PRIUS_V: diff --git a/opendbc_repo/opendbc/car/toyota/tests/test_toyota.py b/opendbc_repo/opendbc/car/toyota/tests/test_toyota.py index d2a4b45a5..3ef83d3a3 100644 --- a/opendbc_repo/opendbc/car/toyota/tests/test_toyota.py +++ b/opendbc_repo/opendbc/car/toyota/tests/test_toyota.py @@ -752,21 +752,21 @@ class TestToyotaCarController: assert parser.vl["LKAS_HUD"]["LEFT_LINE"] == 0 assert parser.vl["LKAS_HUD"]["RIGHT_LINE"] == 0 - def test_acc_control_can_suppress_long_press_behavior_while_gap_button_is_held(self): + def test_acc_control_uses_valid_long_press_modes(self): packer = CANPacker(DBC[CAR.TOYOTA_HIGHLANDER_TSS2][Bus.pt]) parser = CANParser(DBC[CAR.TOYOTA_HIGHLANDER_TSS2][Bus.pt], [("ACC_CONTROL", 0)], 0) - default_msg = toyotacan.create_accel_command( + normal_msg = toyotacan.create_accel_command( packer, 0.0, False, True, False, False, 1, False, 0, False, ) - parser.update([(1, [default_msg])]) + parser.update([(1, [normal_msg])]) assert parser.vl["ACC_CONTROL"]["ALLOW_LONG_PRESS"] == 1 - suppressed_msg = toyotacan.create_accel_command( - packer, 0.0, False, True, False, False, 1, False, 0, False, allow_long_press=0, + reverse_msg = toyotacan.create_accel_command( + packer, 0.0, False, True, False, False, 1, False, 0, True, ) - parser.update([(1, [suppressed_msg])]) - assert parser.vl["ACC_CONTROL"]["ALLOW_LONG_PRESS"] == 0 + parser.update([(1, [reverse_msg])]) + assert parser.vl["ACC_CONTROL"]["ALLOW_LONG_PRESS"] == 2 def test_auto_brake_hold_sends_modified_pre_collision_after_timer(self): controller = self._make_controller() diff --git a/opendbc_repo/opendbc/car/toyota/toyotacan.py b/opendbc_repo/opendbc/car/toyota/toyotacan.py index 54b677ac7..43a7a8d38 100644 --- a/opendbc_repo/opendbc/car/toyota/toyotacan.py +++ b/opendbc_repo/opendbc/car/toyota/toyotacan.py @@ -41,10 +41,9 @@ def create_lta_steer_command_2(packer, frame): def create_accel_command(packer, accel, pcm_cancel, permit_braking, standstill_req, lead, acc_type, fcw_alert, - distance, reverse_cruise_active, allow_long_press=None): + distance, reverse_cruise_active): # TODO: find the exact canceling bit that does not create a chime - if allow_long_press is None: - allow_long_press = 2 if reverse_cruise_active else 1 + allow_long_press = 2 if reverse_cruise_active else 1 values = { "ACCEL_CMD": accel, diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index f82ef6b3c..57dc5bb07 100644 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -12,6 +12,7 @@ from openpilot.common.swaglog import cloudlog from opendbc.car.car_helpers import interfaces from opendbc.car.chrysler.values import pacifica_hybrid_aol_stock_acc_mode from opendbc.car.gm.values import CAR as GM_CAR +from opendbc.car.honda.values import CAR as HONDA_CAR from opendbc.car.nissan.values import CAR as NISSAN_CAR from opendbc.car.vehicle_model import VehicleModel from openpilot.selfdrive.controls.lib.drive_helpers import MAX_LATERAL_JERK, clip_curvature, get_lateral_active @@ -23,6 +24,7 @@ from openpilot.selfdrive.controls.lib.latcontrol_torque import ( BOLT_2018_2021_STEER_RATIO_TEST_SCALE, LatControlTorque, get_bolt_2017_steer_ratio_scale, + get_honda_accord_steer_ratio_scale, ) from openpilot.selfdrive.controls.lib.longcontrol import LongControl from openpilot.selfdrive.car.cruise_state import should_cancel_stock_cruise @@ -394,10 +396,15 @@ class Controls: lp = self.sm['liveParameters'] x = max(lp.stiffnessFactor, 0.1) sr = max(lp.steerRatio, 0.1) + custom_accord_ratio = getattr(self.starpilot_toggles, "steerRatio", self.CP.steerRatio) + accord_ratio_is_explicit = getattr(self.starpilot_toggles, "use_custom_steerRatio", False) and \ + abs(custom_accord_ratio - self.CP.steerRatio) > 0.01 if self.CP.carFingerprint == GM_CAR.CHEVROLET_BOLT_CC_2017: sr *= get_bolt_2017_steer_ratio_scale(CS.vEgo) elif self.CP.carFingerprint == GM_CAR.CHEVROLET_BOLT_CC_2018_2021: sr *= BOLT_2018_2021_STEER_RATIO_TEST_SCALE + elif self.CP.carFingerprint == HONDA_CAR.HONDA_ACCORD and not accord_ratio_is_explicit: + sr *= get_honda_accord_steer_ratio_scale(CS.vEgo) self.VM.update_params(x, sr) steer_angle_without_offset = math.radians(CS.steeringAngleDeg - lp.angleOffsetDeg) diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index cd0cd3e29..85e3c7f46 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -483,14 +483,24 @@ class LatControlTorque(LatControl): ff *= get_genesis_g70_unwind_ff_scale( setpoint, measurement, desired_lateral_jerk, CS.vEgo, ) + if kia_carnival_active: + ff *= get_kia_carnival_unwind_ff_scale( + setpoint, measurement, desired_lateral_jerk, CS.vEgo, + ) if ioniq_6_active: vehicle_friction_jerk_deadzone = ( IONIQ_6_2025_FRICTION_JERK_DEADZONE if self.is_ioniq_6_2025 else IONIQ_6_FRICTION_JERK_DEADZONE ) + elif ioniq_5_active: + vehicle_friction_jerk_deadzone = get_ioniq_5_friction_jerk_deadzone(CS.vEgo, setpoint) elif prius_active: vehicle_friction_jerk_deadzone = get_prius_friction_jerk_deadzone(CS.vEgo, setpoint) elif genesis_g70_active: vehicle_friction_jerk_deadzone = get_genesis_g70_friction_jerk_deadzone(CS.vEgo, setpoint) + elif kia_carnival_active: + vehicle_friction_jerk_deadzone = get_kia_carnival_friction_jerk_deadzone( + CS.vEgo, setpoint, desired_lateral_jerk, + ) else: vehicle_friction_jerk_deadzone = 0.0 friction_jerk_deadzone = get_center_chatter_friction_jerk_deadzone( diff --git a/selfdrive/controls/lib/latcontrol_vehicle_tunes.py b/selfdrive/controls/lib/latcontrol_vehicle_tunes.py index 48c26df21..4c3f12ef9 100644 --- a/selfdrive/controls/lib/latcontrol_vehicle_tunes.py +++ b/selfdrive/controls/lib/latcontrol_vehicle_tunes.py @@ -73,6 +73,7 @@ BOLT_2017_CARS = ( GM_CAR.CHEVROLET_BOLT_CC_2017, ) BOLT_CARS = BOLT_2022_2023_CARS + BOLT_2018_2021_CARS + BOLT_2017_CARS +HONDA_ACCORD_STEER_RATIO_SCALE = 14.0 / 16.33 VOLT_STANDARD_CARS = ( GM_CAR.CHEVROLET_VOLT, GM_CAR.CHEVROLET_VOLT_2019, @@ -547,6 +548,24 @@ KIA_CARNIVAL_HIGHWAY_TRANSITION_JERK = 0.45 KIA_CARNIVAL_HIGHWAY_TRANSITION_JERK_WIDTH = 0.15 KIA_CARNIVAL_HIGHWAY_TRANSITION_LAT_CUTOFF = 1.20 KIA_CARNIVAL_HIGHWAY_TRANSITION_LAT_WIDTH = 0.20 +KIA_CARNIVAL_UNWIND_FRICTION_JERK_DEADZONE_MAX = 0.34 +KIA_CARNIVAL_UNWIND_FRICTION_JERK_DEADZONE_SPEED = 15.0 +KIA_CARNIVAL_UNWIND_FRICTION_JERK_DEADZONE_SPEED_WIDTH = 2.0 +KIA_CARNIVAL_UNWIND_FRICTION_JERK_DEADZONE_SPEED_CUTOFF = 23.0 +KIA_CARNIVAL_UNWIND_FRICTION_JERK_DEADZONE_SPEED_CUTOFF_WIDTH = 2.0 +KIA_CARNIVAL_UNWIND_FRICTION_JERK_DEADZONE_LAT = 0.35 +KIA_CARNIVAL_UNWIND_FRICTION_JERK_DEADZONE_LAT_WIDTH = 0.18 +KIA_CARNIVAL_UNWIND_FRICTION_JERK_DEADZONE_JERK = 0.65 +KIA_CARNIVAL_UNWIND_FRICTION_JERK_DEADZONE_JERK_WIDTH = 0.25 +KIA_CARNIVAL_UNWIND_FF_REDUCTION_MAX = 0.45 +KIA_CARNIVAL_UNWIND_FF_SPEED = 15.0 +KIA_CARNIVAL_UNWIND_FF_SPEED_WIDTH = 2.0 +KIA_CARNIVAL_UNWIND_FF_SPEED_CUTOFF = 23.0 +KIA_CARNIVAL_UNWIND_FF_SPEED_CUTOFF_WIDTH = 2.0 +KIA_CARNIVAL_UNWIND_FF_OVERSHOOT = 0.20 +KIA_CARNIVAL_UNWIND_FF_OVERSHOOT_WIDTH = 0.12 +KIA_CARNIVAL_UNWIND_FF_JERK = 0.65 +KIA_CARNIVAL_UNWIND_FF_JERK_WIDTH = 0.25 TUCSON_4TH_GEN_CENTER_TAPER_MAX = 0.44 TUCSON_4TH_GEN_CENTER_TAPER_LAT = 0.28 @@ -688,6 +707,11 @@ IONIQ_5_LOW_SPEED_CENTER_LAT = 0.40 IONIQ_5_LOW_SPEED_CENTER_LAT_WIDTH = 0.10 IONIQ_5_LOW_SPEED_CENTER_JERK = 0.40 IONIQ_5_LOW_SPEED_CENTER_JERK_WIDTH = 0.12 +IONIQ_5_FRICTION_JERK_DEADZONE_MAX = 0.30 +IONIQ_5_FRICTION_JERK_DEADZONE_LAT = 1.25 +IONIQ_5_FRICTION_JERK_DEADZONE_LAT_WIDTH = 0.35 +IONIQ_5_FRICTION_JERK_DEADZONE_SPEED = 18.0 +IONIQ_5_FRICTION_JERK_DEADZONE_SPEED_WIDTH = 3.0 IONIQ_EV_OLD_BASE_LAT_ACCEL_FACTOR_MULT = 1.16 IONIQ_EV_OLD_FF_REDUCTION_LEFT = 0.16 @@ -1082,9 +1106,6 @@ TOYOTA_COROLLA_TSS2_CENTER_OUTPUT_TAPER_SPEED = 4.5 TOYOTA_COROLLA_TSS2_CENTER_OUTPUT_TAPER_SPEED_WIDTH = 1.5 LEXUS_IS_PHASE_SCALE = 0.10 -# The Lexus route still fell short during a clean high-speed turn-in while -# already at the controller limit. Keep this correction small and phase-gated -# so straight-line behavior and unwind tuning are unchanged. LEXUS_IS_TURN_IN_FF_BOOST_LEFT = 0.06 LEXUS_IS_TURN_IN_FF_BOOST_RIGHT = 0.06 LEXUS_IS_UNWIND_FF_REDUCTION_LEFT = 0.10 @@ -1886,6 +1907,10 @@ def get_bolt_2017_steer_ratio_scale(v_ego: float) -> float: return 1.0 + ((BOLT_2017_STEER_RATIO_TEST_SCALE - 1.0) * _bolt_2017_high_speed_factor(v_ego)) +def get_honda_accord_steer_ratio_scale(_v_ego: float) -> float: + return HONDA_ACCORD_STEER_RATIO_SCALE + + def get_bolt_2017_center_taper_scale(desired_lateral_accel: float, v_ego: float) -> float: center_window = _bolt_2017_sigmoid((BOLT_2017_CENTER_TAPER_LAT - abs(desired_lateral_accel)) / BOLT_2017_CENTER_TAPER_WIDTH) return 1.0 - (BOLT_2017_CENTER_TAPER_GAIN * _bolt_2017_high_speed_factor(v_ego) * center_window) @@ -2528,6 +2553,41 @@ def get_kia_carnival_highway_transition_output_scale(desired_lateral_accel: floa return 1.0 - (KIA_CARNIVAL_HIGHWAY_TRANSITION_TAPER_MAX * speed_weight * jerk_weight * lat_weight) +def get_kia_carnival_friction_jerk_deadzone(v_ego: float, desired_lateral_accel: float, + desired_lateral_jerk: float) -> float: + """Reduce abrupt friction reversals during mid-speed curve exits only.""" + speed_weight = _sigmoid((v_ego - KIA_CARNIVAL_UNWIND_FRICTION_JERK_DEADZONE_SPEED) / + KIA_CARNIVAL_UNWIND_FRICTION_JERK_DEADZONE_SPEED_WIDTH) + speed_cutoff = _sigmoid((KIA_CARNIVAL_UNWIND_FRICTION_JERK_DEADZONE_SPEED_CUTOFF - v_ego) / + KIA_CARNIVAL_UNWIND_FRICTION_JERK_DEADZONE_SPEED_CUTOFF_WIDTH) + center_weight = _sigmoid((KIA_CARNIVAL_UNWIND_FRICTION_JERK_DEADZONE_LAT - abs(desired_lateral_accel)) / + KIA_CARNIVAL_UNWIND_FRICTION_JERK_DEADZONE_LAT_WIDTH) + jerk_weight = _sigmoid((abs(desired_lateral_jerk) - KIA_CARNIVAL_UNWIND_FRICTION_JERK_DEADZONE_JERK) / + KIA_CARNIVAL_UNWIND_FRICTION_JERK_DEADZONE_JERK_WIDTH) + return KIA_CARNIVAL_UNWIND_FRICTION_JERK_DEADZONE_MAX * speed_weight * speed_cutoff * center_weight * jerk_weight + + +def get_kia_carnival_unwind_ff_scale(setpoint: float, measured_lateral_accel: float, + desired_lateral_jerk: float, v_ego: float) -> float: + """Remove stale turn feedforward when the measured response carries through an unwind.""" + if setpoint * desired_lateral_jerk >= 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_FF_SPEED) / + KIA_CARNIVAL_UNWIND_FF_SPEED_WIDTH) * + _sigmoid((KIA_CARNIVAL_UNWIND_FF_SPEED_CUTOFF - v_ego) / + KIA_CARNIVAL_UNWIND_FF_SPEED_CUTOFF_WIDTH)) + overshoot_weight = _sigmoid((overshoot - KIA_CARNIVAL_UNWIND_FF_OVERSHOOT) / + KIA_CARNIVAL_UNWIND_FF_OVERSHOOT_WIDTH) + jerk_weight = _sigmoid((abs(desired_lateral_jerk) - KIA_CARNIVAL_UNWIND_FF_JERK) / + KIA_CARNIVAL_UNWIND_FF_JERK_WIDTH) + return 1.0 - (KIA_CARNIVAL_UNWIND_FF_REDUCTION_MAX * speed_weight * overshoot_weight * jerk_weight) + + 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) center_weight = _sigmoid((TUCSON_4TH_GEN_CENTER_TAPER_LAT - abs(desired_lateral_accel)) / TUCSON_4TH_GEN_CENTER_TAPER_LAT_WIDTH) @@ -3029,6 +3089,15 @@ def get_ioniq_5_low_speed_output_limit(desired_lateral_accel: float, return float(np.clip(limit, IONIQ_5_LOW_SPEED_OUTPUT_LIMIT_BASE, 1.0)) +def get_ioniq_5_friction_jerk_deadzone(v_ego: float, desired_lateral_accel: float) -> float: + """Suppress high-speed friction reversals without reducing steady-turn torque.""" + speed_weight = _ioniq_5_sigmoid((max(v_ego, 0.0) - IONIQ_5_FRICTION_JERK_DEADZONE_SPEED) / + IONIQ_5_FRICTION_JERK_DEADZONE_SPEED_WIDTH) + curve_weight = _ioniq_5_sigmoid((IONIQ_5_FRICTION_JERK_DEADZONE_LAT - abs(desired_lateral_accel)) / + IONIQ_5_FRICTION_JERK_DEADZONE_LAT_WIDTH) + return IONIQ_5_FRICTION_JERK_DEADZONE_MAX * speed_weight * curve_weight + + def _ioniq_ev_old_sigmoid(x: float) -> float: return _sigmoid(x) diff --git a/selfdrive/controls/tests/test_latcontrol.py b/selfdrive/controls/tests/test_latcontrol.py index 70f24ae9a..1bb7d6daf 100644 --- a/selfdrive/controls/tests/test_latcontrol.py +++ b/selfdrive/controls/tests/test_latcontrol.py @@ -84,6 +84,7 @@ from openpilot.selfdrive.controls.lib.latcontrol_torque import ( get_genesis_gv70_high_speed_error_scale, get_genesis_gv70_unwind_ff_scale, get_elantra_non_scc_ff_scale, + get_honda_accord_steer_ratio_scale, get_palisade_ff_scale, get_palisade_center_output_scale, get_palisade_center_taper_scale, @@ -113,6 +114,7 @@ from openpilot.selfdrive.controls.lib.latcontrol_torque import ( get_ioniq_5_friction_scale, get_ioniq_5_friction_threshold, get_ioniq_5_center_taper_scale, + get_ioniq_5_friction_jerk_deadzone, get_ioniq_5_low_speed_output_limit, get_ioniq_ev_old_center_taper_scale, get_ioniq_ev_old_ff_scale, @@ -132,8 +134,10 @@ from openpilot.selfdrive.controls.lib.latcontrol_torque import ( get_kia_forte_ff_scale, get_kia_carnival_center_taper_scale, get_kia_carnival_friction_center_fade_scale, + get_kia_carnival_friction_jerk_deadzone, get_kia_carnival_friction_threshold, get_kia_carnival_highway_transition_output_scale, + get_kia_carnival_unwind_ff_scale, get_kia_stinger_2022_center_taper_scale, get_kia_stinger_2022_friction_threshold, get_tucson_4th_gen_center_taper_scale, @@ -668,6 +672,30 @@ class TestLatControl: assert low_speed_abrupt > 0.99 assert large_curve_abrupt > 0.96 + def test_kia_carnival_unwind_friction_jerk_deadzone_is_mid_speed_and_center_gated(self): + low_speed = get_kia_carnival_friction_jerk_deadzone(8.5, 0.0, 1.5) + mid_speed_center = get_kia_carnival_friction_jerk_deadzone(18.0, 0.0, 1.5) + mid_speed_curve = get_kia_carnival_friction_jerk_deadzone(18.0, 0.8, 1.5) + high_speed = get_kia_carnival_friction_jerk_deadzone(30.0, 0.0, 1.5) + calm_transition = get_kia_carnival_friction_jerk_deadzone(18.0, 0.0, 0.2) + + assert low_speed < 0.02 + assert mid_speed_center > 0.18 + assert mid_speed_curve < 0.05 + assert high_speed < 0.05 + assert calm_transition < 0.05 + + def test_kia_carnival_unwind_ff_scale_only_reduces_overshoot(self): + steady_turn = get_kia_carnival_unwind_ff_scale(0.80, 0.90, 0.60, 18.0) + clean_unwind = get_kia_carnival_unwind_ff_scale(0.20, 0.20, -1.5, 18.0) + overshooting_unwind = get_kia_carnival_unwind_ff_scale(0.20, 0.90, -1.5, 18.0) + highway_overshoot = get_kia_carnival_unwind_ff_scale(0.20, 0.90, -1.5, 30.0) + + assert steady_turn == pytest.approx(1.0) + assert clean_unwind == pytest.approx(1.0) + assert overshooting_unwind < 0.70 + assert highway_overshoot > overshooting_unwind + 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.5, 0.0, 20.0) > get_genesis_g90_ff_scale(-0.5, 0.0, 20.0) @@ -904,6 +932,16 @@ class TestLatControl: assert unwind_right_scale <= unwind_left_scale assert get_ioniq_5_friction_threshold(25.0, 0.0, 0.0) >= get_hkg_canfd_base_friction_threshold(25.0) + def test_ioniq_5_friction_jerk_deadzone_is_high_speed_curve_gated(self): + low_speed = get_ioniq_5_friction_jerk_deadzone(8.0, 0.9) + high_speed_center = get_ioniq_5_friction_jerk_deadzone(25.0, 0.0) + high_speed_curve = get_ioniq_5_friction_jerk_deadzone(25.0, 0.9) + high_lateral_accel = get_ioniq_5_friction_jerk_deadzone(25.0, 2.0) + + assert low_speed < 0.02 + assert high_speed_center > high_speed_curve > 0.0 + assert high_lateral_accel < high_speed_curve + def test_rav4_prime_phase_shaping(self): left_turn_in = get_rav4_prime_ff_scale(1.0, 0.8, 13.0) right_turn_in = get_rav4_prime_ff_scale(-1.0, -0.8, 13.0) @@ -1665,6 +1703,11 @@ class TestLatControl: assert controller.pid._k_p[1] == pytest.approx([value * 2.0 for value in base_kp_v]) assert controller.pid._k_i[1] == pytest.approx([value * 1.25 for value in base_ki_v]) + def test_honda_accord_steer_ratio_calibration(self): + expected_scale = 14.0 / 16.33 + assert get_honda_accord_steer_ratio_scale(0.0) == pytest.approx(expected_scale) + assert get_honda_accord_steer_ratio_scale(20.0) == pytest.approx(expected_scale) + def test_subaru_impreza_pid_output_scale_preserves_small_errors(self): assert get_subaru_impreza_pid_output_scale(0.0) == 1.0 assert get_subaru_impreza_pid_output_scale(0.75) == 1.0 diff --git a/selfdrive/controls/tests/test_speed_limit_controller.py b/selfdrive/controls/tests/test_speed_limit_controller.py index 74cbd15f7..46c0bf31e 100644 --- a/selfdrive/controls/tests/test_speed_limit_controller.py +++ b/selfdrive/controls/tests/test_speed_limit_controller.py @@ -63,6 +63,8 @@ def make_toggles(**overrides): "speed_limit_priority_highest": False, "speed_limit_priority_lowest": False, "vision_speed_limit_detection": False, + "vision_speed_limit_low_limit_filter": False, + "vision_speed_limit_low_limit_threshold": mph(25), } defaults.update(overrides) return SimpleNamespace(**defaults) @@ -96,6 +98,107 @@ def mph(value): return value * CV.MPH_TO_MS +@pytest.mark.parametrize("limit_mph", [15, 25]) +def test_low_vision_limit_filter_blocks_configured_boundary(limit_mph): + controller = make_controller( + speed_limit_priority1="Vision", + vision_speed_limit_detection=True, + vision_speed_limit_low_limit_filter=True, + vision_speed_limit_low_limit_threshold=mph(25), + ) + try: + controller.starpilot_planner.params_memory.put_float("VisionSpeedLimit", mph(limit_mph)) + sm = make_sm(gas_pressed=False, v_cruise_kph=25 * CV.MPH_TO_KPH) + + controller.update_limits(0.0, datetime.now(timezone.utc), False, mph(25), mph(20), sm) + + assert controller.vision_limit == pytest.approx(mph(limit_mph)) + assert controller.target == 0 + assert controller.source == "None" + finally: + controller.shutdown() + + +def test_low_vision_limit_filter_allows_limit_above_threshold(): + controller = make_controller( + speed_limit_priority1="Vision", + vision_speed_limit_detection=True, + vision_speed_limit_low_limit_filter=True, + vision_speed_limit_low_limit_threshold=mph(25), + ) + try: + controller.starpilot_planner.params_memory.put_float("VisionSpeedLimit", mph(30)) + sm = make_sm(gas_pressed=False, v_cruise_kph=30 * CV.MPH_TO_KPH) + + controller.update_limits(0.0, datetime.now(timezone.utc), False, mph(30), mph(25), sm) + + assert controller.target == pytest.approx(mph(30)) + assert controller.source == "Vision" + finally: + controller.shutdown() + + +def test_low_vision_limit_filter_is_action_only_for_display(): + controller = make_controller( + speed_limit_priority1="Vision", + vision_speed_limit_detection=True, + vision_speed_limit_low_limit_filter=True, + vision_speed_limit_low_limit_threshold=mph(25), + ) + try: + controller.starpilot_planner.params_memory.put_float("VisionSpeedLimit", mph(15)) + sm = make_sm(gas_pressed=False, v_cruise_kph=20 * CV.MPH_TO_KPH) + + controller.update_limits(0.0, datetime.now(timezone.utc), False, mph(20), mph(15), sm, display_only=True) + + assert controller.target == pytest.approx(mph(15)) + assert controller.source == "Vision" + finally: + controller.shutdown() + + +def test_low_vision_limit_filter_does_not_filter_dashboard_source(): + controller = make_controller( + speed_limit_priority1="Vision", + speed_limit_priority2="Dashboard", + vision_speed_limit_detection=True, + vision_speed_limit_low_limit_filter=True, + vision_speed_limit_low_limit_threshold=mph(25), + ) + try: + controller.starpilot_planner.params_memory.put_float("VisionSpeedLimit", mph(15)) + sm = make_sm(gas_pressed=False, v_cruise_kph=20 * CV.MPH_TO_KPH) + + controller.update_limits(mph(15), datetime.now(timezone.utc), False, mph(20), mph(15), sm) + + assert controller.target == pytest.approx(mph(15)) + assert controller.source == "Dashboard" + finally: + controller.shutdown() + + +def test_low_vision_limit_filter_does_not_restore_filtered_vision_fallback(): + controller = make_controller( + speed_limit_priority1="Vision", + slc_fallback_previous_speed_limit=True, + vision_speed_limit_detection=True, + vision_speed_limit_low_limit_filter=True, + vision_speed_limit_low_limit_threshold=mph(25), + ) + try: + controller.previous_source = "Vision" + controller.previous_target = mph(15) + controller.starpilot_planner.params_memory.put_float("VisionSpeedLimit", mph(15)) + sm = make_sm(gas_pressed=False, v_cruise_kph=20 * CV.MPH_TO_KPH) + + controller.update_limits(0.0, datetime.now(timezone.utc), False, mph(20), mph(15), sm) + + assert controller.target == 0 + assert controller.source == "None" + finally: + controller.shutdown() + + def test_large_vision_delta_requires_three_detector_frames(): controller = make_controller( speed_limit_priority1="Vision", diff --git a/starpilot/common/safe_mode.py b/starpilot/common/safe_mode.py index 354f6cefe..8b2cdb3d1 100644 --- a/starpilot/common/safe_mode.py +++ b/starpilot/common/safe_mode.py @@ -153,6 +153,8 @@ SAFE_MODE_MANAGED_KEYS = ( "Offset7", "SpeedLimitFiller", "VisionSpeedLimitDetection", + "VisionSpeedLimitLowLimitFilter", + "VisionSpeedLimitLowLimitThreshold", "VASMEnabled", "CustomPersonalities", "TrafficPersonalityProfile", diff --git a/starpilot/common/starpilot_variables.py b/starpilot/common/starpilot_variables.py index 3cc367382..1198c269c 100644 --- a/starpilot/common/starpilot_variables.py +++ b/starpilot/common/starpilot_variables.py @@ -1341,6 +1341,16 @@ class StarPilotVariables: toggle.speed_limit_filler = self.get_value("SpeedLimitFiller") toggle.vision_speed_limit_detection = self.get_value("VisionSpeedLimitDetection") + toggle.vision_speed_limit_low_limit_filter = self.get_value( + "VisionSpeedLimitLowLimitFilter", + condition=toggle.speed_limit_controller and toggle.vision_speed_limit_detection, + ) + toggle.vision_speed_limit_low_limit_threshold = self.get_value( + "VisionSpeedLimitLowLimitThreshold", + cast=float, + condition=toggle.vision_speed_limit_low_limit_filter, + conversion=speed_conversion, + ) toggle.v_asm_enabled = self.get_value("VASMEnabled") toggle.startup_alert_top = self.get_value("StartupMessageTop", cast=str, default="") diff --git a/starpilot/controls/lib/speed_limit_controller.py b/starpilot/controls/lib/speed_limit_controller.py index 40758e7f5..d465554b1 100644 --- a/starpilot/controls/lib/speed_limit_controller.py +++ b/starpilot/controls/lib/speed_limit_controller.py @@ -139,6 +139,12 @@ class SpeedLimitController: (gas_pressed and v_ego > target_with_offset) ) + def low_vision_limit_filtered(self, limit): + return ( + getattr(self.starpilot_toggles, "vision_speed_limit_low_limit_filter", False) and + 0 < limit <= max(getattr(self.starpilot_toggles, "vision_speed_limit_low_limit_threshold", 0), 0) + ) + def clear_override_for_source_limit(self, desired_source, desired_target, had_override): if desired_source == "None" or desired_target <= 0: return @@ -347,6 +353,8 @@ class SpeedLimitController: vision_enabled = getattr(self.starpilot_toggles, "vision_speed_limit_detection", False) self.vision_limit = self.starpilot_planner.params_memory.get_float("VisionSpeedLimit") if vision_enabled else 0 usable_vision_limit = self.vision_limit + if not display_only and self.low_vision_limit_filtered(usable_vision_limit): + usable_vision_limit = 0 # The planner clamps V_CRUISE_UNSET to V_CRUISE_MAX, so plausibility must use the raw selected speed. raw_set_speed_kph = float(sm["carState"].vCruise) selected_set_speed = raw_set_speed_kph * CV.KPH_TO_MS if 0 < raw_set_speed_kph < V_CRUISE_UNSET else 0 @@ -406,7 +414,8 @@ class SpeedLimitController: desired_target = self.mapbox_limit if not display_only and desired_target == 0: - if self.previous_target > 0 and self.starpilot_toggles.slc_fallback_previous_speed_limit: + previous_vision_limit_filtered = self.previous_source == "Vision" and self.low_vision_limit_filtered(self.previous_target) + if self.previous_target > 0 and self.starpilot_toggles.slc_fallback_previous_speed_limit and not previous_vision_limit_filtered: desired_source = self.previous_source desired_target = self.previous_target diff --git a/starpilot/controls/starpilot_card.py b/starpilot/controls/starpilot_card.py index 8e4502b4b..92e2cadaa 100644 --- a/starpilot/controls/starpilot_card.py +++ b/starpilot/controls/starpilot_card.py @@ -53,6 +53,7 @@ class StarPilotCard: self.prev_cruise_enabled = False self.decel_pressed = False self.cancelPressed_previously = False + self.cancel_pulse_glide_suppressed = False self.distancePressed_previously = False self.force_coast = False self.pulse_and_glide = False @@ -89,6 +90,7 @@ class StarPilotCard: elif getattr(starpilot_toggles, f"pulse_and_glide_via_{key}"): if getattr(sm["carControl"], "longActive", False): self.pulse_and_glide = not self.pulse_and_glide + return True elif getattr(starpilot_toggles, f"pause_lateral_via_{key}"): self.pause_lateral = not self.pause_lateral elif getattr(starpilot_toggles, f"pause_longitudinal_via_{key}"): @@ -136,6 +138,34 @@ class StarPilotCard: def update(self, carState, starpilotCarState, sm, starpilot_toggles): self.switchback_mode_enabled = self.params_memory.get_bool("SwitchbackModeEnabled") self._handle_favorite_traffic_mode_action(sm) + + pulse_glide_cancel_override = bool(getattr(sm["carControl"], "longActive", False)) and any( + getattr(starpilot_toggles, f"pulse_and_glide_via_cancel{suffix}", False) + for suffix in ("", "_long", "_very_long") + ) + cancel_pressed = bool(getattr(starpilotCarState, "cancelPressed", False)) + if pulse_glide_cancel_override: + carState.buttonEvents = [ + be for be in carState.buttonEvents + if not ( + self._button_type_raw(be) == int(ButtonType.cancel) and + (be.pressed or self.cancel_pulse_glide_suppressed) + ) + ] + + lkas_pressed = any( + self._button_type_raw(be) == int(ButtonType.lkas) and be.pressed + for be in carState.buttonEvents + ) + pulse_glide_lkas_override = bool(getattr(sm["carControl"], "longActive", False)) and getattr( + starpilot_toggles, "pulse_and_glide_via_lkas", False + ) + if pulse_glide_lkas_override: + carState.buttonEvents = [ + be for be in carState.buttonEvents + if self._button_type_raw(be) != int(ButtonType.lkas) + ] + button_event_types = [self._button_type_raw(be) for be in carState.buttonEvents] button_aol_supported = self.CP.brand == "hyundai" or starpilot_toggles.lkas_allowed_for_aol button_managed_aol = starpilot_toggles.always_on_lateral_lkas or (button_aol_supported and starpilot_toggles.main_cruise_aol_toggle) @@ -254,7 +284,6 @@ class StarPilotCard: self.handle_button_event("distance_long", sm, starpilot_toggles) self.handle_button_event("distance_very_long", sm, starpilot_toggles) - cancel_pressed = bool(getattr(starpilotCarState, "cancelPressed", False)) if cancel_pressed: self.cancel_counter += 1 elif not self.cancelPressed_previously: @@ -262,15 +291,27 @@ class StarPilotCard: self.cancelPressed_previously = cancel_pressed - if not cancel_pressed and 1 <= self.cancel_counter < self.long_press_threshold: - self.handle_button_event("cancel", sm, starpilot_toggles) + pulse_glide_cancel_consumed = False + if not cancel_pressed and self.cancel_pulse_glide_suppressed: + pass + elif not cancel_pressed and 1 <= self.cancel_counter < self.long_press_threshold: + pulse_glide_cancel_consumed = self.handle_button_event("cancel", sm, starpilot_toggles) or False elif self.cancel_counter == self.long_press_threshold: - self.handle_button_event("cancel_long", sm, starpilot_toggles) + pulse_glide_cancel_consumed = self.handle_button_event("cancel_long", sm, starpilot_toggles) or False elif self.cancel_counter == self.very_long_press_threshold: - self.handle_button_event("cancel_long", sm, starpilot_toggles) - self.handle_button_event("cancel_very_long", sm, starpilot_toggles) + pulse_glide_cancel_consumed = self.handle_button_event("cancel_long", sm, starpilot_toggles) or False + pulse_glide_cancel_consumed |= self.handle_button_event("cancel_very_long", sm, starpilot_toggles) or False - if any(be.pressed and be_type == ButtonType.lkas for be, be_type in zip(carState.buttonEvents, button_event_types, strict=False)): + if pulse_glide_cancel_consumed: + self.cancel_pulse_glide_suppressed = True + carState.buttonEvents = [ + be for be in carState.buttonEvents + if self._button_type_raw(be) != int(ButtonType.cancel) + ] + elif not cancel_pressed and self.cancel_pulse_glide_suppressed: + self.cancel_pulse_glide_suppressed = False + + if lkas_pressed: self.handle_button_event("lkas", sm, starpilot_toggles) if getattr(starpilot_toggles, "has_canfd_media_buttons", False): diff --git a/starpilot/controls/tests/test_starpilot_card.py b/starpilot/controls/tests/test_starpilot_card.py index 684d6797f..22d3e6988 100644 --- a/starpilot/controls/tests/test_starpilot_card.py +++ b/starpilot/controls/tests/test_starpilot_card.py @@ -73,11 +73,20 @@ def make_toggles(**overrides): "bookmark_via_cancel": False, "bookmark_via_cancel_long": False, "bookmark_via_cancel_very_long": False, + "experimental_mode_via_cancel": False, + "experimental_mode_via_cancel_long": False, + "experimental_mode_via_cancel_very_long": False, + "force_coast_via_cancel": False, + "force_coast_via_cancel_long": False, + "force_coast_via_cancel_very_long": False, "bookmark_via_lkas": False, "conditional_experimental_mode": False, "experimental_mode_via_lkas": False, "force_coast_via_lkas": False, "pulse_and_glide_available": False, + "pulse_and_glide_via_cancel": False, + "pulse_and_glide_via_cancel_long": False, + "pulse_and_glide_via_cancel_very_long": False, "pulse_and_glide_via_lkas": False, "lkas_allowed_for_aol": False, "main_cruise_aol_toggle": False, @@ -117,6 +126,83 @@ def test_pulse_and_glide_requires_developer_access_and_active_longitudinal(monke assert result.pulseAndGlide is False +def test_pulse_and_glide_consumes_native_cancel_when_mapped(monkeypatch, tmp_path): + monkeypatch.setattr(spc, "Params", FakeParams) + monkeypatch.setattr(spc, "is_FrogsGoMoo", lambda: False) + monkeypatch.setattr(spc, "ERROR_LOGS_PATH", tmp_path) + + card = spc.StarPilotCard(SimpleNamespace(brand="gm"), SimpleNamespace(alternativeExperience=0)) + sm = make_sm() + sm["carControl"].longActive = True + toggles = make_toggles( + pulse_and_glide_available=True, + pulse_and_glide_via_cancel=True, + ) + starpilot_car_state = SimpleNamespace(distancePressed=False, cancelPressed=True) + + press = make_car_state(button_events=[SimpleNamespace(type=spc.ButtonType.cancel, pressed=True)]) + card.update(press, starpilot_car_state, sm, toggles) + assert press.buttonEvents == [] + + starpilot_car_state.cancelPressed = False + release = make_car_state(button_events=[SimpleNamespace(type=spc.ButtonType.cancel, pressed=False)]) + result = card.update(release, starpilot_car_state, sm, toggles) + + assert card.pulse_and_glide is True + assert result.pulseAndGlide is True + assert release.buttonEvents == [] + + +def test_pulse_and_glide_consumes_lkas_when_mapped(monkeypatch, tmp_path): + monkeypatch.setattr(spc, "Params", FakeParams) + monkeypatch.setattr(spc, "is_FrogsGoMoo", lambda: False) + monkeypatch.setattr(spc, "ERROR_LOGS_PATH", tmp_path) + + card = spc.StarPilotCard(SimpleNamespace(brand="gm"), SimpleNamespace(alternativeExperience=0)) + sm = make_sm() + sm["carControl"].longActive = True + toggles = make_toggles( + pulse_and_glide_available=True, + pulse_and_glide_via_lkas=True, + ) + starpilot_car_state = SimpleNamespace(distancePressed=False) + car_state = make_car_state(button_events=[SimpleNamespace(type=spc.ButtonType.lkas, pressed=True)]) + + result = card.update(car_state, starpilot_car_state, sm, toggles) + + assert card.pulse_and_glide is True + assert result.pulseAndGlide is True + assert car_state.buttonEvents == [] + + +def test_pulse_and_glide_long_cancel_consumes_release_after_threshold(monkeypatch, tmp_path): + monkeypatch.setattr(spc, "Params", FakeParams) + monkeypatch.setattr(spc, "is_FrogsGoMoo", lambda: False) + monkeypatch.setattr(spc, "ERROR_LOGS_PATH", tmp_path) + + card = spc.StarPilotCard(SimpleNamespace(brand="gm"), SimpleNamespace(alternativeExperience=0)) + sm = make_sm() + sm["carControl"].longActive = True + toggles = make_toggles( + pulse_and_glide_available=True, + pulse_and_glide_via_cancel_long=True, + ) + starpilot_car_state = SimpleNamespace(distancePressed=False, cancelPressed=True) + + for frame in range(card.long_press_threshold): + button_events = [SimpleNamespace(type=spc.ButtonType.cancel, pressed=True)] if frame == 0 else [] + card.update(make_car_state(button_events=button_events), starpilot_car_state, sm, toggles) + + assert card.pulse_and_glide is True + + starpilot_car_state.cancelPressed = False + release = make_car_state(button_events=[SimpleNamespace(type=spc.ButtonType.cancel, pressed=False)]) + card.update(release, starpilot_car_state, sm, toggles) + + assert card.pulse_and_glide is True + assert release.buttonEvents == [] + + def make_car_state(available=False, enabled=False, button_events=None, brake_pressed=False, gas_pressed=False): return SimpleNamespace( buttonEvents=button_events or [], diff --git a/starpilot/system/the_galaxy/assets/components/tools/device_settings_layout.json b/starpilot/system/the_galaxy/assets/components/tools/device_settings_layout.json index de1363238..0602c166a 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/device_settings_layout.json +++ b/starpilot/system/the_galaxy/assets/components/tools/device_settings_layout.json @@ -1607,6 +1607,27 @@ "parent_key": "SpeedLimitController", "settings_tier": "advanced" }, + { + "key": "VisionSpeedLimitLowLimitFilter", + "label": "Ignore Low Vision Speed Limits", + "description": "Prevent SLC from acting on vision-detected speed limits at or below the configured threshold. Detection, display, debugging, and training collection remain active.", + "data_type": "bool", + "ui_type": "toggle", + "parent_key": "VisionSpeedLimitDetection", + "settings_tier": "advanced" + }, + { + "key": "VisionSpeedLimitLowLimitThreshold", + "label": "Ignore At or Below", + "description": "Vision-detected limits at or below this value will not control speed. The value uses your selected mph or km/h unit.", + "data_type": "int", + "ui_type": "numeric", + "min": 5, + "max": 80, + "step": 5, + "parent_key": "VisionSpeedLimitLowLimitFilter", + "settings_tier": "advanced" + }, { "key": "VisionSpeedLimitAutoBookmark", "label": "Auto-Bookmark Vision Signs", diff --git a/starpilot/system/the_galaxy/tests/test_device_settings_layout.py b/starpilot/system/the_galaxy/tests/test_device_settings_layout.py index 841844076..6db921654 100644 --- a/starpilot/system/the_galaxy/tests/test_device_settings_layout.py +++ b/starpilot/system/the_galaxy/tests/test_device_settings_layout.py @@ -197,6 +197,27 @@ def test_vasm_is_default_off_and_configured_only_in_galaxy(): assert all("VASM" not in path.read_text(encoding="utf-8") for path in physical_settings) +def test_low_vision_limit_filter_is_default_off_and_configured_only_in_galaxy(): + sections = _params_by_section(_layout()) + longitudinal = sections["Longitudinal (Speed & Following)"] + toggle = longitudinal["VisionSpeedLimitLowLimitFilter"] + threshold = longitudinal["VisionSpeedLimitLowLimitThreshold"] + + assert toggle["parent_key"] == "VisionSpeedLimitDetection" + assert threshold["parent_key"] == "VisionSpeedLimitLowLimitFilter" + assert threshold["min"] == 5 + assert threshold["max"] == 80 + assert threshold["step"] == 5 + assert _declared_default("VisionSpeedLimitLowLimitFilter") == "0" + assert _declared_default("VisionSpeedLimitLowLimitThreshold") == "25" + + physical_settings = ( + REPO_ROOT / "selfdrive/ui/layouts/settings/starpilot/longitudinal.py", + REPO_ROOT / "selfdrive/ui/layouts/settings/starpilot/aethergrid.py", + ) + assert all("VisionSpeedLimitLowLimit" not in path.read_text(encoding="utf-8") for path in physical_settings) + + def test_pip_preview_is_under_driving_screen_widgets_and_configured_only_in_galaxy(): sections = _params_by_section(_layout()) visual = sections["Visual (Display & UI)"]