diff --git a/openpilot/starpilot/aol/source_availability.py b/openpilot/starpilot/aol/source_availability.py index 199bcba7be..38a7d1e8de 100644 --- a/openpilot/starpilot/aol/source_availability.py +++ b/openpilot/starpilot/aol/source_availability.py @@ -123,10 +123,10 @@ def ordinary_source_availability(self, CS, now_ns, native=None, *, replay: bool, anchor = getattr(self, 'aol_ordinary_source_anchor', None) companion = getattr(self, 'aol_intent_companion', None) events = self.events.to_msg() - from openpilot.starpilot.car.gm.aol import held_sdgm_lateral_pedal + from openpilot.starpilot.car.gm.aol import held_gm_lateral_pedal from openpilot.starpilot.car.toyota.aol import held_lateral_pedal toyota_pedal = held_lateral_pedal(self, CS) - held_pedal = held_sdgm_lateral_pedal(self, CS) or toyota_pedal + held_pedal = held_gm_lateral_pedal(self, CS) or toyota_pedal if held_pedal: events = [event for event in events if not (event.name == EventName.pedalPressed and event.userDisable and not event.softDisable and not event.immediateDisable)] @@ -265,7 +265,7 @@ def ordinary_source_availability(self, CS, now_ns, native=None, *, replay: bool, return False native = native or native_provider(self.sm, self.CP, now_ns=now_ns, axis_session_id=self.aol_session_id) lateral_intent_only = bool(proof[8] and not proof[9] and not self.enabled) - if (native is None or held_pedal and (native.requestedLongitudinal or native.longitudinalAllowed) or + if (native is None or toyota_pedal and (native.requestedLongitudinal or native.longitudinalAllowed) or lateral_intent_only and not (accepted.allowedLatch and accepted.lateralArmed and not accepted.pauseLateral and not getattr(self, 'aol_dm_lateral_inhibit', False) and native.requestedLateral and native.lateralAllowed) or diff --git a/openpilot/starpilot/aol/tests/test_bolt_recorded_source_gaps.py b/openpilot/starpilot/aol/tests/test_bolt_recorded_source_gaps.py new file mode 100644 index 0000000000..d871bbaa92 --- /dev/null +++ b/openpilot/starpilot/aol/tests/test_bolt_recorded_source_gaps.py @@ -0,0 +1,138 @@ +"""Bolt routes 25f676999e/f3031d6bc7: host gaps cannot become native faults. + +Production SelfdriveD/Controls callers and serialized controlled native peers; +this does not replay the device scheduler or drive a vehicle. +""" +from contextlib import ExitStack +from unittest import mock + +import pytest +from opendbc.car.gm.tests.test_bolt_pedal import params +from opendbc.car.gm.values import CAR +from openpilot.cereal import messaging +from openpilot.common.prefix import OpenpilotPrefix +from openpilot.selfdrive.selfdrived.events import EventName +from openpilot.starpilot.aol.intent import read_settings +from openpilot.starpilot.aol.tests import test_ioniq_availability as fixture +from openpilot.starpilot.aol.tests.test_input_deadline_continuity import intent + +BASE = 10_000_000_000 + + +def callers(cleanup, full=False, candidate=CAR.CHEVROLET_BOLT_CC_2018_2021): + cleanup.enter_context(OpenpilotPrefix()) + case = fixture.IoniqAvailabilityTests(methodName='runTest') + cleanup.callback(case.doCleanups) + sd, controls, step = case._authority_callers(full, cp=params(candidate, pedal=True, camera=True), genuine_events=True) + sd.aol_settings = read_settings(sd.params) + return sd, controls, step + + +def test_route29_missing_card_wire_keeps_only_original_intent_lease(): + with ExitStack() as cleanup: + sd, controls, step = callers(cleanup) + for tick in (0, 1): + _, _, command = step(tick, gas=True, native_request_long=False) + assert command.latActive and not command.longActive + original = sd.sm.update_msgs + def without_card(stamp, messages): + return original(stamp, [m for m in messages if m.which() not in ('aolIntentWire', 'starpilotCarState')]) + with mock.patch.object(sd.sm, 'update_msgs', side_effect=without_card): + for tick in (4, 7, 10, 13): + axis, _, command = step(tick, timeout=True, gas=True, native_request_long=False, + intent_message=intent(BASE + 10_000_000, 2), offset_ns=1_500_000) + assert not axis.lateralActive and not command.latActive and not command.longActive + assert not sd.aol_authority_lost and EventName.controlsMismatch not in sd.events.names + assert str(axis.faultReason) == 'availabilityPause' and axis.sourceCarStateMonoTime == 0 + assert not sd.sm.alive['aolIntentWire'] + axis, _, command = step(14, gas=True, native_request_long=False) + assert axis.lateralActive and command.latActive and not command.longActive + assert not sd.aol_authority_lost + with mock.patch('openpilot.selfdrive.controls.controlsd.time.monotonic_ns', return_value=axis.validUntilMonoTime + 1): + expired, _ = controls.state_control() + assert not expired.latActive and not expired.longActive + + +@pytest.mark.parametrize('candidate', (CAR.CHEVROLET_BOLT_CC_2017, CAR.CHEVROLET_BOLT_CC_2018_2021, + CAR.CHEVROLET_BOLT_CC_2022_2023, CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL)) +@pytest.mark.parametrize('pedal', ('brake', 'regen')) +def test_route28_accepted_lateral_pedal_cutoff_retains_intent_without_output(candidate, pedal): + with ExitStack() as cleanup: + sd, _, step = callers(cleanup, full=True, candidate=candidate) + from opendbc.car.car_helpers import interfaces + from openpilot.selfdrive.car.card import Car + cp = params(candidate, pedal=True, camera=True) + card = Car(CI=interfaces[candidate](cp), RI=interfaces[candidate].RadarInterface(cp)) + cleanup.callback(card.vehicle_startup.close) + assert card.CP.to_dict() == sd.CP.to_dict() + card.slc_producer_session = 'authority-card' + card.aol_card_intent.allowed_latch = True + def receive(now): + frames = [sd.pm.messages[name] for name in ('aolAxisState', 'onroadEvents', 'selfdriveState')] + for service in ('pandaStates', 'aolSafetyWire', 'deviceState'): + msg = messaging.new_message(service, len(sd.sm[service]) if service in ('pandaStates', 'aolSafetyWire') else None, + valid=sd.sm.valid[service], logMonoTime=sd.sm.logMonoTime[service]) + setattr(msg, service, sd.sm[service]) + frames.append(msg.as_reader()) + card.sm.update_msgs(now / 1e9, frames) + return card.aol_disarming_fault(sd.CS_prev, int(card.sm.logMonoTime['onroadEvents']), now) + for tick in (0, 1): + _, host, command = step(tick) + assert host.enabled and command.longActive + assert not receive(BASE + tick * 10_000_000) + state = fixture.car_state(brake=pedal == 'brake') + state.regenBraking = pedal == 'regen' + axis, host, command = step(2, state_stamp=BASE + 20_000_000, offset_ns=5_042_623, observed_state=state, + intent_message=intent(BASE + 20_000_000, 3)) + assert command.latActive and not command.longActive and not host.enabled + assert axis.sourceCarStateMonoTime == BASE + 20_000_000 + assert not receive(BASE + 25_042_623) + # Exact source/publication gap shape from route 28. The last native FULL + # receipt may still be in flight after the host has already withdrawn LONG. + sd.car_state_samples = () + with mock.patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=BASE + 52_866_472): + sd.publish_axis_proof(sd.CS_prev) + paused = sd.pm.messages['aolAxisState'].aolAxisState + assert str(paused.faultReason) == 'availabilityPause' + assert not paused.lateralActive and not paused.longitudinalActive and paused.sourceCarStateMonoTime == 0 + assert not sd.aol_authority_lost and EventName.controlsMismatch not in sd.events.names + from openpilot.starpilot.car.gm.aol import gm_lateral_pedal_source_zero + from openpilot.starpilot.aol.runtime import current_native + native = current_native(sd.sm, sd.CP, now_ns=BASE + 52_866_472, axis_session_id=sd.aol_session_id) + for field, value in (('accFaulted', True), ('steerFaultTemporary', True), ('steerFaultPermanent', True), + ('gearShifter', 'unknown'), ('canValid', False)): + invalid = sd.CS_prev.as_builder() + setattr(invalid, field, value) + assert not gm_lateral_pedal_source_zero(card.aol_transport_feedback, card.CP, invalid, paused, + native, BASE + 52_866_472, producer='authority-card') + assert not receive(BASE + 52_866_472) + axis, host, command = step(6, observed_state=state, native_request_long=False, panda_controls_allowed=False) + assert command.latActive and not command.longActive and not host.enabled and axis.lateralActive + + +@pytest.mark.parametrize('fault', ('native', 'can', 'cancel', 'new_pedal', 'intent_expired', 'model')) +def test_held_pedal_never_hides_real_faults_or_admits_a_new_edge(fault): + with ExitStack() as cleanup: + sd, _, step = callers(cleanup, full=True) + step(0) + state = fixture.car_state(brake=True) + step(1, observed_state=state) + sd.car_state_samples = () + cs = sd.CS_prev + if fault == 'native': + sd.sm.valid['aolSafetyWire'] = False + elif fault == 'can': + cs = cs.as_builder() + cs.canValid = False + elif fault == 'cancel': + sd.events.add(EventName.buttonCancel) + elif fault == 'new_pedal': + cs = cs.as_builder() + elif fault == 'model': + sd.sm.valid['modelV2'] = False + now = BASE + (220_000_001 if fault == 'intent_expired' else 42_866_472) + with mock.patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=now): + sd.publish_axis_proof(cs) + axis = sd.pm.messages['aolAxisState'].aolAxisState + assert not axis.lateralActive and not axis.longitudinalActive + assert sd.aol_authority_lost and EventName.controlsMismatch in sd.events.names diff --git a/openpilot/starpilot/aol/transport_pause.py b/openpilot/starpilot/aol/transport_pause.py index 36272c5599..9678b23331 100644 --- a/openpilot/starpilot/aol/transport_pause.py +++ b/openpilot/starpilot/aol/transport_pause.py @@ -162,6 +162,7 @@ class AvailabilityPause: self.baseline_ns = 0 return False if source_expired: + # Retain intent under its original 200ms lease, never a renewed command. latest = companion.latest raw = latest[2] if latest is not None and latest[1] else None verified = companion.verified_sm_projection @@ -178,7 +179,7 @@ class AvailabilityPause: not 0 < raw.carStateLogMonoTime <= now_ns or raw.observedMonoTime > now_ns or now_ns - raw.carStateLogMonoTime > SAFETY_MAX_AGE_NS or (int(sm.logMonoTime['aolIntentWire']) != raw.carStateLogMonoTime and not projection) or - not sm.seen['aolIntentWire'] or not sm.valid['aolIntentWire'] or not sm.alive['aolIntentWire']): + not sm.seen['aolIntentWire'] or not sm.valid['aolIntentWire']): self.active = False return False inventory = (healthy.pandaSerial, tuple(p.hardwareSerial for p in healthy.pandaInventory)) @@ -898,8 +899,8 @@ class TransportPauseFeedback: toyota_pedal and event.name == log.OnroadEvent.EventName.pedalPressed) and event.userDisable and not event.softDisable and not event.immediateDisable)] if cp.brand == 'gm': - from openpilot.starpilot.car.gm.aol import sdgm_lateral_pedal_source_zero - if sdgm_lateral_pedal_source_zero(self, cp, cs, axis, native, now_ns, + from openpilot.starpilot.car.gm.aol import gm_lateral_pedal_source_zero + if gm_lateral_pedal_source_zero(self, cp, cs, axis, native, now_ns, producer=intent_producer_session): events = [event for event in events if not (event.name == log.OnroadEvent.EventName.pedalPressed and event.userDisable and not event.softDisable and not event.immediateDisable)] diff --git a/openpilot/starpilot/car/gm/aol.py b/openpilot/starpilot/car/gm/aol.py index 607b8eda94..1ab2b5cf71 100644 --- a/openpilot/starpilot/car/gm/aol.py +++ b/openpilot/starpilot/car/gm/aol.py @@ -96,34 +96,35 @@ def steering_source_gap_intent_only(owner, cs, companion, now_ns: int) -> bool: return owner.ordinary_source_availability(cs, now_ns) -def held_sdgm_lateral_pedal(owner, cs) -> bool: +def held_gm_lateral_pedal(owner, cs) -> bool: """An already accepted LAT-only pedal state remains intent, never output.""" from openpilot.starpilot.nostalgia import physical_cancel anchor = getattr(owner, 'aol_ordinary_source_anchor', None) - if not sdgm_alpha_lateral_owner(owner.CP) or anchor is None: + if not qualified_gm(owner.CP) or not aol_enabled(owner.CP) or anchor is None: return False proof, native, accepted, _, accepted_state = anchor settings = getattr(owner, 'aol_settings', None) return bool(cs is accepted_state and not getattr(owner, 'car_state_samples', ()) and proof[8] and not proof[9] and not owner.enabled and not owner.active and native is not None and native.requestedLateral and native.lateralAllowed and - not native.requestedLongitudinal and not native.longitudinalAllowed and accepted is not None and accepted.allowedLatch and accepted.lateralArmed and accepted.settingsQualified and not accepted.pauseLateral and not accepted.pauseLongitudinal and settings is not None and settings.enabled and not getattr(owner, 'aol_dm_lateral_inhibit', False) and - sdgm_alpha_pedal_withdrawal(owner.CP, cs) and not cs.gasPressed and + cs.cruiseState.available and (cs.brakePressed or cs.regenBraking) and + not cs.steeringDisengage and not cs.gasPressed and not cs.seatbeltUnlatched and not physical_cancel(cs, owner.CP) and (not cs.brakePressed or cs.standstill or cs.vEgo >= settings.pause_brake_mps)) -def sdgm_lateral_pedal_source_zero(feedback, cp, cs, axis, native, now_ns, *, producer): +def gm_lateral_pedal_source_zero(feedback, cp, cs, axis, native, now_ns, *, producer): """Fresh physical braking cannot turn an accepted LAT-only source wait fatal.""" from openpilot.starpilot.aol.runtime import SAFETY_MAX_AGE_NS from openpilot.starpilot.nostalgia import physical_cancel + from opendbc.car.structs import car prior = feedback.signature - return bool(sdgm_alpha_lateral_owner(cp) and feedback.lateral_only_baseline and + return bool(qualified_gm(cp) and aol_enabled(cp) and feedback.ever_lateral_active and not feedback.failed and not feedback.pending_event and prior is not None and prior[0] == feedback.session and prior[7] == feedback.session and prior[14:16] == (producer, feedback.availability_epoch) and @@ -134,10 +135,15 @@ def sdgm_lateral_pedal_source_zero(feedback, cp, cs, axis, native, now_ns, *, pr str(axis.faultReason) == 'availabilityPause' and axis.sourceCarStateMonoTime == 0 and axis.desiredLateral and axis.nativeAcknowledged and not axis.desiredLongitudinal and not axis.lateralActive and not axis.longitudinalActive and - sdgm_alpha_pedal_withdrawal(cp, cs) and not cs.gasPressed and not cs.seatbeltUnlatched and + cs.canValid and not cs.canTimeout and cs.cruiseState.available and + not cs.steerFaultTemporary and not cs.steerFaultPermanent and not cs.accFaulted and + cs.gearShifter in (car.CarState.GearShifter.drive, car.CarState.GearShifter.low, + car.CarState.GearShifter.sport, car.CarState.GearShifter.brake, + car.CarState.GearShifter.manumatic) and + (cs.brakePressed or cs.regenBraking) and not cs.doorOpen and not cs.steeringDisengage and + not cs.gasPressed and not cs.seatbeltUnlatched and not physical_cancel(cs, cp) and native is not None and - native.requestedLateral and native.lateralAllowed and - not native.requestedLongitudinal and not native.longitudinalAllowed) + native.requestedLateral and native.lateralAllowed) def cold_gm_main_intent_current(feedback, sm, cp, cs, axis, native, now_ns, *, producer): diff --git a/prebuilt.json b/prebuilt.json index efff94d5d7..b58ffa2c30 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "0871a6b9131b55f6b8f2838589b548cd3ab52825acb1a6c367da7512c000aa76", "schema_version": 1} +{"protected_inventory_sha256": "a0affde1033797b56d8d350d2d6b221f689cc4b52aa9844cc4f8478ba00ff442", "schema_version": 1}