diff --git a/opendbc_repo/opendbc/car/hyundai/g90_startup.py b/opendbc_repo/opendbc/car/hyundai/g90_startup.py index 97282468ed..41b85d416e 100644 --- a/opendbc_repo/opendbc/car/hyundai/g90_startup.py +++ b/opendbc_repo/opendbc/car/hyundai/g90_startup.py @@ -2,10 +2,11 @@ from copy import deepcopy import time -from opendbc.car import Bus +from opendbc.car import Bus, make_tester_present_msg from opendbc.car.hyundai.values import CAR, HyundaiFlags from opendbc.car.isotp_parallel_query import IsoTpParallelQuery from opendbc.can.parser import MAX_BAD_COUNTER +from opendbc.car.hyundai.ev9_keeper import EV9Keeper as StartupKeeper from opendbc.car.hyundai.ecu_startup import HyundaiECUStartup, Outcome, stock_copy as stock_copy @@ -16,8 +17,55 @@ def required(cp): class G90Startup(HyundaiECUStartup): - def __init__(self, cp, callbacks): + def __init__(self, cp, callbacks, *, clock=time.monotonic, keeper_factory=StartupKeeper): super().__init__(cp, callbacks, address=0x7d0, bus=0, label='G90') + self.clock = clock + self.keeper_factory = keeper_factory + self.keeper = None + self.handed_off = False + + def prepare(self, *, admission): + result = super().prepare(admission=admission) + if required(self.cp) and self.outcome is Outcome.SENT_UNCONFIRMED: + self.keeper = self.keeper_factory( + lambda: self.callbacks[1]([make_tester_present_msg(self.address, self.bus, suppress_response=True)]), clock=self.clock) + try: + self.keeper.start() + except Exception: + if self.keeper.thread is not None and self.keeper.thread.ident is None: + self.keeper.event.set() + self.keeper.thread = None + self.close() + raise + return result + + def check(self): + super().check() + keeper = getattr(self, 'keeper', None) + if keeper is not None and keeper.abort_reason is not None: + keeper.stop() + self.outcome = Outcome.ABORT_UNCERTAIN + raise RuntimeError('G90 startup diagnostic continuity failed') + + def maintain(self, *, configured): + self.check() + + def sent(self, frames, *, valid): + keeper = getattr(self, 'keeper', None) + if keeper is None or self.handed_off or not valid or self.closed or not self.published: + return + tester = make_tester_present_msg(self.address, self.bus, suppress_response=True) + if tester not in frames: + return + keeper.stop() + self.check() + self.handed_off = True + + def close(self): + keeper = getattr(self, 'keeper', None) + if keeper is not None: + keeper.stop() + super().close() def _query(self, *args, **kwargs): return IsoTpParallelQuery(*args, **kwargs) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_g90_continuity.py b/opendbc_repo/opendbc/car/hyundai/tests/test_g90_continuity.py new file mode 100644 index 0000000000..7d5410bed0 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_g90_continuity.py @@ -0,0 +1,153 @@ +from unittest.mock import patch + +import pytest + +from opendbc.car import gen_empty_fingerprint, make_tester_present_msg +from opendbc.car.hyundai import g90_startup +from opendbc.car.hyundai.ecu_startup import Outcome +from opendbc.car.hyundai.ev9_keeper import EV9Keeper +from opendbc.car.hyundai.g90_startup import G90Startup +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.values import CAR + + +class ManualKeeper(EV9Keeper): + def start(self): + self.tick() + + +def prepared(*, aol=False, send_error=False, keeper_factory=ManualKeeper, holders=None): + now = [8.637416955] + sent = [] + cp = CarInterface.get_params(CAR.GENESIS_G90, gen_empty_fingerprint(), [], alpha_long=True, is_release=False, docs=False) + assert cp.openpilotLongitudinalControl + if aol: + cp.safetyConfigs[0].safetyParam |= 0x400 + cp.alternativeExperience = 32 + + def send(frames): + if send_error and make_tester_present_msg(0x7d0, 0, suppress_response=True) in frames: + raise OSError('Actual sender rejected tester') + sent.extend((now[0], frame) for frame in frames) + + def query(can_send, can_recv, bus, addresses, requests, responses): + request = requests[0] + + class Query: + def get_data(self, timeout): + can_send([(0x7d0, bytes([len(request)]) + request + bytes(7 - len(request)), 0)]) + return {} if request == b"\x28\x83\x01" else {(0x7d0, None): b''} + return Query() + + owner = G90Startup(cp, (list, send)) + if holders is not None: + holders.append(owner) + owner.clock = lambda: now[0] + owner.keeper_factory = keeper_factory + with patch.object(g90_startup, 'IsoTpParallelQuery', side_effect=query), patch.object(g90_startup.time, 'sleep'): + owner.prepare(admission=lambda: True) + return owner, now, sent + + +@pytest.mark.parametrize('aol', [False, True]) +def test_recorded_disable_to_first_controller_interval_has_continuous_tester(aol): + owner, now, sent = prepared(aol=aol) + tester = make_tester_present_msg(0x7d0, 0, suppress_response=True) + try: + assert any(frame == tester for _, frame in sent), 'No tester starts after successful ECU disable' + for second in range(1, 7): + now[0] = 8.637416955 + second + owner.keeper.tick() + now[0] = 15.046147807 + times = [stamp for stamp, frame in sent if frame == tester] + assert len(times) == 7 + assert max(right - left for left, right in zip(times, times[1:], strict=False)) <= 1.000001 + assert now[0] - times[-1] < 1.0 + assert owner.outcome is Outcome.SENT_UNCONFIRMED + assert all(frame == tester or frame[1][:3] == b"\x02\x10\x03" or frame[1][:4] == b"\x03\x28\x83\x01" for _, frame in sent) + finally: + owner.published = True + owner.close() + + +@pytest.mark.parametrize('kind', ['invalid', 'empty', 'wrong_bus', 'wrong_payload', 'unpublished', 'valid']) +def test_only_actual_valid_controller_tester_receipt_ends_continuity(kind): + owner, now, sent = prepared() + try: + tester = make_tester_present_msg(0x7d0, 0, suppress_response=True) + owner.published = kind != 'unpublished' + frames = [tester] + if kind == 'empty': + frames = [] + elif kind == 'wrong_bus': + frames = [(tester[0], tester[1], 1)] + elif kind == 'wrong_payload': + frames = [(tester[0], b"\x02\x3e\x00" + bytes(5), tester[2])] + owner.sent(frames, valid=kind != 'invalid') + assert owner.handed_off == (kind == 'valid') + assert owner.keeper.event.is_set() == (kind == 'valid') + count = len(sent) + now[0] += 1.0 + owner.keeper.tick() + assert len(sent) == count + (kind != 'valid') + finally: + owner.published = True + owner.close() + + +@pytest.mark.parametrize('fault', ['deadline', 'clock_reversal', 'sender']) +def test_failed_continuity_cannot_be_acknowledged_or_rearmed(fault): + owner, now, sent = prepared(send_error=fault == 'sender') + owner.published = True + try: + if fault == 'deadline': + now[0] += 60.0 + elif fault == 'clock_reversal': + now[0] -= 1.0 + owner.keeper.tick() + with pytest.raises(RuntimeError, match='diagnostic continuity failed'): + owner.maintain(configured=True) + assert owner.outcome is Outcome.ABORT_UNCERTAIN + assert not owner.handed_off + count = len(sent) + now[0] += 1.0 + assert not owner.keeper.tick() + assert len(sent) == count + finally: + owner.close() + + +def test_close_stops_tester_before_unpublished_stock_restoration(): + owner, now, sent = prepared() + requests = [] + + def query(*args, **kwargs): + assert owner.keeper.event.is_set() + request = args[4][0] + requests.append(request) + + class Query: + def get_data(self, timeout): + return {(0x7d0, None): b''} + return Query() + + with patch.object(g90_startup, 'IsoTpParallelQuery', side_effect=query): + owner.close() + assert requests == [b"\x10\x03", b"\x28\x00\x01", b"\x10\x01"] + assert owner.closed and owner.restored + count = len(sent) + now[0] += 1.0 + assert not owner.keeper.tick() + assert len(sent) == count + + +def test_actual_thread_start_failure_restores_stock_and_preserves_original_error(): + holders = [] + with patch('threading.Thread.start', side_effect=OSError('thread launch failed')): + with pytest.raises(OSError, match='thread launch failed'): + prepared(keeper_factory=EV9Keeper, holders=holders) + owner = holders[0] + assert owner.closed and owner.restored + assert not owner.published + assert owner.keeper.event.is_set() + assert owner.keeper.thread is None diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_g90_continuity_native.py b/opendbc_repo/opendbc/car/hyundai/tests/test_g90_continuity_native.py new file mode 100644 index 0000000000..c9ba931dec --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_g90_continuity_native.py @@ -0,0 +1,80 @@ +from types import SimpleNamespace +from unittest.mock import patch + +import pytest + +from opendbc.car import Bus, make_tester_present_msg +from opendbc.car.hyundai.tests.test_g90_continuity import prepared +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.values import CAR, DBC +from opendbc.safety.tests.common import CANPackerSafety +from opendbc.safety.tests.test_hyundai import checksum +from opendbc.safety.tests.libsafety import libsafety_py +from openpilot.selfdrive.car.card import Car +from openpilot.starpilot.vehicle_startup import VehicleStartupOwner + + +@pytest.mark.parametrize('address,data', [(0x420, '501420c8b84b6a00'), (0x421, '000000ffe37f2db1')]) +@pytest.mark.parametrize('bus', [0, 2]) +def test_recorded_returned_stock_scc_keeps_native_relay_protection(address, data, bus): + safety = libsafety_py.libsafety + safety.set_alternative_experience(32) + assert safety.set_safety_hooks(8, 0xC04) == 0 + safety.init_tests() + safety.set_alternative_experience(32) + packer = CANPackerSafety(DBC[CAR.GENESIS_G90][Bus.pt]) + for name, values, fix in ( + ('WHL_SPD11', {'WHL_SPD_AliveCounter_LSB': 0, 'WHL_SPD_AliveCounter_MSB': 0}, checksum), + ('TCS13', {'AliveCounterTCS': 0}, checksum), + ('MDPS12', {}, None), + ('CLU11', {'CF_Clu_AliveCnt1': 0}, None), + ('EMS16', {'CRUISE_LAMP_S': 0, 'AliveCounter': 0}, checksum), + ('BCM_PO_11', {'LDA_BTN': 0}, None), + ): + assert safety.safety_rx_hook(packer.make_can_msg_safety(name, 0, values, fix_checksum=fix)), name + safety.set_timer(1000) + safety.safety_tick() + assert safety.safety_config_valid() + assert not safety.get_relay_malfunction() + safety.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, bytes.fromhex(data))) + assert bool(safety.get_relay_malfunction()) == (bus == 0) + assert not safety.get_controls_allowed() + assert safety.aol_get_permission_mask() == 0 + if bus == 0: + tester = make_tester_present_msg(0x7d0, 0, suppress_response=True) + assert not safety.safety_tx_hook(libsafety_py.make_CANPacket(tester[0], tester[2], tester[1])) + + +@pytest.mark.parametrize('receipt', ['valid', 'invalid', 'exception']) +def test_current_card_publisher_transfers_continuity_only_after_success(receipt): + owner, now, sent = prepared() + owner.published = True + delivered = [] + card = Car.__new__(Car) + card.CI = CarInterface(owner.cp) + card.CP = owner.cp + card.vehicle_startup = VehicleStartupOwner(owner) + + def send(service, packet): + assert service == 'sendcan' + assert not owner.handed_off + if receipt == 'exception': + raise OSError('Actual publication failed') + delivered.append(packet) + + try: + with patch.object(card, 'pm', SimpleNamespace(send=send), create=True), patch.object(card, 'volt_startup_keepalive', return_value=False): + frames = [make_tester_present_msg(0x7d0, 0, suppress_response=True)] + if receipt == 'exception': + with pytest.raises(OSError, match='publication failed'): + card.publish_sendcan(frames) + else: + card.publish_sendcan(frames, valid=receipt == 'valid') + assert bool(delivered) == (receipt != 'exception') + assert owner.handed_off == (receipt == 'valid') + count = len(sent) + now[0] += 1.0 + owner.keeper.tick() + assert len(sent) == count + (receipt != 'valid') + finally: + owner.close() diff --git a/prebuilt.json b/prebuilt.json index b4353eccb2..01b7cc9754 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "c319992bbe9280bf5904b8e8eb6a87d0f2d16d1a5a21482238b9d7e021ad06ff", "schema_version": 1} +{"protected_inventory_sha256": "226a3f00d9020c539ae74d575cbc3fb3525405d370335ae84327401130bce1b7", "schema_version": 1} diff --git a/tools/ci/run_vehicle_tests.py b/tools/ci/run_vehicle_tests.py index 0e95e6b971..49b35660f9 100755 --- a/tools/ci/run_vehicle_tests.py +++ b/tools/ci/run_vehicle_tests.py @@ -33,6 +33,7 @@ DEBUG_SAFETY_CLASS_TARGETS = { RELEASE_TARGETS = ( + "opendbc_repo/opendbc/car/hyundai/tests/test_g90_continuity.py", "opendbc_repo/opendbc/car/hyundai/tests/test_disable_response.py", "openpilot/starpilot/aol/tests/test_angle_stop_native_feedback.py", "openpilot/starpilot/aol/tests/test_angle_stop_fault_guards.py",