From 7403185bd8a4c764409f7bb9c993a7ba8ea816b6 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:46:20 -0500 Subject: [PATCH] Keep Ford steering selection visible with AOL enabled --- .../galaxy/tests/test_ford_angle_settings.py | 2 +- .../tests/test_ford_selector_visibility.py | 74 +++++++++++++++++++ .../starpilot/ui/ford_lateral_feature.py | 13 +++- 3 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 openpilot/starpilot/galaxy/tests/test_ford_selector_visibility.py diff --git a/openpilot/starpilot/galaxy/tests/test_ford_angle_settings.py b/openpilot/starpilot/galaxy/tests/test_ford_angle_settings.py index 4e15ae2901..3cb243fc4d 100644 --- a/openpilot/starpilot/galaxy/tests/test_ford_angle_settings.py +++ b/openpilot/starpilot/galaxy/tests/test_ford_angle_settings.py @@ -70,7 +70,7 @@ class TestFordAngleSettings(unittest.TestCase): for identity in (CAR.FORD_EDGE_MK2, CAR.FORD_MONDEO_MK5, CAR.FORD_TRANSIT_MK5): self.cp = ford_params(identity) self.assertFalse(any(r["label"] == "Ford steering controller" for r in self.page()["rows"])) - for field, value in (("alternativeExperience", 32), ("passive", True), ("dashcamOnly", True), ("notCar", True)): + for field, value in (("alternativeExperience", 16), ("passive", True), ("dashcamOnly", True), ("notCar", True)): self.cp = ford_params(CAR.FORD_BRONCO_SPORT_MK1) setattr(self.cp, field, value) self.assertFalse(any(r["label"] == "Ford steering controller" for r in self.page()["rows"])) diff --git a/openpilot/starpilot/galaxy/tests/test_ford_selector_visibility.py b/openpilot/starpilot/galaxy/tests/test_ford_selector_visibility.py new file mode 100644 index 0000000000..8367ed58cf --- /dev/null +++ b/openpilot/starpilot/galaxy/tests/test_ford_selector_visibility.py @@ -0,0 +1,74 @@ +import tempfile +import unittest +from pathlib import Path +from types import SimpleNamespace + +from opendbc.car import structs +from opendbc.car.ford.values import CAR, FordFlags +from openpilot.starpilot.ui.ford_lateral_feature import FordLateralFeature + + +class TestFordSelectorVisibility(unittest.TestCase): + def setUp(self): + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + self.root = Path(directory.name) + self.cp = structs.CarParams(brand='ford', carFingerprint=CAR.FORD_MUSTANG_MACH_E_MK1, + flags=int(FordFlags.CANFD), alternativeExperience=32, pcmCruise=True, + steerControlType='angle', transmissionType='automatic', + safetyConfigs=[{'safetyModel': 'ford', 'safetyParam': 18}]) + self.params = SimpleNamespace(get_param_path=lambda key: str(self.root / key)) + self.owner = SimpleNamespace(params=self.params, vehicle_params=lambda: self.cp, + authority=lambda _: True, vehicle_fingerprint=lambda: self.cp.carFingerprint) + self.feature = FordLateralFeature(self.owner) + self.save('AlwaysOnLateral', b'1') + self.save('SafeMode', b'0') + + def save(self, key, value): + (self.root / key).write_bytes(value) + + def test_aol_marked_mach_e_visible_without_mutating_configuration(self): + for alpha in (False, True): + self.cp.openpilotLongitudinalControl = alpha + self.cp.safetyConfigs[0].safetyParam = 18 + int(alpha) + before = self.cp.to_dict() + rows = self.feature.rows(True) + self.assertEqual(len(rows), 1) + self.assertEqual(rows[0].choices, ('Current curvature', 'BluePilot angle')) + self.assertFalse(rows[0].available) + self.assertIn('Turn off Always On Lateral', rows[0].reason) + self.assertEqual(self.cp.to_dict(), before) + + def test_saved_aol_off_allows_selection_without_waiting_for_cp_restart(self): + self.save('AlwaysOnLateral', b'0') + self.assertTrue(self.feature.rows(True)[0].available) + self.assertEqual(self.cp.alternativeExperience, 32) + self.assertEqual(self.cp.safetyConfigs[0].safetyParam, 18) + + def test_onroad_and_safe_mode_still_deny_selection(self): + self.save('AlwaysOnLateral', b'0') + self.assertFalse(self.feature.rows(False)[0].available) + self.save('SafeMode', b'1') + self.assertFalse(self.feature.rows(True)[0].available) + + def test_reader_uses_the_same_display_only_capability(self): + self.cp = self.cp.as_reader() + self.assertEqual(len(self.feature.rows(True)), 1) + self.assertEqual(self.cp.alternativeExperience, 32) + + def test_invalid_native_owner_remains_hidden(self): + for word in (0, 2, 130, 65535): + self.cp.safetyConfigs[0].safetyParam = word + self.assertEqual(self.feature.rows(True), ()) + + def test_unrelated_experience_and_brand_remain_hidden(self): + self.cp.alternativeExperience = 16 + self.assertEqual(self.feature.rows(True), ()) + self.cp.alternativeExperience = 32 + self.cp.brand = 'toyota' + self.assertEqual(self.feature.rows(True), ()) + + def test_existing_unmarked_mach_e_stays_available(self): + self.cp.alternativeExperience = 0 + self.save('AlwaysOnLateral', b'0') + self.assertTrue(self.feature.rows(True)[0].available) diff --git a/openpilot/starpilot/ui/ford_lateral_feature.py b/openpilot/starpilot/ui/ford_lateral_feature.py index 9cdaa73ad5..6cc0711e8a 100644 --- a/openpilot/starpilot/ui/ford_lateral_feature.py +++ b/openpilot/starpilot/ui/ford_lateral_feature.py @@ -1,5 +1,7 @@ """One saved Ford enum shared by Galaxy and the native feature presentation.""" from opendbc.bluepilot_lateral.hosts.starpilot import supported +from opendbc.car import structs +from opendbc.car.ford.aol import qualified as aol_qualified from openpilot.starpilot.car.ford.lateral_preferences import KEY, CHOICES, read_choice from openpilot.starpilot.saved_source import read_saved from openpilot.starpilot.saved_document import commit_exact @@ -13,8 +15,15 @@ class FordLateralFeature: def capability(self): cp = self.owner.vehicle_params() try: - if cp is None or not supported(cp): + if cp is None: return None + if not supported(cp): + if not aol_qualified(cp, marked_only=True): + return None + next_drive = structs.CarParams(**cp.to_dict()) + next_drive.alternativeExperience = 0 + if not supported(next_drive): + return None return (str(cp.carFingerprint), int(cp.flags), int(cp.alternativeExperience), bool(cp.openpilotLongitudinalControl), bool(cp.pcmCruise), str(cp.steerControlType), bool(cp.passive), bool(cp.dashcamOnly), bool(cp.notCar), tuple((str(c.safetyModel), int(c.safetyParam)) for c in cp.safetyConfigs)) @@ -40,7 +49,7 @@ class FordLateralFeature: choices=tuple(CHOICES.values()), available=parked and self.owner.authority("parked_preferences") and readable and dependencies is not None, capability=capability, dependencies=dependencies or (), repair_value="" if valid else CHOICES[0], default_value=CHOICES[0], - reason="Applies next drive. BluePilot angle is available with normal cruise control on supported Ford vehicles."),) + reason="Applies next drive. Turn off Always On Lateral and Safe Mode to select BluePilot angle with normal cruise control."),) def apply(self, request): value = next((code for code, label in CHOICES.items() if label == request.value), None)