diff --git a/opendbc_repo/opendbc/car/hyundai/discovery.py b/opendbc_repo/opendbc/car/hyundai/discovery.py index 8bea025b0f..811afdf980 100644 --- a/opendbc_repo/opendbc/car/hyundai/discovery.py +++ b/opendbc_repo/opendbc/car/hyundai/discovery.py @@ -17,10 +17,17 @@ HDA1_NORMAL_BUS_LAYOUT = {(0, 0x120): 32, (2, 0x120): 32, (0, 0x35): 32, (0, 0xE (0, 0xA0): 24, (0, 0x1A0): 32, (0, 0x1CF): 8, (0, 0x130): 16} +HDA2_NORMAL_BUS_LAYOUT = {(2, 0x50): 16, (2, 0x2A4): 24, (1, 0x35): 32, (1, 0xEA): 24, + (1, 0xA0): 24, (1, 0x1A0): 32, (1, 0x1CF): 8, (1, 0x130): 16} + + def normal_bus_topology(can_recv, *, clock=time.monotonic): deadline = clock() + .12 layouts = (NORMAL_BUS_LAYOUT, CLASSIC_NORMAL_BUS_LAYOUT) observed = [{} for _ in layouts] + original_invalid = False + hda2_observed = {} + hda2_invalid = False hda1_observed = {} hda1_invalid = False for _ in range(24): @@ -33,14 +40,24 @@ def normal_bus_topology(can_recv, *, clock=time.monotonic): if key in layout: length = len(frame.dat) if length != layout[key]: - return False + # Standard HDA2 reuses the classic MDPS identifier with 16 bytes. + if key != (0, 0x251) or length != 16: + return False + original_invalid = True received[key] = length + if key in HDA2_NORMAL_BUS_LAYOUT: + if len(frame.dat) != HDA2_NORMAL_BUS_LAYOUT[key]: + hda2_invalid = True + else: + hda2_observed[key] = len(frame.dat) if key in HDA1_NORMAL_BUS_LAYOUT: if len(frame.dat) != HDA1_NORMAL_BUS_LAYOUT[key]: hda1_invalid = True else: hda1_observed[key] = len(frame.dat) - if (any(received == layout for layout, received in zip(layouts, observed, strict=True)) or - not hda1_invalid and hda1_observed == HDA1_NORMAL_BUS_LAYOUT): + # Other families reuse HDA2 identifiers with different packet lengths. + if (not hda2_invalid and hda2_observed == HDA2_NORMAL_BUS_LAYOUT or + not original_invalid and (any(received == layout for layout, received in zip(layouts, observed, strict=True)) or + not hda1_invalid and hda1_observed == HDA1_NORMAL_BUS_LAYOUT)): return True return False diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_hda2_normal_discovery.py b/opendbc_repo/opendbc/car/hyundai/tests/test_hda2_normal_discovery.py new file mode 100644 index 0000000000..e5c779a451 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_hda2_normal_discovery.py @@ -0,0 +1,92 @@ +"""Standard HDA2 wiring selects fresh normal-bus queries, never vehicle identity.""" +import json +from pathlib import Path +from unittest.mock import patch + +import pytest + +from opendbc.car import gen_empty_fingerprint +from opendbc.car.can_definitions import CanData +from opendbc.car.car_helpers import fingerprint +from opendbc.car.hyundai.discovery import CLASSIC_NORMAL_BUS_LAYOUT, HDA1_NORMAL_BUS_LAYOUT, NORMAL_BUS_LAYOUT, normal_bus_topology +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.values import CAR +from opendbc.car.vin import VIN_UNKNOWN +from openpilot.cereal import log +from openpilot.starpilot.aol.panda_faults import blocking_fault_list +from openpilot.starpilot.car.hyundai.aol import policy_for + +CAPTURE = json.loads((Path(__file__).parent / 'testdata/hda2_discovery.json').read_text()) + + +def frames(collision=True): + values = ([CAPTURE['classicIdCollision']] if collision else []) + CAPTURE['frames'] + return [CanData(f['address'], bytes.fromhex(f['data']), f['bus']) for f in values] + + +@pytest.mark.parametrize('collision', [False, True]) +def test_recorded_hda2_layout_selects_normal_wiring(collision): + assert normal_bus_topology(lambda **kw: [frames(collision)]), 'HDA2_NORMAL_QUERY_LAYOUT_MISSING' + + +@pytest.mark.parametrize('index', range(8)) +@pytest.mark.parametrize('defect', ['missing', 'length', 'bus', 'echo', 'contradiction']) +def test_incomplete_malformed_or_echoed_hda2_still_cannot_select_query_family(index, defect): + packets = frames() + original = packets.pop(index + 1) + if defect in ('length', 'contradiction'): + packets.append(CanData(original.address, original.dat[:-1], original.src)) + if defect == 'contradiction': + packets.append(original) + elif defect != 'missing': + packets.append(CanData(original.address, original.dat, original.src + (128 if defect == 'echo' else 1))) + assert not normal_bus_topology(lambda **kw: [packets]) + + +@pytest.mark.parametrize('layout', [NORMAL_BUS_LAYOUT, CLASSIC_NORMAL_BUS_LAYOUT, HDA1_NORMAL_BUS_LAYOUT]) +def test_original_family_contradictions_still_fail_without_complete_hda2_proof(layout): + packets = [CanData(address, bytes(length), bus) for (bus, address), length in layout.items()] + packets.insert(0, CanData(0x251, bytes(16), 0)) + assert not normal_bus_topology(lambda **kw: [packets]) + + +@pytest.mark.parametrize('collision', [False, True]) +def test_complete_hda2_proof_can_span_multiple_can_drains(collision): + packets = frames(collision) + batches = iter([[f] for f in packets]) + assert normal_bus_topology(lambda **kw: [next(batches, [])]) + + +@pytest.mark.parametrize('collision', [False, True]) +def test_actual_fingerprint_keeps_hda2_on_normal_wiring_and_requires_fresh_identity(collision): + switches = [] + with patch('opendbc.car.car_helpers.get_vin', return_value=(-1, -1, VIN_UNKNOWN)) as vin, \ + patch('opendbc.car.car_helpers.get_fw_versions', return_value=[]) as query, \ + patch('opendbc.car.car_helpers.get_present_ecus', return_value=set()) as present, \ + patch('opendbc.car.car_helpers.get_fw_versions_ordered', return_value=[]) as generic, \ + patch('opendbc.car.car_helpers.match_fw_to_car', return_value=(True, set())), \ + patch('opendbc.car.car_helpers.can_fingerprint', return_value=(None, {})): + actual = fingerprint(lambda wait_for_one=False: [frames(collision)], lambda packets: None, switches.append, None) + assert actual[0] is None + assert switches and not any(switches), 'HDA2_ENTERED_OBD_QUERY_FALLBACK' + assert vin.call_args.args[2] == (0, 1) + query.assert_called_once() + assert query.call_args.kwargs == {'query_brand': 'hyundai', 'normal_bus_only': True} + present.assert_not_called() + generic.assert_not_called() + + +@pytest.mark.parametrize('identity', [CAR.KIA_EV6, CAR.HYUNDAI_IONIQ_5]) +@pytest.mark.parametrize('release', [False, True]) +def test_exact_ev6_stock_aol_word_and_neighbor_keep_interrupt_fault_blocking(identity, release): + observed = gen_empty_fingerprint() + for frame in frames(): + observed[frame.src][frame.address] = len(frame.dat) + cp = CarInterface.get_params(identity, observed, [], False, release, False) + assert cp.pcmCruise and not cp.openpilotLongitudinalControl + assert cp.safetyConfigs[0].safetyParam == 0x11 + cp.safetyConfigs[0].safetyParam |= policy_for(cp).safety_param_addition + assert cp.safetyConfigs[0].safetyParam == 0x811 + if identity == CAR.KIA_EV6: + assert cp.flags == 10241 + assert blocking_fault_list([log.PandaState.FaultType.interruptRateCan2], cp) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/testdata/hda2_discovery.json b/opendbc_repo/opendbc/car/hyundai/tests/testdata/hda2_discovery.json new file mode 100644 index 0000000000..6f6b013723 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/testdata/hda2_discovery.json @@ -0,0 +1,50 @@ +{ + "build": "6e11c50dfce6da18db9700b7305a2febcbd4ee9f", + "frames": [ + { + "bus": 1, + "address": 53, + "data": "c9f71a510500250526f5fe100040383f04000000203996d40500000000000000" + }, + { + "bus": 1, + "address": 463, + "data": "46e0002000000000" + }, + { + "bus": 1, + "address": 304, + "data": "004db302020300000400000000000000" + }, + { + "bus": 1, + "address": 234, + "data": "2d032110414300001408d50fe9ff0000e9ff000000000000" + }, + { + "bus": 1, + "address": 160, + "data": "d216a974be4feb05300230022f022e020040010000000000" + }, + { + "bus": 2, + "address": 80, + "data": "8d8c1f02400008006400010000000000" + }, + { + "bus": 1, + "address": 416, + "data": "3acc8b56083564000000000000080000fff33f130a000500fe07000000000000" + }, + { + "bus": 2, + "address": 676, + "data": "f64b0622115512338333a000d20000000000000000000000" + } + ], + "classicIdCollision": { + "bus": 0, + "address": 593, + "data": "4f2f0515140206210400000000000000" + } +} diff --git a/prebuilt.json b/prebuilt.json index e9bf5ebd9f..f622aa2df2 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "567658c0f7e93cec0b15bec55dcabded810f6cd2c1d3321009976fa147360f4a", "schema_version": 1} +{"protected_inventory_sha256": "997dfa5352e8044453598a3f140eda0561e8ceb6a086696f7965545297e18a2e", "schema_version": 1}