diff --git a/common/libcommon.a b/common/libcommon.a index fcf773362..9e7d86a5b 100644 Binary files a/common/libcommon.a and b/common/libcommon.a differ diff --git a/common/params_keys.h b/common/params_keys.h index 79a7b0969..ba07a4f82 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -18,6 +18,7 @@ inline static std::unordered_map keys = { {"BootCount", {PERSISTENT, INT}}, {"BluetoothAudioAddress", {PERSISTENT, STRING}}, {"BluetoothAudioTestActive", {CLEAR_ON_MANAGER_START | DONT_LOG, BOOL}}, + {"BluetoothDisconnectControllersOffroad", {PERSISTENT, BOOL, "0"}}, {"BluetoothEnabled", {PERSISTENT, BOOL, "0"}}, {"CalibrationParams", {PERSISTENT, BYTES}}, {"CameraDebugExpGain", {CLEAR_ON_MANAGER_START, STRING}}, diff --git a/common/params_pyx.so b/common/params_pyx.so index 6370489fa..7ac79addc 100755 Binary files a/common/params_pyx.so and b/common/params_pyx.so differ diff --git a/opendbc_repo/opendbc/car/gm/carcontroller.py b/opendbc_repo/opendbc/car/gm/carcontroller.py index bcc6ea182..ca7da73bd 100644 --- a/opendbc_repo/opendbc/car/gm/carcontroller.py +++ b/opendbc_repo/opendbc/car/gm/carcontroller.py @@ -852,7 +852,6 @@ class CarController(CarControllerBase): CAR.CHEVROLET_VOLT_CC, CAR.CHEVROLET_MALIBU_CC, CAR.CHEVROLET_MALIBU_HYBRID_CC, - CAR.BUICK_LACROSSE, } if (self.CP.enableGasInterceptorDEPRECATED and self.CP.carFingerprint in CC_REGEN_PADDLE_CAR and diff --git a/opendbc_repo/opendbc/car/gm/interface.py b/opendbc_repo/opendbc/car/gm/interface.py index 17023e47a..10329962c 100755 --- a/opendbc_repo/opendbc/car/gm/interface.py +++ b/opendbc_repo/opendbc/car/gm/interface.py @@ -408,7 +408,7 @@ class CarInterface(CarInterfaceBase): ret.steerActuatorDelay = 0.1 # Default delay, not measured yet ret.steerLimitTimer = 0.4 - ret.radarTimeStepDEPRECATED = 0.0667 # GM radar runs at 15Hz instead of the standard 20Hz + ret.radarTimeStepDEPRECATED = 0.15 if candidate == CAR.BUICK_LACROSSE else 0.0667 ret.longitudinalActuatorDelay = 0.5 # large delay to initially start braking if candidate in ( @@ -440,7 +440,7 @@ class CarInterface(CarInterfaceBase): elif candidate in (CAR.BUICK_LACROSSE, CAR.BUICK_LACROSSE_ASCM, CAR.BUICK_LACROSSE_ASCM_19US): CarInterfaceBase.configure_torque_tune(CAR.BUICK_LACROSSE, ret.lateralTuning) if candidate == CAR.BUICK_LACROSSE_ASCM_19US: - ret.minSteerSpeed = 27 * CV.MPH_TO_MS + ret.minSteerSpeed = 28 * CV.MPH_TO_MS elif candidate == CAR.CADILLAC_ESCALADE: ret.minEnableSpeed = -1. # engage speed is decided by pcm diff --git a/opendbc_repo/opendbc/car/gm/tests/test_gm.py b/opendbc_repo/opendbc/car/gm/tests/test_gm.py index ca34e609c..a7e24c535 100644 --- a/opendbc_repo/opendbc/car/gm/tests/test_gm.py +++ b/opendbc_repo/opendbc/car/gm/tests/test_gm.py @@ -205,6 +205,33 @@ class TestBoltGps: class TestGMInterface: + def test_lacrosse_obd_and_ascm_integrations_remain_separate(self): + obd_params = interfaces[CAR.BUICK_LACROSSE].get_params( + CAR.BUICK_LACROSSE, + _empty_fingerprint(), + [], + alpha_long=False, + is_release=False, + docs=False, + starpilot_toggles=_test_starpilot_toggles(), + ) + ascm_params = interfaces[CAR.BUICK_LACROSSE_ASCM].get_params( + CAR.BUICK_LACROSSE_ASCM, + _empty_fingerprint(), + [], + alpha_long=False, + is_release=False, + docs=False, + starpilot_toggles=_test_starpilot_toggles(), + ) + + assert obd_params.networkLocation == structs.CarParams.NetworkLocation.gateway + assert obd_params.openpilotLongitudinalControl + assert obd_params.radarTimeStepDEPRECATED == pytest.approx(0.15) + assert ascm_params.networkLocation == structs.CarParams.NetworkLocation.fwdCamera + assert not ascm_params.openpilotLongitudinalControl + assert ascm_params.radarTimeStepDEPRECATED == pytest.approx(0.0667) + @parameterized.expand([ CAR.CHEVROLET_BOLT_CC_2017, CAR.CHEVROLET_BOLT_CC_2018_2021, @@ -291,6 +318,14 @@ class TestGMInterface: assert car_params.minSteerSpeed == pytest.approx(7 * CV.MPH_TO_MS) + def test_lacrosse_2019_ascm_min_steer_speed_is_28_mph(self): + car_model = CAR.BUICK_LACROSSE_ASCM_19US + CarInterface = interfaces[car_model] + car_params = CarInterface.get_params(car_model, _empty_fingerprint(), [], alpha_long=False, is_release=False, docs=False, + starpilot_toggles=_test_starpilot_toggles()) + + assert car_params.minSteerSpeed == pytest.approx(28 * CV.MPH_TO_MS) + @parameterized.expand([ ("interceptor", True), ("ascm_int", False), diff --git a/opendbc_repo/opendbc/car/hyundai/carcontroller.py b/opendbc_repo/opendbc/car/hyundai/carcontroller.py index 196a7c742..4e2262968 100644 --- a/opendbc_repo/opendbc/car/hyundai/carcontroller.py +++ b/opendbc_repo/opendbc/car/hyundai/carcontroller.py @@ -860,7 +860,9 @@ class CarController(CarControllerBase): lka_steering = self.CP.flags & HyundaiFlags.CANFD_LKA_STEERING longitudinal_active = bool(self.long_active_ecu and getattr(CC, "longActive", False)) - lfa_longitudinal_active = longitudinal_active if self.CP.carFingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN else self.CP.openpilotLongitudinalControl + lfa_status_cars = (CAR.HYUNDAI_IONIQ_6, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN) + lfa_longitudinal_active = self.CP.openpilotLongitudinalControl \ + if self.CP.carFingerprint in lfa_status_cars else longitudinal_active lka_steering_long = lka_steering and lfa_longitudinal_active ccnc_non_hda2 = self.CP.flags & HyundaiFlags.CCNC and not lka_steering use_egmp_dynamic_long_tuning = egmp_dynamic_longitudinal_tuning(self.CP) and self.long_active_ecu and \ @@ -890,7 +892,8 @@ class CarController(CarControllerBase): if angle_lkas_alt: steering_msg_active = bool(steering_msg_active and drive_gear) angle_lkas_alt_standstill_handoff = bool(getattr(CS.out, "standstill", False) and not CC.latActive) - forward_stock_lkas = self.CP.carFingerprint in CANFD_ANGLE_LONGITUDINAL_CAR and angle_lkas_alt and ( + forward_stock_lkas = (self.CP.carFingerprint in CANFD_ANGLE_LONGITUDINAL_CAR or + self.CP.carFingerprint == CAR.KIA_SPORTAGE_HEV_2026) and angle_lkas_alt and ( angle_lkas_alt_standstill_handoff or not (drive_gear and (CC.latActive or CC.enabled)) ) preserve_stock_lfa_status = preserve_stock_canfd_lfa_status(self.CP.carFingerprint) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py index 05d8c92af..e3a571adf 100644 --- a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py @@ -2484,10 +2484,11 @@ class TestHyundaiFingerprint: CP = CarParams.new_message() CP.carFingerprint = CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.EV | HyundaiFlags.CANFD_LKA_STEERING) - CP.openpilotLongitudinalControl = False + CP.openpilotLongitudinalControl = True controller = CarController(DBC[CP.carFingerprint], CP) controller.frame = 1 + controller.long_active_ecu = True can_bus = CanBus(CP) parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS", 0)], can_bus.ACAN) stock_lkas = { @@ -2529,7 +2530,6 @@ class TestHyundaiFingerprint: assert parser.vl["LKAS"]["STEER_MODE"] == 0 assert parser.vl["LKAS"]["NEW_SIGNAL_2"] == 0 - CP.openpilotLongitudinalControl = True lfa_parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LFA", 0)], can_bus.ECAN) lfa_msgs = hyundaicanfd.create_steering_messages(controller.packer, CP, can_bus, True, True, 0, 0.0) assert [(controller.packer.dbc.addr_to_msg[addr].name, bus) for addr, _, bus in lfa_msgs] == [("LFA", can_bus.ECAN), ("LKAS", can_bus.ACAN)] @@ -2537,13 +2537,12 @@ class TestHyundaiFingerprint: assert lfa_parser.can_valid assert lfa_parser.vl["LFA"]["DAMP_FACTOR"] == 100 - controller.long_active_ecu = True cc.longActive = False inactive_msgs = controller.create_canfd_msgs(0, True, 0.44, 0.0, 0.0, 0.0, False, cc.hudControl, cs, cc, get_test_toggles(), lka_icon=2, lfa_icon=2) steering_names = [(controller.packer.dbc.addr_to_msg[addr].name, bus) for addr, _, bus in inactive_msgs if controller.packer.dbc.addr_to_msg[addr].name in ("LFA", "LKAS")] - assert steering_names == [("LKAS", can_bus.ACAN)] + assert steering_names == [("LFA", can_bus.ECAN), ("LKAS", can_bus.ACAN)] controller.frame = 1 cc.longActive = True @@ -2708,7 +2707,7 @@ class TestHyundaiFingerprint: assert len([msg for msg in msgs if msg[0] == 0x110]) == expected_lkas_msgs @pytest.mark.parametrize("standstill", [False, True]) - def test_sportage_angle_lkas_alt_keeps_inactive_status_in_drive(self, standstill): + def test_sportage_angle_lkas_alt_forwards_stock_status_when_inactive(self, standstill): CP = CarParams.new_message() CP.carFingerprint = CAR.KIA_SPORTAGE_HEV_2026 CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.HYBRID | HyundaiFlags.CANFD_ANGLE_STEERING | @@ -2716,60 +2715,16 @@ class TestHyundaiFingerprint: CP.openpilotLongitudinalControl = False controller = CarController(DBC[CP.carFingerprint], CP) - can_bus = CanBus(CP) - parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS_ALT", 0)], can_bus.ACAN) - stock_lkas = { - "CHECKSUM": 1234, - "COUNTER": 42, - "LKA_OptUsmSta": 2, - "LKA_MODE": 2, - "LKA_RcgSta": 3, - "LKA_AVAILABLE": 3, - "LKA_LHLnWrnSta": 3, - "LKA_RHLnWrnSta": 3, - "LKA_WARNING": 1, - "LKA_HndsoffSnd": 1, - "LKA_StrSnd": 1, - "LKA_SysIndReq": 4, - "LKA_ICON": 2, - "FCA_SYSWARN": 1, - "StrTqReqVal": 17, - "TORQUE_REQUEST": 17, - "ActToiSta": 3, - "STEER_REQ": 1, - "ToiFltSta": 3, - "LFA_BUTTON": 1, - "LKA_SysWrn": 15, - "LKA_ASSIST": 1, - "Damping_Gain": 0, - "STEER_MODE": 5, - "NEW_SIGNAL_2": 0, - "LKAS_ANGLE_ACTIVE": 2, - "LKA_UsmMod": 3, - "HAS_LANE_SAFETY": 1, - "ADAS_StrAnglReqVal": 12.3, - "ADAS_ACIAnglTqRedcGainVal": 0.42, - "DAMP_FACTOR": 0, - } cc = SimpleNamespace(enabled=False, latActive=False, actuators=SimpleNamespace(longControlState=LongCtrlState.off), leftBlinker=False, rightBlinker=False, hudControl=SimpleNamespace()) - cs = SimpleNamespace(stock_lfa_msg=None, stock_lkas_msg=stock_lkas, + cs = SimpleNamespace(stock_lfa_msg=None, stock_lkas_msg={}, out=SimpleNamespace(standstill=standstill, steeringAngleDeg=0.0, gearShifter=structs.CarState.GearShifter.drive)) msgs = controller.create_canfd_msgs(0, False, 0.0, 0.0, 0.0, 0.0, False, cc.hudControl, cs, cc, get_test_toggles(), lka_icon=1, lfa_icon=1) - lkas_msgs = [msg for msg in msgs if msg[0] == 0x110] - assert len(lkas_msgs) == 1 - - parser.update([(1, lkas_msgs)]) - assert parser.can_valid - assert parser.vl["LKAS_ALT"]["LKA_StrSnd"] == 2 - assert parser.vl["LKAS_ALT"]["LKA_SysIndReq"] == 1 - assert parser.vl["LKAS_ALT"]["LKA_RcgSta"] == 0 - assert parser.vl["LKAS_ALT"]["LKA_AVAILABLE"] == 0 - assert parser.vl["LKAS_ALT"]["LKAS_ANGLE_ACTIVE"] == 1 + assert not [msg for msg in msgs if msg[0] in (0x110, 0x12A)] def test_ev9_inactive_angle_steering_does_not_suppress_stock_lfa(self): CP = CarParams.new_message() diff --git a/scripts/model_compiler.py b/scripts/model_compiler.py index cb181b0af..3383eb2ca 100644 --- a/scripts/model_compiler.py +++ b/scripts/model_compiler.py @@ -69,10 +69,6 @@ def build_compile_env(*, supercombo: bool = False) -> dict[str, str]: int(str(env.get(key)), 0) except (TypeError, ValueError): env[key] = default - if supercombo: - # Unified supercombo artifacts must use upstream compile defaults. The - # legacy QCOM tuning causes a reproducible HCQ timeline failure here. - env.pop("QCOM_PRIORITY", None) return env diff --git a/selfdrive/car/cruise.py b/selfdrive/car/cruise.py index 7c047b8fe..42c7f2562 100644 --- a/selfdrive/car/cruise.py +++ b/selfdrive/car/cruise.py @@ -214,7 +214,7 @@ class VCruiseHelper: engage_floor_kph = max(V_CRUISE_MIN, 7.0 * CV.MPH_TO_KPH) resume_pressed = any(b.type in (ButtonType.accelCruise, ButtonType.resumeCruise) for b in CS.buttonEvents) - remembered_resume = resume_prev_button and (self.gm_cc_only or self.redneck_non_pcm) + remembered_resume = resume_prev_button and self._uses_software_cruise() if self.v_cruise_initialized and (resume_pressed or remembered_resume): self.v_cruise_kph = self.v_cruise_kph_last diff --git a/selfdrive/car/tests/test_cruise_speed.py b/selfdrive/car/tests/test_cruise_speed.py index e5acbd538..00f8f4f27 100644 --- a/selfdrive/car/tests/test_cruise_speed.py +++ b/selfdrive/car/tests/test_cruise_speed.py @@ -313,6 +313,22 @@ class TestVCruiseHelper: assert V_CRUISE_MIN <= self.v_cruise_helper.v_cruise_kph <= V_CRUISE_MAX assert self.v_cruise_helper.v_cruise_initialized + def test_resume_keeps_previous_software_cruise_speed(self): + engage_cs = car.CarState(vEgo=75 * CV.MPH_TO_MS) + self.v_cruise_helper.initialize_v_cruise(engage_cs, experimental_mode=False, resume_prev_button=False, + starpilot_toggles=self.starpilot_toggles) + + disabled_cs = car.CarState(cruiseState={"available": True}) + self.v_cruise_helper.update_v_cruise(disabled_cs, enabled=False, is_metric=False, + speed_limit_changed=False, starpilot_toggles=self.starpilot_toggles) + + resume_cs = car.CarState(vEgo=22 * CV.MPH_TO_MS) + self.v_cruise_helper.initialize_v_cruise(resume_cs, experimental_mode=False, resume_prev_button=True, + starpilot_toggles=self.starpilot_toggles) + + assert self.v_cruise_helper.v_cruise_kph == pytest.approx(75 * CV.MPH_TO_KPH) + assert self.v_cruise_helper.v_cruise_cluster_kph == pytest.approx(75 * CV.MPH_TO_KPH) + def test_initialize_v_cruise_matches_speed_limit(self): self.reset_cruise_speed_state() self.starpilot_toggles.set_speed_limit = True diff --git a/selfdrive/controls/lib/latcontrol_vehicle_tunes.py b/selfdrive/controls/lib/latcontrol_vehicle_tunes.py index f179381ef..be4874624 100644 --- a/selfdrive/controls/lib/latcontrol_vehicle_tunes.py +++ b/selfdrive/controls/lib/latcontrol_vehicle_tunes.py @@ -275,7 +275,7 @@ GENESIS_G70_FRICTION_JERK_DEADZONE_LAT = 0.30 GENESIS_G70_FRICTION_JERK_DEADZONE_LAT_WIDTH = 0.08 GENESIS_G70_FRICTION_JERK_DEADZONE_SPEED = 12.0 GENESIS_G70_FRICTION_JERK_DEADZONE_SPEED_WIDTH = 3.5 -GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_MAX = 0.16 +GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_MAX = 0.22 GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_SPEED = 35.0 * CV.MPH_TO_MS GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_SPEED_WIDTH = 8.0 * CV.MPH_TO_MS GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_LAT = 0.35 @@ -284,7 +284,7 @@ GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_LAT_CUTOFF = 1.25 GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_LAT_CUTOFF_WIDTH = 0.25 GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_JERK = 0.20 GENESIS_G70_CURVE_UNWIND_FRICTION_JERK_DEADZONE_JERK_WIDTH = 0.12 -GENESIS_G70_CENTER_OUTPUT_TAPER_MAX = 0.22 +GENESIS_G70_CENTER_OUTPUT_TAPER_MAX = 0.26 GENESIS_G70_CENTER_OUTPUT_TAPER_LAT = 0.30 GENESIS_G70_CENTER_OUTPUT_TAPER_LAT_WIDTH = 0.10 GENESIS_G70_CENTER_OUTPUT_TAPER_SPEED = 18.0 diff --git a/selfdrive/controls/lib/longitudinal_planner.py b/selfdrive/controls/lib/longitudinal_planner.py index 01ed01f4b..c96528ff0 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/selfdrive/controls/lib/longitudinal_planner.py @@ -397,6 +397,27 @@ def get_vehicle_min_accel(CP, v_ego): return float(ACCEL_MIN) +def get_far_lead_coast_cap(lead, v_ego, desired_gap, output_a_target): + if lead is None or not bool(getattr(lead, "status", False)): + return float(output_a_target) + + v_ego = float(v_ego) + lead_distance = float(getattr(lead, "dRel", float("inf"))) + lead_speed = float(getattr(lead, "vLead", v_ego)) + closing_speed = v_ego - lead_speed + if ( + v_ego <= 10.0 or + closing_speed <= 0.5 or + lead_distance < FAR_LEAD_COAST_MIN_DISTANCE or + lead_distance <= float(desired_gap) + FAR_LEAD_COAST_MIN_GAP_MARGIN or + lead_distance / max(closing_speed, 0.1) < FAR_LEAD_COAST_MIN_TTC or + max(0.0, -float(getattr(lead, "aLeadK", 0.0))) > FAR_LEAD_COAST_MAX_LEAD_BRAKE + ): + return float(output_a_target) + + return max(float(output_a_target), -FAR_LEAD_COAST_MAX_DECEL) + + # Restored planner constants retained by CEM, stop, and departure paths. A_CRUISE_MIN = -1.0 # The stop distance runs ~9 m long through the mid-approach, which leaves the obstacle slack @@ -433,6 +454,11 @@ VEHICLE_FAR_FOLLOW_SLEW_MIN_DISTANCE_TIME = 1.35 VEHICLE_FAR_FOLLOW_SLEW_MIN_HEADWAY = 1.35 VEHICLE_FAR_FOLLOW_SLEW_MIN_TTC = 8.0 VEHICLE_FAR_FOLLOW_SLEW_MAX_LATERAL_OFFSET = 1.5 +FAR_LEAD_COAST_MIN_DISTANCE = 45.0 +FAR_LEAD_COAST_MIN_TTC = 8.0 +FAR_LEAD_COAST_MIN_GAP_MARGIN = 6.0 +FAR_LEAD_COAST_MAX_LEAD_BRAKE = 0.35 +FAR_LEAD_COAST_MAX_DECEL = 0.20 RADAR_DEPART_CONFLICT_MAX_EGO_SPEED = 1.6 RADAR_DEPART_CONFLICT_MIN_RADAR_LATERAL = 1.5 RADAR_DEPART_CONFLICT_MAX_RADAR_DISTANCE = 18.0 @@ -3069,6 +3095,28 @@ class LongitudinalPlanner: panic_bypass, ) + far_lead_coast_allowed = ( + not experimental_mode and + comfort_lead is not None and + desired_gap is not None and + not output_should_stop and + not vision_low_speed_stop_active and + not close_lead_caps and + not panic_bypass and + not depart_safety_veto and + inside_gap_closing_cap is None and + not bool(getattr(sm['starpilotPlan'], 'forcingStop', False)) and + not bool(getattr(sm['starpilotPlan'], 'redLight', False)) and + not bool(getattr(sm['starpilotPlan'], 'stopSignConfirmed', False)) + ) + if far_lead_coast_allowed: + output_a_target = get_far_lead_coast_cap( + comfort_lead, + scene_v_ego, + desired_gap, + output_a_target, + ) + if radar_gap_settle_active: output_a_target = RADAR_STANDSTILL_GAP_SETTLE_ACCEL output_should_stop = False diff --git a/selfdrive/controls/radard.py b/selfdrive/controls/radard.py index a5ae074fc..549a1ae2f 100644 --- a/selfdrive/controls/radard.py +++ b/selfdrive/controls/radard.py @@ -7,6 +7,7 @@ from typing import Any import capnp from cereal import messaging, log, car, custom +from cereal.services import SERVICE_LIST from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL, Priority, config_realtime_process @@ -42,6 +43,11 @@ def is_bosch_a_radar_car(CP) -> bool: return CP.brand == "honda" and CP.carFingerprint in HONDA_BOSCH_A and not CP.radarUnavailable +def has_slow_radar_tracks(CP) -> bool: + radar_ts = float(getattr(CP, "radarTimeStepDEPRECATED", DT_MDL) or DT_MDL) + return not CP.radarUnavailable and radar_ts > 2.0 / SERVICE_LIST["liveTracks"].frequency + + # Adjacent-lane stopped-vehicle detector, used as a stop-line hint on red-light # approaches. The qualifier is the DECELERATION HISTORY, not the current speed: roadside # furniture and curb-parked cars never show a moving -> stopped transition, so testing @@ -636,8 +642,9 @@ def main() -> None: cloudlog.info("radard got CarParams") # *** setup messaging + ignore_avg_freq = ['liveTracks'] if has_slow_radar_tracks(CP) else None sm = messaging.SubMaster(['modelV2', 'carState', 'liveTracks'], poll='modelV2', - ignore_valid=['starpilotPlan']) + ignore_avg_freq=ignore_avg_freq, ignore_valid=['starpilotPlan']) pm = messaging.PubMaster(['radarState']) radar_ts = float(getattr(CP, "radarTimeStepDEPRECATED", DT_MDL) or DT_MDL) diff --git a/selfdrive/controls/tests/test_leads.py b/selfdrive/controls/tests/test_leads.py index 651304062..144c3aa79 100644 --- a/selfdrive/controls/tests/test_leads.py +++ b/selfdrive/controls/tests/test_leads.py @@ -14,6 +14,7 @@ from openpilot.selfdrive.controls.radard import ( RadarD, g90_low_speed_radar_lead_sane, g90_radar_lead_lateral_sane, + has_slow_radar_tracks, is_bosch_a_radar_car, match_vision_to_track, ) @@ -96,6 +97,15 @@ class TestLeads: assert bosch_a.lead_prob_filters[0].dt == pytest.approx(DT_MDL) assert bosch_a.kalman_params.A[0][1] == pytest.approx(HONDA_BOSCH_A_RADAR_TS) + def test_slow_radar_frequency_relaxation_is_scoped(self): + slow_radar = SimpleNamespace(radarTimeStepDEPRECATED=0.15, radarUnavailable=False) + normal_radar = SimpleNamespace(radarTimeStepDEPRECATED=0.1, radarUnavailable=False) + unavailable_radar = SimpleNamespace(radarTimeStepDEPRECATED=0.15, radarUnavailable=True) + + assert has_slow_radar_tracks(slow_radar) + assert not has_slow_radar_tracks(normal_radar) + assert not has_slow_radar_tracks(unavailable_radar) + @pytest.mark.skipif(platform.system() == "Darwin", reason="SocketEventHandle requires eventfd") def test_radar_fault(self): # if there's no radar-related can traffic, radard should either not respond or respond with an error diff --git a/selfdrive/controls/tests/test_longitudinal_planner.py b/selfdrive/controls/tests/test_longitudinal_planner.py index 7ee9b018a..4230de142 100644 --- a/selfdrive/controls/tests/test_longitudinal_planner.py +++ b/selfdrive/controls/tests/test_longitudinal_planner.py @@ -18,7 +18,13 @@ from opendbc.car.toyota.values import CAR as TOYOTA_CAR import openpilot.selfdrive.controls.lib.longitudinal_planner as longitudinal_planner_module from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N -from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner, get_coast_accel, get_vehicle_min_accel, should_publish_planner_fcw +from openpilot.selfdrive.controls.lib.longitudinal_planner import ( + LongitudinalPlanner, + get_coast_accel, + get_far_lead_coast_cap, + get_vehicle_min_accel, + should_publish_planner_fcw, +) from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import ( LongitudinalMpc, build_model_lead_trajectory, @@ -310,6 +316,24 @@ def test_mpc_panic_bypass_immediately_removes_duplicate_vision_filter(): assert mpc.lead_v_filter.x == pytest.approx(10.0) +def test_far_lead_coast_cap_delays_nonurgent_deceleration(): + lead = make_lead(status=True, d_rel=128.0, v_lead=16.7, a_lead=0.2, radar=True) + + assert get_far_lead_coast_cap(lead, 26.6, 115.0, -0.43) == pytest.approx(-0.20) + assert get_far_lead_coast_cap(lead, 26.6, 115.0, 0.10) == pytest.approx(0.10) + + +@pytest.mark.parametrize("d_rel,v_lead,a_lead,desired_gap", [ + (50.0, 20.0, 0.2, 45.0), # only a small gap remains + (128.0, 8.0, 0.2, 115.0), # urgent closing time + (128.0, 16.7, -0.5, 115.0), # the lead is braking materially +]) +def test_far_lead_coast_cap_preserves_urgent_or_close_deceleration(d_rel, v_lead, a_lead, desired_gap): + lead = make_lead(status=True, d_rel=d_rel, v_lead=v_lead, a_lead=a_lead, radar=True) + + assert get_far_lead_coast_cap(lead, 26.6, desired_gap, -0.43) == pytest.approx(-0.43) + + def test_hrv_far_follow_output_slew_damps_only_continuous_safe_follow(): v_ego = 24.0 CP = CarInterface.get_non_essential_params(CAR.HONDA_HRV_3G) diff --git a/starpilot/common/assets/device_settings_layout.json b/starpilot/common/assets/device_settings_layout.json index 7c248e9dc..61f0a0a9e 100644 --- a/starpilot/common/assets/device_settings_layout.json +++ b/starpilot/common/assets/device_settings_layout.json @@ -201,6 +201,8 @@ "min": 0.0, "max": 99.0, "step": 1.0, + "unit_type": "vehicle_speed", + "metric_max": 150.0, "parent_key": "LaneChanges", "settings_tier": "simple" }, @@ -339,6 +341,8 @@ "min": 0.0, "max": 99.0, "step": 1.0, + "unit_type": "vehicle_speed", + "metric_max": 150.0, "parent_key": "QOLLateral", "settings_tier": "simple" }, @@ -1016,6 +1020,8 @@ "min": 0.0, "max": 99.0, "step": 1.0, + "unit_type": "vehicle_speed", + "metric_max": 150.0, "parent_key": "ConditionalExperimental", "settings_tier": "simple" }, @@ -1028,6 +1034,8 @@ "min": 0.0, "max": 99.0, "step": 1.0, + "unit_type": "vehicle_speed", + "metric_max": 150.0, "parent_key": "ConditionalExperimental", "settings_tier": "simple" }, @@ -1113,6 +1121,8 @@ "min": 0.0, "max": 99.0, "step": 1.0, + "unit_type": "vehicle_speed", + "metric_max": 150.0, "parent_key": "ConditionalExperimental", "settings_tier": "simple" }, @@ -1690,6 +1700,10 @@ "ui_type": "numeric", "min": 1.0, "max": 99.0, + "step": 1.0, + "precision": 0, + "unit_type": "vehicle_speed", + "metric_max": 150.0, "parent_key": "QOLLongitudinal", "settings_tier": "simple" }, @@ -1701,6 +1715,10 @@ "ui_type": "numeric", "min": 1.0, "max": 99.0, + "step": 1.0, + "precision": 0, + "unit_type": "vehicle_speed", + "metric_max": 150.0, "parent_key": "QOLLongitudinal", "settings_tier": "simple" }, @@ -1774,6 +1792,10 @@ "ui_type": "numeric", "min": 0.0, "max": 99.0, + "step": 1.0, + "precision": 0, + "unit_type": "vehicle_speed", + "metric_max": 150.0, "parent_key": "QOLLongitudinal", "settings_tier": "simple" }, @@ -1787,6 +1809,8 @@ "max": 30.0, "step": 0.5, "precision": 1, + "unit_type": "vehicle_speed", + "imperial_max": 15.0, "parent_key": "QOLLongitudinal", "settings_tier": "advanced" }, @@ -2223,6 +2247,12 @@ "ui_type": "numeric", "min": -99.0, "max": 99.0, + "step": 1.0, + "precision": 0, + "unit_type": "vehicle_speed", + "metric_min": -150.0, + "metric_max": 150.0, + "unit_range_index": 0, "parent_key": "SpeedLimitController", "settings_tier": "advanced" }, @@ -2234,6 +2264,12 @@ "ui_type": "numeric", "min": -99.0, "max": 99.0, + "step": 1.0, + "precision": 0, + "unit_type": "vehicle_speed", + "metric_min": -150.0, + "metric_max": 150.0, + "unit_range_index": 1, "parent_key": "SpeedLimitController", "settings_tier": "advanced" }, @@ -2245,6 +2281,12 @@ "ui_type": "numeric", "min": -99.0, "max": 99.0, + "step": 1.0, + "precision": 0, + "unit_type": "vehicle_speed", + "metric_min": -150.0, + "metric_max": 150.0, + "unit_range_index": 2, "parent_key": "SpeedLimitController", "settings_tier": "advanced" }, @@ -2256,6 +2298,12 @@ "ui_type": "numeric", "min": -99.0, "max": 99.0, + "step": 1.0, + "precision": 0, + "unit_type": "vehicle_speed", + "metric_min": -150.0, + "metric_max": 150.0, + "unit_range_index": 3, "parent_key": "SpeedLimitController", "settings_tier": "advanced" }, @@ -2267,6 +2315,12 @@ "ui_type": "numeric", "min": -99.0, "max": 99.0, + "step": 1.0, + "precision": 0, + "unit_type": "vehicle_speed", + "metric_min": -150.0, + "metric_max": 150.0, + "unit_range_index": 4, "parent_key": "SpeedLimitController", "settings_tier": "advanced" }, @@ -2278,6 +2332,12 @@ "ui_type": "numeric", "min": -99.0, "max": 99.0, + "step": 1.0, + "precision": 0, + "unit_type": "vehicle_speed", + "metric_min": -150.0, + "metric_max": 150.0, + "unit_range_index": 5, "parent_key": "SpeedLimitController", "settings_tier": "advanced" }, @@ -2289,6 +2349,12 @@ "ui_type": "numeric", "min": -99.0, "max": 99.0, + "step": 1.0, + "precision": 0, + "unit_type": "vehicle_speed", + "metric_min": -150.0, + "metric_max": 150.0, + "unit_range_index": 6, "parent_key": "SpeedLimitController", "settings_tier": "advanced" }, @@ -2341,6 +2407,8 @@ "min": 0.0, "max": 99.0, "step": 1.0, + "unit_type": "vehicle_speed", + "metric_max": 150.0, "parent_key": "ConditionalChill", "settings_tier": "advanced" }, @@ -2353,6 +2421,8 @@ "min": 0.0, "max": 99.0, "step": 1.0, + "unit_type": "vehicle_speed", + "metric_max": 150.0, "parent_key": "ConditionalChill", "settings_tier": "advanced" }, @@ -2385,6 +2455,8 @@ "min": 0.0, "max": 15.0, "step": 1.0, + "unit_type": "vehicle_speed", + "metric_max": 30.0, "parent_key": "ConditionalChill", "settings_tier": "advanced" }, @@ -2454,6 +2526,7 @@ "min": 5, "max": 80, "step": 5, + "unit_type": "vehicle_speed", "parent_key": "VisionSpeedLimitLowLimitFilter", "settings_tier": "advanced" }, @@ -4818,6 +4891,7 @@ "min": 0.0, "max": 99.0, "step": 1.0, + "unit_type": "vehicle_speed", "parent_key": "GalaxyDeveloperMode", "settings_tier": "advanced" }, diff --git a/starpilot/system/bluetooth/daemon.py b/starpilot/system/bluetooth/daemon.py index 9851bb40f..b8ad4bf48 100644 --- a/starpilot/system/bluetooth/daemon.py +++ b/starpilot/system/bluetooth/daemon.py @@ -20,6 +20,7 @@ AUDIO_TEST_HOLD_TIME = 3.0 RECONNECT_INTERVAL_SECONDS = 15.0 RECONNECT_MAX_BACKOFF_SECONDS = 300.0 MANUAL_DISCONNECT_SUPPRESSION_SECONDS = 300.0 +CONTROLLER_OFFROAD_DISCONNECT_DELAY_SECONDS = 120.0 class BluetoothController: @@ -36,6 +37,9 @@ class BluetoothController: self._last_reconnect = 0.0 self._reconnect_backoff: dict[str, tuple[int, float]] = {} self._manual_disconnect_until: dict[str, float] = {} + self._offroad_since: float | None = None + self._policy_disconnected: set[str] = set() + self._policy_disconnect_retry_after: dict[str, float] = {} self._scan_deadline = 0.0 self._audio_test_deadline = 0.0 self._sleep = sleep @@ -272,17 +276,65 @@ class BluetoothController: self._client().stop_discovery() self._scan_deadline = 0.0 + def _maintain_controller_offroad_policy(self, status: dict[str, Any], now: float) -> bool: + if not status["offroad"]: + self._offroad_since = None + if self._policy_disconnected: + for address in self._policy_disconnected: + self._reconnect_backoff.pop(address, None) + self._policy_disconnected.clear() + self._policy_disconnect_retry_after.clear() + self._last_reconnect = 0.0 + return False + + if self._offroad_since is None: + self._offroad_since = now + + if not self.params.get_bool("BluetoothDisconnectControllersOffroad"): + if self._policy_disconnected: + self._policy_disconnected.clear() + self._policy_disconnect_retry_after.clear() + self._last_reconnect = 0.0 + return False + + if now - self._offroad_since < CONTROLLER_OFFROAD_DISCONNECT_DELAY_SECONDS: + return False + + for device in status["devices"]: + if not device.get("paired") or not device.get("controller") or not device.get("connected"): + continue + address = str(device["address"]).upper() + if now < self._policy_disconnect_retry_after.get(address, 0.0): + continue + self._policy_disconnected.add(address) + self._policy_disconnect_retry_after[address] = now + RECONNECT_INTERVAL_SECONDS + try: + with self._lock: + self._client().disconnect(address) + except RuntimeError as error: + if "notconnected" not in str(error).replace(" ", "").lower(): + self._policy_disconnected.discard(address) + self._policy_disconnect_retry_after.pop(address, None) + cloudlog.warning(f"Bluetooth offroad controller disconnect failed for {address}: {error}") + except Exception as error: + self._policy_disconnected.discard(address) + self._policy_disconnect_retry_after.pop(address, None) + cloudlog.warning(f"Bluetooth offroad controller disconnect failed for {address}: {error}") + return True + def maintain_connections(self) -> None: while True: time.sleep(2) + now = time.monotonic() if not self.params.get_bool("BluetoothEnabled"): + self._maintain_controller_offroad_policy({"offroad": self._offroad(), "devices": []}, now) continue try: status = self.status() if not status["available"] or not status["powered"]: continue - now = time.monotonic() self._maintain_scan(status, now) + suspend_controller_reconnect = self._maintain_controller_offroad_policy(status, now) if self._pairing_address or now - self._last_reconnect < RECONNECT_INTERVAL_SECONDS: continue self._last_reconnect = now @@ -298,6 +350,8 @@ class BluetoothController: self._reconnect_backoff.pop(address, None) for device in candidates: if device["audio"] or device["controller"]: + if suspend_controller_reconnect and device["controller"]: + continue address = device["address"].upper() if now < self._manual_disconnect_until.get(address, 0.0): continue diff --git a/starpilot/system/bluetooth/tests/test_bluetooth.py b/starpilot/system/bluetooth/tests/test_bluetooth.py index 27cff7f0d..b1ad326ba 100644 --- a/starpilot/system/bluetooth/tests/test_bluetooth.py +++ b/starpilot/system/bluetooth/tests/test_bluetooth.py @@ -432,6 +432,48 @@ def test_scan_stops_after_timeout(): assert not client.discovering and controller._scan_deadline == 0.0 +def test_controller_offroad_disconnect_policy_is_opt_in_and_delayed(): + params = FakeParams(IsOffroad=True, BluetoothEnabled=True, BluetoothDisconnectControllersOffroad=False) + client = FakeBlueZ() + controller = BluetoothController(params, lambda: client, FakeRadio()) + controller._bluez = client + controller_status = { + "offroad": True, + "devices": [ + {**client.device, "name": "Controller", "audio": False, "controller": True, "connected": True}, + {**client.device, "address": "AA:BB:CC:DD:EE:FF", "audio": True, "controller": False, "connected": True}, + ], + } + + assert not controller._maintain_controller_offroad_policy(controller_status, 100.0) + params.put_bool("BluetoothDisconnectControllersOffroad", True) + assert not controller._maintain_controller_offroad_policy(controller_status, 219.9) + assert client.actions == [] + + assert controller._maintain_controller_offroad_policy(controller_status, 220.0) + assert client.actions == [("disconnect", client.device["address"])] + assert client.device["address"].upper() in controller._policy_disconnected + assert controller._maintain_controller_offroad_policy(controller_status, 221.0) + assert client.actions == [("disconnect", client.device["address"])] + + +def test_controller_offroad_disconnect_policy_reconnects_onroad(): + params = FakeParams(IsOffroad=True, BluetoothEnabled=True, BluetoothDisconnectControllersOffroad=True) + controller = BluetoothController(params, FakeBlueZ, FakeRadio()) + address = "00:11:22:33:44:55" + controller._offroad_since = 100.0 + controller._policy_disconnected.add(address) + controller._reconnect_backoff[address] = (3, 500.0) + controller._last_reconnect = 210.0 + + assert not controller._maintain_controller_offroad_policy({"offroad": False, "devices": []}, 220.0) + assert controller._offroad_since is None + assert controller._policy_disconnected == set() + assert controller._policy_disconnect_retry_after == {} + assert address not in controller._reconnect_backoff + assert controller._last_reconnect == 0.0 + + def test_pair_keeps_discovery_until_pair_starts(): params = FakeParams(IsOffroad=True, BluetoothEnabled=True) client = FakeBlueZ() diff --git a/starpilot/system/the_galaxy/assets/components/router.js b/starpilot/system/the_galaxy/assets/components/router.js index 8d69bc1de..d85e23547 100644 --- a/starpilot/system/the_galaxy/assets/components/router.js +++ b/starpilot/system/the_galaxy/assets/components/router.js @@ -3,7 +3,7 @@ import { createBrowserHistory, createRouter } from "/assets/vendor/remix-router- import { hideSidebar } from "/assets/js/utils.js" import { DeviceSettings } from "/assets/components/tools/device_settings.js?v=favorite-c4-hint-1" import { Bluetooth } from "/assets/components/tools/bluetooth.js?v=bluetooth-live-15" -import { WheelControls } from "/assets/components/tools/wheel_controls.js?v=controllers-2" +import { WheelControls } from "/assets/components/tools/wheel_controls.js?v=controllers-3" import { DoorControl } from "/assets/components/tools/doors.js" import { ErrorLogs } from "/assets/components/tools/error_logs.js" import { VehicleFeatures } from "/assets/components/tools/vehicle_features.js" diff --git a/starpilot/system/the_galaxy/assets/components/tools/device_settings.css b/starpilot/system/the_galaxy/assets/components/tools/device_settings.css index 074600d98..f467958fa 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/device_settings.css +++ b/starpilot/system/the_galaxy/assets/components/tools/device_settings.css @@ -342,6 +342,24 @@ margin-bottom: var(--margin-sm); } +.ds-unit-note { + align-items: center; + background: var(--input-bg); + border: var(--border-style-main); + border-radius: var(--border-radius-base); + color: var(--text-muted); + display: flex; + font-size: var(--font-size-sm); + gap: var(--gap-sm); + margin-bottom: var(--margin-base); + padding: var(--padding-sm) var(--padding-base); +} + +.ds-unit-note i, +.ds-unit-note strong { + color: var(--main-fg); +} + /* ――― Empty Filter State ――― */ .ds-empty { color: var(--text-muted); 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 53fa0c6ae..75deefe81 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/device_settings.js +++ b/starpilot/system/the_galaxy/assets/components/tools/device_settings.js @@ -1,4 +1,5 @@ import { html, reactive } from "/assets/vendor/arrow-core.js" +import { formatNumericParamValue, resolveVehicleUnitParam, vehicleSpeedUnit } from "/assets/mobile/js/params.js" const endpointOptionsCache = {} const endpointOptionsInflight = {} @@ -448,40 +449,6 @@ async function fetchLayoutAndParams() { scheduleSyncInputs() } -function formatSliderValue(val, stepStr, precisionInt, key) { - if (val === null || val === undefined) return "--" - const v = parseFloat(val) - if (Number.isNaN(v)) return val - - if (key === "SwitchbackModeCooldown") { - if (v === 0) return "Off" - return v === 1 ? "1 min" : `${v} min` - } - - if (key === "DeviceShutdown") { - return v === 1 ? "1 hour" : `${v} hours` - } - - const volumeKeys = [ - "BelowSteerSpeedVolume", "DisengageVolume", "EngageVolume", "PromptVolume", - "PromptDistractedVolume", "RefuseVolume", - "WarningImmediateVolume", "WarningSoftVolume", - ] - if (key && volumeKeys.includes(key)) { - if (v === 0) return "Muted" - if (v === 101) return "Auto" - return `${v}%` - } - - if (precisionInt !== undefined && precisionInt !== null) { - return Number(v.toFixed(precisionInt)).toString() - } - - if (!stepStr || !stepStr.includes(".")) return Math.round(v).toString() - const dec = stepStr.split(".")[1].length - return Number(v.toFixed(dec)).toString() -} - function formatReadoutValue(p) { const raw = state.values[p.key] const v = parseFloat(raw) @@ -505,6 +472,7 @@ function formatStepValue(step, precision) { } function numericBounds(param) { + param = resolveVehicleUnitParam(param, state.values) const defaultBounds = { min: param.min !== undefined ? param.min : (param.data_type === "float" ? 0.0 : 0), max: param.max !== undefined ? param.max : (param.data_type === "float" ? 100.0 : 100), @@ -952,13 +920,7 @@ function syncNumericDisplay(param, rawValue) { const displayEl = document.getElementById(`ds-display-${param.key}`) if (!displayEl) return - const bounds = numericBounds(param) - displayEl.textContent = formatSliderValue( - rawValue, - String(bounds.step), - param.precision, - param.key, - ) + displayEl.textContent = formatNumericParamValue(param, rawValue, state.values) } async function updateNumericParam(param, numericValue, options = {}) { @@ -1311,10 +1273,9 @@ function matchesFilter(p) { if (!state.filter) return true if (isGroupParam(p)) return false const q = state.filter.toLowerCase() - const label = String(p.label || "").toLowerCase() - const key = String(p.key || "").toLowerCase() - const description = String(p.description || "").toLowerCase() - return label.includes(q) || key.includes(q) || description.includes(q) + const displayParam = resolveVehicleUnitParam(p, state.values) + return [displayParam.label, displayParam.key, displayParam.description, displayParam.unit, displayParam.unit_search_terms] + .some(value => String(value || "").toLowerCase().includes(q)) } function clearSearchFilter() { @@ -1376,8 +1337,7 @@ function formatFlmValue(param, value) { if (value === undefined || value === null) return "not set" if (param.data_type === "bool") return value ? "On" : "Off" if (param.ui_type === "numeric") { - const bounds = numericBounds(param) - return formatSliderValue(value, String(bounds.step), param.precision, param.key) + return formatNumericParamValue(param, value, state.values) } return String(value) } @@ -1562,6 +1522,8 @@ function renderSettingRow(p) { return "" } + p = resolveVehicleUnitParam(p, state.values) + const isNumeric = p.ui_type === "numeric" const isSlider = isNumeric && p.control === "slider" const isText = p.ui_type === "text" @@ -1604,8 +1566,8 @@ function renderSettingRow(p) { @input="${(event) => previewSliderParam(p, event.currentTarget.value)}" @change="${(event) => commitSliderParam(p, event.currentTarget.value)}" />
- ${formatSliderValue(numericBounds(p).min, String(numericBounds(p).step), p.precision, p.key)} - ${formatSliderValue(numericBounds(p).max, String(numericBounds(p).step), p.precision, p.key)} + ${formatNumericParamValue(p, numericBounds(p).min, state.values)} + ${formatNumericParamValue(p, numericBounds(p).max, state.values)}
- ${formatSliderValue(bounds.min, String(bounds.step), p.precision, p.key)} to ${formatSliderValue(bounds.max, String(bounds.step), p.precision, p.key)} + ${formatNumericParamValue(p, bounds.min, state.values)} to ${formatNumericParamValue(p, bounds.max, state.values)} Step: ${stepLabel} per click Default: ${defaultLabel}
@@ -1790,8 +1752,7 @@ function renderSettingRow(p) { if (isColor) return formatColorDisplayValue(p) if (isReadout) return formatReadoutValue(p) const currentValue = state.sliderPreviewValues[p.key] ?? state.values[p.key] - const bounds = numericBounds(p) - return currentValue !== undefined ? formatSliderValue(currentValue, String(bounds.step), p.precision, p.key) : ".." + return currentValue !== undefined ? formatNumericParamValue(p, currentValue, state.values) : ".." }}` : ""}
@@ -1857,6 +1818,11 @@ export function DeviceSettings({ params }) {

Toggles

+
+ + Vehicle-unit speed settings use ${() => vehicleSpeedUnit(state.values)} and follow the comma's Use Metric System toggle. Each control shows its adjustment step. +
+
!state.loading && !state.available && state.mappings.length ? html`
The wheel control service is starting.
` : ""} ${() => state.testing ? testPanel() : ""} + +
Connected input devices diff --git a/starpilot/system/the_galaxy/assets/mobile/css/material.css b/starpilot/system/the_galaxy/assets/mobile/css/material.css index 17df4da62..7984686b0 100644 --- a/starpilot/system/the_galaxy/assets/mobile/css/material.css +++ b/starpilot/system/the_galaxy/assets/mobile/css/material.css @@ -592,6 +592,24 @@ ul { list-style: none; margin: 0; padding: 0; } transition: transform var(--motion-fast), box-shadow var(--motion-fast); } +.gx-unit-note { + align-items: center; + background: var(--primary-container); + border: 1px solid var(--primary); + border-radius: var(--radius-md); + color: var(--on-primary-container); + display: flex; + font-size: var(--fs-sm); + gap: var(--sp-2); + margin: 0 0 var(--sp-4); + padding: var(--sp-3) var(--sp-4); +} + +.gx-unit-note i { + color: var(--primary); + font-size: 1.2rem; +} + [data-theme="light"] .gx-card { border: 1px solid rgba(120, 73, 232, 0.22); } @@ -749,6 +767,13 @@ ul { list-style: none; margin: 0; padding: 0; } .gx-slider-row .gx-row__value { text-align: left; min-width: 0; } .gx-slider-row .gx-slider-reset { align-self: flex-end; } +.gx-slider-meta { + color: var(--text-muted); + display: flex; + font-size: var(--fs-xs); + justify-content: space-between; +} + input[type="range"].gx-slider { -webkit-appearance: none; appearance: none; @@ -1497,4 +1522,4 @@ input[type="color"].gx-color { .gx-menu-btn { display: inline-flex; } .gx-back-btn { display: none; } .gx-content { padding-bottom: var(--sp-6); } -} \ No newline at end of file +} diff --git a/starpilot/system/the_galaxy/assets/mobile/js/components/GalaxyToggleCard.js b/starpilot/system/the_galaxy/assets/mobile/js/components/GalaxyToggleCard.js index 5fd48954f..e99304857 100644 --- a/starpilot/system/the_galaxy/assets/mobile/js/components/GalaxyToggleCard.js +++ b/starpilot/system/the_galaxy/assets/mobile/js/components/GalaxyToggleCard.js @@ -1,8 +1,8 @@ import { api, showSnackbar } from "../api.js" import { - coerceValueByType, formatSliderValue, formatReadoutValue, getColorDefault, + coerceValueByType, formatNumericParamValue, formatReadoutValue, getColorDefault, normalizeHexColor, numericBounds, numericEpsilon, snapNumericToBoundsAndStep, - stepPrecision, + resolveVehicleUnitParam, stepPrecision, } from "../params.js" import { FavoritesEditor } from "./FavoritesEditor.js" @@ -12,6 +12,7 @@ export const GalaxyToggleCard = { props: { param: { type: Object, required: true }, value: { default: undefined }, + values: { type: Object, default: () => ({}) }, locked: { type: Boolean, default: false }, manageable: { type: Boolean, default: false }, manageOpen: { type: Boolean, default: false }, @@ -28,8 +29,9 @@ export const GalaxyToggleCard = { } }, computed: { - bounds() { return numericBounds(this.param, {}) }, - precision() { return stepPrecision(this.bounds.step, this.param.precision) }, + displayParam() { return resolveVehicleUnitParam(this.param, this.values) }, + bounds() { return numericBounds(this.displayParam, this.values) }, + precision() { return stepPrecision(this.bounds.step, this.displayParam.precision) }, epsilon() { return numericEpsilon(this.precision) }, isSlider() { return this.isNumeric }, isNumeric() { return this.param.ui_type === "numeric" }, @@ -38,11 +40,17 @@ export const GalaxyToggleCard = { currentValue() { return this.preview !== undefined ? this.preview : this.value }, displayValue() { if (this.isColor) return normalizeHexColor(this.value) ? normalizeHexColor(this.value).toUpperCase() : "Stock" - if (this.isReadout) return formatReadoutValue(this.param, this.value) - return this.value !== undefined && this.value !== null ? formatSliderValue(this.value, String(this.bounds.step), this.param.precision, this.param.key) : ".." + if (this.isReadout) return formatReadoutValue(this.displayParam, this.value) + return this.value !== undefined && this.value !== null ? formatNumericParamValue(this.displayParam, this.value, this.values) : ".." }, sliderDisplay() { - return this.value !== undefined ? formatSliderValue(this.currentValue, String(this.bounds.step), this.param.precision, this.param.key) : ".." + return this.value !== undefined ? formatNumericParamValue(this.displayParam, this.currentValue, this.values) : ".." + }, + sliderRangeDisplay() { + return `${formatNumericParamValue(this.displayParam, this.bounds.min, this.values)} to ${formatNumericParamValue(this.displayParam, this.bounds.max, this.values)}` + }, + sliderStepDisplay() { + return formatNumericParamValue(this.displayParam, this.bounds.step, this.values) }, isColor() { return this.param.ui_type === "color" }, isAction() { return this.param.ui_type === "action" }, @@ -167,10 +175,10 @@ export const GalaxyToggleCard = {
- {{ param.label }} - Advanced + {{ displayParam.label }} + Advanced - {{ param.description }} + {{ displayParam.description }}
Locked: This setting can only be changed while parked.
@@ -190,6 +198,10 @@ export const GalaxyToggleCard = { :value="currentValue" :disabled="locked || updating" @input="onSliderInput" @change="onSliderCommit" @blur="onSliderBlur" @touchstart="beginInteract" @mousedown="beginInteract" @keydown="beginInteract" /> +
+ {{ sliderRangeDisplay }} + Step: {{ sliderStepDisplay }} +
diff --git a/starpilot/system/the_galaxy/assets/mobile/js/components/ParamSections.js b/starpilot/system/the_galaxy/assets/mobile/js/components/ParamSections.js index d94b8b2fc..5827d7a85 100644 --- a/starpilot/system/the_galaxy/assets/mobile/js/components/ParamSections.js +++ b/starpilot/system/the_galaxy/assets/mobile/js/components/ParamSections.js @@ -1,5 +1,5 @@ import { api } from "../api.js" -import { isSettingVisible, slugifySectionName, applyParamChange } from "../params.js" +import { isSettingVisible, resolveVehicleUnitParam, slugifySectionName, applyParamChange } from "../params.js" import { SettingTree } from "./SettingTree.js" import { GalaxySection } from "./GalaxySection.js" @@ -35,7 +35,9 @@ export const ParamSections = { matches(p) { if (!this.search) return true const q = this.search.toLowerCase() - return [p.label, p.key, p.description].some((v) => String(v || "").toLowerCase().includes(q)) + const displayParam = resolveVehicleUnitParam(p, this.values) + return [displayParam.label, displayParam.key, displayParam.description, displayParam.unit, displayParam.unit_search_terms] + .some((v) => String(v || "").toLowerCase().includes(q)) }, async load() { try { diff --git a/starpilot/system/the_galaxy/assets/mobile/js/components/SettingTree.js b/starpilot/system/the_galaxy/assets/mobile/js/components/SettingTree.js index 7d32b8915..8c95c4dee 100644 --- a/starpilot/system/the_galaxy/assets/mobile/js/components/SettingTree.js +++ b/starpilot/system/the_galaxy/assets/mobile/js/components/SettingTree.js @@ -30,7 +30,7 @@ export const SettingTree = { template: `