From ebb096a9562b79ec0f75bc29885e9debabce7a96 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Fri, 28 Aug 2026 13:45:11 -0500 Subject: [PATCH] estate sale --- opendbc_repo/opendbc/car/ford/carstate.py | 76 ++++++++++++++- .../opendbc/car/ford/tests/test_ford.py | 18 ++++ opendbc_repo/opendbc/car/hyundai/values.py | 1 + opendbc_repo/opendbc/car/interfaces.py | 1 + .../opendbc/car/subaru/carcontroller.py | 34 +++++-- .../opendbc/car/subaru/tests/test_subaru.py | 22 ++++- .../opendbc/car/tests/test_car_interfaces.py | 1 + .../opendbc/safety/modes/hyundai_common.h | 12 ++- opendbc_repo/opendbc/safety/tests/common.py | 2 + .../opendbc/safety/tests/test_hyundai.py | 26 +++++ scripts/model_compiler.py | 5 +- selfdrive/controls/lib/latcontrol_pid.py | 8 ++ .../controls/lib/latcontrol_vehicle_tunes.py | 23 +++++ .../lib/longitudinal_vehicle_tunes.py | 17 +++- selfdrive/controls/tests/test_latcontrol.py | 30 ++++++ .../tests/test_longitudinal_planner.py | 44 ++++++++- .../controls/tests/test_starpilot_planner.py | 1 + selfdrive/ui/mici/onroad/model_renderer.py | 5 +- .../ui/mici/tests/test_lead_indicator.py | 33 +++++++ starpilot/assets/download_functions.py | 21 ++-- starpilot/assets/model_manager.py | 86 +++++++++++------ starpilot/assets/tests/test_model_pipeline.py | 18 +++- starpilot/assets/theme_manager.py | 96 ++++++++++--------- .../common/starpilot_download_utilities.py | 20 ++-- starpilot/starpilot_process.py | 2 +- 25 files changed, 492 insertions(+), 110 deletions(-) create mode 100644 selfdrive/ui/mici/tests/test_lead_indicator.py diff --git a/opendbc_repo/opendbc/car/ford/carstate.py b/opendbc_repo/opendbc/car/ford/carstate.py index a7dea6506..061a6f2f9 100644 --- a/opendbc_repo/opendbc/car/ford/carstate.py +++ b/opendbc_repo/opendbc/car/ford/carstate.py @@ -209,7 +209,79 @@ class CarState(CarStateBase): def get_can_parsers(CP): gps_config = get_car_gps_config(CP) gps_messages = [(name, 0) for name in gps_config.messages] if gps_config is not None else [] + + pt_messages = [ + ("BrakeSysFeatures", 50), + ("Yaw_Data_FD1", 100), + ("DesiredTorqBrk", 50), + ("EngVehicleSpThrottle", 100), + ("EngVehicleSpThrottle2", 50), + ("BrakeSnData_4", 50), + ("EngBrakeData", 10), + ("EPAS_INFO", 50), + ("Cluster_Info1_FD1", 10), + ("Steering_Data_FD1", 10), + ("BodyInfo_3_FD1", 2), + ("RCMStatusMessage2_FD1", 10), + ("BCM_Lamp_Stat_FD1", 0), + *gps_messages, + ] + + if CP.flags & FordFlags.ALT_STEER_ANGLE: + pt_messages += [ + ("SteeringPinion_Data_Alt", 100), + ("ParkAid_Data", 50), + ] + else: + pt_messages += [("SteeringPinion_Data", 100)] + + if CP.flags & FordFlags.CANFD: + pt_messages += [ + ("Lane_Assist_Data3_FD1", 33), + ("Cluster_Info_3_FD1", 10), + ] + else: + pt_messages += [("INSTRUMENT_PANEL", 1)] + + if CP.transmissionType == TransmissionType.automatic: + if CP.flags & FordFlags.CANFD: + pt_messages += [("Gear_Shift_by_Wire_FD1", 10)] + elif CP.flags & FordFlags.ALT_STEER_ANGLE: + pt_messages += [("TransGearData", 10)] + else: + pt_messages += [("PowertrainData_10", 10)] + + if CP.enableBsm and not (CP.flags & FordFlags.CANFD): + pt_messages += [ + ("Side_Detect_L_Stat", 5), + ("Side_Detect_R_Stat", 5), + ] + + cam_messages = [ + ("ACCDATA", 50), + ("ACCDATA_2", 50), + ("ACCDATA_3", 5), + ("IPMA_Data", 1), + ] + + if CP.flags & FordFlags.CANFD: + cam_messages += [ + ("Traffic_RecognitnData", 1), + ("IPMA_Data2", 1), + ] + else: + cam_messages += [("Traffic_RecognitnData", 0)] + + if CP.enableBsm and CP.flags & FordFlags.CANFD: + cam_messages += [ + ("Side_Detect_L_Stat", 5), + ("Side_Detect_R_Stat", 5), + ] + + if CP.flags & FordFlags.LKA_STEERING: + cam_messages += [("LateralMotionControl", 20)] + return { - Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], gps_messages, CanBus(CP).main), - Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus(CP).camera), + Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, CanBus(CP).main), + Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], cam_messages, CanBus(CP).camera), } diff --git a/opendbc_repo/opendbc/car/ford/tests/test_ford.py b/opendbc_repo/opendbc/car/ford/tests/test_ford.py index a93b94ee3..1b7762932 100644 --- a/opendbc_repo/opendbc/car/ford/tests/test_ford.py +++ b/opendbc_repo/opendbc/car/ford/tests/test_ford.py @@ -4,6 +4,7 @@ from types import SimpleNamespace from hypothesis import settings, given, strategies as st from parameterized import parameterized +import pytest from opendbc.car import Bus, gen_empty_fingerprint from opendbc.can import CANPacker @@ -274,6 +275,23 @@ def test_mach_e_can_gps_messages_are_optional_main_bus_inputs(): assert all(parser.message_states[address].ignore_alive for address in (0x462, 0x463, 0x464)) +def test_lightning_low_rate_camera_messages_use_declared_frequencies(): + cp = CarInterface.get_params(CAR.FORD_F_150_LIGHTNING_MK1, gen_empty_fingerprint(), [], True, False, False, None) + cp.enableBsm = True + parser = CarInterface.CarState.get_can_parsers(cp)[Bus.cam] + + expected_frequencies = { + "IPMA_Data": 1, + "Traffic_RecognitnData": 1, + "Side_Detect_L_Stat": 5, + "Side_Detect_R_Stat": 5, + } + for message, frequency in expected_frequencies.items(): + state = parser.message_states[parser.dbc.name_to_msg[message].address] + assert state.frequency == frequency + assert state.timeout_threshold == pytest.approx(10e9 / frequency) + + def test_hands_free_cluster_status_is_opt_in(): packer = CANPacker("ford_lincoln_base_pt") CAN = SimpleNamespace(main=0) diff --git a/opendbc_repo/opendbc/car/hyundai/values.py b/opendbc_repo/opendbc/car/hyundai/values.py index 5f55b5a49..8ded9ecf5 100644 --- a/opendbc_repo/opendbc/car/hyundai/values.py +++ b/opendbc_repo/opendbc/car/hyundai/values.py @@ -111,6 +111,7 @@ class HyundaiSafetyFlags(IntFlag): class HyundaiStarPilotSafetyFlags(IntFlag): + AOL_MAIN_LKAS_ON_ENGAGE = 128 AOL_MAIN_LKAS_SYNC = 32 HAS_LDA_BUTTON = 1024 AOL_LKAS_ON_ENGAGE = 2048 diff --git a/opendbc_repo/opendbc/car/interfaces.py b/opendbc_repo/opendbc/car/interfaces.py index 77f3171d7..48c1f1b2d 100644 --- a/opendbc_repo/opendbc/car/interfaces.py +++ b/opendbc_repo/opendbc/car/interfaces.py @@ -260,6 +260,7 @@ class CarInterfaceBase(ABC): if candidate == HYUNDAI.HYUNDAI_ELANTRA_HEV_2024 and \ getattr(starpilot_toggles, "always_on_lateral_main", False): fp_ret.safetyConfigs[-1].safetyParam |= HyundaiStarPilotSafetyFlags.AOL_LKAS_ON_ENGAGE.value + fp_ret.safetyConfigs[-1].safetyParam |= HyundaiStarPilotSafetyFlags.AOL_MAIN_LKAS_ON_ENGAGE.value # LKASButtonControl == 9 means BUTTON_FUNCTIONS["AOL_TOGGLE"] in starpilot_variables. if params.get_bool("AlwaysOnLateral") and params.get_int("LKASButtonControl") == 9: diff --git a/opendbc_repo/opendbc/car/subaru/carcontroller.py b/opendbc_repo/opendbc/car/subaru/carcontroller.py index 88c2131ce..2f76b5085 100644 --- a/opendbc_repo/opendbc/car/subaru/carcontroller.py +++ b/opendbc_repo/opendbc/car/subaru/carcontroller.py @@ -22,6 +22,7 @@ _LEGACY_2025_REENGAGE_MAX_STEER_RATE = 2.0 _LEGACY_2025_REENGAGE_MAX_ANGLE_DELTA = 1.0 _LEGACY_2025_RECLAIM_FRAMES = 36 _LEGACY_2025_RECLAIM_EXPONENT = 2.5 +_ANGLE_OVERRIDE_CONFIRM_FRAMES = 2 _ANGLE_OVERRIDE_HOLD_FRAMES = 10 _ANGLE_REENGAGE_SETTLE_FRAMES = 8 _ANGLE_REENGAGE_MAX_STEER_RATE = 2.0 @@ -47,6 +48,7 @@ class CarController(CarControllerBase): self.apply_torque_last = 0 self.apply_steer_last = 0 self.driver_override = False + self.angle_override_confirm_frames = 0 self.legacy_2025_lkas_active = False self.legacy_2025_handoff_active = False self.legacy_2025_override_hold_frames = 0 @@ -139,6 +141,8 @@ class CarController(CarControllerBase): return msg def _reset_legacy_2025_handoff(self): + self.driver_override = False + self.angle_override_confirm_frames = 0 self.legacy_2025_handoff_active = False self.legacy_2025_override_hold_frames = 0 self.legacy_2025_reengage_settle_frames = 0 @@ -151,7 +155,8 @@ class CarController(CarControllerBase): self._reset_legacy_2025_handoff() return False - if getattr(CS.out, "steeringPressed", False): + driver_override = self._update_angle_driver_override(CS) + if driver_override: self.legacy_2025_handoff_active = True self.legacy_2025_override_hold_frames = _LEGACY_2025_OVERRIDE_HOLD_FRAMES self.legacy_2025_reengage_settle_frames = 0 @@ -202,6 +207,8 @@ class CarController(CarControllerBase): return target_angle def _reset_angle_handoff(self): + self.driver_override = False + self.angle_override_confirm_frames = 0 self.angle_handoff_active = False self.angle_override_hold_frames = 0 self.angle_reengage_settle_frames = 0 @@ -214,7 +221,8 @@ class CarController(CarControllerBase): self._reset_angle_handoff() return False - if getattr(CS.out, "steeringPressed", False): + driver_override = self._update_angle_driver_override(CS) + if driver_override: self.angle_handoff_active = True self.angle_override_hold_frames = _ANGLE_OVERRIDE_HOLD_FRAMES self.angle_reengage_settle_frames = 0 @@ -253,6 +261,22 @@ class CarController(CarControllerBase): self.angle_reclaim_start_angle = CS.out.steeringAngleDeg return True + def _update_angle_driver_override(self, CS): + """Debounce the higher-confidence raw torque override signal for angle cars.""" + abs_torque = abs(getattr(CS.out, "steeringTorque", 0.0)) + if self.driver_override: + if abs_torque < self.p.STEER_OVERRIDE_TORQUE_LOW: + self.driver_override = False + elif abs_torque > self.p.STEER_OVERRIDE_TORQUE_HIGH: + self.angle_override_confirm_frames += 1 + if self.angle_override_confirm_frames >= _ANGLE_OVERRIDE_CONFIRM_FRAMES: + self.driver_override = True + self.angle_override_confirm_frames = 0 + else: + self.angle_override_confirm_frames = 0 + + return self.driver_override + def _angle_reclaim_target(self, target_angle): if self.angle_reclaim_frames <= 0: return target_angle @@ -325,12 +349,6 @@ class CarController(CarControllerBase): self.angle_lkas_active = lkas_active return subarucan.create_steering_control_angle(self.packer, apply_steer, lkas_active, self.angle_bus) - abs_torque = abs(CS.out.steeringTorque) - if abs_torque > self.p.STEER_OVERRIDE_TORQUE_HIGH: - self.driver_override = True - elif abs_torque < self.p.STEER_OVERRIDE_TORQUE_LOW: - self.driver_override = False - mads_only = CC.latActive and not getattr(CC, "enabled", False) mads_only_ok = CS.out.vEgoRaw > _ANGLE_MADS_MIN_SPEED and \ abs(CS.out.steeringAngleDeg) < _ANGLE_MADS_MAX_STEER_ANGLE diff --git a/opendbc_repo/opendbc/car/subaru/tests/test_subaru.py b/opendbc_repo/opendbc/car/subaru/tests/test_subaru.py index 802d30bb6..ee68dd0a4 100644 --- a/opendbc_repo/opendbc/car/subaru/tests/test_subaru.py +++ b/opendbc_repo/opendbc/car/subaru/tests/test_subaru.py @@ -351,6 +351,7 @@ def test_legacy_2025_waits_for_manual_steering_to_settle_before_reengaging(): vEgoRaw=6.2, steeringAngleDeg=-121.55, steeringRateDeg=350.0, + steeringTorque=250.0, steeringPressed=True, gearShifter=structs.CarState.GearShifter.drive, standstill=False, @@ -359,10 +360,14 @@ def test_legacy_2025_waits_for_manual_steering_to_settle_before_reengaging(): msg = controller.lateral_angle(CC, CS) parser.update([(1, [msg])]) + + msg = controller.lateral_angle(CC, CS) + parser.update([(2, [msg])]) assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0 assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(CS.out.steeringAngleDeg) CS.out.steeringPressed = False + CS.out.steeringTorque = 0.0 CS.out.steeringAngleDeg = -113.78 msg = controller.lateral_angle(CC, CS) parser.update([(2, [msg])]) @@ -411,6 +416,7 @@ def test_legacy_2025_manual_handoff_reclaim_is_gradual(): vEgoRaw=3.7, steeringAngleDeg=2.5, steeringRateDeg=-45.0, + steeringTorque=250.0, steeringPressed=True, gearShifter=structs.CarState.GearShifter.drive, standstill=False, @@ -419,9 +425,13 @@ def test_legacy_2025_manual_handoff_reclaim_is_gradual(): msg = controller.lateral_angle(CC, CS) parser.update([(1, [msg])]) + + msg = controller.lateral_angle(CC, CS) + parser.update([(2, [msg])]) assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0 CS.out.steeringPressed = False + CS.out.steeringTorque = 0.0 CS.out.steeringRateDeg = 0.0 for i in range(19): msg = controller.lateral_angle(CC, CS) @@ -480,6 +490,10 @@ def test_angle_controller_tracks_driver_override(): msg = controller.lateral_angle(CC, CS) + assert not controller.driver_override + + msg = controller.lateral_angle(CC, CS) + assert controller.driver_override assert controller.p.STEER_OVERRIDE_TORQUE_HIGH == 150 assert controller.p.STEER_OVERRIDE_TORQUE_LOW == 100 @@ -541,7 +555,7 @@ def test_ascent_angle_controller_uses_fixed_angle_rate_limits(): vEgoRaw=21.66, steeringAngleDeg=-25.77, steeringRateDeg=0.0, - steeringTorque=-149.0, + steeringTorque=-250.0, steeringPressed=False, gearShifter=structs.CarState.GearShifter.drive, standstill=False, @@ -563,7 +577,7 @@ def test_angle_controller_yields_until_manual_steering_settles(platform): vEgoRaw=21.66, steeringAngleDeg=-25.06, steeringRateDeg=35.0, - steeringTorque=-149.0, + steeringTorque=-250.0, steeringPressed=True, gearShifter=structs.CarState.GearShifter.drive, standstill=False, @@ -572,10 +586,14 @@ def test_angle_controller_yields_until_manual_steering_settles(platform): msg = controller.lateral_angle(CC, CS) parser.update([(1, [msg])]) + + msg = controller.lateral_angle(CC, CS) + parser.update([(2, [msg])]) assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0 assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(CS.out.steeringAngleDeg) CS.out.steeringPressed = False + CS.out.steeringTorque = 0.0 CS.out.steeringAngleDeg = -17.91 CS.out.steeringRateDeg = 0.0 for i in range(18): diff --git a/opendbc_repo/opendbc/car/tests/test_car_interfaces.py b/opendbc_repo/opendbc/car/tests/test_car_interfaces.py index ef0f85764..f2f5f5346 100644 --- a/opendbc_repo/opendbc/car/tests/test_car_interfaces.py +++ b/opendbc_repo/opendbc/car/tests/test_car_interfaces.py @@ -323,6 +323,7 @@ class TestCarInterfaces: ) assert fp_car_params.safetyConfigs[-1].safetyParam & HyundaiStarPilotSafetyFlags.AOL_LKAS_ON_ENGAGE.value + assert fp_car_params.safetyConfigs[-1].safetyParam & HyundaiStarPilotSafetyFlags.AOL_MAIN_LKAS_ON_ENGAGE.value def test_toyota_disable_openpilot_long_sets_stock_long_safety_flag(self): CarInterface = interfaces[TOYOTA_CAR.TOYOTA_PRIUS_TSS2] diff --git a/opendbc_repo/opendbc/safety/modes/hyundai_common.h b/opendbc_repo/opendbc/safety/modes/hyundai_common.h index 5487a9765..4d0cc32df 100644 --- a/opendbc_repo/opendbc/safety/modes/hyundai_common.h +++ b/opendbc_repo/opendbc/safety/modes/hyundai_common.h @@ -51,6 +51,9 @@ bool hyundai_has_lda_button = false; extern bool hyundai_aol_lkas_on_engage; bool hyundai_aol_lkas_on_engage = false; +extern bool hyundai_aol_main_lkas_on_engage; +bool hyundai_aol_main_lkas_on_engage = false; + extern bool hyundai_non_scc; bool hyundai_non_scc = false; @@ -78,6 +81,7 @@ void hyundai_common_init(uint16_t param) { const uint16_t HYUNDAI_PARAM_ALT_LIMITS_2 = 512; const int HYUNDAI_PARAM_HAS_LDA_BUTTON = 1024; + const uint16_t HYUNDAI_PARAM_AOL_MAIN_LKAS_ON_ENGAGE = 128; const uint16_t HYUNDAI_PARAM_AOL_LKAS_ON_ENGAGE = 2048; const uint16_t HYUNDAI_PARAM_NON_SCC = 4096; const uint16_t HYUNDAI_PARAM_CAN_CANFD_BLENDED = 8192; @@ -94,6 +98,7 @@ void hyundai_common_init(uint16_t param) { hyundai_can_canfd_blended = GET_FLAG(param, HYUNDAI_PARAM_CAN_CANFD_BLENDED); hyundai_has_lda_button = GET_FLAG(param, HYUNDAI_PARAM_HAS_LDA_BUTTON); + hyundai_aol_main_lkas_on_engage = GET_FLAG(param, HYUNDAI_PARAM_AOL_MAIN_LKAS_ON_ENGAGE); hyundai_aol_lkas_on_engage = GET_FLAG(param, HYUNDAI_PARAM_AOL_LKAS_ON_ENGAGE); hyundai_non_scc = GET_FLAG(param, HYUNDAI_PARAM_NON_SCC); hyundai_cancel_button_enable = GET_FLAG(param, HYUNDAI_PARAM_CANCEL_BTN_ENABLE); @@ -165,7 +170,12 @@ void hyundai_common_cruise_buttons_check(const int cruise_button, const bool mai if (main_button && !main_button_prev) { if (!hyundai_aol_main_lkas_sync) { - acc_main_on = !acc_main_on; + const bool main_turning_on = !acc_main_on; + acc_main_on = main_turning_on; + if (main_turning_on && hyundai_aol_main_lkas_on_engage && + ((alternative_experience & ALT_EXP_ALWAYS_ON_LATERAL) != 0)) { + lkas_on = true; + } } } main_button_prev = main_button; diff --git a/opendbc_repo/opendbc/safety/tests/common.py b/opendbc_repo/opendbc/safety/tests/common.py index 40eb80bd9..70e955006 100644 --- a/opendbc_repo/opendbc/safety/tests/common.py +++ b/opendbc_repo/opendbc/safety/tests/common.py @@ -1102,6 +1102,7 @@ class SafetyTest(SafetyTestBase): continue if {attr, current_test}.issubset({'TestHyundaiLongitudinalSafety', 'TestHyundaiLongitudinalSafetyCameraSCC', 'TestHyundaiSafetyFCEVLong', 'TestHyundaiLongitudinalAolLkasOnEngageSafety', + 'TestHyundaiLongitudinalAolMainLkasOnEngageSafety', 'TestHyundaiSafetyCanRefreshLong', 'TestHyundaiSafetyCanRefreshLongCameraSCC', 'TestHyundaiCanCanfdBlendedLongitudinalSafety', 'TestHyundaiLegacyLongitudinalSafety', @@ -1157,6 +1158,7 @@ class SafetyTest(SafetyTestBase): if attr.startswith('TestHyundaiLongitudinal') or attr in ('TestHyundaiSafetyFCEVLong', 'TestHyundaiLongitudinalAolLkasOnEngageSafety', + 'TestHyundaiLongitudinalAolMainLkasOnEngageSafety', 'TestHyundaiCanCanfdBlendedLongitudinalSafety', 'TestHyundaiLegacyLongitudinalSafety', 'TestHyundaiLegacyLongitudinalSafetyHEV'): diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai.py b/opendbc_repo/opendbc/safety/tests/test_hyundai.py index 29d07ee43..aa6e7570d 100755 --- a/opendbc_repo/opendbc/safety/tests/test_hyundai.py +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai.py @@ -622,6 +622,32 @@ class TestHyundaiLongitudinalAolLkasOnEngageSafety(HyundaiAolLkasOnEngageBase, T self.safety.init_tests() +class TestHyundaiLongitudinalAolMainLkasOnEngageSafety(TestHyundaiLongitudinalSafety): + def setUp(self): + self.packer = CANPackerSafety("hyundai_kia_generic") + self.safety = libsafety_py.libsafety + self.safety.set_safety_hooks( + CarParams.SafetyModel.hyundai, + HyundaiSafetyFlags.LONG | HyundaiStarPilotSafetyFlags.AOL_MAIN_LKAS_ON_ENGAGE, + ) + self.safety.init_tests() + + def test_aol_lkas_auto_enables_on_main_engagement(self): + self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL) + self.safety.set_controls_allowed(False) + + self._rx(self._button_msg(Buttons.NONE, main_button=1)) + self._rx(self._button_msg(Buttons.NONE, main_button=0)) + self.assertTrue(self.safety.get_acc_main_on()) + self.assertTrue(self.safety.get_lkas_on()) + self.assertTrue(self.safety.get_aol_allowed()) + + self._rx(self._user_brake_msg(True)) + self.assertFalse(self.safety.get_controls_allowed()) + self._set_prev_torque(0) + self.assertTrue(self._tx(self._torque_cmd_msg(self.MAX_RATE_UP))) + + class TestHyundaiAolLkasOnEngageStockSafety(HyundaiAolLkasOnEngageStockBase, TestHyundaiSafety): def setUp(self): self.packer = CANPackerSafety("hyundai_kia_generic") diff --git a/scripts/model_compiler.py b/scripts/model_compiler.py index 4d0285f88..34475d722 100644 --- a/scripts/model_compiler.py +++ b/scripts/model_compiler.py @@ -558,10 +558,13 @@ def compile_driving( command += ["--behavior-version", version] compile_env = build_compile_env(supercombo=input_format == "supercombo") if external_gpu: + gpu_debug = os.environ.get("STARPILOT_GPU_DEBUG", "1") + if gpu_debug not in {"1", "2"}: + gpu_debug = "1" for qcom_only_flag in ("IMAGE", "NOLOCALS", "OPENPILOT_HACKS"): compile_env.pop(qcom_only_flag, None) compile_env.update({ - "DEBUG": "1", + "DEBUG": gpu_debug, "DEV": "USB+AMD:LLVM", "WARP_DEV": "QCOM", "FLOAT16": "1", diff --git a/selfdrive/controls/lib/latcontrol_pid.py b/selfdrive/controls/lib/latcontrol_pid.py index 73c37684d..c5141a301 100644 --- a/selfdrive/controls/lib/latcontrol_pid.py +++ b/selfdrive/controls/lib/latcontrol_pid.py @@ -8,6 +8,7 @@ from openpilot.selfdrive.controls.lib.latcontrol import LatControl from openpilot.selfdrive.controls.lib.latcontrol_vehicle_tunes import ( RAV4_TSS2_CARS, SUBARU_IMPREZA_CARS, + get_honda_crv_5g_pid_output, get_rav4_tss2_pid_output, get_subaru_impreza_pid_output_scale, ) @@ -103,6 +104,7 @@ class LatControlPID(LatControl): self.honda_lateral_pid_kp_scale = 1.0 self.honda_lateral_pid_ki_scale = 1.0 self.is_civic_bosch_modified = CP.carFingerprint == HONDA.HONDA_CIVIC_BOSCH and bool(CP.flags & HondaFlags.EPS_MODIFIED) + self.is_honda_crv_5g = CP.carFingerprint == HONDA.HONDA_CRV_5G self.is_subaru_impreza = CP.carFingerprint in SUBARU_IMPREZA_CARS self.is_rav4_tss2 = CP.carFingerprint in RAV4_TSS2_CARS self.prev_angle_steers_des_no_offset = 0.0 @@ -171,6 +173,12 @@ class LatControlPID(LatControl): output_torque = raw_output_torque * get_subaru_impreza_pid_output_scale(error) output_torque = float(max(min(output_torque, self.steer_max), -self.steer_max)) + if self.is_honda_crv_5g: + output_torque = get_honda_crv_5g_pid_output( + output_torque, self.prev_output_torque, angle_steers_des_no_offset, CS.vEgo, + ) + output_torque = float(max(min(output_torque, self.steer_max), -self.steer_max)) + if self.is_rav4_tss2: output_torque = get_rav4_tss2_pid_output(output_torque, self.prev_output_torque, angle_steers_des_no_offset, CS.vEgo) diff --git a/selfdrive/controls/lib/latcontrol_vehicle_tunes.py b/selfdrive/controls/lib/latcontrol_vehicle_tunes.py index 4f91b9534..8cde01a2a 100644 --- a/selfdrive/controls/lib/latcontrol_vehicle_tunes.py +++ b/selfdrive/controls/lib/latcontrol_vehicle_tunes.py @@ -1176,6 +1176,14 @@ RAV4_TSS2_PID_CENTER_ANGLE = 14.0 RAV4_TSS2_PID_CENTER_ANGLE_WIDTH = 3.0 RAV4_TSS2_PID_OUTPUT_SCALE_MIN = 0.62 RAV4_TSS2_PID_OUTPUT_ALPHA_MIN = 0.28 + +HONDA_CRV_5G_PID_LOW_SPEED = 18.0 * CV.MPH_TO_MS +HONDA_CRV_5G_PID_LOW_SPEED_WIDTH = 3.0 * CV.MPH_TO_MS +HONDA_CRV_5G_PID_CENTER_ANGLE = 14.0 +HONDA_CRV_5G_PID_CENTER_ANGLE_WIDTH = 3.0 +HONDA_CRV_5G_PID_OUTPUT_SCALE_MIN = 0.62 +HONDA_CRV_5G_PID_OUTPUT_ALPHA_MIN = 0.28 + RAV4_TSS2_CENTER_FRICTION_THRESHOLD_GAIN = 0.14 RAV4_TSS2_CENTER_FRICTION_LAT = 0.30 RAV4_TSS2_CENTER_FRICTION_LAT_WIDTH = 0.08 @@ -1808,6 +1816,21 @@ def get_rav4_tss2_pid_output(output_torque: float, prev_output_torque: float, return float(prev_output_torque + output_alpha * (limited_output - prev_output_torque)) +def get_honda_crv_5g_pid_output(output_torque: float, prev_output_torque: float, + desired_angle_deg: float, v_ego: float) -> float: + """Damp low-speed CR-V 5G center reversals without blunting real turns.""" + speed_weight = _sigmoid((HONDA_CRV_5G_PID_LOW_SPEED - max(v_ego, 0.0)) / + HONDA_CRV_5G_PID_LOW_SPEED_WIDTH) + center_weight = _sigmoid((HONDA_CRV_5G_PID_CENTER_ANGLE - abs(desired_angle_deg)) / + HONDA_CRV_5G_PID_CENTER_ANGLE_WIDTH) + envelope = speed_weight * center_weight + + output_scale = 1.0 - ((1.0 - HONDA_CRV_5G_PID_OUTPUT_SCALE_MIN) * envelope) + output_alpha = 1.0 - ((1.0 - HONDA_CRV_5G_PID_OUTPUT_ALPHA_MIN) * envelope) + limited_output = output_torque * output_scale + return float(prev_output_torque + output_alpha * (limited_output - prev_output_torque)) + + def _rav4_tss2_center_envelope(desired_lateral_accel: float, v_ego: float) -> float: speed_weight = _sigmoid((RAV4_TSS2_CENTER_SPEED - max(v_ego, 0.0)) / RAV4_TSS2_CENTER_SPEED_WIDTH) diff --git a/selfdrive/controls/lib/longitudinal_vehicle_tunes.py b/selfdrive/controls/lib/longitudinal_vehicle_tunes.py index 6ed420054..e626faa40 100644 --- a/selfdrive/controls/lib/longitudinal_vehicle_tunes.py +++ b/selfdrive/controls/lib/longitudinal_vehicle_tunes.py @@ -3,8 +3,10 @@ import numpy as np HONDA_HRV_3G_FAR_FOLLOW_BRAKE_SLEW_RATE = 3.0 HONDA_HRV_3G_FAR_FOLLOW_RELEASE_SLEW_RATE = 2.0 -HONDA_CRV_5G_FAR_FOLLOW_BRAKE_SLEW_RATE = 2.5 -HONDA_CRV_5G_FAR_FOLLOW_RELEASE_SLEW_RATE = 1.75 +HONDA_CRV_5G_FAR_FOLLOW_BRAKE_SLEW_RATE = 1.5 +HONDA_CRV_5G_FAR_FOLLOW_RELEASE_SLEW_RATE = 1.0 +HONDA_ACCORD_FAR_FOLLOW_BRAKE_SLEW_RATE = 2.0 +HONDA_ACCORD_FAR_FOLLOW_RELEASE_SLEW_RATE = 1.5 HONDA_HRV_3G_UNTRACKED_SLOW_LEAD_DECEL_SCALE = 1.35 HONDA_ACCORD_LEAD_DEPART_ACCEL_HOLD_MAX_ACCEL = 0.85 HONDA_ACCORD_LEAD_DEPART_ACCEL_ASSIST = 0.25 @@ -19,6 +21,7 @@ HONDA_ACCORD_STOP_GO_MIN_MODEL_PROB = 0.95 HONDA_ACCORD_STOP_GO_ACCEL_RISE_RATE = 4.0 HYUNDAI_ELANTRA_LEAD_FOLLOW_JERK_SCALE = 1.25 GENESIS_GV70_ELECTRIFIED_LEAD_FOLLOW_JERK_SCALE = 1.35 +FORD_LIGHTNING_LEAD_FOLLOW_JERK_SCALE = 1.20 GM_SILVERADO_EARLY_FOLLOW_MIN_EGO_SPEED = 18.0 GM_SILVERADO_EARLY_FOLLOW_MAX_DISTANCE = 130.0 GM_SILVERADO_EARLY_FOLLOW_MIN_MODEL_PROB = 0.85 @@ -33,6 +36,7 @@ FORD_LIGHTNING_FAR_FOLLOW_BRAKE_SLEW_RATE = 2.5 FORD_LIGHTNING_FAR_FOLLOW_RELEASE_SLEW_RATE = 1.75 FORD_LIGHTNING_STANDSTILL_GUARD_DISTANCE_MARGIN = 5.0 FORD_LIGHTNING_STANDSTILL_GUARD_MAX_LEAD_SPEED = 0.60 +FORD_LIGHTNING_GAP_SETTLE_MAX_EXTRA_GAP = 3.0 TOYOTA_SIENNA_POST_DEPARTURE_RESTOP_MAX_EGO_SPEED = 2.0 TOYOTA_SIENNA_POST_DEPARTURE_RESTOP_MAX_LEAD_SPEED = 0.45 TOYOTA_SIENNA_POST_DEPARTURE_RESTOP_MAX_LEAD_DELTA = 0.35 @@ -254,6 +258,8 @@ def allow_honda_crv_5g_vision_gap_settle(CP): def get_standstill_gap_settle_max_extra_gap(CP): if is_honda_crv_5g(CP): return HONDA_CRV_5G_GAP_SETTLE_MAX_EXTRA_GAP + if is_ford_f150_lightning(CP): + return FORD_LIGHTNING_GAP_SETTLE_MAX_EXTRA_GAP return 1.5 @@ -387,6 +393,11 @@ def allow_radar_standstill_gap_settle(CP): def get_far_follow_output_slew_rates(CP): + if CP.brand == "honda" and str(CP.carFingerprint) == "HONDA_ACCORD": + return ( + HONDA_ACCORD_FAR_FOLLOW_BRAKE_SLEW_RATE, + HONDA_ACCORD_FAR_FOLLOW_RELEASE_SLEW_RATE, + ) if CP.brand == "honda" and str(CP.carFingerprint) == "HONDA_HRV_3G": return ( HONDA_HRV_3G_FAR_FOLLOW_BRAKE_SLEW_RATE, @@ -425,6 +436,8 @@ def get_lead_follow_jerk_scale(CP): str(getattr(CP, "carFingerprint", "")) == "GENESIS_GV70_ELECTRIFIED_1ST_GEN" ): return GENESIS_GV70_ELECTRIFIED_LEAD_FOLLOW_JERK_SCALE + if is_ford_f150_lightning(CP): + return FORD_LIGHTNING_LEAD_FOLLOW_JERK_SCALE return 1.0 diff --git a/selfdrive/controls/tests/test_latcontrol.py b/selfdrive/controls/tests/test_latcontrol.py index e46e9a39e..52a9afc4b 100644 --- a/selfdrive/controls/tests/test_latcontrol.py +++ b/selfdrive/controls/tests/test_latcontrol.py @@ -27,6 +27,7 @@ from openpilot.selfdrive.controls.lib.latcontrol_pid import ( LatControlPID, get_civic_bosch_modified_pid_output_alpha, get_civic_bosch_modified_pid_output_scale, + get_honda_crv_5g_pid_output, ) from openpilot.selfdrive.controls.lib.latcontrol_vehicle_tunes import ( clear_flm_runtime_overrides, @@ -1901,6 +1902,35 @@ class TestLatControl: assert abs(large_turn) > abs(low_speed) assert highway > low_speed + def test_honda_crv_5g_pid_output_damps_low_speed_center_reversals(self): + low_speed = get_honda_crv_5g_pid_output(1.0, -1.0, 4.0, 8.0 * 0.44704) + large_turn = get_honda_crv_5g_pid_output(1.0, -1.0, 24.0, 8.0 * 0.44704) + highway = get_honda_crv_5g_pid_output(1.0, -1.0, 4.0, 25.0 * 0.44704) + + assert abs(low_speed) < 0.50 + assert abs(large_turn) > abs(low_speed) + assert highway > low_speed + + def test_honda_crv_5g_pid_output_update_path(self, monkeypatch): + controller, VM, CS, params, starpilot_toggles = self._build_pid_controller(HONDA.HONDA_CRV_5G) + CS.vEgo = 8.0 * 0.44704 + CS.steeringAngleDeg = 4.0 + tuned_output, _, lac_log = controller.update( + True, CS, VM, params, False, 0.0, False, 0.2, None, None, starpilot_toggles, + ) + + monkeypatch.setattr(latcontrol_pid, "get_honda_crv_5g_pid_output", lambda output, *_args: output) + base_controller, base_VM, base_CS, base_params, base_toggles = self._build_pid_controller(HONDA.HONDA_CRV_5G) + base_CS.vEgo = 8.0 * 0.44704 + base_CS.steeringAngleDeg = 4.0 + base_output, _, _ = base_controller.update( + True, base_CS, base_VM, base_params, False, 0.0, False, 0.2, None, None, base_toggles, + ) + + assert controller.is_honda_crv_5g + assert lac_log.active + assert abs(tuned_output) < abs(base_output) + def test_rav4_tss2_torque_center_tune_fades_before_real_turns(self): low_speed_center = get_rav4_tss2_center_output_scale(0.05, 8.0) low_speed_turn = get_rav4_tss2_center_output_scale(1.0, 8.0) diff --git a/selfdrive/controls/tests/test_longitudinal_planner.py b/selfdrive/controls/tests/test_longitudinal_planner.py index ab69f3f11..0a6241078 100644 --- a/selfdrive/controls/tests/test_longitudinal_planner.py +++ b/selfdrive/controls/tests/test_longitudinal_planner.py @@ -329,8 +329,8 @@ def test_crv_far_follow_output_slew_damps_nonurgent_lead_transition(): planner.lead_two = make_lead(status=False) brake_rate, release_rate = get_far_follow_output_slew_rates(CP) - assert brake_rate == pytest.approx(2.5) - assert release_rate == pytest.approx(1.75) + assert brake_rate == pytest.approx(1.5) + assert release_rate == pytest.approx(1.0) initial = planner.get_vehicle_far_follow_slew_target( v_ego, prev_target=0.0, target=-0.6, output_should_stop=False, panic_bypass=False, @@ -343,6 +343,45 @@ def test_crv_far_follow_output_slew_damps_nonurgent_lead_transition(): assert smoothed == pytest.approx(initial + release_rate * planner.dt) +def test_accord_far_follow_output_slew_damps_nonurgent_radar_transition(): + v_ego = 24.0 + CP = CarInterface.get_non_essential_params(CAR.HONDA_ACCORD) + planner = LongitudinalPlanner(CP, init_v=v_ego) + planner.lead_one = make_lead(status=True, d_rel=58.0, v_lead=20.0, radar=True, model_prob=0.99) + planner.lead_two = make_lead(status=False) + + brake_rate, release_rate = get_far_follow_output_slew_rates(CP) + assert brake_rate == pytest.approx(2.0) + assert release_rate == pytest.approx(1.5) + + initial = planner.get_vehicle_far_follow_slew_target( + v_ego, prev_target=0.0, target=-0.6, output_should_stop=False, panic_bypass=False, + ) + smoothed = planner.get_vehicle_far_follow_slew_target( + v_ego, prev_target=initial, target=0.4, output_should_stop=False, panic_bypass=False, + ) + + assert initial == pytest.approx(-0.6) + assert smoothed == pytest.approx(initial + release_rate * planner.dt) + + +@pytest.mark.parametrize("output_should_stop,panic_bypass", [(True, False), (False, True)]) +def test_accord_far_follow_output_slew_bypasses_urgent_scenes(output_should_stop, panic_bypass): + CP = CarInterface.get_non_essential_params(CAR.HONDA_ACCORD) + planner = LongitudinalPlanner(CP, init_v=24.0) + planner.lead_one = make_lead(status=True, d_rel=58.0, v_lead=20.0, radar=True, model_prob=0.99) + planner.lead_two = make_lead(status=False) + planner.far_follow_output_slew_active = True + + target = planner.get_vehicle_far_follow_slew_target( + 24.0, prev_target=0.4, target=-1.0, + output_should_stop=output_should_stop, panic_bypass=panic_bypass, + ) + + assert target == pytest.approx(-1.0) + assert not planner.far_follow_output_slew_active + + @pytest.mark.parametrize("d_rel,v_lead,output_should_stop,panic_bypass", [ (20.0, 20.0, False, False), (35.0, 18.0, False, False), @@ -755,6 +794,7 @@ def test_lightning_stopped_lead_guard_tune_is_vehicle_specific(): assert get_standstill_stopped_lead_guard_distance_margin(civic) == pytest.approx(3.0) assert get_standstill_stopped_lead_guard_max_lead_speed(lightning, 0.45) == pytest.approx(0.60) assert get_standstill_stopped_lead_guard_max_lead_speed(civic, 0.45) == pytest.approx(0.45) + assert get_standstill_gap_settle_max_extra_gap(lightning) == pytest.approx(3.0) assert get_tracked_lead_catchup_headway_margins(lightning) == pytest.approx((0.10, 0.25)) assert get_tracked_lead_catchup_bias_gain(lightning) == pytest.approx(0.65) assert get_tracked_lead_catchup_headway_margins(civic) is None diff --git a/selfdrive/controls/tests/test_starpilot_planner.py b/selfdrive/controls/tests/test_starpilot_planner.py index cf4ef8081..f27727c57 100644 --- a/selfdrive/controls/tests/test_starpilot_planner.py +++ b/selfdrive/controls/tests/test_starpilot_planner.py @@ -43,6 +43,7 @@ def test_force_stop_jerk_scale_is_platform_specific(): def test_lead_follow_jerk_scale_is_platform_specific(): assert get_lead_follow_jerk_scale(SimpleNamespace(brand="hyundai", carFingerprint="HYUNDAI_ELANTRA_2021")) == 1.25 assert get_lead_follow_jerk_scale(SimpleNamespace(brand="hyundai", carFingerprint="GENESIS_GV70_ELECTRIFIED_1ST_GEN")) == 1.35 + assert get_lead_follow_jerk_scale(SimpleNamespace(brand="ford", carFingerprint="FORD_F_150_LIGHTNING_MK1")) == 1.20 assert get_lead_follow_jerk_scale(SimpleNamespace(brand="other", carFingerprint="OTHER_CAR")) == 1.0 diff --git a/selfdrive/ui/mici/onroad/model_renderer.py b/selfdrive/ui/mici/onroad/model_renderer.py index 619443ca0..a1da04202 100644 --- a/selfdrive/ui/mici/onroad/model_renderer.py +++ b/selfdrive/ui/mici/onroad/model_renderer.py @@ -136,7 +136,7 @@ class ModelRenderer(Widget): model = sm['modelV2'] radar_state = sm['radarState'] if sm.valid['radarState'] else None lead_one = radar_state.leadOne if radar_state else None - render_lead_indicator = self._longitudinal_control and radar_state is not None and lead_indicator_enabled(self._params, hide_by_default=True) + render_lead_indicator = self._should_render_lead_indicator(radar_state) # Update model data when needed model_updated = sm.updated['modelV2'] @@ -161,6 +161,9 @@ class ModelRenderer(Widget): if render_lead_indicator and radar_state: self._draw_lead_indicator() + def _should_render_lead_indicator(self, radar_state) -> bool: + return radar_state is not None and lead_indicator_enabled(self._params, hide_by_default=True) + def _update_raw_points(self, model): """Update raw 3D points from model data""" self._path.raw_points = np.array([model.position.x, model.position.y, model.position.z], dtype=np.float32).T diff --git a/selfdrive/ui/mici/tests/test_lead_indicator.py b/selfdrive/ui/mici/tests/test_lead_indicator.py new file mode 100644 index 000000000..e617cbdbd --- /dev/null +++ b/selfdrive/ui/mici/tests/test_lead_indicator.py @@ -0,0 +1,33 @@ +from types import SimpleNamespace + +import openpilot.selfdrive.ui.mici.onroad.model_renderer as model_renderer + + +class _FakeParams: + def __init__(self, enabled: bool): + self.enabled = enabled + + def get(self, key): + assert key == "HideLeadMarker" + return b"0" if self.enabled else b"1" + + def get_bool(self, key): + assert key == "HideLeadMarker" + return not self.enabled + + +def test_lead_indicator_renders_in_aol_without_longitudinal_control(monkeypatch): + monkeypatch.setattr(model_renderer, "ui_state", SimpleNamespace(always_on_lateral_active=True)) + renderer = object.__new__(model_renderer.ModelRenderer) + renderer._params = _FakeParams(enabled=True) + renderer._longitudinal_control = False + + assert renderer._should_render_lead_indicator(SimpleNamespace()) + + +def test_lead_indicator_still_honors_disabled_setting(): + renderer = object.__new__(model_renderer.ModelRenderer) + renderer._params = _FakeParams(enabled=False) + + assert not renderer._should_render_lead_indicator(SimpleNamespace()) + assert not renderer._should_render_lead_indicator(None) diff --git a/starpilot/assets/download_functions.py b/starpilot/assets/download_functions.py index 07f34a4fd..df817335a 100644 --- a/starpilot/assets/download_functions.py +++ b/starpilot/assets/download_functions.py @@ -8,12 +8,10 @@ import urllib.parse from datetime import datetime from pathlib import Path +from openpilot.starpilot.common.starpilot_download_utilities import HF_BUCKET_URL, GITHUB_URL from openpilot.starpilot.common.starpilot_utilities import delete_file, is_url_pingable RESOURCES_REPO = os.getenv("STARPILOT_RESOURCES_REPO", "firestar5683/StarPilot-Resources") -GITLAB_RESOURCES_REPO = os.getenv("STARPILOT_GITLAB_RESOURCES_REPO", "firestar5683/FrogPilot-Resources") -GITHUB_URL = f"https://raw.githubusercontent.com/{RESOURCES_REPO}" -GITLAB_URL = f"https://gitlab.com/{GITLAB_RESOURCES_REPO}/-/raw" LFS_POINTER_PREFIX = b"version https://git-lfs.github.com/spec/v1\n" MAX_MULTIPART_FILES = 100 @@ -204,12 +202,19 @@ def get_remote_file_size(url, suppress_errors=False): handle_request_error(error, None, None, None, None) return 0 +def get_resource_urls(): + """Return resource origins in priority order; GitHub is the only fallback.""" + urls = [] + if is_url_pingable("https://huggingface.co"): + urls.append(HF_BUCKET_URL) + if is_url_pingable("https://github.com") or is_url_pingable("https://api.github.com"): + urls.append(GITHUB_URL) + return urls + + def get_repository_url(): - if is_url_pingable("https://github.com"): - if check_github_rate_limit(): - return GITHUB_URL - if is_url_pingable("https://gitlab.com"): - return GITLAB_URL + for url in get_resource_urls(): + return url return None def handle_error(destination, error_message, error, download_param, progress_param, params_memory): diff --git a/starpilot/assets/model_manager.py b/starpilot/assets/model_manager.py index dd1e31aa7..a6540b789 100644 --- a/starpilot/assets/model_manager.py +++ b/starpilot/assets/model_manager.py @@ -5,12 +5,12 @@ import re import urllib.request from pathlib import Path +from urllib.parse import quote from openpilot.starpilot.assets.download_functions import ( - GITLAB_URL, download_file, download_multipart_file, - get_repository_url, + get_resource_urls, handle_error, handle_request_error, verify_download, @@ -461,21 +461,41 @@ class ModelManager: handle_request_error(error, None, None, None, None) return [] - def _get_manifest(self, repo_url: str) -> tuple[str | None, list[dict]]: + @staticmethod + def _is_huggingface_url(url: str) -> bool: + return "huggingface.co/buckets/" in url + + @staticmethod + def _hf_manifest_paths(manifest_version: str) -> tuple[str, ...]: + return ( + f"model_names_{manifest_version}.json", + f"manifests/model_names_{manifest_version}.json", + ) + + def _get_manifest(self, resource_urls: str | list[str]) -> tuple[str | None, list[dict]]: + if isinstance(resource_urls, str): + resource_urls = [resource_urls] + for manifest_version in MANIFEST_CANDIDATES: - for manifest_path in self._manifest_paths(manifest_version): - model_info = self._fetch_manifest(f"{repo_url}/{manifest_path}") - if not model_info: - continue + for resource_url in resource_urls: + manifest_paths = ( + self._hf_manifest_paths(manifest_version) + if self._is_huggingface_url(resource_url) + else self._manifest_paths(manifest_version) + ) + for manifest_path in manifest_paths: + model_info = self._fetch_manifest(f"{resource_url}/{manifest_path}") + if not model_info: + continue - filtered = [ - model for model in model_info - if is_supported_artifact_format(model.get("artifact_format")) - ] - if not filtered: - continue + filtered = [ + model for model in model_info + if is_supported_artifact_format(model.get("artifact_format")) + ] + if not filtered: + continue - return manifest_version, filtered + return manifest_version, filtered return None, [] @@ -659,12 +679,12 @@ class ModelManager: if self.downloading_model: return - repo_url = get_repository_url() - if repo_url is None: - print("GitHub and GitLab are offline...") + resource_urls = get_resource_urls() + if not resource_urls: + print("Hugging Face and GitHub are offline...") return - manifest_version, model_info = self._get_manifest(repo_url) + manifest_version, model_info = self._get_manifest(resource_urls) if not model_info: print("No compatible tinygrad manifest found.") return @@ -713,9 +733,9 @@ class ModelManager: self.downloading_model = False return - repo_url = get_repository_url() - if not repo_url: - handle_error(None, "GitHub and GitLab are offline...", "Repository unavailable", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM, self.params_memory) + resource_urls = get_resource_urls() + if not resource_urls: + handle_error(None, "Hugging Face and GitHub are offline...", "Repository unavailable", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM, self.params_memory) self.downloading_model = False return @@ -742,12 +762,18 @@ class ModelManager: if custom_url: candidate_urls.append((custom_url, True, False)) - file_url = f"{repo_url}/Models/{filename}" - candidate_urls.append((file_url, False, True)) + for resource_url in resource_urls: + if self._is_huggingface_url(resource_url): + artifact_urls_for_source = [ + f"{resource_url}/models/{quote(self._canonical_model_key(model_to_download), safe='')}/{filename}", + f"{resource_url}/{filename}", + ] + else: + artifact_urls_for_source = [f"{resource_url}/Models/{filename}"] - fallback_url = f"{GITLAB_URL}/Models/{filename}" - if fallback_url != file_url: - candidate_urls.append((fallback_url, False, True)) + for artifact_url in artifact_urls_for_source: + if not any(existing[0] == artifact_url for existing in candidate_urls): + candidate_urls.append((artifact_url, False, True)) download_succeeded = False for candidate_url, allow_unknown_size, allow_multipart in candidate_urls: @@ -805,12 +831,12 @@ class ModelManager: self.params_memory.remove(ALLOW_GPU_DOWNLOAD_WITHOUT_GPU_PARAM) def _download_all_models(self, allow_gpu_without_gpu: bool): - repo_url = get_repository_url() - if not repo_url: - handle_error(None, "GitHub and GitLab are offline...", "Repository unavailable", MODEL_DOWNLOAD_ALL_PARAM, DOWNLOAD_PROGRESS_PARAM, self.params_memory) + resource_urls = get_resource_urls() + if not resource_urls: + handle_error(None, "Hugging Face and GitHub are offline...", "Repository unavailable", MODEL_DOWNLOAD_ALL_PARAM, DOWNLOAD_PROGRESS_PARAM, self.params_memory) return - manifest_version, model_info = self._get_manifest(repo_url) + manifest_version, model_info = self._get_manifest(resource_urls) if not model_info: handle_error(None, "Unable to fetch models...", "Model list unavailable", MODEL_DOWNLOAD_ALL_PARAM, DOWNLOAD_PROGRESS_PARAM, self.params_memory) return diff --git a/starpilot/assets/tests/test_model_pipeline.py b/starpilot/assets/tests/test_model_pipeline.py index 16d2183be..863e41a4d 100644 --- a/starpilot/assets/tests/test_model_pipeline.py +++ b/starpilot/assets/tests/test_model_pipeline.py @@ -23,6 +23,22 @@ def test_v24_manifest_is_loaded_from_models_checkout(): assert ModelManager._manifest_paths("v24") == ("Models/model_names_v24.json",) +def test_resource_sources_prefer_huggingface_then_github(monkeypatch): + monkeypatch.setattr(download_functions, "is_url_pingable", lambda url: True) + assert download_functions.get_resource_urls() == [ + download_functions.HF_BUCKET_URL, + download_functions.GITHUB_URL, + ] + assert all("gitlab" not in url for url in download_functions.get_resource_urls()) + + +def test_huggingface_manifest_has_root_and_manifests_fallbacks(): + assert ModelManager._hf_manifest_paths("v24") == ( + "model_names_v24.json", + "manifests/model_names_v24.json", + ) + + def test_old_manifest_ids_resolve_to_v23_namespace(): manager = object.__new__(ModelManager) manager.available_models = ["pop223", "tr14223"] @@ -36,7 +52,7 @@ def test_model_cleanup_matches_legacy_split_artifacts(): assert model_manager.is_driving_artifact_file("driving_vision_tinygrad.pkl") assert model_manager.is_driving_artifact_file("driving_off_policy_tinygrad.pkl.p00") assert not model_manager.is_driving_artifact_file("dmonitoring_model_tinygrad.pkl") - assert not model_manager.is_driving_artifact_file("local-test_driving_tinygrad.pkl") + assert model_manager.is_driving_artifact_file("local-test_driving_tinygrad.pkl") def test_behavior_version_does_not_control_artifact_layout(): diff --git a/starpilot/assets/theme_manager.py b/starpilot/assets/theme_manager.py index 140a75840..f19cf262e 100644 --- a/starpilot/assets/theme_manager.py +++ b/starpilot/assets/theme_manager.py @@ -12,9 +12,8 @@ import zipfile from datetime import date, timedelta from dateutil import easter from pathlib import Path -from urllib.parse import quote_plus -from openpilot.starpilot.common.starpilot_download_utilities import GITHUB_URL, GITLAB_URL, download_file, get_repository_url, handle_error, verify_download +from openpilot.starpilot.common.starpilot_download_utilities import HF_BUCKET, GITHUB_URL, download_file, get_resource_urls, handle_error, verify_download from openpilot.starpilot.common.theme_asset_names import find_matching_theme_asset_file, find_matching_theme_asset_name from openpilot.starpilot.common.starpilot_utilities import delete_file, extract_zip, load_json_file, update_json_file from openpilot.starpilot.common.starpilot_variables import ACTIVE_THEME_PATH, RANDOM_EVENTS_PATH, RESOURCES_REPO, THEME_SAVE_PATH @@ -219,44 +218,49 @@ class ThemeManager: def download_theme(self, theme_component, theme_name, asset_param, starpilot_toggles): self.downloading_theme = True - repo_url = get_repository_url(self.session) - if not repo_url: - handle_error(None, asset_param, "Repository unavailable", "GitHub and GitLab are offline...", self.params_memory, DOWNLOAD_PROGRESS_PARAM) + resource_urls = get_resource_urls(self.session) + if not resource_urls: + handle_error(None, asset_param, "Repository unavailable", "Hugging Face and GitHub are offline...", self.params_memory, DOWNLOAD_PROGRESS_PARAM) self.downloading_theme = False return - alternate_url = GITLAB_URL if "raw.githubusercontent" in repo_url else GITHUB_URL - primary_source = "GitLab" if "gitlab" in repo_url else "GitHub" - if theme_component == "boot_logos": - download_link = f"{repo_url}/Themes/bootlogo" download_path = THEME_SAVE_PATH / "bootlogos" / theme_name extensions = [".png", ".jpg", ".jpeg"] name_candidates = list(dict.fromkeys([theme_name, theme_name.replace("_", "-"), theme_name.replace("-", "_")])) elif theme_component == "distance_icons": - download_link = f"{repo_url}/Distance-Icons/{theme_name}" download_path = THEME_SAVE_PATH / "theme_packs" / theme_name / theme_component extensions = [".zip"] name_candidates = [theme_name] elif theme_component == "steering_wheels": - download_link = f"{repo_url}/Steering-Wheels/{theme_name}" download_path = THEME_SAVE_PATH / theme_component / theme_name extensions = [".gif", ".png"] name_candidates = [theme_name] else: - download_link = f"{repo_url}/Themes/{theme_name}/{theme_component}" download_path = THEME_SAVE_PATH / "theme_packs" / theme_name / theme_component extensions = [".zip"] name_candidates = [theme_name] for extension in extensions: theme_path = download_path.with_suffix(extension) - theme_urls = [f"{download_link}/{candidate}{extension}" for candidate in name_candidates] if theme_component == "boot_logos" else [download_link + extension] + theme_urls = [] + for resource_url in resource_urls: + source_prefix = f"{resource_url}/theme" if "huggingface.co/buckets/" in resource_url else resource_url + if theme_component == "boot_logos": + path_prefix = f"{source_prefix}/Themes/bootlogo" + theme_urls.extend(f"{path_prefix}/{candidate}{extension}" for candidate in name_candidates) + elif theme_component == "distance_icons": + theme_urls.append(f"{source_prefix}/Distance-Icons/{theme_name}{extension}") + elif theme_component == "steering_wheels": + theme_urls.append(f"{source_prefix}/Steering-Wheels/{theme_name}{extension}") + else: + theme_urls.append(f"{source_prefix}/Themes/{theme_name}/{theme_component}{extension}") for theme_url in theme_urls: delete_file(theme_path) - print(f"Downloading theme from {primary_source}: {theme_name}") + source = "Hugging Face" if "huggingface.co/buckets/" in theme_url else "GitHub" + print(f"Downloading theme from {source}: {theme_name}") download_file(CANCEL_DOWNLOAD_PARAM, theme_path, asset_param, self.params_memory, DOWNLOAD_PROGRESS_PARAM, self.session, theme_url) if self.params_memory.get_bool(CANCEL_DOWNLOAD_PARAM): @@ -267,7 +271,7 @@ class ThemeManager: return if verify_download(theme_path, self.params_memory, self.session, theme_url): - print(f"Theme {theme_name} downloaded and verified successfully from {primary_source}!") + print(f"Theme {theme_name} downloaded and verified successfully from {source}!") self.update_theme_size(theme_component, theme_name, theme_path.stat().st_size) if extension == ".zip": @@ -282,21 +286,34 @@ class ThemeManager: self.update_themes(starpilot_toggles) return - if self.handle_verification_failure(extension, theme_component, theme_name, asset_param, theme_path, download_path, starpilot_toggles, alternate_url): - return - handle_error(download_path, asset_param, "Download failed...", "Download failed...", self.params_memory, DOWNLOAD_PROGRESS_PARAM) self.downloading_theme = False def fetch_assets(self, repo_url, starpilot_toggles): is_github = "github" in repo_url - is_gitlab = "gitlab" in repo_url - - repo_encoded = quote_plus(RESOURCES_REPO) + is_huggingface = "huggingface.co/buckets/" in repo_url assets = {"boot_logos": [], "themes": {}, "wheels": []} try: def list_files(branch): + if is_huggingface: + response = self.session.get(f"https://huggingface.co/api/buckets/{HF_BUCKET}/tree?recursive=true", timeout=10) + response.raise_for_status() + prefix = { + "Themes": "theme/Themes/", + "Distance-Icons": "theme/Distance-Icons/", + "Steering-Wheels": "theme/Steering-Wheels/", + }[branch] + return [ + { + "path": item.get("path", "")[len(prefix):], + "name": Path(item.get("path", "")).name, + "type": item.get("type"), + "size": item.get("size", 0), + } + for item in response.json() + if item.get("type") == "file" and item.get("path", "").startswith(prefix) + ] if is_github: response = self.session.get(f"https://api.github.com/repos/{RESOURCES_REPO}/git/trees/{branch}?recursive=1", timeout=10) response.raise_for_status() @@ -310,27 +327,12 @@ class ThemeManager: for item in response.json().get("tree", []) if item.get("type") == "blob" ] - if is_gitlab: - response = self.session.get(f"https://gitlab.com/api/v4/projects/{repo_encoded}/repository/tree?ref={branch}&recursive=true", timeout=10) - response.raise_for_status() - return [ - { - "path": item.get("path", ""), - "name": item.get("name", ""), - "type": item.get("type"), - "size": 0, - } - for item in response.json() - if item.get("type") in ("blob", "file") - ] print(f"Unsupported repository URL: {repo_url}") return [] def file_size(branch, path, fallback): - if is_github: + if is_github or is_huggingface: return int(fallback or 0) - response = self.session.head(f"https://gitlab.com/api/v4/projects/{repo_encoded}/repository/files/{quote_plus(path)}/raw?ref={branch}", timeout=10) - return int(response.headers.get("content-length", 0)) if response.ok else 0 for branch in ["Distance-Icons", "Steering-Wheels"]: for item in list_files(branch): @@ -402,7 +404,8 @@ class ThemeManager: return assets except requests.exceptions.RequestException as error: - print(f"Failed to fetch theme sizes from {'GitHub' if is_github else 'GitLab'}: {error}") + source = "Hugging Face" if is_huggingface else "GitHub" + print(f"Failed to fetch theme sizes from {source}: {error}") return {} @staticmethod @@ -465,9 +468,8 @@ class ThemeManager: "christmas_week": date(year, 12, 25) } - def handle_verification_failure(self, extension, theme_component, theme_name, asset_param, theme_path, download_path, starpilot_toggles, fallback_url=GITLAB_URL): - is_github = "raw.githubusercontent" in fallback_url - source = "GitHub" if is_github else "GitLab" + def handle_verification_failure(self, extension, theme_component, theme_name, asset_param, theme_path, download_path, starpilot_toggles, fallback_url=GITHUB_URL): + source = "GitHub" if theme_component == "boot_logos": download_link = f"{fallback_url}/Themes/bootlogo" @@ -750,13 +752,17 @@ class ThemeManager: self.sync_local_resources() - repo_url = get_repository_url(self.session) - if repo_url is None: - print("GitHub and GitLab are offline...") + resource_urls = get_resource_urls(self.session) + if not resource_urls: + print("Hugging Face and GitHub are offline...") self.update_theme_params([], [], [], [], [], [], []) return - assets = self.fetch_assets(repo_url, starpilot_toggles) + assets = {} + for repo_url in resource_urls: + assets = self.fetch_assets(repo_url, starpilot_toggles) + if assets: + break if not assets: return diff --git a/starpilot/common/starpilot_download_utilities.py b/starpilot/common/starpilot_download_utilities.py index a73d10be2..a86753fc9 100644 --- a/starpilot/common/starpilot_download_utilities.py +++ b/starpilot/common/starpilot_download_utilities.py @@ -7,9 +7,9 @@ from datetime import datetime, timezone from openpilot.starpilot.common.starpilot_utilities import delete_file, is_url_pingable from openpilot.starpilot.common.starpilot_variables import RESOURCES_REPO -GITLAB_RESOURCES_REPO = os.getenv("STARPILOT_GITLAB_RESOURCES_REPO", "firestar5683/FrogPilot-Resources") +HF_BUCKET = os.getenv("STARPILOT_HF_BUCKET", "firestar4430/StarPilot-Resources") +HF_BUCKET_URL = f"https://huggingface.co/buckets/{HF_BUCKET}/resolve" GITHUB_URL = f"https://raw.githubusercontent.com/{RESOURCES_REPO}" -GITLAB_URL = f"https://gitlab.com/{GITLAB_RESOURCES_REPO}/-/raw" def download_file(cancel_param, destination, download_param, params_memory, progress_param, session, url, offset_bytes=0, total_bytes=0): try: @@ -85,13 +85,21 @@ def get_remote_file_size(params_memory, session, url): def get_repository_url(session): - if (is_url_pingable("https://github.com") or is_url_pingable("https://api.github.com")) and not github_rate_limited(session): - return GITHUB_URL - if is_url_pingable("https://gitlab.com"): - return GITLAB_URL + for url in get_resource_urls(session): + return url return None +def get_resource_urls(session): + """Return resource origins in priority order; GitHub is the only fallback.""" + urls = [] + if is_url_pingable("https://huggingface.co"): + urls.append(HF_BUCKET_URL) + if is_url_pingable("https://github.com") or is_url_pingable("https://api.github.com"): + urls.append(GITHUB_URL) + return urls + + def github_rate_limited(session): try: response = session.get("https://api.github.com/rate_limit", timeout=10) diff --git a/starpilot/starpilot_process.py b/starpilot/starpilot_process.py index 166b28103..c44d68950 100644 --- a/starpilot/starpilot_process.py +++ b/starpilot/starpilot_process.py @@ -203,7 +203,7 @@ def transition_onroad(error_log): error_log.unlink() def update_checks(now, model_manager, theme_manager, thread_manager, params, params_memory, starpilot_toggles, boot_run=False): - while not (is_url_pingable("https://github.com") or is_url_pingable("https://gitlab.com")): + while not (is_url_pingable("https://huggingface.co") or is_url_pingable("https://github.com")): time.sleep(60) model_manager.update_models(boot_run)