From 5f741d988715650147c00c901d17154bcc20bbd9 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Sun, 1 Nov 2026 23:04:00 -0600 Subject: [PATCH] Keep Mazda source handoffs within the original control lease --- openpilot/selfdrive/selfdrived/selfdrived.py | 4 +- .../aol/tests/test_mazda_source_deadline.py | 126 ++++++++++++++++++ prebuilt.json | 2 +- tools/ci/run_vehicle_tests.py | 1 + 4 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 openpilot/starpilot/aol/tests/test_mazda_source_deadline.py diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py index ab11404e00..7e672adf4b 100755 --- a/openpilot/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -829,8 +829,10 @@ class SelfdriveD: def source_deadline_deferral_supported(self): from openpilot.starpilot.car.hyundai.aol import source_deadline_supported from openpilot.starpilot.car.toyota.aol import qualified as qualified_toyota + from opendbc.car.mazda.stock_aol import qualified as qualified_mazda return (qualified_ioniq6(self.CP) or qualified_gm(self.CP) or source_deadline_supported(self.CP) or - qualified_toyota(self.CP, marked_only=True) or self.ford_source_deadline_supported()) + qualified_toyota(self.CP, marked_only=True) or qualified_mazda(self.CP, marked_only=True) or + self.ford_source_deadline_supported()) def held_axis_override_wait(self, CS, proof, now_ns, *, fresh_successor=False): """Recognize an already applied override; never admit a new physical edge.""" diff --git a/openpilot/starpilot/aol/tests/test_mazda_source_deadline.py b/openpilot/starpilot/aol/tests/test_mazda_source_deadline.py new file mode 100644 index 0000000000..8b30d4fd6d --- /dev/null +++ b/openpilot/starpilot/aol/tests/test_mazda_source_deadline.py @@ -0,0 +1,126 @@ +"""Recorded Mazda source cutoff; real SD/Controls, synthetic clocks and transport.""" +from contextlib import ExitStack +from dataclasses import replace +from unittest import mock + +import pytest +from opendbc.car import gen_empty_fingerprint +from opendbc.car.mazda.interface import CarInterface +from opendbc.car.mazda.values import CAR +from openpilot.cereal import log +from openpilot.common.prefix import OpenpilotPrefix +from openpilot.starpilot.aol.tests import test_ioniq_availability as fixture +from openpilot.starpilot.aol.tests.test_input_deadline_continuity import BASE, intent +from openpilot.starpilot.aol.wire import decode_intent, encode_intent + + +@pytest.mark.parametrize('identity', (CAR.MAZDA_CX5_2022, CAR.MAZDA_CX9_2021)) +@pytest.mark.parametrize('condition', ('healthy', 'deadline', 'native_denied', 'cancel', 'brake', 'main_off', + 'eps_fault', 'invalid_can', 'producer', 'pause', 'repeated')) +def test_mazda_source_cutoff_preserves_only_original_command_lease(identity, condition): + with OpenpilotPrefix(), ExitStack() as cleanup: + case = fixture.IoniqAvailabilityTests(methodName='runTest') + cleanup.callback(case.doCleanups) + cp = CarInterface.get_params(identity, gen_empty_fingerprint(), [], False, False, False) + sd, controls, drive = case._authority_callers(True, cp=cp, genuine_events=True) + state = fixture.car_state() + state.cruiseState.available = state.cruiseState.enabled = True + drive(0, observed_state=state, native_request_long=False, long_allowed=False, panda_controls_allowed=True) + old, _, command = drive(2, state_stamp=BASE, offset_ns=4_237_448, + intent_message=intent(BASE, 3), observed_state=state, native_request_long=False, long_allowed=False, panda_controls_allowed=True) + assert old.lateralActive and command.latActive and not command.longActive + original = sd.pm.messages['aolAxisState'].as_builder().to_bytes() + now = BASE + 30_453_437 + sd.aol_step_input_proof = sd.aol_published_input_proof + sd.aol_step_deferral_checked = False + sd.aol_step_intent_drain_count = 0 + sd.aol_step_intent_drain_ok = True + if condition == 'deadline': + now = old.validUntilMonoTime + elif condition == 'native_denied': + sd.sm.valid['aolSafetyWire'] = False + elif condition == 'cancel': + sd.events.add(log.OnroadEvent.EventName.buttonCancel) + elif condition == 'brake': + state.brakePressed = True + elif condition == 'main_off': + state.cruiseState.available = False + state.cruiseState.enabled = False + sd.events.add(log.OnroadEvent.EventName.wrongCarMode) + elif condition == 'eps_fault': + state.steerFaultPermanent = True + elif condition == 'invalid_can': + state.canValid = False + elif condition in ('producer', 'pause'): + message = intent(BASE + 29_000_000, 4) + value = decode_intent(message.aolIntentWire) + message.aolIntentWire = encode_intent(replace(value, **( + {'producerSessionId': 'different-card'} if condition == 'producer' else {'pauseLateral': True}))) + sd.aol_intent_companion._observe(message.logMonoTime, message.valid, message.aolIntentWire) + elif condition == 'repeated': + sd.aol_step_deferral_checked = True + initial = (sd.state_machine.state, sd.state_machine.soft_disable_timer, sd.enabled, sd.active) + sd.car_state_samples = ((state.as_reader(), True),) + with mock.patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=now): + deferred = sd.defer_expired_axis_publication(state, initial, now_ns=now) + assert deferred == (condition == 'healthy') + assert sd.pm.messages['aolAxisState'].as_builder().to_bytes() == original + assert old.sourceCarStateMonoTime == BASE + assert old.validUntilMonoTime == BASE + 34_237_448 + # Every consumer still withdraws at the original absolute deadline. + with mock.patch('openpilot.selfdrive.controls.controlsd.time.monotonic_ns', return_value=old.validUntilMonoTime + 1): + expired, _ = controls.state_control() + assert not expired.latActive and not expired.longActive + if condition == 'healthy': + fresh = BASE + 27_821_823 + axis, _, command = drive(6, state_stamp=fresh, offset_ns=-7_000_000, + intent_message=intent(fresh, 4), observed_state=state, native_request_long=False, long_allowed=False, panda_controls_allowed=True) + assert axis.sourceCarStateMonoTime == fresh and axis.lateralActive and command.latActive + assert not command.longActive + assert log.OnroadEvent.EventName.commIssue not in sd.events.names + assert log.OnroadEvent.EventName.controlsMismatch not in sd.events.names + + +@pytest.mark.parametrize('identity', (CAR.MAZDA_CX5_2022, CAR.MAZDA_CX9_2021)) +def test_mazda_actual_step_defers_warning_then_consumes_fresh_successor(identity): + with OpenpilotPrefix(), ExitStack() as cleanup: + case = fixture.IoniqAvailabilityTests(methodName='runTest') + cleanup.callback(case.doCleanups) + cp = CarInterface.get_params(identity, gen_empty_fingerprint(), [], False, False, False) + sd, controls, drive = case._authority_callers(True, cp=cp, genuine_events=True) + state = fixture.car_state() + state.cruiseState.available = state.cruiseState.enabled = True + options = {'observed_state': state, 'native_request_long': False, + 'long_allowed': False, 'panda_controls_allowed': True} + drive(0, **options) + old, _, command = drive(2, state_stamp=BASE, offset_ns=4_237_448, + intent_message=intent(BASE, 3), **options) + assert old.lateralActive and command.latActive + original = sd.pm.messages['aolAxisState'].as_builder().to_bytes() + now = [BASE + 29_900_000] + actual_events = sd.update_events + + def events(cs): + result = actual_events(cs) + now[0] = BASE + 30_453_437 + return result + + with mock.patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', side_effect=lambda: now[0]), \ + mock.patch.object(sd, 'update_events', side_effect=events): + axis, _, command = drive(5, timeout=True, offset_ns=-100_000, + intent_message=intent(BASE, 3), **options) + assert sd.aol_axis_publication_deferred + assert sd.pm.messages['aolAxisState'].as_builder().to_bytes() == original + assert command.latActive and not command.longActive + assert log.OnroadEvent.EventName.commIssue not in sd.events.names + assert log.OnroadEvent.EventName.controlsMismatch not in sd.events.names + with mock.patch('openpilot.selfdrive.controls.controlsd.time.monotonic_ns', return_value=old.validUntilMonoTime + 1): + expired, _ = controls.state_control() + assert not expired.latActive and not expired.longActive + fresh = BASE + 27_821_823 + axis, _, command = drive(6, state_stamp=fresh, offset_ns=-7_000_000, + intent_message=intent(fresh, 4), **options) + assert axis.sourceCarStateMonoTime == fresh and axis.lateralActive and command.latActive + assert not command.longActive + assert log.OnroadEvent.EventName.commIssue not in sd.events.names + assert log.OnroadEvent.EventName.controlsMismatch not in sd.events.names diff --git a/prebuilt.json b/prebuilt.json index ca7bbaff5b..655ac906d1 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "b1d97b7008d6b691c0ab1be89789b81b3a68217e1dfaa48c1bccdaef6532ae30", "schema_version": 1} +{"protected_inventory_sha256": "3c88c96a3d4067ed5c4cb263e809b67c05ac819dd274f40c42af5c974c0998d2", "schema_version": 1} diff --git a/tools/ci/run_vehicle_tests.py b/tools/ci/run_vehicle_tests.py index 13f782f133..7db31758cc 100644 --- a/tools/ci/run_vehicle_tests.py +++ b/tools/ci/run_vehicle_tests.py @@ -39,6 +39,7 @@ FORD_BLUEPILOT_NATIVE_TARGETS = tuple(str(path.relative_to(ROOT)) for path in so # Representative production callers plus their source/native refusal cases. # The emitted selected_test_ids record exact parametrized configurations. AOL_LIFECYCLE_TARGETS = ( + 'openpilot/starpilot/aol/tests/test_mazda_source_deadline.py', "openpilot/starpilot/aol/tests/test_gv60_hda1_alpha_aol.py", "opendbc_repo/opendbc/car/hyundai/tests/test_torque_ev_deferred_takeover.py", 'openpilot/starpilot/aol/tests/test_hkg_early_long_withdrawal.py',