From 1430c59da9c9b4adc5cc9b1a30280c2be83cf54a Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Fri, 24 Apr 2026 00:41:11 -0500 Subject: [PATCH] Volt | Genesis | Ioniq | Kia | Model randomizer --- .../opendbc/car/hyundai/tests/test_hyundai.py | 39 +++++++- opendbc_repo/opendbc/car/hyundai/values.py | 10 +- panda/board/obj/gitversion.h | 2 +- panda/board/obj/version | 2 +- selfdrive/controls/lib/latcontrol_torque.py | 68 +++++++------- selfdrive/controls/tests/test_latcontrol.py | 6 +- selfdrive/modeld/modeld.py | 41 +++++++-- starpilot/assets/model_manager.py | 92 ++++++++++++++++++- starpilot/starpilot_process.py | 12 ++- 9 files changed, 220 insertions(+), 52 deletions(-) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py index dc8e37a0b..d54af6cfb 100644 --- a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py @@ -2,17 +2,19 @@ from hypothesis import settings, given, strategies as st import pytest -from opendbc.car import gen_empty_fingerprint +from opendbc.can import CANPacker +from opendbc.car import Bus, gen_empty_fingerprint from opendbc.car.structs import CarParams from opendbc.car.fw_versions import build_fw_dict from opendbc.car.hyundai.carstate import CarState from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai import hyundaicanfd from opendbc.car.hyundai.hyundaicanfd import CanBus from opendbc.car.hyundai.radar_interface import RADAR_START_ADDR from opendbc.car.hyundai.values import CAMERA_SCC_CAR, CANFD_CAR, CAN_GEARS, CAR, CHECKSUM, DATE_FW_ECUS, \ HYBRID_CAR, EV_CAR, FW_QUERY_CONFIG, LEGACY_SAFETY_MODE_CAR, CANFD_FUZZY_WHITELIST, \ UNSUPPORTED_LONGITUDINAL_CAR, PLATFORM_CODE_ECUS, HYUNDAI_VERSION_REQUEST_LONG, \ - HyundaiFlags, get_platform_codes, HyundaiSafetyFlags + CarControllerParams, DBC, HyundaiFlags, get_platform_codes, HyundaiSafetyFlags from opendbc.car.hyundai.fingerprints import FW_VERSIONS Ecu = CarParams.Ecu @@ -120,6 +122,39 @@ class TestHyundaiFingerprint: assert CarState.get_canfd_blinker_sig_names(CAR.HYUNDAI_KONA_EV_2ND_GEN, False) == ("LEFT_LAMP_ALT", "RIGHT_LAMP_ALT") assert CarState.get_canfd_blinker_sig_names(CAR.KIA_EV6, False) == ("LEFT_LAMP", "RIGHT_LAMP") + def test_sportage_angle_steering_uses_adas_cmd_with_send_lfa(self): + fingerprint = gen_empty_fingerprint() + cam_can = CanBus(None, fingerprint).CAM + fingerprint[cam_can][0xCB] = 24 + CP = CarInterface.get_params(CAR.KIA_SPORTAGE_HEV_2026, fingerprint, [], False, False, False, None) + + assert CP.flags & HyundaiFlags.SEND_LFA + assert CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING + + packer = CANPacker(DBC[CP.carFingerprint][Bus.pt]) + msgs = hyundaicanfd.create_steering_messages(packer, CP, CanBus(CP), True, True, 1.0, 12.3) + assert [(addr, bus) for addr, _, bus in msgs] == [(0xCB, CanBus(CP).ECAN)] + + def test_sportage_angle_jerk_override_is_scoped(self): + sportage = CarParams.new_message() + sportage.carFingerprint = CAR.KIA_SPORTAGE_HEV_2026 + sportage.flags = int(HyundaiFlags.CANFD | HyundaiFlags.CANFD_ANGLE_STEERING) + + comparison_angle = CarParams.new_message() + comparison_angle.carFingerprint = CAR.KIA_EV6 + comparison_angle.flags = int(HyundaiFlags.CANFD | HyundaiFlags.CANFD_ANGLE_STEERING) + + ioniq6 = CarParams.new_message() + ioniq6.carFingerprint = CAR.HYUNDAI_IONIQ_6 + ioniq6.flags = int(HyundaiFlags.CANFD | HyundaiFlags.CANFD_LKA_STEERING | HyundaiFlags.CANFD_LKA_STEERING_ALT) + + sportage_params = CarControllerParams(sportage) + comparison_params = CarControllerParams(comparison_angle) + ioniq6_params = CarControllerParams(ioniq6) + + assert sportage_params.ANGLE_LIMITS.MAX_LATERAL_JERK < comparison_params.ANGLE_LIMITS.MAX_LATERAL_JERK + assert comparison_params.ANGLE_LIMITS.MAX_LATERAL_JERK == ioniq6_params.ANGLE_LIMITS.MAX_LATERAL_JERK + def test_blacklisted_parts(self, subtests): # Asserts no ECUs known to be shared across platforms exist in the database. # Tucson having Santa Cruz camera and EPS for example diff --git a/opendbc_repo/opendbc/car/hyundai/values.py b/opendbc_repo/opendbc/car/hyundai/values.py index 433ef07ac..4e7986882 100644 --- a/opendbc_repo/opendbc/car/hyundai/values.py +++ b/opendbc_repo/opendbc/car/hyundai/values.py @@ -1,5 +1,5 @@ import re -from dataclasses import dataclass, field +from dataclasses import dataclass, field, replace from enum import IntFlag from opendbc.car import ACCELERATION_DUE_TO_GRAVITY, Bus, CarSpecs, DbcDict, PlatformConfig, Platforms, uds @@ -29,6 +29,7 @@ class CarControllerParams: ANGLE_ACTIVE_TORQUE_REDUCTION_GAIN = 0.6 def __init__(self, CP, vEgoRaw=100.): + self.ANGLE_LIMITS = self.ANGLE_LIMITS self.STEER_DELTA_UP = 3 self.STEER_DELTA_DOWN = 7 self.STEER_DRIVER_ALLOWANCE = 50 @@ -52,6 +53,13 @@ class CarControllerParams: if CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING: self.STEER_THRESHOLD = 175 + # The Sportage angle port is rough at low speed on the higher global jerk limit. + # Keep the branch-wide higher limit for other cars, but restore the older calmer + # jerk ceiling on this port only. + if CP.carFingerprint == CAR.KIA_SPORTAGE_HEV_2026: + self.ANGLE_LIMITS = replace(self.ANGLE_LIMITS, + MAX_LATERAL_JERK=3.0 + (ACCELERATION_DUE_TO_GRAVITY * AVERAGE_ROAD_ROLL)) + # To determine the limit for your car, find the maximum value that the stock LKAS will request. # If the max stock LKAS request is <384, add your car to this list. elif CP.carFingerprint in (CAR.GENESIS_G80, CAR.HYUNDAI_ELANTRA, CAR.HYUNDAI_ELANTRA_GT_I30, CAR.HYUNDAI_IONIQ, diff --git a/panda/board/obj/gitversion.h b/panda/board/obj/gitversion.h index b0fb11698..5b56f7025 100644 --- a/panda/board/obj/gitversion.h +++ b/panda/board/obj/gitversion.h @@ -1,2 +1,2 @@ extern const uint8_t gitversion[19]; -const uint8_t gitversion[19] = "DEV-49c71e2e-DEBUG"; +const uint8_t gitversion[19] = "DEV-f20d22d6-DEBUG"; diff --git a/panda/board/obj/version b/panda/board/obj/version index f9afba01b..d00843f0e 100644 --- a/panda/board/obj/version +++ b/panda/board/obj/version @@ -1 +1 @@ -DEV-49c71e2e-DEBUG \ No newline at end of file +DEV-f20d22d6-DEBUG \ No newline at end of file diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 39fa8fa24..d3a2bfebb 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -155,81 +155,81 @@ VOLT_STANDARD_FF_CUTOFF = 1.30 VOLT_STANDARD_FF_CUTOFF_WIDTH = 0.24 VOLT_STANDARD_TRANSITION_SPEED = 10.0 VOLT_STANDARD_PHASE_SCALE = 0.10 -VOLT_STANDARD_TURN_IN_BOOST_LEFT = 0.02 +VOLT_STANDARD_TURN_IN_BOOST_LEFT = 0.06 VOLT_STANDARD_TURN_IN_BOOST_RIGHT = 0.30 -VOLT_STANDARD_UNWIND_TAPER_LEFT = 0.08 -VOLT_STANDARD_UNWIND_TAPER_RIGHT = 0.14 +VOLT_STANDARD_UNWIND_TAPER_LEFT = 0.12 +VOLT_STANDARD_UNWIND_TAPER_RIGHT = 0.22 VOLT_STANDARD_FRICTION_MULT = 1.04 VOLT_STANDARD_FRICTION_LAT_RISE = 0.20 VOLT_STANDARD_FRICTION_JERK_RISE = 0.24 VOLT_STANDARD_TURN_IN_THRESHOLD_REDUCTION_LEFT = 0.02 VOLT_STANDARD_TURN_IN_THRESHOLD_REDUCTION_RIGHT = 0.18 -VOLT_STANDARD_UNWIND_THRESHOLD_INCREASE_LEFT = 0.02 -VOLT_STANDARD_UNWIND_THRESHOLD_INCREASE_RIGHT = 0.10 +VOLT_STANDARD_UNWIND_THRESHOLD_INCREASE_LEFT = 0.04 +VOLT_STANDARD_UNWIND_THRESHOLD_INCREASE_RIGHT = 0.18 VOLT_STANDARD_TURN_IN_FRICTION_BOOST_LEFT = 0.01 VOLT_STANDARD_TURN_IN_FRICTION_BOOST_RIGHT = 0.10 -VOLT_STANDARD_UNWIND_FRICTION_REDUCTION_LEFT = 0.02 -VOLT_STANDARD_UNWIND_FRICTION_REDUCTION_RIGHT = 0.08 -VOLT_STANDARD_CENTER_TAPER_MAX = 0.11 -VOLT_STANDARD_CENTER_TAPER_LAT = 0.12 +VOLT_STANDARD_UNWIND_FRICTION_REDUCTION_LEFT = 0.04 +VOLT_STANDARD_UNWIND_FRICTION_REDUCTION_RIGHT = 0.14 +VOLT_STANDARD_CENTER_TAPER_MAX = 0.08 +VOLT_STANDARD_CENTER_TAPER_LAT = 0.10 VOLT_STANDARD_CENTER_TAPER_LAT_WIDTH = 0.02 -VOLT_STANDARD_CENTER_TAPER_SPEED = 17.0 +VOLT_STANDARD_CENTER_TAPER_SPEED = 20.0 VOLT_STANDARD_CENTER_TAPER_SPEED_WIDTH = 2.5 GENESIS_G90_LATERAL_TESTING_GROUND_ID = testing_ground.id_4 GENESIS_G90_FF_GAIN_LEFT = 0.13 -GENESIS_G90_FF_GAIN_RIGHT = 0.10 +GENESIS_G90_FF_GAIN_RIGHT = 0.14 GENESIS_G90_FF_ONSET = 0.10 GENESIS_G90_FF_ONSET_WIDTH = 0.05 GENESIS_G90_FF_CUTOFF = 1.55 GENESIS_G90_FF_CUTOFF_WIDTH = 0.30 GENESIS_G90_TRANSITION_SPEED = 8.5 GENESIS_G90_PHASE_SCALE = 0.12 -GENESIS_G90_TURN_IN_BOOST_LEFT = 0.14 -GENESIS_G90_TURN_IN_BOOST_RIGHT = 0.22 -GENESIS_G90_UNWIND_TAPER_LEFT = 0.12 -GENESIS_G90_UNWIND_TAPER_RIGHT = 0.18 +GENESIS_G90_TURN_IN_BOOST_LEFT = 0.22 +GENESIS_G90_TURN_IN_BOOST_RIGHT = 0.34 +GENESIS_G90_UNWIND_TAPER_LEFT = 0.18 +GENESIS_G90_UNWIND_TAPER_RIGHT = 0.26 GENESIS_G90_FRICTION_MULT = 1.02 GENESIS_G90_FRICTION_LAT_RISE = 0.22 GENESIS_G90_FRICTION_JERK_RISE = 0.24 GENESIS_G90_TURN_IN_THRESHOLD_REDUCTION_LEFT = 0.10 GENESIS_G90_TURN_IN_THRESHOLD_REDUCTION_RIGHT = 0.14 -GENESIS_G90_UNWIND_THRESHOLD_INCREASE_LEFT = 0.08 -GENESIS_G90_UNWIND_THRESHOLD_INCREASE_RIGHT = 0.14 -GENESIS_G90_TURN_IN_FRICTION_BOOST_LEFT = 0.05 -GENESIS_G90_TURN_IN_FRICTION_BOOST_RIGHT = 0.08 -GENESIS_G90_UNWIND_FRICTION_REDUCTION_LEFT = 0.07 -GENESIS_G90_UNWIND_FRICTION_REDUCTION_RIGHT = 0.11 +GENESIS_G90_UNWIND_THRESHOLD_INCREASE_LEFT = 0.12 +GENESIS_G90_UNWIND_THRESHOLD_INCREASE_RIGHT = 0.20 +GENESIS_G90_TURN_IN_FRICTION_BOOST_LEFT = 0.06 +GENESIS_G90_TURN_IN_FRICTION_BOOST_RIGHT = 0.10 +GENESIS_G90_UNWIND_FRICTION_REDUCTION_LEFT = 0.10 +GENESIS_G90_UNWIND_FRICTION_REDUCTION_RIGHT = 0.16 IONIQ_6_LATERAL_TESTING_GROUND_ID = testing_ground.id_5 IONIQ_6_LATERAL_TESTING_GROUND_VARIANT = "C" -IONIQ_6_FF_GAIN_LEFT = 0.04 -IONIQ_6_FF_GAIN_RIGHT = 0.015 +IONIQ_6_FF_GAIN_LEFT = 0.045 +IONIQ_6_FF_GAIN_RIGHT = 0.008 IONIQ_6_FF_ONSET = 0.10 IONIQ_6_FF_ONSET_WIDTH = 0.04 IONIQ_6_FF_CUTOFF = 0.60 IONIQ_6_FF_CUTOFF_WIDTH = 0.14 IONIQ_6_TRANSITION_SPEED = 10.0 IONIQ_6_PHASE_SCALE = 0.10 -IONIQ_6_TURN_IN_BOOST_LEFT = 0.38 -IONIQ_6_TURN_IN_BOOST_RIGHT = 0.32 -IONIQ_6_UNWIND_TAPER_LEFT = 0.34 -IONIQ_6_UNWIND_TAPER_RIGHT = 0.54 +IONIQ_6_TURN_IN_BOOST_LEFT = 0.42 +IONIQ_6_TURN_IN_BOOST_RIGHT = 0.40 +IONIQ_6_UNWIND_TAPER_LEFT = 0.40 +IONIQ_6_UNWIND_TAPER_RIGHT = 0.66 IONIQ_6_FRICTION_MULT = 0.995 IONIQ_6_FRICTION_LAT_RISE = 0.20 IONIQ_6_FRICTION_JERK_RISE = 0.24 IONIQ_6_TURN_IN_THRESHOLD_REDUCTION_LEFT = 0.12 IONIQ_6_TURN_IN_THRESHOLD_REDUCTION_RIGHT = 0.10 -IONIQ_6_UNWIND_THRESHOLD_INCREASE_LEFT = 0.24 -IONIQ_6_UNWIND_THRESHOLD_INCREASE_RIGHT = 0.36 +IONIQ_6_UNWIND_THRESHOLD_INCREASE_LEFT = 0.28 +IONIQ_6_UNWIND_THRESHOLD_INCREASE_RIGHT = 0.44 IONIQ_6_TURN_IN_FRICTION_BOOST_LEFT = 0.05 IONIQ_6_TURN_IN_FRICTION_BOOST_RIGHT = 0.04 -IONIQ_6_UNWIND_FRICTION_REDUCTION_LEFT = 0.18 -IONIQ_6_UNWIND_FRICTION_REDUCTION_RIGHT = 0.28 -IONIQ_6_CENTER_TAPER_MAX = 0.05 -IONIQ_6_CENTER_TAPER_LAT = 0.10 +IONIQ_6_UNWIND_FRICTION_REDUCTION_LEFT = 0.22 +IONIQ_6_UNWIND_FRICTION_REDUCTION_RIGHT = 0.34 +IONIQ_6_CENTER_TAPER_MAX = 0.025 +IONIQ_6_CENTER_TAPER_LAT = 0.08 IONIQ_6_CENTER_TAPER_LAT_WIDTH = 0.02 -IONIQ_6_CENTER_TAPER_SPEED = 20.0 +IONIQ_6_CENTER_TAPER_SPEED = 22.0 IONIQ_6_CENTER_TAPER_SPEED_WIDTH = 2.5 KIA_EV6_LATERAL_TESTING_GROUND_ID = testing_ground.id_6 diff --git a/selfdrive/controls/tests/test_latcontrol.py b/selfdrive/controls/tests/test_latcontrol.py index da947ebc9..99f93e196 100644 --- a/selfdrive/controls/tests/test_latcontrol.py +++ b/selfdrive/controls/tests/test_latcontrol.py @@ -181,7 +181,7 @@ class TestLatControl: def test_genesis_g90_ff_scale_curve(self): assert get_genesis_g90_ff_scale(0.0, 0.0, 20.0) == 1.0 - assert get_genesis_g90_ff_scale(0.5, 0.0, 20.0) > get_genesis_g90_ff_scale(-0.5, 0.0, 20.0) + assert get_genesis_g90_ff_scale(-0.5, 0.0, 20.0) > get_genesis_g90_ff_scale(0.5, 0.0, 20.0) assert get_genesis_g90_ff_scale(0.6, 0.7, 8.0) > get_genesis_g90_ff_scale(0.6, 0.0, 8.0) > get_genesis_g90_ff_scale(0.6, -0.7, 8.0) assert get_genesis_g90_ff_scale(-0.6, -0.7, 8.0) > get_genesis_g90_ff_scale(-0.6, 0.0, 8.0) > get_genesis_g90_ff_scale(-0.6, 0.7, 8.0) assert get_genesis_g90_ff_scale(2.0, 0.0, 20.0) < get_genesis_g90_ff_scale(0.8, 0.0, 20.0) @@ -219,7 +219,9 @@ class TestLatControl: right_turn_in = get_ioniq_6_friction_threshold(6.0, -0.5, -0.8) left_unwind = get_ioniq_6_friction_threshold(6.0, 0.5, -0.8) right_unwind = get_ioniq_6_friction_threshold(6.0, -0.5, 0.8) - assert left_turn_in < right_turn_in < base < left_unwind < right_unwind + assert max(left_turn_in, right_turn_in) < base + assert left_unwind >= base + assert right_unwind >= base def test_ioniq_6_friction_scale_curve(self): base = get_ioniq_6_friction_scale(25.0, 0.5, 0.8) diff --git a/selfdrive/modeld/modeld.py b/selfdrive/modeld/modeld.py index ff90ad1c1..66cc541ec 100755 --- a/selfdrive/modeld/modeld.py +++ b/selfdrive/modeld/modeld.py @@ -60,6 +60,39 @@ def _get_param_str(params: Params, key: str, default: str = "") -> str: return str(val) +def _get_default_param_str(params: Params, key: str) -> str: + try: + val = params.get_default_value(key) + except Exception: + return "" + if val is None: + return "" + if isinstance(val, bytes): + try: + return val.decode("utf-8") + except Exception: + return "" + return str(val) + + +def _resolve_mirrored_param(params: Params, primary_key: str, secondary_key: str) -> str: + primary_val = _get_param_str(params, primary_key).strip() + secondary_val = _get_param_str(params, secondary_key).strip() + if primary_val == secondary_val: + return secondary_val or primary_val + + primary_default = _get_default_param_str(params, primary_key).strip() + secondary_default = _get_default_param_str(params, secondary_key).strip() + primary_non_default = bool(primary_val) and primary_val != primary_default + secondary_non_default = bool(secondary_val) and secondary_val != secondary_default + + if secondary_non_default: + return secondary_val + if primary_non_default: + return primary_val + return secondary_val or primary_val + + def _canonical_model_id(model_id: str) -> str: key = (model_id or "").strip().lower() return BUILTIN_MODEL_KEY if key in BUILTIN_MODEL_ALIASES else key @@ -143,14 +176,10 @@ class ModelState: def __init__(self, context: CLContext): # Dynamically build paths based on current model ID params = Params() - model_id_raw = _get_param_str(params, "Model") - if not model_id_raw: - model_id_raw = _get_param_str(params, "DrivingModel", BUILTIN_MODEL_KEY) + model_id_raw = _resolve_mirrored_param(params, "Model", "DrivingModel") or BUILTIN_MODEL_KEY model_id = _canonical_model_id(model_id_raw) - model_version = _get_param_str(params, "ModelVersion") - if not model_version: - model_version = _get_param_str(params, "DrivingModelVersion") + model_version = _resolve_mirrored_param(params, "ModelVersion", "DrivingModelVersion") model_dir = MODELS_PATH use_builtin_model = model_id == BUILTIN_MODEL_KEY diff --git a/starpilot/assets/model_manager.py b/starpilot/assets/model_manager.py index 26de0c0a2..316ae5f52 100644 --- a/starpilot/assets/model_manager.py +++ b/starpilot/assets/model_manager.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import json +import random import re import urllib.request @@ -89,6 +90,12 @@ class ModelManager: return raw.decode("utf-8", errors="ignore").strip() return str(raw).strip() + def _param_bool(self, key: str) -> bool: + try: + return bool(self.params.get_bool(key)) + except Exception: + return self._param_text(key).lower() in {"1", "true", "yes", "on"} + def _default_param_text(self, key: str) -> str: try: default_value = self.params.get_default_value(key) @@ -100,6 +107,23 @@ class ModelManager: return default_value.decode("utf-8", errors="ignore").strip() return str(default_value).strip() + def _resolve_mirrored_param(self, primary_key: str, secondary_key: str) -> str: + primary_val = self._param_text(primary_key) + secondary_val = self._param_text(secondary_key) + if primary_val == secondary_val: + return secondary_val or primary_val + + primary_default = self._default_param_text(primary_key) + secondary_default = self._default_param_text(secondary_key) + primary_non_default = bool(primary_val) and primary_val != primary_default + secondary_non_default = bool(secondary_val) and secondary_val != secondary_default + + if secondary_non_default: + return secondary_val + if primary_non_default: + return primary_val + return secondary_val or primary_val + def _load_catalog_from_params(self): self.available_models = [entry for entry in self._param_text("AvailableModels").split(",") if entry] self.model_versions = [entry for entry in self._param_text("ModelVersions").split(",") if entry] @@ -119,7 +143,7 @@ class ModelManager: def _ensure_model_params(self): selected_model = self._selected_model() - current_version = self._param_text("ModelVersion") or self._param_text("DrivingModelVersion") + current_version = self._resolve_mirrored_param("ModelVersion", "DrivingModelVersion") if not current_version: current_version = self._default_param_text("ModelVersion") or self._default_param_text("DrivingModelVersion") or "v11" @@ -141,8 +165,15 @@ class ModelManager: if index < len(self.model_versions) and model_key } + def _blacklisted_model_keys(self) -> set[str]: + return { + self._canonical_model_key(entry) + for entry in self._param_text("BlacklistedModels").split(",") + if entry.strip() + } + def _selected_model(self) -> str: - selected = self._param_text("Model") or self._param_text("DrivingModel") + selected = self._resolve_mirrored_param("Model", "DrivingModel") if selected: return self._canonical_model_key(selected) default_value = self._default_param_text("Model") or self._default_param_text("DrivingModel") @@ -178,6 +209,60 @@ class ModelManager: return False return all((MODELS_PATH / filename).is_file() for filename in required_files) + def _installed_model_choices(self) -> list[tuple[str, str, str]]: + self._load_catalog_from_params() + version_map = self._model_version_map() + blacklisted_keys = self._blacklisted_model_keys() + choices: list[tuple[str, str, str]] = [] + seen_keys: set[str] = set() + + for index, model_key in enumerate(self.available_models): + if not model_key: + continue + + canonical_key = self._canonical_model_key(model_key) + if canonical_key in blacklisted_keys or canonical_key in seen_keys: + continue + + model_version = version_map.get(model_key) or version_map.get(canonical_key) or "" + if not model_version and is_builtin_model_key(canonical_key): + model_version = self._default_param_text("ModelVersion") or self._default_param_text("DrivingModelVersion") or "v11" + + if not self._is_model_downloaded(model_key, model_version): + continue + + model_name = self.available_model_names[index] if index < len(self.available_model_names) else canonical_key + choices.append((canonical_key, model_name, model_version)) + seen_keys.add(canonical_key) + + return choices + + def randomize_selected_model(self) -> str | None: + if not self._param_bool("ModelRandomizer"): + return None + + choices = self._installed_model_choices() + if not choices: + print("Model Randomizer skipped: no installed, non-blacklisted models available.") + return None + + selected = self._selected_model() + eligible_choices = [choice for choice in choices if self._canonical_model_key(choice[0]) != selected] + if not eligible_choices: + eligible_choices = choices + + model_key, model_name, model_version = random.choice(eligible_choices) + if not model_version: + model_version = self._default_param_text("ModelVersion") or self._default_param_text("DrivingModelVersion") or "v11" + + self._set_model_param_keys(model_key, model_name, model_version) + try: + self.params_memory.put_bool("StarPilotTogglesUpdated", True) + except Exception: + pass + print(f"Model Randomizer selected {model_name} ({model_key}, {model_version}).") + return model_key + def _sync_selected_model_version(self): version_map = self._model_version_map() name_map = {model_key: model_name for model_key, model_name in zip(self.available_models, self.available_model_names)} @@ -194,7 +279,7 @@ class ModelManager: self._set_model_param_keys(selected, selected_name, version) return - fallback_version = self._param_text("ModelVersion") or self._param_text("DrivingModelVersion") + fallback_version = self._resolve_mirrored_param("ModelVersion", "DrivingModelVersion") if not fallback_version: fallback_version = self._default_param_text("ModelVersion") or self._default_param_text("DrivingModelVersion") or "v11" self._set_model_param_keys(selected, name_map.get(selected, ""), fallback_version) @@ -390,6 +475,7 @@ class ModelManager: self.params_memory.put(DOWNLOAD_PROGRESS_PARAM, "All models downloaded!") self.params_memory.remove(MODEL_DOWNLOAD_ALL_PARAM) + self.randomize_selected_model() def update_tinygrad(self): # This branch ships tinygrad runtime in-tree. "Update" here refreshes local model files. diff --git a/starpilot/starpilot_process.py b/starpilot/starpilot_process.py index 55f38c808..41376faff 100644 --- a/starpilot/starpilot_process.py +++ b/starpilot/starpilot_process.py @@ -115,7 +115,7 @@ def sync_drive_stats(params, session): except Exception as exception: print(f"Failed to sync drive stats: {exception}") -def transition_offroad(starpilot_planner, theme_manager, thread_manager, time_validated, sm, params, starpilot_toggles): +def transition_offroad(starpilot_planner, model_manager, theme_manager, thread_manager, time_validated, sm, params, starpilot_toggles): params.put("LastGPSPosition", json.dumps(starpilot_planner.gps_position)) if starpilot_toggles.lock_doors_timer != 0: @@ -124,6 +124,8 @@ def transition_offroad(starpilot_planner, theme_manager, thread_manager, time_va if starpilot_toggles.random_themes: theme_manager.update_active_theme(time_validated, starpilot_toggles, randomize_theme=True) + model_manager.randomize_selected_model() + if time_validated: thread_manager.run_with_lock(send_stats) @@ -193,6 +195,7 @@ def starpilot_thread(): run_update_checks = False safe_mode_active = safe_mode_enabled(params_raw) started_previously = False + model_randomizer_previously = params.get_bool("ModelRandomizer") time_validated = False error_log = ERROR_LOGS_PATH / "error.txt" @@ -216,7 +219,7 @@ def starpilot_thread(): starpilot_toggles = update_toggles(starpilot_variables, started, theme_manager, thread_manager, time_validated, params, starpilot_toggles) serialized_starpilot_toggles = serialize_starpilot_toggles(starpilot_toggles) toggle_broadcast_pending = True - transition_offroad(starpilot_planner, theme_manager, thread_manager, time_validated, sm, params, starpilot_toggles) + transition_offroad(starpilot_planner, model_manager, theme_manager, thread_manager, time_validated, sm, params, starpilot_toggles) run_update_checks = True elif started and not started_previously: @@ -268,6 +271,11 @@ def starpilot_thread(): serialized_starpilot_toggles = serialize_starpilot_toggles(starpilot_toggles) toggle_broadcast_pending = True + model_randomizer_enabled = params.get_bool("ModelRandomizer") + if model_randomizer_enabled and not model_randomizer_previously and not started: + model_manager.randomize_selected_model() + model_randomizer_previously = model_randomizer_enabled + periodic_update_due = monotonic_now >= next_periodic_update_check if periodic_update_due: next_periodic_update_check = get_next_periodic_update_check(monotonic_now, periodic_update_phase)