"""Saved Torque/AOL UI actions against real Params, CarParams and runtime parsers.""" from unittest.mock import Mock from openpilot.starpilot.ui.presentation import BitmapFonts from dataclasses import replace from pathlib import Path import tempfile import unittest from types import SimpleNamespace from unittest.mock import patch from opendbc.car import gen_empty_fingerprint from opendbc.car.car_helpers import interfaces from opendbc.car.honda.interface import CarInterface from opendbc.car.honda.values import CAR as HONDA from opendbc.car.hyundai.interface import CarInterface as HyundaiInterface from opendbc.car.hyundai.ioniq6_handoff import build_ioniq6_hda2_long_candidate from opendbc.car.hyundai.values import CAR as HYUNDAI from openpilot.common.params import Params from openpilot.starpilot.aol.intent import AolCardIntent, CRUISE_LONG_PRESS, independent_axis_requested, read_settings as read_aol_settings from openpilot.starpilot.car.hyundai.aol import qualified_ioniq6 from openpilot.starpilot.aol.runtime import decide_axes from openpilot.selfdrive.car.tests.test_hyundai_aol import candidate as ioniq6_candidate, state as ioniq6_state from opendbc.car.structs import car from openpilot.starpilot.lateral.torque_runtime import read_settings as read_torque_settings from openpilot.starpilot.lateral.torque_settings import DOCUMENT_KEY, parse_document from openpilot.starpilot.lateral.torque_tuning import TorqueSource, TorqueTuning from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner from openpilot.starpilot.ui.feature_settings_state import ( FEATURE_ROW_TOP, FeatureInput, FeatureRow, FeatureSettingsRequest, FeatureSettingsState, row_change, row_default, ) from openpilot.starpilot.ui import feature_settings_compact as compact from openpilot.starpilot.ui import feature_settings as large from openpilot.starpilot.ui.presentation import Profile def required_change(row: FeatureRow, direction: int = 1) -> FeatureSettingsRequest: request = row_change(row, direction) if request is None: raise AssertionError(f"Expected an editable row: {row.key}") return request class LateralFeatureSettingsTests(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory() self.addCleanup(self.temp.cleanup) self.params = Params(self.temp.name) # These action/repair cases start from an explicit saved Off preference. self.params.put_bool("AlwaysOnLateral", False, block=True) self.parked = True self.cp = interfaces[HYUNDAI.HYUNDAI_IONIQ_6].get_non_essential_params(HYUNDAI.HYUNDAI_IONIQ_6) self.owner = FeatureSettingsOwner(self.params, lambda group: self.parked, vehicle_fingerprint=lambda: str(self.cp.carFingerprint), vehicle_params=lambda: self.cp) def row(self, page, key): state = self.owner.snapshot(page, parked=True, system_long=True, lateral_context=True, metric=False) return next(item for item in state.rows if item.key == key) def test_global_steering_pause_rows_units_defaults_and_angle_vehicle(self): from opendbc.car.ford.values import CAR as FORD for angle in (False, True): if angle: self.cp = interfaces[FORD.FORD_ESCAPE_MK4].get_non_essential_params(FORD.FORD_ESCAPE_MK4) self.params.put_bool('IsMetric', False, block=True) speed = self.row('aol', 'PauseLateralSpeed') steering = self.owner.snapshot('torque', parked=True, system_long=True, lateral_context=True, metric=False) self.assertFalse(any(row.key in ('PauseLateralSpeed', 'PauseLateralOnSignal', 'LateralResumeDelay') for row in steering.rows)) self.assertTrue(speed.available) self.assertEqual((speed.value, speed.unit), ('0', 'mph')) self.assertTrue(self.owner.apply(required_change(speed))) stale = required_change(self.row('aol', 'PauseLateralSpeed')) self.params.put_bool('IsMetric', True, block=True) self.assertFalse(self.owner.apply(stale)) self.assertEqual(self.row('aol', 'PauseLateralSpeed').unit, 'km/h') reset = row_default(self.row('aol', 'PauseLateralSpeed')) assert reset is not None self.assertTrue(self.owner.apply(reset)) self.assertEqual(self.params.get('PauseLateralSpeed'), 0.) signal = self.row('aol', 'PauseLateralOnSignal') self.assertTrue(self.owner.apply(required_change(signal))) self.assertTrue(self.params.get_bool('PauseLateralOnSignal')) self.assertTrue(self.owner.apply(required_change(self.row('aol', 'PauseLateralOnSignal'), -1))) self.assertFalse(self.params.get_bool('PauseLateralOnSignal')) self.assertFalse(self.owner.apply(replace(required_change(self.row('aol', 'LateralResumeDelay')), value='nan'))) Path(self.params.get_param_path('PauseLateralSpeed')).write_bytes(b'nan') repair = self.row('aol', 'PauseLateralSpeed') self.assertEqual(repair.repair_value, '0') request = required_change(repair) self.assertFalse(self.owner.apply(replace(request, value='5'))) self.assertTrue(self.owner.apply(request)) self.params.put('PauseLateralSpeed', 0., block=True) def test_default_clears_only_selected_torque_override_and_rejects_stale_source(self): self.assertTrue(self.owner.apply(required_change(self.row("torque", "torque:factor:value")))) self.assertTrue(self.owner.apply(required_change(self.row("torque", "torque:friction:value")))) row = self.row("torque", "torque:factor:value") request = row_default(row) assert request is not None self.assertEqual(request.key, "torque:factor:mode") self.assertEqual(request.value, "Vehicle/learned") tune = self.cp.lateralTuning.torque vehicle = TorqueTuning(TorqueSource.VEHICLE, str(self.cp.carFingerprint), tune.latAccelFactor, tune.latAccelOffset, tune.friction) before = read_torque_settings(self.params, vehicle) self.assertTrue(self.owner.apply(request)) after = read_torque_settings(self.params, vehicle) self.assertIsNone(after.user_factor) self.assertEqual(after.user_friction, before.user_friction) self.assertFalse(self.owner.apply(request)) self.assertEqual(self.row("torque", "torque:factor:value").reason, "Uses the supplied vehicle tune") def test_torque_custom_values_and_learning_preference_are_independent(self): rows = self.owner.snapshot("torque", parked=True, system_long=True, lateral_context=True, metric=False).rows self.assertFalse(any(row.key in ("AdvancedLateralTune", "torque_adopt") for row in rows)) factor = self.row("torque", "torque:factor:value") self.assertTrue(self.owner.apply(required_change(factor))) tune = self.cp.lateralTuning.torque vehicle = TorqueTuning(TorqueSource.VEHICLE, str(self.cp.carFingerprint), tune.latAccelFactor, tune.latAccelOffset, tune.friction) saved = read_torque_settings(self.params, vehicle) self.assertTrue(saved.valid) self.assertIsNotNone(saved.user_factor) self.assertIsNone(saved.user_friction) self.assertIsNone(self.params.get("AdvancedLateralTune")) self.assertIsNone(self.params.get("ForceAutoTuneOff")) self.assertTrue(self.owner.apply(required_change(self.row("torque", "torque:factor:reset")))) self.assertIsNone(read_torque_settings(self.params, vehicle).user_factor) def test_onroad_saved_torque_edit_keeps_vehicle_binding_and_parked_repair(self): owner = FeatureSettingsOwner(self.params, lambda group: group != "parked_preferences", vehicle_fingerprint=lambda: str(self.cp.carFingerprint), vehicle_params=lambda: self.cp) page = owner.snapshot("torque", parked=False, system_long=False, lateral_context=True, metric=False, configure_while_driving=True) factor = next(row for row in page.rows if row.key == "torque:factor:value") request = required_change(factor) self.assertTrue(factor.available) self.assertTrue(owner.apply(request)) self.assertFalse(owner.apply(request)) current = next(row for row in owner.snapshot("torque", parked=False, system_long=False, lateral_context=True, metric=False, configure_while_driving=True).rows if row.key == factor.key) self.cp.carFingerprint = "TOYOTA_CAMRY" self.assertFalse(owner.apply(required_change(current))) def test_torque_corrupt_dependent_preserves_bytes_until_confirmed_reset(self): Path(self.params.get_param_path("SteerLatAccel")).write_bytes(b"nan") page = self.owner.snapshot("torque", parked=True, system_long=True, lateral_context=True, metric=False) self.assertFalse(any(row.key == "torque:factor:value" for row in page.rows)) self.assertEqual(Path(self.params.get_param_path("SteerLatAccel")).read_bytes(), b"nan") reset = next(row for row in page.rows if row.key == "torque_reset") self.assertFalse(reset.available) Path(self.params.get_param_path(DOCUMENT_KEY)).write_bytes(b"bad") self.assertTrue(self.owner.apply(self.confirm(self.row("torque", "torque_reset")))) self.assertEqual(Path(self.params.get_param_path("SteerLatAccel")).read_bytes(), b"nan") self.assertTrue(self.row("torque", "torque:factor:value").available) def test_torque_corrupt_learning_preference_blocks_numeric_write(self): request = required_change(self.row("torque", "torque:factor:value")) Path(self.params.get_param_path("ForceAutoTuneOff")).write_bytes(b"maybe") self.assertFalse(self.owner.apply(request)) self.assertEqual(Path(self.params.get_param_path("ForceAutoTuneOff")).read_bytes(), b"maybe") self.honda() page = self.owner.snapshot("torque", parked=True, system_long=True, lateral_context=True, metric=False) self.assertFalse(any(row.key == "torque:factor:value" for row in page.rows)) def test_unreadable_saved_source_is_visible_and_not_writable(self): self.honda() path = Path(self.params.get_param_path("AolBrakePauseSpeedMps")) path.write_bytes(b"4") from openpilot.starpilot.ui import feature_settings_owner original = feature_settings_owner.read_saved def unreadable(params, key, limit): return (b"", False) if key == "AolBrakePauseSpeedMps" else original(params, key, limit) with patch.object(feature_settings_owner, "read_saved", side_effect=unreadable): threshold = self.row("aol", "AolBrakePauseSpeedMps") self.assertFalse(threshold.available) self.assertFalse(self.row("aol", "AlwaysOnLateral").available) self.assertEqual(path.read_bytes(), b"4") def test_torque_capability_tuple_and_parked_evidence_rechecked(self): request = required_change(self.row("torque", "torque:factor:value")) self.cp.lateralTuning.torque.latAccelFactor *= 1.1 self.assertFalse(self.owner.apply(request)) current = required_change(self.row("torque", "torque:factor:value")) self.parked = False self.assertFalse(self.owner.apply(current)) @staticmethod def confirm(row): return FeatureSettingsRequest(row.key, row.source, "confirm", confirmation=True, vehicle_fingerprint=row.vehicle_fingerprint, capability=row.capability, dependencies=row.dependencies) def test_torque_inferred_custom_partial_and_reset_preserve_legacy(self): self.params.put("SteerLatAccel", self.cp.lateralTuning.torque.latAccelFactor * 1.1, block=True) old = Path(self.params.get_param_path("SteerLatAccel")).read_bytes() factor = self.row("torque", "torque:factor:value") self.assertEqual(factor.reason, "Uses your custom value") self.assertTrue(self.owner.apply(required_change(self.row("torque", "torque:friction:value")))) profile = parse_document(Path(self.params.get_param_path(DOCUMENT_KEY)).read_bytes())[str(self.cp.carFingerprint)] self.assertEqual(profile.factor.mode, "custom") self.assertEqual(profile.friction.mode, "custom") self.assertTrue(self.owner.apply(required_change(self.row("torque", "torque:factor:reset")))) profile = parse_document(Path(self.params.get_param_path(DOCUMENT_KEY)).read_bytes())[str(self.cp.carFingerprint)] self.assertEqual(profile.factor.mode, "source") self.assertEqual(profile.friction.mode, "custom") self.assertEqual(Path(self.params.get_param_path("SteerLatAccel")).read_bytes(), old) def test_torque_stale_source_capability_and_invalid_reset(self): request = required_change(self.row("torque", "torque:factor:value")) self.assertTrue(self.owner.apply(request)) self.assertFalse(self.owner.apply(request)) old_request = required_change(self.row("torque", "torque:factor:value")) Path(self.params.get_param_path(DOCUMENT_KEY)).write_bytes(b"bad") self.assertFalse(self.owner.apply(old_request)) self.assertTrue(self.owner.apply(self.confirm(self.row("torque", "torque_reset")))) self.assertEqual(parse_document(Path(self.params.get_param_path(DOCUMENT_KEY)).read_bytes()), {}) def test_torque_final_confirmation_rechecks_readability_and_fingerprint(self): Path(self.params.get_param_path(DOCUMENT_KEY)).write_bytes(b"bad") request = self.confirm(self.row("torque", "torque_reset")) original = self.owner.torque._readable with patch.object(self.owner.torque, "_readable", side_effect=lambda key: key != DOCUMENT_KEY and original(key)), \ patch.object(self.params, "put", wraps=self.params.put) as put: self.assertFalse(self.owner.apply(request)) put.assert_not_called() with patch.object(self.owner.torque, "vehicle_fingerprint", return_value="TOYOTA_CAMRY"), \ patch.object(self.params, "put", wraps=self.params.put) as put: self.assertFalse(self.owner.apply(request)) put.assert_not_called() def test_torque_basis_change_pauses_custom_until_confirmed_review(self): self.assertTrue(self.owner.apply(required_change(self.row("torque", "torque:factor:value")))) self.cp.lateralTuning.torque.latAccelFactor *= 1.05 state = self.owner.snapshot("torque", parked=True, system_long=True, lateral_context=True, metric=False) self.assertTrue(any(row.key == "torque_rebase" for row in state.rows)) review = self.row("torque", "torque_rebase") self.assertIn("Lateral acceleration", review.value) self.assertFalse(self.owner.apply(replace(self.confirm(review), confirmation=False))) self.assertTrue(self.owner.apply(self.confirm(review))) profile = parse_document(Path(self.params.get_param_path(DOCUMENT_KEY)).read_bytes())[str(self.cp.carFingerprint)] self.assertEqual(profile.factor.mode, "custom") self.cp.lateralTuning.torque.latAccelFactor *= 3 self.assertFalse(self.owner.apply(self.confirm(self.row("torque", "torque_rebase")))) self.assertTrue(self.owner.apply(self.confirm(self.row("torque", "torque_reset_profile")))) profile = parse_document(Path(self.params.get_param_path(DOCUMENT_KEY)).read_bytes())[str(self.cp.carFingerprint)] self.assertEqual(profile.factor.mode, "source") def honda(self): self.cp = CarInterface.get_params(HONDA.HONDA_CIVIC_BOSCH, gen_empty_fingerprint(), [], True, False, False) self.cp.safetyConfigs[-1].safetyParam |= 0x20 def test_aol_explicit_metric_to_si_write_preserves_legacy(self): self.honda() self.params.put_bool("IsMetric", True, block=True) self.params.put("PauseAOLOnBrake", 10.0, block=True) legacy = Path(self.params.get_param_path("PauseAOLOnBrake")).read_bytes() threshold = self.row("aol", "AolBrakePauseSpeedMps") self.assertEqual((threshold.value, threshold.unit), ("36.0", "km/h")) self.assertTrue(self.owner.apply(required_change(threshold))) self.assertEqual(Path(self.params.get_param_path("PauseAOLOnBrake")).read_bytes(), legacy) self.assertAlmostEqual(read_aol_settings(self.params).pause_brake_mps, 37 / 3.6) def test_aol_threshold_source_and_units_stale_release(self): self.honda() self.params.put("PauseAOLOnBrake", 10.0, block=True) request = required_change(self.row("aol", "AolBrakePauseSpeedMps")) self.params.put_bool("IsMetric", True, block=True) self.assertFalse(self.owner.apply(request)) self.params.remove("IsMetric") self.params.put("PauseAOLOnBrake", 11.0, block=True) self.assertFalse(self.owner.apply(request)) self.params.put("PauseAOLOnBrake", 10.0, block=True) self.params.put("AolBrakePauseSpeedMps", 1.0, block=True) self.assertFalse(self.owner.apply(request)) def test_fresh_honda_without_native_bit_can_save_request_preference(self): self.cp = CarInterface.get_params(HONDA.HONDA_CIVIC_BOSCH, gen_empty_fingerprint(), [], True, False, False) self.assertEqual(self.cp.safetyConfigs[-1].safetyParam & 0x20, 0) master = self.row("aol", "AlwaysOnLateral") self.assertTrue(master.available) self.assertTrue(self.owner.apply(required_change(master))) saved = read_aol_settings(self.params) self.assertTrue(saved.enabled) self.assertTrue(independent_axis_requested(saved)) self.cp.safetyConfigs[-1].safetyParam |= 0x20 self.assertTrue(self.row("aol", "AlwaysOnLateral").available) def test_ioniq6_stock_cp_exposes_saved_requests_without_claiming_live_long(self): fp = gen_empty_fingerprint() fp[2].update({0x50: 16, 0x2A4: 24}) fp[1].update({0x1CF: 8, 0x1AA: 16, 0x35: 32, 0x175: 24, 0xA0: 24, 0xEA: 24, 0x1BA: 24, 0x1E5: 16, 0x36A: 16}) fp[0][0x3A5] = 24 self.cp = HyundaiInterface.get_params(HYUNDAI.HYUNDAI_IONIQ_6, fp, [], False, False, False) self.assertEqual(self.cp.safetyConfigs[0].safetyParam, 0x11) master = self.row("aol", "AlwaysOnLateral") self.assertTrue(master.available) self.assertIn("independently of cruise control", master.reason) self.assertFalse(any(row.key == "wheel:LKASButtonControl" for row in self.owner.snapshot( "wheel", parked=True, system_long=True, lateral_context=True, metric=False).rows)) nostalgia = self.row("vehicle", "NostalgiaMode") self.assertTrue(nostalgia.available) self.assertTrue(self.owner.apply(required_change(nostalgia))) self.assertTrue(self.params.get_bool("NostalgiaMode")) self.assertTrue(self.owner.apply(required_change(master))) self.assertTrue(read_aol_settings(self.params).enabled) self.assertFalse(self.cp.openpilotLongitudinalControl) tagged = build_ioniq6_hda2_long_candidate(self.cp, fp) self.assertIsNotNone(tagged) self.cp = tagged self.assertTrue(self.row("vehicle", "NostalgiaMode").available) tagged.safetyConfigs[0].safetyParam |= 0x800 self.cp = tagged self.assertTrue(self.row("aol", "AlwaysOnLateral").available) self.assertFalse(any(row.key == "wheel:LKASButtonControl" for row in self.owner.snapshot( "wheel", parked=True, system_long=True, lateral_context=True, metric=False).rows)) def test_ioniq5_left_paddle_setting_uses_existing_long_configuration(self): from openpilot.starpilot.tests.test_nostalgia import _ioniq5_params self.cp = _ioniq5_params() row = self.row("vehicle", "NostalgiaMode") self.assertTrue(row.available) self.assertEqual(row.label, "Left Paddle Longitudinal Pause") self.assertTrue(self.owner.apply(required_change(row))) self.assertTrue(self.params.get_bool("NostalgiaMode")) def test_ioniq6_nostalgia_is_independent_of_aol_master_and_exact_source(self): fp = gen_empty_fingerprint() fp[2].update({0x50: 16, 0x2A4: 24}) fp[1].update({0x1CF: 8, 0x1AA: 16, 0x35: 32, 0x175: 24, 0xA0: 24, 0xEA: 24, 0x1BA: 24, 0x1E5: 16, 0x36A: 16}) fp[0][0x3A5] = 24 stock = HyundaiInterface.get_params(HYUNDAI.HYUNDAI_IONIQ_6, fp, [], False, False, False) self.cp = stock self.assertTrue(self.row("vehicle", "NostalgiaMode").available) tagged = build_ioniq6_hda2_long_candidate(stock, fp) self.assertIsNotNone(tagged) self.cp = tagged self.assertTrue(self.row("vehicle", "NostalgiaMode").available) tagged.safetyConfigs[0].safetyParam |= 0x800 self.cp = tagged row = self.row("vehicle", "NostalgiaMode") self.assertEqual(row.value, "Off") self.assertTrue(row.available) self.assertFalse(self.params.get_bool("AlwaysOnLateral")) self.assertTrue(self.owner.apply(required_change(row))) self.assertTrue(self.params.get_bool("NostalgiaMode")) self.assertFalse(self.params.get_bool("AlwaysOnLateral")) old = self.row("vehicle", "NostalgiaMode") self.params.put_bool("NostalgiaMode", False, block=True) self.assertFalse(self.owner.apply(required_change(old))) path = Path(self.params.get_param_path("NostalgiaMode")) path.write_bytes(b"01") repair = self.row("vehicle", "NostalgiaMode") self.assertEqual(repair.repair_value, "Off") self.assertTrue(self.owner.apply(required_change(repair))) self.assertEqual(path.read_bytes(), b"0") on = required_change(self.row("vehicle", "NostalgiaMode")) self.parked = False self.assertFalse(self.owner.apply(on)) def test_aol_invalid_source_explicit_repair_and_master_guard(self): self.honda() path = Path(self.params.get_param_path("AolBrakePauseSpeedMps")) path.write_bytes(b"bad") master = self.row("aol", "AlwaysOnLateral") self.assertFalse(master.available) self.params.put_bool("AlwaysOnLateral", True, block=True) self.assertTrue(self.owner.apply(required_change(self.row("aol", "AlwaysOnLateral")))) self.assertFalse(self.params.get_bool("AlwaysOnLateral")) repair = self.row("aol", "AolBrakePauseSpeedMps") self.assertEqual(repair.repair_value, "0") self.assertTrue(self.owner.apply(required_change(repair))) self.assertEqual(self.params.get("AolBrakePauseSpeedMps"), 0.0) master = self.row("aol", "AlwaysOnLateral") self.assertTrue(self.owner.apply(required_change(master))) self.assertTrue(read_aol_settings(self.params).enabled) def test_wheel_page_moves_assignments_without_moving_feature_settings(self): self.honda() def snapshot(page): return self.owner.snapshot(page, parked=True, system_long=True, lateral_context=True, metric=False) self.assertIn("wheel", [row.page for row in snapshot("hub").rows]) self.assertEqual({row.key for row in snapshot("aol").rows}, {"AlwaysOnLateral", "AolBrakePauseSpeedMps", "PauseLateralSpeed", "PauseLateralOnSignal", "LateralResumeDelay"}) self.assertIn("wheel:LKASButtonControl", {row.key for row in snapshot("wheel").rows}) self.cp = interfaces["TOYOTA_COROLLA_TSS2"].get_non_essential_params("TOYOTA_COROLLA_TSS2") self.assertFalse(any(row.key.endswith("ModeButtonControl") for row in snapshot("wheel").rows)) def test_aol_button_intent_and_safety_tuple(self): self.honda() self.params.put("LKASButtonControl", 0, block=True) row = self.row("wheel", "wheel:LKASButtonControl") request = FeatureSettingsRequest(row.key, row.source, "Pause steering", capability=row.capability, vehicle_fingerprint=row.vehicle_fingerprint, dependencies=row.dependencies) self.assertTrue(self.owner.apply(request)) self.assertEqual(read_aol_settings(self.params).lkas_action, 3) self.assertFalse(any(item.key == "CancelButtonControl" for item in self.owner.snapshot("aol", parked=True, system_long=True, lateral_context=True, metric=False).rows)) main = self.row("wheel", "wheel:MainCruiseButtonControl") self.cp.safetyConfigs[-1].safetyParam &= ~0x20 self.assertFalse(self.owner.apply(required_change(main))) def test_wheel_assignment_can_be_saved_in_drive_without_enabling_aol_master(self): self.honda() self.params.put("LKASButtonControl", 0, block=True) self.parked = False self.owner = FeatureSettingsOwner(self.params, lambda group: group == "preferences", vehicle_fingerprint=lambda: str(self.cp.carFingerprint), vehicle_params=lambda: self.cp) state = self.owner.snapshot("wheel", parked=False, system_long=False, lateral_context=False, metric=False) row = next(row for row in state.rows if row.key == "wheel:LKASButtonControl") self.assertTrue(row.available) request = FeatureSettingsRequest(row.key, row.source, "Pause steering", capability=row.capability, vehicle_fingerprint=row.vehicle_fingerprint, dependencies=row.dependencies) self.assertTrue(self.owner.apply(request)) self.assertEqual(read_aol_settings(self.params).lkas_action, 3) self.assertFalse(self.owner.snapshot("aol", parked=False, system_long=False, lateral_context=False, metric=False).rows[0].available) main = next(row for row in self.owner.snapshot("wheel", parked=False, system_long=False, lateral_context=False, metric=False).rows if row.key == "wheel:MainCruiseButtonControl") self.cp.safetyConfigs[-1].safetyParam &= ~0x20 self.assertFalse(self.owner.apply(required_change(main))) def test_ioniq_distance_saved_pause_reaches_only_native_acknowledged_axes(self): stock, long_cp = ioniq6_candidate(False) self.cp = stock self.params.put_bool("AlwaysOnLateral", True, block=True) self.assertFalse(any(row.key == "wheel:LKASButtonControl" for row in self.owner.snapshot( "wheel", parked=True, system_long=True, lateral_context=True, metric=False).rows)) self.assertFalse(any(row.key == "wheel:MainCruiseButtonControl" for row in self.owner.snapshot( "wheel", parked=True, system_long=True, lateral_context=True, metric=False).rows)) cases = (("DistanceButtonControl", "Pause steering", 1, (False, True), True), ("LongDistanceButtonControl", "Pause longitudinal", CRUISE_LONG_PRESS, (True, False), True), ("VeryLongDistanceButtonControl", "Pause steering", CRUISE_LONG_PRESS * 5, (False, True), True), ("DistanceButtonControl", "Pause steering", 1, (False, True), False)) for key, choice, ticks, expected, aol_on in cases: with self.subTest(key=key, aol_on=aol_on): self.params.put_bool("AlwaysOnLateral", aol_on, block=True) row = self.row("wheel", "wheel:" + key) self.assertTrue(row.available) self.assertTrue({"Off", "Pause steering", "Pause longitudinal"}.issubset(row.choices)) request = FeatureSettingsRequest("wheel:" + key, row.source, choice, vehicle_fingerprint=row.vehicle_fingerprint, capability=row.capability, dependencies=row.dependencies) self.assertTrue(self.owner.apply(request)) slot = ("DistanceButtonControl", "LongDistanceButtonControl", "VeryLongDistanceButtonControl").index(key) self.assertEqual(read_aol_settings(self.params).distance_actions[slot], 3 if choice == "Pause steering" else 4) self.cp = long_cp.as_reader().as_builder() self.cp.safetyConfigs[0].safetyParam |= 0x800 self.assertTrue(qualified_ioniq6(self.cp)) intent = AolCardIntent(read_aol_settings(self.params), explicit_latch=True) cs = ioniq6_state() intent.update(cs) if aol_on: cs.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.lkas, pressed=True)] intent.update(cs) cs.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.lkas, pressed=False)] intent.update(cs) cs.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.gapAdjustCruise, pressed=True)] intent.update(cs) cs.buttonEvents = [] for _ in range(ticks - 1): intent.update(cs) cs.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.gapAdjustCruise, pressed=False)] intent.update(cs) latch, pause_lat, pause_long = intent.output(cs) self.assertEqual(latch, aol_on) self.assertEqual((not pause_lat, not pause_long), expected) wire = SimpleNamespace(allowedLatch=latch, pauseLateral=pause_lat, pauseLongitudinal=pause_long) def axes(native, aol_on=aol_on, wire=wire, cs=cs): return decide_axes(standard_lateral=not aol_on, standard_longitudinal=True, intent=wire, native=native, car_state=cs, initialized=True, model_ready=True, no_entry=False, immediate_disable=False, dm_lockout=False, pause_brake_mps=0.0) missing = axes(None) self.assertEqual((missing.desired_lateral, missing.desired_longitudinal), expected) self.assertEqual(missing.mode, "off") native = SimpleNamespace(requestedLateral=expected[0], requestedLongitudinal=expected[1], lateralAllowed=True, longitudinalAllowed=True) active = axes(native) self.assertEqual((active.lateral_active, active.longitudinal_active), expected) self.cp = stock reset = self.row("wheel", "wheel:" + key) self.assertTrue(self.owner.apply(FeatureSettingsRequest("wheel:" + key, reset.source, "Off", vehicle_fingerprint=reset.vehicle_fingerprint, capability=reset.capability, dependencies=reset.dependencies))) def test_ioniq_distance_canonical_repair_and_fixed_gestures(self): self.cp, _ = ioniq6_candidate(False) state = self.owner.snapshot("wheel", parked=True, system_long=True, lateral_context=True, metric=False) self.assertFalse(any(row.key in ("wheel:LKASButtonControl", "wheel:MainCruiseButtonControl") for row in state.rows)) key = "DistanceButtonControl" path = Path(self.params.get_param_path(key)) for raw in (b"03", b"99", b"bad"): path.write_bytes(raw) row = self.row("wheel", "wheel:" + key) self.assertEqual(row.value, "Invalid saved action") self.assertEqual(row.repair_value, "Off") self.assertTrue(self.owner.apply(required_change(row))) self.assertEqual(path.read_bytes(), b"0") def test_honda_distance_toggle_remains_editable_and_ioniq_stage_revokes(self): self.honda() key = "DistanceButtonControl" self.params.put(key, 9, block=True) row = self.row("wheel", "wheel:" + key) self.assertEqual(row.value, "Toggle AOL") request = FeatureSettingsRequest(row.key, row.source, "Pause steering", capability=row.capability) self.assertTrue(self.owner.apply(request)) self.assertEqual(Path(self.params.get_param_path(key)).read_bytes(), b"3") self.cp, _ = ioniq6_candidate(False) request = required_change(self.row("wheel", "wheel:" + key)) self.cp.carVin = "changed" self.assertFalse(self.owner.apply(request)) self.assertEqual(Path(self.params.get_param_path(key)).read_bytes(), b"3") def test_native_large_and_compact_child_actions(self): hub = self.owner.snapshot("hub", parked=True, system_long=True, lateral_context=True, metric=False) actions = [] control = FeatureInput(actions.append) index = next(i for i, row in enumerate(hub.rows) if row.page == "torque") hub = replace(hub, scroll=index) control.press(650, 300, hub) control.release(650, 300, hub) self.assertEqual(actions[0].row.page, "torque") class Button: def __init__(self, text, value): self.text, self.value, self.click = text, value, None def set_click_callback(self, callback): self.click = callback def set_enabled(self, enabled): self.enabled = enabled row = self.row("torque", "torque:factor:value") refreshed = [] session = Mock(feature_request=self.owner.apply) adapter = compact.FeatureSettingsCompact(session) with patch.object(compact, "BigButton", Button): buttons = adapter._editable(row, lambda: refreshed.append(True), Mock()) next(button for button in buttons if button.text.endswith("+")).click() self.assertEqual(refreshed, [True]) source = Path(self.params.get_param_path(DOCUMENT_KEY)).read_bytes() self.assertEqual(parse_document(source)[str(self.cp.carFingerprint)].factor.mode, "custom") row = self.row("torque", "torque:factor:reset") action = FeatureInput.target(1900, FEATURE_ROW_TOP + 77, FeatureSettingsState(page="torque", rows=(row,))) self.assertIsNotNone(action) assert action is not None self.assertEqual(action.kind, "change") self.assertEqual(action.row, row) request = required_change(row, action.direction) self.assertTrue(self.owner.apply(request)) self.assertEqual(parse_document(Path(self.params.get_param_path(DOCUMENT_KEY)).read_bytes())[ str(self.cp.carFingerprint)].factor.mode, "source") def test_large_repair_labels_match_requested_values(self): text = [] fonts = SimpleNamespace(profile=Profile.LARGE, draw=lambda value, *_args: text.append(value), measure=lambda value, _role, size: SimpleNamespace(width=len(value) * size * .5), vertical_ink=lambda _value, _role, size: (size * .2, size * .8)) state = FeatureSettingsState(rows=( FeatureRow("AolBrakePauseSpeedMps", "Brake pause below", "Invalid", available=True, repair_value="0"), FeatureRow("wheel:LKASButtonControl", "LKAS press", "Unsupported", available=True, repair_value="Off"), )) with patch.object(large.rl, "draw_rectangle_rounded"), patch.object(large.rl, "draw_rectangle_rounded_lines_ex"), \ patch.object(large.rl, "draw_line"), patch.object(large.rl, "draw_line_ex"), patch.object(large.rl, "draw_circle"), \ patch.object(large.clip, "begin_scissor_mode"), \ patch.object(large.clip, "end_scissor_mode"): large.FeatureSettingsView(Mock(spec=BitmapFonts, **vars(fonts))).render(state) self.assertIn("Set 0", text) self.assertIn("Set Off", text) def test_starpilot_tuning_help_is_visible_in_large_native_settings(self): class Fonts: profile = Profile.LARGE def __init__(self): self.text = [] def draw(self, value, *args, **kwargs): self.text.append(value) def measure(self, value, _role, size): return SimpleNamespace(width=len(value) * size * .5) def vertical_ink(self, _value, _role, size): return size * .2, size * .8 self.assertTrue(self.owner.apply(required_change(self.row("torque", "LateralControllerSelection")))) fonts = Fonts() state = self.owner.snapshot("torque", parked=True, system_long=True, lateral_context=True, metric=False) state = replace(state, scroll=next(i for i, row in enumerate(state.rows) if row.label == "Use StarPilot Auto Learn")) with (patch.object(large.rl, "draw_rectangle_rounded"), patch.object(large.rl, "draw_line"), patch.object(large, "draw_rounded_stroke"), patch.object(large, "draw_aether_toggle"), patch.object(large, "draw_settings_header"), patch.object(large.clip, "begin_scissor_mode"), patch.object(large.clip, "end_scissor_mode")): large.FeatureSettingsView(Mock(spec=BitmapFonts, profile=Profile.LARGE, measure=fonts.measure, draw=fonts.draw, vertical_ink=fonts.vertical_ink)).render(state) self.assertIn("Use StarPilot Auto Learn", fonts.text) controller = self.row("torque", "LateralControllerSelection") self.assertIn("https://firestar.link/discord", controller.reason) self.assertEqual(state.rows[state.scroll].reason, "StarPilot’s FLM tuning will attempt to automatically correct torque profiles as you drive.") self.assertTrue(state.rows[state.scroll].available) self.assertEqual(FeatureInput.target(650, 300, state).row, state.rows[state.scroll]) self.assertTrue(self.owner.apply(required_change(self.row("torque", "LateralControllerSelection")))) standard = self.owner.snapshot("torque", parked=True, system_long=True, lateral_context=True, metric=False) self.assertTrue(any(row.label == "Use StarPilot Auto Learn" for row in standard.rows)) if __name__ == "__main__": unittest.main()