diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 065591cf86..fd20cee92f 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -86,6 +86,9 @@ struct StarPilotCarState @0xf35cc4560bbf6ec2 { customPressed @17 :Bool; alwaysOnLateralAllowed @18 :Bool; dashboardStopSign @19 :UInt8; # 0 = no signal / platform doesn't publish + cancelPressed @20 :Bool; + cancelLongPressed @21 :Bool; + cancelVeryLongPressed @22 :Bool; } struct StarPilotDeviceState @0xda96579883444c35 { diff --git a/common/params_keys.h b/common/params_keys.h index 98d460b7ae..d4584a385c 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -217,6 +217,8 @@ inline static std::unordered_map keys = { {"CustomCruise", {PERSISTENT, FLOAT, "1.0", "1.0", 2}}, {"CustomCruiseLong", {PERSISTENT, FLOAT, "5.0", "5.0", 2}}, {"CustomPersonalities", {PERSISTENT, BOOL, "0", "0", 2}}, + {"CancelButtonControl", {PERSISTENT, INT, "1", "0", 2}}, + {"CancelButtonControlsMigrated", {PERSISTENT, BOOL, "0", "0"}}, {"TrafficPersonalityProfile", {PERSISTENT, BOOL, "1", "1", 2}}, {"AggressivePersonalityProfile", {PERSISTENT, BOOL, "1", "1", 2}}, {"StandardPersonalityProfile", {PERSISTENT, BOOL, "1", "1", 2}}, @@ -349,6 +351,7 @@ inline static std::unordered_map keys = { {"LKASButtonControl", {PERSISTENT, INT, "5", "0", 2}}, {"LockDoors", {PERSISTENT, BOOL, "1", "0", 0}}, {"LockDoorsTimer", {PERSISTENT, INT, "0", "0", 0}}, + {"LongCancelButtonControl", {PERSISTENT, INT, "5", "0", 2}}, {"LongDistanceButtonControl", {PERSISTENT, INT, "5", "0", 2}}, {"LongModeButtonControl", {PERSISTENT, INT, "0", "0", 2}}, {"LongStarButtonControl", {PERSISTENT, INT, "0", "0", 2}}, @@ -583,6 +586,7 @@ inline static std::unordered_map keys = { {"VEgoStartingStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}}, {"VEgoStopping", {PERSISTENT, FLOAT, "0.0", "0.0", 3}}, {"VEgoStoppingStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}}, + {"VeryLongCancelButtonControl", {PERSISTENT, INT, "6", "0", 2}}, {"VeryLongDistanceButtonControl", {PERSISTENT, INT, "6", "0", 2}}, {"VeryLongModeButtonControl", {PERSISTENT, INT, "0", "0", 2}}, {"VeryLongStarButtonControl", {PERSISTENT, INT, "0", "0", 2}}, diff --git a/opendbc_repo/opendbc/car/gm/carstate.py b/opendbc_repo/opendbc/car/gm/carstate.py index d0183c031c..537d7bd88e 100644 --- a/opendbc_repo/opendbc/car/gm/carstate.py +++ b/opendbc_repo/opendbc/car/gm/carstate.py @@ -39,6 +39,10 @@ BOLT_GEN1_CANCEL_PERSONALITY_CARS = { CAR.CHEVROLET_BOLT_CC_2017, CAR.CHEVROLET_BOLT_CC_2018_2021, } +BOLT_CANCEL_BUTTON_CARS = BOLT_GEN1_CANCEL_PERSONALITY_CARS | { + CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL, + CAR.CHEVROLET_BOLT_CC_2022_2023, +} class CarState(CarStateBase): @@ -317,7 +321,7 @@ class CarState(CarStateBase): getattr(starpilot_toggles, "remap_cancel_to_distance", False) and self.CP.openpilotLongitudinalControl and bool(self.CP.flags & GMFlags.PEDAL_LONG.value) and - self.CP.carFingerprint in (BOLT_GEN1_CANCEL_PERSONALITY_CARS | {CAR.CHEVROLET_MALIBU_HYBRID_CC}) + self.CP.carFingerprint in (BOLT_CANCEL_BUTTON_CARS | {CAR.CHEVROLET_MALIBU_HYBRID_CC}) ) malibu_cancel_passthrough = ( remap_cancel_to_distance and @@ -325,32 +329,26 @@ class CarState(CarStateBase): self.CP.openpilotLongitudinalControl and bool(self.CP.flags & GMFlags.PEDAL_LONG.value) ) - bolt_cancel_personality = ( + bolt_cancel_button = ( remap_cancel_to_distance and - self.CP.carFingerprint in BOLT_GEN1_CANCEL_PERSONALITY_CARS and + self.CP.carFingerprint in BOLT_CANCEL_BUTTON_CARS and self.CP.openpilotLongitudinalControl and bool(self.CP.flags & GMFlags.PEDAL_LONG.value) ) + bolt_cancel_lkas_conflict = bolt_cancel_button and self.CP.carFingerprint in BOLT_GEN1_CANCEL_PERSONALITY_CARS cruise_button_map = BUTTONS_DICT - if malibu_cancel_passthrough or bolt_cancel_personality: + if malibu_cancel_passthrough or bolt_cancel_button: cruise_button_map = {k: v for k, v in BUTTONS_DICT.items() if k != CruiseButtons.CANCEL} cruise_events = create_button_events( self.cruise_buttons, prev_cruise_buttons, cruise_button_map, unpressed_btn=CruiseButtons.UNPRESS ) - cancel_gap_events = [] - if bolt_cancel_personality and self.cruise_buttons != prev_cruise_buttons: - if prev_cruise_buttons == CruiseButtons.CANCEL: - cancel_gap_events.append(structs.CarState.ButtonEvent(pressed=False, type=ButtonType.gapAdjustCruise)) - if self.cruise_buttons == CruiseButtons.CANCEL: - cancel_gap_events.append(structs.CarState.ButtonEvent(pressed=True, type=ButtonType.gapAdjustCruise)) - suppress_malibu_side_buttons = malibu_cancel_passthrough and ( self.cruise_buttons in (CruiseButtons.CANCEL, CruiseButtons.MAIN) or prev_cruise_buttons in (CruiseButtons.CANCEL, CruiseButtons.MAIN) ) - suppress_bolt_cancel_lkas = bolt_cancel_personality and ( + suppress_bolt_cancel_lkas = bolt_cancel_lkas_conflict and ( self.cruise_buttons == CruiseButtons.CANCEL or prev_cruise_buttons == CruiseButtons.CANCEL ) @@ -365,7 +363,6 @@ class CarState(CarStateBase): if self.cruise_buttons != CruiseButtons.UNPRESS or prev_cruise_buttons != CruiseButtons.INIT: ret.buttonEvents = [ *cruise_events, - *cancel_gap_events, *distance_events, *lkas_events, ] @@ -374,9 +371,8 @@ class CarState(CarStateBase): ret.lowSpeedAlert = True fp_ret = custom.StarPilotCarState.new_message() - if bolt_cancel_personality and self.cruise_buttons == CruiseButtons.CANCEL: - # Feed long-press personality logic as if distance is held while CANCEL is held. - fp_ret.distancePressed = True + if bolt_cancel_button and self.cruise_buttons == CruiseButtons.CANCEL: + fp_ret.cancelPressed = True fp_ret.sportGear = pt_cp.vl["SportMode"]["SportMode"] == 1 return ret, fp_ret diff --git a/opendbc_repo/opendbc/car/gm/interface.py b/opendbc_repo/opendbc/car/gm/interface.py index 09e4d7f6a9..e0ad846307 100755 --- a/opendbc_repo/opendbc/car/gm/interface.py +++ b/opendbc_repo/opendbc/car/gm/interface.py @@ -93,12 +93,16 @@ BOLT_PEDAL_LONG_CARS = { CAR.CHEVROLET_MALIBU_HYBRID_CC, } -# Cancel-to-personality mapping target: gen1 Bolt pedal-long paths only. +# Cancel-button remap support uses the same safety path on all pedal-long Bolts, +# but only gen1 needs extra LKAS suppression in CarState. BOLT_GEN1_CANCEL_PERSONALITY_CARS = { CAR.CHEVROLET_BOLT_CC_2017, CAR.CHEVROLET_BOLT_CC_2018_2021, } -CANCEL_REMAP_DISTANCE_CARS = BOLT_GEN1_CANCEL_PERSONALITY_CARS +CANCEL_REMAP_DISTANCE_CARS = BOLT_GEN1_CANCEL_PERSONALITY_CARS | { + CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL, + CAR.CHEVROLET_BOLT_CC_2022_2023, +} class CarInterface(CarInterfaceBase): diff --git a/opendbc_repo/opendbc/car/hyundai/carcontroller.py b/opendbc_repo/opendbc/car/hyundai/carcontroller.py index a5ba851e08..4f307de42d 100644 --- a/opendbc_repo/opendbc/car/hyundai/carcontroller.py +++ b/opendbc_repo/opendbc/car/hyundai/carcontroller.py @@ -63,82 +63,6 @@ IONIQ_6_IPEDAL_REGEN_STATE = 0x50 IONIQ_6_IPEDAL_REGEN_STATE_2_PENDING = 0x01 IONIQ_6_IPEDAL_PROGRESS_RETRY_WAIT_FRAMES = 10 IONIQ_6_IPEDAL_RETRY_WAIT_FRAMES = 30 -ANGLE_SAFETY_BASELINE_MODEL = "KIA_SPORTAGE_HEV_2026" - - -def get_baseline_safety_cp(): - from opendbc.car.hyundai.interface import CarInterface - return CarInterface.get_non_essential_params(ANGLE_SAFETY_BASELINE_MODEL) - - -def calculate_angle_torque_reduction_gain(params, CS, apply_torque_last, target_torque_reduction_gain): - target_gain = max(target_torque_reduction_gain, params.ANGLE_ACTIVE_TORQUE_REDUCTION_GAIN) - - driver_torque = abs(CS.out.steeringTorque) - alpha = np.interp(driver_torque, [params.STEER_THRESHOLD * 0.8, params.STEER_THRESHOLD * 2.0], [0.02, 0.1]) - - if CS.out.steeringPressed: - scale = 100.0 - clamped_torque_gain = max(apply_torque_last, params.ANGLE_ACTIVE_TORQUE_REDUCTION_GAIN) - target_gain = params.ANGLE_MIN_TORQUE_REDUCTION_GAIN + (clamped_torque_gain - params.ANGLE_MIN_TORQUE_REDUCTION_GAIN) * \ - np.exp(-(driver_torque - params.STEER_THRESHOLD) / scale) - - new_gain = apply_torque_last + alpha * (target_gain - apply_torque_last) - return float(np.clip(new_gain, params.ANGLE_MIN_TORQUE_REDUCTION_GAIN, params.ANGLE_MAX_TORQUE_REDUCTION_GAIN)) - - -def sp_smooth_angle(v_ego_raw: float, apply_angle: float, apply_angle_last: float) -> float: - if abs(apply_angle - apply_angle_last) <= 0.1: - return apply_angle - - adjusted_alpha = np.interp(v_ego_raw, CarControllerParams.SMOOTHING_ANGLE_VEGO_MATRIX, - CarControllerParams.SMOOTHING_ANGLE_ALPHA_MATRIX) - adjusted_alpha_limited = float(min(float(adjusted_alpha), 1.0)) - return (apply_angle * adjusted_alpha_limited) + (apply_angle_last * (1.0 - adjusted_alpha_limited)) - - -class TorqueReductionGainController: - def __init__(self, angle_threshold=3.0, debounce_time=0.5, min_gain=0.0, max_gain=1.0, - ramp_up_rate=0.1, ramp_down_rate=0.05): - self.angle_threshold = angle_threshold - self.debounce_time = debounce_time - self.min_gain = min_gain - self.max_gain = max_gain - self.ramp_up_rate = ramp_up_rate - self.ramp_down_rate = ramp_down_rate - self.saturated_since = None - self.gain = min_gain - self.last_update_time = 0.0 - - def update(self, last_requested_angle, actual_angle, lat_active): - now = self.last_update_time + DT_CTRL if self.last_update_time > 0.0 else DT_CTRL - dt = now - self.last_update_time if self.last_update_time > 0.0 else DT_CTRL - self.last_update_time = now - - angle_error = abs(last_requested_angle - actual_angle) - saturated = lat_active and angle_error > self.angle_threshold - - if saturated: - if self.saturated_since is None: - self.saturated_since = now - elif (now - self.saturated_since) > self.debounce_time: - self.gain = min(self.gain + self.ramp_up_rate * dt, self.max_gain) - else: - self.saturated_since = None - self.gain = max(self.gain - self.ramp_down_rate * dt, self.min_gain) - - if not lat_active: - self.gain = self.min_gain - self.saturated_since = None - - return self.gain - - def reset(self): - self.gain = self.min_gain - self.saturated_since = None - self.last_update_time = 0.0 - - @dataclass class Ioniq6LongitudinalTuningState: desired_accel: float = 0.0 @@ -300,12 +224,10 @@ class CarController(CarControllerBase): self.packer = CANPacker(dbc_names[Bus.pt]) self.angle_limit_counter = 0 self.VM = VehicleModel(CP) - self.BASELINE_VM = VehicleModel(get_baseline_safety_cp()) self.accel_last = 0 self.apply_torque_last = 0 self.apply_angle_last = 0.0 - self.angle_enable_smoothing_factor = True self.car_fingerprint = CP.carFingerprint self.last_button_frame = 0 self.ecu_disable_failed = False @@ -326,14 +248,6 @@ class CarController(CarControllerBase): self._ioniq_6_regen_request_sent = False self._ioniq_6_last_gear = structs.CarState.GearShifter.unknown self._genesis_g90_long_tuning = GenesisG90LongitudinalTuningState() - self.angle_torque_reduction_gain_controller = TorqueReductionGainController( - angle_threshold=0.3, - debounce_time=0.1, - min_gain=self.params.ANGLE_ACTIVE_TORQUE_REDUCTION_GAIN, - max_gain=self.params.ANGLE_MAX_TORQUE_REDUCTION_GAIN, - ramp_up_rate=self.params.ANGLE_RAMP_UP_TORQUE_REDUCTION_RATE, - ramp_down_rate=self.params.ANGLE_RAMP_DOWN_TORQUE_REDUCTION_RATE, - ) def _reset_ioniq_6_always_ipedal(self) -> None: self._ioniq_6_always_ipedal_pending = False @@ -442,23 +356,17 @@ class CarController(CarControllerBase): desired_angle = float(np.clip(actuators.steeringAngleDeg, -self.params.ANGLE_LIMITS.STEER_ANGLE_MAX, self.params.ANGLE_LIMITS.STEER_ANGLE_MAX)) - if self.angle_enable_smoothing_factor and abs(CS.out.vEgoRaw) < CarControllerParams.SMOOTHING_ANGLE_MAX_VEGO: - desired_angle = sp_smooth_angle(CS.out.vEgoRaw, desired_angle, self.apply_angle_last) - apply_angle = apply_steer_angle_limits_vm(desired_angle, self.apply_angle_last, CS.out.vEgoRaw, CS.out.steeringAngleDeg, CC.latActive, self.params, self.VM) - if self.CP.carFingerprint != ANGLE_SAFETY_BASELINE_MODEL: - apply_angle = apply_steer_angle_limits_vm(apply_angle or desired_angle, self.apply_angle_last, CS.out.vEgoRaw, - CS.out.steeringAngleDeg, CC.latActive, self.params, self.BASELINE_VM) + if CS.out.steeringPressed and abs(CS.out.steeringTorque) > self.params.STEER_THRESHOLD: + apply_torque = self.params.ANGLE_MIN_TORQUE_REDUCTION_GAIN + elif CC.latActive and CS.out.vEgoRaw < 0.3: + apply_torque = self.params.ANGLE_ACTIVE_TORQUE_REDUCTION_GAIN + else: + apply_torque = self.params.ANGLE_MAX_TORQUE_REDUCTION_GAIN if CC.latActive else 0.0 - target_torque_reduction_gain = self.angle_torque_reduction_gain_controller.update( - last_requested_angle=self.apply_angle_last, - actual_angle=CS.out.steeringAngleDeg, - lat_active=CC.latActive, - ) - apply_torque = calculate_angle_torque_reduction_gain(self.params, CS, self.apply_torque_last, target_torque_reduction_gain) - apply_steer_req = CC.latActive and apply_torque != 0 + apply_steer_req = CC.latActive and apply_torque > 0.0 torque_fault = False if apply_angle is None: diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py index f92f95eff8..749ca178da 100644 --- a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py @@ -8,10 +8,9 @@ from opendbc.car import Bus, ButtonType, gen_empty_fingerprint, structs from opendbc.car.structs import CarControl, CarParams from opendbc.car.fw_versions import build_fw_dict, match_fw_to_car from opendbc.car.hyundai.carcontroller import CarController, Ioniq6LongitudinalTuningState, GenesisG90LongitudinalTuningState, \ - IONIQ_6_IPEDAL_PADDLE_BURST_COUNT, IONIQ_6_IPEDAL_NEXT_COUNTER_BURST_COUNT, \ - calculate_angle_torque_reduction_gain, sp_smooth_angle, \ - update_ioniq_6_longitudinal_tuning, \ - update_genesis_g90_longitudinal_tuning + IONIQ_6_IPEDAL_PADDLE_BURST_COUNT, IONIQ_6_IPEDAL_NEXT_COUNTER_BURST_COUNT, \ + update_ioniq_6_longitudinal_tuning, \ + update_genesis_g90_longitudinal_tuning from opendbc.car.hyundai.carstate import CarState, decode_ioniq_6_blindspot_radar_state, decode_ioniq_6_ipedal_intermediate_state, \ decode_ioniq_6_ipedal_state, decode_ioniq_6_max_regen_state from opendbc.car.hyundai.interface import CarInterface @@ -1075,32 +1074,6 @@ class TestHyundaiFingerprint: assert sportage_params.ANGLE_LIMITS.MAX_ANGLE_RATE > comparison_params.ANGLE_LIMITS.MAX_ANGLE_RATE assert comparison_params.ANGLE_LIMITS.MAX_LATERAL_JERK == ioniq6_params.ANGLE_LIMITS.MAX_LATERAL_JERK - def test_angle_smoothing_is_speed_dependent(self): - target_angle = 12.0 - last_angle = 0.0 - - low_speed_smoothed = sp_smooth_angle(0.0, target_angle, last_angle) - high_speed_smoothed = sp_smooth_angle(25.0, target_angle, last_angle) - - assert last_angle < low_speed_smoothed < target_angle - assert high_speed_smoothed == pytest.approx(target_angle) - assert low_speed_smoothed < high_speed_smoothed - - def test_angle_torque_reduction_gain_gives_up_on_driver_override(self): - sportage = CarParams.new_message() - sportage.carFingerprint = CAR.KIA_SPORTAGE_HEV_2026 - sportage.flags = int(HyundaiFlags.CANFD | HyundaiFlags.CANFD_ANGLE_STEERING) - params = CarControllerParams(sportage) - - no_override_cs = SimpleNamespace(out=SimpleNamespace(steeringTorque=0.0, steeringPressed=False)) - override_cs = SimpleNamespace(out=SimpleNamespace(steeringTorque=float(params.STEER_THRESHOLD * 2), steeringPressed=True)) - - no_override_gain = calculate_angle_torque_reduction_gain(params, no_override_cs, params.ANGLE_ACTIVE_TORQUE_REDUCTION_GAIN, 1.0) - override_gain = calculate_angle_torque_reduction_gain(params, override_cs, 1.0, 1.0) - - assert params.ANGLE_ACTIVE_TORQUE_REDUCTION_GAIN <= no_override_gain <= params.ANGLE_MAX_TORQUE_REDUCTION_GAIN - assert params.ANGLE_MIN_TORQUE_REDUCTION_GAIN <= override_gain < no_override_gain - def test_ioniq_5_canfd_aux_messages_are_optional(self): toggles = get_test_toggles() fingerprint = gen_empty_fingerprint() diff --git a/opendbc_repo/opendbc/car/hyundai/values.py b/opendbc_repo/opendbc/car/hyundai/values.py index 6e7b3b8f65..0dc54b2a6e 100644 --- a/opendbc_repo/opendbc/car/hyundai/values.py +++ b/opendbc_repo/opendbc/car/hyundai/values.py @@ -32,14 +32,8 @@ class CarControllerParams: MAX_ANGLE_RATE=5, ) ANGLE_MAX_TORQUE_REDUCTION_GAIN = 1.0 - ANGLE_MIN_TORQUE_REDUCTION_GAIN = 0.1 + ANGLE_MIN_TORQUE_REDUCTION_GAIN = 0.6 ANGLE_ACTIVE_TORQUE_REDUCTION_GAIN = 0.6 - ANGLE_RAMP_UP_TORQUE_REDUCTION_RATE = 0.008 - ANGLE_RAMP_DOWN_TORQUE_REDUCTION_RATE = 0.0012 - ANGLE_TORQUE_OVERRIDE_CYCLES = 17 - SMOOTHING_ANGLE_VEGO_MATRIX = [0.0, 8.5, 11.0, 13.8, 22.22] - SMOOTHING_ANGLE_ALPHA_MATRIX = [0.05, 0.1, 0.3, 0.6, 1.0] - SMOOTHING_ANGLE_MAX_VEGO = SMOOTHING_ANGLE_VEGO_MATRIX[-1] def __init__(self, CP, vEgoRaw=100.): self.ANGLE_LIMITS = self.ANGLE_LIMITS diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index 3237442641..77365b91fb 100644 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -11,7 +11,6 @@ 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.safety import ALTERNATIVE_EXPERIENCE from openpilot.common.params import Params from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper, DT_CTRL @@ -160,6 +159,7 @@ class SelfdriveD: self.starpilot_AM = AlertManager() self.starpilot_events = Events(starpilot=True) + self.cancel_pressed_previously = False self.distance_pressed_previously = False self.display_timer = 0 @@ -168,8 +168,7 @@ class SelfdriveD: self.starpilot_events_prev = [] - remap_cancel_to_distance = bool(self.CP.alternativeExperience & ALTERNATIVE_EXPERIENCE.GM_REMAP_CANCEL_TO_DISTANCE) - self.has_menu = self.CP.brand == "gm" and not (self.CP.flags & GMFlags.NO_CAMERA.value) and not remap_cancel_to_distance + self.has_menu = self.CP.brand == "gm" and not (self.CP.flags & GMFlags.NO_CAMERA.value) self.FPCP = messaging.log_from_bytes(self.params.get("StarPilotCarParams", block=True), custom.StarPilotCarParams) @@ -516,10 +515,22 @@ class SelfdriveD: if self.sm['modelV2'].frameDropPerc > 20: self.events.add(EventName.modeldLagging) - # Decrement personality on distance button press + # Decrement personality on configured steering-wheel button presses if self.CP.openpilotLongitudinalControl: + cancel_pressed = False distance_pressed = False + if self.starpilot_toggles.personality_profile_via_cancel: + cancel_pressed |= bool(getattr(self.sm['starpilotCarState'], "cancelPressed", False)) + cancel_pressed &= not ( + self.sm['starpilotCarState'].cancelLongPressed or + self.sm['starpilotCarState'].cancelVeryLongPressed + ) + if self.starpilot_toggles.personality_profile_via_cancel_long: + cancel_pressed |= self.sm['starpilotCarState'].cancelLongPressed + if self.starpilot_toggles.personality_profile_via_cancel_very_long: + cancel_pressed |= self.sm['starpilotCarState'].cancelVeryLongPressed + if self.starpilot_toggles.personality_profile_via_distance: distance_pressed |= any(not be.pressed and be.type == ButtonType.gapAdjustCruise for be in CS.buttonEvents) distance_pressed &= not (self.sm['starpilotCarState'].distanceLongPressed or self.sm['starpilotCarState'].distanceVeryLongPressed) @@ -530,6 +541,11 @@ class SelfdriveD: if self.starpilot_toggles.personality_profile_via_lkas: distance_pressed |= any(not be.pressed and be.type == ButtonType.lkas for be in CS.buttonEvents) + if not cancel_pressed and self.cancel_pressed_previously and not self.safe_mode: + self.personality = (self.personality - 1) % 3 + self.params.put_nonblocking('LongitudinalPersonality', self.personality) + self.events.add(EventName.personalityChanged) + if not distance_pressed and self.distance_pressed_previously and not self.safe_mode: if self.display_timer > 0 or not self.has_menu: self.personality = (self.personality - 1) % 3 @@ -537,6 +553,7 @@ class SelfdriveD: self.events.add(EventName.personalityChanged) self.display_timer = 350 + self.cancel_pressed_previously = cancel_pressed self.distance_pressed_previously = distance_pressed self.display_timer -= 1 diff --git a/selfdrive/ui/layouts/settings/starpilot/vehicle.py b/selfdrive/ui/layouts/settings/starpilot/vehicle.py index acb3127566..32d1354af2 100644 --- a/selfdrive/ui/layouts/settings/starpilot/vehicle.py +++ b/selfdrive/ui/layouts/settings/starpilot/vehicle.py @@ -35,6 +35,7 @@ from openpilot.selfdrive.ui.mici.layouts.settings.fingerprint_catalog import ( get_fingerprint_catalog, shorten_model_label, ) +from openpilot.starpilot.common.starpilot_variables import migrate_cancel_button_controls ACTION_OPTIONS = [ @@ -466,9 +467,10 @@ class VehicleSettingsManagerView(Widget): rows.append({"target_id": "toggle:FrogsGoMoosTweak", "type": "toggle", "title": tr("FrogsGoMoo Tweak"), "get_state": lambda: self._controller._params.get_bool("FrogsGoMoosTweak")}) - rows.append({"target_id": "toggle:RemapCancelToDistance", "type": "toggle", - "title": tr("Remap Cancel Button"), "subtitle": tr("Remap the Cancel button to act as the Distance button."), - "get_state": lambda: self._controller._params.get_bool("RemapCancelToDistance")}) + if cs.isBolt and cs.hasPedal: + rows.append({"target_id": "toggle:RemapCancelToDistance", "type": "toggle", + "title": tr("Remap Cancel Button"), "subtitle": tr("Treat the Cancel button as an extra mappable steering-wheel button."), + "get_state": lambda: self._controller._params.get_bool("RemapCancelToDistance")}) if cs.isHKGCanFd: rows.append({"target_id": "toggle:AlwaysIPedal", "type": "toggle", "title": tr("Always I-Pedal"), @@ -487,6 +489,10 @@ class VehicleSettingsManagerView(Widget): for key in ("DistanceButtonControl", "LongDistanceButtonControl", "VeryLongDistanceButtonControl"): rows.append({"target_id": f"select:{key}", "type": "select", "title": tr(self._controller._action_title(key)), "get_value": lambda k=key: self._controller._get_action_name(k), "pill_width": 140}) + if cs.isBolt and cs.hasPedal and self._controller._params.get_bool("RemapCancelToDistance"): + for key in ("CancelButtonControl", "LongCancelButtonControl", "VeryLongCancelButtonControl"): + rows.append({"target_id": f"select:{key}", "type": "select", "title": tr(self._controller._action_title(key)), + "get_value": lambda k=key: self._controller._get_action_name(k), "pill_width": 140}) if not cs.isSubaru and not (cs.lkasAllowedForAOL and self._controller._params.get_bool("AlwaysOnLateral") and self._controller._params.get_bool("AlwaysOnLateralLKAS")): rows.append({"target_id": "select:LKASButtonControl", "type": "select", "title": tr("LKAS Button"), "get_value": lambda: self._controller._get_action_name("LKASButtonControl"), "pill_width": 140}) @@ -507,8 +513,11 @@ class StarPilotVehicleSettingsLayout(_SettingsPage): def _action_title(self, key: str) -> str: titles = { + "CancelButtonControl": "Cancel Button", "DistanceButtonControl": "Distance Button", + "LongCancelButtonControl": "Cancel (Long Press)", "LongDistanceButtonControl": "Distance (Long Press)", + "VeryLongCancelButtonControl": "Cancel (Very Long)", "VeryLongDistanceButtonControl": "Distance (Very Long)", "LKASButtonControl": "LKAS Button", "ModeButtonControl": "Mode Button", @@ -521,16 +530,10 @@ class StarPilotVehicleSettingsLayout(_SettingsPage): return titles.get(key, key) def _get_action_name(self, key: str) -> str: - if key == "LKASButtonControl" and self._params.get_bool("RemapCancelToDistance"): - if self._params.get_int("LKASButtonControl") != 0: - self._params.put_int("LKASButtonControl", 0) - return tr(ACTION_NAME_BY_ID[0]) idx = self._params.get_int(key) return tr(ACTION_NAME_BY_ID.get(idx, ACTION_NAMES[0])) def _get_available_actions(self, key: str | None = None) -> list[str]: - if key == "LKASButtonControl" and self._params.get_bool("RemapCancelToDistance"): - return [tr(ACTION_NAME_BY_ID[0])] cs = starpilot_state.car_state return [tr(o["name"]) for o in ACTION_OPTIONS if cs.hasOpenpilotLongitudinal or not o.get("requires_longitudinal", False)] @@ -550,8 +553,8 @@ class StarPilotVehicleSettingsLayout(_SettingsPage): if param_key == "RemapCancelToDistance": new_state = not self._params.get_bool("RemapCancelToDistance") self._params.put_bool("RemapCancelToDistance", new_state) - if new_state and self._params.get_int("LKASButtonControl") != 0: - self._params.put_int("LKASButtonControl", 0) + if new_state: + migrate_cancel_button_controls(self._params) return current = self._params.get_bool(param_key) if self._params.get(param_key, encoding="utf-8") is not None else False self._params.put_bool(param_key, not current) @@ -616,10 +619,6 @@ class StarPilotVehicleSettingsLayout(_SettingsPage): gui_app.push_widget(dialog) def _show_action_picker(self, key: str): - if key == "LKASButtonControl" and self._params.get_bool("RemapCancelToDistance"): - if self._params.get_int("LKASButtonControl") != 0: - self._params.put_int("LKASButtonControl", 0) - return actions = self._get_available_actions(key) current = self._get_action_name(key) if current not in actions: diff --git a/starpilot/common/starpilot_variables.py b/starpilot/common/starpilot_variables.py index c58411ed1b..1a9b01b42d 100644 --- a/starpilot/common/starpilot_variables.py +++ b/starpilot/common/starpilot_variables.py @@ -136,6 +136,13 @@ BUTTON_FUNCTIONS = { "BOOKMARK": 8, } +CANCEL_BUTTON_MIGRATION_KEY = "CancelButtonControlsMigrated" +CANCEL_BUTTON_MAPPINGS = ( + ("DistanceButtonControl", "CancelButtonControl"), + ("LongDistanceButtonControl", "LongCancelButtonControl"), + ("VeryLongDistanceButtonControl", "VeryLongCancelButtonControl"), +) + DEVELOPER_SIDEBAR_METRICS = { "NONE": 0, "ACCELERATION_CURRENT": 1, @@ -315,16 +322,30 @@ def process_starpilot_toggles(toggles): return StarPilotVariables().starpilot_toggles def update_starpilot_toggles(): + migrate_cancel_button_controls() + if not hasattr(update_starpilot_toggles, "_params_memory"): update_starpilot_toggles._params_memory = Params(memory=True) update_starpilot_toggles._params_memory.put_bool("StarPilotTogglesUpdated", True) +def migrate_cancel_button_controls(params: Params | None = None) -> bool: + params = params or Params(return_defaults=True) + if params.get_bool(CANCEL_BUTTON_MIGRATION_KEY) or not params.get_bool("RemapCancelToDistance"): + return False + + for source_key, target_key in CANCEL_BUTTON_MAPPINGS: + params.put_int(target_key, params.get_int(source_key)) + + params.put_bool(CANCEL_BUTTON_MIGRATION_KEY, True) + return True + class StarPilotVariables: def __init__(self): self.params = Params(return_defaults=True) self.params_raw = Params() self.params_memory = Params(memory=True) + migrate_cancel_button_controls(self.params) self.starpilot_toggles = SimpleNamespace() toggle = self.starpilot_toggles @@ -781,6 +802,10 @@ class StarPilotVariables: toggle.always_ipedal = self.get_value("AlwaysIPedal", condition=toggle.car_model == HYUNDAI_CAR.HYUNDAI_IONIQ_6) toggle.nostalgia_mode = self.get_value("NostalgiaMode", condition=toggle.openpilot_longitudinal and toggle.car_model == HYUNDAI_CAR.HYUNDAI_IONIQ_6) + toggle.remap_cancel_to_distance = self.get_value( + "RemapCancelToDistance", + condition=toggle.car_make == "gm" and toggle.has_pedal and "BOLT" in toggle.car_model, + ) distance_button_control = self.get_value("DistanceButtonControl", cast=float) toggle.experimental_mode_via_distance = toggle.openpilot_longitudinal and distance_button_control == BUTTON_FUNCTIONS["EXPERIMENTAL_MODE"] @@ -815,6 +840,51 @@ class StarPilotVariables: toggle.traffic_mode_via_distance_very_long = toggle.openpilot_longitudinal and distance_button_control_very_long == BUTTON_FUNCTIONS["TRAFFIC_MODE"] toggle.bookmark_via_distance_very_long = distance_button_control_very_long == BUTTON_FUNCTIONS["BOOKMARK"] + cancel_button_control = self.get_value( + "CancelButtonControl", + cast=float, + condition=toggle.remap_cancel_to_distance, + ) + toggle.experimental_mode_via_cancel = toggle.openpilot_longitudinal and cancel_button_control == BUTTON_FUNCTIONS["EXPERIMENTAL_MODE"] + toggle.experimental_mode_via_press |= toggle.experimental_mode_via_cancel + toggle.force_coast_via_cancel = toggle.openpilot_longitudinal and cancel_button_control == BUTTON_FUNCTIONS["FORCE_COAST"] + toggle.pause_lateral_via_cancel = cancel_button_control == BUTTON_FUNCTIONS["PAUSE_LATERAL"] + toggle.pause_longitudinal_via_cancel = toggle.openpilot_longitudinal and cancel_button_control == BUTTON_FUNCTIONS["PAUSE_LONGITUDINAL"] + toggle.personality_profile_via_cancel = toggle.openpilot_longitudinal and cancel_button_control == BUTTON_FUNCTIONS["PERSONALITY_PROFILE"] + toggle.switchback_mode_via_cancel = cancel_button_control == BUTTON_FUNCTIONS["SWITCHBACK_MODE"] + toggle.traffic_mode_via_cancel = toggle.openpilot_longitudinal and cancel_button_control == BUTTON_FUNCTIONS["TRAFFIC_MODE"] + toggle.bookmark_via_cancel = cancel_button_control == BUTTON_FUNCTIONS["BOOKMARK"] + + cancel_button_control_long = self.get_value( + "LongCancelButtonControl", + cast=float, + condition=toggle.remap_cancel_to_distance, + ) + toggle.experimental_mode_via_cancel_long = toggle.openpilot_longitudinal and cancel_button_control_long == BUTTON_FUNCTIONS["EXPERIMENTAL_MODE"] + toggle.experimental_mode_via_press |= toggle.experimental_mode_via_cancel_long + toggle.force_coast_via_cancel_long = toggle.openpilot_longitudinal and cancel_button_control_long == BUTTON_FUNCTIONS["FORCE_COAST"] + toggle.pause_lateral_via_cancel_long = cancel_button_control_long == BUTTON_FUNCTIONS["PAUSE_LATERAL"] + toggle.pause_longitudinal_via_cancel_long = toggle.openpilot_longitudinal and cancel_button_control_long == BUTTON_FUNCTIONS["PAUSE_LONGITUDINAL"] + toggle.personality_profile_via_cancel_long = toggle.openpilot_longitudinal and cancel_button_control_long == BUTTON_FUNCTIONS["PERSONALITY_PROFILE"] + toggle.switchback_mode_via_cancel_long = cancel_button_control_long == BUTTON_FUNCTIONS["SWITCHBACK_MODE"] + toggle.traffic_mode_via_cancel_long = toggle.openpilot_longitudinal and cancel_button_control_long == BUTTON_FUNCTIONS["TRAFFIC_MODE"] + toggle.bookmark_via_cancel_long = cancel_button_control_long == BUTTON_FUNCTIONS["BOOKMARK"] + + cancel_button_control_very_long = self.get_value( + "VeryLongCancelButtonControl", + cast=float, + condition=toggle.remap_cancel_to_distance, + ) + toggle.experimental_mode_via_cancel_very_long = toggle.openpilot_longitudinal and cancel_button_control_very_long == BUTTON_FUNCTIONS["EXPERIMENTAL_MODE"] + toggle.experimental_mode_via_press |= toggle.experimental_mode_via_cancel_very_long + toggle.force_coast_via_cancel_very_long = toggle.openpilot_longitudinal and cancel_button_control_very_long == BUTTON_FUNCTIONS["FORCE_COAST"] + toggle.pause_lateral_via_cancel_very_long = cancel_button_control_very_long == BUTTON_FUNCTIONS["PAUSE_LATERAL"] + toggle.pause_longitudinal_via_cancel_very_long = toggle.openpilot_longitudinal and cancel_button_control_very_long == BUTTON_FUNCTIONS["PAUSE_LONGITUDINAL"] + toggle.personality_profile_via_cancel_very_long = toggle.openpilot_longitudinal and cancel_button_control_very_long == BUTTON_FUNCTIONS["PERSONALITY_PROFILE"] + toggle.switchback_mode_via_cancel_very_long = cancel_button_control_very_long == BUTTON_FUNCTIONS["SWITCHBACK_MODE"] + toggle.traffic_mode_via_cancel_very_long = toggle.openpilot_longitudinal and cancel_button_control_very_long == BUTTON_FUNCTIONS["TRAFFIC_MODE"] + toggle.bookmark_via_cancel_very_long = cancel_button_control_very_long == BUTTON_FUNCTIONS["BOOKMARK"] + toggle.frogsgomoo_tweak = self.get_value("FrogsGoMoosTweak", condition=toggle.openpilot_longitudinal and toggle.car_make == "toyota") toggle.stoppingDecelRate = 0.01 if toggle.frogsgomoo_tweak else toggle.stoppingDecelRate toggle.vEgoStarting = 0.1 if toggle.frogsgomoo_tweak else toggle.vEgoStarting @@ -1210,10 +1280,6 @@ class StarPilotVariables: condition=toggle.openpilot_longitudinal and toggle.car_make == "gm", ) toggle.remote_start_boots_comma = self.get_value("RemoteStartBootsComma", condition=toggle.car_make == "gm") - toggle.remap_cancel_to_distance = self.get_value( - "RemapCancelToDistance", - condition=toggle.car_make == "gm" and toggle.has_pedal and "BOLT" in toggle.car_model, - ) gm_auto_hold_supported = toggle.car_model in LEGACY_VOLT_STOCK_ACC_CARS toggle.gm_auto_hold = self.get_value("GMAutoHold", condition=gm_auto_hold_supported) diff --git a/starpilot/common/tests/test_starpilot_variables.py b/starpilot/common/tests/test_starpilot_variables.py index 265becafbc..b23b11e868 100644 --- a/starpilot/common/tests/test_starpilot_variables.py +++ b/starpilot/common/tests/test_starpilot_variables.py @@ -30,17 +30,33 @@ def test_get_starpilot_toggles_uses_last_non_empty_broadcast(monkeypatch): class _FakeParams: - def __init__(self, floats=None): + def __init__(self, floats=None, ints=None, bools=None): self.floats = dict(floats or {}) + self.ints = dict(ints or {}) + self.bools = dict(bools or {}) def get_float(self, key): return float(self.floats.get(key, 0.0)) + def get_int(self, key): + return int(self.ints.get(key, 0)) + + def get_bool(self, key): + return bool(self.bools.get(key, False)) + def put_float(self, key, value): self.floats[key] = float(value) + def put_int(self, key, value): + self.ints[key] = int(value) + + def put_bool(self, key, value): + self.bools[key] = bool(value) + def remove(self, key): self.floats.pop(key, None) + self.ints.pop(key, None) + self.bools.pop(key, None) def test_sync_stock_param_does_not_stomp_existing_custom_value_when_stock_missing(): @@ -52,3 +68,26 @@ def test_sync_stock_param_does_not_stomp_existing_custom_value_when_stock_missin assert params.get_float("SteerDelay") == 0.35 assert params.get_float("SteerDelayStock") == 0.10 + + +def test_cancel_button_migration_copies_distance_actions_once(): + params = _FakeParams( + ints={ + "DistanceButtonControl": 8, + "LongDistanceButtonControl": 4, + "VeryLongDistanceButtonControl": 7, + }, + bools={"RemapCancelToDistance": True}, + ) + + assert spv.migrate_cancel_button_controls(params) is True + assert params.get_int("CancelButtonControl") == 8 + assert params.get_int("LongCancelButtonControl") == 4 + assert params.get_int("VeryLongCancelButtonControl") == 7 + assert params.get_bool(spv.CANCEL_BUTTON_MIGRATION_KEY) is True + + params.put_int("DistanceButtonControl", 1) + params.put_int("CancelButtonControl", 3) + + assert spv.migrate_cancel_button_controls(params) is False + assert params.get_int("CancelButtonControl") == 3 diff --git a/starpilot/controls/starpilot_card.py b/starpilot/controls/starpilot_card.py index e85c356261..38710063e8 100644 --- a/starpilot/controls/starpilot_card.py +++ b/starpilot/controls/starpilot_card.py @@ -25,6 +25,7 @@ class StarPilotCard: self.prev_active = False self.prev_cruise_enabled = False self.decel_pressed = False + self.cancelPressed_previously = False self.distancePressed_previously = False self.force_coast = False self.modePressed_previously = False @@ -37,6 +38,7 @@ class StarPilotCard: self.traffic_mode_enabled = False self.gap_counter = 0 + self.cancel_counter = 0 self._distance_poll_counter = 0 self._onroad_distance_pressed = False @@ -149,6 +151,22 @@ 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: + self.cancel_counter = 0 + + 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) + elif self.cancel_counter == self.long_press_threshold: + self.handle_button_event("cancel_long", sm, starpilot_toggles) + 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) + if any(be.pressed and be.type == ButtonType.lkas for be in carState.buttonEvents): self.handle_button_event("lkas", sm, starpilot_toggles) @@ -186,6 +204,8 @@ class StarPilotCard: starpilotCarState.accelPressed = self.accel_pressed starpilotCarState.alwaysOnLateralAllowed = self.always_on_lateral_allowed starpilotCarState.alwaysOnLateralEnabled = self.always_on_lateral_enabled + starpilotCarState.cancelLongPressed = self.very_long_press_threshold > self.cancel_counter >= self.long_press_threshold + starpilotCarState.cancelVeryLongPressed = self.cancel_counter >= self.very_long_press_threshold starpilotCarState.decelPressed = self.decel_pressed starpilotCarState.distanceLongPressed = self.very_long_press_threshold > self.gap_counter >= self.long_press_threshold starpilotCarState.distanceVeryLongPressed = self.gap_counter >= self.very_long_press_threshold diff --git a/starpilot/controls/tests/test_starpilot_card.py b/starpilot/controls/tests/test_starpilot_card.py index 50d43a5431..bae444bbfb 100644 --- a/starpilot/controls/tests/test_starpilot_card.py +++ b/starpilot/controls/tests/test_starpilot_card.py @@ -46,6 +46,9 @@ def make_toggles(**overrides): "always_on_lateral_lkas": False, "always_on_lateral_main": False, "always_on_lateral_pause_speed": 0.0, + "bookmark_via_cancel": False, + "bookmark_via_cancel_long": False, + "bookmark_via_cancel_very_long": False, "bookmark_via_lkas": False, "conditional_experimental_mode": False, "experimental_mode_via_lkas": False, @@ -167,3 +170,29 @@ def test_pacifica_hybrid_main_aol_waits_for_set_press(monkeypatch, tmp_path): ret = card.update(car_state, starpilot_car_state, sm, toggles) assert ret.alwaysOnLateralAllowed is False assert ret.alwaysOnLateralEnabled is False + + +def test_cancel_button_short_press_can_run_independent_mapping(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() + toggles = make_toggles(bookmark_via_cancel=True) + starpilot_car_state = SimpleNamespace(distancePressed=False, cancelPressed=False) + + card.update(make_car_state(), starpilot_car_state, sm, toggles) + assert card.params_memory.get_int("WheelButtonBookmarkCounter") == 0 + + starpilot_car_state.cancelPressed = True + ret = card.update(make_car_state(), starpilot_car_state, sm, toggles) + assert ret.cancelLongPressed is False + assert ret.cancelVeryLongPressed is False + assert card.params_memory.get_int("WheelButtonBookmarkCounter") == 0 + + starpilot_car_state.cancelPressed = False + ret = card.update(make_car_state(), starpilot_car_state, sm, toggles) + assert ret.cancelLongPressed is False + assert ret.cancelVeryLongPressed is False + assert card.params_memory.get_int("WheelButtonBookmarkCounter") == 1 diff --git a/starpilot/system/the_pond/assets/components/tools/device_settings.js b/starpilot/system/the_pond/assets/components/tools/device_settings.js index 3fccbeb618..6b3e05fae8 100644 --- a/starpilot/system/the_pond/assets/components/tools/device_settings.js +++ b/starpilot/system/the_pond/assets/components/tools/device_settings.js @@ -719,10 +719,9 @@ function clearSearchFilter() { scheduleSyncInputs() } +const cancelButtonKeys = new Set(["CancelButtonControl", "LongCancelButtonControl", "VeryLongCancelButtonControl"]) + function getSettingLockReason(param) { - if (param?.key === "LKASButtonControl" && !!state.values.RemapCancelToDistance) { - return "Cancel remap requires the LKAS button to stay on No Action." - } return "" } @@ -748,6 +747,9 @@ function renderSettingRow(p) { if (!isParamEnabledForChildren(p.parent_key)) return "" if (!state.expanded[p.parent_key]) return "" } + if (cancelButtonKeys.has(p?.key) && !state.values.RemapCancelToDistance) { + return "" + } const isNumeric = p.ui_type === "numeric" const isColor = p.ui_type === "color" diff --git a/starpilot/system/the_pond/assets/components/tools/device_settings_layout.json b/starpilot/system/the_pond/assets/components/tools/device_settings_layout.json index 870bc3e0f5..fbd601f35d 100644 --- a/starpilot/system/the_pond/assets/components/tools/device_settings_layout.json +++ b/starpilot/system/the_pond/assets/components/tools/device_settings_layout.json @@ -2318,8 +2318,8 @@ "params": [ { "key": "RemapCancelToDistance", - "label": "Remap Cancel To Distance", - "description": "On pedal-interceptor Bolts, remap the steering-wheel CANCEL button to distance/personality input.", + "label": "Remap Cancel Button", + "description": "On pedal-interceptor Bolts, treat the steering-wheel CANCEL button as an extra mappable button.", "data_type": "bool", "ui_type": "toggle" }, @@ -2337,6 +2337,51 @@ "data_type": "bool", "ui_type": "toggle" }, + { + "key": "CancelButtonControl", + "label": "Cancel Button (Short Press)", + "description": "Action performed when the remapped \"Cancel\" button is pressed.", + "data_type": "int", + "ui_type": "dropdown", + "options": [ + { + "value": 0, + "label": "No Action" + }, + { + "value": 1, + "label": "Change Personality" + }, + { + "value": 2, + "label": "Force Coast" + }, + { + "value": 3, + "label": "Pause Steering" + }, + { + "value": 4, + "label": "Pause Accel/Brake" + }, + { + "value": 5, + "label": "Toggle Experimental" + }, + { + "value": 6, + "label": "Toggle Traffic" + }, + { + "value": 7, + "label": "Toggle Switchback" + }, + { + "value": 8, + "label": "Create Bookmark" + } + ] + }, { "key": "DistanceButtonControl", "label": "Distance Button (Short Press)", @@ -2382,6 +2427,51 @@ } ] }, + { + "key": "LongCancelButtonControl", + "label": "Cancel Button (Long Press)", + "description": "Action performed when the remapped \"Cancel\" button is pressed for more than 0.5 seconds.", + "data_type": "int", + "ui_type": "dropdown", + "options": [ + { + "value": 0, + "label": "No Action" + }, + { + "value": 1, + "label": "Change Personality" + }, + { + "value": 2, + "label": "Force Coast" + }, + { + "value": 3, + "label": "Pause Steering" + }, + { + "value": 4, + "label": "Pause Accel/Brake" + }, + { + "value": 5, + "label": "Toggle Experimental" + }, + { + "value": 6, + "label": "Toggle Traffic" + }, + { + "value": 7, + "label": "Toggle Switchback" + }, + { + "value": 8, + "label": "Create Bookmark" + } + ] + }, { "key": "LongDistanceButtonControl", "label": "Distance Button (Long Press)", @@ -2427,6 +2517,51 @@ } ] }, + { + "key": "VeryLongCancelButtonControl", + "label": "Cancel Button (Extra Long Press)", + "description": "Action performed when the remapped \"Cancel\" button is pressed for more than 2.5 seconds.", + "data_type": "int", + "ui_type": "dropdown", + "options": [ + { + "value": 0, + "label": "No Action" + }, + { + "value": 1, + "label": "Change Personality" + }, + { + "value": 2, + "label": "Force Coast" + }, + { + "value": 3, + "label": "Pause Steering" + }, + { + "value": 4, + "label": "Pause Accel/Brake" + }, + { + "value": 5, + "label": "Toggle Experimental" + }, + { + "value": 6, + "label": "Toggle Traffic" + }, + { + "value": 7, + "label": "Toggle Switchback" + }, + { + "value": 8, + "label": "Create Bookmark" + } + ] + }, { "key": "VeryLongDistanceButtonControl", "label": "Distance Button (Extra Long Press)", diff --git a/starpilot/system/the_pond/the_pond.py b/starpilot/system/the_pond/the_pond.py index 1fdd43fe65..9096eab440 100644 --- a/starpilot/system/the_pond/the_pond.py +++ b/starpilot/system/the_pond/the_pond.py @@ -63,7 +63,7 @@ from openpilot.starpilot.common.maps_catalog import ( from openpilot.starpilot.common.experimental_state import sync_persist_experimental_state from openpilot.starpilot.common.starpilot_utilities import delete_file, get_lock_status, run_cmd from openpilot.starpilot.common.starpilot_variables import ACTIVE_THEME_PATH, ERROR_LOGS_PATH, EXCLUDED_KEYS, LEGACY_STARPILOT_PARAM_RENAMES, MAPS_PATH, MODELS_PATH, RESOURCES_REPO, SCREEN_RECORDINGS_PATH, STOCK_THEME_PATH, THEME_SAVE_PATH,\ - default_ev_tuning_enabled, update_starpilot_toggles + default_ev_tuning_enabled, migrate_cancel_button_controls, update_starpilot_toggles from openpilot.starpilot.common.testing_grounds import ( DEFAULT_TESTING_GROUND_VARIANT as SHARED_DEFAULT_TESTING_GROUND_VARIANT, TESTING_GROUND_VARIANT_LABELS as SHARED_TESTING_GROUND_VARIANT_LABELS, @@ -784,17 +784,6 @@ def _get_param_int_value(key, default=0): except Exception: return int(default) -def _enforce_cancel_remap_lkas_lock(): - if not params.get_bool("RemapCancelToDistance"): - return False - - if _get_param_int_value("LKASButtonControl", 0) == 0: - return False - - params.put("LKASButtonControl", "0") - update_starpilot_toggles() - return True - def _get_system_uptime_seconds(): try: with open("/proc/uptime", "r", encoding="utf-8") as uptime_file: @@ -3822,21 +3811,23 @@ def setup(app): metered_enabled = str_val.strip() in ("1", "true", "True") gsm_metered_apply_result = _apply_cellular_metered_setting(metered_enabled) - locked_lkas = _enforce_cancel_remap_lkas_lock() + migrated_cancel_buttons = migrate_cancel_button_controls(params) update_starpilot_toggles() response = {"message": f"Parameter '{key}' updated successfully."} updated = {} if key == "RemapCancelToDistance" and params.get_bool("RemapCancelToDistance"): updated["RemapCancelToDistance"] = True - updated["LKASButtonControl"] = 0 - response["message"] = "Remap Cancel To Distance enabled. LKAS Button has been locked to No Action." - elif key == "LKASButtonControl" and params.get_bool("RemapCancelToDistance"): - updated["LKASButtonControl"] = 0 - updated["RemapCancelToDistance"] = True - response["message"] = "LKAS Button is locked to No Action while Remap Cancel To Distance is enabled." - elif locked_lkas: - updated["LKASButtonControl"] = 0 + response["message"] = "Remap Cancel Button enabled." + if migrated_cancel_buttons: + if key == "RemapCancelToDistance" and params.get_bool("RemapCancelToDistance"): + response["message"] = "Remap Cancel Button enabled. Existing distance mappings were copied to the new cancel button." + for source_key, target_key in ( + ("DistanceButtonControl", "CancelButtonControl"), + ("LongDistanceButtonControl", "LongCancelButtonControl"), + ("VeryLongDistanceButtonControl", "VeryLongCancelButtonControl"), + ): + updated[target_key] = _get_param_int_value(target_key, _get_param_int_value(source_key, 0)) if gsm_metered_apply_result is not None: updated["GsmMetered"] = str_val.strip() in ("1", "true", "True") @@ -3864,7 +3855,7 @@ def setup(app): @app.route("/api/params/all", methods=["GET"]) def get_all_params(): - _enforce_cancel_remap_lkas_lock() + migrate_cancel_button_controls(params) allowed_keys, types = _get_param_type_info() defaults_lookup = _get_default_param_values() diff --git a/starpilot/ui/qt/offroad/vehicle_settings.cc b/starpilot/ui/qt/offroad/vehicle_settings.cc index b445ea3f4f..8766684cfb 100644 --- a/starpilot/ui/qt/offroad/vehicle_settings.cc +++ b/starpilot/ui/qt/offroad/vehicle_settings.cc @@ -178,7 +178,7 @@ StarPilotVehiclesPanel::StarPilotVehiclesPanel(StarPilotSettingsWindow *parent, {"GMDashSpoofOffsets", tr("Apply Offsets To Dash Spoof"), tr("On GM pedal-long cars, add the configured set-speed offset to the spoofed dash set speed so it matches the on-screen set speed."), ""}, {"LongPitch", tr("Smooth Pedal Response on Hills"), tr("Smoothen acceleration and braking when driving downhill/uphill."), ""}, {"RemoteStartBootsComma", tr("Remote Start Boots comma"), tr("Use the remote-start GM panda firmware at boot.

Required for GM remote-start startup signal behavior."), ""}, - {"RemapCancelToDistance", tr("Remap Cancel To Distance"), tr("On pedal-interceptor Bolts, remap the steering-wheel CANCEL button to distance/personality input."), ""}, + {"RemapCancelToDistance", tr("Remap Cancel Button"), tr("On pedal-interceptor Bolts, treat the steering-wheel CANCEL button as an extra mappable button."), ""}, {"VoltSNG", tr("Stop-and-Go Hack"), tr("Force stop-and-go on the 2017 Chevy Volt."), ""}, {"HKGToggles", tr("Hyundai/Kia/Genesis Settings"), tr("StarPilot features for Genesis, Hyundai, and Kia vehicles."), ""}, @@ -306,11 +306,6 @@ StarPilotVehiclesPanel::StarPilotVehiclesPanel(StarPilotSettingsWindow *parent, QSet rebootKeys = {"RemapCancelToDistance", "TacoTuneHacks"}; for (const QString &key : rebootKeys) { QObject::connect(static_cast(toggles[key]), &ToggleControl::toggleFlipped, [key, this](bool state) { - if (key == "RemapCancelToDistance" && state && params.getInt("LKASButtonControl") != 0) { - params.putInt("LKASButtonControl", 0); - updateStarPilotToggles(); - } - if (started) { if (key == "TacoTuneHacks" && state) { if (StarPilotConfirmationDialog::toggleReboot(this)) { diff --git a/starpilot/ui/qt/offroad/wheel_settings.cc b/starpilot/ui/qt/offroad/wheel_settings.cc index acaf64a5dd..189528429d 100644 --- a/starpilot/ui/qt/offroad/wheel_settings.cc +++ b/starpilot/ui/qt/offroad/wheel_settings.cc @@ -35,19 +35,6 @@ QString getWheelFunctionLabel(Params ¶ms, const QString &key) { return functionsMap.value(params.getInt(key.toStdString()), QObject::tr("No Action")); } -bool lockLkasButtonIfNeeded(Params ¶ms) { - if (!params.getBool("RemapCancelToDistance")) { - return false; - } - - if (params.getInt("LKASButtonControl") != 0) { - params.putInt("LKASButtonControl", 0); - updateStarPilotToggles(); - } - - return true; -} - } // namespace StarPilotWheelPanel::StarPilotWheelPanel(StarPilotSettingsWindow *parent, bool forceOpen) : StarPilotListWidget(parent), parent(parent) { @@ -84,8 +71,11 @@ StarPilotWheelPanel::StarPilotWheelPanel(StarPilotSettingsWindow *parent, bool f }); const std::vector> wheelToggles { + {"CancelButtonControl", tr("Cancel Button"), tr("Action performed when the remapped \"Cancel\" button is pressed."), "../../starpilot/assets/toggle_icons/icon_mute.png"}, {"DistanceButtonControl", tr("Distance Button"), tr("Action performed when the \"Distance\" button is pressed."), "../../starpilot/assets/toggle_icons/icon_mute.png"}, + {"LongCancelButtonControl", tr("Cancel Button (Long Press)"), tr("Action performed when the remapped \"Cancel\" button is pressed for more than 0.5 seconds."), "../../starpilot/assets/toggle_icons/icon_mute.png"}, {"LongDistanceButtonControl", tr("Distance Button (Long Press)"), tr("Action performed when the \"Distance\" button is pressed for more than 0.5 seconds."), "../../starpilot/assets/toggle_icons/icon_mute.png"}, + {"VeryLongCancelButtonControl", tr("Cancel Button (Very Long Press)"), tr("Action performed when the remapped \"Cancel\" button is pressed for more than 2.5 seconds."), "../../starpilot/assets/toggle_icons/icon_mute.png"}, {"VeryLongDistanceButtonControl", tr("Distance Button (Very Long Press)"), tr("Action performed when the \"Distance\" button is pressed for more than 2.5 seconds."), "../../starpilot/assets/toggle_icons/icon_mute.png"}, {"LKASButtonControl", tr("LKAS Button"), tr("Action performed when the \"LKAS\" button is pressed."), "../../starpilot/assets/toggle_icons/icon_mute.png"}, {"ModeButtonControl", tr("Mode Button"), tr("Action performed when the \"Mode\" button is pressed."), "../../starpilot/assets/toggle_icons/icon_mute.png"}, @@ -99,12 +89,6 @@ StarPilotWheelPanel::StarPilotWheelPanel(StarPilotSettingsWindow *parent, bool f for (const auto &[param, title, desc, icon] : wheelToggles) { ButtonControl *wheelToggle = new ButtonControl(title, tr("SELECT"), desc); QObject::connect(wheelToggle, &ButtonControl::clicked, [key = param, parent, wheelToggle, this]() { - if (key == "LKASButtonControl" && lockLkasButtonIfNeeded(params)) { - wheelToggle->setValue(tr("No Action")); - wheelToggle->setEnabled(false); - return; - } - QMap functionsMap = getWheelFunctionsMap(); if (parent->hasOpenpilotLongitudinal) { const QMap longitudinalFunctionsMap = getLongitudinalWheelFunctionsMap(); @@ -120,13 +104,7 @@ StarPilotWheelPanel::StarPilotWheelPanel(StarPilotSettingsWindow *parent, bool f updateStarPilotToggles(); } }); - - if (param == "LKASButtonControl" && lockLkasButtonIfNeeded(params)) { - wheelToggle->setValue(tr("No Action")); - wheelToggle->setEnabled(false); - } else { - wheelToggle->setValue(getWheelFunctionLabel(params, param)); - } + wheelToggle->setValue(getWheelFunctionLabel(params, param)); toggles[param] = wheelToggle; @@ -158,6 +136,15 @@ void StarPilotWheelPanel::updateToggles() { setVisible &= !parent->lkasAllowedForAOL || !(params.getBool("AlwaysOnLateral") && params.getBool("AlwaysOnLateralLKAS")); } + if (!showAllToggles && ( + key == "CancelButtonControl" || + key == "LongCancelButtonControl" || + key == "VeryLongCancelButtonControl")) { + setVisible &= parent->isBolt; + setVisible &= parent->hasPedal; + setVisible &= params.getBool("RemapCancelToDistance"); + } + if (!showAllToggles && key == "NostalgiaMode") { setVisible &= parent->isHKGCanFd; setVisible &= parent->hasOpenpilotLongitudinal; @@ -178,13 +165,7 @@ void StarPilotWheelPanel::updateToggles() { } if (ButtonControl *wheelToggle = qobject_cast(toggle)) { - if (key == "LKASButtonControl") { - const bool lkasLocked = lockLkasButtonIfNeeded(params); - wheelToggle->setEnabled(!lkasLocked); - wheelToggle->setValue(lkasLocked ? tr("No Action") : getWheelFunctionLabel(params, key)); - } else { - wheelToggle->setValue(getWheelFunctionLabel(params, key)); - } + wheelToggle->setValue(getWheelFunctionLabel(params, key)); } toggle->setVisible(setVisible); diff --git a/tools/StarPilot/feasibleparams.txt b/tools/StarPilot/feasibleparams.txt index db13a23a43..1ef8f11d76 100644 --- a/tools/StarPilot/feasibleparams.txt +++ b/tools/StarPilot/feasibleparams.txt @@ -72,6 +72,7 @@ CustomPersonalities CustomSignals CustomSounds CustomUI +CancelButtonControl DebugMode DecelerationProfile DeveloperMetrics @@ -154,6 +155,7 @@ LiveParameters LiveTorqueParameters LockDoors LockDoorsTimer +LongCancelButtonControl LongDistanceButtonControl LongPitch LongitudinalActuatorDelay @@ -333,6 +335,7 @@ VEgoStarting VEgoStartingStock VEgoStopping VEgoStoppingStock +VeryLongCancelButtonControl VeryLongDistanceButtonControl VoltSNG WarningImmediateVolume