mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
Preserve the original EV6 output proof across an input timeout
This commit is contained in:
@@ -504,9 +504,11 @@ class SelfdriveD:
|
||||
self.events.add(EventName.radarFault)
|
||||
hardware_events_added = len(self.events) - num_events
|
||||
|
||||
from opendbc.car.hyundai.ev6_aol import qualified as ev6_deadline_qualified
|
||||
input_proof = getattr(self, 'aol_step_input_proof', None)
|
||||
reconcile_input = bool(input_proof is not None and not REPLAY and self.aol_replay and self.axis_transport_required and
|
||||
self.CP.openpilotLongitudinalControl and qualified_ioniq6(self.CP) and
|
||||
self.CP.openpilotLongitudinalControl and
|
||||
(qualified_ioniq6(self.CP) or ev6_deadline_qualified(self.CP, marked_only=True)) and
|
||||
input_proof[0] == self.aol_session_id and
|
||||
(input_proof[8] or input_proof[9]))
|
||||
self.aol_event_input_reconcile = reconcile_input
|
||||
@@ -806,6 +808,10 @@ class SelfdriveD:
|
||||
|
||||
return CS
|
||||
|
||||
def source_deadline_deferral_supported(self):
|
||||
from opendbc.car.hyundai.ev6_aol import qualified as ev6_qualified
|
||||
return qualified_ioniq6(self.CP) or qualified_gm(self.CP) or ev6_qualified(self.CP, marked_only=True)
|
||||
|
||||
def held_axis_override_wait(self, CS, proof, now_ns, *, fresh_successor=False):
|
||||
"""Recognize an already applied override; never admit a new physical edge."""
|
||||
if (proof is None or not (qualified_ioniq6(self.CP) or qualified_gm(self.CP)) or not self.sm.seen['carControl'] or
|
||||
@@ -968,14 +974,24 @@ class SelfdriveD:
|
||||
|
||||
def defer_expired_axis_publication(self, CS, initial_state, *, now_ns=None):
|
||||
"""Keep an existing unexpired proof for one normal tick; issue no new lease."""
|
||||
if REPLAY or not self.aol_replay or not self.axis_transport_required or not (qualified_ioniq6(self.CP) or qualified_gm(self.CP)):
|
||||
if REPLAY or not self.aol_replay or not self.axis_transport_required or not self.source_deadline_deferral_supported():
|
||||
return False
|
||||
proof = getattr(self, 'aol_step_input_proof', None)
|
||||
if now_ns is None:
|
||||
now_ns = time.monotonic_ns()
|
||||
source = self.aol_car_state_log_ns
|
||||
source_valid = self.conditional_car_state_valid
|
||||
from opendbc.car.hyundai.ev6_aol import qualified as ev6_qualified
|
||||
if (ev6_qualified(self.CP, marked_only=True) and source == 0 and not source_valid and
|
||||
getattr(self, 'aol_expired_healthy_source', False) and
|
||||
not getattr(self, 'car_state_samples', ()) and not getattr(self, 'car_state_queue_invalid', False) and
|
||||
getattr(self, 'car_state_retained_valid', False)):
|
||||
# A socket timeout cleared the selected source. Validate only the old
|
||||
# publication's receipt; keep the source invalid and its clocks unchanged.
|
||||
source = getattr(self, 'car_state_retained_source_ns', 0)
|
||||
source_valid = True
|
||||
if (getattr(self, 'aol_step_deferral_checked', False) or proof is None or proof != getattr(self, 'aol_published_input_proof', None) or
|
||||
not self.conditional_car_state_valid or not 0 < source <= now_ns or
|
||||
not source_valid or not 0 < source <= now_ns or
|
||||
now_ns - source <= INTENT_MAX_AGE_NS or source != proof[1] or
|
||||
now_ns >= proof[3] or self.aol_authority_lost or
|
||||
getattr(self, 'aol_fault_reason', 'none') != 'none' or
|
||||
@@ -1443,7 +1459,7 @@ class SelfdriveD:
|
||||
final_ns = time.monotonic_ns()
|
||||
if ((axis.lateralActive or axis.longitudinalActive) and
|
||||
(not 0 < self.aol_car_state_log_ns <= final_ns or final_ns - self.aol_car_state_log_ns > INTENT_MAX_AGE_NS) and
|
||||
(qualified_ioniq6(self.CP) or qualified_gm(self.CP))):
|
||||
self.source_deadline_deferral_supported()):
|
||||
if (source_context is not None and source_context[1] and
|
||||
self.defer_expired_axis_publication(CS, source_context[0])):
|
||||
# Serialization crossed the same source boundary. Retain only the
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
"""Recorded EV6 8f2e source cutoff keeps only its original unexpired proof."""
|
||||
from contextlib import ExitStack
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
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_ev6_physical_authorization import ev6_params
|
||||
from openpilot.starpilot.aol.tests.test_ordinary_source_availability import settings
|
||||
|
||||
SOURCE = 10_020_000_000
|
||||
|
||||
|
||||
@pytest.mark.parametrize('socket_timeout', [False, True])
|
||||
@pytest.mark.parametrize('age_ns,published_offset_ns', [
|
||||
(30_960_101, 4_945_854),
|
||||
(31_244_572, 5_262_889),
|
||||
(31_383_633, 5_039_179),
|
||||
(30_890_079, 4_798_596),
|
||||
(31_008_968, 5_325_976),
|
||||
(31_351_205, 5_392_434),
|
||||
(31_544_229, 5_502_541),
|
||||
(32_161_269, 6_033_539),
|
||||
(31_684_749, 5_741_086),
|
||||
(31_896_523, 5_808_274),
|
||||
(32_254_551, 5_671_607),
|
||||
(31_565_476, 5_393_216),
|
||||
(30_879_839, 5_223_629),
|
||||
])
|
||||
def test_ev6_recorded_source_cutoff_retains_only_original_unexpired_output(age_ns, published_offset_ns, socket_timeout):
|
||||
case = fixture.IoniqAvailabilityTests(methodName='runTest')
|
||||
with ExitStack() as cleanup:
|
||||
cleanup.callback(case.doCleanups)
|
||||
cleanup.enter_context(OpenpilotPrefix())
|
||||
cp = ev6_params()
|
||||
sd, controls, drive = case._authority_callers(True, cp=cp, genuine_events=True)
|
||||
assert sd.CP.carFingerprint == 'KIA_EV6'
|
||||
assert sd.CP.safetyConfigs[0].safetyParam == 0x815 and sd.CP.alternativeExperience == 32
|
||||
for tick in (0, 1):
|
||||
drive(tick, **settings('combined', 10_000_000_000 + tick * 10_000_000, tick + 1))
|
||||
original, state, command = drive(2, state_stamp=SOURCE, offset_ns=published_offset_ns, **settings('combined', SOURCE, 3))
|
||||
assert original.lateralActive and original.longitudinalActive and command.latActive and command.longActive
|
||||
deadline = original.validUntilMonoTime
|
||||
assert deadline == SOURCE + published_offset_ns + 30_000_000
|
||||
old = original.to_dict()
|
||||
timer = sd.state_machine.soft_disable_timer
|
||||
sd.car_state_poller = mock.Mock()
|
||||
sd.car_state_poller.poll.return_value = []
|
||||
axis, state, command = drive(5, timeout=socket_timeout, state_stamp=SOURCE, offset_ns=age_ns - 30_000_000,
|
||||
**settings('combined', SOURCE, 3))
|
||||
assert sd.aol_axis_publication_deferred
|
||||
if socket_timeout:
|
||||
assert sd.aol_car_state_log_ns == 0 and not sd.conditional_car_state_valid
|
||||
else:
|
||||
assert sd.aol_car_state_log_ns == SOURCE and sd.conditional_car_state_valid
|
||||
assert axis.to_dict() == old and axis.validUntilMonoTime == deadline
|
||||
assert state.enabled and sd.state_machine.soft_disable_timer == timer
|
||||
assert log.OnroadEvent.EventName.commIssue not in sd.events.names
|
||||
# The consumer stops at the original cutoff; retaining a proof never renews it.
|
||||
with mock.patch('openpilot.selfdrive.controls.controlsd.time.monotonic_ns', return_value=deadline + 1):
|
||||
expired, _ = controls.state_control()
|
||||
assert not expired.latActive and not expired.longActive
|
||||
# A second stale loop cannot repeat the deferral even within that deadline.
|
||||
next_axis, _, next_command = drive(5, timeout=True, state_stamp=SOURCE, offset_ns=age_ns - 30_000_000 + 100_000,
|
||||
**settings('combined', SOURCE, 3))
|
||||
assert not sd.aol_axis_publication_deferred
|
||||
assert not next_axis.lateralActive and not next_axis.longitudinalActive
|
||||
assert not next_command.latActive and not next_command.longActive
|
||||
|
||||
|
||||
@pytest.mark.parametrize('fault', ['can', 'epsTemporary', 'epsPermanent', 'acc', 'brake', 'gas',
|
||||
'pandaRx', 'relay', 'nativeInvalid', 'uncalibrated'])
|
||||
def test_ev6_unexpired_output_never_defers_a_new_fault(fault):
|
||||
case = fixture.IoniqAvailabilityTests(methodName='runTest')
|
||||
with ExitStack() as cleanup:
|
||||
cleanup.callback(case.doCleanups)
|
||||
cleanup.enter_context(OpenpilotPrefix())
|
||||
sd, _, drive = case._authority_callers(True, cp=ev6_params(), genuine_events=True)
|
||||
for tick in (0, 1, 2):
|
||||
stamp = 10_000_000_000 + tick * 10_000_000
|
||||
axis, _, command = drive(tick, state_stamp=stamp, offset_ns=4_945_854 if tick == 2 else 0,
|
||||
**settings('combined', stamp, tick + 1))
|
||||
assert axis.lateralActive and axis.longitudinalActive and command.latActive and command.longActive
|
||||
state = fixture.car_state()
|
||||
changes = {}
|
||||
if fault == 'can':
|
||||
state.canValid = False
|
||||
elif fault == 'epsTemporary':
|
||||
state.steerFaultTemporary = True
|
||||
elif fault == 'epsPermanent':
|
||||
state.steerFaultPermanent = True
|
||||
elif fault == 'acc':
|
||||
state.accFaulted = True
|
||||
elif fault == 'brake':
|
||||
changes['brake'] = True
|
||||
elif fault == 'gas':
|
||||
changes['gas'] = True
|
||||
elif fault == 'pandaRx':
|
||||
changes['panda_fault'] = True
|
||||
elif fault == 'relay':
|
||||
changes['relay_fault'] = True
|
||||
elif fault == 'nativeInvalid':
|
||||
changes['native_valid'] = False
|
||||
elif fault == 'uncalibrated':
|
||||
# Calibration publishes at 1 Hz; the normal fixture cadence correctly
|
||||
# skips a replacement only 30 ms later. Deliver the new envelope itself.
|
||||
stamp = SOURCE + 30_960_101
|
||||
calibration = messaging.new_message('extrinsicsCalibration', valid=True, logMonoTime=stamp)
|
||||
calibration.extrinsicsCalibration.calStatus = 'uncalibrated'
|
||||
sd.sm.update_msgs(stamp / 1e9, [calibration.as_reader()])
|
||||
if fault in ('can', 'epsTemporary', 'epsPermanent', 'acc'):
|
||||
changes['observed_state'] = state
|
||||
axis, _, command = drive(5, state_stamp=SOURCE, offset_ns=960_101,
|
||||
**settings('combined', SOURCE, 3), **changes)
|
||||
if fault == 'uncalibrated':
|
||||
assert sd.sm['extrinsicsCalibration'].calStatus == log.ExtrinsicsCalibration.Status.uncalibrated
|
||||
assert not sd.aol_axis_publication_deferred
|
||||
# The existing fault/pedal rules select the remaining axes; the previous
|
||||
# combined proof cannot survive the new restriction.
|
||||
assert not (axis.lateralActive and axis.longitudinalActive)
|
||||
assert not (command.latActive and command.longActive)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('timing', ['timely', 'late_poll', 'processing_crosses_deadline'])
|
||||
def test_ev6_fresh_pair_reconciles_without_extending_the_old_deadline(timing):
|
||||
from openpilot.starpilot.aol.tests import test_input_deadline_continuity as continuity
|
||||
|
||||
def ev6_callers(case, full_op=True, *, stock_angle=False):
|
||||
assert full_op and not stock_angle
|
||||
sd, controls, drive = case._authority_callers(True, cp=ev6_params(), genuine_events=True)
|
||||
assert sd.CP.carFingerprint == 'KIA_EV6'
|
||||
assert sd.CP.safetyConfigs[0].safetyParam == 0x815 and sd.CP.alternativeExperience == 32
|
||||
for tick in (0, 1):
|
||||
stamp = 10_000_000_000 + tick * 10_000_000
|
||||
drive(tick, **settings('combined', stamp, tick + 1))
|
||||
return sd, controls, drive, None, None
|
||||
|
||||
# Reuse the existing timing stimulus with the captured EV6 configuration;
|
||||
# production event, source, native and consumer checks remain real.
|
||||
with mock.patch.object(fixture.IoniqAvailabilityTests, '_ioniq_availability_callers', ev6_callers):
|
||||
continuity.test_fresh_source_replaces_expired_output_without_restoring_disengagement('ordinary', timing)
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"protected_inventory_sha256": "dc8ad8a50e217c02cb820e05312df6d418b5c0853da58033fd068049e19737fe", "schema_version": 1}
|
||||
{"protected_inventory_sha256": "f519a895eeac1226e4ab79722e1227633eee2369587b661939cb6e5f10c4ae2d", "schema_version": 1}
|
||||
|
||||
@@ -24,6 +24,7 @@ from tools.ci import vehicle_pytest
|
||||
# Representative production callers plus their source/native refusal cases.
|
||||
# The emitted selected_test_ids record exact parametrized configurations.
|
||||
AOL_LIFECYCLE_TARGETS = (
|
||||
'openpilot/starpilot/aol/tests/test_ev6_source_deadline.py',
|
||||
"openpilot/starpilot/car/rivian/tests/test_aol_policy.py",
|
||||
"openpilot/starpilot/car/rivian/tests/test_aol_native.py",
|
||||
"openpilot/starpilot/car/rivian/tests/test_aol_lifecycle.py",
|
||||
@@ -114,6 +115,7 @@ DEBUG_SAFETY_CLASS_TARGETS = {
|
||||
|
||||
|
||||
RELEASE_TARGETS = (
|
||||
'openpilot/starpilot/aol/tests/test_ev6_source_deadline.py',
|
||||
"openpilot/starpilot/aol/tests/test_ev9_main_withdrawal_guards.py",
|
||||
"opendbc_repo/opendbc/safety/tests/test_ev9_combined_aol.py::TestEV9CombinedReleaseDenied",
|
||||
"opendbc_repo/opendbc/car/hyundai/tests/test_ev9_combined_aol.py::TestEV9CombinedProfile",
|
||||
|
||||
@@ -46,6 +46,11 @@ class TestVehicleCollection(unittest.TestCase):
|
||||
}
|
||||
self.assertLessEqual(expected, set(targets_for('safety-release')))
|
||||
|
||||
def test_ev6_source_deadline_refusals_run_in_lifecycle_and_release_ci(self):
|
||||
target = 'openpilot/starpilot/aol/tests/test_ev6_source_deadline.py'
|
||||
self.assertIn(target, targets_for('aol-lifecycle'))
|
||||
self.assertIn(target, targets_for('safety-release'))
|
||||
|
||||
def test_ev6_release_ci_exercises_real_alpha_startup_and_physical_owner(self):
|
||||
expected = {
|
||||
'openpilot/starpilot/tests/test_ev6_startup.py',
|
||||
|
||||
Reference in New Issue
Block a user