diff --git a/common/libcommon.a b/common/libcommon.a index 4e3ea38e2..c245bbb32 100644 Binary files a/common/libcommon.a and b/common/libcommon.a differ diff --git a/common/params_keys.h b/common/params_keys.h index 57a8a28f1..8f1346939 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -323,7 +323,7 @@ inline static std::unordered_map keys = { {"FLMActiveProfileId", {PERSISTENT, STRING, "", "", 2}}, {"FLMTrialBaseline", {PERSISTENT | DONT_LOG, JSON, "{}", "{}"}}, {"FLMTrialApplied", {PERSISTENT, BOOL, "0", "0", 2}}, - {"FPSCounter", {PERSISTENT, BOOL, "1", "0", 3}}, + {"FPSCounter", {PERSISTENT, BOOL, "0", "0", 3}}, {"GalaxyDashboardStats", {PERSISTENT | DONT_LOG, JSON, "{}", "{}"}}, {"StarPilotApiToken", {PERSISTENT | DONT_LOG, STRING, "", "", 0}}, {"StarPilotCarParams", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BYTES, "", ""}}, @@ -448,7 +448,7 @@ inline static std::unordered_map keys = { {"NoUploads", {PERSISTENT, BOOL, "0", "0", 2}}, {"NudgelessLaneChange", {PERSISTENT, BOOL, "0", "0", 0}}, {"NudgelessLaneChangeOnlyWhenEngaged", {PERSISTENT, BOOL, "0", "0", 1}}, - {"NumericalTemp", {PERSISTENT, BOOL, "1", "0", 3}}, + {"NumericalTemp", {PERSISTENT, BOOL, "0", "0", 3}}, {"Offset1", {PERSISTENT, FLOAT, "5.0", "0.0", 0}}, {"Offset2", {PERSISTENT, FLOAT, "5.0", "0.0", 0}}, {"Offset3", {PERSISTENT, FLOAT, "5.0", "0.0", 0}}, @@ -520,11 +520,11 @@ inline static std::unordered_map keys = { {"SetSpeedOffset", {PERSISTENT, FLOAT, "0.0", "0.0", 2}}, {"ShowCEMStatus", {PERSISTENT, BOOL, "1", "0", 2}}, {"ShowCCMStatus", {PERSISTENT, BOOL, "0", "0", 2}}, - {"ShowCPU", {PERSISTENT, BOOL, "1", "0", 3}}, + {"ShowCPU", {PERSISTENT, BOOL, "0", "0", 3}}, {"ShowCSCStatus", {PERSISTENT, BOOL, "1", "0", 2}}, {"ShowGPU", {PERSISTENT, BOOL, "0", "0", 3}}, {"ShowIP", {PERSISTENT, BOOL, "0", "0", 3}}, - {"ShowMemoryUsage", {PERSISTENT, BOOL, "1", "0", 3}}, + {"ShowMemoryUsage", {PERSISTENT, BOOL, "0", "0", 3}}, {"ShowModeStatusBanner", {PERSISTENT, BOOL, "1", "0", 2}}, {"ShownToggleDescriptions", {PERSISTENT, JSON, "{}", "{}"}}, {"ShowSLCOffset", {PERSISTENT, BOOL, "1", "0", 0}}, @@ -534,7 +534,7 @@ inline static std::unordered_map keys = { {"ShowStoppingPointMetrics", {PERSISTENT, BOOL, "1", "0", 3}}, {"ShowStorageLeft", {PERSISTENT, BOOL, "0", "0", 3}}, {"ShowStorageUsed", {PERSISTENT, BOOL, "0", "0", 3}}, - {"SidebarMetrics", {PERSISTENT, BOOL, "1", "0", 3}}, + {"SidebarMetrics", {PERSISTENT, BOOL, "0", "0", 3}}, {"SidebarOpen", {PERSISTENT, BOOL, "0", "0", 0}}, {"SignalAnimation", {PERSISTENT, STRING, "stock", "stock", 0}}, {"SignalMetrics", {PERSISTENT, BOOL, "0", "0", 3}}, @@ -567,7 +567,7 @@ inline static std::unordered_map keys = { {"SpeedLimitSources", {PERSISTENT, BOOL, "0", "0", 3}}, {"VisionSpeedLimitAutoBookmark", {PERSISTENT, BOOL, "0", "0", 0}}, {"VisionSpeedLimitAutoPreserveSegment", {PERSISTENT, BOOL, "0", "0", 0}}, - {"VisionSpeedLimitDetection", {PERSISTENT, BOOL, "0", "0", 0}}, + {"VisionSpeedLimitDetection", {PERSISTENT, BOOL, "1", "0", 0}}, {"VisionSpeedLimitTrainingCollector", {PERSISTENT, BOOL, "1", "1", 0}}, {"StandardFollow", {PERSISTENT, FLOAT, "1.45", "1.45", 2}}, {"StandardFollowHigh", {PERSISTENT, FLOAT, "1.2", "1.2", 2}}, diff --git a/common/params_pyx.so b/common/params_pyx.so index 3714fe7f2..60e34851d 100755 Binary files a/common/params_pyx.so and b/common/params_pyx.so differ diff --git a/opendbc_repo/opendbc/car/toyota/carcontroller.py b/opendbc_repo/opendbc/car/toyota/carcontroller.py index 47c0583a3..8a4e41701 100644 --- a/opendbc_repo/opendbc/car/toyota/carcontroller.py +++ b/opendbc_repo/opendbc/car/toyota/carcontroller.py @@ -52,12 +52,18 @@ LOCK_CMD = b"\x40\x05\x30\x11\x00\x80\x00\x00" UNLOCK_CMD = b"\x40\x05\x30\x11\x00\x40\x00\x00" +def is_ths_hybrid(CP) -> bool: + return CP.carFingerprint == CAR.TOYOTA_PRIUS or ( + CP.carFingerprint == CAR.TOYOTA_CAMRY and bool(CP.flags & ToyotaFlags.HYBRID.value) + ) + + def get_long_tune(CP, params): kiBP = [2., 5.] kiV = [0.5, 0.25] k_f = 1.0 - if CP.carFingerprint == CAR.TOYOTA_PRIUS: + if is_ths_hybrid(CP): k_f = 0.8 elif CP.carFingerprint not in TSS2_CAR: kiBP = [0., 5., 35.] @@ -440,7 +446,7 @@ class CarController(CarControllerBase): else: # constantly slowly unwind integral to recover from large temporary errors unwind_rate = ACCEL_PID_UNWIND - if self.CP.carFingerprint == CAR.TOYOTA_PRIUS and pcm_accel_cmd * self.long_pid.i < 0.0: + if is_ths_hybrid(self.CP) and pcm_accel_cmd * self.long_pid.i < 0.0: unwind_rate *= PRIUS_INTEGRAL_MISMATCH_UNWIND self.long_pid.i -= unwind_rate * float(np.sign(self.long_pid.i)) diff --git a/opendbc_repo/opendbc/car/toyota/interface.py b/opendbc_repo/opendbc/car/toyota/interface.py index b59acb1c0..95b156274 100644 --- a/opendbc_repo/opendbc/car/toyota/interface.py +++ b/opendbc_repo/opendbc/car/toyota/interface.py @@ -158,6 +158,8 @@ class CarInterface(CarInterfaceBase): ret.minEnableSpeed = -1. if (stop_and_go or ret.enableGasInterceptorDEPRECATED) else MIN_ACC_SPEED prius_long_defaults = candidate == CAR.TOYOTA_PRIUS and ret.openpilotLongitudinalControl + camry_hybrid_long_defaults = (candidate == CAR.TOYOTA_CAMRY and ret.openpilotLongitudinalControl and + bool(ret.flags & ToyotaFlags.HYBRID.value)) if candidate in TSS2_CAR or ret.enableGasInterceptorDEPRECATED or prius_long_defaults: ret.flags |= ToyotaFlags.RAISED_ACCEL_LIMIT.value @@ -170,6 +172,13 @@ class CarInterface(CarInterfaceBase): if ret.flags & ToyotaFlags.HYBRID.value: ret.longitudinalActuatorDelay = 0.05 + if camry_hybrid_long_defaults: + # The THS eCVT responds much faster than the legacy non-TSS2 ICE tune. + ret.longitudinalActuatorDelay = 0.05 + ret.vEgoStopping = 0.25 + ret.vEgoStarting = 0.25 + ret.stoppingDecelRate = 0.3 + if ret.enableGasInterceptorDEPRECATED: # Pedal/SDSU Toyotas feel best with a softer final stop clamp. ret.longitudinalActuatorDelay = max(ret.longitudinalActuatorDelay, 0.2) diff --git a/opendbc_repo/opendbc/car/toyota/tests/test_toyota.py b/opendbc_repo/opendbc/car/toyota/tests/test_toyota.py index 9f7df7e55..dc10379ec 100644 --- a/opendbc_repo/opendbc/car/toyota/tests/test_toyota.py +++ b/opendbc_repo/opendbc/car/toyota/tests/test_toyota.py @@ -8,7 +8,7 @@ from opendbc.can import CANPacker, CANParser from opendbc.car.structs import CarParams from opendbc.car.fw_versions import build_fw_dict from opendbc.car.toyota import toyotacan -from opendbc.car.toyota.carcontroller import CarController, get_prius_positive_feedforward_scale, limit_interceptor_pcm_accel, \ +from opendbc.car.toyota.carcontroller import CarController, get_long_tune, get_prius_positive_feedforward_scale, limit_interceptor_pcm_accel, \ limit_interceptor_stopping_accel, limit_no_lead_cruise_sign_flip, \ limit_prius_stopping_accel, update_permit_braking from opendbc.car.toyota.carstate import calculate_interceptor_gas_pressed @@ -82,7 +82,7 @@ class TestToyotaInterfaces: assert abs(car_params.vEgoStopping - 0.25) < 1e-6 assert abs(car_params.vEgoStarting - 0.25) < 1e-6 - def test_camry_continental_radar_keeps_standard_longitudinal_tune(self): + def test_camry_hybrid_continental_radar_uses_ths_longitudinal_tune(self): fingerprint = {bus: ({0x2FF: 8} if bus == 0 else {}) for bus in range(8)} hybrid_fw = [CarParams.CarFw(ecu=Ecu.hybrid, address=0x7D2, fwVersion=b"test")] car_params = CarInterface.get_params( @@ -98,17 +98,44 @@ class TestToyotaInterfaces: assert car_params.openpilotLongitudinalControl assert not car_params.radarUnavailable assert abs(car_params.radarTimeStepDEPRECATED - 0.1) < 1e-6 - assert abs(car_params.longitudinalActuatorDelay - 0.15) < 1e-6 - assert abs(car_params.vEgoStopping - 0.5) < 1e-6 - assert abs(car_params.vEgoStarting - 0.5) < 1e-6 - assert abs(car_params.stoppingDecelRate - 0.8) < 1e-6 + assert abs(car_params.longitudinalActuatorDelay - 0.05) < 1e-6 + assert abs(car_params.vEgoStopping - 0.25) < 1e-6 + assert abs(car_params.vEgoStarting - 0.25) < 1e-6 + assert abs(car_params.stoppingDecelRate - 0.3) < 1e-6 assert not car_params.flags & ToyotaFlags.NO_STOP_TIMER.value + controller = get_long_tune(car_params, SimpleNamespace(ACCEL_MIN=-3.5, ACCEL_MAX=2.0)) + controller.speed = 0.0 + assert controller.k_i == pytest.approx(0.5) + assert controller.k_f == pytest.approx(0.8) + radar_interface = RadarInterface(car_params) assert radar_interface.radar_acc_tssp assert radar_interface.rcp is not None assert radar_interface.pt_cp is not None + def test_camry_ice_keeps_legacy_longitudinal_tune(self): + fingerprint = {bus: ({0x2FF: 8} if bus == 0 else {}) for bus in range(8)} + car_params = CarInterface.get_params( + CAR.TOYOTA_CAMRY, + fingerprint, + [], + alpha_long=True, + is_release=False, + docs=False, + starpilot_toggles=SimpleNamespace(), + ) + + assert not car_params.flags & ToyotaFlags.HYBRID.value + assert car_params.longitudinalActuatorDelay == pytest.approx(0.15) + assert car_params.vEgoStopping == pytest.approx(0.5) + assert car_params.stoppingDecelRate == pytest.approx(0.8) + + controller = get_long_tune(car_params, SimpleNamespace(ACCEL_MIN=-3.5, ACCEL_MAX=2.0)) + controller.speed = 0.0 + assert controller.k_i == pytest.approx(3.6) + assert controller.k_f == pytest.approx(1.0) + def test_camry_continental_radar_converts_absolute_target_speed(self): radar_interface = RadarInterface.__new__(RadarInterface) radar_interface.CP = SimpleNamespace(wheelSpeedFactor=1.0) diff --git a/starpilot/navigation/mapd_wrapper.py b/starpilot/navigation/mapd_wrapper.py index 2a8ab6f51..ad2fc4163 100644 --- a/starpilot/navigation/mapd_wrapper.py +++ b/starpilot/navigation/mapd_wrapper.py @@ -10,6 +10,7 @@ from collections import defaultdict, deque from pathlib import Path from openpilot.common.basedir import BASEDIR +from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog MAPD_DIR = Path(BASEDIR) / "starpilot/navigation" @@ -19,6 +20,8 @@ RESTART_DELAY_S = 0.25 MISSING_TILE_BACKOFF_S = 30.0 FAILURE_WINDOW_S = 3.0 FAILURE_THRESHOLD = 3 +MISSING_COVERAGE_EXIT_CODE = 3 +ROAD_STATE_POLL_S = 1.0 def extract_bounds_filename(line: str) -> str | None: @@ -75,25 +78,19 @@ class CorruptTileMonitor: def quarantine_offline_tile(filename: str) -> Path | None: tile_path = Path(filename) try: - relative_path = tile_path.relative_to(OFFLINE_ROOT) + tile_path.relative_to(OFFLINE_ROOT) except ValueError: cloudlog.warning(f"mapd_wrapper refusing to quarantine unexpected path: {filename}") return None - quarantine_path = tile_path if tile_path.is_file() else None - if quarantine_path is None and len(relative_path.parts) >= 2: - archive_path = OFFLINE_ROOT / relative_path.parts[0] / f"{relative_path.parts[1]}.tar.gz" - if archive_path.is_file(): - quarantine_path = archive_path - - if quarantine_path is None: + if not tile_path.is_file(): return None - quarantined = quarantine_path.with_name(f"{quarantine_path.name}.corrupt.{int(time.time())}") + quarantined = tile_path.with_name(f"{tile_path.name}.corrupt.{int(time.time())}") try: - quarantine_path.rename(quarantined) + tile_path.rename(quarantined) except OSError: - cloudlog.exception(f"mapd_wrapper failed to quarantine offline data: {quarantine_path}") + cloudlog.exception(f"mapd_wrapper failed to quarantine offline data: {tile_path}") return None return quarantined @@ -145,6 +142,15 @@ def run_mapd_once() -> int: for line in proc.stdout: print(line, end="") bad_tile = monitor.observe(line) + + # mapd reports an unmarshal failure even when no offline tile is installed. + # Stop its resulting hot loop until the next onroad process cycle. + missing_tile = monitor.current_filename + if is_offline_read_error(line) and missing_tile is not None and not Path(missing_tile).is_file(): + cloudlog.info(f"mapd_wrapper has no offline tile for {missing_tile}; stopping mapd until the next drive") + terminate_child(proc) + return MISSING_COVERAGE_EXIT_CODE + if bad_tile is None: continue @@ -170,7 +176,14 @@ def run_mapd_once() -> int: return proc.wait() +def wait_for_road_state_change(params: Params) -> None: + initial_onroad = params.get_bool("IsOnroad") + while params.get_bool("IsOnroad") == initial_onroad: + time.sleep(ROAD_STATE_POLL_S) + + def main() -> None: + params = Params() while True: exit_code = run_mapd_once() if exit_code == 1: @@ -179,6 +192,9 @@ def main() -> None: if exit_code == 2: time.sleep(MISSING_TILE_BACKOFF_S) continue + if exit_code == MISSING_COVERAGE_EXIT_CODE: + wait_for_road_state_change(params) + continue raise SystemExit(exit_code) diff --git a/starpilot/navigation/test_mapd_wrapper.py b/starpilot/navigation/test_mapd_wrapper.py index e0a148d8e..a3ce7a16b 100644 --- a/starpilot/navigation/test_mapd_wrapper.py +++ b/starpilot/navigation/test_mapd_wrapper.py @@ -4,7 +4,7 @@ import subprocess from pathlib import Path -from openpilot.starpilot.navigation.mapd_wrapper import CorruptTileMonitor, quarantine_offline_tile, terminate_child +from openpilot.starpilot.navigation.mapd_wrapper import CorruptTileMonitor, quarantine_offline_tile, run_mapd_once, terminate_child, wait_for_road_state_change def _loading_line(filename: str) -> str: @@ -43,21 +43,60 @@ def test_quarantine_offline_tile_renames_file(tmp_path, monkeypatch): assert Path(quarantined).name.startswith(f"{tile.name}.corrupt.") -def test_quarantine_offline_tile_renames_backing_archive(tmp_path, monkeypatch): +def test_quarantine_offline_tile_ignores_missing_file(tmp_path, monkeypatch): offline_root = tmp_path / "offline" - archive = offline_root / "34/-88.tar.gz" - archive.parent.mkdir(parents=True) - archive.write_text("bad archive") - virtual_tile = offline_root / "34/-88/34.750000_-87.750000_35.000000_-87.500000" + missing_tile = offline_root / "34/-88/34.750000_-87.750000_35.000000_-87.500000" monkeypatch.setitem(quarantine_offline_tile.__globals__, "OFFLINE_ROOT", offline_root) - quarantined = quarantine_offline_tile(virtual_tile.as_posix()) + assert quarantine_offline_tile(missing_tile.as_posix()) is None - assert quarantined is not None - assert not archive.exists() - assert Path(quarantined).exists() - assert Path(quarantined).name.startswith(f"{archive.name}.corrupt.") + +def test_run_mapd_once_stops_for_missing_offline_coverage(tmp_path, monkeypatch): + missing_tile = tmp_path / "offline/36/-98/37.500000_-98.000000_37.750000_-97.750000" + output = [] + for _ in range(4): + output.extend((_loading_line(missing_tile.as_posix()), _error_line())) + + class CompletedProcess: + pid = 123 + + def __init__(self): + self.stdout = iter(f"{line}\n" for line in output) + self.terminated = False + + def poll(self): + return 0 if self.terminated else None + + def terminate(self): + self.terminated = True + + def wait(self, timeout=None): + return 0 + + proc = CompletedProcess() + monkeypatch.setitem(run_mapd_once.__globals__, "OFFLINE_ROOT", tmp_path / "offline") + monkeypatch.setattr(subprocess, "Popen", lambda *args, **kwargs: proc) + monkeypatch.setattr("signal.signal", lambda *args: None) + + assert run_mapd_once() == 3 + assert proc.terminated + + +def test_missing_coverage_waits_for_road_state_change(monkeypatch): + class Params: + states = iter((True, True, False)) + + def get_bool(self, key): + assert key == "IsOnroad" + return next(self.states) + + sleeps = [] + monkeypatch.setattr("time.sleep", sleeps.append) + + wait_for_road_state_change(Params()) + + assert sleeps == [1.0] def test_terminate_child_tolerates_wedged_process(): diff --git a/system/manager/launch_param_migrations.py b/system/manager/launch_param_migrations.py index 4d47478ff..12ee4a536 100644 --- a/system/manager/launch_param_migrations.py +++ b/system/manager/launch_param_migrations.py @@ -10,6 +10,15 @@ LONG_PITCH_KEY = "LongPitch" STEER_KP_KEY = "SteerKP" STEER_KP_STOCK_KEY = "SteerKPStock" USE_OLD_UI_KEY = "UseOldUI" +VISION_SPEED_LIMIT_DETECTION_KEY = "VisionSpeedLimitDetection" +DEVELOPER_METRIC_DISPLAY_KEYS = ( + "FPSCounter", + "ShowCPU", + "ShowGPU", + "NumericalTemp", + "ShowMemoryUsage", + "SidebarMetrics", +) DEFAULT_STEER_KP = 0.6 LEGACY_STEER_KP = 0.7 @@ -20,6 +29,8 @@ BRANCH_DEFAULTS_MIGRATION_MARKER = ".starpilot_branch_defaults_migrations_v1" ACCELERATION_PROFILE_MIGRATION_MARKER = ".starpilot_acceleration_profile_default_v1" USE_OLD_UI_MIGRATION_MARKER = ".starpilot_use_old_ui_migration_v2" LATERAL_METHOD_REBRAND_MIGRATION_MARKER = ".starpilot_lateral_method_rebrand_v1" +VISION_SPEED_LIMIT_DETECTION_MIGRATION_MARKER = ".starpilot_vision_speed_limit_detection_v1" +DEVELOPER_METRIC_DISPLAY_MIGRATION_MARKER = ".starpilot_developer_metric_display_off_v1" MARKER_DIRNAME = ".starpilot_param_migrations" LATERAL_METHOD_PARAM_SUFFIXES = ( @@ -99,6 +110,14 @@ def _lateral_method_rebrand_marker_path(params: ParamsLike) -> Path: return _marker_dir_path(params) / LATERAL_METHOD_REBRAND_MIGRATION_MARKER +def _vision_speed_limit_detection_marker_path(params: ParamsLike) -> Path: + return _marker_dir_path(params) / VISION_SPEED_LIMIT_DETECTION_MIGRATION_MARKER + + +def _developer_metric_display_marker_path(params: ParamsLike) -> Path: + return _marker_dir_path(params) / DEVELOPER_METRIC_DISPLAY_MIGRATION_MARKER + + def _marker_dir_path(params: ParamsLike) -> Path: params_path = Path(params.get_param_path()) # Params.clear_all() removes unknown files inside the params directory, so @@ -208,11 +227,36 @@ def _apply_lateral_method_rebrand_migration(params: ParamsLike, marker: Path) -> marker.touch() +def _apply_vision_speed_limit_detection_migration(params: ParamsLike, marker: Path) -> None: + if marker.exists(): + return + + marker.parent.mkdir(parents=True, exist_ok=True) + + params.put_bool(VISION_SPEED_LIMIT_DETECTION_KEY, True) + + marker.touch() + + +def _apply_developer_metric_display_migration(params: ParamsLike, marker: Path) -> None: + if marker.exists(): + return + + marker.parent.mkdir(parents=True, exist_ok=True) + + for key in DEVELOPER_METRIC_DISPLAY_KEYS: + params.put_bool(key, False) + + marker.touch() + + def apply_launch_param_migrations(params: ParamsLike, marker_path: Path | None = None, branch_defaults_marker_path: Path | None = None, acceleration_profile_marker_path: Path | None = None, use_old_ui_marker_path: Path | None = None, - lateral_method_rebrand_marker_path: Path | None = None) -> None: + lateral_method_rebrand_marker_path: Path | None = None, + vision_speed_limit_detection_marker_path: Path | None = None, + developer_metric_display_marker_path: Path | None = None) -> None: _apply_legacy_launch_param_migrations(params, marker_path or _default_marker_path(params)) # Keep branch-default rollout on its own marker so older installs that already # have the legacy marker still receive this one-time param reset. @@ -224,6 +268,12 @@ def apply_launch_param_migrations(params: ParamsLike, marker_path: Path | None = _apply_lateral_method_rebrand_migration( params, lateral_method_rebrand_marker_path or _lateral_method_rebrand_marker_path(params) ) + _apply_vision_speed_limit_detection_migration( + params, vision_speed_limit_detection_marker_path or _vision_speed_limit_detection_marker_path(params) + ) + _apply_developer_metric_display_migration( + params, developer_metric_display_marker_path or _developer_metric_display_marker_path(params) + ) def main() -> int: diff --git a/system/manager/test/test_launch_param_migrations.py b/system/manager/test/test_launch_param_migrations.py index d514bcb19..c26184cdf 100644 --- a/system/manager/test/test_launch_param_migrations.py +++ b/system/manager/test/test_launch_param_migrations.py @@ -3,12 +3,15 @@ from pathlib import Path from openpilot.system.manager.launch_param_migrations import ( ACCELERATION_PROFILE_MIGRATION_MARKER, BRANCH_DEFAULTS_MIGRATION_MARKER, + DEVELOPER_METRIC_DISPLAY_KEYS, + DEVELOPER_METRIC_DISPLAY_MIGRATION_MARKER, DEFAULT_STEER_KP, LAUNCH_PARAM_MIGRATION_MARKER, LATERAL_METHOD_REBRAND_MIGRATION_MARKER, MARKER_DIRNAME, STANDARD_ACCELERATION_PROFILE, USE_OLD_UI_MIGRATION_MARKER, + VISION_SPEED_LIMIT_DETECTION_MIGRATION_MARKER, apply_launch_param_migrations, ) @@ -258,3 +261,51 @@ def test_apply_launch_param_migrations_preserves_active_lateral_method_trial(tmp assert params.get(f"FLM{suffix}") == value assert not Path(params.get_param_path(f"{legacy_prefix}{suffix}")).exists() assert marker_path(tmp_path, LATERAL_METHOD_REBRAND_MIGRATION_MARKER).is_file() + + +def test_apply_launch_param_migrations_enables_vision_speed_limit_detection_once(tmp_path): + params = FileBackedFakeParams(tmp_path / "params") + params.put_bool("VisionSpeedLimitDetection", False) + params.put("SLCPriority1", "Dashboard") + params.put("SLCPriority2", "Map Data") + + apply_launch_param_migrations(params) + + assert params.get_bool("VisionSpeedLimitDetection") + assert params.get("SLCPriority1") == "Dashboard" + assert params.get("SLCPriority2") == "Map Data" + assert marker_path(tmp_path, VISION_SPEED_LIMIT_DETECTION_MIGRATION_MARKER).is_file() + + +def test_apply_launch_param_migrations_does_not_reenable_vision_speed_limit_detection_after_marker(tmp_path): + params = FileBackedFakeParams(tmp_path / "params") + params.put_bool("VisionSpeedLimitDetection", False) + marker_path(tmp_path, VISION_SPEED_LIMIT_DETECTION_MIGRATION_MARKER).touch() + + apply_launch_param_migrations(params) + + assert not params.get_bool("VisionSpeedLimitDetection") + + +def test_apply_launch_param_migrations_disables_developer_metric_display_once(tmp_path): + params = FileBackedFakeParams(tmp_path / "params") + for key in DEVELOPER_METRIC_DISPLAY_KEYS: + params.put_bool(key, True) + + apply_launch_param_migrations(params) + + for key in DEVELOPER_METRIC_DISPLAY_KEYS: + assert not params.get_bool(key) + assert marker_path(tmp_path, DEVELOPER_METRIC_DISPLAY_MIGRATION_MARKER).is_file() + + +def test_apply_launch_param_migrations_preserves_developer_metric_display_after_marker(tmp_path): + params = FileBackedFakeParams(tmp_path / "params") + for key in DEVELOPER_METRIC_DISPLAY_KEYS: + params.put_bool(key, True) + marker_path(tmp_path, DEVELOPER_METRIC_DISPLAY_MIGRATION_MARKER).touch() + + apply_launch_param_migrations(params) + + for key in DEVELOPER_METRIC_DISPLAY_KEYS: + assert params.get_bool(key)