Preserve GM lateral intent across brief Card source gaps

This commit is contained in:
firestar5683
2026-10-31 21:48:35 -05:00
parent cd585f55c2
commit 77ff308c76
5 changed files with 161 additions and 16 deletions
@@ -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
@@ -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
+4 -3
View File
@@ -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)]
+15 -9
View File
@@ -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):
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "0871a6b9131b55f6b8f2838589b548cd3ab52825acb1a6c367da7512c000aa76", "schema_version": 1}
{"protected_inventory_sha256": "a0affde1033797b56d8d350d2d6b221f689cc4b52aa9844cc4f8478ba00ff442", "schema_version": 1}