Files
StarPilot/openpilot/starpilot/aol/tests/test_panda_inventory.py
T
firestar5683 80fbb4f6db Feature registration
Register vehicle controls, planner modes, UI actions and Galaxy services after their providers exist. Wire the shared runtime entrypoints and cover their combined behavior.

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
Co-authored-by: Daniel Koepping <elkoled@gmail.com>
Co-authored-by: Danny <dannyirwin31@yahoo.com>
Co-authored-by: DimitriChrysafis <Dimitri.chrysafis@gmail.com>
Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com>
Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com>
Co-authored-by: Kacper Rączy <gfw.kra@gmail.com>
Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu>
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>
Co-authored-by: stef <19478336+stefpi@users.noreply.github.com>
Co-authored-by: whoisdomi <dcantu83@gmail.com>

Co-authored-by: Intelli <contact@intelli.software>
Co-authored-by: haraschax <6804392+haraschax@users.noreply.github.com>
Co-authored-by: prabhaavp <prabhaav@msu.edu>
2026-10-14 20:02:39 -05:00

558 lines
29 KiB
Python

"""Inventory consumers use coherent producer health and preserve axis guards.
Synthetic IPC exercises actual SelfdriveD, Controls and Card callers. Physical
USB and production main enumeration/reconnect remain separate qualifications.
"""
from dataclasses import replace
from unittest import mock
import pytest
from openpilot.cereal import custom, log, messaging
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.car.tests.test_hyundai_aol import candidate
from openpilot.starpilot.aol.runtime import current_native, native_inventory_matches_cp
from openpilot.starpilot.aol.transport_pause import healthy_transport
from openpilot.starpilot.aol.vehicle import native_matches_cp, policy_for
from openpilot.starpilot.aol.wire import PandaSlot, SafetyState, decode_safety, encode_safety
from openpilot.starpilot.aol.tests import test_runtime as runtime_tests
from openpilot.starpilot.aol.tests.test_runtime import car_state
NOW = 10_000_000_000
BOOT_OFFSET = 9_000_000_000
def configured_cp():
_, cp = candidate(True)
cp.safetyConfigs[-1].safetyParam |= 0x0800
policy = policy_for(cp)
cp.safetyConfigs[-1].safetyParam |= policy.safety_param_addition
cp.alternativeExperience |= policy.alternative_experience_addition
assert native_matches_cp(cp, cp.safetyConfigs[-1].safetyModel.raw, cp.safetyConfigs[-1].safetyParam)
return cp
def bound_state(cp, *, now=NOW, serial='authority-panda', session='authority-session'):
slots = tuple(PandaSlot(i, serial if i == len(cp.safetyConfigs) - 1 else 'auxiliary-panda',
int(config.safetyModel.raw), int(config.safetyParam), int(cp.alternativeExperience),
str(config.safetyModel) not in ('silent', 'noOutput'))
for i, config in enumerate(cp.safetyConfigs))
return SafetyState(1, True, now, now + 200_000_000,
slots[-1].safetyModel, slots[-1].safetyParam, True, True, True, True,
serial, session, now + BOOT_OFFSET, slots)
def sm_for(cp, state, *, stamp=None, panda_stamp=None):
services = ['aolSafetyWire', 'pandaStates']
sm = messaging.SubMaster(services, ignore_avg_freq=services)
native = messaging.new_message('aolSafetyWire', 0, valid=True,
logMonoTime=state.observedMonoTime if stamp is None else stamp)
native.aolSafetyWire = encode_safety(state)
pandas = messaging.new_message('pandaStates', len(cp.safetyConfigs), valid=True,
logMonoTime=state.sourcePandaStatesMonoTime if panda_stamp is None else panda_stamp)
for panda, config in zip(pandas.pandaStates, cp.safetyConfigs, strict=True):
panda.safetyModel, panda.safetyParam = config.safetyModel, config.safetyParam
panda.alternativeExperience = cp.alternativeExperience
panda.controlsAllowed = str(config.safetyModel) not in ('silent', 'noOutput')
sm.update_msgs((NOW - 1_000_000) / 1e9, [native.as_reader(), pandas.as_reader()])
return sm
@pytest.mark.parametrize('mutation', ['serial', 'slot', 'model', 'param', 'ae', 'empty_serial',
'rx_invalid', 'heartbeat', 'fault'])
def test_present_inventory_rejects_borrowed_or_malformed_owner(mutation):
cp = configured_cp()
state = bound_state(cp)
assert current_native(sm_for(cp, state), cp, now_ns=NOW, axis_session_id=state.axisSessionId) == state
changes = {'slot': {'slotIndex': 1}, 'model': {'safetyModel': 19}, 'param': {'safetyParam': 0},
'ae': {'alternativeExperience': cp.alternativeExperience ^ 1}, 'empty_serial': {'hardwareSerial': ''},
'rx_invalid': {'safetyRxChecksInvalid': True}, 'heartbeat': {'heartbeatLost': True}, 'fault': {'faults': 1}}
bad = replace(state, pandaSerial='borrowed-same-profile') if mutation == 'serial' else replace(
state, pandaInventory=(replace(state.pandaInventory[0], **changes[mutation]),))
assert current_native(sm_for(cp, bad), cp, now_ns=NOW, axis_session_id=state.axisSessionId) is None
@pytest.mark.parametrize('boundary', ['at_200ms', 'past_200ms', 'future', 'wrong_session', 'wrong_event', 'extended_lease'])
def test_inventory_uses_original_monotonic_lease(boundary):
cp = configured_cp()
state = bound_state(cp)
now = NOW + (200_000_000 if boundary == 'at_200ms' else 200_000_001 if boundary == 'past_200ms' else 0)
if boundary == 'future':
state = bound_state(cp, now=NOW + 1)
if boundary == 'extended_lease':
state = replace(state, validUntilMonoTime=NOW + 200_000_001)
sm = sm_for(cp, state, stamp=NOW - 1 if boundary == 'wrong_event' else None)
native = current_native(sm, cp, now_ns=now,
axis_session_id='foreign-session' if boundary == 'wrong_session' else state.axisSessionId)
assert (native is not None) == (boundary == 'at_200ms')
@pytest.mark.parametrize('arrival', ['older', 'newer', 'boot_offset', 'latest_fault', 'latest_invalid'])
def test_coherent_health_avoids_independently_conflated_stamp_race(arrival):
cp = configured_cp()
state = bound_state(cp)
stamp = NOW - 100_000_000 if arrival == 'older' else NOW + 1 if arrival == 'newer' else NOW + BOOT_OFFSET
sm = sm_for(cp, state, panda_stamp=stamp)
if arrival == 'latest_fault':
message = messaging.new_message('pandaStates', 1, valid=True, logMonoTime=stamp)
message.pandaStates[0] = sm['pandaStates'][0]
message.pandaStates[0].heartbeatLost = True
sm.update_msgs(NOW / 1e9, [message.as_reader()])
elif arrival == 'latest_invalid':
sm.valid['pandaStates'] = False
healthy = healthy_transport(sm, cp, car_state(), state.axisSessionId, NOW)
assert healthy == (arrival not in ('latest_fault', 'latest_invalid'))
def test_legacy_and_strict_version_rollout():
cp = configured_cp()
state = bound_state(cp)
legacy = replace(state, sourcePandaStatesMonoTime=0, pandaInventory=())
assert decode_safety(encode_safety(legacy)) == legacy
assert current_native(sm_for(cp, legacy, panda_stamp=NOW), cp, now_ns=NOW) == legacy
assert not legacy.pandaInventory # Historical compatibility is not inventory qualification.
for version in (1, 2):
wire = custom.AolAxisState.SafetyWire.new_message(kind=1, version=version)
if version == 1:
wire.sourcePandaStatesMonoTime = NOW
assert decode_safety(wire.to_bytes()) is None
with pytest.raises(ValueError):
encode_safety(replace(state, sourcePandaStatesMonoTime=0))
def test_two_slot_identity_does_not_bypass_existing_port_admission():
cp = configured_cp()
original = cp.safetyConfigs[0].to_dict()
cp.safetyConfigs = [{'safetyModel': 'noOutput', 'safetyParam': 0}, original]
state = bound_state(cp)
assert native_inventory_matches_cp(state, cp)
assert not native_matches_cp(cp, state.safetyModel, state.safetyParam)
assert current_native(sm_for(cp, state), cp, now_ns=NOW) is None
for slots in ((state.pandaInventory[1], state.pandaInventory[0]),
(replace(state.pandaInventory[0], hardwareSerial=state.pandaSerial), state.pandaInventory[1]),
(replace(state.pandaInventory[0], controlsAllowed=True), state.pandaInventory[1]),
(state.pandaInventory[1],)):
assert not native_inventory_matches_cp(replace(state, pandaInventory=slots), cp)
@pytest.mark.parametrize('failure', ['none', 'serial', 'ae', 'heartbeat', 'critical', 'stale_intent', 'transport_pause'])
def test_actual_sd_controls_card_inventory_callers(failure):
case = runtime_tests.IpcAxisContractTests(methodName='runTest')
try:
with OpenpilotPrefix():
sd, controls, step, card, feedback = case._transport_callers()
real_encode = encode_safety
corrupt = [False]
def production_shaped_wire(value):
state = bound_state(sd.CP, now=value.observedMonoTime, serial=value.pandaSerial, session=value.axisSessionId)
state = replace(state, lateralAllowed=value.lateralAllowed, longitudinalAllowed=value.longitudinalAllowed,
requestedLateral=value.requestedLateral, requestedLongitudinal=value.requestedLongitudinal)
if corrupt[0] and failure == 'serial':
state = replace(state, pandaSerial='borrowed-same-profile')
if corrupt[0] and failure in ('ae', 'heartbeat'):
slot = replace(state.pandaInventory[0], **({'alternativeExperience': sd.CP.alternativeExperience ^ 1}
if failure == 'ae' else {'heartbeatLost': True}))
state = replace(state, pandaInventory=(slot,))
return real_encode(state)
with mock.patch('openpilot.starpilot.aol.tests.test_ioniq_availability.encode_safety', production_shaped_wire):
axis, _, command = step(0)
assert axis.lateralActive and axis.longitudinalActive and command.latActive and command.longActive
assert not feedback(NOW, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
assert card.aol_card_intent.allowed_latch
corrupt[0] = True
axis, driver_state, command = step(8 if failure == 'transport_pause' else 4 if failure == 'stale_intent' else 1,
old_intent=failure == 'stale_intent', timeout=failure == 'transport_pause',
critical_event=log.OnroadEvent.EventName.canError if failure == 'critical' else None)
now = int(sd.pm.messages['aolAxisState'].logMonoTime)
fault = feedback(now, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
if failure == 'none':
assert axis.lateralActive and axis.longitudinalActive and command.latActive and command.longActive
assert not fault and card.aol_card_intent.allowed_latch
elif failure == 'transport_pause':
assert not axis.lateralActive and not axis.longitudinalActive and not command.latActive and not command.longActive
assert not fault and card.aol_card_intent.allowed_latch
# Coherent inventory qualifies ordinary host-loss soft disable;
# active commands stay OFF and only exact armed intent is retained.
assert str(axis.faultReason) == 'availabilityPause'
assert axis.sourceCarStateMonoTime == 0 and not sd.conditional_car_state_valid
assert driver_state.enabled and driver_state.state == log.SelfdriveState.OpenpilotState.softDisabling
assert log.OnroadEvent.EventName.commIssue in sd.events.names
assert not sd.aol_authority_lost and not getattr(sd, 'aol_availability_long_inhibit', False)
else:
assert not axis.lateralActive and not axis.longitudinalActive and not command.latActive and not command.longActive
assert fault and not card.aol_card_intent.allowed_latch
assert str(axis.faultReason) == 'critical'
finally:
case.doCleanups()
class _CancelObservationReady(Exception):
pass
class _Ev6CancelCaller:
"""Real parser/Card receipt and physical intent, with synthetic native IPC.
Halt state_update after its real CAN observation; SLC/radar are unrelated to
this regression. The joined production actor separately proves native TX.
"""
def __init__(self):
import time
from types import SimpleNamespace
from opendbc.car.hyundai.interface import CarInterface
from opendbc.can.packer import CANPacker
from openpilot.selfdrive.car.card import Car
from openpilot.starpilot.aol.intent import AolSettings, AolProcessFaultContext, AOL_TOGGLE
from openpilot.starpilot.aol.tests.test_ev6_physical_authorization import ev6_params
from openpilot.starpilot.aol.transport_pause import TransportPauseFeedback
from openpilot.starpilot.car.hyundai.ev6_intent import Ev6CardIntent
self.clock = time
self.cp = ev6_params()
assert self.cp.safetyConfigs[-1].safetyParam == 0x15
self.cp.safetyConfigs[-1].safetyParam = 0x815
self.cp.alternativeExperience = 32
self.card = Car.__new__(Car)
self.card.CP, self.card.CI = self.cp, CarInterface(self.cp)
self.card.CI.update([])
self.packers = {bus: CANPacker(parser.dbc.name) for bus, parser in self.card.CI.can_parsers.items()}
self.counters = {}
self.drive = next(value for value, name in self.card.CI.CS.shifter_values.items() if name == 'D')
self._observation_stubs = {
'RI': SimpleNamespace(update=lambda _: None),
'timing_mark': lambda _: None,
'observe_ioniq6_long_authority': lambda *_: None,
'update_vehicle_state_context': lambda _: None,
'refresh_slc_configuration': self._stop_after_observation,
}
self.card.can_sock = object()
self.card.can_rcv_cum_timeout_counter = 0
services = ['aolAxisState', 'aolSafetyWire', 'pandaStates', 'onroadEvents']
self.card.sm = messaging.SubMaster(services, ignore_avg_freq=services)
self.card.aol_transport_feedback = TransportPauseFeedback(self.cp)
self.card.aol_process_fault_context = AolProcessFaultContext()
self.card.aol_card_intent = Ev6CardIntent(AolSettings(True, 0., AOL_TOGGLE, 0, (0, 0, 0), (0, 0, 0)))
self.sequence = 0
self.last_now = 0
@staticmethod
def _stop_after_observation(_):
raise _CancelObservationReady
def parse(self, button=0, *, stamp_delta=0, lkas=0):
common = {'GEAR': self.drive, 'DRIVER_SEATBELT': 1, 'DriverBraking': 0, 'ACCEnable': 0, 'ACC_REQ': 0,
'WHL_SpdFLVal': 45., 'WHL_SpdFRVal': 45., 'WHL_SpdRLVal': 45., 'WHL_SpdRRVal': 45.,
'ACCMode': 0, 'MainMode_ACC': 1, 'VSetDis': 60, 'DISTANCE_UNIT': 0,
'MDPS_StrTqSnsrVal': 0, 'MDPS_OutTqVal': 0, 'MDPS_LkaFailSta': 0}
frames = []
for bus, parser in self.card.CI.can_parsers.items():
for address in sorted(parser.addresses):
message = parser.dbc.addr_to_msg[address]
values = {name: value for name, value in common.items() if name in message.sigs}
if message.name == 'CRUISE_BUTTONS':
values.update(ADAPTIVE_CRUISE_MAIN_BTN=0, LDA_BTN=lkas, CRUISE_BUTTONS=button)
key = (parser.bus, message.name)
counter = self.counters.get(key, 0)
self.counters[key] = counter + 1
if 'COUNTER' in message.sigs:
values['COUNTER'] = counter % (16 if message.name == 'CRUISE_BUTTONS' else 256)
frames.append(self.packers[bus].make_can_msg(message.name, parser.bus, values))
can = messaging.new_message('can', len(frames), valid=True,
logMonoTime=self.clock.clock_gettime_ns(self.clock.CLOCK_BOOTTIME) + stamp_delta)
for target, (address, data, bus) in zip(can.can, frames, strict=True):
target.address, target.dat, target.src = address, data, bus
with (
mock.patch('openpilot.selfdrive.car.card.messaging.drain_sock_raw', return_value=[can.to_bytes()]),
mock.patch.multiple(self.card, create=True,
RI=self._observation_stubs['RI'],
timing_mark=self._observation_stubs['timing_mark'],
observe_ioniq6_long_authority=self._observation_stubs['observe_ioniq6_long_authority'],
update_vehicle_state_context=self._observation_stubs['update_vehicle_state_context'],
refresh_slc_configuration=self._observation_stubs['refresh_slc_configuration']),
):
with pytest.raises(_CancelObservationReady):
self.card.state_update()
self.cs = self.card._aol_can_observation[0]
self.last_now = self.clock.monotonic_ns()
assert self.cs.canValid and not self.cs.canTimeout
return self.cs
def intent(self, *, fault=False, active=False, now=None):
now = self.last_now if now is None else now
owner = self.card.aol_card_intent
owner.observe_physical_samples(self.card.CI.CS, now_ns=now)
owner.update(self.cs, now_ns=now, fault_active=fault, standard_enabled=active)
owner.observe_full_engagement(self.cs, enabled=active, active=active, source_ns=now,
observation_valid=True, now_ns=now, fault_active=fault)
return owner
def arm(self):
from opendbc.car.hyundai.values import Buttons
self.parse(Buttons.NONE)
assert not self.intent().allowed_latch
self.parse(Buttons.SET_DECEL)
assert not self.intent().allowed_latch
self.parse(Buttons.NONE)
assert not self.intent().allowed_latch
assert self.intent(active=True, now=self.last_now + 1).allowed_latch
assert self.card.aol_card_intent.physical.authorized
def evidence(self, *, partial=False, lat_only=False, now=None, mutation=None, event=None):
now = self.clock.monotonic_ns() if now is None else now
self.last_now = now
self.sequence += 1
state = bound_state(self.cp, now=now, session='ev6-cancel-session')
state = replace(state, requestedLongitudinal=not lat_only, longitudinalAllowed=not partial and not lat_only,
sourcePandaStatesMonoTime=self.clock.clock_gettime_ns(self.clock.CLOCK_BOOTTIME),
pandaInventory=tuple(replace(p, controlsAllowed=not partial and not lat_only) for p in state.pandaInventory))
if mutation == 'owner':
state = replace(state, pandaSerial='replacement',
pandaInventory=tuple(replace(p, hardwareSerial='replacement') for p in state.pandaInventory))
if mutation in ('ae', 'rx', 'heartbeat', 'native_fault'):
changes = {'ae': {'alternativeExperience': 0}, 'rx': {'safetyRxChecksInvalid': True},
'heartbeat': {'heartbeatLost': True}, 'native_fault': {'faults': 1}}
state = replace(state, pandaInventory=tuple(replace(p, **changes[mutation]) for p in state.pandaInventory))
if mutation == 'native_stale':
state = replace(state, observedMonoTime=now - 200_000_001, validUntilMonoTime=now - 1)
if mutation == 'lat_denied':
state = replace(state, lateralAllowed=False)
native = messaging.new_message('aolSafetyWire', 0, valid=True, logMonoTime=state.observedMonoTime)
native.aolSafetyWire = encode_safety(state)
pandas = messaging.new_message('pandaStates', len(self.cp.safetyConfigs), valid=True,
logMonoTime=state.sourcePandaStatesMonoTime)
for panda, config in zip(pandas.pandaStates, self.cp.safetyConfigs, strict=True):
panda.safetyModel, panda.safetyParam = config.safetyModel, config.safetyParam
panda.alternativeExperience = self.cp.alternativeExperience
panda.controlsAllowed = not partial and not lat_only
if mutation == 'latest_fault':
pandas.pandaStates[-1].heartbeatLost = True
axis = messaging.new_message('aolAxisState', valid=True, logMonoTime=now)
axis.aolAxisState = {'sessionId': state.axisSessionId, 'sequence': self.sequence,
'sourceCarStateMonoTime': now, 'observedMonoTime': now, 'validUntilMonoTime': now + 30_000_000,
'mode': 'lateralOnly' if lat_only else 'combined', 'lateralActive': True, 'longitudinalActive': not lat_only,
'qualified': True, 'desiredLateral': True, 'desiredLongitudinal': not lat_only,
'nativeAcknowledged': True, 'faultReason': 'none', 'faultSessionId': state.axisSessionId}
if mutation == 'session':
axis.aolAxisState.sessionId = axis.aolAxisState.faultSessionId = 'new-session'
if mutation == 'sequence':
axis.aolAxisState.sequence = 1
if mutation == 'axis_stale':
axis.logMonoTime = axis.aolAxisState.observedMonoTime = now - 30_000_001
axis.aolAxisState.validUntilMonoTime = now - 1
events = messaging.new_message('onroadEvents', int(event is not None), valid=True, logMonoTime=now)
if event is not None:
events.onroadEvents[0].name = event
events.onroadEvents[0].immediateDisable = True
self.card.sm.update_msgs(now / 1e9, [native.as_reader(), pandas.as_reader(), axis.as_reader(), events.as_reader()])
self.native = state
return now
def fault(self, *, now=None):
now = self.last_now if now is None else now
result = self.card.aol_disarming_fault(self.cs, int(self.card.sm.logMonoTime['onroadEvents']), now)
self.intent(fault=result, active=True, now=now)
return result
def baseline(self):
self.arm()
self.parse()
self.evidence()
assert not self.fault()
feedback = self.card.aol_transport_feedback
assert feedback.session and feedback.lateral_baseline_ns and feedback.lateral_inventory
assert self.card.aol_card_intent.allowed_latch
def assert_no_exemption(self):
feedback = self.card.aol_transport_feedback
assert not feedback.qualified and not feedback.awaiting and not feedback.paused
@pytest.mark.parametrize('order', ['wire_before_press', 'press_before_wire'])
def test_ev6_cancel_real_card_async_orders_retain_only_existing_intent(order):
from opendbc.car.hyundai.values import Buttons
from openpilot.starpilot.aol.runtime import ordinary_axis_acknowledged
with OpenpilotPrefix():
caller = _Ev6CancelCaller()
caller.baseline()
if order == 'wire_before_press':
caller.evidence(partial=True)
caller.parse(Buttons.CANCEL)
else:
caller.parse(Buttons.CANCEL)
caller.evidence()
assert caller.card.aol_cancel_source(caller.cs, caller.last_now) > 0
assert not caller.fault()
first = caller.card.aol_transport_feedback.longitudinal_cancel_ns
assert first > 0
caller.assert_no_exemption()
caller.parse(Buttons.NONE)
caller.evidence(partial=True)
assert not caller.fault()
assert caller.card.aol_transport_feedback.longitudinal_cancel_ns == first
assert caller.card.aol_card_intent.allowed_latch and caller.card.aol_card_intent.output(caller.cs)[0]
assert not ordinary_axis_acknowledged(caller.card.sm, caller.cp, now_ns=caller.last_now)
caller.parse()
caller.evidence(lat_only=True)
assert not caller.fault()
assert ordinary_axis_acknowledged(caller.card.sm, caller.cp, now_ns=caller.last_now)
assert not caller.native.requestedLongitudinal and not caller.native.longitudinalAllowed
assert caller.card.aol_transport_feedback.longitudinal_cancel_ns == 0
caller.assert_no_exemption()
assert caller.card.aol_card_intent.allowed_latch
@pytest.mark.parametrize('mutation', ['cp', 'owner', 'ae', 'session', 'sequence'])
def test_ev6_cancel_rejects_changed_identity_and_continuity(mutation):
from opendbc.car.hyundai.values import Buttons
with OpenpilotPrefix():
caller = _Ev6CancelCaller()
caller.baseline()
caller.parse(Buttons.CANCEL)
if mutation == 'cp':
caller.cp.safetyConfigs[-1].safetyParam = 0x15
caller.evidence(partial=True, mutation=mutation)
assert caller.fault()
caller.assert_no_exemption()
assert not caller.card.aol_card_intent.allowed_latch
@pytest.mark.parametrize('source', ['queued_old', 'future_batch', 'old_local', 'wrong_cs', 'native_stale', 'axis_stale'])
def test_ev6_cancel_requires_current_parser_batch_and_two_clock_receipts(source):
from opendbc.car.hyundai.values import Buttons
with OpenpilotPrefix():
caller = _Ev6CancelCaller()
caller.baseline()
caller.parse(Buttons.CANCEL, stamp_delta=-40_000_000 if source == 'queued_old' else 1_000_000_000 if source == 'future_batch' else 0)
if source == 'wrong_cs':
caller.cs = caller.cs.as_reader().as_builder()
now = caller.clock.monotonic_ns() + (30_000_001 if source == 'old_local' else 0)
caller.evidence(partial=True, now=now, mutation=source)
if source in ('queued_old', 'future_batch', 'old_local', 'wrong_cs'):
assert caller.card.aol_cancel_source(caller.cs, now) == 0
assert caller.fault()
caller.assert_no_exemption()
assert not caller.card.aol_card_intent.allowed_latch
def test_ev6_cancel_deadline_cannot_be_renewed_by_duplicate_held_or_release():
from opendbc.car.hyundai.values import Buttons
with OpenpilotPrefix():
caller = _Ev6CancelCaller()
caller.baseline()
caller.parse(Buttons.CANCEL)
caller.evidence(partial=True)
assert not caller.fault()
first = caller.card.aol_transport_feedback.longitudinal_cancel_ns
assert not caller.fault() # Identical Card observation is not a new occurrence.
assert caller.card.aol_transport_feedback.longitudinal_cancel_ns == first
for button in (Buttons.CANCEL, Buttons.NONE):
caller.parse(button)
caller.evidence(partial=True)
assert not caller.fault()
assert caller.card.aol_transport_feedback.longitudinal_cancel_ns == first
# Fresh typed IPC keeps the independent 30ms baseline current. Explicit
# input times exercise the deadline without a global clock override/sleep.
for offset in range(20_000_000, 180_000_001, 20_000_000):
caller.evidence(partial=True, now=first + offset)
assert not caller.card.aol_disarming_fault(caller.cs, caller.last_now, caller.last_now)
assert caller.card.aol_transport_feedback.longitudinal_cancel_ns == first
caller.assert_no_exemption()
assert first + 200_000_001 - caller.card.aol_transport_feedback.lateral_baseline_ns <= 30_000_000
caller.evidence(partial=True, now=first + 200_000_001)
assert caller.fault()
caller.assert_no_exemption()
assert not caller.card.aol_card_intent.allowed_latch
@pytest.mark.parametrize('mutation', ['lat_denied', 'rx', 'heartbeat', 'native_fault', 'latest_fault', 'steer', 'acc'])
def test_ev6_cancel_lateral_and_hard_fault_vetoes_remain_closed(mutation):
from opendbc.car.hyundai.values import Buttons
with OpenpilotPrefix():
caller = _Ev6CancelCaller()
caller.baseline()
caller.parse(Buttons.CANCEL)
if mutation == 'steer':
caller.cs.steerFaultPermanent = True
if mutation == 'acc':
caller.cs.accFaulted = True
caller.evidence(partial=True, mutation=mutation)
assert caller.fault()
caller.assert_no_exemption()
assert not caller.card.aol_card_intent.allowed_latch
@pytest.mark.parametrize('case', ['no_baseline', 'non_cancel'])
def test_ev6_cancel_needs_physical_occurrence_and_accepted_lateral_baseline(case):
from opendbc.car.hyundai.values import Buttons
with OpenpilotPrefix():
caller = _Ev6CancelCaller()
if case == 'no_baseline':
caller.arm()
else:
caller.baseline()
caller.parse(Buttons.CANCEL if case == 'no_baseline' else Buttons.NONE)
caller.evidence(partial=True)
fault = caller.fault()
assert caller.card.aol_transport_feedback.longitudinal_cancel_ns == 0
caller.assert_no_exemption()
if case == 'non_cancel':
assert fault and not caller.card.aol_card_intent.allowed_latch
assert not caller.native.longitudinalAllowed
@pytest.mark.parametrize('event', [log.OnroadEvent.EventName.controlsMismatch, log.OnroadEvent.EventName.canError],
ids=['controlsMismatch', 'canError'])
def test_ev6_cancel_never_exempts_controls_mismatch_or_critical_events(event):
from opendbc.car.hyundai.values import Buttons
with OpenpilotPrefix():
caller = _Ev6CancelCaller()
caller.baseline()
caller.parse(Buttons.CANCEL)
caller.evidence(partial=True, event=event)
assert caller.fault()
caller.assert_no_exemption()
assert not caller.card.aol_card_intent.allowed_latch
@pytest.mark.parametrize('change', ['latest_fault', 'cp_change', 'native_expiry', 'borrowed_owner'])
def test_feedback_native_snapshot_is_revalidated_each_observation(change):
from openpilot.starpilot.aol import transport_pause
cp = configured_cp()
state = bound_state(cp)
services = ['aolAxisState', 'aolSafetyWire', 'pandaStates', 'onroadEvents']
sm = messaging.SubMaster(services, ignore_avg_freq=services)
initial = sm_for(cp, state)
native = messaging.new_message('aolSafetyWire', 0, valid=True, logMonoTime=NOW)
native.aolSafetyWire = encode_safety(state)
pandas = messaging.new_message('pandaStates', len(cp.safetyConfigs), valid=True, logMonoTime=NOW + BOOT_OFFSET)
pandas.pandaStates = initial['pandaStates']
axis = messaging.new_message('aolAxisState', valid=True, logMonoTime=NOW)
axis.aolAxisState = {'sessionId': state.axisSessionId, 'sequence': 1,
'sourceCarStateMonoTime': NOW, 'observedMonoTime': NOW, 'validUntilMonoTime': NOW + 30_000_000,
'qualified': True, 'desiredLateral': True, 'lateralActive': True, 'nativeAcknowledged': True,
'faultReason': 'none', 'faultSessionId': state.axisSessionId}
events = messaging.new_message('onroadEvents', 0, valid=True, logMonoTime=NOW)
sm.update_msgs(NOW / 1e9, [native.as_reader(), pandas.as_reader(), axis.as_reader(), events.as_reader()])
feedback = transport_pause.TransportPauseFeedback(cp)
with mock.patch.object(transport_pause, 'current_native', wraps=current_native) as validate:
assert not feedback.observe(sm, cp, car_state(), NOW)
assert validate.call_count == 1
assert feedback.lateral_baseline_ns == NOW and feedback.lateral_inventory
later = NOW + 1_000_000
if change == 'latest_fault':
pandas.pandaStates[-1].heartbeatLost = True
sm.update_msgs(later / 1e9, [pandas.as_reader()])
elif change == 'cp_change':
cp.safetyConfigs[-1].safetyParam ^= 1
else:
bad = (replace(state, validUntilMonoTime=NOW + 500_000) if change == 'native_expiry' else
replace(state, pandaSerial='borrowed-owner'))
native.aolSafetyWire = encode_safety(bad)
sm.update_msgs(later / 1e9, [native.as_reader()])
assert feedback.observe(sm, cp, car_state(), later)
assert validate.call_count == 2
assert feedback.failed and not feedback.qualified and not feedback.awaiting