mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-29 01:23:58 +08:00
Defer EV9 ECU takeover until replacement controls are ready
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
@@ -1 +1 @@
|
||||
{"protected_inventory_sha256": "ef1feef62efb0cad69cc0c347b1a8157902ef4baf4410b5913afff45973dae55", "schema_version": 1}
|
||||
{"protected_inventory_sha256": "06bd107160eba869ae5b5cdcee08f84d7a7c019c7faea39c45f8a58800506032", "schema_version": 1}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user