diff --git a/common/params_keys.h b/common/params_keys.h index 313cbca7b..2c4df6140 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -160,6 +160,7 @@ inline static std::unordered_map keys = { {"AggressiveJerkSpeed", {PERSISTENT, FLOAT, "50.0", "50.0", 3}}, {"AggressiveJerkSpeedDecrease", {PERSISTENT, FLOAT, "50.0", "50.0", 3}}, {"AlertVolumeControl", {PERSISTENT, BOOL, "0", "0", 2}}, + {"AllowImpossibleAcceleration", {PERSISTENT, BOOL, "0", "0", 3}}, {"AlwaysOnLateral", {PERSISTENT, BOOL, "1", "0", 0}}, {"AlwaysOnLateralLKAS", {PERSISTENT, BOOL, "1", "0", 2}}, {"ApiCache_DriveStats", {PERSISTENT, JSON, "{}", "{}"}}, diff --git a/opendbc_repo/opendbc/car/car.capnp b/opendbc_repo/opendbc/car/car.capnp index 121c3c0b3..e859aa006 100644 --- a/opendbc_repo/opendbc/car/car.capnp +++ b/opendbc_repo/opendbc/car/car.capnp @@ -285,6 +285,8 @@ struct CarState { setCruise @9; resumeCruise @10; gapAdjustCruise @11; + accelHardCruise @12; + decelHardCruise @13; } } diff --git a/opendbc_repo/opendbc/car/gm/carstate.py b/opendbc_repo/opendbc/car/gm/carstate.py index 86db1bb9a..3bbbe6b14 100644 --- a/opendbc_repo/opendbc/car/gm/carstate.py +++ b/opendbc_repo/opendbc/car/gm/carstate.py @@ -33,6 +33,8 @@ AUTO_HOLD_REGEN_RELEASE_COOLDOWN_S = 1.0 BUTTONS_DICT = {CruiseButtons.RES_ACCEL: ButtonType.accelCruise, CruiseButtons.DECEL_SET: ButtonType.decelCruise, CruiseButtons.MAIN: ButtonType.mainCruise, CruiseButtons.CANCEL: ButtonType.cancel} +HARD_BUTTONS_DICT = {CruiseButtons.RES_ACCEL: ButtonType.accelHardCruise, CruiseButtons.DECEL_SET: ButtonType.decelHardCruise} +NORMAL_CRUISE_BUTTONS = (CruiseButtons.RES_ACCEL, CruiseButtons.DECEL_SET) GearShifter = structs.CarState.GearShifter BOLT_GEN1_CANCEL_PERSONALITY_CARS = { @@ -64,6 +66,8 @@ class CarState(CarStateBase): self.prev_distance_button = 0 self.distance_button = 0 + self.hard_cruise_buttons = CruiseButtons.INIT + self.force_reset_cruise_buttons = False self.single_pedal_mode = False self.auto_hold_armed = False @@ -92,8 +96,8 @@ class CarState(CarStateBase): if not self.CP.pcmCruise: for b in buttonEvents: # The ECM allows enabling on falling edge of set, but only rising edge of resume - if (b.type == ButtonType.accelCruise and b.pressed) or \ - (b.type == ButtonType.decelCruise and not b.pressed): + if (b.type in (ButtonType.accelCruise, ButtonType.accelHardCruise) and b.pressed) or \ + (b.type in (ButtonType.decelCruise, ButtonType.decelHardCruise) and not b.pressed): return True return False @@ -119,9 +123,11 @@ class CarState(CarStateBase): sdgm_non_volt = self.CP.carFingerprint in SDGM_CAR and self.CP.carFingerprint not in kaofui_state_cars prev_cruise_buttons = self.cruise_buttons + prev_hard_cruise_buttons = self.hard_cruise_buttons prev_distance_button = self.distance_button if not sdgm_non_volt: self.cruise_buttons = pt_cp.vl["ASCMSteeringButton"]["ACCButtons"] + self.hard_cruise_buttons = pt_cp.vl["ASCMSteeringButton"]["ACCButtonsHard"] self.distance_button = pt_cp.vl["ASCMSteeringButton"]["DistanceButton"] self.buttons_counter = pt_cp.vl["ASCMSteeringButton"]["RollingCounter"] self.steering_button_checksum = pt_cp.vl["ASCMSteeringButton"]["SteeringButtonChecksum"] @@ -131,9 +137,20 @@ class CarState(CarStateBase): self.steering_button_prefix = (int(acc_always_one) & 1) | ((int(acc_hidden_bit) & 1) << 6) else: self.cruise_buttons = cam_cp.vl["ASCMSteeringButton"]["ACCButtons"] + self.hard_cruise_buttons = cam_cp.vl["ASCMSteeringButton"]["ACCButtonsHard"] self.distance_button = cam_cp.vl["ASCMSteeringButton"]["DistanceButton"] self.buttons_counter = cam_cp.vl["ASCMSteeringButton"]["RollingCounter"] self.steering_button_ts_nanos = cam_cp.ts_nanos["ASCMSteeringButton"]["ACCButtons"] + + # A GM hard press keeps the normal cruise button signal active too. Suppress + # the normal button until the wheel reports a different normal state. + if self.hard_cruise_buttons != CruiseButtons.INIT and self.cruise_buttons in NORMAL_CRUISE_BUTTONS: + self.force_reset_cruise_buttons = True + if self.force_reset_cruise_buttons and self.cruise_buttons in NORMAL_CRUISE_BUTTONS: + self.cruise_buttons = CruiseButtons.UNPRESS + elif self.force_reset_cruise_buttons and self.cruise_buttons not in NORMAL_CRUISE_BUTTONS: + self.force_reset_cruise_buttons = False + self.pscm_status = copy.copy(pt_cp.vl["PSCMStatus"]) self.moving_backward = (pt_cp.vl["EBCMWheelSpdRear"]["RLWheelDir"] == 2) or (pt_cp.vl["EBCMWheelSpdRear"]["RRWheelDir"] == 2) @@ -178,7 +195,10 @@ class CarState(CarStateBase): ret.brakePressed = ret.brake >= VOLT_EBCM_BRAKE_PRESSED_THRESHOLD elif self.CP.carFingerprint in {CAR.CHEVROLET_MALIBU_CC} or (self.CP.carFingerprint == CAR.CHEVROLET_BLAZER and not no_accel_pos): ret.brakePressed = ret.brake >= 8 - elif (self.CP.flags & GMFlags.FORCE_BRAKE_C9.value) or ((self.CP.networkLocation == NetworkLocation.fwdCamera) and (self.CP.carFingerprint != CAR.CHEVROLET_BLAZER)): + elif (self.CP.flags & GMFlags.FORCE_BRAKE_C9.value) or ( + self.CP.networkLocation == NetworkLocation.fwdCamera and + self.CP.carFingerprint not in (SDGM_CAR | ASCM_INT | {CAR.CHEVROLET_BLAZER}) + ): ret.brakePressed = pt_cp.vl["ECMEngineStatus"]["BrakePressed"] != 0 else: # Some Volt 2016-17 have loose brake pedal push rod retainers which causes the ECM to believe @@ -374,13 +394,18 @@ class CarState(CarStateBase): lkas_events = [] if (suppress_malibu_side_buttons or suppress_bolt_cancel_lkas) else create_button_events( self.lkas_enabled, self.lkas_previously_enabled, {1: ButtonType.lkas} ) + hard_cruise_events = create_button_events( + self.hard_cruise_buttons, prev_hard_cruise_buttons, HARD_BUTTONS_DICT, unpressed_btn=CruiseButtons.INIT + ) # Don't add events if transitioning from INIT, unless it's to an actual button. - if self.cruise_buttons != CruiseButtons.UNPRESS or prev_cruise_buttons != CruiseButtons.INIT: + if (self.cruise_buttons != CruiseButtons.UNPRESS or prev_cruise_buttons != CruiseButtons.INIT or + self.hard_cruise_buttons != CruiseButtons.INIT or prev_hard_cruise_buttons != CruiseButtons.INIT): ret.buttonEvents = [ *cruise_events, *distance_events, *lkas_events, + *hard_cruise_events, ] if ret.vEgo < self.CP.minSteerSpeed: diff --git a/opendbc_repo/opendbc/dbc/generator/gm/gm_global_a_powertrain.dbc b/opendbc_repo/opendbc/dbc/generator/gm/gm_global_a_powertrain.dbc index 297c4686c..6e8eec072 100644 --- a/opendbc_repo/opendbc/dbc/generator/gm/gm_global_a_powertrain.dbc +++ b/opendbc_repo/opendbc/dbc/generator/gm/gm_global_a_powertrain.dbc @@ -146,6 +146,7 @@ BO_ 481 ASCMSteeringButton: 7 K124_ASCM SG_ LKAButton : 23|1@0+ (1,0) [0|0] "" NEO SG_ ACCAlwaysOne : 24|1@0+ (1,0) [0|1] "" XXX SG_ ACCButtons : 46|3@0+ (1,0) [0|0] "" NEO + SG_ ACCButtonsHard : 26|2@0+ (1,0) [0|3] "" XXX SG_ DriveModeButton : 39|1@0+ (1,0) [0|1] "" XXX SG_ RollingCounter : 33|2@0+ (1,0) [0|3] "" NEO SG_ SteeringButtonChecksum : 43|12@0+ (1,0) [0|255] "" NEO diff --git a/opendbc_repo/opendbc/dbc/gm_global_a_powertrain_generated.dbc b/opendbc_repo/opendbc/dbc/gm_global_a_powertrain_generated.dbc index 2207a4418..7aea83fed 100644 --- a/opendbc_repo/opendbc/dbc/gm_global_a_powertrain_generated.dbc +++ b/opendbc_repo/opendbc/dbc/gm_global_a_powertrain_generated.dbc @@ -166,6 +166,7 @@ BO_ 481 ASCMSteeringButton: 7 K124_ASCM SG_ LKAButton : 23|1@0+ (1,0) [0|0] "" NEO SG_ ACCAlwaysOne : 24|1@0+ (1,0) [0|1] "" XXX SG_ ACCButtons : 46|3@0+ (1,0) [0|0] "" NEO + SG_ ACCButtonsHard : 26|2@0+ (1,0) [0|3] "" XXX SG_ DriveModeButton : 39|1@0+ (1,0) [0|1] "" XXX SG_ RollingCounter : 33|2@0+ (1,0) [0|3] "" NEO SG_ SteeringButtonChecksum : 43|12@0+ (1,0) [0|255] "" NEO diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index d3530e193..00424615e 100644 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -283,9 +283,9 @@ class Car: CS.vCruise = float(self.v_cruise_helper.v_cruise_kph) CS.vCruiseCluster = float(self.v_cruise_helper.v_cruise_cluster_kph) - if any(be.type in (ButtonType.accelCruise, ButtonType.resumeCruise) for be in filtered_CS.buttonEvents): + if any(be.type in (ButtonType.accelCruise, ButtonType.accelHardCruise, ButtonType.resumeCruise) for be in filtered_CS.buttonEvents): self.resume_prev_button = True - elif any(be.type in (ButtonType.decelCruise, ButtonType.setCruise) for be in filtered_CS.buttonEvents): + elif any(be.type in (ButtonType.decelCruise, ButtonType.decelHardCruise, ButtonType.setCruise) for be in filtered_CS.buttonEvents): self.resume_prev_button = False FPCS = self.starpilot_card.update(CS, FPCS, self.sm, self.starpilot_toggles) diff --git a/selfdrive/car/cruise.py b/selfdrive/car/cruise.py index 1102d613e..7e81302e3 100644 --- a/selfdrive/car/cruise.py +++ b/selfdrive/car/cruise.py @@ -24,11 +24,17 @@ CRUISE_LONG_PRESS = 50 CRUISE_NEAREST_FUNC = { ButtonType.accelCruise: math.ceil, ButtonType.decelCruise: math.floor, + ButtonType.accelHardCruise: math.ceil, + ButtonType.decelHardCruise: math.floor, } CRUISE_INTERVAL_SIGN = { ButtonType.accelCruise: +1, ButtonType.decelCruise: -1, + ButtonType.accelHardCruise: +1, + ButtonType.decelHardCruise: -1, } +HARD_CRUISE_BUTTONS = (ButtonType.accelHardCruise, ButtonType.decelHardCruise) +ACCEL_CRUISE_BUTTONS = (ButtonType.accelCruise, ButtonType.accelHardCruise) class VCruiseHelper: @@ -37,7 +43,12 @@ class VCruiseHelper: self.v_cruise_kph = V_CRUISE_UNSET self.v_cruise_cluster_kph = V_CRUISE_UNSET self.v_cruise_kph_last = 0 - self.button_timers = {ButtonType.decelCruise: 0, ButtonType.accelCruise: 0} + self.button_timers = { + ButtonType.decelCruise: 0, + ButtonType.accelCruise: 0, + ButtonType.decelHardCruise: 0, + ButtonType.accelHardCruise: 0, + } self.button_change_states = {btn: {"standstill": False, "enabled": False} for btn in self.button_timers} self.gm_cc_only = self.CP.carFingerprint in CC_ONLY_CAR and self.CP.flags & GMFlags.CC_LONG.value @@ -121,7 +132,7 @@ class VCruiseHelper: # Don't adjust speed when pressing resume to exit standstill cruise_standstill = self.button_change_states[button_type]["standstill"] or CS.cruiseState.standstill - if button_type == ButtonType.accelCruise and cruise_standstill: + if button_type in ACCEL_CRUISE_BUTTONS and cruise_standstill: return # Don't adjust speed if we've enabled since the button was depressed (some ports enable on rising edge) @@ -129,7 +140,7 @@ class VCruiseHelper: return short_interval, long_interval = self._get_cruise_delta_intervals(starpilot_toggles) - v_cruise_delta_interval = long_interval if long_press else short_interval + v_cruise_delta_interval = long_interval if long_press or button_type in HARD_CRUISE_BUTTONS else short_interval v_cruise_delta = v_cruise_delta * v_cruise_delta_interval if v_cruise_delta_interval % 5 == 0 and self.v_cruise_kph % v_cruise_delta != 0: # partial interval self.v_cruise_kph = CRUISE_NEAREST_FUNC[button_type](self.v_cruise_kph / v_cruise_delta) * v_cruise_delta @@ -137,7 +148,7 @@ class VCruiseHelper: self.v_cruise_kph += v_cruise_delta * CRUISE_INTERVAL_SIGN[button_type] # If set is pressed while overriding, clip cruise speed to minimum of vEgo - if CS.gasPressed and button_type in (ButtonType.decelCruise, ButtonType.setCruise): + if CS.gasPressed and button_type in (ButtonType.decelCruise, ButtonType.decelHardCruise, ButtonType.setCruise): self.v_cruise_kph = max(self.v_cruise_kph, CS.vEgo * CV.MS_TO_KPH) self.v_cruise_kph = np.clip(round(self.v_cruise_kph, 1), V_CRUISE_MIN, V_CRUISE_MAX) @@ -161,7 +172,7 @@ class VCruiseHelper: return engage_floor_kph = max(V_CRUISE_MIN, 7.0 * CV.MPH_TO_KPH) - resume_pressed = any(b.type in (ButtonType.accelCruise, ButtonType.resumeCruise) for b in CS.buttonEvents) + resume_pressed = any(b.type in (ButtonType.accelCruise, ButtonType.accelHardCruise, ButtonType.resumeCruise) for b in CS.buttonEvents) remembered_resume = resume_prev_button and (self.gm_cc_only or self.redneck_non_pcm) if self.v_cruise_initialized and (resume_pressed or remembered_resume): diff --git a/selfdrive/car/tests/test_cruise_speed.py b/selfdrive/car/tests/test_cruise_speed.py index 2db064861..0b5a5e05a 100644 --- a/selfdrive/car/tests/test_cruise_speed.py +++ b/selfdrive/car/tests/test_cruise_speed.py @@ -1,5 +1,6 @@ import pytest import itertools +import math import numpy as np from parameterized import parameterized_class @@ -101,6 +102,62 @@ class TestVCruiseHelper: ) assert pressed == (self.v_cruise_helper.v_cruise_kph == self.v_cruise_helper.v_cruise_kph_last) + def test_hard_press_uses_long_press_interval(self): + self.enable(52 * CV.MPH_TO_MS, False) + initial_v_cruise_kph = self.v_cruise_helper.v_cruise_kph + + pressed_cs = car.CarState(cruiseState={"available": True}) + pressed_cs.buttonEvents = [ButtonEvent(type=ButtonType.accelHardCruise, pressed=True)] + self.v_cruise_helper.update_v_cruise( + pressed_cs, + enabled=True, + is_metric=False, + speed_limit_changed=False, + starpilot_toggles=self.starpilot_toggles, + ) + + released_cs = car.CarState(cruiseState={"available": True}) + released_cs.buttonEvents = [ButtonEvent(type=ButtonType.accelHardCruise, pressed=False)] + self.v_cruise_helper.update_v_cruise( + released_cs, + enabled=True, + is_metric=False, + speed_limit_changed=False, + starpilot_toggles=self.starpilot_toggles, + ) + + hard_interval = self.starpilot_toggles.cruise_increase_long * IMPERIAL_INCREMENT + expected_kph = math.ceil(initial_v_cruise_kph / hard_interval) * hard_interval + assert self.v_cruise_helper.v_cruise_kph == pytest.approx(expected_kph) + + def test_hard_decel_press_uses_long_press_interval(self): + self.enable(52 * CV.MPH_TO_MS, False) + initial_v_cruise_kph = self.v_cruise_helper.v_cruise_kph + + pressed_cs = car.CarState(cruiseState={"available": True}) + pressed_cs.buttonEvents = [ButtonEvent(type=ButtonType.decelHardCruise, pressed=True)] + self.v_cruise_helper.update_v_cruise( + pressed_cs, + enabled=True, + is_metric=False, + speed_limit_changed=False, + starpilot_toggles=self.starpilot_toggles, + ) + + released_cs = car.CarState(cruiseState={"available": True}) + released_cs.buttonEvents = [ButtonEvent(type=ButtonType.decelHardCruise, pressed=False)] + self.v_cruise_helper.update_v_cruise( + released_cs, + enabled=True, + is_metric=False, + speed_limit_changed=False, + starpilot_toggles=self.starpilot_toggles, + ) + + hard_interval = self.starpilot_toggles.cruise_increase_long * IMPERIAL_INCREMENT + expected_kph = math.floor(initial_v_cruise_kph / hard_interval) * hard_interval + assert self.v_cruise_helper.v_cruise_kph == pytest.approx(expected_kph) + def test_rising_edge_enable(self): """ Some car interfaces may enable on rising edge of a button, diff --git a/selfdrive/selfdrived/helpers.py b/selfdrive/selfdrived/helpers.py index f7468cbe4..7e67b705f 100644 --- a/selfdrive/selfdrived/helpers.py +++ b/selfdrive/selfdrived/helpers.py @@ -22,11 +22,15 @@ class ExcessiveActuationCheck: self._excessive_counter = 0 self._engaged_counter = 0 - def update(self, sm: messaging.SubMaster, CS: car.CarState, calibrated_pose: Pose) -> ExcessiveActuationType | None: + def update(self, sm: messaging.SubMaster, CS: car.CarState, calibrated_pose: Pose, + allow_impossible_acceleration: bool = False) -> ExcessiveActuationType | None: # CS.aEgo can be noisy to bumps in the road, transitioning from standstill, losing traction, etc. # longitudinal accel_calibrated = calibrated_pose.acceleration.x - excessive_long_actuation = sm['carControl'].longActive and (accel_calibrated > ACCEL_MAX * 2 or accel_calibrated < ACCEL_MIN * 2) + excessive_long_actuation = ( + not allow_impossible_acceleration and sm['carControl'].longActive and + (accel_calibrated > ACCEL_MAX * 2 or accel_calibrated < ACCEL_MIN * 2) + ) # lateral yaw_rate = calibrated_pose.angular_velocity.yaw diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index 510e671c4..59deca910 100644 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import json import os import time import threading @@ -71,6 +72,9 @@ class SelfdriveD: self.pose_calibrator = PoseCalibrator() self.calibrated_pose: Pose | None = None self.excessive_actuation_check = ExcessiveActuationCheck() + self.allow_impossible_acceleration = self.params.get_bool("AllowImpossibleAcceleration") + if self.allow_impossible_acceleration: + self.clear_longitudinal_excessive_actuation_alert() self.excessive_actuation = self.params.get("Offroad_ExcessiveActuation") is not None # Setup sockets @@ -172,6 +176,32 @@ class SelfdriveD: self.FPCP = messaging.log_from_bytes(self.params.get("StarPilotCarParams", block=True), custom.StarPilotCarParams) + def clear_longitudinal_excessive_actuation_alert(self): + alert = self.params.get("Offroad_ExcessiveActuation") + if not alert: + return + + if isinstance(alert, bytes): + try: + alert = json.loads(alert.decode("utf-8", errors="replace")) + except json.JSONDecodeError: + return + elif isinstance(alert, str): + try: + alert = json.loads(alert) + except json.JSONDecodeError: + return + + if not isinstance(alert, dict): + return + + extra = alert.get("extra", "") + if isinstance(extra, bytes): + extra = extra.decode("utf-8", errors="replace") + + if str(extra).strip().lower() == "longitudinal": + self.params.remove("Offroad_ExcessiveActuation") + def update_events(self, CS): """Compute onroadEvents from carState""" @@ -221,7 +251,7 @@ class SelfdriveD: return # Block resume if cruise never previously enabled - resume_pressed = any(be.type in (ButtonType.accelCruise, ButtonType.resumeCruise) for be in CS.buttonEvents) + resume_pressed = any(be.type in (ButtonType.accelCruise, ButtonType.accelHardCruise, ButtonType.resumeCruise) for be in CS.buttonEvents) if not self.CP.pcmCruise and CS.vCruise > 250 and resume_pressed: self.events.add(EventName.resumeBlocked) @@ -332,7 +362,9 @@ class SelfdriveD: self.calibrated_pose = self.pose_calibrator.build_calibrated_pose(device_pose) if self.calibrated_pose is not None: - excessive_actuation = self.excessive_actuation_check.update(self.sm, CS, self.calibrated_pose) + excessive_actuation = self.excessive_actuation_check.update( + self.sm, CS, self.calibrated_pose, self.allow_impossible_acceleration + ) if not self.excessive_actuation and excessive_actuation is not None: set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text=str(excessive_actuation)) self.excessive_actuation = True 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 eb329f0bb..9699cf86e 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 @@ -3219,6 +3219,13 @@ "description": "Only enable this if the wide camera is broken or for development!\n\nDisabling the wide camera may degrade driving performance and cause instability.\n\nRequires a reboot to take effect.", "data_type": "bool", "ui_type": "toggle" + }, + { + "key": "AllowImpossibleAcceleration", + "label": "Allow Impossible Acceleration", + "description": "WARNING: This suppresses openpilot's excessive longitudinal actuation diagnostic when measured acceleration looks impossible for the requested gas/brake command.\n\nLeave this OFF unless you are intentionally testing edge cases like shifting to neutral and understand that it can hide a real actuation problem.", + "data_type": "bool", + "ui_type": "toggle" } ] } diff --git a/starpilot/system/the_pond/the_pond.py b/starpilot/system/the_pond/the_pond.py index 4416a7bb9..60874fb3c 100644 --- a/starpilot/system/the_pond/the_pond.py +++ b/starpilot/system/the_pond/the_pond.py @@ -2255,6 +2255,30 @@ def _serialize_param_write_value(raw_value): return raw_value.decode("utf-8", errors="replace") return str(raw_value or "") +def _offroad_excessive_actuation_type(): + alert = _safe_params_get_live_raw("Offroad_ExcessiveActuation") + if not alert: + return "" + + if isinstance(alert, bytes): + try: + alert = json.loads(alert.decode("utf-8", errors="replace")) + except json.JSONDecodeError: + return "" + elif isinstance(alert, str): + try: + alert = json.loads(alert) + except json.JSONDecodeError: + return "" + + if not isinstance(alert, dict): + return "" + + extra = alert.get("extra", "") + if isinstance(extra, bytes): + extra = extra.decode("utf-8", errors="replace") + return str(extra).strip().lower() + def _apply_cellular_metered_setting(metered_enabled): """Apply GsmMetered changes to active NetworkManager GSM profiles.""" if not shutil.which("nmcli"): @@ -3742,6 +3766,18 @@ def setup(app): if key == "AutomaticUpdates" and params.get_bool("IsOnroad"): return jsonify({"error": "Cannot change Automatic Updates while driving."}), 403 + if key == "AllowImpossibleAcceleration": + enabled = str_val.strip() in ("1", "true", "True") + params.put_bool(key, enabled) + if enabled and _offroad_excessive_actuation_type() == "longitudinal": + params.remove("Offroad_ExcessiveActuation") + + update_starpilot_toggles() + return jsonify({ + "message": f"Parameter '{key}' updated successfully.", + "updated": {key: enabled}, + }), 200 + if key in {"EVTuning", "TruckTuning"}: enabled = str_val.strip() in ("1", "true", "True") params.put_bool(key, enabled)