From e4dfbd1a606874cfe21d496bd1175931305cf057 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Sun, 12 Jul 2026 21:47:48 -0500 Subject: [PATCH] Warbler --- common/params_keys.h | 5 +- .../opendbc/car/ford/carcontroller.py | 86 ++++++++++----- opendbc_repo/opendbc/car/ford/carstate.py | 11 ++ opendbc_repo/opendbc/car/ford/fingerprints.py | 19 ++++ opendbc_repo/opendbc/car/ford/fordcan.py | 73 +++++++++---- opendbc_repo/opendbc/car/ford/interface.py | 4 +- opendbc_repo/opendbc/car/ford/values.py | 13 +++ .../opendbc/car/hyundai/fingerprints.py | 11 ++ opendbc_repo/opendbc/car/hyundai/values.py | 8 ++ .../opendbc/car/torque_data/override.toml | 1 + .../opendbc/car/torque_data/substitute.toml | 1 + .../opendbc/car/toyota/carcontroller.py | 11 +- .../opendbc/car/toyota/tests/test_toyota.py | 16 +++ opendbc_repo/opendbc/car/toyota/toyotacan.py | 8 +- opendbc_repo/opendbc/safety/modes/ford.h | 12 +- .../safety/tests/safety_replay/helpers.py | 9 +- .../opendbc/safety/tests/test_ford.py | 24 +++- .../evaluate_reviewed_route_events.py | 65 ++++++++++- selfdrive/locationd/lagd.py | 3 +- .../ui/layouts/settings/starpilot/lateral.py | 11 +- selfdrive/ui/lib/starpilot_state.py | 5 +- starpilot/common/lateral_delay.py | 5 + starpilot/common/safe_mode.py | 2 + starpilot/common/starpilot_variables.py | 38 ++++++- .../common/tests/test_starpilot_variables.py | 33 ++++++ starpilot/controls/starpilot_card.py | 22 +++- .../controls/tests/test_starpilot_card.py | 103 ++++++++++++++++++ starpilot/system/speed_limit_vision.py | 13 ++- .../system/tests/test_speed_limit_vision.py | 9 +- .../the_galaxy/assets/components/home/home.js | 2 + .../components/tools/device_settings.js | 15 ++- .../tools/device_settings_layout.json | 18 ++- .../assets/components/tools/tuning.js | 2 + starpilot/system/the_galaxy/ftm_workspace.py | 12 +- .../the_galaxy/tests/test_ftm_workspace.py | 23 +++- .../tests/test_navigation_params.py | 53 ++++++--- starpilot/system/the_galaxy/the_galaxy.py | 57 ++++++++-- starpilot/system/the_galaxy/utilities.py | 99 +++++++++++++++++ system/manager/launch_param_migrations.py | 23 +++- system/manager/process_config.py | 4 +- .../test/test_launch_param_migrations.py | 35 +++++- system/manager/test/test_manager.py | 20 ++-- tools/StarPilot/feasibleparams.txt | 2 + 43 files changed, 844 insertions(+), 142 deletions(-) create mode 100644 starpilot/common/lateral_delay.py diff --git a/common/params_keys.h b/common/params_keys.h index 58a303dd2..29ad6fefe 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -126,7 +126,8 @@ inline static std::unordered_map keys = { {"SecOCKey", {PERSISTENT | DONT_LOG, STRING}}, {"ShowDebugInfo", {PERSISTENT, BOOL}}, {"ShowAllToggles", {PERSISTENT, BOOL, "0", "0", 3}}, - {"TryRaylibUI", {PERSISTENT, BOOL, "0"}}, + {"TryRaylibUI", {PERSISTENT, BOOL, "1"}}, + {"UseOldUI", {PERSISTENT, BOOL, "0"}}, {"UsePrebuilt", {PERSISTENT, BOOL, "1"}}, {"RouteCount", {PERSISTENT, INT, "0"}}, {"SnoozeUpdate", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, @@ -581,6 +582,7 @@ inline static std::unordered_map keys = { {"StartupMessageTop", {PERSISTENT, STRING, "Be ready to take over at any time", "Be ready to take over at any time", 0}}, {"StaticPedalsOnUI", {PERSISTENT, BOOL, "0", "0", 1}}, {"SteerDelay", {PERSISTENT, FLOAT, "0.0", "0.0", 3}}, + {"SteerDelayModeMigrated", {PERSISTENT, BOOL}}, {"SteerDelayStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}}, {"SteerFriction", {PERSISTENT, FLOAT, "0.0", "0.0", 3}}, {"SteerFrictionStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}}, @@ -632,6 +634,7 @@ inline static std::unordered_map keys = { {"UpdateTinygrad", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}}, {"UpdateWheelImage", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}}, {"UseActiveTheme", {CLEAR_ON_MANAGER_START, BOOL, "0", "0"}}, + {"UseAutoSteerDelay", {PERSISTENT, BOOL, "1", "1", 3}}, {"UseKonikServer", {PERSISTENT, BOOL, "0", "0", 2}}, {"UseSI", {PERSISTENT, BOOL, "1", "1", 3}}, {"UserFavorites", {PERSISTENT, STRING, "", "", 1}}, diff --git a/opendbc_repo/opendbc/car/ford/carcontroller.py b/opendbc_repo/opendbc/car/ford/carcontroller.py index 3e8eb5628..19bcb2015 100644 --- a/opendbc_repo/opendbc/car/ford/carcontroller.py +++ b/opendbc_repo/opendbc/car/ford/carcontroller.py @@ -16,6 +16,11 @@ AVERAGE_ROAD_ROLL = 0.06 # ~3.4 degrees, 6% superelevation. higher actual roll MAX_LATERAL_ACCEL = ISO_LATERAL_ACCEL - (ACCELERATION_DUE_TO_GRAVITY * AVERAGE_ROAD_ROLL) # ~2.4 m/s^2 +def apply_ford_angle(desired_angle_deg: float, current_angle_deg: float) -> float: + relative_angle = desired_angle_deg - current_angle_deg + return float(np.clip(relative_angle, -5.8, 5.8)) + + def anti_overshoot(apply_curvature, apply_curvature_last, v_ego): diff = 0.1 tau = 5 # 5s smooths over the overshoot @@ -65,6 +70,7 @@ class CarController(CarControllerBase): self.CAN = fordcan.CanBus(CP) self.apply_curvature_last = 0 + self.apply_angle_last = 0 self.anti_overshoot_curvature_last = 0 self.accel = 0.0 self.gas = 0.0 @@ -98,38 +104,62 @@ class CarController(CarControllerBase): can_sends.append(fordcan.create_button_msg(self.packer, self.CAN.camera, CS.buttons_stock_values, tja_toggle=True)) ### lateral control ### - # send steer msg at 20Hz - if (self.frame % CarControllerParams.STEER_STEP) == 0: - # Bronco and some other cars consistently overshoot curv requests - # Apply some deadzone + smoothing convergence to avoid oscillations - if self.CP.carFingerprint in (CAR.FORD_BRONCO_SPORT_MK1, CAR.FORD_F_150_MK14): - self.anti_overshoot_curvature_last = anti_overshoot(actuators.curvature, self.anti_overshoot_curvature_last, CS.out.vEgoRaw) - apply_curvature = self.anti_overshoot_curvature_last + if self.CP.flags & FordFlags.LKA_STEERING: + lka_active = CC.latActive and CS.lkas_available + if lka_active: + self.apply_angle_last = apply_ford_angle(actuators.steeringAngleDeg, CS.out.steeringAngleDeg) + current_curvature = -CS.out.yawRate / max(CS.out.vEgoRaw, 0.1) + self.apply_curvature_last = apply_ford_curvature_limits(actuators.curvature, self.apply_curvature_last, current_curvature, + CS.out.vEgoRaw, 0., True, self.CP) else: - apply_curvature = actuators.curvature + self.apply_angle_last = 0. + self.apply_curvature_last = 0. - # apply rate limits, curvature error limit, and clip to signal range - current_curvature = -CS.out.yawRate / max(CS.out.vEgoRaw, 0.1) + # Keep the stock LMC heartbeat present while steering through Lane_Assist_Data1. + if (self.frame % CarControllerParams.STEER_STEP) == 0: + can_sends.append(fordcan.create_lat_ctl_msg(self.packer, self.CAN, False, 0., 0., 0., 0., + stock_lmc=CS.lateral_motion_control)) - self.apply_curvature_last = apply_ford_curvature_limits(apply_curvature, self.apply_curvature_last, current_curvature, - CS.out.vEgoRaw, 0., CC.latActive, self.CP) + if (self.frame % CarControllerParams.LKA_STEP) == 0: + direction = 0 + if lka_active: + direction = 2 if CS.out.steeringAngleDeg > 0 else 4 + ramp_type = 1 if abs(self.apply_angle_last) >= 5 else 0 + can_sends.append(fordcan.create_lka_msg(self.packer, self.CAN, active=lka_active, apply_angle=self.apply_angle_last, + direction=direction, ramp_type=ramp_type, curvature=-self.apply_curvature_last)) + else: + # send steer msg at 20Hz + if (self.frame % CarControllerParams.STEER_STEP) == 0: + # Bronco and some other cars consistently overshoot curv requests + # Apply some deadzone + smoothing convergence to avoid oscillations + if self.CP.carFingerprint in (CAR.FORD_BRONCO_SPORT_MK1, CAR.FORD_F_150_MK14): + self.anti_overshoot_curvature_last = anti_overshoot(actuators.curvature, self.anti_overshoot_curvature_last, CS.out.vEgoRaw) + apply_curvature = self.anti_overshoot_curvature_last + else: + apply_curvature = actuators.curvature - if self.CP.flags & FordFlags.CANFD: - # TODO: extended mode - # Ford uses four individual signals to dictate how to drive to the car. Curvature alone (limited to 0.02m/s^2) - # can actuate the steering for a large portion of any lateral movements. However, in order to get further control on - # steer actuation, the other three signals are necessary. Ford controls vehicles differently than most other makes. - # A detailed explanation on ford control can be found here: - # https://www.f150gen14.com/forum/threads/introducing-bluepilot-a-ford-specific-fork-for-comma3x-openpilot.24241/#post-457706 - mode = 1 if CC.latActive else 0 - counter = (self.frame // CarControllerParams.STEER_STEP) % 0x10 - can_sends.append(fordcan.create_lat_ctl2_msg(self.packer, self.CAN, mode, 0., 0., -self.apply_curvature_last, 0., counter)) - else: - can_sends.append(fordcan.create_lat_ctl_msg(self.packer, self.CAN, CC.latActive, 0., 0., -self.apply_curvature_last, 0.)) + # apply rate limits, curvature error limit, and clip to signal range + current_curvature = -CS.out.yawRate / max(CS.out.vEgoRaw, 0.1) - # send lka msg at 33Hz - if (self.frame % CarControllerParams.LKA_STEP) == 0: - can_sends.append(fordcan.create_lka_msg(self.packer, self.CAN)) + self.apply_curvature_last = apply_ford_curvature_limits(apply_curvature, self.apply_curvature_last, current_curvature, + CS.out.vEgoRaw, 0., CC.latActive, self.CP) + + if self.CP.flags & FordFlags.CANFD: + # TODO: extended mode + # Ford uses four individual signals to dictate how to drive to the car. Curvature alone (limited to 0.02m/s^2) + # can actuate the steering for a large portion of any lateral movements. However, in order to get further control on + # steer actuation, the other three signals are necessary. Ford controls vehicles differently than most other makes. + # A detailed explanation on ford control can be found here: + # https://www.f150gen14.com/forum/threads/introducing-bluepilot-a-ford-specific-fork-for-comma3x-openpilot.24241/#post-457706 + mode = 1 if CC.latActive else 0 + counter = (self.frame // CarControllerParams.STEER_STEP) % 0x10 + can_sends.append(fordcan.create_lat_ctl2_msg(self.packer, self.CAN, mode, 0., 0., -self.apply_curvature_last, 0., counter)) + else: + can_sends.append(fordcan.create_lat_ctl_msg(self.packer, self.CAN, CC.latActive, 0., 0., -self.apply_curvature_last, 0.)) + + # send lka msg at 33Hz + if (self.frame % CarControllerParams.LKA_STEP) == 0: + can_sends.append(fordcan.create_lka_msg(self.packer, self.CAN)) ### longitudinal control ### # send acc msg at 50Hz @@ -195,6 +225,8 @@ class CarController(CarControllerBase): self.lead_distance_bars_last = hud_control.leadDistanceBars new_actuators = actuators.as_builder() + if self.CP.flags & FordFlags.LKA_STEERING: + new_actuators.steeringAngleDeg = self.apply_angle_last + CS.out.steeringAngleDeg new_actuators.curvature = self.apply_curvature_last new_actuators.accel = self.accel new_actuators.gas = self.gas diff --git a/opendbc_repo/opendbc/car/ford/carstate.py b/opendbc_repo/opendbc/car/ford/carstate.py index f5d94011c..f35a3b491 100644 --- a/opendbc_repo/opendbc/car/ford/carstate.py +++ b/opendbc_repo/opendbc/car/ford/carstate.py @@ -20,6 +20,8 @@ class CarState(CarStateBase): self.distance_button = 0 self.lc_button = 0 + self.lkas_available = False + self.lateral_motion_control = None def update(self, can_parsers, starpilot_toggles) -> structs.CarState: cp = can_parsers[Bus.pt] @@ -108,6 +110,15 @@ class CarState(CarStateBase): # Stock values from IPMA so that we can retain some stock functionality self.acc_tja_status_stock_values = cp_cam.vl["ACCDATA_3"] self.lkas_status_stock_values = cp_cam.vl["IPMA_Data"] + if self.CP.flags & FordFlags.LKA_STEERING: + try: + self.lkas_available = cp.vl["Lane_Assist_Data3_FD1"]["LaActAvail_D_Actl"] == 3 + except KeyError: + self.lkas_available = False + try: + self.lateral_motion_control = cp_cam.vl["LateralMotionControl"] + except KeyError: + self.lateral_motion_control = None ret.buttonEvents = [ *create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise}), diff --git a/opendbc_repo/opendbc/car/ford/fingerprints.py b/opendbc_repo/opendbc/car/ford/fingerprints.py index 6b3e33701..a48e70526 100644 --- a/opendbc_repo/opendbc/car/ford/fingerprints.py +++ b/opendbc_repo/opendbc/car/ford/fingerprints.py @@ -106,10 +106,12 @@ FW_VERSIONS = { }, CAR.FORD_F_150_MK14: { (Ecu.eps, 0x730, None): [ + b'ML3V-14D003-BA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'ML3V-14D003-BC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'ML3V-14D003-BD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.abs, 0x760, None): [ + b'ML34-2D053-AJ\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'NL34-2D053-CA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PL34-2D053-CA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PL34-2D053-CC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', @@ -117,6 +119,7 @@ FW_VERSIONS = { b'PL3V-2D053-BB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.fwdRadar, 0x764, None): [ + b'ML3T-14D049-AH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'ML3T-14D049-AK\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'ML3T-14D049-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], @@ -124,6 +127,7 @@ FW_VERSIONS = { b'ML3T-14H102-ABR\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'ML3T-14H102-ABS\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'ML3T-14H102-ABT\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'ML3T-14H102-ACA\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PJ6T-14H102-ABJ\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PJ6T-14H102-ABS\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'RJ6T-14H102-ACJ\x00\x00\x00\x00\x00\x00\x00\x00\x00', @@ -211,6 +215,7 @@ FW_VERSIONS = { b'PB3C-2D053-ZD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PB3C-2D053-ZG\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PB3C-2D053-ZJ\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'RB3C-2D053-AK\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.fwdRadar, 0x764, None): [ b'ML3T-14D049-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', @@ -220,4 +225,18 @@ FW_VERSIONS = { b'RJ6T-14H102-BBB\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], }, + CAR.FORD_TRANSIT_MK5: { + (Ecu.eps, 0x730, None): [ + b'KK21-14D003-AM\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.abs, 0x760, None): [ + b'NK41-2D053-DF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.fwdRadar, 0x764, None): [ + b'PC4T-14D049-AA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.fwdCamera, 0x706, None): [ + b'NK3T-14F397-AB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + }, } diff --git a/opendbc_repo/opendbc/car/ford/fordcan.py b/opendbc_repo/opendbc/car/ford/fordcan.py index 2ac68c442..721569b82 100644 --- a/opendbc_repo/opendbc/car/ford/fordcan.py +++ b/opendbc_repo/opendbc/car/ford/fordcan.py @@ -1,3 +1,5 @@ +import math + from opendbc.car import CanBusBase, structs HUDControl = structs.CarControl.HUDControl @@ -33,20 +35,40 @@ def calculate_lat_ctl2_checksum(mode: int, counter: int, dat: bytearray) -> int: return 0xFF - (checksum & 0xFF) -def create_lka_msg(packer, CAN: CanBus): +def create_lka_msg(packer, CAN: CanBus, active: bool = False, apply_angle: float = 0.0, + direction: int = 0, ramp_type: int = 0, curvature: float = 0.0): """ - Creates an empty CAN message for the Ford LKA Command. + Creates a CAN message for the Ford LKA Command. - This command can apply "Lane Keeping Aid" maneuvers, which are subject to the PSCM lockout. + On LKA-steering platforms, this command applies Lane Keeping Aid maneuvers through the PSCM. Frequency is 33Hz. """ - return packer.make_can_msg("Lane_Assist_Data1", CAN.main, {}) + if active: + mrad = math.radians(max(-5.8, min(5.8, apply_angle))) * 1000.0 + mrad = max(-102.4, min(102.3, mrad)) + curvature = max(-0.01023, min(0.01023, curvature)) + else: + mrad = 0.0 + direction = 0 + ramp_type = 0 + curvature = 0.0 + + values = { + "LkaDrvOvrrd_D_Rq": 0, + "LkaActvStats_D2_Req": direction if active else 0, + "LaRefAng_No_Req": mrad, + "LaRampType_B_Req": ramp_type, + "LaCurvature_No_Calc": curvature, + "LdwActvStats_D_Req": 0, + "LdwActvIntns_D_Req": 3, + } + return packer.make_can_msg("Lane_Assist_Data1", CAN.main, values) def create_lat_ctl_msg(packer, CAN: CanBus, lat_active: bool, path_offset: float, path_angle: float, curvature: float, - curvature_rate: float): + curvature_rate: float, stock_lmc=None): """ Creates a CAN message for the Ford TJA/LCA Command. @@ -68,20 +90,33 @@ def create_lat_ctl_msg(packer, CAN: CanBus, lat_active: bool, path_offset: float Frequency is 20Hz. """ - values = { - "LatCtlRng_L_Max": 0, # Unknown [0|126] meter - "HandsOffCnfm_B_Rq": 0, # Unknown: 0=Inactive, 1=Active [0|1] - "LatCtl_D_Rq": 1 if lat_active else 0, # Mode: 0=None, 1=ContinuousPathFollowing, 2=InterventionLeft, - # 3=InterventionRight, 4-7=NotUsed [0|7] - "LatCtlRampType_D_Rq": 0, # Ramp speed: 0=Slow, 1=Medium, 2=Fast, 3=Immediate [0|3] - # Makes no difference with curvature control - "LatCtlPrecision_D_Rq": 1, # Precision: 0=Comfortable, 1=Precise, 2/3=NotUsed [0|3] - # The stock system always uses comfortable - "LatCtlPathOffst_L_Actl": path_offset, # Path offset [-5.12|5.11] meter - "LatCtlPath_An_Actl": path_angle, # Path angle [-0.5|0.5235] radians - "LatCtlCurv_NoRate_Actl": curvature_rate, # Curvature rate [-0.001024|0.00102375] 1/meter^2 - "LatCtlCurv_No_Actl": curvature, # Curvature [-0.02|0.02094] 1/meter - } + if stock_lmc is not None: + values = { + "LatCtlRng_L_Max": stock_lmc["LatCtlRng_L_Max"], + "HandsOffCnfm_B_Rq": stock_lmc["HandsOffCnfm_B_Rq"], + "LatCtl_D_Rq": 0, + "LatCtlRampType_D_Rq": stock_lmc["LatCtlRampType_D_Rq"], + "LatCtlPrecision_D_Rq": stock_lmc["LatCtlPrecision_D_Rq"], + "LatCtlPathOffst_L_Actl": stock_lmc["LatCtlPathOffst_L_Actl"], + "LatCtlPath_An_Actl": stock_lmc["LatCtlPath_An_Actl"], + "LatCtlCurv_NoRate_Actl": stock_lmc["LatCtlCurv_NoRate_Actl"], + "LatCtlCurv_No_Actl": stock_lmc["LatCtlCurv_No_Actl"], + } + else: + values = { + "LatCtlRng_L_Max": 0, # Unknown [0|126] meter + "HandsOffCnfm_B_Rq": 0, # Unknown: 0=Inactive, 1=Active [0|1] + "LatCtl_D_Rq": 1 if lat_active else 0, # Mode: 0=None, 1=ContinuousPathFollowing, 2=InterventionLeft, + # 3=InterventionRight, 4-7=NotUsed [0|7] + "LatCtlRampType_D_Rq": 0, # Ramp speed: 0=Slow, 1=Medium, 2=Fast, 3=Immediate [0|3] + # Makes no difference with curvature control + "LatCtlPrecision_D_Rq": 1, # Precision: 0=Comfortable, 1=Precise, 2/3=NotUsed [0|3] + # The stock system always uses comfortable + "LatCtlPathOffst_L_Actl": path_offset, # Path offset [-5.12|5.11] meter + "LatCtlPath_An_Actl": path_angle, # Path angle [-0.5|0.5235] radians + "LatCtlCurv_NoRate_Actl": curvature_rate, # Curvature rate [-0.001024|0.00102375] 1/meter^2 + "LatCtlCurv_No_Actl": curvature, # Curvature [-0.02|0.02094] 1/meter + } return packer.make_can_msg("LateralMotionControl", CAN.main, values) diff --git a/opendbc_repo/opendbc/car/ford/interface.py b/opendbc_repo/opendbc/car/ford/interface.py index 97685df58..17036e46f 100644 --- a/opendbc_repo/opendbc/car/ford/interface.py +++ b/opendbc_repo/opendbc/car/ford/interface.py @@ -31,7 +31,7 @@ class CarInterface(CarInterfaceBase): ret.radarUnavailable = Bus.radar not in DBC[candidate] ret.steerControlType = structs.CarParams.SteerControlType.angle - ret.steerActuatorDelay = 0.2 + ret.steerActuatorDelay = 0.05 if ret.flags & FordFlags.LKA_STEERING else 0.2 ret.steerLimitTimer = 1.0 ret.steerAtStandstill = True @@ -63,6 +63,8 @@ class CarInterface(CarInterfaceBase): if fingerprint[CAN.camera].get(0x3d6) != 8 or fingerprint[CAN.camera].get(0x186) != 8: carlog.error('dashcamOnly: SecOC is unsupported') ret.dashcamOnly = True + elif ret.flags & FordFlags.LKA_STEERING: + ret.safetyConfigs[-1].safetyParam |= FordSafetyFlags.LKA_STEERING.value else: # Lock out if the car does not have needed lateral and longitudinal control APIs. # Note that we also check CAN for adaptive cruise, but no known signal for LCA exists diff --git a/opendbc_repo/opendbc/car/ford/values.py b/opendbc_repo/opendbc/car/ford/values.py index 6b8c1b3ca..ef2b0e3ab 100644 --- a/opendbc_repo/opendbc/car/ford/values.py +++ b/opendbc_repo/opendbc/car/ford/values.py @@ -46,11 +46,13 @@ class CarControllerParams: class FordSafetyFlags(IntFlag): LONG_CONTROL = 1 CANFD = 2 + LKA_STEERING = 4 class FordFlags(IntFlag): # Static flags CANFD = 1 + LKA_STEERING = 2 class RADAR: @@ -111,6 +113,13 @@ class FordCANFDPlatformConfig(FordPlatformConfig): self.flags |= FordFlags.CANFD +@dataclass +class FordLKASteeringPlatformConfig(FordPlatformConfig): + def init(self): + super().init() + self.flags |= FordFlags.LKA_STEERING + + @dataclass class FordF150LightningPlatform(FordCANFDPlatformConfig): def init(self): @@ -178,6 +187,10 @@ class CAR(Platforms): [FordCarDocs("Ford Ranger 2024", "Adaptive Cruise Control with Lane Centering", setup_video="https://www.youtube.com/watch?v=2oJlXCKYOy0")], CarSpecs(mass=2000, wheelbase=3.27, steerRatio=17.0), ) + FORD_TRANSIT_MK5 = FordLKASteeringPlatformConfig( + [FordCarDocs("Ford Transit 2025", "Co-Pilot360 Assist+")], + CarSpecs(mass=2068, wheelbase=3.302, steerRatio=16.7), + ) # FW response contains a combined software and part number diff --git a/opendbc_repo/opendbc/car/hyundai/fingerprints.py b/opendbc_repo/opendbc/car/hyundai/fingerprints.py index 7ab86a9b9..6493dc2ed 100644 --- a/opendbc_repo/opendbc/car/hyundai/fingerprints.py +++ b/opendbc_repo/opendbc/car/hyundai/fingerprints.py @@ -1018,6 +1018,17 @@ FW_VERSIONS = { b'\xf1\x00CN7 MDPS C 1.00 1.04 56310BY050\x00 4CNHC104', ], }, + CAR.HYUNDAI_ELANTRA_HEV_2026: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00CN7HMFC AT USA LHD 1.00 1.05 99210-AA510 240509', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00CN7_ RDR ----- 1.00 1.01 99110-AA500 ', + ], + (Ecu.eps, 0x7d4, None): [ + b'\xf1\x00CN7 MDPS C 1.00 1.03 56300BY670\x00 4CSHC103', + ], + }, CAR.HYUNDAI_KONA_HEV: { (Ecu.abs, 0x7d1, None): [ b'\xf1\x00OS IEB \x01 104 \x11 58520-CM000', diff --git a/opendbc_repo/opendbc/car/hyundai/values.py b/opendbc_repo/opendbc/car/hyundai/values.py index 21d9815dc..81e8d8475 100644 --- a/opendbc_repo/opendbc/car/hyundai/values.py +++ b/opendbc_repo/opendbc/car/hyundai/values.py @@ -295,6 +295,14 @@ class CAR(Platforms): CarSpecs(mass=3017 * CV.LB_TO_KG, wheelbase=2.72, steerRatio=12.9, tireStiffnessFactor=0.65), flags=HyundaiFlags.CHECKSUM_CRC8 | HyundaiFlags.HYBRID, ) + # 2026 CN7 Hybrid Limited. Initial port based on route + # 24d8ddb7d33b028f/00000008--d1f2ac19cc; keep this separate from the + # 2021-23 platform until its changed CAN receive checks are validated. + HYUNDAI_ELANTRA_HEV_2026 = HyundaiPlatformConfig( + [HyundaiCarDocs("Hyundai Elantra Hybrid 2026", "Limited", car_parts=CarParts.common([CarHarness.hyundai_k]))], + HYUNDAI_ELANTRA_HEV_2021.specs, + flags=HyundaiFlags.CHECKSUM_CRC8 | HyundaiFlags.HYBRID, + ) HYUNDAI_GENESIS = HyundaiPlatformConfig( [ # TODO: check 2015 packages diff --git a/opendbc_repo/opendbc/car/torque_data/override.toml b/opendbc_repo/opendbc/car/torque_data/override.toml index 616dbbe14..1e1ef2999 100644 --- a/opendbc_repo/opendbc/car/torque_data/override.toml +++ b/opendbc_repo/opendbc/car/torque_data/override.toml @@ -36,6 +36,7 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"] "FORD_F_150_LIGHTNING_MK1" = [nan, 1.5, nan] "FORD_MUSTANG_MACH_E_MK1" = [nan, 1.5, nan] "FORD_RANGER_MK2" = [nan, 1.5, nan] +"FORD_TRANSIT_MK5" = [nan, 1.5, nan] ### # No steering wheel diff --git a/opendbc_repo/opendbc/car/torque_data/substitute.toml b/opendbc_repo/opendbc/car/torque_data/substitute.toml index 6f95b4cba..01d4b02d5 100644 --- a/opendbc_repo/opendbc/car/torque_data/substitute.toml +++ b/opendbc_repo/opendbc/car/torque_data/substitute.toml @@ -40,6 +40,7 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"] "HYUNDAI_ELANTRA" = "HYUNDAI_SONATA_LF" "HYUNDAI_ELANTRA_GT_I30" = "HYUNDAI_SONATA_LF" "HYUNDAI_ELANTRA_HEV_2021" = "HYUNDAI_SONATA" +"HYUNDAI_ELANTRA_HEV_2026" = "HYUNDAI_SONATA" "HYUNDAI_TUCSON" = "HYUNDAI_SANTA_FE" "HYUNDAI_SANTA_FE_2022" = "HYUNDAI_SANTA_FE_HEV_2022" "KIA_K5_HEV_2020" = "KIA_K5_2021" diff --git a/opendbc_repo/opendbc/car/toyota/carcontroller.py b/opendbc_repo/opendbc/car/toyota/carcontroller.py index 5cc8485a7..47c0583a3 100644 --- a/opendbc_repo/opendbc/car/toyota/carcontroller.py +++ b/opendbc_repo/opendbc/car/toyota/carcontroller.py @@ -487,8 +487,13 @@ 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)) + CS.acc_type, fcw_alert, self.distance_button, starpilot_toggles.reverse_cruise_increase, + allow_long_press)) 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, @@ -507,7 +512,9 @@ class CarController(CarControllerBase): if self.CP.carFingerprint in UNSUPPORTED_DSU_CAR: can_sends.append(toyotacan.create_acc_cancel_command(self.packer)) else: - 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 = 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)) # *** 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 bfb9be070..6ed814b71 100644 --- a/opendbc_repo/opendbc/car/toyota/tests/test_toyota.py +++ b/opendbc_repo/opendbc/car/toyota/tests/test_toyota.py @@ -357,6 +357,22 @@ 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): + 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( + packer, 0.0, False, True, False, False, 1, False, 0, False, + ) + parser.update([(1, [default_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, + ) + parser.update([(1, [suppressed_msg])]) + assert parser.vl["ACC_CONTROL"]["ALLOW_LONG_PRESS"] == 0 + def test_auto_brake_hold_sends_modified_pre_collision_after_timer(self): controller = self._make_controller() controller.packer = CANPacker(DBC[CAR.TOYOTA_CAMRY_TSS2][Bus.pt]) diff --git a/opendbc_repo/opendbc/car/toyota/toyotacan.py b/opendbc_repo/opendbc/car/toyota/toyotacan.py index 394b59851..54b677ac7 100644 --- a/opendbc_repo/opendbc/car/toyota/toyotacan.py +++ b/opendbc_repo/opendbc/car/toyota/toyotacan.py @@ -40,8 +40,12 @@ def create_lta_steer_command_2(packer, frame): return packer.make_can_msg("STEERING_LTA_2", 0, values) -def create_accel_command(packer, accel, pcm_cancel, permit_braking, standstill_req, lead, acc_type, fcw_alert, distance, reverse_cruise_active): +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): # 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 + values = { "ACCEL_CMD": accel, "ACC_TYPE": acc_type, @@ -50,7 +54,7 @@ def create_accel_command(packer, accel, pcm_cancel, permit_braking, standstill_r "PERMIT_BRAKING": permit_braking, "RELEASE_STANDSTILL": not standstill_req, "CANCEL_REQ": pcm_cancel, - "ALLOW_LONG_PRESS": 2 if reverse_cruise_active else 1, + "ALLOW_LONG_PRESS": allow_long_press, "ACC_CUT_IN": fcw_alert, # only shown when ACC enabled } return packer.make_can_msg("ACC_CONTROL", 0, values) diff --git a/opendbc_repo/opendbc/safety/modes/ford.h b/opendbc_repo/opendbc/safety/modes/ford.h index 6a2fdb20c..5f240aef0 100644 --- a/opendbc_repo/opendbc/safety/modes/ford.h +++ b/opendbc_repo/opendbc/safety/modes/ford.h @@ -86,6 +86,8 @@ static bool ford_get_quality_flag_valid(const CANPacket_t *msg) { #define FORD_CANFD_INACTIVE_CURVATURE_RATE 1024U +static bool ford_lka_steering = false; + // Curvature rate limits #define FORD_LIMITS(limit_lateral_acceleration) { \ .max_angle = 1000, /* 0.02 curvature */ \ @@ -227,12 +229,10 @@ static bool ford_tx_hook(const CANPacket_t *msg) { // Safety check for Lane_Assist_Data1 action if (msg->addr == FORD_Lane_Assist_Data1) { - // Do not allow steering using Lane_Assist_Data1 (Lane-Departure Aid). - // This message must be sent for Lane Centering to work, and can include - // values such as the steering angle or lane curvature for debugging, - // but the action (LkaActvStats_D2_Req) must be set to zero. unsigned int action = msg->data[0] >> 5; - if (action != 0U) { + bool valid_lka_action = action == 0U; + valid_lka_action |= ford_lka_steering && controls_allowed && ((action == 2U) || (action == 4U)); + if (!valid_lka_action) { tx = false; } } @@ -330,7 +330,9 @@ static safety_config ford_init(uint16_t param) { }; const uint16_t FORD_PARAM_CANFD = 2; + const uint16_t FORD_PARAM_LKA_STEERING = 4; const bool ford_canfd = GET_FLAG(param, FORD_PARAM_CANFD); + ford_lka_steering = GET_FLAG(param, FORD_PARAM_LKA_STEERING); bool ford_longitudinal = false; diff --git a/opendbc_repo/opendbc/safety/tests/safety_replay/helpers.py b/opendbc_repo/opendbc/safety/tests/safety_replay/helpers.py index 4f91a9bf6..17264af0f 100644 --- a/opendbc_repo/opendbc/safety/tests/safety_replay/helpers.py +++ b/opendbc_repo/opendbc/safety/tests/safety_replay/helpers.py @@ -31,7 +31,9 @@ def is_steering_msg(mode, param, addr): elif mode == CarParams.SafetyModel.subaru: ret = addr == 0x122 elif mode == CarParams.SafetyModel.ford: - ret = addr == 0x3d6 if param & FordSafetyFlags.CANFD else addr == 0x3d3 + ret = addr == (0x3ca if param & FordSafetyFlags.LKA_STEERING else + 0x3d6 if param & FordSafetyFlags.CANFD else + 0x3d3) elif mode == CarParams.SafetyModel.nissan: ret = addr == 0x169 elif mode == CarParams.SafetyModel.rivian: @@ -67,7 +69,10 @@ def get_steer_value(mode, param, msg): torque = ((msg.data[3] & 0x1F) << 8) | msg.data[2] torque = -to_signed(torque, 13) elif mode == CarParams.SafetyModel.ford: - if param & FordSafetyFlags.CANFD: + if param & FordSafetyFlags.LKA_STEERING: + action = msg.data[0] >> 5 + angle = 1 if action in (2, 4) else 0 + elif param & FordSafetyFlags.CANFD: angle = ((msg.data[2] << 3) | (msg.data[3] >> 5)) - 1000 else: angle = ((msg.data[0] << 3) | (msg.data[1] >> 5)) - 1000 diff --git a/opendbc_repo/opendbc/safety/tests/test_ford.py b/opendbc_repo/opendbc/safety/tests/test_ford.py index 292d3c188..2bfe71b6a 100755 --- a/opendbc_repo/opendbc/safety/tests/test_ford.py +++ b/opendbc_repo/opendbc/safety/tests/test_ford.py @@ -76,6 +76,7 @@ class TestFordSafetyBase(common.CarSafetyTest): STEER_MESSAGE = 0 # Curvature control limits + LKA_STEERING = False DEG_TO_CAN = 50000 # 1 / (2e-5) rad to can MAX_CURVATURE = 0.02 MAX_CURVATURE_ERROR = 0.002 @@ -354,12 +355,13 @@ class TestFordSafetyBase(common.CarSafetyTest): self._set_prev_desired_angle(sign * (curvature_offset + initial_curvature)) self.assertEqual(should_tx, self._tx(self._lat_ctl_msg(True, 0, 0, sign * (curvature_offset + desired_curvature), 0))) - def test_prevent_lkas_action(self): - self.safety.set_controls_allowed(1) - self.assertFalse(self._tx(self._lkas_command_msg(1))) - - self.safety.set_controls_allowed(0) - self.assertFalse(self._tx(self._lkas_command_msg(1))) + def test_lkas_action(self): + for controls_allowed in (0, 1): + self.safety.set_controls_allowed(controls_allowed) + for action in range(8): + should_tx = action == 0 + should_tx |= self.LKA_STEERING and controls_allowed and action in (2, 4) + self.assertEqual(should_tx, self._tx(self._lkas_command_msg(action))) def test_acc_buttons(self): for allowed in (0, 1): @@ -484,6 +486,16 @@ class TestFordLongitudinalSafety(TestFordLongitudinalSafetyBase): pass +class TestFordLKASteeringSafety(TestFordLongitudinalSafety): + LKA_STEERING = True + + def setUp(self): + self.packer = CANPackerSafety("ford_lincoln_base_pt") + self.safety = libsafety_py.libsafety + self.safety.set_safety_hooks(CarParams.SafetyModel.ford, FordSafetyFlags.LKA_STEERING) + self.safety.init_tests() + + class TestFordCANFDLongitudinalSafety(TestFordLongitudinalSafetyBase): STEER_MESSAGE = MSG_LateralMotionControl2 diff --git a/scripts/speed_limit_vision/evaluate_reviewed_route_events.py b/scripts/speed_limit_vision/evaluate_reviewed_route_events.py index 75203b248..54ae2091e 100644 --- a/scripts/speed_limit_vision/evaluate_reviewed_route_events.py +++ b/scripts/speed_limit_vision/evaluate_reviewed_route_events.py @@ -66,9 +66,28 @@ def parse_args() -> argparse.Namespace: action="store_true", help="Mark three agreeing high-confidence regulatory model crops as strong consensus.", ) + parser.add_argument( + "--strong-model-min-proposal-confidence", + type=float, + help="Override proposal confidence required for a crop to contribute to strong model consensus.", + ) + parser.add_argument( + "--strong-model-consensus-min-read-confidence", + type=float, + help="Override classifier confidence required for a crop to contribute to strong model consensus.", + ) + parser.add_argument( + "--strong-model-consensus-min-support", + type=int, + help="Override the number of agreeing classifier crops required for strong model consensus.", + ) parser.add_argument("--initial-speed-limit", type=int, default=0, help="Seed each replay window with a currently published speed limit.") parser.add_argument("--positive-only", action="store_true", help="Replay only reviewed speed signs, omitting ignored-crop windows.") + parser.add_argument("--negative-only", action="store_true", help="Replay only ignored not-speed-limit windows.") parser.add_argument("--route-file", type=Path, help="Only replay routes listed one per line in this file.") + parser.add_argument("--strong-detection-confidence", type=float, help="Override one-frame publication confidence.") + parser.add_argument("--consistent-detections", type=int, help="Override matching reads required for an initial publication.") + parser.add_argument("--change-consistent-detections", type=int, help="Override matching reads required to change a publication.") parser.add_argument("--max-cases", type=int, default=0, help="Optional evaluation cap after deduplication.") return parser.parse_args() @@ -111,7 +130,7 @@ def load_cases(queue_path: Path, labels_path: Path, dedupe_seconds: float) -> li return cases -def replay_video_cases(cases: list[ReviewedCase], args: argparse.Namespace) -> dict[str, tuple[list[int], list[int], int]]: +def replay_video_cases(cases: list[ReviewedCase], args: argparse.Namespace) -> dict[str, tuple[list[dict[str, str]], int]]: daemons = { case.record_key: RouteReplayDaemon( runtime_context=None, @@ -160,9 +179,7 @@ def replay_video_cases(cases: list[ReviewedCase], args: argparse.Namespace) -> d results = {} for case in cases: daemon = daemons[case.record_key] - candidates = [int(event["candidateSpeedLimitMph"]) for event in daemon.events if event["event"] == "candidate"] - publishes = [int(event["speedLimitMph"]) for event in daemon.events if event["event"] == "publish"] - results[case.record_key] = candidates, publishes, daemon.inference_frames + results[case.record_key] = daemon.events, daemon.inference_frames return results @@ -195,6 +212,20 @@ def main() -> int: slv.LOW_SPEED_CHANGE_ALLOW_STRONG_CONSENSUS = True if args.enable_strong_model_consensus: slv.DETECTOR_CLASSIFIER_STRONG_MODEL_CONSENSUS_ENABLED = True + if args.strong_model_min_proposal_confidence is not None: + slv.DETECTOR_CLASSIFIER_STRONG_MODEL_MIN_PROPOSAL_CONFIDENCE = args.strong_model_min_proposal_confidence + if args.strong_model_consensus_min_read_confidence is not None: + slv.DETECTOR_CLASSIFIER_STRONG_MODEL_CONSENSUS_MIN_READ_CONFIDENCE = args.strong_model_consensus_min_read_confidence + if args.strong_model_consensus_min_support is not None: + if args.strong_model_consensus_min_support < 1: + raise ValueError("--strong-model-consensus-min-support must be at least 1") + slv.DETECTOR_CLASSIFIER_STRONG_MODEL_CONSENSUS_MIN_SUPPORT = args.strong_model_consensus_min_support + if args.strong_detection_confidence is not None: + slv.STRONG_DETECTION_CONFIDENCE = args.strong_detection_confidence + if args.consistent_detections is not None: + slv.CONSISTENT_DETECTIONS = args.consistent_detections + if args.change_consistent_detections is not None: + slv.CHANGE_CONSISTENT_DETECTIONS = args.change_consistent_detections cases = load_cases(queue_path, labels_path, args.dedupe_seconds) if args.route_file: selected_routes = { @@ -203,13 +234,17 @@ def main() -> int: cases = [case for case in cases if case.route in selected_routes] if args.positive_only: cases = [case for case in cases if not case.negative] + if args.negative_only: + cases = [case for case in cases if case.negative] + if args.positive_only and args.negative_only: + raise ValueError("--positive-only and --negative-only are mutually exclusive") if args.max_cases > 0: cases = cases[:args.max_cases] output_rows: list[dict[str, object]] = [] positive_by_speed: dict[int, Counter[str]] = defaultdict(Counter) negative_counts: Counter[str] = Counter() - results: dict[str, tuple[list[int], list[int], int]] = {} + results: dict[str, tuple[list[dict[str, str]], int]] = {} cases_by_video: dict[Path, list[ReviewedCase]] = defaultdict(list) for case in cases: cases_by_video[case.source_video_path].append(case) @@ -219,9 +254,15 @@ def main() -> int: print(f"Replayed {index}/{len(cases_by_video)} video segments", flush=True) for case in cases: - candidates, publishes, inference_frames = results.get(case.record_key, ([], [], 0)) + events, inference_frames = results.get(case.record_key, ([], 0)) + candidate_events = [event for event in events if event["event"] == "candidate"] + publish_events = [event for event in events if event["event"] == "publish"] + candidates = [int(event["candidateSpeedLimitMph"]) for event in candidate_events] + publishes = [int(event["speedLimitMph"]) for event in publish_events] candidate_hit = case.expected_speed_limit_mph in candidates if not case.negative else False publish_hit = case.expected_speed_limit_mph in publishes if not case.negative else False + wrong_candidate_values = [] if case.negative else [value for value in candidates if value != case.expected_speed_limit_mph] + wrong_publish_values = [] if case.negative else [value for value in publishes if value != case.expected_speed_limit_mph] false_candidate = bool(candidates) if case.negative else False false_publish = bool(publishes) if case.negative else False if case.negative: @@ -231,6 +272,8 @@ def main() -> int: total=1, candidate_hit=int(candidate_hit), publish_hit=int(publish_hit), + wrong_candidate=int(bool(wrong_candidate_values)), + wrong_publish=int(bool(wrong_publish_values)), ) output_rows.append({ "record_key": case.record_key, @@ -241,8 +284,15 @@ def main() -> int: "negative": case.negative, "candidate_values": "|".join(str(value) for value in candidates), "publish_values": "|".join(str(value) for value in publishes), + "candidate_confidences": "|".join(event.get("candidateConfidence", "") for event in candidate_events), + "candidate_strong_consensus": "|".join(event.get("candidateStrongConsensus", "") for event in candidate_events), + "publish_confidences": "|".join(event.get("confidence", "") for event in publish_events), "candidate_hit": candidate_hit, "publish_hit": publish_hit, + "wrong_candidate_values": "|".join(str(value) for value in wrong_candidate_values), + "wrong_publish_values": "|".join(str(value) for value in wrong_publish_values), + "wrong_candidate": bool(wrong_candidate_values), + "wrong_publish": bool(wrong_publish_values), "false_candidate": false_candidate, "false_publish": false_publish, "inference_frames": inference_frames, @@ -274,6 +324,9 @@ def main() -> int: "low_speed_change_consistent_detections": slv.LOW_SPEED_CHANGE_CONSISTENT_DETECTIONS, "low_speed_change_allow_strong_consensus": slv.LOW_SPEED_CHANGE_ALLOW_STRONG_CONSENSUS, "strong_model_consensus_enabled": slv.DETECTOR_CLASSIFIER_STRONG_MODEL_CONSENSUS_ENABLED, + "strong_model_min_proposal_confidence": slv.DETECTOR_CLASSIFIER_STRONG_MODEL_MIN_PROPOSAL_CONFIDENCE, + "strong_model_consensus_min_read_confidence": slv.DETECTOR_CLASSIFIER_STRONG_MODEL_CONSENSUS_MIN_READ_CONFIDENCE, + "strong_model_consensus_min_support": slv.DETECTOR_CLASSIFIER_STRONG_MODEL_CONSENSUS_MIN_SUPPORT, "positive": dict(totals), "positive_by_speed": {str(speed): dict(counts) for speed, counts in sorted(positive_by_speed.items())}, "negative": dict(negative_counts), diff --git a/selfdrive/locationd/lagd.py b/selfdrive/locationd/lagd.py index 5338dc1a5..aad59d1ff 100755 --- a/selfdrive/locationd/lagd.py +++ b/selfdrive/locationd/lagd.py @@ -13,6 +13,7 @@ from openpilot.common.realtime import config_realtime_process from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose, fft_next_good_size, parabolic_peak_interp +from openpilot.starpilot.common.lateral_delay import full_lateral_delay from openpilot.starpilot.common.starpilot_variables import get_starpilot_toggles BLOCK_SIZE = 100 @@ -180,7 +181,7 @@ class LateralLagEstimator: self.window_sec = window_sec self.okay_window_sec = okay_window_sec self.min_recovery_buffer_sec = min_recovery_buffer_sec - self.initial_lag = CP.steerActuatorDelay + 0.2 + self.initial_lag = full_lateral_delay(CP.steerActuatorDelay) self.block_size = block_size self.block_count = block_count self.min_valid_block_count = min_valid_block_count diff --git a/selfdrive/ui/layouts/settings/starpilot/lateral.py b/selfdrive/ui/layouts/settings/starpilot/lateral.py index 0c26ef592..054b126bc 100644 --- a/selfdrive/ui/layouts/settings/starpilot/lateral.py +++ b/selfdrive/ui/layouts/settings/starpilot/lateral.py @@ -313,11 +313,20 @@ class StarPilotLateralLayout(_SettingsPage): disabled_label=tr_noop("Not Available"), visible=alt_on, ), + SettingRow( + "UseAutoSteerDelay", "toggle", tr_noop("Use Auto-Learned Delay"), + subtitle=tr_noop("Learn the full steering delay automatically. The manual value below is ignored while enabled."), + get_state=lambda: p.get_bool("UseAutoSteerDelay"), + set_state=lambda s: p.put_bool("UseAutoSteerDelay", s), + visible=lambda: alt_on() and cs.steerActuatorDelay != 0, + ), SettingRow( "SteerDelay", "value", tr_noop("Actuator Delay"), - subtitle=tr_noop("Time between steering command and vehicle response."), + subtitle=tr_noop("Exact full delay between steering command and vehicle response."), get_value=lambda: f"{p.get_float('SteerDelay'):.2f}s", on_click=lambda: self._show_slider("SteerDelay", 0.01, 1.0, step=0.01, unit="s", value_type="float"), + enabled=lambda: not p.get_bool("UseAutoSteerDelay"), + disabled_label=tr_noop("Disabled while auto-learned delay is enabled."), visible=lambda: alt_on() and cs.steerActuatorDelay != 0, ), SettingRow( diff --git a/selfdrive/ui/lib/starpilot_state.py b/selfdrive/ui/lib/starpilot_state.py index 28c7d8230..3d245b708 100644 --- a/selfdrive/ui/lib/starpilot_state.py +++ b/selfdrive/ui/lib/starpilot_state.py @@ -8,6 +8,7 @@ from openpilot.selfdrive.ui.ui_state import ui_state from cereal import car, log, custom, messaging from opendbc.car.gm.values import GMFlags from opendbc.car.hyundai.values import HyundaiFlags +from openpilot.starpilot.common.lateral_delay import full_lateral_delay @dataclass class StarPilotCarState: @@ -179,7 +180,9 @@ class StarPilotState: ) self.car_state.longitudinalActuatorDelay = float(self._safe_get(CP, "longitudinalActuatorDelay", self.car_state.longitudinalActuatorDelay)) self.car_state.startAccel = float(self._safe_get(CP, "startAccel", self.car_state.startAccel)) - self.car_state.steerActuatorDelay = float(self._safe_get(CP, "steerActuatorDelay", self.car_state.steerActuatorDelay)) + vehicle_steer_delay = self._safe_get(CP, "steerActuatorDelay", None) + if vehicle_steer_delay is not None: + self.car_state.steerActuatorDelay = full_lateral_delay(vehicle_steer_delay) self.car_state.steerRatio = float(self._safe_get(CP, "steerRatio", self.car_state.steerRatio)) self.car_state.stopAccel = float(self._safe_get(CP, "stopAccel", self.car_state.stopAccel)) self.car_state.stoppingDecelRate = float(self._safe_get(CP, "stoppingDecelRate", self.car_state.stoppingDecelRate)) diff --git a/starpilot/common/lateral_delay.py b/starpilot/common/lateral_delay.py new file mode 100644 index 000000000..76da2ec3d --- /dev/null +++ b/starpilot/common/lateral_delay.py @@ -0,0 +1,5 @@ +LATERAL_CONTROL_SOFTWARE_DELAY = 0.2 + + +def full_lateral_delay(vehicle_delay: float) -> float: + return float(vehicle_delay) + LATERAL_CONTROL_SOFTWARE_DELAY diff --git a/starpilot/common/safe_mode.py b/starpilot/common/safe_mode.py index 7e2c81567..9cbbb6df7 100644 --- a/starpilot/common/safe_mode.py +++ b/starpilot/common/safe_mode.py @@ -26,6 +26,7 @@ SAFE_MODE_MANAGED_KEYS = ( "AdvancedLateralTune", "ForceAutoTune", "ForceAutoTuneOff", + "UseAutoSteerDelay", "ForceTorqueController", "SteerDelay", "SteerFriction", @@ -196,6 +197,7 @@ SAFE_MODE_MANAGED_KEYS = ( SAFE_MODE_FIXED_VALUES = { "ExperimentalMode": False, "LongitudinalPersonality": int(log.LongitudinalPersonality.relaxed), + "UseAutoSteerDelay": True, } SAFE_MODE_STOCK_PARAM_MAP = { diff --git a/starpilot/common/starpilot_variables.py b/starpilot/common/starpilot_variables.py index e9b41fdc5..21bdf5ac2 100644 --- a/starpilot/common/starpilot_variables.py +++ b/starpilot/common/starpilot_variables.py @@ -27,6 +27,7 @@ from openpilot.common.params import Params from openpilot.selfdrive.controls.lib.latcontrol_torque import KP from openpilot.selfdrive.modeld.constants import ModelConstants from openpilot.starpilot.common.model_versions import is_tinygrad_model_version +from openpilot.starpilot.common.lateral_delay import full_lateral_delay from openpilot.starpilot.common.accel_profile import ( ACCELERATION_PROFILES, CUSTOM_ACCEL_PROFILE_PARAM_KEYS, @@ -93,6 +94,8 @@ PRIUS_CLUSTER_OFFSET_CARS = { str(TOYOTA_CAR.TOYOTA_PRIUS_TSS2), } +STEER_DELAY_MODE_MIGRATION_KEY = "SteerDelayModeMigrated" + RESOURCES_REPO = os.getenv("STARPILOT_RESOURCES_REPO", "firestar5683/StarPilot-Resources") ACTIVE_THEME_PATH = Path(BASEDIR) / "starpilot/assets/active_theme" @@ -555,6 +558,29 @@ class StarPilotVariables: self.params.put_float(stock_key, live_value) + def _migrate_steer_delay_mode(self, vehicle_delay: float) -> None: + if self.params_raw.get_bool(STEER_DELAY_MODE_MIGRATION_KEY): + return + + def parse_value(raw_value): + try: + return float(raw_value) + except (TypeError, ValueError): + return None + + current_delay = parse_value(self.params_raw.get("SteerDelay")) + previous_stock = parse_value(self.params_raw.get("SteerDelayStock")) + full_stock_delay = full_lateral_delay(vehicle_delay) + + use_auto_delay = current_delay is None or math.isclose(current_delay, 0.0, abs_tol=1e-6) + use_auto_delay |= current_delay is not None and math.isclose(current_delay, vehicle_delay, abs_tol=1e-6) + use_auto_delay |= current_delay is not None and previous_stock is not None and math.isclose(current_delay, previous_stock, abs_tol=1e-6) + + self.params.put_bool("UseAutoSteerDelay", use_auto_delay) + if use_auto_delay: + self.params.put_float("SteerDelay", full_stock_delay) + self.params.put_bool(STEER_DELAY_MODE_MIGRATION_KEY, True) + def update(self, holiday_theme="stock", started=False): toggle = self.starpilot_toggles toggle.tuning_level = self.params.get("TuningLevel") if self.params.get_bool("TuningLevelConfirmed") else TUNING_LEVELS["ADVANCED"] @@ -624,15 +650,16 @@ class StarPilotVariables: startAccel = CP.startAccel stopAccel = CP.stopAccel steerActuatorDelay = CP.steerActuatorDelay + fullSteerActuatorDelay = full_lateral_delay(steerActuatorDelay) steerKp = KP steerRatio = CP.steerRatio toggle.stoppingDecelRate = CP.stoppingDecelRate toggle.vEgoStarting = CP.vEgoStarting toggle.vEgoStopping = CP.vEgoStopping - # Keep stock tuning params synchronized for all device UIs (Qt + raylib). - # Historically this only ran in Qt settings, which left C4 defaults at 0. - self._sync_stock_param("SteerDelay", "SteerDelayStock", steerActuatorDelay) + # Keep stock tuning params synchronized for all device UIs. + self._migrate_steer_delay_mode(steerActuatorDelay) + self._sync_stock_param("SteerDelay", "SteerDelayStock", fullSteerActuatorDelay) self._sync_stock_param("SteerFriction", "SteerFrictionStock", friction) self._sync_stock_param("SteerKP", "SteerKPStock", steerKp) self._sync_stock_param("SteerLatAccel", "SteerLatAccelStock", latAccelFactor) @@ -687,8 +714,9 @@ class StarPilotVariables: toggle.ftm_active_overrides = json.loads(ftm_overrides_raw) if ftm_overrides_raw else {} except Exception: toggle.ftm_active_overrides = {} - toggle.steerActuatorDelay = self.get_value("SteerDelay", cast=float, condition=advanced_lateral_tuning, default=steerActuatorDelay, min=0.01, max=1.0) - toggle.use_custom_steerActuatorDelay = bool(round(toggle.steerActuatorDelay, 2) != round(steerActuatorDelay, 2)) + toggle.use_auto_steer_delay = self.get_value("UseAutoSteerDelay", condition=advanced_lateral_tuning, default=True) + toggle.steerActuatorDelay = self.get_value("SteerDelay", cast=float, condition=advanced_lateral_tuning, default=fullSteerActuatorDelay, min=0.01, max=1.0) + toggle.use_custom_steerActuatorDelay = advanced_lateral_tuning and not toggle.use_auto_steer_delay toggle.friction = self.get_value("SteerFriction", cast=float, condition=advanced_lateral_tuning, default=friction, min=0, max=1) toggle.use_custom_friction = bool(round(toggle.friction, 2) != round(friction, 2)) and is_torque_car and not toggle.force_auto_tune or toggle.force_auto_tune_off toggle.steerKp = [[0], [self.get_value("SteerKP", cast=float, condition=advanced_lateral_tuning and is_torque_car and not is_angle_car, default=steerKp, min=steerKp * 0.5, max=steerKp * 1.5)]] diff --git a/starpilot/common/tests/test_starpilot_variables.py b/starpilot/common/tests/test_starpilot_variables.py index ea1d3b7f1..da9059e75 100644 --- a/starpilot/common/tests/test_starpilot_variables.py +++ b/starpilot/common/tests/test_starpilot_variables.py @@ -44,6 +44,13 @@ class _FakeParams: def get_bool(self, key): return bool(self.bools.get(key, False)) + def get(self, key): + if key in self.floats: + return self.floats[key] + if key in self.ints: + return self.ints[key] + return self.bools.get(key) + def put_float(self, key, value): self.floats[key] = float(value) @@ -70,6 +77,32 @@ def test_sync_stock_param_does_not_stomp_existing_custom_value_when_stock_missin assert params.get_float("SteerDelayStock") == 0.10 +def test_steer_delay_mode_migration_converts_untouched_stock_value_to_full_auto_delay(): + params = _FakeParams({"SteerDelay": 0.11, "SteerDelayStock": 0.11}) + variables = object.__new__(spv.StarPilotVariables) + variables.params = params + variables.params_raw = params + + variables._migrate_steer_delay_mode(0.11) + + assert params.get_bool("UseAutoSteerDelay") is True + assert params.get_float("SteerDelay") == 0.31 + assert params.get_bool(spv.STEER_DELAY_MODE_MIGRATION_KEY) is True + + +def test_steer_delay_mode_migration_preserves_existing_manual_full_delay(): + params = _FakeParams({"SteerDelay": 0.35, "SteerDelayStock": 0.11}) + variables = object.__new__(spv.StarPilotVariables) + variables.params = params + variables.params_raw = params + + variables._migrate_steer_delay_mode(0.11) + + assert params.get_bool("UseAutoSteerDelay") is False + assert params.get_float("SteerDelay") == 0.35 + assert params.get_bool(spv.STEER_DELAY_MODE_MIGRATION_KEY) is True + + def test_cancel_button_migration_copies_distance_actions_once(): params = _FakeParams( ints={ diff --git a/starpilot/controls/starpilot_card.py b/starpilot/controls/starpilot_card.py index 0562ea450..f3f1182ff 100644 --- a/starpilot/controls/starpilot_card.py +++ b/starpilot/controls/starpilot_card.py @@ -40,6 +40,10 @@ class StarPilotCard: self.hyundai_preserve_aol_across_reverse = getattr(self.CP, "carFingerprint", None) == HYUNDAI_CAR.HYUNDAI_SONATA_HYBRID self.hyundai_aol_needs_engagement = self.CP.brand == "hyundai" and not (hyundai_flags & HyundaiFlags.CANFD) and not kia_forte_non_scc self.hyundai_aol_ready = False + # Nissan's angle-control EPS can fault if AOL starts while the wheel is at a + # large parking angle. Require one normal openpilot engagement before AOL. + self.nissan_aol_needs_engagement = self.CP.brand == "nissan" + self.nissan_aol_ready = False self.prev_active = False self.prev_cruise_enabled = False self.decel_pressed = False @@ -114,6 +118,7 @@ class StarPilotCard: def update(self, carState, starpilotCarState, sm, starpilot_toggles): self.switchback_mode_enabled = self.params_memory.get_bool("SwitchbackModeEnabled") button_event_types = [self._button_type_raw(be) for be in carState.buttonEvents] + button_managed_aol = starpilot_toggles.always_on_lateral_lkas or starpilot_toggles.main_cruise_aol_toggle if self.hyundai_aol_needs_engagement: if carState.gearShifter in NON_DRIVING_GEARS: @@ -124,6 +129,13 @@ class StarPilotCard: elif sm["selfdriveState"].active or carState.cruiseState.enabled: self.hyundai_aol_ready = True + if self.nissan_aol_needs_engagement: + if carState.gearShifter in NON_DRIVING_GEARS: + self.nissan_aol_ready = False + self.always_on_lateral_allowed = False + elif sm["selfdriveState"].active: + self.nissan_aol_ready = True + if self.CP.brand == "hyundai" or starpilot_toggles.lkas_allowed_for_aol: for be, be_type in zip(carState.buttonEvents, button_event_types, strict=False): if be_type == ButtonType.lkas and be.pressed and starpilot_toggles.always_on_lateral_lkas: @@ -139,8 +151,10 @@ class StarPilotCard: self.always_on_lateral_allowed = not self.always_on_lateral_allowed elif starpilot_toggles.main_cruise_slc_adopt and starpilot_toggles.speed_limit_controller: self.params_memory.put_bool("SLCAdoptSpeedLimit", True) - elif starpilot_toggles.always_on_lateral_main: - if pacifica_hybrid_aol_requires_set_press(self.CP.carFingerprint, self.CP.pcmCruise): + if starpilot_toggles.always_on_lateral_main and not button_managed_aol: + car_fingerprint = getattr(self.CP, "carFingerprint", None) + pcm_cruise = getattr(self.CP, "pcmCruise", False) + if pacifica_hybrid_aol_requires_set_press(car_fingerprint, pcm_cruise): # Chrysler Pacifica Hybrid stock ACC can fall back to plain cruise if AOL # starts steering before the driver presses SET. if not carState.cruiseState.available: @@ -163,9 +177,11 @@ class StarPilotCard: self.always_on_lateral_enabled = self.always_on_lateral_allowed and self.always_on_lateral_set self.always_on_lateral_enabled &= carState.gearShifter not in NON_DRIVING_GEARS self.always_on_lateral_enabled &= not self.hyundai_aol_needs_engagement or self.hyundai_aol_ready + self.always_on_lateral_enabled &= not self.nissan_aol_needs_engagement or self.nissan_aol_ready self.always_on_lateral_enabled &= sm["starpilotPlan"].lateralCheck self.always_on_lateral_enabled &= sm["liveCalibration"].calPerc >= 1 - self.always_on_lateral_enabled &= (ET.IMMEDIATE_DISABLE not in sm["selfdriveState"].alertType + sm["starpilotSelfdriveState"].alertType) or self.frogs_go_moo + alert_types = sm["selfdriveState"].alertType + sm["starpilotSelfdriveState"].alertType + self.always_on_lateral_enabled &= ET.IMMEDIATE_DISABLE not in alert_types or self.frogs_go_moo self.always_on_lateral_enabled &= not (carState.brakePressed and carState.vEgo < starpilot_toggles.always_on_lateral_pause_speed) or carState.standstill self.always_on_lateral_enabled &= not self.error_log.is_file() or self.frogs_go_moo diff --git a/starpilot/controls/tests/test_starpilot_card.py b/starpilot/controls/tests/test_starpilot_card.py index 725e3ef44..3d5c36339 100644 --- a/starpilot/controls/tests/test_starpilot_card.py +++ b/starpilot/controls/tests/test_starpilot_card.py @@ -215,6 +215,63 @@ def test_hyundai_aol_does_not_auto_start_from_cruise_availability(monkeypatch, t assert ret.alwaysOnLateralEnabled is False +def test_nissan_aol_requires_normal_engagement(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="nissan"), + SimpleNamespace(alternativeExperience=spc.ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL), + ) + assert card.nissan_aol_needs_engagement is True + assert card.nissan_aol_ready is False + starpilot_car_state = SimpleNamespace(distancePressed=False) + sm = make_sm() + toggles = make_toggles(always_on_lateral=True, main_cruise_aol_toggle=True, lkas_allowed_for_aol=True) + car_state = make_car_state(available=True, button_events=[SimpleNamespace(type=spc.ButtonType.mainCruise, pressed=True)]) + + ret = card.update(car_state, starpilot_car_state, sm, toggles) + assert ret.alwaysOnLateralAllowed is True + assert ret.alwaysOnLateralEnabled is False + + sm["selfdriveState"].active = True + ret = card.update(make_car_state(available=True, enabled=True), starpilot_car_state, sm, toggles) + assert ret.alwaysOnLateralEnabled is True + + sm["selfdriveState"].active = False + ret = card.update(make_car_state(available=True), starpilot_car_state, sm, toggles) + assert ret.alwaysOnLateralEnabled is True + + +def test_nissan_aol_engagement_latch_resets_out_of_drive(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="nissan"), + SimpleNamespace(alternativeExperience=spc.ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL), + ) + starpilot_car_state = SimpleNamespace(distancePressed=False) + sm = make_sm() + sm["selfdriveState"].active = True + toggles = make_toggles(always_on_lateral=True, always_on_lateral_main=True) + + ret = card.update(make_car_state(available=True, enabled=True), starpilot_car_state, sm, toggles) + assert ret.alwaysOnLateralEnabled is True + + sm["selfdriveState"].active = False + park_state = make_car_state(available=True) + park_state.gearShifter = spc.GearShifter.park + ret = card.update(park_state, starpilot_car_state, sm, toggles) + assert ret.alwaysOnLateralEnabled is False + assert card.nissan_aol_ready is False + + ret = card.update(make_car_state(available=True), starpilot_car_state, sm, toggles) + assert ret.alwaysOnLateralEnabled is False + + def test_hyundai_canfd_lkas_button_can_toggle_aol_before_engagement(monkeypatch, tmp_path): monkeypatch.setattr(spc, "Params", FakeParams) monkeypatch.setattr(spc, "is_FrogsGoMoo", lambda: False) @@ -371,6 +428,7 @@ def test_honda_lkas_button_pauses_lateral_when_cruise_is_active(monkeypatch, tmp sm = make_sm() sm["selfdriveState"].active = True toggles = make_toggles(always_on_lateral_lkas=True, lkas_allowed_for_aol=True) + card.prev_active = True ret = card.update(car_state, starpilot_car_state, sm, toggles) @@ -383,6 +441,51 @@ def test_honda_lkas_button_pauses_lateral_when_cruise_is_active(monkeypatch, tmp assert ret.pauseLateral is False +def test_honda_main_aol_follows_cruise_main_without_manual_aol_button_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="honda"), + SimpleNamespace(alternativeExperience=spc.ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL), + ) + + toggles = make_toggles(always_on_lateral_main=True, lkas_allowed_for_aol=True) + ret = card.update(make_car_state(available=True), SimpleNamespace(distancePressed=False), make_sm(), toggles) + + assert ret.alwaysOnLateralAllowed is True + assert ret.alwaysOnLateralEnabled is True + + +def test_hyundai_main_aol_persists_after_brake_disengage_without_manual_aol_button_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="hyundai"), + SimpleNamespace(alternativeExperience=spc.ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL), + ) + + sm = make_sm() + toggles = make_toggles(always_on_lateral_main=True) + starpilot_car_state = SimpleNamespace(distancePressed=False) + + sm["selfdriveState"].active = True + enabled_state = make_car_state(available=True, enabled=True) + ret = card.update(enabled_state, starpilot_car_state, sm, toggles) + assert ret.alwaysOnLateralAllowed is True + assert ret.alwaysOnLateralEnabled is True + + sm["selfdriveState"].active = False + disengaged_state = make_car_state(available=True, enabled=False) + ret = card.update(disengaged_state, starpilot_car_state, sm, toggles) + + assert ret.alwaysOnLateralAllowed is True + assert ret.alwaysOnLateralEnabled is True + + def test_main_aol_still_follows_cruise_main_for_other_platforms(monkeypatch, tmp_path): monkeypatch.setattr(spc, "Params", FakeParams) monkeypatch.setattr(spc, "is_FrogsGoMoo", lambda: False) diff --git a/starpilot/system/speed_limit_vision.py b/starpilot/system/speed_limit_vision.py index 066da868e..4cf5ebbd9 100644 --- a/starpilot/system/speed_limit_vision.py +++ b/starpilot/system/speed_limit_vision.py @@ -200,8 +200,9 @@ DETECTOR_CLASSIFIER_TRUSTED_MODEL_MIN_READ_CONFIDENCE = 0.65 DETECTOR_CLASSIFIER_TRUSTED_MODEL_MIN_SUPPORT = 2 DETECTOR_CLASSIFIER_STRONG_MODEL_MIN_PROPOSAL_CONFIDENCE = 0.60 DETECTOR_CLASSIFIER_STRONG_MODEL_MIN_READ_CONFIDENCE = 0.995 +DETECTOR_CLASSIFIER_STRONG_MODEL_CONSENSUS_MIN_READ_CONFIDENCE = 0.95 DETECTOR_CLASSIFIER_STRONG_MODEL_CONSENSUS_ENABLED = True -DETECTOR_CLASSIFIER_STRONG_MODEL_CONSENSUS_MIN_SUPPORT = 3 +DETECTOR_CLASSIFIER_STRONG_MODEL_CONSENSUS_MIN_SUPPORT = 2 DETECTOR_CLASSIFIER_MODEL_ONLY_CONSENSUS_MIN_CONFIDENCE = 0.90 DETECTOR_CLASSIFIER_MODEL_ONLY_CONSENSUS_MIN_SUPPORT = 2 SCHOOL_ZONE_SPEED_PRIOR = 0.12 @@ -1475,6 +1476,13 @@ class SpeedLimitVisionDaemon: proposal_confidence >= DETECTOR_CLASSIFIER_STRONG_MODEL_MIN_PROPOSAL_CONFIDENCE and model_read[1] >= DETECTOR_CLASSIFIER_STRONG_MODEL_MIN_READ_CONFIDENCE ) + strong_model_consensus_read = ( + class_id == 0 and + model_read is not None and + not is_small_box and + proposal_confidence >= DETECTOR_CLASSIFIER_STRONG_MODEL_MIN_PROPOSAL_CONFIDENCE and + model_read[1] >= DETECTOR_CLASSIFIER_STRONG_MODEL_CONSENSUS_MIN_READ_CONFIDENCE + ) model_only_consensus_read = ( not DETECTOR_CLASSIFIER_CROP_OCR_ENABLED and class_id == 0 and @@ -1527,7 +1535,7 @@ class SpeedLimitVisionDaemon: speed_regulatory_support[speed_limit_mph] = speed_regulatory_support.get(speed_limit_mph, 0) + 1 if trusted_model_read: speed_trusted_model_support[speed_limit_mph] = speed_trusted_model_support.get(speed_limit_mph, 0) + 1 - if strong_model_read: + if strong_model_consensus_read: speed_strong_model_support[speed_limit_mph] = speed_strong_model_support.get(speed_limit_mph, 0) + 1 if needs_ocr_confirmation and model_only_consensus_read: speed_model_only_rescue_support[speed_limit_mph] = speed_model_only_rescue_support.get(speed_limit_mph, 0) + 1 @@ -2109,6 +2117,7 @@ class SpeedLimitVisionDaemon: "candidate", candidateSpeedLimitMph=detection.speed_limit_mph, candidateConfidence=round(detection.confidence, 4), + candidateStrongConsensus=detection.strong_consensus, ) self.history.append(HistoryEntry(detection.speed_limit_mph, detection.confidence, now, detection.strong_consensus)) diff --git a/starpilot/system/tests/test_speed_limit_vision.py b/starpilot/system/tests/test_speed_limit_vision.py index 60f65d38e..be62f5ef9 100644 --- a/starpilot/system/tests/test_speed_limit_vision.py +++ b/starpilot/system/tests/test_speed_limit_vision.py @@ -15,11 +15,11 @@ def daemon_with_history(current_speed, entries): def test_speed_change_requires_two_matching_reads(): - daemon = daemon_with_history(40, [(55, 0.70)]) + daemon = daemon_with_history(40, [(55, 0.95)]) assert daemon._confirm_detection() is None daemon.history.append(HistoryEntry(55, 0.76, 1.0)) - assert daemon._confirm_detection() == pytest.approx((55, 0.76)) + assert daemon._confirm_detection() == pytest.approx((55, 0.95)) def test_speed_change_accepts_single_strong_consensus_read(): @@ -69,8 +69,9 @@ def test_detector_classifier_runtime_reads_regulatory_sign_without_ocr(model_onl assert detection.speed_limit_mph == 55 -def test_detector_classifier_marks_three_strong_model_crops_as_consensus(model_only_runtime): - daemon = detector_classifier_daemon(regulatory=True, model_read=(20, 0.999), proposal_confidence=0.80) +def test_detector_classifier_marks_two_strong_model_crops_as_consensus(model_only_runtime): + reads = iter(((20, 0.96), (20, 0.97), None)) + daemon = detector_classifier_daemon(regulatory=True, model_read=lambda _crop: next(reads), proposal_confidence=0.80) detection = daemon._detect_sign_from_detector_classifier(np.zeros((480, 960, 3), dtype=np.uint8)) assert detection is not None diff --git a/starpilot/system/the_galaxy/assets/components/home/home.js b/starpilot/system/the_galaxy/assets/components/home/home.js index 805fcc835..61725d06f 100644 --- a/starpilot/system/the_galaxy/assets/components/home/home.js +++ b/starpilot/system/the_galaxy/assets/components/home/home.js @@ -426,11 +426,13 @@ function renderStorage(storage) { function renderVitals(device) { const uptime = device.uptimeSeconds == null ? "unknown" : formatDuration(device.uptimeSeconds); const cpu = device.cpuTempC == null ? "unknown" : `${formatInt(device.cpuTempC)} C`; + const lanIp = device.lanIp || "unknown"; return `

Vitals

Status${escapeHtml(device.status || "Parked")}
+
LAN IP${escapeHtml(lanIp)}
Uptime${escapeHtml(uptime)}
CPU temp${escapeHtml(cpu)}
diff --git a/starpilot/system/the_galaxy/assets/components/tools/device_settings.js b/starpilot/system/the_galaxy/assets/components/tools/device_settings.js index e30cb4c73..2f2967199 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/device_settings.js +++ b/starpilot/system/the_galaxy/assets/components/tools/device_settings.js @@ -17,7 +17,7 @@ let lastFtmWorkspaceFetch = 0 const DYNAMIC_DEFAULT_DEP_KEYS = new Set(["AccelerationProfile", "EVTuning", "TruckTuning"]) const PANDA_FIRMWARE_TOGGLE_KEYS = new Set(["IgnoreIgnitionLine", "RemoteStartBootsComma", "HKGRemoteStartBootsComma"]) const FTM_ADVANCED_LATERAL_KEYS = new Set([ - "AdvancedLateralTune", "ForceAutoTune", "ForceAutoTuneOff", "SteerDelay", + "AdvancedLateralTune", "ForceAutoTune", "ForceAutoTuneOff", "UseAutoSteerDelay", "SteerDelay", "SteerFriction", "SteerKP", "SteerLatAccel", "SteerRatio", ]) @@ -1020,6 +1020,9 @@ function clearSearchFilter() { const cancelButtonKeys = new Set(["CancelButtonControl", "LongCancelButtonControl", "VeryLongCancelButtonControl"]) function getSettingLockReason(param) { + if (param?.disabled_when_key_true && state.values[param.disabled_when_key_true]) { + return param.disabled_reason || "Disabled by another setting." + } return "" } @@ -1256,7 +1259,7 @@ function renderSettingRow(p) {
${formatSliderValue(bounds.min, String(bounds.step), p.precision, p.key)} to ${formatSliderValue(bounds.max, String(bounds.step), p.precision, p.key)} @@ -1270,7 +1273,7 @@ function renderSettingRow(p) { min="${bounds.min}" max="${bounds.max}" step="${bounds.step}" - disabled="${() => updating}" + disabled="${() => isLocked || updating}" value="${() => formatNumericForInput(resolveCurrentNumericValue(p, numericBounds(p)), precision)}" @keydown="${(e) => { if (e.key !== "Enter") return @@ -1279,17 +1282,17 @@ function renderSettingRow(p) { }}" />
` })()} 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 3c6f47bcf..e205f1145 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 @@ -24,16 +24,26 @@ "ui_type": "toggle", "is_parent_toggle": true }, + { + "key": "UseAutoSteerDelay", + "label": "Use Auto-Learned Delay", + "description": "Automatically learn the full steering delay. The manual actuator delay is ignored while enabled.", + "data_type": "bool", + "ui_type": "toggle", + "parent_key": "AdvancedLateralTune" + }, { "key": "SteerDelay", "label": "Actuator Delay", - "description": "Actuator Delay", + "description": "Exact full delay between steering command and vehicle response.", "data_type": "float", "ui_type": "numeric", "min": 0.01, "max": 1.0, "step": 0.01, "precision": 2, + "disabled_when_key_true": "UseAutoSteerDelay", + "disabled_reason": "Auto-learned delay is enabled.", "parent_key": "AdvancedLateralTune" }, { @@ -3633,9 +3643,9 @@ "icon": "bi-exclamation-triangle", "params": [ { - "key": "TryRaylibUI", - "label": "Try raylib UI", - "description": "Use the beta raylib UI instead of the default Qt UI on tici/tizi devices. This setting has no effect on C4/mici devices.", + "key": "UseOldUI", + "label": "Use Old UI", + "description": "Use the old Qt UI instead of the default raylib UI on tici/tizi devices. This setting has no effect on C4/mici devices.", "data_type": "bool", "ui_type": "toggle" }, diff --git a/starpilot/system/the_galaxy/assets/components/tools/tuning.js b/starpilot/system/the_galaxy/assets/components/tools/tuning.js index afdfbc723..219026a3d 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/tuning.js +++ b/starpilot/system/the_galaxy/assets/components/tools/tuning.js @@ -543,6 +543,7 @@ function mergedFtmOverrides() { function formatTuneComparisonValue(value) { if (Array.isArray(value)) return renderCurve(value) + if (typeof value === "boolean") return value ? "On" : "Off" const numeric = Number(value) return Number.isFinite(numeric) ? numeric.toFixed(3) : String(value ?? "-") } @@ -556,6 +557,7 @@ function tuneComparisonRows() { const rows = [ ["Lat accel", "SteerLatAccel"], ["Friction", "SteerFriction"], + ["Auto steer delay", "UseAutoSteerDelay"], ["Steer delay", "SteerDelay"], ["Steer ratio", "SteerRatio"], ["KP", "SteerKP"], diff --git a/starpilot/system/the_galaxy/ftm_workspace.py b/starpilot/system/the_galaxy/ftm_workspace.py index 31eda3515..76d0d53b3 100644 --- a/starpilot/system/the_galaxy/ftm_workspace.py +++ b/starpilot/system/the_galaxy/ftm_workspace.py @@ -30,6 +30,7 @@ from openpilot.selfdrive.controls.lib.latcontrol_vehicle_tunes import ( get_standard_friction_threshold, normalize_ftm_overrides, ) +from openpilot.starpilot.common.lateral_delay import full_lateral_delay from openpilot.system.hardware import PC from openpilot.system.hardware.hw import Paths from openpilot.tools.lib.logreader import LogReader @@ -47,6 +48,7 @@ TRIAL_PARAM_SPECS = { "AdvancedLateralTune": "bool", "ForceAutoTune": "bool", "ForceAutoTuneOff": "bool", + "UseAutoSteerDelay": "bool", "SteerDelay": "float", "SteerFriction": "float", "SteerKP": "float", @@ -61,6 +63,7 @@ FTM_ADVANCED_LATERAL_PARAM_KEYS = { "AdvancedLateralTune", "ForceAutoTune", "ForceAutoTuneOff", + "UseAutoSteerDelay", "SteerDelay", "SteerFriction", "SteerKP", @@ -661,7 +664,7 @@ def _segment_samples(segment_source: RouteSource) -> tuple[list[FTMSample], car. def _current_param_state(CP, params: Params) -> dict[str, Any]: advanced_enabled = params.get_bool("AdvancedLateralTune") torque_tune = CP.lateralTuning.torque if CP.lateralTuning.which() == "torque" else None - stock_delay = float(getattr(CP, "steerActuatorDelay", 0.0) or 0.0) + stock_delay = full_lateral_delay(float(getattr(CP, "steerActuatorDelay", 0.0) or 0.0)) stock_ratio = float(getattr(CP, "steerRatio", 0.0) or 0.0) stock_friction = float(getattr(torque_tune, "friction", 0.0) or 0.0) if torque_tune is not None else 0.0 stock_lat_accel = float(getattr(torque_tune, "latAccelFactor", 0.0) or 0.0) if torque_tune is not None else 0.0 @@ -669,6 +672,7 @@ def _current_param_state(CP, params: Params) -> dict[str, Any]: "AdvancedLateralTune": advanced_enabled, "ForceAutoTune": params.get_bool("ForceAutoTune"), "ForceAutoTuneOff": params.get_bool("ForceAutoTuneOff"), + "UseAutoSteerDelay": params.get_bool("UseAutoSteerDelay"), "SteerDelay": params.get_float("SteerDelay", return_default=True, default=stock_delay) if advanced_enabled else stock_delay, "SteerFriction": params.get_float("SteerFriction", return_default=True, default=stock_friction) if advanced_enabled else stock_friction, "SteerKP": params.get_float("SteerKP", return_default=True, default=KP) if advanced_enabled else KP, @@ -690,7 +694,8 @@ def _stock_param_state(CP, capabilities: dict[str, Any]) -> dict[str, Any]: if rich_profile and meta.get("profile") == rich_profile } return { - "SteerDelay": float(getattr(CP, "steerActuatorDelay", 0.0) or 0.0), + "UseAutoSteerDelay": True, + "SteerDelay": full_lateral_delay(float(getattr(CP, "steerActuatorDelay", 0.0) or 0.0)), "SteerFriction": float(getattr(torque_tune, "friction", 0.0) or 0.0) if torque_tune is not None else 0.0, "SteerKP": float(KP), "SteerLatAccel": float(getattr(torque_tune, "latAccelFactor", 0.0) or 0.0) if torque_tune is not None else 0.0, @@ -1425,6 +1430,9 @@ def _merge_primary_adjustments(suggestions: list[dict[str, Any]], multiplier: fl if not math.isclose(float(bucket["current"]), next_value, abs_tol=max(precision / 2.0, 1e-6)): params_delta[param_key] = next_value + if "SteerDelay" in params_delta: + params_delta["UseAutoSteerDelay"] = False + supported_knobs = get_ftm_supported_vehicle_knobs() for symbol, bucket in vehicle_targets.items(): meta = supported_knobs.get(symbol) diff --git a/starpilot/system/the_galaxy/tests/test_ftm_workspace.py b/starpilot/system/the_galaxy/tests/test_ftm_workspace.py index cfc20f3cc..8b6fba26e 100644 --- a/starpilot/system/the_galaxy/tests/test_ftm_workspace.py +++ b/starpilot/system/the_galaxy/tests/test_ftm_workspace.py @@ -241,7 +241,8 @@ def test_stock_param_state_captures_generic_and_rich_defaults(tmp_path): stock = module._stock_param_state(CP, capabilities) assert stock["SteerLatAccel"] == pytest.approx(3.0) assert stock["SteerFriction"] == pytest.approx(0.09) - assert stock["SteerDelay"] == pytest.approx(0.1) + assert stock["UseAutoSteerDelay"] is True + assert stock["SteerDelay"] == pytest.approx(0.3) assert stock["SteerRatio"] == pytest.approx(14.26) assert len(stock["FTMBaseFrictionThresholds"]["hkg_canfd"]["values"]) == 5 assert stock["FTMVehicleKnobs"]["hyundai_ioniq_6.turn_in_boost_left"] == pytest.approx(1.64) @@ -570,6 +571,26 @@ def test_merge_primary_adjustments_averages_conflicting_deltas(tmp_path): assert overrides == {} +def test_merge_primary_adjustments_disables_auto_delay_for_manual_delay_trial(tmp_path): + module, _ = _load_ftm_workspace_module(tmp_path) + suggestions = [{ + "severity": 1.0, + "primaryAdjustmentRaw": { + "type": "generic_param", + "paramKey": "SteerDelay", + "current": 0.31, + "suggested": 0.33, + "delta": 0.02, + }, + }] + + params_delta, overrides, _ = module._merge_primary_adjustments(suggestions, 1.0) + + assert params_delta["SteerDelay"] == pytest.approx(0.33) + assert params_delta["UseAutoSteerDelay"] is False + assert overrides == {} + + def test_apply_and_revert_trial_profile_round_trip(tmp_path): module, fake_params_cls = _load_ftm_workspace_module(tmp_path) workspace = module.ensure_ftm_workspace() diff --git a/starpilot/system/the_galaxy/tests/test_navigation_params.py b/starpilot/system/the_galaxy/tests/test_navigation_params.py index 35ff03fc7..9574041c7 100644 --- a/starpilot/system/the_galaxy/tests/test_navigation_params.py +++ b/starpilot/system/the_galaxy/tests/test_navigation_params.py @@ -70,7 +70,11 @@ class WritableFakeParams: def _params_client(monkeypatch, values, device_type): fake_params = WritableFakeParams(values) monkeypatch.setattr(the_galaxy, "params", fake_params) - monkeypatch.setattr(the_galaxy, "_get_param_type_info", lambda: ({"TryRaylibUI"}, {"TryRaylibUI": bool})) + monkeypatch.setattr( + the_galaxy, + "_get_param_type_info", + lambda: ({"UseOldUI", "TryRaylibUI"}, {"UseOldUI": bool, "TryRaylibUI": bool}), + ) monkeypatch.setattr(the_galaxy.HARDWARE, "get_device_type", lambda: device_type) monkeypatch.setattr(the_galaxy.Paths, "comma_home", lambda: "/tmp/dashboard-test-home", raising=False) @@ -184,36 +188,51 @@ def test_galaxy_session_value_matches_cookie_format(): ) == f"testGalaxySlug01%3A{'a' * 64}" -def test_try_raylib_ui_is_noop_on_c4_mici(monkeypatch): - client, fake_params = _params_client(monkeypatch, {"TryRaylibUI": False, "IsOnroad": False}, "mici") +def test_use_old_ui_is_noop_on_c4_mici(monkeypatch): + client, fake_params = _params_client(monkeypatch, {"UseOldUI": False, "IsOnroad": False}, "mici") - response = client.put("/api/params", json={"key": "TryRaylibUI", "value": True}) + response = client.put("/api/params", json={"key": "UseOldUI", "value": True}) payload = response.get_json() assert response.status_code == 200 - assert payload["updated"] == {"TryRaylibUI": False} - assert fake_params.values["TryRaylibUI"] is False + assert payload["updated"] == {"UseOldUI": False, "TryRaylibUI": False} + assert fake_params.values["UseOldUI"] is False assert fake_params.writes == [] -def test_try_raylib_ui_writes_on_big_device_offroad(monkeypatch): - client, fake_params = _params_client(monkeypatch, {"TryRaylibUI": False, "IsOnroad": False}, "tici") +def test_use_old_ui_writes_on_big_device_offroad(monkeypatch): + client, fake_params = _params_client(monkeypatch, {"UseOldUI": False, "TryRaylibUI": True, "IsOnroad": False}, "tici") - response = client.put("/api/params", json={"key": "TryRaylibUI", "value": True}) + response = client.put("/api/params", json={"key": "UseOldUI", "value": True}) payload = response.get_json() assert response.status_code == 200 - assert payload["updated"] == {"TryRaylibUI": True} - assert fake_params.values["TryRaylibUI"] is True - assert fake_params.writes == [("TryRaylibUI", True)] + assert payload["updated"] == {"UseOldUI": True, "TryRaylibUI": False} + assert fake_params.values["UseOldUI"] is True + assert fake_params.values["TryRaylibUI"] is False + assert fake_params.writes == [("UseOldUI", True), ("TryRaylibUI", False)] -def test_try_raylib_ui_rejects_big_device_onroad_change(monkeypatch): - client, fake_params = _params_client(monkeypatch, {"TryRaylibUI": False, "IsOnroad": True}, "tici") +def test_use_old_ui_rejects_big_device_onroad_change(monkeypatch): + client, fake_params = _params_client(monkeypatch, {"UseOldUI": False, "TryRaylibUI": True, "IsOnroad": True}, "tici") - response = client.put("/api/params", json={"key": "TryRaylibUI", "value": True}) + response = client.put("/api/params", json={"key": "UseOldUI", "value": True}) assert response.status_code == 403 - assert response.get_json()["error"] == "Cannot change Try raylib UI while driving." - assert fake_params.values["TryRaylibUI"] is False + assert response.get_json()["error"] == "Cannot change Use Old UI while driving." + assert fake_params.values["UseOldUI"] is False + assert fake_params.values["TryRaylibUI"] is True assert fake_params.writes == [] + + +def test_legacy_try_raylib_ui_payload_updates_use_old_ui(monkeypatch): + client, fake_params = _params_client(monkeypatch, {"UseOldUI": True, "TryRaylibUI": False, "IsOnroad": False}, "tici") + + response = client.put("/api/params", json={"key": "TryRaylibUI", "value": True}) + payload = response.get_json() + + assert response.status_code == 200 + assert payload["updated"] == {"UseOldUI": False, "TryRaylibUI": True} + assert fake_params.values["UseOldUI"] is False + assert fake_params.values["TryRaylibUI"] is True + assert fake_params.writes == [("UseOldUI", False), ("TryRaylibUI", True)] diff --git a/starpilot/system/the_galaxy/the_galaxy.py b/starpilot/system/the_galaxy/the_galaxy.py index b7a55999c..0ee544a82 100644 --- a/starpilot/system/the_galaxy/the_galaxy.py +++ b/starpilot/system/the_galaxy/the_galaxy.py @@ -63,6 +63,7 @@ from openpilot.starpilot.common.maps_catalog import ( ) from openpilot.starpilot.common.experimental_state import sync_persist_chill_state, sync_persist_experimental_state from openpilot.starpilot.common.favorite_slots import FAVORITE_SLOTS_PARAM, normalize_favorite_slots +from openpilot.starpilot.common.lateral_delay import full_lateral_delay 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, migrate_cancel_button_controls, update_starpilot_toggles @@ -845,6 +846,7 @@ _TROUBLESHOOT_CEM_KEYS = [ _TROUBLESHOOT_ADVANCED_LATERAL_KEYS = [ "AdvancedLateralTune", + "UseAutoSteerDelay", "SteerDelay", "SteerFriction", "SteerOffset", @@ -2450,6 +2452,18 @@ def _is_blank_param_raw(raw_value): return len(raw_value.strip()) == 0 return False +def _get_use_old_ui_enabled(): + if not _raylib_ui_toggle_affects_device(): + return False + + raw_value = _safe_params_get_live_raw("UseOldUI") + if _is_blank_param_raw(raw_value): + legacy_raw_value = _safe_params_get_live_raw("TryRaylibUI") + if not _is_blank_param_raw(legacy_raw_value): + return not _coerce_param_value(legacy_raw_value, bool) + + return _coerce_param_value(raw_value, bool) + def _has_runtime_default_value(key, raw_value): if _is_blank_param_raw(raw_value): return False @@ -2483,7 +2497,7 @@ def _get_runtime_default_param_overrides(): overrides["EVTuning"] = default_ev_tuning_enabled(cp) car_param_defaults = { - "SteerDelay": getattr(cp, "steerActuatorDelay", None), + "SteerDelay": full_lateral_delay(getattr(cp, "steerActuatorDelay", 0.0)), "SteerRatio": getattr(cp, "steerRatio", None), "LongitudinalActuatorDelay": getattr(cp, "longitudinalActuatorDelay", None), "StartAccel": getattr(cp, "startAccel", None), @@ -2528,6 +2542,11 @@ def _get_runtime_default_param_overrides(): return overrides def _get_current_param_value(key, value_type, defaults_lookup=None): + if key == "UseOldUI": + return _get_use_old_ui_enabled() + if key == "TryRaylibUI": + return _raylib_ui_toggle_affects_device() and not _get_use_old_ui_enabled() + if key == CUSTOM_ACCEL_PROFILE_INITIALIZED_KEY: return _get_custom_accel_profile_initialized() @@ -3102,6 +3121,12 @@ def _build_troubleshoot_payload(): "value": _get_fingerprint_snapshot_text(), "resettable": False, }, + { + "id": "lan_ip", + "label": "LAN IP", + "value": utilities.get_current_lan_ip() or "Unavailable", + "resettable": False, + }, *_get_hardware_snapshot_items(), { "id": "driving_model", @@ -3761,6 +3786,8 @@ def setup(app): def disable_device_settings_asset_cache(response): if request.path in { "/assets/components/router.js", + "/assets/components/home/home.js", + "/assets/components/home/home.css", "/assets/components/tools/device_settings.js", "/assets/components/tools/device_settings.css", "/assets/components/tools/device_settings_layout.json", @@ -4166,22 +4193,24 @@ def setup(app): if key not in allowed_keys: return jsonify({"error": f"Parameter '{key}' is not editable."}), 403 - if key == "TryRaylibUI": + if key in {"UseOldUI", "TryRaylibUI"}: enabled = str_val.strip() in ("1", "true", "True") + use_old_ui = enabled if key == "UseOldUI" else not enabled + updated = {"UseOldUI": use_old_ui, "TryRaylibUI": not use_old_ui} if not _raylib_ui_toggle_affects_device(): - current_enabled = params.get_bool("TryRaylibUI") return jsonify({ - "message": "Try raylib UI is only available on tici/tizi devices.", - "updated": {"TryRaylibUI": current_enabled}, + "message": "Use Old UI is only available on tici/tizi devices.", + "updated": {"UseOldUI": False, "TryRaylibUI": False}, }), 200 if params.get_bool("IsOnroad"): - return jsonify({"error": "Cannot change Try raylib UI while driving."}), 403 + return jsonify({"error": "Cannot change Use Old UI while driving."}), 403 - params.put_bool("TryRaylibUI", enabled) + params.put_bool("UseOldUI", use_old_ui) + params.put_bool("TryRaylibUI", not use_old_ui) return jsonify({ - "message": f"{'Raylib' if enabled else 'Qt'} UI selected. UI will restart shortly.", - "updated": {"TryRaylibUI": enabled}, + "message": f"{'Old' if use_old_ui else 'Raylib'} UI selected. UI will restart shortly.", + "updated": updated, }), 200 # 1. Prevent changing the model or reboot-required toggles while the car is actively driving @@ -4499,6 +4528,10 @@ def setup(app): return _serialize_param_write_value(_get_custom_accel_profile_initialized()), 200 if request_key == "LeadIndicator": return _serialize_param_write_value(_get_lead_indicator_enabled()), 200 + if request_key == "UseOldUI": + return ("1" if _get_use_old_ui_enabled() else "0"), 200 + if request_key == "TryRaylibUI": + return ("1" if _raylib_ui_toggle_affects_device() and not _get_use_old_ui_enabled() else "0"), 200 if request_key == "IsRHD" and not params.get_bool("IsRHDOverride"): return ("1" if params.get_bool("IsRhdDetected") else "0"), 200 value = params.get(request_key) or "" @@ -4535,7 +4568,11 @@ def setup(app): default_val = defaults_lookup.get(key) try: - if t == bool: + if key == "UseOldUI": + result[key] = False + elif key == "TryRaylibUI": + result[key] = _raylib_ui_toggle_affects_device() + elif t == bool: if isinstance(default_val, bytes): default_str = default_val.decode("utf-8", errors="replace") else: diff --git a/starpilot/system/the_galaxy/utilities.py b/starpilot/system/the_galaxy/utilities.py index 3ae33c466..6cbffd704 100644 --- a/starpilot/system/the_galaxy/utilities.py +++ b/starpilot/system/the_galaxy/utilities.py @@ -2,11 +2,13 @@ import base64 import copy import hashlib +import ipaddress import json import os import re import secrets import shutil +import socket import subprocess import sys import threading @@ -75,6 +77,7 @@ DASHBOARD_ANALYZER_LOG_PATH = "/tmp/galaxy_dashboard_analyzer.log" DASHBOARD_ANALYZER_STATUS_PATH = Path("/tmp/galaxy_dashboard_analyzer_status.json") DASHBOARD_ANALYZER_STATUS_MAX_AGE_SECONDS = 30 * 60 DASHBOARD_TOP_MODEL_LIMIT = 3 +LAN_IP_CACHE_TTL_SECONDS = 10.0 DASHBOARD_EVENT_DISTRACTED = "driverDistracted2" DASHBOARD_EVENT_UNRESPONSIVE = "driverUnresponsive3" DASHBOARD_TIME_SOURCE_LOG = "log" @@ -104,9 +107,103 @@ _DASHBOARD_CACHE = { "updated_at": 0.0, "value": None, } +_LAN_IP_CACHE = { + "updated_at": 0.0, + "value": None, +} _DASHBOARD_ANALYZER_LOCK = threading.Lock() _DASHBOARD_ANALYZER_PROCESS = None params = Params(return_defaults=True) +_CGNAT_NETWORK = ipaddress.ip_network("100.64.0.0/10") + + +def _format_lan_ip(ip_text): + try: + address = ipaddress.ip_address(str(ip_text).strip()) + except ValueError: + return "" + + if ( + address.version != 4 + or address.is_loopback + or address.is_link_local + or address.is_multicast + or address.is_unspecified + or address in _CGNAT_NETWORK + ): + return "" + + return str(address) + + +def _candidate_lan_ips_from_ip_addr(): + try: + result = subprocess.run( + ["ip", "-o", "-4", "addr", "show", "scope", "global", "up"], + check=False, + capture_output=True, + text=True, + timeout=0.5, + ) + except Exception: + return [] + + candidates = [] + ignored_prefixes = ("lo", "tailscale", "tun", "docker", "br-", "veth", "zt", "wg") + for line in result.stdout.splitlines(): + parts = line.split() + if len(parts) < 4: + continue + + interface = parts[1].rstrip(":") + if interface.startswith(ignored_prefixes): + continue + + try: + inet_index = parts.index("inet") + except ValueError: + continue + + candidate = _format_lan_ip(parts[inet_index + 1].split("/", 1)[0] if inet_index + 1 < len(parts) else "") + if candidate: + candidates.append(candidate) + + return candidates + + +def get_current_lan_ip(): + now = time.monotonic() + if now - _LAN_IP_CACHE["updated_at"] < LAN_IP_CACHE_TTL_SECONDS: + return _LAN_IP_CACHE["value"] + + candidates = [] + + try: + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + sock.settimeout(0.2) + sock.connect(("8.8.8.8", 80)) + candidates.append(sock.getsockname()[0]) + except Exception: + pass + + candidates.extend(_candidate_lan_ips_from_ip_addr()) + + try: + result = subprocess.run(["hostname", "-I"], check=False, capture_output=True, text=True, timeout=0.5) + candidates.extend(result.stdout.split()) + except Exception: + pass + + for candidate in candidates: + ip = _format_lan_ip(candidate) + if ip: + _LAN_IP_CACHE["updated_at"] = time.monotonic() + _LAN_IP_CACHE["value"] = ip + return ip + + _LAN_IP_CACHE["updated_at"] = time.monotonic() + _LAN_IP_CACHE["value"] = None + return None def secure_filename(filename): @@ -2410,11 +2507,13 @@ def _build_device_summary(params_obj): is_onroad = _params_get_bool(params_obj, "IsOnroad") uptime_seconds = _read_uptime_seconds() cpu_temp_c = _read_cpu_temp_c() + lan_ip = get_current_lan_ip() return { "status": "Driving" if is_onroad else "Parked", "online": True, "uptimeSeconds": uptime_seconds, "cpuTempC": cpu_temp_c, + "lanIp": lan_ip, } diff --git a/system/manager/launch_param_migrations.py b/system/manager/launch_param_migrations.py index 33f80f06c..cfe62a786 100644 --- a/system/manager/launch_param_migrations.py +++ b/system/manager/launch_param_migrations.py @@ -9,6 +9,8 @@ from typing import Protocol LONG_PITCH_KEY = "LongPitch" STEER_KP_KEY = "SteerKP" STEER_KP_STOCK_KEY = "SteerKPStock" +TRY_RAYLIB_UI_KEY = "TryRaylibUI" +USE_OLD_UI_KEY = "UseOldUI" DEFAULT_STEER_KP = 0.6 LEGACY_STEER_KP = 0.7 @@ -17,6 +19,7 @@ QT_STEER_KP_PLACEHOLDER = 1.0 LAUNCH_PARAM_MIGRATION_MARKER = ".starpilot_launch_param_migrations_v2" BRANCH_DEFAULTS_MIGRATION_MARKER = ".starpilot_branch_defaults_migrations_v1" ACCELERATION_PROFILE_MIGRATION_MARKER = ".starpilot_acceleration_profile_default_v1" +USE_OLD_UI_MIGRATION_MARKER = ".starpilot_use_old_ui_migration_v1" MARKER_DIRNAME = ".starpilot_param_migrations" LEGACY_CE_STOPPED_LEAD_DEFAULT = True @@ -81,6 +84,10 @@ def _acceleration_profile_marker_path(params: ParamsLike) -> Path: return _marker_dir_path(params) / ACCELERATION_PROFILE_MIGRATION_MARKER +def _use_old_ui_marker_path(params: ParamsLike) -> Path: + return _marker_dir_path(params) / USE_OLD_UI_MIGRATION_MARKER + + def _marker_dir_path(params: ParamsLike) -> Path: params_path = Path(params.get_param_path()) # Params.clear_all() removes unknown files inside the params directory, so @@ -161,9 +168,22 @@ def _apply_acceleration_profile_default_migration(params: ParamsLike, marker: Pa marker.touch() +def _apply_use_old_ui_migration(params: ParamsLike, marker: Path) -> None: + if marker.exists(): + return + + marker.parent.mkdir(parents=True, exist_ok=True) + + if _param_file_exists(params, TRY_RAYLIB_UI_KEY) and not _param_file_exists(params, USE_OLD_UI_KEY): + params.put_bool(USE_OLD_UI_KEY, not params.get_bool(TRY_RAYLIB_UI_KEY)) + + marker.touch() + + def apply_launch_param_migrations(params: ParamsLike, marker_path: Path | None = None, branch_defaults_marker_path: Path | None = None, - acceleration_profile_marker_path: Path | None = None) -> None: + acceleration_profile_marker_path: Path | None = None, + use_old_ui_marker_path: Path | None = None) -> None: _apply_legacy_launch_param_migrations(params, marker_path or _default_marker_path(params)) # Keep branch-default rollout on its own marker so older installs that already # have the legacy marker still receive this one-time param reset. @@ -171,6 +191,7 @@ def apply_launch_param_migrations(params: ParamsLike, marker_path: Path | None = _apply_acceleration_profile_default_migration( params, acceleration_profile_marker_path or _acceleration_profile_marker_path(params) ) + _apply_use_old_ui_migration(params, use_old_ui_marker_path or _use_old_ui_marker_path(params)) def main() -> int: diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 2dd15c05b..24a09d0f2 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -123,7 +123,7 @@ class BigDeviceUIProcess: return self.should_run_fn(started, params, CP, starpilot_toggles) def _desired_process(self): - return self._raylib_process if self._params is not None and self._params.get_bool("TryRaylibUI") else self._qt_process + return self._qt_process if self._params is not None and self._params.get_bool("UseOldUI") else self._raylib_process def start(self) -> None: desired_process = self._desired_process() @@ -228,7 +228,7 @@ device_type = HARDWARE.get_device_type() if device_type in ("tici", "tizi"): procs.append(BigDeviceUIProcess(always_run, watchdog_max_dt=UI_WATCHDOG_MAX_DT)) else: - # C4 (mici) already runs the Python raylib UI path; TryRaylibUI must not affect it. + # C4 (mici) already runs the Python raylib UI path; UseOldUI must not affect it. procs.append(PythonProcess("ui", "selfdrive.ui.ui", always_run, watchdog_max_dt=UI_WATCHDOG_MAX_DT)) procs += [ diff --git a/system/manager/test/test_launch_param_migrations.py b/system/manager/test/test_launch_param_migrations.py index 5f1701948..a30b5ab16 100644 --- a/system/manager/test/test_launch_param_migrations.py +++ b/system/manager/test/test_launch_param_migrations.py @@ -7,6 +7,7 @@ from openpilot.system.manager.launch_param_migrations import ( LAUNCH_PARAM_MIGRATION_MARKER, MARKER_DIRNAME, STANDARD_ACCELERATION_PROFILE, + USE_OLD_UI_MIGRATION_MARKER, apply_launch_param_migrations, ) @@ -50,7 +51,9 @@ class FileBackedFakeParams: def marker_path(tmp_path: Path, marker_name: str) -> Path: - return tmp_path / MARKER_DIRNAME / "params" / marker_name + path = tmp_path / MARKER_DIRNAME / "params" / marker_name + path.parent.mkdir(parents=True, exist_ok=True) + return path def test_apply_launch_param_migrations_sets_branch_defaults_once(tmp_path): @@ -202,3 +205,33 @@ def test_apply_launch_param_migrations_preserves_custom_acceleration_profile_wit apply_launch_param_migrations(params) assert params.get_int("AccelerationProfile") == 1 + + +def test_apply_launch_param_migrations_inverts_try_raylib_ui_enabled(tmp_path): + params = FileBackedFakeParams(tmp_path / "params") + params.put_bool("TryRaylibUI", True) + + apply_launch_param_migrations(params) + + assert not params.get_bool("UseOldUI") + assert marker_path(tmp_path, USE_OLD_UI_MIGRATION_MARKER).is_file() + + +def test_apply_launch_param_migrations_inverts_try_raylib_ui_disabled(tmp_path): + params = FileBackedFakeParams(tmp_path / "params") + params.put_bool("TryRaylibUI", False) + + apply_launch_param_migrations(params) + + assert params.get_bool("UseOldUI") + assert marker_path(tmp_path, USE_OLD_UI_MIGRATION_MARKER).is_file() + + +def test_apply_launch_param_migrations_does_not_overwrite_use_old_ui(tmp_path): + params = FileBackedFakeParams(tmp_path / "params") + params.put_bool("TryRaylibUI", False) + params.put_bool("UseOldUI", False) + + apply_launch_param_migrations(params) + + assert not params.get_bool("UseOldUI") diff --git a/system/manager/test/test_manager.py b/system/manager/test/test_manager.py index b6837713b..ba1a55681 100644 --- a/system/manager/test/test_manager.py +++ b/system/manager/test/test_manager.py @@ -138,26 +138,26 @@ class TestManager: ui_process._qt_process = qt_process ui_process._raylib_process = raylib_process - params = FileBackedFakeParams(tmp_path / "params", {"TryRaylibUI": False}) + params = FileBackedFakeParams(tmp_path / "params", {"UseOldUI": False}) assert ui_process.should_run(False, params, car.CarParams.new_message(), SimpleNamespace()) ui_process.start() - assert ui_process.proc is qt_process.proc - assert qt_process.starts == 1 - assert raylib_process.starts == 0 + assert ui_process.proc is raylib_process.proc + assert qt_process.starts == 0 + assert raylib_process.starts == 1 - params.put_bool("TryRaylibUI", True) + params.put_bool("UseOldUI", True) assert ui_process.should_run(True, params, car.CarParams.new_message(), SimpleNamespace()) ui_process.start() - assert ui_process.proc is qt_process.proc + assert ui_process.proc is raylib_process.proc assert qt_process.stops == 0 - assert raylib_process.starts == 0 + assert qt_process.starts == 0 assert ui_process.should_run(False, params, car.CarParams.new_message(), SimpleNamespace()) ui_process.start() - assert qt_process.stops == 1 - assert raylib_process.starts == 1 - assert ui_process.proc is raylib_process.proc + assert raylib_process.stops == 1 + assert qt_process.starts == 1 + assert ui_process.proc is qt_process.proc def test_blacklisted_procs(self): # TODO: ensure there are blacklisted procs until we have a dedicated test diff --git a/tools/StarPilot/feasibleparams.txt b/tools/StarPilot/feasibleparams.txt index 78999ef66..1106aff71 100644 --- a/tools/StarPilot/feasibleparams.txt +++ b/tools/StarPilot/feasibleparams.txt @@ -331,6 +331,7 @@ StartupMessageBottom StartupMessageTop StaticPedalsOnUI SteerDelay +SteerDelayModeMigrated SteerDelayStock SteerFriction SteerFrictionStock @@ -367,6 +368,7 @@ TuningLevelConfirmed TurnDesires UnlockDoors UpdaterAvailableBranches +UseAutoSteerDelay UseKonikServer UsePrebuilt UseSI