mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
346 lines
18 KiB
Python
346 lines
18 KiB
Python
"""Temporary EPS retains intent through fresh proof and bounded event joins."""
|
|
from contextlib import ExitStack
|
|
from dataclasses import replace
|
|
|
|
import pytest
|
|
|
|
from openpilot.cereal import log, messaging
|
|
from openpilot.common.prefix import OpenpilotPrefix
|
|
from openpilot.selfdrive.selfdrived.events import Events
|
|
from openpilot.starpilot.aol.tests import test_ioniq_availability as fixture
|
|
from opendbc.car.structs import car
|
|
|
|
|
|
def setup_case():
|
|
case = fixture.IoniqAvailabilityTests(methodName='runTest')
|
|
return case
|
|
|
|
|
|
def baseline(case):
|
|
sd, controls, drive, card, receive = case._ioniq_availability_callers(full_op=False)
|
|
for tick in range(2, 152):
|
|
axis, _, command = drive(tick, gas=tick >= 151, native_request_long=False)
|
|
assert axis.lateralActive and command.latActive and not command.longActive
|
|
assert receive(10_000_000_000 + tick * 10_000_000, axis=sd.pm.messages['aolAxisState'],
|
|
event=sd.pm.messages['onroadEvents'])
|
|
return sd, controls, drive, card, receive
|
|
|
|
|
|
def leases(feedback):
|
|
return (feedback.lateral_baseline_ns, feedback.lateral_source_ns, feedback.lateral_native_ns,
|
|
feedback.healthy_feedback_ns, feedback.healthy_native_ns)
|
|
|
|
|
|
@pytest.mark.parametrize('new_axis', [False, True], ids=['already_received_active', 'fresh_active_first_received_with_eps'])
|
|
@pytest.mark.parametrize('silent', [False, True])
|
|
def test_first_temporary_eps_keeps_intent_without_renewing_prior_leases(new_axis, silent):
|
|
case = setup_case()
|
|
with ExitStack() as cleanup:
|
|
cleanup.callback(case.doCleanups)
|
|
cleanup.enter_context(OpenpilotPrefix())
|
|
sd, _, drive, card, receive = baseline(case)
|
|
feedback = card.aol_transport_feedback
|
|
original = leases(feedback)
|
|
old_signature = feedback.signature
|
|
axis, _, healthy_command = drive(152, gas=True, native_request_long=False)
|
|
proof, event = sd.pm.messages['aolAxisState'], sd.pm.messages['onroadEvents']
|
|
assert axis.lateralActive and healthy_command.latActive
|
|
assert axis.sequence > old_signature[1]
|
|
if not new_axis:
|
|
assert receive(11_520_000_000, axis=proof, event=event)
|
|
original = leases(feedback)
|
|
temporary = fixture.car_state()
|
|
temporary.gasPressed = temporary.steerFaultTemporary = True
|
|
temporary.standstill = silent
|
|
temporary.vEgo = 3.323399305343628
|
|
temporary.steeringAngleDeg = -305.79998779296875
|
|
# Card receives the preceding healthy axis with its first EPS sample,
|
|
# before SelfdriveD publishes the physical withdrawal event.
|
|
assert receive(11_529_500_000, axis=proof, event=event, state=temporary)
|
|
assert card.aol_card_intent.allowed_latch and not feedback.failed
|
|
assert leases(feedback) == original
|
|
assert feedback.signature[4] == proof.aolAxisState.validUntilMonoTime
|
|
for tick in range(153, 159):
|
|
active_native = tick < 155
|
|
withdrawn, published_state, command = drive(tick, gas=True, observed_state=temporary,
|
|
allowed=active_native, native_request_lat=active_native,
|
|
native_request_long=False)
|
|
assert not withdrawn.lateralActive and not withdrawn.longitudinalActive
|
|
assert not command.latActive and not command.longActive and not sd.enabled
|
|
assert published_state.alertType == 'steerTempUnavailableSilent/warning'
|
|
assert str(published_state.alertSound) == 'prompt'
|
|
assert not published_state.enabled and not published_state.active
|
|
assert receive(10_000_000_000 + tick * 10_000_000, axis=sd.pm.messages['aolAxisState'],
|
|
event=sd.pm.messages['onroadEvents'], state=temporary)
|
|
assert card.aol_card_intent.allowed_latch and not feedback.failed
|
|
assert leases(feedback) == original
|
|
recovered, _, command = drive(159, gas=True, native_request_lat=True, native_request_long=False)
|
|
assert recovered.lateralActive and command.latActive
|
|
assert not recovered.longitudinalActive and not command.longActive and not sd.enabled
|
|
assert receive(11_590_000_000, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
|
|
assert card.aol_card_intent.allowed_latch
|
|
|
|
|
|
@pytest.mark.parametrize('fault', ['permanent_eps', 'invalid_can', 'can_timeout', 'invalid_native',
|
|
'producer', 'epoch', 'source_age', 'old_output_expired', 'cancel', 'settings_disabled', 'hard_event', 'mixed_soft',
|
|
'source_reordered', 'stamp_reordered', 'session', 'fault_session'])
|
|
def test_fresh_active_eps_never_masks_hard_fault_or_bad_original_provenance(fault):
|
|
case = setup_case()
|
|
with ExitStack() as cleanup:
|
|
cleanup.callback(case.doCleanups)
|
|
cleanup.enter_context(OpenpilotPrefix())
|
|
sd, _, drive, card, receive = baseline(case)
|
|
drive(152, gas=True, native_request_long=False)
|
|
proof = sd.pm.messages['aolAxisState'].as_builder()
|
|
event = sd.pm.messages['onroadEvents']
|
|
temporary = fixture.car_state()
|
|
temporary.gasPressed = temporary.steerFaultTemporary = True
|
|
if fault == 'permanent_eps':
|
|
temporary.steerFaultPermanent = True
|
|
elif fault == 'invalid_can':
|
|
temporary.canValid = False
|
|
elif fault == 'can_timeout':
|
|
temporary.canTimeout = True
|
|
elif fault == 'invalid_native':
|
|
sd.sm.valid['aolSafetyWire'] = False
|
|
elif fault == 'producer':
|
|
proof.aolAxisState.intentProducerSessionId = 'other-card'
|
|
elif fault == 'epoch':
|
|
proof.aolAxisState.intentIntegrityEpoch += 1
|
|
elif fault == 'source_age':
|
|
proof.aolAxisState.sourceCarStateMonoTime -= 30_000_001
|
|
elif fault == 'source_reordered':
|
|
proof.aolAxisState.sourceCarStateMonoTime = card.aol_transport_feedback.signature[8] - 1
|
|
elif fault == 'stamp_reordered':
|
|
stamp = card.aol_transport_feedback.signature[2] - 1
|
|
proof.logMonoTime = stamp
|
|
proof.aolAxisState.observedMonoTime = stamp
|
|
proof.aolAxisState.validUntilMonoTime = stamp + 30_000_000
|
|
proof.aolAxisState.sourceCarStateMonoTime = stamp - 1
|
|
elif fault == 'session':
|
|
proof.aolAxisState.sessionId = proof.aolAxisState.faultSessionId = 'other-selfdrive'
|
|
elif fault == 'fault_session':
|
|
proof.aolAxisState.faultSessionId = 'other-selfdrive'
|
|
elif fault == 'cancel':
|
|
temporary.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.cancel, pressed=True)]
|
|
elif fault == 'settings_disabled':
|
|
card.aol_card_intent.settings = replace(card.aol_card_intent.settings, enabled=False)
|
|
elif fault in ('hard_event', 'mixed_soft'):
|
|
events = Events()
|
|
events.add(log.OnroadEvent.EventName.controlsMismatch if fault == 'hard_event' else log.OnroadEvent.EventName.commIssue)
|
|
message = messaging.new_message('onroadEvents', len(events), valid=True, logMonoTime=event.logMonoTime)
|
|
message.onroadEvents = events.to_msg()
|
|
event = message.as_reader()
|
|
now = 11_540_000_001 if fault == 'old_output_expired' else 11_529_500_000
|
|
assert not receive(now, axis=proof.as_reader(), event=event, state=temporary)
|
|
assert not card.aol_card_intent.allowed_latch
|
|
|
|
|
|
def test_two_fresh_active_eps_receipts_cannot_chain_past_original_source_lease():
|
|
case = setup_case()
|
|
with ExitStack() as cleanup:
|
|
cleanup.callback(case.doCleanups)
|
|
cleanup.enter_context(OpenpilotPrefix())
|
|
sd, _, drive, card, receive = baseline(case)
|
|
feedback = card.aol_transport_feedback
|
|
original = leases(feedback)
|
|
temporary = fixture.car_state()
|
|
temporary.gasPressed = temporary.steerFaultTemporary = True
|
|
for tick in (152, 153):
|
|
# SD has not yet received EPS; each real publication uses a fresh
|
|
# healthy source. Card must keep only intent inside its fixed baseline.
|
|
axis, _, command = drive(tick, gas=True, native_request_long=False)
|
|
assert axis.lateralActive and command.latActive
|
|
now = 10_000_000_000 + tick * 10_000_000 + 9_500_000
|
|
assert receive(now, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'], state=temporary)
|
|
assert leases(feedback) == original and not feedback.failed
|
|
axis, _, _ = drive(154, gas=True, native_request_long=False)
|
|
assert axis.lateralActive
|
|
now = 11_540_000_001
|
|
assert now - original[1] > 30_000_000
|
|
assert now <= sd.pm.messages['aolAxisState'].aolAxisState.validUntilMonoTime
|
|
assert not receive(now, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'], state=temporary)
|
|
assert not card.aol_card_intent.allowed_latch
|
|
assert leases(feedback) == original
|
|
|
|
|
|
@pytest.mark.parametrize('order', ['proof_first', 'event_first', 'same_cycle'])
|
|
def test_actual_eps_withdrawal_delivery_orders_keep_only_existing_intent(order):
|
|
case = fixture.IoniqAvailabilityTests(methodName='runTest')
|
|
with ExitStack() as cleanup:
|
|
cleanup.callback(case.doCleanups)
|
|
cleanup.enter_context(OpenpilotPrefix())
|
|
sd, _, drive, card, receive = baseline(case)
|
|
feedback = card.aol_transport_feedback
|
|
original = leases(feedback)
|
|
drive(152, gas=True, native_request_long=False)
|
|
temporary = fixture.car_state()
|
|
temporary.gasPressed = temporary.steerFaultTemporary = True
|
|
assert receive(11_529_500_000, axis=sd.pm.messages['aolAxisState'],
|
|
event=sd.pm.messages['onroadEvents'], state=temporary)
|
|
assert leases(feedback) == original and not feedback.failed
|
|
withdrawn, published_state, command = drive(153, gas=True, observed_state=temporary,
|
|
native_request_lat=True, native_request_long=False)
|
|
assert not withdrawn.lateralActive and not withdrawn.longitudinalActive
|
|
assert not command.latActive and not command.longActive and not sd.enabled
|
|
assert published_state.alertType == 'steerTempUnavailableSilent/warning'
|
|
assert str(published_state.alertSound) == 'prompt'
|
|
assert not published_state.enabled and not published_state.active
|
|
proof, event = sd.pm.messages['aolAxisState'], sd.pm.messages['onroadEvents']
|
|
assert proof.aolAxisState.faultEventMonoTime == event.logMonoTime
|
|
deliveries = [{'axis': proof}, {'event': event}] if order == 'proof_first' else \
|
|
[{'event': event}, {'axis': proof}] if order == 'event_first' else [{'axis': proof, 'event': event}]
|
|
for index, delivery in enumerate(deliveries):
|
|
assert receive(11_530_000_000 + index * 100_000, state=temporary, **delivery)
|
|
assert card.aol_card_intent.allowed_latch and not feedback.failed
|
|
assert leases(feedback) == original
|
|
recovered, _, recovered_command = drive(154, gas=True, native_request_lat=True, native_request_long=False)
|
|
assert recovered.lateralActive and recovered_command.latActive
|
|
assert not recovered.longitudinalActive and not recovered_command.longActive and not sd.enabled
|
|
assert receive(11_540_000_000, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
|
|
assert card.aol_card_intent.allowed_latch and not feedback.failed
|
|
|
|
|
|
@pytest.mark.parametrize('fault', ['missing_event', 'late_event', 'duplicate_timeout', 'higher_sequence_timeout',
|
|
'changed_occurrence', 'hard_event', 'mixed_soft', 'user_event', 'invalid_native', 'native_inventory',
|
|
'producer', 'epoch', 'permanent_eps', 'can_timeout', 'cancel', 'settings_disabled', 'stale_source'])
|
|
def test_pending_eps_join_cannot_extend_or_hide_faults(fault):
|
|
case = fixture.IoniqAvailabilityTests(methodName='runTest')
|
|
with ExitStack() as cleanup:
|
|
cleanup.callback(case.doCleanups)
|
|
cleanup.enter_context(OpenpilotPrefix())
|
|
sd, _, drive, card, receive = baseline(case)
|
|
feedback = card.aol_transport_feedback
|
|
original = leases(feedback)
|
|
drive(152, gas=True, native_request_long=False)
|
|
temporary = fixture.car_state()
|
|
temporary.gasPressed = temporary.steerFaultTemporary = True
|
|
assert receive(11_529_500_000, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'], state=temporary)
|
|
axis, _, command = drive(153, gas=True, observed_state=temporary,
|
|
native_request_lat=True, native_request_long=False)
|
|
assert not axis.lateralActive and not axis.longitudinalActive
|
|
assert not command.latActive and not command.longActive
|
|
proof = sd.pm.messages['aolAxisState']
|
|
event = sd.pm.messages['onroadEvents']
|
|
anchor = proof.aolAxisState.faultEventMonoTime
|
|
assert receive(11_530_000_000, axis=proof, state=temporary)
|
|
assert feedback.pending_event == anchor and feedback.awaiting and not feedback.failed
|
|
assert leases(feedback) == original
|
|
now = 11_530_100_000
|
|
delivery = {}
|
|
if fault in ('missing_event', 'late_event', 'duplicate_timeout', 'higher_sequence_timeout'):
|
|
if fault == 'duplicate_timeout':
|
|
assert receive(11_550_000_000, axis=proof, state=temporary)
|
|
assert feedback.pending_event == anchor
|
|
elif fault == 'higher_sequence_timeout':
|
|
for tick in (154, 155):
|
|
axis, _, command = drive(tick, gas=True, observed_state=temporary, allowed=False,
|
|
native_request_lat=False, native_request_long=False)
|
|
assert not command.latActive and not command.longActive
|
|
assert sd.pm.messages['aolAxisState'].aolAxisState.faultEventMonoTime == anchor
|
|
assert receive(10_000_000_000 + tick * 10_000_000, axis=sd.pm.messages['aolAxisState'], state=temporary)
|
|
assert feedback.pending_event == anchor
|
|
assert leases(feedback) == original
|
|
axis, _, command = drive(156, gas=True, observed_state=temporary, allowed=False,
|
|
native_request_lat=False, native_request_long=False)
|
|
proof = sd.pm.messages['aolAxisState']
|
|
assert not axis.lateralActive and not command.latActive and not command.longActive
|
|
assert proof.aolAxisState.faultEventMonoTime == anchor
|
|
now = anchor + 30_000_001
|
|
assert now <= proof.aolAxisState.validUntilMonoTime
|
|
delivery = {'axis': proof}
|
|
if fault == 'late_event':
|
|
delivery['event'] = event
|
|
elif fault == 'changed_occurrence':
|
|
changed = proof.as_builder()
|
|
changed.aolAxisState.faultEventMonoTime += 1
|
|
delivery = {'axis': changed.as_reader()}
|
|
elif fault in ('hard_event', 'mixed_soft', 'user_event'):
|
|
events = Events()
|
|
events.add(log.OnroadEvent.EventName.steerTempUnavailable)
|
|
events.add({'hard_event': log.OnroadEvent.EventName.controlsMismatch,
|
|
'mixed_soft': log.OnroadEvent.EventName.commIssue,
|
|
'user_event': log.OnroadEvent.EventName.buttonCancel}[fault])
|
|
message = messaging.new_message('onroadEvents', len(events), valid=True, logMonoTime=anchor)
|
|
message.onroadEvents = events.to_msg()
|
|
delivery = {'event': message.as_reader()}
|
|
elif fault == 'invalid_native':
|
|
sd.sm.valid['aolSafetyWire'] = False
|
|
elif fault == 'native_inventory':
|
|
message = messaging.new_message('pandaStates', len(sd.sm['pandaStates']), valid=True,
|
|
logMonoTime=sd.sm.logMonoTime['pandaStates'])
|
|
message.pandaStates = sd.sm['pandaStates']
|
|
message.pandaStates[-1].safetyParam ^= 1
|
|
sd.sm.update_msgs(now / 1e9, [message.as_reader()])
|
|
elif fault in ('producer', 'epoch', 'stale_source'):
|
|
changed = proof.as_builder()
|
|
if fault == 'producer':
|
|
changed.aolAxisState.intentProducerSessionId = 'other-card'
|
|
elif fault == 'epoch':
|
|
changed.aolAxisState.intentIntegrityEpoch += 1
|
|
else:
|
|
changed.aolAxisState.sourceCarStateMonoTime -= 30_000_001
|
|
delivery = {'axis': changed.as_reader()}
|
|
elif fault == 'permanent_eps':
|
|
temporary.steerFaultPermanent = True
|
|
elif fault == 'can_timeout':
|
|
temporary.canTimeout = True
|
|
elif fault == 'cancel':
|
|
temporary.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.cancel, pressed=True)]
|
|
elif fault == 'settings_disabled':
|
|
card.aol_card_intent.settings = replace(card.aol_card_intent.settings, enabled=False)
|
|
assert not receive(now, state=temporary, **delivery)
|
|
assert not card.aol_card_intent.allowed_latch
|
|
assert leases(feedback) == original
|
|
|
|
|
|
def test_initial_eps_off_cannot_reorder_the_preceding_accepted_active_source():
|
|
case = fixture.IoniqAvailabilityTests(methodName='runTest')
|
|
with ExitStack() as cleanup:
|
|
cleanup.callback(case.doCleanups)
|
|
cleanup.enter_context(OpenpilotPrefix())
|
|
sd, _, drive, card, receive = baseline(case)
|
|
drive(152, gas=True, native_request_long=False)
|
|
temporary = fixture.car_state()
|
|
temporary.gasPressed = temporary.steerFaultTemporary = True
|
|
assert receive(11_529_500_000, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'], state=temporary)
|
|
previous = card.aol_transport_feedback.signature
|
|
axis, _, command = drive(153, gas=True, observed_state=temporary,
|
|
native_request_lat=True, native_request_long=False)
|
|
assert not axis.lateralActive and not command.latActive and not command.longActive
|
|
proof = sd.pm.messages['aolAxisState'].as_builder()
|
|
proof.aolAxisState.sourceCarStateMonoTime = previous[8] - 1
|
|
assert proof.aolAxisState.sourceCarStateMonoTime <= proof.aolAxisState.observedMonoTime
|
|
assert 11_530_000_000 - proof.aolAxisState.sourceCarStateMonoTime < 30_000_000
|
|
assert not receive(11_530_000_000, axis=proof.as_reader(), state=temporary)
|
|
assert not card.aol_card_intent.allowed_latch
|
|
|
|
|
|
def test_axis_first_eps_join_uses_last_accepted_active_proof_without_renewing_leases():
|
|
case = fixture.IoniqAvailabilityTests(methodName='runTest')
|
|
with OpenpilotPrefix(), ExitStack() as cleanup:
|
|
cleanup.callback(case.doCleanups)
|
|
sd, _, drive, card, receive = baseline(case)
|
|
feedback = card.aol_transport_feedback
|
|
original = leases(feedback)
|
|
drive(152, gas=True, native_request_long=False)
|
|
temporary = fixture.car_state()
|
|
temporary.gasPressed = temporary.steerFaultTemporary = True
|
|
assert receive(11_529_500_000, axis=sd.pm.messages['aolAxisState'],
|
|
event=sd.pm.messages['onroadEvents'], state=temporary)
|
|
active = feedback.signature
|
|
assert leases(feedback) == original
|
|
withdrawn, _, command = drive(153, gas=True, observed_state=temporary,
|
|
native_request_lat=True, native_request_long=False)
|
|
assert not withdrawn.lateralActive and not withdrawn.longitudinalActive
|
|
assert not command.latActive and not command.longActive
|
|
proof, event = sd.pm.messages['aolAxisState'], sd.pm.messages['onroadEvents']
|
|
now = 11_541_000_000
|
|
assert now - original[1] > 30_000_000
|
|
assert now - active[8] <= 30_000_000 and now <= active[4]
|
|
assert receive(now, axis=proof, state=temporary)
|
|
assert card.aol_card_intent.allowed_latch and not feedback.failed
|
|
assert leases(feedback) == original
|
|
assert receive(now + 100_000, event=event, state=temporary)
|
|
assert card.aol_card_intent.allowed_latch and leases(feedback) == original
|