diff --git a/opendbc_repo/opendbc/car/hyundai/interface.py b/opendbc_repo/opendbc/car/hyundai/interface.py index c20be87ff..78c16b210 100644 --- a/opendbc_repo/opendbc/car/hyundai/interface.py +++ b/opendbc_repo/opendbc/car/hyundai/interface.py @@ -253,6 +253,11 @@ class CarInterface(CarInterfaceBase): ret.vEgoStarting = 0.5 ret.vEgoStopping = 0.35 + if candidate == CAR.HYUNDAI_ELANTRA_2021: + ret.longitudinalActuatorDelay = 0.22 + ret.stopAccel = -1.5 + ret.stoppingDecelRate = 0.5 + if candidate == CAR.HYUNDAI_IONIQ_6: ret.longitudinalActuatorDelay = 0.6 diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py index 49625e40d..73ef7b2e2 100644 --- a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py @@ -704,6 +704,14 @@ class TestHyundaiFingerprint: assert CP.vEgoStopping == pytest.approx(0.35) assert CP.stoppingDecelRate == pytest.approx(0.35) + def test_elantra_2021_longitudinal_params_match_observed_response(self): + toggles = get_test_toggles() + CP = CarInterface.get_params(CAR.HYUNDAI_ELANTRA_2021, gen_empty_fingerprint(), [], True, False, False, toggles) + + assert CP.longitudinalActuatorDelay == pytest.approx(0.22) + assert CP.stopAccel == pytest.approx(-1.5) + assert CP.stoppingDecelRate == pytest.approx(0.5) + def test_kia_niro_phev_2022_longitudinal_params_soften_final_stop_hold(self): toggles = get_test_toggles() CP = CarInterface.get_params(CAR.KIA_NIRO_PHEV_2022, gen_empty_fingerprint(), [], True, False, False, toggles) diff --git a/opendbc_repo/opendbc/car/torque_data/override.toml b/opendbc_repo/opendbc/car/torque_data/override.toml index 05f1522ad..2d5049643 100644 --- a/opendbc_repo/opendbc/car/torque_data/override.toml +++ b/opendbc_repo/opendbc/car/torque_data/override.toml @@ -18,6 +18,7 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"] # Toyota LTA also has torque "TOYOTA_RAV4_TSS2_2023" = [nan, 3.0, nan] "TOYOTA_MATRIX_RETROFIT" = [4.05, 1.84, 0.10] +"TOYOTA_RAV4_PRIME" = [1.7, 2.0, 0.14] # Tesla angle based controllers "TESLA_MODEL_3" = [nan, 2.5, nan] diff --git a/opendbc_repo/opendbc/car/torque_data/substitute.toml b/opendbc_repo/opendbc/car/torque_data/substitute.toml index 401af9d64..98992ec9f 100644 --- a/opendbc_repo/opendbc/car/torque_data/substitute.toml +++ b/opendbc_repo/opendbc/car/torque_data/substitute.toml @@ -9,7 +9,6 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"] "TOYOTA_ALPHARD_TSS2" = "TOYOTA_SIENNA" "TOYOTA_PRIUS_V" = "TOYOTA_PRIUS" -"TOYOTA_RAV4_PRIME" = "TOYOTA_RAV4_TSS2" "TOYOTA_SIENNA_4TH_GEN" = "TOYOTA_RAV4_TSS2" "LEXUS_IS" = "LEXUS_NX" "LEXUS_CTH" = "LEXUS_NX" diff --git a/opendbc_repo/opendbc/car/toyota/tests/test_toyota.py b/opendbc_repo/opendbc/car/toyota/tests/test_toyota.py index e66ca9e44..bc2133662 100644 --- a/opendbc_repo/opendbc/car/toyota/tests/test_toyota.py +++ b/opendbc_repo/opendbc/car/toyota/tests/test_toyota.py @@ -53,6 +53,8 @@ class TestToyotaInterfaces: assert default_params.lateralTuning.which() == "pid" assert forced_params.lateralTuning.which() == "torque" + assert forced_params.lateralTuning.torque.latAccelFactor == pytest.approx(1.7) + assert forced_params.lateralTuning.torque.friction == pytest.approx(0.14) def test_tss2_dbc(self): # We make some assumptions about TSS2 platforms, diff --git a/selfdrive/ui/lib/mode_banner.py b/selfdrive/ui/lib/mode_banner.py new file mode 100644 index 000000000..8bee4cbc5 --- /dev/null +++ b/selfdrive/ui/lib/mode_banner.py @@ -0,0 +1,95 @@ +from enum import StrEnum + +import pyray as rl + +from openpilot.starpilot.common.experimental_state import requested_experimental_mode + + +class ModeBannerVariant(StrEnum): + CHILL = "chill" + EXPERIMENTAL = "experimental" + CONDITIONAL_EXPERIMENTAL = "conditional_experimental" + CONDITIONAL_CHILL = "conditional_chill" + + +def get_mode_banner_variant(params, params_memory=None) -> ModeBannerVariant: + if params.get_bool("SafeMode"): + return ModeBannerVariant.CHILL + if params.get_bool("ConditionalExperimental"): + return ModeBannerVariant.CONDITIONAL_EXPERIMENTAL + if params.get_bool("ConditionalChill"): + return ModeBannerVariant.CONDITIONAL_CHILL + if requested_experimental_mode(params, params_memory): + return ModeBannerVariant.EXPERIMENTAL + return ModeBannerVariant.CHILL + + +def _color(red: int, green: int, blue: int, alpha: int) -> rl.Color: + return rl.Color(red, green, blue, alpha) + + +def _lerp_color(start: rl.Color, end: rl.Color, progress: float, alpha: int) -> rl.Color: + progress = max(0.0, min(1.0, progress)) + return rl.Color( + round(start.r + (end.r - start.r) * progress), + round(start.g + (end.g - start.g) * progress), + round(start.b + (end.b - start.b) * progress), + alpha, + ) + + +def _conditional_colors(variant: ModeBannerVariant, alpha: int) -> tuple[rl.Color, rl.Color, rl.Color, rl.Color]: + blue = _color(35, 149, 255, alpha) + mint = _color(20, 255, 171, alpha) + orange = _color(255, 138, 22, alpha) + red = _color(219, 56, 34, alpha) + if variant == ModeBannerVariant.CONDITIONAL_EXPERIMENTAL: + return blue, mint, orange, red + return orange, red, blue, mint + + +def mode_banner_color(variant: ModeBannerVariant, progress: float, alpha: int = 255) -> rl.Color: + progress = max(0.0, min(1.0, progress)) + if variant == ModeBannerVariant.CHILL: + return _lerp_color(_color(20, 255, 171, alpha), _color(35, 149, 255, alpha), progress, alpha) + if variant == ModeBannerVariant.EXPERIMENTAL: + return _lerp_color(_color(255, 155, 63, alpha), _color(219, 56, 34, alpha), progress, alpha) + + dominant_start, dominant_end, target_start, target_end = _conditional_colors(variant, alpha) + if progress <= 0.58: + return _lerp_color(dominant_start, dominant_end, progress / 0.58, alpha) + if progress <= 0.80: + return _lerp_color(dominant_end, target_start, (progress - 0.58) / 0.22, alpha) + return _lerp_color(target_start, target_end, ((progress - 0.80) / 0.20) * 0.67, alpha) + + +def mode_atom_color(variant: ModeBannerVariant, progress: float, alpha: int = 255) -> rl.Color: + # The compact atom reads left-to-right as blue to mint in fixed Chill mode. + if variant == ModeBannerVariant.CHILL: + progress = 1.0 - progress + return mode_banner_color(variant, progress, alpha) + + +def draw_mode_banner_gradient(rect: rl.Rectangle, variant: ModeBannerVariant, alpha: int = 255) -> None: + if variant in (ModeBannerVariant.CHILL, ModeBannerVariant.EXPERIMENTAL): + rl.draw_rectangle_gradient_h( + int(rect.x), int(rect.y), int(rect.width), int(rect.height), + mode_banner_color(variant, 0.0, alpha), mode_banner_color(variant, 1.0, alpha), + ) + return + + transition_start = int(rect.x + rect.width * 0.58) + transition_end = int(rect.x + rect.width * 0.80) + right = int(rect.x + rect.width) + rl.draw_rectangle_gradient_h( + int(rect.x), int(rect.y), transition_start - int(rect.x), int(rect.height), + mode_banner_color(variant, 0.0, alpha), mode_banner_color(variant, 0.58, alpha), + ) + rl.draw_rectangle_gradient_h( + transition_start, int(rect.y), transition_end - transition_start, int(rect.height), + mode_banner_color(variant, 0.58, alpha), mode_banner_color(variant, 0.80, alpha), + ) + rl.draw_rectangle_gradient_h( + transition_end, int(rect.y), right - transition_end, int(rect.height), + mode_banner_color(variant, 0.80, alpha), mode_banner_color(variant, 1.0, alpha), + ) diff --git a/selfdrive/ui/mici/layouts/home.py b/selfdrive/ui/mici/layouts/home.py index 19471367c..55dba71b2 100644 --- a/selfdrive/ui/mici/layouts/home.py +++ b/selfdrive/ui/mici/layouts/home.py @@ -5,11 +5,13 @@ import time from cereal import log import pyray as rl from collections.abc import Callable +from importlib.resources import as_file from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.layouts import HBoxLayout from openpilot.system.ui.widgets.icon_widget import IconWidget from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos +from openpilot.system.ui.lib.application import ASSETS_DIR, gui_app, FontWeight, MousePos +from openpilot.selfdrive.ui.lib.mode_banner import ModeBannerVariant, get_mode_banner_variant, mode_atom_color from openpilot.selfdrive.ui.lib.starpilot_version import STARPILOT_DISPLAY_VERSION from openpilot.selfdrive.ui.ui_state import ui_state @@ -81,6 +83,68 @@ class NetworkIcon(Widget): rl.draw_texture_ex(draw_net_txt, rl.Vector2(draw_x, draw_y), 0.0, 1.0, rl.Color(255, 255, 255, int(255 * 0.9))) +class ModeStatusAtom(Widget): + def __init__(self): + super().__init__() + self._variant = ModeBannerVariant.CHILL + self._textures = self._make_textures() + self.set_rect(rl.Rectangle(0, 0, 48, 48)) + self.set_enabled(False) + self.refresh() + + @staticmethod + def _make_textures() -> dict[ModeBannerVariant, rl.Texture]: + textures = {} + with as_file(ASSETS_DIR.joinpath("icons_mici/experimental_mode.png")) as asset_path: + source = rl.load_image(asset_path.as_posix()) + + pixel_count = source.width * source.height + source_pixels = bytearray(rl.ffi.buffer(source.data, pixel_count * 4)) + try: + for variant in ModeBannerVariant: + tinted = rl.image_copy(source) + tinted_pixels = bytearray(source_pixels) + gradient = [mode_atom_color(variant, x / max(source.width - 1, 1)) for x in range(source.width)] + for y in range(source.height): + for x in range(source.width): + offset = (y * source.width + x) * 4 + opacity = source_pixels[offset + 3] + if opacity == 0: + continue + + color = gradient[x] + source_shade = max(source_pixels[offset:offset + 3]) / 255.0 + shade = 0.65 + 0.35 * source_shade + tinted_pixels[offset] = round(color.r * shade) + tinted_pixels[offset + 1] = round(color.g * shade) + tinted_pixels[offset + 2] = round(color.b * shade) + tinted_pixels[offset + 3] = opacity + + rl.ffi.buffer(tinted.data, len(tinted_pixels))[:] = bytes(tinted_pixels) + texture = rl.load_texture_from_image(tinted) + rl.set_texture_filter(texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR) + rl.set_texture_wrap(texture, rl.TextureWrap.TEXTURE_WRAP_CLAMP) + rl.unload_image(tinted) + textures[variant] = texture + finally: + rl.unload_image(source) + return textures + + def refresh(self) -> None: + self._variant = get_mode_banner_variant(ui_state.params, ui_state.params_memory) + + def _render(self, rect: rl.Rectangle) -> None: + texture = self._textures[self._variant] + source = rl.Rectangle(0, 0, texture.width, texture.height) + rl.draw_texture_pro(texture, source, rect, rl.Vector2(0, 0), 0, rl.WHITE) + + def __del__(self): + if rl.is_window_ready(): + for texture in getattr(self, "_textures", {}).values(): + if texture.id != 0: + rl.unload_texture(texture) + + class MiciHomeLayout(Widget): def __init__(self): super().__init__() @@ -95,7 +159,7 @@ class MiciHomeLayout(Widget): self._experimental_mode = False self._current_model_name = "default" - self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48)) + self._mode_status_atom = ModeStatusAtom() self._egpu_icon = IconWidget("icons_mici/egpu.png", (50, 37)) self._egpu_icon_gray = IconWidget("icons_mici/egpu_gray.png", (50, 37)) self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46)) @@ -103,7 +167,7 @@ class MiciHomeLayout(Widget): self._status_bar_layout = HBoxLayout([ IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9), NetworkIcon(), - self._experimental_icon, + self._mode_status_atom, self._egpu_icon, self._egpu_icon_gray, self._mic_icon, @@ -123,6 +187,7 @@ class MiciHomeLayout(Widget): def _update_params(self): self._experimental_mode = ui_state.params.get_bool("ExperimentalMode") + self._mode_status_atom.refresh() def _clean_model_name(value: str) -> str: return re.sub(r"[πŸ—ΊοΈπŸ‘€πŸ“‘]", "", value).replace("(Default)", "").strip() @@ -152,6 +217,7 @@ class MiciHomeLayout(Widget): if ui_state.has_longitudinal_control: self._experimental_mode = not self._experimental_mode ui_state.params.put("ExperimentalMode", self._experimental_mode) + self._mode_status_atom.refresh() self._mouse_down_t = None self._did_long_press = True @@ -214,7 +280,6 @@ class MiciHomeLayout(Widget): self._version_commit_label.render() # ***** Center-aligned bottom section icons ***** - self._experimental_icon.set_visible(self._experimental_mode) self._egpu_icon.set_visible(ui_state.usbgpu_active) self._egpu_icon_gray.set_visible(ui_state.usbgpu and not ui_state.usbgpu_active) self._mic_icon.set_visible(ui_state.recording_audio) diff --git a/selfdrive/ui/tests/test_mode_banner.py b/selfdrive/ui/tests/test_mode_banner.py new file mode 100644 index 000000000..c2046eaa8 --- /dev/null +++ b/selfdrive/ui/tests/test_mode_banner.py @@ -0,0 +1,50 @@ +from openpilot.selfdrive.ui.lib.mode_banner import ModeBannerVariant, get_mode_banner_variant, mode_atom_color, mode_banner_color + + +class FakeParams: + def __init__(self, bools=None, ints=None): + self.bools = bools or {} + self.ints = ints or {} + + def get_bool(self, key): + return self.bools.get(key, False) + + def get_int(self, key, default=0): + return self.ints.get(key, default) + + +def _rgb(color): + return color.r, color.g, color.b + + +def test_mode_banner_variant_tracks_fixed_and_conditional_modes(): + assert get_mode_banner_variant(FakeParams()) == ModeBannerVariant.CHILL + assert get_mode_banner_variant(FakeParams({"ExperimentalMode": True})) == ModeBannerVariant.EXPERIMENTAL + assert get_mode_banner_variant(FakeParams({"ConditionalExperimental": True})) == ModeBannerVariant.CONDITIONAL_EXPERIMENTAL + assert get_mode_banner_variant(FakeParams({"ConditionalChill": True})) == ModeBannerVariant.CONDITIONAL_CHILL + assert get_mode_banner_variant(FakeParams({"SafeMode": True, "ConditionalExperimental": True})) == ModeBannerVariant.CHILL + + +def test_conditional_gradients_keep_full_major_and_partial_minor_colors(): + conditional_experimental = ModeBannerVariant.CONDITIONAL_EXPERIMENTAL + assert _rgb(mode_banner_color(conditional_experimental, 0.0)) == (35, 149, 255) + assert _rgb(mode_banner_color(conditional_experimental, 0.58)) == (20, 255, 171) + assert _rgb(mode_banner_color(conditional_experimental, 0.80)) == (255, 138, 22) + assert _rgb(mode_banner_color(conditional_experimental, 1.0)) == (231, 83, 30) + + conditional_chill = ModeBannerVariant.CONDITIONAL_CHILL + assert _rgb(mode_banner_color(conditional_chill, 0.0)) == (255, 138, 22) + assert _rgb(mode_banner_color(conditional_chill, 0.58)) == (219, 56, 34) + assert _rgb(mode_banner_color(conditional_chill, 0.80)) == (35, 149, 255) + assert _rgb(mode_banner_color(conditional_chill, 1.0)) == (25, 220, 199) + + +def test_atom_gradients_use_compact_icon_directions(): + assert _rgb(mode_atom_color(ModeBannerVariant.CHILL, 0.0)) == (35, 149, 255) + assert _rgb(mode_atom_color(ModeBannerVariant.CHILL, 1.0)) == (20, 255, 171) + assert _rgb(mode_atom_color(ModeBannerVariant.EXPERIMENTAL, 0.0)) == (255, 155, 63) + assert _rgb(mode_atom_color(ModeBannerVariant.EXPERIMENTAL, 1.0)) == (219, 56, 34) + + for variant in (ModeBannerVariant.CONDITIONAL_EXPERIMENTAL, ModeBannerVariant.CONDITIONAL_CHILL): + assert _rgb(mode_atom_color(variant, 0.0)) == _rgb(mode_banner_color(variant, 0.0)) + assert _rgb(mode_atom_color(variant, 1.0)) == _rgb(mode_banner_color(variant, 1.0)) diff --git a/selfdrive/ui/widgets/exp_mode_button.py b/selfdrive/ui/widgets/exp_mode_button.py index 241d48169..b1fd40f14 100644 --- a/selfdrive/ui/widgets/exp_mode_button.py +++ b/selfdrive/ui/widgets/exp_mode_button.py @@ -4,6 +4,7 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget +from openpilot.selfdrive.ui.lib.mode_banner import ModeBannerVariant, draw_mode_banner_gradient, get_mode_banner_variant from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.starpilot.common.experimental_state import requested_experimental_mode @@ -18,74 +19,18 @@ class ExperimentalModeButton(Widget): self.params = Params() self.experimental_mode = requested_experimental_mode(self.params, ui_state.params_memory) - self.conditional_mode = self._get_conditional_mode() + self.mode_variant = get_mode_banner_variant(self.params, ui_state.params_memory) self.chill_pixmap = gui_app.texture("icons/couch.png", self.img_width, self.img_width) self.experimental_pixmap = gui_app.texture("icons/experimental_grey.png", self.img_width, self.img_width) def show_event(self): self.experimental_mode = requested_experimental_mode(self.params, ui_state.params_memory) - self.conditional_mode = self._get_conditional_mode() - - def _get_conditional_mode(self): - if self.params.get_bool("SafeMode"): - return None - if self.params.get_bool("ConditionalExperimental"): - return "experimental" - if self.params.get_bool("ConditionalChill"): - return "chill" - return None - - def _get_gradient_colors(self): - alpha = 0xCC if self.is_pressed else 0xFF - - if self.experimental_mode: - return rl.Color(255, 155, 63, alpha), rl.Color(219, 56, 34, alpha) - else: - return rl.Color(20, 255, 171, alpha), rl.Color(35, 149, 255, alpha) - - def _draw_gradient_background(self, rect): - if self.conditional_mode: - alpha = 0xCC if self.is_pressed else 0xFF - blue = rl.Color(35, 149, 255, alpha) - mint = rl.Color(20, 255, 171, alpha) - orange = rl.Color(255, 138, 22, alpha) - red = rl.Color(219, 56, 34, alpha) - if self.conditional_mode == "experimental": - dominant_start, dominant_end = blue, mint - target_start, target_end = orange, red - else: - dominant_start, dominant_end = orange, red - target_start, target_end = blue, mint - - transition_start = int(rect.x + rect.width * 0.58) - transition_end = int(rect.x + rect.width * 0.80) - right = int(rect.x + rect.width) - dominant_width = transition_start - int(rect.x) - target_width = right - transition_end - target_progress = 0.67 - target_visible_end = rl.Color( - round(target_start.r + (target_end.r - target_start.r) * target_progress), - round(target_start.g + (target_end.g - target_start.g) * target_progress), - round(target_start.b + (target_end.b - target_start.b) * target_progress), - alpha, - ) - - rl.draw_rectangle_gradient_h(int(rect.x), int(rect.y), dominant_width, int(rect.height), - dominant_start, dominant_end) - rl.draw_rectangle_gradient_h(transition_start, int(rect.y), transition_end - transition_start, int(rect.height), - dominant_end, target_start) - rl.draw_rectangle_gradient_h(transition_end, int(rect.y), target_width, int(rect.height), - target_start, target_visible_end) - return - - start_color, end_color = self._get_gradient_colors() - rl.draw_rectangle_gradient_h(int(rect.x), int(rect.y), int(rect.width), int(rect.height), - start_color, end_color) + self.mode_variant = get_mode_banner_variant(self.params, ui_state.params_memory) def _render(self, rect): rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) - self._draw_gradient_background(rect) + draw_mode_banner_gradient(rect, self.mode_variant, 0xCC if self.is_pressed else 0xFF) rl.draw_rectangle_rounded_lines_ex(self._rect, 0.19, 10, 5, rl.BLACK) rl.end_scissor_mode() @@ -95,9 +40,9 @@ class ExperimentalModeButton(Widget): rl.draw_line_ex(rl.Vector2(line_x, rect.y), rl.Vector2(line_x, rect.y + rect.height), 3, separator_color) # Draw text label (left aligned) - if self.conditional_mode == "experimental": + if self.mode_variant == ModeBannerVariant.CONDITIONAL_EXPERIMENTAL: text = tr("CONDITIONAL EXPERIMENTAL") - elif self.conditional_mode == "chill": + elif self.mode_variant == ModeBannerVariant.CONDITIONAL_CHILL: text = tr("CONDITIONAL CHILL") else: text = tr("EXPERIMENTAL MODE ON") if self.experimental_mode else tr("CHILL MODE ON") diff --git a/starpilot/common/starpilot_variables.py b/starpilot/common/starpilot_variables.py index 70c7a24a3..3f0f09158 100644 --- a/starpilot/common/starpilot_variables.py +++ b/starpilot/common/starpilot_variables.py @@ -284,13 +284,6 @@ EXCLUDED_KEYS = { "UptimeOffroad" } -TUNING_LEVELS = { - "MINIMAL": 0, - "STANDARD": 1, - "ADVANCED": 2, - "DEVELOPER": 3 -} - # Shared params handles for modules that import these from starpilot_variables. params = Params(return_defaults=True) params_memory = Params(memory=True) @@ -419,8 +412,6 @@ class StarPilotVariables: toggle = self.starpilot_toggles self.default_values = {key.decode(): self.params.get_default_value(key) for key in self.params.all_keys()} - self.tuning_levels = {key.decode(): self.params.get_tuning_level(key) for key in self.params.all_keys()} - branch = get_build_metadata().channel self.release_branch = branch == "StarPilot" self.staging_branch = branch == "StarPilot-Staging" @@ -431,8 +422,6 @@ class StarPilotVariables: # Development/vetting branches are no longer gated into dashcam mode. toggle.block_user = False - toggle.tuning_level = self.params.get("TuningLevel") if self.params.get_bool("TuningLevelConfirmed") else TUNING_LEVELS["ADVANCED"] - device_management = self.get_value("DeviceManagement") toggle.use_higher_bitrate = device_management @@ -460,8 +449,8 @@ class StarPilotVariables: return f"#{color.get('alpha', 255):02X}{color.get('red', 255):02X}{color.get('green', 255):02X}{color.get('blue', 255):02X}" return "#FFFFFFFF" - def get_value(self, key, cast=bool, condition=True, conversion=None, default=None, min=None, max=None, respect_tuning_level=True): - if not condition or (respect_tuning_level and self.starpilot_toggles.tuning_level < self.tuning_levels.get(key, 0)): + def get_value(self, key, cast=bool, condition=True, conversion=None, default=None, min=None, max=None): + if not condition: if default is not None: value = default elif cast is bool: @@ -509,9 +498,7 @@ class StarPilotVariables: return value def get_button_function(self, key, condition=True): - # Tuning level should hide wheel-mapping controls, not silently revert their - # runtime behavior to defaults after the driver has configured them. - return self.get_value(key, cast=float, condition=condition, respect_tuning_level=False) + return self.get_value(key, cast=float, condition=condition) def migrate_prius_cluster_offset(self, car_model): if car_model not in PRIUS_CLUSTER_OFFSET_CARS or self.params_raw.get_bool(PRIUS_CLUSTER_OFFSET_MIGRATION_KEY): @@ -595,7 +582,6 @@ class StarPilotVariables: def update(self, holiday_theme="stock", started=False, clear_update_flag=True): toggle = self.starpilot_toggles - toggle.tuning_level = self.params.get("TuningLevel") if self.params.get_bool("TuningLevelConfirmed") else TUNING_LEVELS["ADVANCED"] # CarParams uses this value to select the matching Panda safety configuration. toggle.tesla_cooperative_steering = self.params.get_bool("TeslaCoopSteering") diff --git a/starpilot/common/tests/test_starpilot_variables.py b/starpilot/common/tests/test_starpilot_variables.py index d3391e89f..694d45aa6 100644 --- a/starpilot/common/tests/test_starpilot_variables.py +++ b/starpilot/common/tests/test_starpilot_variables.py @@ -160,15 +160,13 @@ def test_cancel_button_migration_copies_distance_actions_once(): assert params.get_int("CancelButtonControl") == 3 -def test_button_function_ignores_tuning_level_gate(): +def test_runtime_values_ignore_legacy_tuning_level_metadata(): params = _FakeParams(ints={"LKASButtonControl": spv.BUTTON_FUNCTIONS["AOL_TOGGLE"]}) variables = object.__new__(spv.StarPilotVariables) variables.params = params - variables.starpilot_toggles = SimpleNamespace(tuning_level=spv.TUNING_LEVELS["STANDARD"]) - variables.tuning_levels = {"LKASButtonControl": spv.TUNING_LEVELS["ADVANCED"]} variables.default_values = {"LKASButtonControl": str(spv.BUTTON_FUNCTIONS["EXPERIMENTAL_MODE"])} - assert variables.get_value("LKASButtonControl", cast=int) == spv.BUTTON_FUNCTIONS["EXPERIMENTAL_MODE"] + assert variables.get_value("LKASButtonControl", cast=int) == spv.BUTTON_FUNCTIONS["AOL_TOGGLE"] assert variables.get_button_function("LKASButtonControl") == spv.BUTTON_FUNCTIONS["AOL_TOGGLE"] diff --git a/starpilot/system/the_galaxy/flm_workspace.py b/starpilot/system/the_galaxy/flm_workspace.py index f42277271..c6bbf99a9 100644 --- a/starpilot/system/the_galaxy/flm_workspace.py +++ b/starpilot/system/the_galaxy/flm_workspace.py @@ -2629,6 +2629,8 @@ def _apply_param_bundle(params: Params, bundle: dict[str, Any]) -> None: elif kind == "string": params.put(key, str(value or "")) + Params(memory=True).put_bool("StarPilotTogglesUpdated", True) + def _merge_flm_override_state(base: dict[str, Any], delta: dict[str, Any]) -> dict[str, Any]: base = normalize_flm_overrides(base) @@ -2815,6 +2817,7 @@ def accept_trial_as_baseline() -> dict[str, Any]: params.put_bool("FLMTrialApplied", False) params.put("FLMActiveProfileId", "") _clear_persistent_trial_baseline(params) + Params(memory=True).put_bool("StarPilotTogglesUpdated", True) for path in paths["snapshots"].glob("*.json"): path.unlink() diff --git a/starpilot/system/the_galaxy/tests/test_flm_workspace.py b/starpilot/system/the_galaxy/tests/test_flm_workspace.py index 9b9219b63..d838eb73a 100644 --- a/starpilot/system/the_galaxy/tests/test_flm_workspace.py +++ b/starpilot/system/the_galaxy/tests/test_flm_workspace.py @@ -22,44 +22,51 @@ def _simple_module(name, **attrs): def _install_flm_import_stubs(tmp_path): class FakeParams: _store = {} + _memory_store = {} - def __init__(self, return_defaults=False): + def __init__(self, return_defaults=False, memory=False): self.return_defaults = return_defaults + self.memory = memory + + @property + def _values(self): + return type(self)._memory_store if self.memory else type(self)._store def get(self, key, block=False, return_default=False, encoding=None, default=None): del block, return_default - value = self._store.get(key, default) + value = self._values.get(key, default) if encoding and isinstance(value, bytes): return value.decode(encoding, errors="replace") return value def get_bool(self, key, default=False): - value = self._store.get(key, default) + value = self._values.get(key, default) if isinstance(value, bool): return value return str(value).strip().lower() in ("1", "true", "yes", "on") def get_float(self, key, block=False, return_default=False, default=0.0): del block, return_default - value = self._store.get(key, default) + value = self._values.get(key, default) try: return float(value) except Exception: return default def put(self, key, value): - self._store[key] = value + self._values[key] = value def put_bool(self, key, value): - self._store[key] = bool(value) + self._values[key] = bool(value) def put_float(self, key, value): - self._store[key] = float(value) + self._values[key] = float(value) def remove(self, key): - self._store.pop(key, None) + self._values.pop(key, None) FakeParams._store = {} + FakeParams._memory_store = {} class FakeHyundaiFlags: CANFD = 1 @@ -850,7 +857,9 @@ def test_apply_and_revert_trial_profile_round_trip(tmp_path): assert fake_params_cls._store["FLMTrialBaseline"]["params"]["SteerLatAccel"] == pytest.approx(1.5) assert fake_params_cls._store["FLMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.turn_in_boost_left"] == pytest.approx(0.08) assert fake_params_cls._store["FLMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.unwind_taper_left"] == pytest.approx(0.55) + assert fake_params_cls._memory_store["StarPilotTogglesUpdated"] is True + fake_params_cls._memory_store["StarPilotTogglesUpdated"] = False revert_result = module.revert_trial_profile() assert revert_result["snapshot"]["profileId"] == profile_id assert fake_params_cls._store["AdvancedLateralTune"] is False @@ -858,6 +867,7 @@ def test_apply_and_revert_trial_profile_round_trip(tmp_path): assert fake_params_cls._store["FLMTrialApplied"] is False assert "FLMTrialBaseline" not in fake_params_cls._store assert fake_params_cls._store["FLMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.unwind_taper_left"] == pytest.approx(0.55) + assert fake_params_cls._memory_store["StarPilotTogglesUpdated"] is True def test_repeated_trial_revisions_revert_to_original_baseline(tmp_path): @@ -1040,6 +1050,7 @@ def test_irrecoverable_trial_can_keep_current_values_as_new_baseline(tmp_path): assert fake_params_cls._store["FLMActiveOverrides"]["vehicleKnobs"]["generic.turn_in_boost_left"] == pytest.approx(0.1) assert fake_params_cls._store["FLMActiveProfileId"] == "" assert fake_params_cls._store["FLMTrialApplied"] is False + assert fake_params_cls._memory_store["StarPilotTogglesUpdated"] is True def test_workspace_hydrates_display_metadata_for_existing_active_trial(tmp_path):