Files
StarPilot/openpilot/starpilot/tests/test_ford_main_pulse.py
T

189 lines
9.7 KiB
Python

"""Ford auto-MAIN synthetic pulse must stay within its original input window."""
import pytest
from opendbc.car import structs
from dataclasses import replace
from openpilot.starpilot.aol.intent import AolSettings
from openpilot.starpilot.car.ford.aol import FordCardIntent
def state(*, available, **kwargs):
return structs.CarState(
canValid=True, cruiseState=structs.CarState.CruiseState(available=available), gearShifter=structs.CarState.GearShifter.drive, **kwargs
).as_reader()
@pytest.mark.parametrize('guard', ('late', 'backwards', 'cancel', 'main-off', 'can', 'permanent', 'acc', 'disabled', 'fault'))
def test_pending_main_cannot_replay_after_stale_or_disarming_input(guard):
intent = FordCardIntent(
None, AolSettings(enabled=True, pause_brake_mps=0.0, lkas_action=0, main_action=0, distance_actions=(0, 0, 0), cancel_actions=(0, 0, 0))
)
intent.update(state(available=False), now_ns=1_000_000_000, fault_active=False)
intent.update(state(available=True), now_ns=1_010_000_000, fault_active=False)
assert not intent.allowed_latch
kwargs = {}
available = True
now = 1_020_000_000
if guard == 'late':
now = 1_040_000_001
elif guard == 'backwards':
now = 1_009_000_000
elif guard == 'cancel':
kwargs['buttonEvents'] = [structs.CarState.ButtonEvent(type=structs.CarState.ButtonEvent.Type.cancel, pressed=True)]
elif guard == 'main-off':
available = False
elif guard == 'can':
kwargs['canTimeout'] = True
elif guard == 'permanent':
kwargs['steerFaultPermanent'] = True
elif guard == 'acc':
kwargs['accFaulted'] = True
elif guard == 'disabled':
intent.settings = replace(intent.settings, enabled=False)
intent.update(state(available=available, **kwargs), now_ns=now, fault_active=guard == 'fault')
assert not intent.allowed_latch
intent.update(state(available=True), now_ns=max(now, 1_020_000_000) + 10_000_000, fault_active=False)
# A new actual MAIN rise is separate; stale pending delivery has been consumed.
if guard != 'main-off':
assert not intent.allowed_latch
def test_main_pulse_arms_once_after_prior_main_off_feedback_clears():
intent = FordCardIntent(None, AolSettings(True, 0.0, 0, 0, (0, 0, 0), (0, 0, 0)))
intent.update(state(available=False), now_ns=1_000_000_000, fault_active=False)
intent.update(state(available=True), now_ns=1_010_000_000, fault_active=False)
assert not intent.allowed_latch
intent.update(state(available=True), now_ns=1_020_000_000, fault_active=False)
assert intent.allowed_latch
intent.update(state(available=True), now_ns=1_030_000_000, fault_active=False)
assert intent.allowed_latch and not intent.main_pulse
@pytest.mark.parametrize('edge_fault', ('host', 'permanent', 'acc', 'disabled'))
def test_fault_at_original_main_edge_cannot_arm_after_fault_clears(edge_fault):
intent = FordCardIntent(None, AolSettings(True, 0.0, 0, 0, (0, 0, 0), (0, 0, 0)))
intent.update(state(available=False), now_ns=1_000_000_000, fault_active=False)
if edge_fault == 'disabled':
intent.settings = replace(intent.settings, enabled=False)
cs = state(available=True, steerFaultPermanent=edge_fault == 'permanent', accFaulted=edge_fault == 'acc')
intent.update(cs, now_ns=1_010_000_000, fault_active=edge_fault == 'host')
intent.settings = replace(intent.settings, enabled=True)
intent.update(state(available=True), now_ns=1_020_000_000, fault_active=False)
assert not intent.allowed_latch
intent.update(state(available=True), now_ns=1_030_000_000, fault_active=False)
assert not intent.allowed_latch
@pytest.mark.parametrize('standard_enabled', [False, True])
@pytest.mark.parametrize('delay_ns', [10_000_000, 30_000_000, 30_000_001])
def test_first_main_arms_when_ordinary_cruise_precedes_pulse(standard_enabled, delay_ns):
intent = FordCardIntent(None, AolSettings(True, 0.0, 0, 0, (0, 0, 0), (0, 0, 0)))
intent.update(state(available=False), now_ns=1_000_000_000, fault_active=True)
# Unit input for the readiness decision; the coupled regression obtains this
# decision from actual Card/SD publications and native receive hooks.
intent.main_idle_transition_qualified = True
edge = 1_010_000_000
intent.update(state(available=True), now_ns=edge, fault_active=True, standard_enabled=standard_enabled)
assert intent.main_pending_ns == edge and not intent.allowed_latch
intent.update(state(available=True), now_ns=edge + delay_ns, fault_active=False,
standard_enabled=standard_enabled)
assert intent.allowed_latch == (delay_ns <= 30_000_000)
assert not intent.main_pending_ns
def startup_receipt(source_pause):
from openpilot.starpilot.tests import test_ford_reverse_intent as wire
cp, cs = wire.cp_state()
intent = FordCardIntent(cp, AolSettings(True, 0.0, 0, 0, (0, 0, 0), (0, 0, 0)))
intent.update(state(available=False), now_ns=wire.NOW - 10_000_000, fault_active=True)
sm, native = wire.sample(cp, wire.NOW, wire.NOW + 10_000_000, 1, active=False, requested=False, allowed=False)
def off(axis):
axis.nativeAcknowledged = False
if source_pause:
axis.sourceCarStateMonoTime = 0
axis.faultReason = 'availabilityPause'
wire.change(sm, 'aolAxisState', off)
wire.change(sm, 'onroadEvents', lambda events: setattr(events[0], 'name', 'wrongCarMode'))
return intent, cp, cs, sm, native, wire.NOW + 10_000_000
def test_first_main_qualifies_only_current_off_receipt():
from openpilot.starpilot.tests import test_ford_reverse_intent as wire
intent, cp, cs, sm, native, now = startup_receipt(False)
stamps = dict(sm.logMonoTime)
intent.observe_startup_readiness(sm, cs, native=native, panda_ready=True, now_ns=now, producer=wire.PRODUCER)
assert intent.main_idle_transition_qualified
assert not intent.allowed_latch and sm.logMonoTime == stamps
intent.update(cs.as_reader(), fault_active=True, now_ns=now)
assert intent.main_pending_ns == now and not intent.allowed_latch
# A genuine ongoing fault consumes the pending input, even after eligibility.
intent.update(cs.as_reader(), fault_active=True, now_ns=now + 10_000_000)
intent.update(cs.as_reader(), fault_active=False, now_ns=now + 20_000_000)
assert not intent.allowed_latch and not intent.main_pending_ns
STARTUP_REFUSALS = (
'stale', 'future', 'deadline', 'producer', 'session', 'event-stamp', 'wrong-event', 'critical',
'soft', 'can', 'timeout', 'permanent', 'temporary', 'acc', 'sensor', 'door', 'gear', 'disengage',
'native-rx', 'heartbeat', 'native-request', 'native-grant', 'axis-active', 'axis-ack', 'panda-missing',
)
@pytest.mark.parametrize('source_pause', [False, True])
@pytest.mark.parametrize('fault', STARTUP_REFUSALS)
def test_first_main_does_not_admit_faulted_or_unowned_receipt(source_pause, fault):
from openpilot.starpilot.tests import test_ford_reverse_intent as wire
intent, cp, cs, sm, native, now = startup_receipt(source_pause)
producer, panda_ready = wire.PRODUCER, True
if fault == 'stale':
now += 30_000_001
elif fault == 'future':
now = wire.NOW
elif fault == 'deadline':
wire.change(sm, 'aolAxisState', lambda axis: setattr(axis, 'validUntilMonoTime', wire.NOW + 30_000_001))
elif fault == 'producer':
producer = 'other-card'
elif fault == 'session':
wire.change(sm, 'aolAxisState', lambda axis: setattr(axis, 'faultSessionId', 'other-axis'))
elif fault == 'event-stamp':
wire.change(sm, 'aolAxisState', lambda axis: setattr(axis, 'faultEventMonoTime', wire.NOW - 1))
if not source_pause:
sm.logMonoTime['onroadEvents'] = wire.NOW + 1
elif fault in ('wrong-event', 'critical', 'soft'):
def event_change(events):
events[0].name = 'controlsMismatch'
events[0].immediateDisable = fault == 'critical'
events[0].softDisable = fault == 'soft'
wire.change(sm, 'onroadEvents', event_change)
elif fault in ('can', 'timeout', 'permanent', 'temporary', 'acc', 'sensor', 'door', 'disengage'):
key = {'can': 'canValid', 'timeout': 'canTimeout', 'permanent': 'steerFaultPermanent', 'temporary': 'steerFaultTemporary',
'acc': 'accFaulted', 'sensor': 'vehicleSensorsInvalid', 'door': 'doorOpen', 'disengage': 'steeringDisengage'}[fault]
setattr(cs, key, fault != 'can')
elif fault == 'gear':
cs.gearShifter = 'reverse'
elif fault in ('native-rx', 'heartbeat'):
wire.change(sm, 'pandaStates', lambda pandas: setattr(pandas[0], 'safetyRxChecksInvalid' if fault == 'native-rx' else 'heartbeatLost', True))
elif fault in ('native-request', 'native-grant'):
native = wire.native_change(sm, native, **{'requestedLateral' if fault == 'native-request' else 'lateralAllowed': True})
elif fault in ('axis-active', 'axis-ack'):
wire.change(sm, 'aolAxisState', lambda axis: setattr(axis, 'lateralActive' if fault == 'axis-active' else 'nativeAcknowledged', True))
elif fault == 'panda-missing':
panda_ready = False
before = native
intent.observe_startup_readiness(sm, cs, native=native, panda_ready=panda_ready, now_ns=now, producer=producer)
assert not intent.main_idle_transition_qualified
assert native == before and not intent.allowed_latch
intent.update(cs.as_reader(), fault_active=True, now_ns=now)
intent.update(cs.as_reader(), fault_active=False, now_ns=now + 10_000_000)
assert not intent.allowed_latch and not intent.main_pending_ns
def test_source_zero_availability_fault_cannot_qualify_first_main():
from openpilot.starpilot.tests import test_ford_reverse_intent as wire
intent, cp, cs, sm, native, now = startup_receipt(True)
intent.observe_startup_readiness(sm, cs, native=native, panda_ready=True, now_ns=now, producer=wire.PRODUCER)
assert not intent.main_idle_transition_qualified
intent.update(cs.as_reader(), fault_active=True, now_ns=now)
intent.update(cs.as_reader(), fault_active=False, now_ns=now + 10_000_000)
assert not intent.allowed_latch and not intent.main_pending_ns