Recognize standard HDA2 wiring before diagnostic queries

This commit is contained in:
firestar5683
2026-10-29 18:59:39 -05:00
parent e3fbbdddb9
commit 33e87cea5a
4 changed files with 163 additions and 4 deletions
+20 -3
View File
@@ -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
@@ -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)
@@ -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"
}
}
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "567658c0f7e93cec0b15bec55dcabded810f6cd2c1d3321009976fa147360f4a", "schema_version": 1}
{"protected_inventory_sha256": "997dfa5352e8044453598a3f140eda0561e8ceb6a086696f7965545297e18a2e", "schema_version": 1}