diff --git a/opendbc_repo/opendbc/safety/tests/test_ioniq6_native_pedal_order.py b/opendbc_repo/opendbc/safety/tests/test_ioniq6_native_pedal_order.py new file mode 100644 index 0000000000..dd193107b4 --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_ioniq6_native_pedal_order.py @@ -0,0 +1,149 @@ +"""Ioniq6 route9b native brake withdrawal before the host's pedal sample.""" +from contextlib import ExitStack + +import pytest + +from opendbc.can import CANPacker +from opendbc.safety.tests import test_hyundai_ioniq6_long as native_fixture +from openpilot.cereal import log, messaging +from openpilot.common.prefix import OpenpilotPrefix +from openpilot.starpilot.aol.tests import test_ioniq_availability as fixture +from openpilot.starpilot.aol.tests.test_gm_held_native import Peers +from openpilot.starpilot.aol.transport_pause import TransportPauseFeedback +from openpilot.starpilot.aol.wire import PandaSlot, SafetyState, encode_safety + +BASE = 10_000_000_000 +# Actual route9b/segment2 ECAN TCS frames, counters195 and196. CRC is validated by real native RX. +TCS_BEFORE = bytes.fromhex('3389c3008803880311140000010000010f00000000000000') +TCS_BRAKE = bytes.fromhex('72f4c4008403840301140200010000000f00000000000000') + + +def profile(alternate): + _, cp = native_fixture.params('lkas_alt' if alternate else 'lkas') + assert cp is not None + cp.safetyConfigs[0].safetyParam |= 0x0800 + return cp + + +def physical_full(case, packer, raw): + case.mode(raw, stamp=BASE // 1000) + case.safety.set_aol_test_heartbeat(True) + for name, values in (('ACCELERATOR', {'GEAR': 5}), ('TCS', {'ACCEnable': 0}), + ('WHEEL_SPEEDS', dict.fromkeys(('WHL_SpdFLVal', 'WHL_SpdFRVal', 'WHL_SpdRLVal', 'WHL_SpdRRVal'), 45.)), + ('MDPS', {}), ('CRUISE_BUTTONS', {'COUNTER': 1})): + assert case.safety.safety_rx_hook(case.packet(packer.make_can_msg(name, 1, values))) + case.safety.aol_set_host_request(3) + for counter, values in ((2, {'LDA_BTN': 1}), (3, {}), (4, {'CRUISE_BUTTONS': 2}), (5, {})): + assert case.safety.safety_rx_hook(case.packet(packer.make_can_msg('CRUISE_BUTTONS', 1, dict(COUNTER=counter, **values)))) + case.safety.aol_set_host_request(3) + assert case.safety.get_controls_allowed() and case.safety.aol_get_permission_mask() == 3 + + +@pytest.mark.parametrize('alternate', [False, True]) +def test_real_native_brake_before_host_sample_keeps_granted_lateral(alternate): + case = fixture.IoniqAvailabilityTests(methodName='runTest') + native = native_fixture.TestHyundaiIoniq6Long(methodName='runTest') + with OpenpilotPrefix(), ExitStack() as cleanup: + cleanup.callback(case.doCleanups) + cleanup.callback(native.doCleanups) + cp = profile(alternate) + sd, _, step = case._authority_callers(True, cp=cp, genuine_events=True) + native.setUp() + packer = CANPacker('hyundai_canfd_generated') + physical_full(native, packer, int(cp.safetyConfigs[0].safetyParam)) + for tick in range(10): + native.refresh_required_rx(packer, tick + 6, (BASE + tick * 10_000_000) // 1000) + native.safety.set_aol_test_heartbeat(True) + native.safety.aol_set_host_request(3) + assert native.safety.aol_get_permission_mask() == 3 + axis, state, command = step(tick) + assert state.enabled and axis.lateralActive and command.longActive + native.safety.set_timer((BASE + 95_000_000) // 1000) + assert native.safety.safety_rx_hook(native.packet((0x175, TCS_BEFORE, 1))) + before = native.safety.get_controls_allowed() + assert before + positive = packer.make_can_msg('SCC_CONTROL', 1, {'COUNTER': 0, 'ACCMode': 1, 'aReqRaw': 1., 'aReqValue': 1.}) + assert native.safety.safety_tx_hook(native.packet(positive)) + assert native.safety.safety_rx_hook(native.packet((0x175, TCS_BRAKE, 1))) + mask = native.safety.aol_get_permission_mask() + assert mask == 1 + positive = packer.make_can_msg('SCC_CONTROL', 1, {'COUNTER': 1, 'ACCMode': 1, 'aReqRaw': 1., 'aReqValue': 1.}) + assert not native.safety.safety_tx_hook(native.packet(positive)) + # Health was sampled before the permission read; parsed host brake has not arrived. + axis, state, command = step(10, native_request_lat=True, native_request_long=True, + allowed=True, long_allowed=False, panda_controls_allowed=before) + assert state.enabled and axis.lateralActive and command.latActive + assert not axis.longitudinalActive and not command.longActive + assert not sd.aol_authority_lost and log.OnroadEvent.EventName.controlsMismatch not in sd.events.names + axis, state, command = step(11, brake=True, native_request_lat=True, native_request_long=True, + long_allowed=False, panda_controls_allowed=False) + assert not state.enabled and not command.longActive + assert log.OnroadEvent.EventName.controlsMismatch not in sd.events.names + axis, state, command = step(12, native_request_lat=True, native_request_long=False, + long_allowed=False, panda_controls_allowed=False) + assert axis.lateralActive and command.latActive and not command.longActive + + +@pytest.mark.parametrize('alternate', [False, True]) +@pytest.mark.parametrize('fault', ['lat_denied', 'native_invalid', 'rx_fault', 'relay_fault', 'missing_prior']) +def test_native_early_withdrawal_preserves_all_existing_fault_vetoes(alternate, fault): + case = fixture.IoniqAvailabilityTests(methodName='runTest') + with OpenpilotPrefix(), ExitStack() as cleanup: + cleanup.callback(case.doCleanups) + sd, _, step = case._authority_callers(True, cp=profile(alternate), genuine_events=True) + for tick in range(10): + axis, state, command = step(tick) + assert state.enabled and axis.lateralActive and command.longActive + options = dict(native_request_lat=True, native_request_long=True, long_allowed=False, panda_controls_allowed=True) + if fault == 'lat_denied': + options['allowed'] = False + elif fault == 'native_invalid': + options['native_valid'] = False + elif fault == 'rx_fault': + options['panda_fault'] = True + elif fault == 'relay_fault': + options['relay_fault'] = True + else: + sd.aol_published_input_proof = None + axis, state, command = step(10, **options) + assert log.OnroadEvent.EventName.controlsMismatch in sd.events.names + assert not state.enabled and not axis.lateralActive and not command.latActive and not command.longActive + + +def feedback_peers(cp, now, *, denied=False, controls=True): + config = cp.safetyConfigs[0] + slot = PandaSlot(0, 'ioniq6-unit', 28, config.safetyParam, cp.alternativeExperience, controls) + native = SafetyState(1, True, now, now + 200_000_000, 28, config.safetyParam, + True, not denied, True, True, 'ioniq6-unit', 'ioniq6-session', now, (slot,)) + wire = messaging.new_message('aolSafetyWire', 0, valid=True, logMonoTime=now) + wire.aolSafetyWire = encode_safety(native) + pandas = messaging.new_message('pandaStates', 1, valid=True, logMonoTime=now) + pandas.pandaStates[0] = dict(safetyModel='hyundaiCanfd', safetyParam=config.safetyParam, + alternativeExperience=cp.alternativeExperience, controlsAllowed=controls) + axis = messaging.new_message('aolAxisState', valid=True, logMonoTime=now) + axis.aolAxisState = dict(qualified=True, sequence=1 + (now - BASE) // 10_000_000, sessionId='ioniq6-session', faultSessionId='ioniq6-session', + observedMonoTime=now, validUntilMonoTime=now + 30_000_000, sourceCarStateMonoTime=now, + desiredLateral=True, lateralActive=True, desiredLongitudinal=True, longitudinalActive=True, + nativeAcknowledged=True, mode='combined', faultReason='none', intentProducerSessionId='ioniq6-card', intentIntegrityEpoch=1) + events = messaging.new_message('onroadEvents', 0, valid=True, logMonoTime=now) + return Peers([m.as_reader() for m in (wire, pandas, axis, events)]) + + +@pytest.mark.parametrize('alternate', [False, True]) +@pytest.mark.parametrize('controls', [False, True]) +def test_card_feedback_keeps_existing_latch_during_native_first_withdrawal(alternate, controls): + cp = profile(alternate) + feedback = TransportPauseFeedback(cp) + cs = fixture.car_state() + cs.cruiseState.available = cs.cruiseState.enabled = True + assert not feedback.observe(feedback_peers(cp, BASE), cp, cs, BASE, + latched=True, intent_producer_session='ioniq6-card') + assert feedback.ever_lateral_active and feedback.lateral_baseline_ns == BASE + assert feedback.session == 'ioniq6-session' and feedback.lateral_inventory + assert feedback.availability_producer == 'ioniq6-card' + now = BASE + 10_000_000 + assert not feedback.observe(feedback_peers(cp, now, denied=True, controls=controls), cp, cs, now, + latched=True, intent_producer_session='ioniq6-card') + assert not feedback.failed + assert feedback.healthy_feedback_ns == BASE and feedback.healthy_native_ns == BASE + assert feedback.lateral_baseline_ns == now diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py index dad4bed974..31c5c11e07 100644 --- a/openpilot/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -1714,8 +1714,11 @@ class SelfdriveD: # Toyota's native LONG mask also checks gas, brake and belt independently # of controls_allowed. Health and that mask may precede the parsed pedal # edge: honor LONG withdrawal while the existing native LAT grant stands. - # The exact Toyota policy and all proof/health checks above remain required. + # Ioniq6 also checks the physical brake before its separately published host sample. + # Retain only its already granted LAT; all proof, health and LONG withdrawal gates remain. + # The exact vehicle policies and all proof/health checks above remain required. return bool(gm_native_accepts_cp(self.CP, native.safetyModel, native.safetyParam) or + qualified_ioniq6(self.CP) or toyota_aol_qualified(self.CP, marked_only=True) or not native.pandaInventory[owners[0]].controlsAllowed and not self.sm['pandaStates'][owners[0]].controlsAllowed) diff --git a/openpilot/starpilot/aol/transport_pause.py b/openpilot/starpilot/aol/transport_pause.py index ed0ff416f2..d319089cfd 100644 --- a/openpilot/starpilot/aol/transport_pause.py +++ b/openpilot/starpilot/aol/transport_pause.py @@ -307,15 +307,17 @@ class TransportPauseFeedback: """ from openpilot.starpilot.nostalgia import physical_cancel from openpilot.starpilot.car.gm.aol import native_accepts_cp + from openpilot.starpilot.car.hyundai.aol import qualified_ioniq6 gm = native_accepts_cp(cp, int(cp.safetyConfigs[0].safetyModel.raw), int(cp.safetyConfigs[0].safetyParam)) + early_long = gm or cp.openpilotLongitudinalControl and qualified_ioniq6(cp) if (not latched or self.failed or self.paused or self.pending_event or self.retention_started_ns or not self.session or str(axis.sessionId) != self.session or not producer or producer != self.availability_producer or str(getattr(axis, 'intentProducerSessionId', '')) != producer or int(getattr(axis, 'intentIntegrityEpoch', 0)) != self.availability_epoch or not self.lateral_inventory or not self.lateral_baseline_ns or - str(axis.faultReason) != 'none' or not gm and str(axis.mode) != 'lateralOnly' or not axis.nativeAcknowledged or - not axis.desiredLateral or not axis.lateralActive or axis.longitudinalActive and not gm or + str(axis.faultReason) != 'none' or not early_long and str(axis.mode) != 'lateralOnly' or not axis.nativeAcknowledged or + not axis.desiredLateral or not axis.lateralActive or axis.longitudinalActive and not early_long or not 0 < self.lateral_source_ns <= axis.sourceCarStateMonoTime <= axis.observedMonoTime <= now_ns or now_ns - axis.sourceCarStateMonoTime > AXIS_MAX_AGE_NS or not cs.canValid or cs.canTimeout or cs.steerFaultTemporary or cs.steerFaultPermanent or cs.accFaulted or @@ -332,7 +334,7 @@ class TransportPauseFeedback: return None owners = [index for index, config in enumerate(cp.safetyConfigs) if str(config.safetyModel) not in ('silent', 'noOutput')] - return native if (len(owners) == 1 and (gm or not native.pandaInventory[owners[0]].controlsAllowed and + return native if (len(owners) == 1 and (early_long or not native.pandaInventory[owners[0]].controlsAllowed and not sm['pandaStates'][owners[0]].controlsAllowed)) else None def _temporary_host_intent_current(self, sm, cp, cs, axis, stamp, now_ns, *, latched, producer): diff --git a/prebuilt.json b/prebuilt.json index c099175cd5..15ea674483 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "b5bc81205648c97489057d37376179a97e15b43cc622ca1c2c77241a5408865c", "schema_version": 1} +{"protected_inventory_sha256": "0f60df516139cd3d3de7afc26989a738ea49e9525f48ce8a4089784a9abd64e0", "schema_version": 1}