Handle Ioniq 6 native brake withdrawal before host sampling

This commit is contained in:
firestar5683
2026-10-21 22:48:16 -05:00
parent 70799d5bc3
commit 4b9de265bf
4 changed files with 159 additions and 5 deletions
@@ -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
+4 -1
View File
@@ -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)
+5 -3
View File
@@ -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):
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "b5bc81205648c97489057d37376179a97e15b43cc622ca1c2c77241a5408865c", "schema_version": 1}
{"protected_inventory_sha256": "0f60df516139cd3d3de7afc26989a738ea49e9525f48ce8a4089784a9abd64e0", "schema_version": 1}