Defer EV9 ECU takeover until replacement controls are ready

This commit is contained in:
firestar5683
2026-10-21 19:37:46 -05:00
parent 44beeb7606
commit 1973ad6439
13 changed files with 845 additions and 28 deletions
@@ -160,9 +160,10 @@ class CarController(CarControllerBase):
self._ray_disengage_frame = None
def update(self, CC, CS, now_nanos):
if self.pe_longitudinal is not None and (self.pe_startup is None or not self.pe_startup.can_emit(self)):
pending_startup_required = self.pe_longitudinal is not None or self.ev9_longitudinal is not None
if pending_startup_required and (self.pe_startup is None or not self.pe_startup.can_emit(self)):
return structs.CarControl.Actuators.new_message(), []
if self.pe_longitudinal is not None and not self.pe_startup.handed_off:
if pending_startup_required and not self.pe_startup.handed_off:
CC = structs.CarControl.new_message().as_reader()
_projection = self.learning_projection_observer
if _projection is not None:
+1 -1
View File
@@ -70,7 +70,7 @@ class CarState(CarStateBase):
self.long_buttons = (LongCruiseButtons(track_main=CP.carFingerprint != CAR.HYUNDAI_IONIQ_5_PE)
if long_buttons_qualified(CP) else None)
self.pe_long = pe_long_qualified(CP)
self.pe_ready = PEReadySource() if self.pe_long else None
self.pe_ready = PEReadySource() if self.pe_long or ev9_long_qualified(CP) else None
self.ev9_long = ev9_long_qualified(CP) or self.pe_long
self.ev9_camera_lead = (CANFDCameraLead(CP, CanBus(CP).ECAN) if self.pe_long else
EV9CameraLead(CP) if self.ev9_long else None)
@@ -0,0 +1,44 @@
"""EV9 pending startup retains source UDS outcomes and waits for replacement controls."""
import time
from opendbc.car.hyundai.ev9_longitudinal import qualified
from opendbc.car.hyundai.ioniq5pe_startup import PEPendingStartup
from opendbc.car.isotp_parallel_query import IsoTpParallelQuery
class EV9PendingStartup(PEPendingStartup):
@staticmethod
def profile_qualified(cp):
return qualified(cp)
def __init__(self, cp, callbacks, *, stock_cp, exchange=None, **kwargs):
self.disable_response = None
super().__init__(cp, callbacks, stock_cp=stock_cp,
exchange=self._ev9_exchange if exchange is None else exchange, **kwargs)
def _ev9_exchange(self, recv, send, request, response):
attempts = 10 if request == b"\x10\x03" else 1
for _ in range(attempts):
query = IsoTpParallelQuery(send, recv, 1, [0x730], [request], [b""])
replies = query.get_data(.1, total_timeout=.1)
if request == b"\x28\x01\x01":
if not replies:
# Silence is sent, unconfirmed; the inherited post-request quiet and
# independent-source proof must still pass before ownership commits.
self.disable_response = 'no_reply_sent_unconfirmed'
return True
valid = all(payload == b"\x68\x01" for payload in replies.values())
self.disable_response = 'positive_communication_control' if valid else 'negative_or_malformed_reply'
return valid
payload = replies.get((0x730, None))
if request in (b"\x10\x03", b"\x10\x01"):
valid = payload is not None and payload.startswith(response) and len(payload) in (2, 6)
else:
valid = payload == response
if valid:
if request == b"\x10\x03":
time.sleep(.05)
return True
if request == b"\x10\x03":
time.sleep(.1)
return False
@@ -74,10 +74,10 @@ class CarInterface(CarInterfaceBase):
if gv70_required(cp):
return GV70Startup(cp, callbacks) if requested else None
from opendbc.car.hyundai.ev9_longitudinal import candidate as ev9_candidate, qualified as ev9_qualified
from opendbc.car.hyundai.ev9_startup import EV9Startup
from opendbc.car.hyundai.ev9_pending_startup import EV9PendingStartup
ev9_cp = ev9_candidate(cp, enabled=requested, is_release=False)
if ev9_qualified(ev9_cp):
return EV9Startup(ev9_cp, callbacks, stock_cp=cp) if requested else None
return EV9PendingStartup(ev9_cp, callbacks, stock_cp=cp) if requested else None
from opendbc.car.hyundai.torque_ev_startup import candidate as torque_ev_candidate, TorqueEVStartup
torque_cp = torque_ev_candidate(cp, requested=requested, is_release=False)
if torque_cp is not None:
@@ -40,10 +40,14 @@ class Lease:
class PEPendingStartup:
@staticmethod
def profile_qualified(cp):
return qualified(cp)
def __init__(self, cp, callbacks, *, stock_cp, clock=time.monotonic,
boot_clock=clock_boot_ns,
exchange=query_ioniq6_adas, event_factory=threading.Event, thread_factory=threading.Thread):
if not qualified(cp) or not stock_qualified(stock_cp):
if not self.profile_qualified(cp) or not stock_qualified(stock_cp) or stock_cp.carFingerprint != cp.carFingerprint:
raise ValueError('PE startup requires exact separately prepared PE LONG and stock CP')
self.owner = Lease(copy_cp(cp))
self.stock_cp = copy_cp(stock_cp)
@@ -1,5 +1,6 @@
"""EV9 optional LONG actual parser/controller and reached-cadence boundaries."""
import unittest
from types import SimpleNamespace
from opendbc.can import CANPacker
from opendbc.car import Bus, structs
from opendbc.car.hyundai.ev9_longitudinal import candidate, qualified, EV9LongitudinalPolicy, LongCtrlState
@@ -41,6 +42,9 @@ class TestEV9LongController(unittest.TestCase):
cp = candidate(params(candidate=CAR.KIA_EV9), enabled=True, is_release=False)
self.assertTrue(qualified(cp))
ci = CarInterface(cp)
# This fixture isolates controller calibration after a controlled owner
# acknowledgment. Pending-owner/first-native-output tests use the real owner.
ci.CC.pe_startup = SimpleNamespace(handed_off=True, can_emit=lambda controller: True)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
ci.update([])
for tick in range(1, 13):
@@ -0,0 +1,64 @@
"""EV9 source UDS distinctions through the real ISO-TP decoder."""
from unittest.mock import patch
import pytest
from opendbc.car.can_definitions import CanData
from opendbc.car.hyundai.ev9_longitudinal import candidate
from opendbc.car.hyundai.ev9_pending_startup import EV9PendingStartup
from opendbc.car.hyundai.tests.test_ioniq5pe_stock import params
from opendbc.car.hyundai.values import CAR
@pytest.mark.parametrize('reply,accepted,outcome', [
(b'\x68\x01', True, 'positive_communication_control'),
(None, True, 'no_reply_sent_unconfirmed'),
(b'\x7f\x28\x22', False, 'negative_or_malformed_reply'),
(b'\x7f\x28\x78', False, 'negative_or_malformed_reply'),
(b'\x68\x03', False, 'negative_or_malformed_reply'),
(b'\x68\x01\x00', False, 'negative_or_malformed_reply'),
])
def test_real_decoder_does_not_turn_silence_or_nrc_into_positive_ack(reply, accepted, outcome):
stock = params(candidate=CAR.KIA_EV9)
cp = candidate(stock, enabled=True, is_release=False)
sent, pending = [], []
def receive(**kwargs):
frames = pending[:]
pending.clear()
return [frames] if frames else []
def send(frames):
sent.extend(frames)
if reply is not None:
pending.append(CanData(0x738, bytes((len(reply),)) + reply + bytes(7 - len(reply)), 1))
owner = EV9PendingStartup(cp, (receive, send), stock_cp=stock)
assert owner._ev9_exchange(receive, send, b'\x28\x01\x01', b'\x68\x01') is accepted
assert owner.disable_response == outcome
assert sent == [CanData(0x730, b'\x03\x28\x01\x01\x00\x00\x00\x00', 1)]
assert not owner.owner.active()
assert not owner.handed_off
def test_session_success_retains_source_50ms_settle():
stock = params(candidate=CAR.KIA_EV9)
owner = EV9PendingStartup(candidate(stock, enabled=True, is_release=False), (lambda **kw: [], lambda frames: None), stock_cp=stock)
with patch('opendbc.car.hyundai.ev9_pending_startup.IsoTpParallelQuery') as query, \
patch('opendbc.car.hyundai.ev9_pending_startup.time.sleep') as sleep:
query.return_value.get_data.return_value = {(0x730, None): b'\x50\x03\x00\x32\x01\xf4'}
assert owner._ev9_exchange(lambda **kw: [], lambda frames: None, b'\x10\x03', b'\x50\x03')
query.return_value.get_data.assert_called_once_with(.1, total_timeout=.1)
sleep.assert_called_once_with(.05)
def test_missing_session_retains_ten_bounded_source_attempts_without_disable():
stock = params(candidate=CAR.KIA_EV9)
owner = EV9PendingStartup(candidate(stock, enabled=True, is_release=False), (lambda **kw: [], lambda frames: None), stock_cp=stock)
with patch('opendbc.car.hyundai.ev9_pending_startup.IsoTpParallelQuery') as query, \
patch('opendbc.car.hyundai.ev9_pending_startup.time.sleep') as sleep:
query.return_value.get_data.return_value = {}
assert not owner._ev9_exchange(lambda **kw: [], lambda frames: None, b'\x10\x03', b'\x50\x03')
assert query.call_count == sleep.call_count == 10
assert all(call.args[4] == [b'\x10\x03'] for call in query.call_args_list)
assert owner.disable_response is None
@@ -0,0 +1,595 @@
import unittest
from types import SimpleNamespace
from typing import cast
from unittest.mock import patch
from opendbc.can import CANPacker
from opendbc.car import Bus, CanData, structs
from opendbc.car.hyundai.interface import CarInterface
from opendbc.car.hyundai.ev9_longitudinal import candidate, qualified
from opendbc.car.hyundai.ev9_pending_startup import EV9PendingStartup as PEPendingStartup
from opendbc.car.hyundai.ioniq5pe_startup import Phase
from opendbc.car.hyundai.ioniq5pe_ready import PEReadySource
from opendbc.car.hyundai.hyundaicanfd import hkg_can_fd_checksum
from opendbc.car.hyundai.tests.test_ioniq5pe_stock import params as stock_params
from opendbc.car.hyundai.tests.test_ev9_long_controller import packets
from opendbc.car.hyundai.values import CAR, DBC
from opendbc.car.hyundai.ioniq6_handoff import TimestampedCanPacket
from openpilot.selfdrive.car.card import Car
def params():
return stock_params(candidate=CAR.KIA_EV9)
class ThreadFixture:
def __init__(self, **kwargs):
self.kwargs = kwargs
self.started = self.joined = False
def start(self):
self.started = True
def join(self):
self.joined = True
class EV9StartupFixture(unittest.TestCase):
def setUp(self):
self.boot = 1_000_000_000
self.seconds = 1.
self.sent = []
self.queries = []
self.admitted = True
self.disable_ok = True
self.restore_ok = True
self.tick = 0
boot = patch('opendbc.car.hyundai.ioniq5pe_ready.clock_boot_ns', side_effect=lambda: self.boot)
boot.start()
self.addCleanup(boot.stop)
self.stock = params()
self.stock_before = self.stock.to_dict()
self.cp = candidate(self.stock, enabled=True, is_release=False)
self.assertTrue(qualified(self.cp))
def exchange(self, recv, send, request, response):
self.queries.append((request, response))
if request == b'\x28\x01\x01':
return self.disable_ok
if request == b'\x28\x00\x01':
return self.restore_ok
return (request, response) in ((b'\x10\x03', b'\x50\x03'), (b'\x10\x01', b'\x50\x01'))
def instance(self, ready=False, *, seal=True):
packer = CANPacker(DBC[CAR.KIA_EV9][Bus.pt])
def recv(**kwargs):
self.tick += 1
self.boot += 10_000_000
self.seconds += .01
return [TimestampedCanPacket(packets(packer, self.tick, advance_counter=True), self.boot)]
owner = PEPendingStartup(self.cp, (recv, self.sent.extend), stock_cp=self.stock,
clock=lambda: self.seconds, boot_clock=lambda: self.boot,
exchange=self.exchange, thread_factory=ThreadFixture)
with patch.object(owner, '_ready_probe', return_value=ready):
cp = owner.prepare(admission=lambda: self.admitted)
ci = CarInterface(cp)
owner.configure(ci)
owner.finalize_aol_configuration(ci)
if seal:
owner.seal_publication()
return owner, ci, packer
def warm(self, owner, ci, packer):
for _ in range(12):
self.tick += 1
self.boot += 10_000_000
ci.update([(self.boot, packets(packer, self.tick, advance_counter=True))])
self.assertTrue(ci.CS.out.canValid)
self.assertTrue(owner.sources_current(ci, self.boot))
def acknowledge(self, owner, ci):
self.admitted = False
config = ci.CP.safetyConfigs[0]
panda = SimpleNamespace(safetyModel=config.safetyModel, safetyParam=config.safetyParam,
alternativeExperience=0, safetyRxChecksInvalid=False, controlsAllowed=False,
faults=[], ignitionLine=True, ignitionCan=False)
owner.after_state(ci=ci, state=ci.CS.out, now_ns=self.boot, control_enabled=False,
control_current=True, pandas=[panda], panda_log_ns=self.boot,
panda_recv_ns=self.boot, panda_current=True)
def commit(self, owner, ci):
owner.commit_pending(ci, sources_current=owner.sources_current(ci, self.boot), control_current=True)
class TestEV9PendingStartup(EV9StartupFixture, unittest.TestCase):
def test_prepare_preserves_factory_output_and_never_sends_disable_or_keeper(self):
owner, ci, _ = self.instance()
self.assertTrue(owner.pending_target)
self.assertFalse(owner.owner.active())
self.assertEqual(self.queries, [])
self.assertEqual(self.sent, [])
self.assertIsNone(owner.thread)
self.assertEqual(self.stock.to_dict(), self.stock_before)
cc = structs.CarControl(enabled=True, latActive=True, longActive=True)
cc.actuators.accel = 1.
_, frames = ci.apply(cc.as_reader(), self.boot)
self.assertEqual(frames, [])
def test_ready_or_missing_ready_evidence_returns_exact_stock_without_ecu_mutation(self):
for ready in (True, None):
owner, ci, _ = self.instance(ready)
self.assertEqual(ci.CP.to_dict(), self.stock_before)
self.assertEqual(owner.owner.phase, Phase.STOCK)
self.assertFalse(owner.pending_target)
self.assertEqual(self.queries, [])
self.assertEqual(self.sent, [])
owner.close()
def test_pending_commit_requires_inactive_host_native_diagnostic_and_current_sources(self):
owner, ci, packer = self.instance()
self.warm(owner, ci, packer)
for source, control in ((False, True), (True, False)):
with self.assertRaises(RuntimeError):
owner.commit_pending(ci, sources_current=source, control_current=control)
self.admitted = False
with self.assertRaises(RuntimeError):
self.commit(owner, ci)
self.assertEqual(self.queries, [])
self.assertFalse(owner.takeover_attempted)
def test_late_ready_before_commit_aborts_published_target_without_disable(self):
owner, ci, packer = self.instance()
self.warm(owner, ci, packer)
self.boot += 10_000_000
address, payload, bus = packer.make_can_msg('ACCELERATOR', 1, {'COUNTER': 13, 'GEAR': 5})
raw = bytearray(payload)
raw[3] |= 0x40
raw[:2] = hkg_can_fd_checksum(address, None, raw).to_bytes(2, 'little')
ci.update([(self.boot, [(address, bytes(raw), bus)])])
with self.assertRaises(RuntimeError):
self.commit(owner, ci)
self.assertEqual(self.queries, [])
self.assertFalse(owner.takeover_attempted)
self.assertEqual(ci.CP.safetyConfigs[0].safetyParam, 0x5c95)
def test_negative_or_silent_disable_restores_and_aborts_without_relabeling_cp(self):
owner, ci, packer = self.instance()
self.warm(owner, ci, packer)
self.disable_ok = False
before = ci.CP.to_dict()
with self.assertRaises(RuntimeError):
self.commit(owner, ci)
self.assertTrue(owner.takeover_attempted)
self.assertTrue(owner.restored)
self.assertIn((b'\x28\x00\x01', b'\x68\x00'), self.queries)
self.assertEqual(ci.CP.to_dict(), before)
self.assertFalse(owner.can_emit(ci.CC))
def test_positive_ack_with_continued_physical_scc_aborts_and_restores(self):
owner, ci, packer = self.instance()
self.warm(owner, ci, packer)
recv = owner.callbacks[0]
def still_sending(**kwargs):
result = recv(**kwargs)
result[0].append(packer.make_can_msg('SCC_CONTROL', 1, {'ACCMode': 0}))
return result
owner.callbacks = (still_sending, owner.callbacks[1])
with self.assertRaises(RuntimeError):
self.commit(owner, ci)
self.assertTrue(owner.restored)
self.assertFalse(owner.can_emit(ci.CC))
self.assertEqual(owner.quiet_failure['address'], '0x1a0')
self.assertEqual(ci.CP.safetyConfigs[0].safetyParam, 0x5c95)
def test_silence_without_postack_independent_can_is_not_ownership_proof(self):
owner, ci, packer = self.instance()
self.warm(owner, ci, packer)
def empty(**kwargs):
self.seconds += .01
self.boot += 10_000_000
return []
owner.callbacks = (empty, owner.callbacks[1])
with self.assertRaises(RuntimeError):
self.commit(owner, ci)
self.assertTrue(owner.restored)
self.assertFalse(owner.owner.active())
def test_uncertain_restoration_stays_abort_and_cannot_claim_stock(self):
owner, ci, packer = self.instance()
self.warm(owner, ci, packer)
self.disable_ok = self.restore_ok = False
with self.assertRaises(RuntimeError):
self.commit(owner, ci)
self.assertFalse(owner.restored)
self.assertEqual(owner.owner.phase, Phase.ABORT)
with self.assertRaises(RuntimeError):
owner.close()
self.assertEqual(ci.CP.safetyConfigs[0].safetyParam, 0x5c95)
def test_postack_sources_and_native_receipt_gate_actual_controller_then_neutral_send_handoff(self):
owner, ci, packer = self.instance()
self.warm(owner, ci, packer)
self.commit(owner, ci)
self.assertFalse(owner.sources_current(ci, self.boot))
cc = structs.CarControl.new_message()
_, frames = ci.apply(cc.as_reader(), self.boot)
self.assertEqual(frames, [])
self.warm(owner, ci, packer)
self.assertFalse(owner.can_emit(ci.CC))
self.acknowledge(owner, ci)
self.assertTrue(owner.can_emit(ci.CC))
self.assertTrue(owner.before_control(configured=True, sources_current=True, control_current=True))
self.assertFalse(owner.before_control(configured=False, sources_current=True, control_current=True))
self.assertFalse(owner.can_emit(ci.CC))
self.acknowledge(owner, ci)
owner.sent(list(self.sent), valid=True)
self.assertFalse(owner.handed_off)
_, frames = ci.apply(cc.as_reader(), self.boot)
self.assertTrue({0x1A0, 0xCB, 0x100, 0x730}.issubset({frame[0] for frame in frames}))
owner.sent(frames, valid=False)
self.assertFalse(owner.handed_off)
owner.sent(frames, valid=True)
self.assertTrue(owner.handed_off)
self.assertTrue(owner.thread.joined)
self.admitted = True
owner.close()
self.assertTrue(owner.restored)
def test_ev9_retains_its_current_mdps_estimate_and_fault_mask2(self):
owner, ci, packer = self.instance()
self.warm(owner, ci, packer)
for tick, fault in enumerate((0, 1, 2, 4, 7), 13):
self.boot += 10_000_000
frame = packer.make_can_msg('MDPS', 1, {'MDPS_EstStrAnglVal': 12.3, 'MDPS_PaStrAnglVal': -45.6,
'MDPS_ADAS_AciFltSig_Lv2': fault, 'COUNTER': tick})
ci.update([(self.boot, [frame])])
self.assertAlmostEqual(ci.CS.angle_steering_angle, 12.3)
self.assertEqual(ci.CS.angle_steering_fault, bool(fault & 2))
def test_first_replacement_deadline_is_software_guard_not_infinite_tester_lease(self):
owner, ci, packer = self.instance()
self.warm(owner, ci, packer)
self.commit(owner, ci)
self.seconds += 1.
owner._tester()
with self.assertRaises(RuntimeError):
owner.check()
self.assertFalse(owner.can_emit(ci.CC))
def test_uds_time_is_included_in_first_replacement_software_bound(self):
owner, ci, packer = self.instance()
self.warm(owner, ci, packer)
exchange = owner.exchange
def delayed_ack(recv, send, request, response):
result = exchange(recv, send, request, response)
if request == b'\x28\x01\x01':
self.seconds += 1.
return result
owner.exchange = delayed_ack
with self.assertRaises(RuntimeError):
self.commit(owner, ci)
self.assertTrue(owner.restored)
self.assertEqual(self.sent, [])
self.assertFalse(owner.can_emit(ci.CC))
def test_invalid_emitted_replacement_cannot_transfer_keeper_as_if_native_accepted(self):
owner, ci, packer = self.instance()
self.warm(owner, ci, packer)
self.commit(owner, ci)
self.warm(owner, ci, packer)
self.acknowledge(owner, ci)
_, frames = ci.apply(structs.CarControl.new_message().as_reader(), self.boot)
bad = []
for address, data, bus in frames:
if address == 0x1A0:
raw = bytearray(data)
raw[0] ^= 1
data = bytes(raw)
bad.append((address, data, bus))
owner.sent(bad, valid=True)
self.assertFalse(owner.handed_off)
self.assertFalse(owner.can_emit(ci.CC))
with self.assertRaises(RuntimeError):
owner.check()
def test_actual_card_teardown_quiesces_and_returns_to_diagnostic_mode_before_restore(self):
owner, ci, packer = self.instance()
self.warm(owner, ci, packer)
self.commit(owner, ci)
self.admitted = False
writes = []
def put_bool(key, value):
writes.append((key, value))
self.assertTrue(owner.closed)
self.admitted = key == 'ControlsReady' and value is False
caller = SimpleNamespace(vehicle_startup=SimpleNamespace(owner=owner),
params=SimpleNamespace(put_bool=put_bool), startup_diagnostic_admission=lambda: self.admitted)
self.assertTrue(Car.prepare_pe_restoration(cast(Car, caller)))
owner.close()
self.assertTrue(owner.restored)
self.assertEqual(writes, [('ControlsReady', False)])
self.assertEqual(self.queries[-1], (b'\x10\x01', b'\x50\x01'))
self.assertEqual(self.stock.to_dict(), self.stock_before)
def test_diagnostic_reentry_timeout_never_sends_restore_under_tester_only_native(self):
owner, ci, packer = self.instance()
self.warm(owner, ci, packer)
self.commit(owner, ci)
self.admitted = False
count = len(self.queries)
timer = [0.]
def now():
timer[0] += .1
return timer[0]
writes = []
caller = SimpleNamespace(vehicle_startup=SimpleNamespace(owner=owner),
params=SimpleNamespace(put_bool=lambda key, value: writes.append((key, value))),
startup_diagnostic_admission=lambda: False)
with patch('openpilot.selfdrive.car.card.time.monotonic', side_effect=now), patch('openpilot.selfdrive.car.card.time.sleep'):
self.assertFalse(Car.prepare_pe_restoration(cast(Car, caller)))
with self.assertRaises(RuntimeError):
owner.close()
self.assertEqual(len(self.queries), count)
self.assertFalse(owner.restored)
self.assertEqual(writes, [('ControlsReady', False)])
def test_stock_fallback_preserves_existing_angle_aol_marker_only(self):
owner, ci, _ = self.instance(True, seal=False)
ci.CP.alternativeExperience = 32
owner.finalize_aol_configuration(ci)
owner.seal_publication()
self.assertTrue(owner.prepared_for(ci.CP))
self.assertEqual(ci.CP.safetyConfigs[0].safetyParam, 0x5c91)
self.assertEqual(self.stock.to_dict(), self.stock_before)
def test_restore_admission_exception_is_uncertain_and_never_relabels_stock(self):
owner, ci, packer = self.instance()
self.warm(owner, ci, packer)
self.commit(owner, ci)
before = ci.CP.to_dict()
count = len(self.queries)
def failed_transport():
raise OSError('Explicit diagnostic-admission fixture failure')
owner.diagnostic_admission = failed_transport
with self.assertRaises(RuntimeError):
owner.close()
self.assertTrue(owner.closed)
self.assertFalse(owner.restored)
self.assertEqual(owner.owner.phase, Phase.ABORT)
self.assertEqual(len(self.queries), count)
self.assertEqual(ci.CP.to_dict(), before)
class TestPEReadyPhysicalSource(unittest.TestCase):
def test_missing_boottime_does_not_create_ready_false_from_defaults(self):
source = PEReadySource()
source.observation = (1_000_000_000, False)
with patch('opendbc.car.hyundai.ioniq5pe_ready.clock_boot_ns', return_value=0):
source.observe([])
self.assertIsNone(source.current(1_000_000_000, 0, 100_000_000))
def test_crc_counter_bus_and_freshness_require_actual_physical_sample(self):
packer = CANPacker('hyundai_canfd_generated')
source = PEReadySource()
for stamp, count in ((1_010_000_000, 0), (1_020_000_000, 1)):
message = packer.make_can_msg('ACCELERATOR', 1, {'COUNTER': count, 'GEAR': 5})
source.update([(stamp, [message])], stamp)
self.assertIs(source.current(1_020_000_000, 1_000_000_000, 100_000_000), False)
self.assertIs(source.current(1_120_000_000, 1_000_000_000, 100_000_000), False)
self.assertIsNone(source.current(1_120_000_001, 1_000_000_000, 100_000_000))
address, data, _ = packer.make_can_msg('ACCELERATOR', 1, {'COUNTER': 2})
wrong_bus = (address, data, 0)
source.update([(1_030_000_000, [wrong_bus])], 1_030_000_000)
assert source.observation is not None
self.assertEqual(source.observation[0], 1_020_000_000)
corrupted = bytearray(data)
corrupted[0] ^= 1
source.update([(1_030_000_000, [(address, bytes(corrupted), 1)])], 1_030_000_000)
self.assertIsNone(source.current(1_030_000_000, 1_000_000_000, 100_000_000))
class TestEV9SessionAndReadyProbe(EV9StartupFixture, unittest.TestCase):
def test_uncertain_session_alone_restores_default_without_communication_control(self):
for outcome in (False, None, OSError('session send uncertain')):
with self.subTest(outcome=outcome):
owner, ci, packer = self.instance()
self.warm(owner, ci, packer)
before = ci.CP.to_dict()
self.queries.clear()
def uncertain_session(recv, send, request, response, outcome=outcome):
self.queries.append((request, response))
if request == b'\x10\x03':
if isinstance(outcome, Exception):
raise outcome
return outcome
return request == b'\x10\x01' and response == b'\x50\x01'
owner.exchange = uncertain_session
with self.assertRaises((RuntimeError, OSError)):
self.commit(owner, ci)
self.assertTrue(owner.session_attempted)
self.assertFalse(owner.takeover_attempted)
self.assertTrue(owner.restored)
self.assertEqual(self.queries, [(b'\x10\x03', b'\x50\x03'), (b'\x10\x01', b'\x50\x01')])
self.assertEqual(ci.CP.to_dict(), before)
self.assertFalse(owner.can_emit(ci.CC))
def test_session_only_uncertain_default_session_stays_unverified_and_reenters_diagnostics(self):
owner, ci, packer = self.instance()
self.warm(owner, ci, packer)
self.queries.clear()
owner.exchange = lambda recv, send, request, response: self.queries.append((request, response))
with self.assertRaises(RuntimeError):
self.commit(owner, ci)
self.assertTrue(owner.session_attempted)
self.assertFalse(owner.takeover_attempted)
self.assertFalse(owner.restored)
self.admitted = False
writes = []
def put_bool(key, value):
writes.append((key, value))
self.admitted = key == 'ControlsReady' and value is False
caller = SimpleNamespace(vehicle_startup=SimpleNamespace(owner=owner),
params=SimpleNamespace(put_bool=put_bool), startup_diagnostic_admission=lambda: self.admitted)
self.assertTrue(Car.prepare_pe_restoration(cast(Car, caller)))
with self.assertRaises(RuntimeError):
owner.close()
with self.assertRaises(RuntimeError):
owner.close()
self.assertFalse(owner.restored)
self.assertEqual(writes, [('ControlsReady', False)])
self.assertTrue(all(request in (b'\x10\x03', b'\x10\x01') for request, _ in self.queries))
def test_live_prepare_probe_revokes_cached_false_after_later_invalid_source(self):
for invalid in (None, 'crc', 'timestamp'):
with self.subTest(invalid=invalid):
owner, _, packer = self.instance()
ticks = 0
def recv(*, packer=packer, invalid=invalid, **kwargs):
nonlocal ticks
ticks += 1
self.boot += 10_000_000
self.seconds += .01
frame = next(f for f in packets(packer, ticks, advance_counter=True) if f[0] == 0x35)
if invalid == 'crc' and ticks >= 45:
data = bytearray(frame[1])
data[-1] ^= 1
frame = CanData(frame[0], bytes(data), frame[2])
stamp = 0 if invalid == 'timestamp' and ticks >= 45 else self.boot
return [TimestampedCanPacket([frame], stamp)]
owner.callbacks = (recv, self.sent.extend)
with patch('opendbc.car.hyundai.ioniq5pe_startup.time.sleep'):
result = owner._ready_probe()
self.assertIs(result, False if invalid is None else None)
self.assertEqual(self.queries, [])
class TestEV9NormalSelection(unittest.TestCase):
def test_saved_choice_and_captured_ev9_factory_reach_sealed_owner_without_setting_writes(self):
from opendbc.car import gen_empty_fingerprint
from opendbc.car.car_helpers import get_car
from opendbc.car.vin import VIN_UNKNOWN
import json
from pathlib import Path
FIXTURE = json.loads((Path(__file__).parent / 'testdata' / 'ev9_startup_capture.json').read_text())
from openpilot.selfdrive.car.card import alpha_long_requested
from openpilot.starpilot.vehicle_preferences import VehicleStartupPreferences
from openpilot.starpilot.vehicle_startup import VehicleStartupOwner
from tempfile import TemporaryDirectory
from pathlib import Path
class Saved:
def __init__(self, alpha):
self.values = {'AlwaysOnLateral': b'1', 'LongPitch': b'0'}
if alpha is not None:
self.values['AlphaLongitudinalEnabled'] = alpha
self.directory = TemporaryDirectory()
self.root = Path(self.directory.name)
for key, value in self.values.items():
(self.root / key).write_bytes(value)
def get_param_path(self, key):
return str(self.root / key)
def get(self, key, **kwargs):
path = self.root / key
raw = path.read_bytes() if path.exists() else None
return raw.decode(kwargs["encoding"]) if raw is not None and kwargs.get("encoding") else raw
def get_bool(self, key):
return self.get(key) == b'1'
for alpha, release in ((None, False), (b'0', False), (b'1', False), (b'1', True)):
with self.subTest(alpha=alpha, release=release):
saved = Saved(alpha)
self.addCleanup(saved.directory.cleanup)
before_files = {p.name: p.read_bytes() for p in saved.root.iterdir()}
before = dict(saved.values)
request = alpha_long_requested(saved, is_release=release)
startup = VehicleStartupOwner()
preferences = VehicleStartupPreferences.read(saved, enabled=True)
finger = gen_empty_fingerprint()
for bus, entries in FIXTURE['fingerprint'].items():
finger[int(bus)].update({int(addr): length for addr, length in entries.items()})
fw = []
def receive(wait_for_one: bool = False) -> list[list[CanData]]:
return []
def before_create(cp, candidate, fingerprints, car_fw, *, preferences=preferences, startup=startup, request=request):
cp = preferences.prepare(cp, fingerprints=fingerprints)
return startup.prepare(cp, CarInterface, (receive, lambda frames: None),
requested=request, admission=lambda: True)
detected = (CAR.KIA_EV9, finger, VIN_UNKNOWN, fw, structs.CarParams.FingerprintSource.fw, True)
with patch('opendbc.car.car_helpers.fingerprint', return_value=detected), \
patch.object(PEPendingStartup, '_ready_probe', return_value=False):
ci = get_car(receive, lambda frames: None, lambda enabled: None,
request, release, pre_create_hook=before_create)
startup.configure(ci)
startup.finalize_aol_configuration(ci)
startup.seal_publication()
self.assertEqual(ci.CP.openpilotLongitudinalControl, request)
self.assertEqual(ci.CP.alphaLongitudinalAvailable, not release)
self.assertEqual(ci.CP.safetyConfigs[0].safetyParam, 0x5c95 if request else 0x5c91)
self.assertEqual(saved.values, before)
self.assertEqual({p.name: p.read_bytes() for p in saved.root.iterdir()}, before_files)
if request:
self.assertIsInstance(startup.owner, PEPendingStartup)
self.assertEqual(startup.owner.owner.phase, Phase.PENDING)
self.assertTrue(startup.owner.prepared_for(ci.CP))
self.assertTrue(startup.owner.owner.published)
else:
self.assertIsNone(startup.owner)
class TestEV9PendingIdentity(unittest.TestCase):
def test_pending_owner_rejects_a_different_vehicle_stock_fallback(self):
long = candidate(params(), enabled=True, is_release=False)
with self.assertRaises(ValueError):
PEPendingStartup(long, (lambda **kw: [], lambda frames: None), stock_cp=stock_params())
def test_pe_owner_also_rejects_an_ev9_stock_fallback(self):
from opendbc.car.hyundai.ioniq5pe_startup import PEPendingStartup as OriginalPEPendingStartup
from opendbc.car.hyundai.ioniq5pe_long_profile import candidate as pe_candidate
stock = stock_params()
long = pe_candidate(stock, enabled=True, is_release=False)
with self.assertRaises(ValueError):
OriginalPEPendingStartup(long, (lambda **kw: [], lambda frames: None), stock_cp=params())
class TestEV9UnpreparedController(unittest.TestCase):
def test_current_inputs_do_not_enable_replacements_before_qualified_owner(self):
stock = params()
cp = candidate(stock, enabled=True, is_release=False)
ci = CarInterface(cp)
ci.update([])
packer = CANPacker(DBC[CAR.KIA_EV9][Bus.pt])
for tick in range(1, 13):
ci.update([(tick * 10_000_000, packets(packer, tick, advance_counter=True))])
self.assertTrue(ci.CS.out.canValid)
control = structs.CarControl(enabled=True, latActive=True, longActive=True)
control.actuators.accel = 1.
_, frames = ci.apply(control.as_reader(), 130_000_000)
self.assertEqual(frames, [])
@@ -11,6 +11,7 @@ from opendbc.car.can_definitions import CanData
from opendbc.car.hyundai.ecu_startup import Outcome
from opendbc.car.hyundai.ev9_longitudinal import candidate, qualified
from opendbc.car.hyundai.ev9_startup import EV9Startup
from opendbc.car.hyundai.ev9_pending_startup import EV9PendingStartup
from opendbc.car.hyundai.interface import CarInterface
from opendbc.car.hyundai.ioniq6_handoff import TimestampedCanPacket
from opendbc.car.hyundai.tests.test_ioniq5pe_stock import params
@@ -209,8 +210,8 @@ class TestEV9Startup(unittest.TestCase):
def test_actual_factory_promotes_only_exact_stock_cp(self):
stock = params(candidate=CAR.KIA_EV9)
actual = CarInterface.startup_owner(stock, (lambda **kwargs: [], lambda frames: None), requested=True)
self.assertIsInstance(actual, EV9Startup)
self.assertTrue(qualified(actual.cp))
self.assertIsInstance(actual, EV9PendingStartup)
self.assertTrue(qualified(actual.owner.cp))
self.assertEqual(actual.stock_cp.to_dict(), stock.to_dict())
self.assertFalse(candidate(stock, enabled=False, is_release=False).openpilotLongitudinalControl)
self.assertFalse(candidate(stock, enabled=True, is_release=True).openpilotLongitudinalControl)
@@ -258,11 +259,18 @@ class TestEV9Startup(unittest.TestCase):
self.assertEqual(owner._disable(owner.callbacks[0], owner.callbacks[1]), expected)
@patch.object(time, "CLOCK_BOOTTIME", getattr(time, "CLOCK_BOOTTIME", time.CLOCK_MONOTONIC), create=True)
def test_actual_get_car_finalizes_long_before_interface_construction(self):
def test_actual_get_car_seals_pending_long_without_early_disable(self):
from opendbc.car import car_helpers, gen_empty_fingerprint
from opendbc.car.hyundai.ioniq5pe_startup import Phase
from openpilot.starpilot.vehicle_startup import VehicleStartupOwner
stock, owner, sent, requests, query = self.owner()
stock = params(candidate=CAR.KIA_EV9)
sent = []
def receive(wait_for_one: bool = False) -> list[list[CanData]]:
return []
callbacks = (receive, sent.extend)
fingerprint = gen_empty_fingerprint()
fingerprint[1].update({0x35: 32, 0x175: 24, 0xA0: 24, 0xEA: 24, 0x1CF: 8, 0x1A0: 32, 0x130: 16})
fingerprint[2].update({0x110: 32, 0x362: 32})
@@ -270,28 +278,21 @@ class TestEV9Startup(unittest.TestCase):
holder = VehicleStartupOwner()
def hook(cp, candidate_id, fingerprints, firmware):
return holder.prepare(cp, CarInterface, owner.callbacks, requested=True, admission=lambda: True)
return holder.prepare(cp, CarInterface, callbacks, requested=True, admission=lambda: True)
with (
patch.object(car_helpers, "fingerprint", return_value=found),
patch.object(EV9Startup, "_ready", return_value=False),
patch.object(EV9Startup, "_query", side_effect=query),
):
# Inject a non-thread keeper at the actual factory boundary.
original_factory = CarInterface.startup_owner
def factory(cp, callbacks, *, requested):
selected = original_factory(cp, callbacks, requested=requested)
selected.keeper_factory = owner.keeper_factory
return selected
with patch.object(CarInterface, "startup_owner", side_effect=factory):
ci = car_helpers.get_car(owner.callbacks[0], owner.callbacks[1], lambda *args: None, True, False, pre_create_hook=hook)
with patch.object(car_helpers, "fingerprint", return_value=found), \
patch.object(EV9PendingStartup, "_ready_probe", return_value=False):
ci = car_helpers.get_car(*callbacks, lambda *args: None, True, False, pre_create_hook=hook)
holder.configure(ci)
holder.seal_publication()
self.assertTrue(qualified(ci.CP))
self.assertEqual(ci.CP.safetyConfigs[0].safetyParam, 0x5C95)
self.assertIs(holder.owner.outcome, Outcome.SENT_UNCONFIRMED)
self.assertIsInstance(holder.owner, EV9PendingStartup)
self.assertIs(holder.owner.owner.phase, Phase.PENDING)
self.assertTrue(holder.owner.owner.published)
self.assertTrue(holder.owner.pending_target)
self.assertEqual(sent, [])
self.assertIsNone(holder.owner.thread)
def test_malformed_long_cp_cannot_bypass_required_startup_owner(self):
from openpilot.starpilot.vehicle_startup import VehicleStartupOwner
@@ -0,0 +1 @@
{"sourceRoute":"64760999c36a2105/0000000b--0af90ff57b","fingerprint":{"0":{"272":32,"357":16,"571":32,"572":32,"573":32,"574":32,"575":32,"576":32,"577":32,"865":32,"256":24,"578":32,"579":32,"580":32,"581":32,"582":32,"583":32,"584":32,"866":32,"560":16,"565":32,"566":32,"567":32,"568":32,"569":32,"570":32,"867":32,"869":32,"1280":16,"976":32,"917":32,"933":24,"934":24,"935":24,"936":24,"937":24,"938":24,"939":24,"940":24,"941":24,"942":24,"943":24,"944":24,"945":24,"946":24,"947":24,"948":24,"949":24,"950":24,"868":32,"870":32,"951":24,"977":32,"978":32,"979":32,"980":32,"952":24,"928":32,"953":24,"954":24,"955":24,"956":24,"957":24,"958":24,"959":24,"960":24,"961":24,"962":24,"963":24,"964":24,"864":32,"896":24},"1":{"234":24,"693":32,"736":32,"74":32,"704":32,"762":32,"805":32,"293":16,"816":32,"101":32,"266":32,"410":32,"855":32,"864":32,"874":16,"384":8,"389":8,"282":16,"426":16,"549":16,"666":8,"767":8,"789":16,"906":16,"1138":8,"304":16,"347":8,"1048":8,"203":24,"96":32,"298":16,"111":8,"352":16,"160":24,"416":32,"373":24,"53":32,"245":32,"325":32,"144":32,"432":32,"522":16,"525":32,"437":32,"698":32,"826":32,"853":32,"798":8,"1099":8,"463":8,"961":8,"648":8,"769":8,"496":16,"962":8,"1037":8,"353":32,"354":32,"442":24,"480":16,"485":16,"490":32,"512":8,"837":8,"908":32,"1280":32,"1058":8,"1264":8,"1402":32,"778":32,"800":32,"721":8,"895":8,"722":8,"817":8,"723":8,"818":8,"724":8,"819":8,"820":8,"1055":8,"1204":8,"1245":8,"1239":8,"1240":8,"1247":8,"1259":8,"928":16,"592":24,"517":24,"932":8,"901":8,"1392":32,"506":32,"703":8,"1151":8,"192":32,"597":32,"741":32,"761":8,"858":8,"879":8,"687":8,"682":32,"565":32,"336":32,"501":32,"602":32,"758":8,"763":8,"799":8,"688":32,"875":16,"892":8,"896":8,"897":8,"898":8,"899":8,"900":8,"944":8,"945":8,"946":8,"1063":8,"1065":8,"1066":8,"1057":8,"1062":8,"90":32,"785":16,"802":16,"210":8,"255":8,"914":8,"915":8,"963":8,"916":8,"977":8,"992":8,"997":8,"993":8,"994":8,"1032":8,"998":8,"1050":8,"1036":8,"1051":8,"1052":8,"1061":8,"1074":8,"1072":8,"1076":8,"1077":8,"1090":8,"1088":8,"1092":8,"1097":8,"415":8,"1093":8,"1108":8,"139":8,"1160":8,"1096":8,"1161":8,"1187":8,"949":32,"934":8,"1141":8,"1174":8,"1029":8,"1040":8,"1041":8,"1042":8,"1043":8,"1044":8,"1046":8,"1047":8,"692":32,"1274":32,"204":8,"849":8,"850":8,"851":8,"881":8,"852":8,"882":8,"883":8,"884":8,"1015":8,"1016":8,"1267":8,"1266":8,"1162":16,"1167":8,"794":32,"842":32,"1008":32,"1012":8,"1027":8,"970":16,"474":32,"1013":32},"2":{"272":32,"357":16,"571":32,"572":32,"573":32,"574":32,"575":32,"576":32,"577":32,"865":32,"256":24,"578":32,"579":32,"580":32,"581":32,"582":32,"583":32,"584":32,"866":32,"560":16,"565":32,"566":32,"567":32,"568":32,"569":32,"570":32,"867":32,"869":32,"1280":16,"976":32,"917":32,"933":24,"934":24,"935":24,"936":24,"937":24,"938":24,"939":24,"940":24,"941":24,"942":24,"943":24,"944":24,"945":24,"946":24,"947":24,"948":24,"949":24,"950":24,"868":32,"870":32,"951":24,"977":32,"978":32,"979":32,"980":32,"952":24,"928":32,"953":24,"954":24,"955":24,"956":24,"957":24,"958":24,"959":24,"960":24,"961":24,"962":24,"963":24,"964":24,"864":32,"896":24},"3":{},"4":{},"5":{},"6":{},"7":{}}}
@@ -0,0 +1,101 @@
import unittest
from opendbc.car import structs
from opendbc.car.hyundai.tests.test_ev9_pending_startup import EV9StartupFixture as PEStartupFixture
from opendbc.car.hyundai.tests.test_ev9_long_controller import packets
from opendbc.safety.tests.test_hyundai_ev9_long import Ev9LongFixture as PEFixture
from opendbc.safety.tests.libsafety import libsafety_py
from openpilot.selfdrive.controls.lib.longcontrol import LongControl
from openpilot.starpilot.longitudinal.extension import LongitudinalContext
class TestEV9PendingControllerNativeDebug(PEStartupFixture, PEFixture, unittest.TestCase):
@staticmethod
def can_packet(frame):
return libsafety_py.make_CANPacket(frame[0], frame[2], frame[1])
def setUp(self):
PEStartupFixture.setUp(self)
PEFixture.setUp(self)
self.assertEqual(self.safety.set_safety_hooks(structs.CarParams.SafetyModel.elm327, 1), 0)
def physical_tick(self, ci, packer, *, button=0, adaptive_main=False, native_hyundai=True):
self.tick += 1
self.boot += 10_000_000
self.now += 10_000
self.safety.set_timer(self.now)
frames = packets(packer, self.tick, advance_counter=True, cruise_button=button, adaptive_main=adaptive_main)
if native_hyundai:
for frame in frames:
accepted = self.safety.safety_rx_hook(libsafety_py.make_CANPacket(frame[0], frame[2], frame[1]))
if frame[0] in (0x35, 0x175, 0xA0, 0xEA, 0x1CF):
self.assertTrue(accepted, hex(frame[0]))
ci.update([(self.boot, frames)])
def test_deferred_owner_to_real_controller_wire_native_then_physical_cancel(self):
owner, ci, packer = self.instance()
for _ in range(12):
self.physical_tick(ci, packer, native_hyundai=False)
self.assertTrue(ci.CS.out.canValid)
self.assertEqual(self.queries, [])
self.commit(owner, ci)
self.assertEqual(self.safety.set_safety_hooks(structs.CarParams.SafetyModel.hyundaiCanfd, 0x5c95), 0)
for _ in range(12):
self.physical_tick(ci, packer)
self.assertFalse(self.safety.get_controls_allowed())
self.acknowledge(owner, ci)
cc = structs.CarControl(enabled=True, latActive=True, longActive=True)
cc.actuators.accel = 1.
cc.actuators.steeringAngleDeg = 1.
cc.actuators.longControlState = structs.CarControl.Actuators.LongControlState.pid
_, first = ci.apply(cc.as_reader(), self.boot)
scc = next(frame[1] for frame in first if frame[0] == 0x1A0)
cb = next(frame[1] for frame in first if frame[0] == 0xCB)
self.assertEqual((scc[8] >> 4) & 7, 0)
self.assertEqual((cb[3] >> 4) & 15, 1)
for frame in first:
self.assertTrue(self.safety.safety_tx_hook(self.can_packet(frame)), hex(frame[0]))
owner.sent(first, valid=True)
self.assertTrue(owner.handed_off)
# EV9 retains its original tracked MAIN policy; physical MAIN precedes SET.
while (self.tick + 1) % 2:
self.physical_tick(ci, packer)
self.physical_tick(ci, packer, adaptive_main=True)
self.physical_tick(ci, packer, adaptive_main=True)
self.physical_tick(ci, packer)
self.assertFalse(self.safety.get_controls_allowed())
positive = None
loop = LongControl(ci.CP)
context = LongitudinalContext(has_lead=False, traffic_mode=False, custom_acceleration=False,
profile_max_accel=0., experimental_mode=False)
press_tick = self.tick + (2 if self.tick % 2 == 0 else 1)
for index in range(40):
next_tick = self.tick + 1
button = 2 if next_tick == press_tick else 0
self.physical_tick(ci, packer, button=button)
self.acknowledge(owner, ci)
active = bool(self.safety.get_controls_allowed())
cc.actuators.accel = float(loop.update(active, ci.CS.out, 1., False, (-3.5, 2.), context=context))
cc.actuators.longControlState = loop.long_control_state
command = cc.as_reader() if active else structs.CarControl.new_message().as_reader()
_, emitted = ci.apply(command, self.boot)
for frame in emitted:
self.assertTrue(self.safety.safety_tx_hook(self.can_packet(frame)), (index, hex(frame[0])))
if frame[0] == 0x1A0:
data = frame[1]
accel = ((((data[17] & 7) << 8) | data[16]) - 1023) / 100.
self.assertGreaterEqual(accel, -3.5)
self.assertLessEqual(accel, 2.)
if accel > 0:
positive = frame
self.assertIsNotNone(positive)
self.assertTrue(self.safety.get_controls_allowed())
while (self.tick + 1) % 2:
self.physical_tick(ci, packer)
self.physical_tick(ci, packer, button=4)
self.assertFalse(self.safety.get_controls_allowed())
self.assertFalse(self.safety.safety_tx_hook(self.can_packet(positive)))
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "ef1feef62efb0cad69cc0c347b1a8157902ef4baf4410b5913afff45973dae55", "schema_version": 1}
{"protected_inventory_sha256": "06bd107160eba869ae5b5cdcee08f84d7a7c019c7faea39c45f8a58800506032", "schema_version": 1}
+2
View File
@@ -33,6 +33,8 @@ DEBUG_SAFETY_CLASS_TARGETS = {
RELEASE_TARGETS = (
"opendbc_repo/opendbc/car/hyundai/tests/test_ev9_pending_protocol.py",
"opendbc_repo/opendbc/car/hyundai/tests/test_ev9_pending_startup.py::TestEV9NormalSelection",
"opendbc_repo/opendbc/car/hyundai/tests/test_batched_ready_observations.py",
"openpilot/starpilot/aol/tests/test_canfd_location_native_recovery.py",
"openpilot/starpilot/aol/tests/test_classic_location_stock_intent.py",