diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py old mode 100755 new mode 100644 index 41ee4f2892..da20730bf0 --- a/openpilot/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -1733,6 +1733,13 @@ class SelfdriveD: mismatch counter retain their original fault ownership. """ from openpilot.starpilot.aol.transport_pause import independent_lateral_health + from opendbc.car.ford.values import CAR as FORD_CAR + from openpilot.starpilot.car.ford.aol import native_accepts_cp as ford_native_accepts_cp + # Established ordinary Mach-E cruise does not require an independent AOL latch. + # Its native LONG pedal withdrawal may precede Card; the original combined + # publication, live LAT grant and every health/deadline check below still apply. + ordinary_ford = bool(self.CP.carFingerprint == FORD_CAR.FORD_MUSTANG_MACH_E_MK1 and + ford_native_accepts_cp(self.CP, int(car.CarParams.SafetyModel.ford), 19)) proof = getattr(self, 'aol_published_input_proof', None) prior = getattr(self, 'aol_published_input_native', None) anchor = getattr(self, 'aol_ordinary_source_anchor', None) @@ -1746,7 +1753,7 @@ class SelfdriveD: anchor[3] != cp_identity(self.CP) or proof[0] != self.aol_session_id or not proof[8] or not proof[9] or not 0 < proof[2] <= now_ns <= proof[3] or intent is None or companion is None or proof[4] != intent.producerSessionId or - proof[5] != companion.integrity_epoch or not intent.allowedLatch or not intent.lateralArmed or + proof[5] != companion.integrity_epoch or not ((intent.allowedLatch and intent.lateralArmed) or ordinary_ford) or not native.requestedLateral or not native.lateralAllowed or not native.requestedLongitudinal or native.longitudinalAllowed or not native.pandaInventory or CS.brakePressed or CS.regenBraking or CS.gasPressed or CS.steeringPressed or @@ -1780,7 +1787,7 @@ class SelfdriveD: # Retain only its already granted LAT; all proof, health and LONG withdrawal gates remain. # The exact vehicle policies and all proof/health checks above remain required. from openpilot.starpilot.car.ford.aol import early_native_long_withdrawal_supported as ford_early_long - return bool(gm_native_accepts_cp(self.CP, native.safetyModel, native.safetyParam) or + return bool(ordinary_ford or gm_native_accepts_cp(self.CP, native.safetyModel, native.safetyParam) or early_native_long_withdrawal_supported(self.CP) or ford_early_long(self.CP) or toyota_aol_qualified(self.CP, marked_only=True) or not native.pandaInventory[owners[0]].controlsAllowed and diff --git a/openpilot/starpilot/aol/tests/test_ford_ordinary_long_withdrawal.py b/openpilot/starpilot/aol/tests/test_ford_ordinary_long_withdrawal.py new file mode 100644 index 0000000000..ca9966b8c5 --- /dev/null +++ b/openpilot/starpilot/aol/tests/test_ford_ordinary_long_withdrawal.py @@ -0,0 +1,94 @@ +from contextlib import ExitStack +import pytest +from openpilot.common.prefix import OpenpilotPrefix +from openpilot.cereal import log +from openpilot.starpilot.aol.tests import test_ioniq_availability as fixture +from opendbc.car import gen_empty_fingerprint +from opendbc.car.ford.interface import CarInterface +from opendbc.car.ford.values import CAR + + +@pytest.fixture +def caller(): + case = fixture.IoniqAvailabilityTests(methodName='runTest') + with ExitStack() as stack: + stack.callback(case.doCleanups) + stack.enter_context(OpenpilotPrefix()) + fingerprint = gen_empty_fingerprint() + fingerprint[0] = {0x5A: 8, 0x3A6: 8, 0x3A7: 8} + cp = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, fingerprint, [], True, False, False) + cp.alternativeExperience = 32 + assert cp.safetyConfigs[-1].safetyParam == 19 and cp.flags == 3 + sd, controls, step = case._authority_callers(True, cp=cp, genuine_events=True) + cs = fixture.car_state() + cs.cruiseState.enabled = True + + def drive(tick, **kw): + return step(tick, latched=False, observed_state=cs, **kw) + + for tick in range(1, 10): + a, s, c = drive(tick) + assert s.enabled and a.lateralActive and a.longitudinalActive and c.latActive and c.longActive + yield sd, cs, drive + + +@pytest.mark.parametrize('pedal', ['gas', 'brake']) +def test_ordinary_pedal_edge_withdraws_long_without_inventing_aol_latch(caller, pedal): + sd, cs, drive = caller + kwargs = {'native_request_lat': True, 'allowed': True, 'native_request_long': True, 'long_allowed': False, 'panda_controls_allowed': pedal == 'gas'} + a, s, c = drive(10, **kwargs) + assert not a.longitudinalActive and not c.longActive + assert a.lateralActive and c.latActive and s.enabled + assert not sd.aol_last_intent.allowedLatch and not sd.aol_last_intent.lateralArmed + assert log.OnroadEvent.EventName.controlsMismatch not in sd.events.names + if pedal == 'brake': + cs.brakePressed = True + a, s, c = drive(11, gas=pedal == 'gas', **kwargs) + assert not a.longitudinalActive and not c.longActive + assert not sd.aol_last_intent.allowedLatch and not sd.aol_last_intent.lateralArmed + assert log.OnroadEvent.EventName.controlsMismatch not in sd.events.names + if pedal == 'brake': + assert not s.enabled and not s.active and not a.lateralActive and not c.latActive + # Releasing brake alone must not rearm ordinary ACC or independent LAT. + cs.brakePressed = False + a, s, c = drive(12) + assert not s.enabled and not a.lateralActive and not a.longitudinalActive and not c.latActive and not c.longActive + else: + assert s.enabled and a.lateralActive and c.latActive + + +@pytest.mark.parametrize('fault', ['lat_denied', 'native_invalid', 'rx_fault', 'relay_fault', 'prior_expired', 'prior_absent', 'proof_absent']) +def test_faults_and_missing_original_proof_remain_immediate(caller, fault): + sd, cs, drive = caller + kw = {'native_request_lat': True, 'allowed': True, 'native_request_long': True, 'long_allowed': False, 'panda_controls_allowed': False} + if fault == 'lat_denied': + kw['allowed'] = False + elif fault == 'native_invalid': + kw['native_valid'] = False + elif fault == 'rx_fault': + kw['panda_fault'] = True + elif fault == 'relay_fault': + kw['relay_fault'] = True + elif fault == 'prior_expired': + p = list(sd.aol_published_input_proof) + p[3] = 10_099_999_999 + sd.aol_published_input_proof = tuple(p) + elif fault == 'prior_absent': + sd.aol_published_input_native = None + elif fault == 'proof_absent': + sd.aol_published_input_proof = None + a, s, c = drive(10, **kw) + assert not a.longitudinalActive and not c.longActive + assert not s.enabled and not a.lateralActive and not c.latActive + assert log.OnroadEvent.EventName.controlsMismatch in sd.events.names + + +def test_persistent_native_long_denial_keeps_original_mismatch_owner(caller): + sd, cs, drive = caller + for tick in range(10, 230): + a, s, c = drive(tick, native_request_lat=True, allowed=True, native_request_long=True, long_allowed=False, panda_controls_allowed=False) + assert not a.longitudinalActive and not c.longActive + if log.OnroadEvent.EventName.controlsMismatch in sd.events.names: + break + assert sd.mismatch_counter >= 200 + assert not s.enabled and not a.lateralActive and not c.latActive diff --git a/prebuilt.json b/prebuilt.json index 4add03b80f..bd5ef4a0ec 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "f8a8a5e56e3891c86ed3d164c987d3b4fda326cdd91e7303eb1b57da536ea662", "schema_version": 1} +{"protected_inventory_sha256": "d90e3f031dbf26c980c4fdb5fdbf891f984bc5425caf5382d95114211f154e08", "schema_version": 1}