IQ.Pilot Prebuilt Release @ 27f668a

This commit is contained in:
IQ.Lvbs CI [bot]
2026-09-03 18:23:24 -05:00
commit b073c5182b
2554 changed files with 679696 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env python3
from iqdbc.car.structs import CarParams
from iqdbc.car.hyundai.values import PLATFORM_CODE_ECUS, get_platform_codes
from iqdbc.car.hyundai.fingerprints import FW_VERSIONS
Ecu = CarParams.Ecu
if __name__ == "__main__":
for car_model, ecus in FW_VERSIONS.items():
print()
print(car_model)
for ecu in sorted(ecus):
if ecu[0] not in PLATFORM_CODE_ECUS:
continue
platform_codes = get_platform_codes(ecus[ecu])
codes = {code for code, _ in platform_codes}
dates = {date for _, date in platform_codes if date is not None}
print(f' (Ecu.{ecu[0]}, {hex(ecu[1])}, {ecu[2]}):')
print(f' Codes: {codes}')
print(f' Dates: {dates}')

View File

@@ -0,0 +1,194 @@
import math
import pytest
from iqdbc.can import CANPacker, CANParser
from iqdbc.car import Bus, gen_empty_fingerprint, structs
from iqdbc.car.hyundai.carstate import CarState, EV_MODE_STATUS_TIMEOUT_NS, _get_ev_mode_state
from iqdbc.car.hyundai.interface import CarInterface
from iqdbc.car.hyundai.values import CANFD_HYBRID_STATUS_ADDR, CANFD_HYBRID_STATUS_DLC, CAR, DBC, EV_MODE_ACTIVE_VALUES, \
EV_MODE_STATUS_ADDR, EV_MODE_STATUS_DLC, EV_MODE_STATUS_MSG, EV_MODE_STATUS_SIGNAL, \
HyundaiExtFlags, HyundaiFlags
from iqpilot.common.params import Params
def get_params(candidate, *, hybrid=True, hybrid_bus=0, hybrid_status=None, hybrid_status_bus=0,
hybrid_status_dlc=CANFD_HYBRID_STATUS_DLC, status_bus=0, status_dlc=EV_MODE_STATUS_DLC):
Params().put_int("HyundaiCameraSCC", 1)
Params().put_int("CanfdHDA2", 1)
fingerprint = gen_empty_fingerprint()
if hybrid:
fingerprint[hybrid_bus][0x105] = 32
if hybrid_status is None:
hybrid_status = hybrid
if hybrid_status:
fingerprint[hybrid_status_bus][CANFD_HYBRID_STATUS_ADDR] = hybrid_status_dlc
if status_bus is not None:
fingerprint[status_bus][EV_MODE_STATUS_ADDR] = status_dlc
return CarInterface.get_params(candidate, fingerprint, [], False, False, False)
@pytest.mark.parametrize(("candidate", "hybrid", "status_bus", "status_dlc", "expected"), (
(CAR.HYUNDAI_SANTAFE_MX5_HEV, True, 0, 32, True),
(CAR.HYUNDAI_SANTAFE_MX5_HEV, False, 0, 32, False),
(CAR.HYUNDAI_SANTAFE_MX5_HEV, True, None, 32, False),
(CAR.HYUNDAI_SANTAFE_MX5_HEV, True, 1, 32, False),
(CAR.HYUNDAI_SANTAFE_MX5_HEV, True, 0, 16, False),
(CAR.KIA_SORENTO_4TH_GEN, False, None, 32, False),
# Shared ICE/HEV/PHEV candidates rely on the observed ECAN capability frames, not their model name.
(CAR.HYUNDAI_TUCSON_4TH_GEN, True, 0, 32, True),
(CAR.HYUNDAI_TUCSON_4TH_GEN, False, 0, 32, False),
(CAR.KIA_SORENTO_HEV_4TH_GEN, True, 0, 32, True),
(CAR.HYUNDAI_KONA_HEV_2ND_GEN, True, 0, 32, True),
(CAR.HYUNDAI_KONA_HEV_2ND_GEN, False, 0, 32, False),
(CAR.HYUNDAI_ELANTRA_HEV_2021, True, 0, 32, False),
))
def test_ev_mode_capability(candidate, hybrid, status_bus, status_dlc, expected):
CP = get_params(candidate, hybrid=hybrid, status_bus=status_bus, status_dlc=status_dlc)
assert bool(CP.extFlags & HyundaiExtFlags.EV_MODE_STATUS_230) == expected
@pytest.mark.parametrize(("hybrid_status_bus", "hybrid_status_dlc"), (
(1, CANFD_HYBRID_STATUS_DLC),
(0, 16),
))
def test_ev_mode_capability_requires_ecan_hybrid_status_dlc32(hybrid_status_bus, hybrid_status_dlc):
CP = get_params(CAR.HYUNDAI_SANTAFE_MX5_HEV, hybrid_status_bus=hybrid_status_bus,
hybrid_status_dlc=hybrid_status_dlc)
assert not bool(CP.extFlags & HyundaiExtFlags.EV_MODE_STATUS_230)
def test_0x105_and_ev_status_without_0xfa_do_not_enable_ev_mode():
CP = get_params(CAR.HYUNDAI_TUCSON_4TH_GEN, hybrid=True, hybrid_status=False)
assert bool(CP.flags & HyundaiFlags.HYBRID)
assert not bool(CP.extFlags & HyundaiExtFlags.EV_MODE_STATUS_230)
def test_ev_mode_display_capability_does_not_change_hybrid_safety_classification():
CP = get_params(CAR.HYUNDAI_TUCSON_4TH_GEN, hybrid=False, hybrid_status=True)
assert not bool(CP.flags & HyundaiFlags.HYBRID)
assert bool(CP.extFlags & HyundaiExtFlags.EV_MODE_STATUS_230)
def test_ev_mode_capability_uses_the_detected_ecan_offset():
CP = get_params(CAR.KIA_SORENTO_HEV_4TH_GEN, hybrid_bus=4, hybrid_status_bus=4, status_bus=4)
assert bool(CP.extFlags & HyundaiExtFlags.EV_MODE_STATUS_230)
def test_ev_mode_parser_registration_is_capability_gated():
supported = get_params(CAR.HYUNDAI_SANTAFE_MX5_HEV)
unsupported = get_params(CAR.KIA_SORENTO_4TH_GEN, hybrid=False, status_bus=1, status_dlc=16)
supported_parser = CarState.get_can_parsers_canfd(None, supported)[Bus.pt]
unsupported_parser = CarState.get_can_parsers_canfd(None, unsupported)[Bus.pt]
assert EV_MODE_STATUS_ADDR in supported_parser.addresses
assert supported_parser.message_states[EV_MODE_STATUS_ADDR].ignore_alive
assert supported_parser.message_states[EV_MODE_STATUS_ADDR].ignore_counter
assert EV_MODE_STATUS_ADDR not in unsupported_parser.addresses
def test_sorento_ice_corner_radar_status_does_not_enable_ev_mode():
Params().put_int("HyundaiCameraSCC", 1)
Params().put_int("CanfdHDA2", 1)
fingerprint = gen_empty_fingerprint()
fingerprint[1][0x230] = 16 # Actual Sorento ICE ACAN corner-radar status frame.
CP = CarInterface.get_params(CAR.KIA_SORENTO_4TH_GEN, fingerprint, [], False, False, False)
parser = CarState.get_can_parsers_canfd(None, CP)[Bus.pt]
assert not bool(CP.extFlags & HyundaiExtFlags.EV_MODE_STATUS_230)
assert EV_MODE_STATUS_ADDR not in parser.addresses
def test_ev_mode_state_requires_a_fresh_dlc32_frame():
CP = get_params(CAR.HYUNDAI_SANTAFE_MX5_HEV)
parser = CarState.get_can_parsers_canfd(None, CP)[Bus.pt]
packer = CANPacker(DBC[CP.carFingerprint][Bus.pt])
assert _get_ev_mode_state(parser) == (False, False)
timestamp = 1_000_000_000
wrong_bus_short_status = (EV_MODE_STATUS_ADDR, b"\x00" * 16, 1)
parser.update([timestamp, [wrong_bus_short_status]])
assert _get_ev_mode_state(parser) == (False, False)
ev_active = packer.make_can_msg(EV_MODE_STATUS_MSG, parser.bus, {"COUNTER": 1, EV_MODE_STATUS_SIGNAL: 6})
parser.update([timestamp + 100_000_000, [ev_active]])
assert _get_ev_mode_state(parser) == (True, True)
ev_inactive = packer.make_can_msg(EV_MODE_STATUS_MSG, parser.bus, {"COUNTER": 2, EV_MODE_STATUS_SIGNAL: 3})
parser.update([timestamp + 200_000_000, [ev_inactive]])
assert _get_ev_mode_state(parser) == (False, True)
parser.dat[EV_MODE_STATUS_ADDR] = b"\x00" * 16
assert _get_ev_mode_state(parser) == (False, False)
parser.dat[EV_MODE_STATUS_ADDR] = ev_inactive[1]
parser.update([timestamp + 200_000_000 + EV_MODE_STATUS_TIMEOUT_NS + 1, []])
assert not parser.bus_timeout
assert _get_ev_mode_state(parser) == (False, False)
@pytest.mark.parametrize("mode", range(16))
def test_ev_mode_enum_mapping(mode):
CP = get_params(CAR.HYUNDAI_SANTAFE_MX5_HEV)
parser = CarState.get_can_parsers_canfd(None, CP)[Bus.pt]
packer = CANPacker(DBC[CP.carFingerprint][Bus.pt])
msg = packer.make_can_msg(EV_MODE_STATUS_MSG, parser.bus, {"COUNTER": mode, EV_MODE_STATUS_SIGNAL: mode})
parser.update([1_000_000_000, [msg]])
assert int(parser.vl[EV_MODE_STATUS_MSG][EV_MODE_STATUS_SIGNAL]) == mode
assert _get_ev_mode_state(parser) == (mode in EV_MODE_ACTIVE_VALUES, True)
@pytest.mark.parametrize(("payload", "expected_mode", "expected_active"), (
("758139048402000000000000000000000000d3009805001c24000000c05dc80f", 1, True),
("4d466f048402000000000000000000000000bf007408001c24000000c05dc80f", 2, True),
("32206e048402000000000000000000000000c9007418001c24000000c05dc80f", 6, True),
# Route 455 mode 3 was a false positive with the old single-bit 0x230 interpretation.
("4f935e0484020000000000000000000000004400640d001c10000000c05dc40f", 3, False),
("066558048402000000000000000000000000c4003020001c24000000c05dc80f", 8, False),
("059683048402000000000000000000000000a1008425001c24000000c05dc80f", 9, False),
("011305048402000000000000000000000000d600a829001c24000000c05dc80f", 10, False),
))
def test_ev_mode_dbc_decodes_real_mx5_frames(payload, expected_mode, expected_active):
parser = CANParser("hyundai_canfd_generated", [(EV_MODE_STATUS_MSG, math.nan)], 0)
updated = parser.update([1_000_000_000, [(EV_MODE_STATUS_ADDR, bytes.fromhex(payload), 0)]])
assert updated == {EV_MODE_STATUS_ADDR}
assert int(parser.vl[EV_MODE_STATUS_MSG][EV_MODE_STATUS_SIGNAL]) == expected_mode
assert _get_ev_mode_state(parser) == (expected_active, True)
def test_ev_mode_rejects_checksum_corruption():
CP = get_params(CAR.HYUNDAI_SANTAFE_MX5_HEV)
parser = CarState.get_can_parsers_canfd(None, CP)[Bus.pt]
payload = bytearray.fromhex("32206e048402000000000000000000000000c9007418001c24000000c05dc80f")
payload[-1] ^= 1
parser.update([1_000_000_000, [(EV_MODE_STATUS_ADDR, bytes(payload), parser.bus)]])
assert EV_MODE_STATUS_ADDR not in parser.dat
assert _get_ev_mode_state(parser) == (False, False)
def test_ev_mode_fields_default_invalid():
state = structs.CarState()
assert not state.evModeActive
assert not state.evModeValid
def test_ev_mode_parser_is_optional_for_can_validity():
CP = get_params(CAR.HYUNDAI_SANTAFE_MX5_HEV)
parser = CarState.get_can_parsers_canfd(None, CP)[Bus.pt]
state = parser.message_states[EV_MODE_STATUS_ADDR]
assert state.ignore_alive
assert state.ignore_counter

View File

@@ -0,0 +1,225 @@
from hypothesis import settings, given, strategies as st
from iqdbc.car import gen_empty_fingerprint
from iqdbc.car.structs import CarParams
from iqdbc.car.fw_versions import build_fw_dict
from iqdbc.car.hyundai.interface import CarInterface
from iqdbc.car.hyundai.radar_interface import RADAR_START_ADDR
from iqdbc.car.hyundai.values import CANFD_CAR, CAN_GEARS, CAR, CHECKSUM, DATE_FW_ECUS, \
HYBRID_CAR, EV_CAR, FW_QUERY_CONFIG, LEGACY_SAFETY_MODE_CAR, \
PLATFORM_CODE_ECUS, HYUNDAI_VERSION_REQUEST_LONG, \
HyundaiFlags, get_platform_codes, HyundaiSafetyFlags
from iqdbc.car.hyundai.fingerprints import FW_VERSIONS
Ecu = CarParams.Ecu
# Some platforms have date codes in a different format we don't yet parse (or are missing).
# For now, assert list of expected missing date cars
NO_DATES_PLATFORMS = {
# CAN FD
CAR.KIA_SPORTAGE_5TH_GEN,
CAR.HYUNDAI_SANTA_CRUZ_1ST_GEN,
CAR.HYUNDAI_TUCSON_4TH_GEN,
# CAN
CAR.HYUNDAI_ELANTRA,
CAR.HYUNDAI_ELANTRA_GT_I30,
CAR.KIA_CEED,
CAR.KIA_FORTE,
CAR.KIA_OPTIMA_G4,
CAR.KIA_OPTIMA_G4_FL,
CAR.KIA_SORENTO,
CAR.HYUNDAI_KONA,
CAR.HYUNDAI_KONA_EV,
CAR.HYUNDAI_KONA_EV_2022,
CAR.HYUNDAI_KONA_HEV,
CAR.HYUNDAI_SONATA_LF,
CAR.HYUNDAI_VELOSTER,
CAR.HYUNDAI_KONA_2022,
}
CANFD_EXPECTED_ECUS = {Ecu.fwdCamera, Ecu.fwdRadar}
class TestHyundaiFingerprint:
def test_feature_detection(self, monkeypatch, tmp_path):
monkeypatch.setenv("PARAMS_ROOT", str(tmp_path))
# radar available
for radar in (True, False):
fingerprint = gen_empty_fingerprint()
if radar:
fingerprint[1][RADAR_START_ADDR] = 8
CP = CarInterface.get_params(CAR.HYUNDAI_SONATA, fingerprint, [], False, False, False)
assert CP.radarUnavailable != radar
def test_alternate_limits(self):
# Alternate lateral control limits, for high torque cars, verify Panda safety mode flag is set
fingerprint = gen_empty_fingerprint()
for car_model in CAR:
CP = CarInterface.get_params(car_model, fingerprint, [], False, False, False)
assert bool(CP.flags & HyundaiFlags.ALT_LIMITS) == bool(CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.ALT_LIMITS)
def test_can_features(self):
# Test no EV/HEV in any gear lists (should all use ELECT_GEAR)
assert set.union(*CAN_GEARS.values()) & (HYBRID_CAR | EV_CAR) == set()
# Test CAN FD cars are not classified with classic-CAN-only parsing or safety modes.
can_specific_feature_list = set.union(*CAN_GEARS.values(), *CHECKSUM.values(), LEGACY_SAFETY_MODE_CAR)
for car_model in CANFD_CAR:
assert car_model not in can_specific_feature_list, "CAN FD car unexpectedly found in a CAN feature list"
def test_hybrid_ev_sets(self):
assert HYBRID_CAR & EV_CAR == set(), "Shared cars between hybrid and EV"
assert HYBRID_CAR <= set(CAR)
assert EV_CAR <= set(CAR)
def test_canfd_ecu_whitelist(self):
# Asserts only expected Ecus can exist in database for CAN-FD cars
for car_model in CANFD_CAR:
ecus = {fw[0] for fw in FW_VERSIONS.get(car_model, {}).keys()}
ecus_not_in_whitelist = ecus - CANFD_EXPECTED_ECUS
ecu_strings = ", ".join([f"Ecu.{ecu}" for ecu in ecus_not_in_whitelist])
assert len(ecus_not_in_whitelist) == 0, \
f"{car_model}: Car model has unexpected ECUs: {ecu_strings}"
def test_blacklisted_parts(self, subtests):
# Asserts no ECUs known to be shared across platforms exist in the database.
# Tucson having Santa Cruz camera and EPS for example
for car_model, ecus in FW_VERSIONS.items():
if car_model == CAR.HYUNDAI_SANTA_CRUZ_1ST_GEN:
continue
with subtests.test(car_model=car_model.value):
for code, _ in get_platform_codes(ecus[(Ecu.fwdCamera, 0x7c4, None)]):
if b"-" not in code:
continue
part = code.split(b"-")[1]
assert not part.startswith(b'CW'), "Car has bad part number"
def test_correct_ecu_response_database(self, subtests):
"""
Assert standard responses for certain ECUs, since they can
respond to multiple queries with different data
"""
expected_fw_prefix = HYUNDAI_VERSION_REQUEST_LONG[1:]
for car_model, ecus in FW_VERSIONS.items():
with subtests.test(car_model=car_model.value):
for ecu, fws in ecus.items():
assert all(fw.startswith(expected_fw_prefix) for fw in fws), \
f"FW from unexpected request in database: {(ecu, fws)}"
@settings(max_examples=100)
@given(data=st.data())
def test_platform_codes_fuzzy_fw(self, data):
"""Ensure function doesn't raise an exception"""
fw_strategy = st.lists(st.binary())
fws = data.draw(fw_strategy)
get_platform_codes(fws)
def test_expected_platform_codes(self, subtests):
# Ensures we don't accidentally add multiple platform codes for a car unless it is intentional
for car_model, ecus in FW_VERSIONS.items():
with subtests.test(car_model=car_model.value):
for ecu, fws in ecus.items():
if ecu[0] not in PLATFORM_CODE_ECUS:
continue
# Third and fourth character are usually EV/hybrid identifiers
codes = {code.split(b"-")[0][:2] for code, _ in get_platform_codes(fws)}
if car_model == CAR.HYUNDAI_PALISADE:
assert codes == {b"LX", b"ON"}, f"Car has unexpected platform codes: {car_model} {codes}"
elif car_model == CAR.HYUNDAI_KONA_EV and ecu[0] == Ecu.fwdCamera:
assert codes == {b"OE", b"OS"}, f"Car has unexpected platform codes: {car_model} {codes}"
else:
assert len(codes) == 1, f"Car has multiple platform codes: {car_model} {codes}"
# Tests for platform codes, part numbers, and FW dates which Hyundai will use to fuzzy
# fingerprint in the absence of full FW matches:
def test_platform_code_ecus_available(self, subtests):
# TODO: add queries for these non-CAN FD cars to get EPS
no_eps_platforms = CANFD_CAR | {CAR.KIA_SORENTO, CAR.KIA_OPTIMA_G4, CAR.KIA_OPTIMA_G4_FL, CAR.KIA_OPTIMA_H,
CAR.KIA_OPTIMA_H_G4_FL, CAR.HYUNDAI_SONATA_LF, CAR.HYUNDAI_TUCSON, CAR.GENESIS_G90, CAR.GENESIS_G80, CAR.HYUNDAI_ELANTRA}
# Asserts ECU keys essential for fuzzy fingerprinting are available on all platforms
for car_model, ecus in FW_VERSIONS.items():
with subtests.test(car_model=car_model.value):
for platform_code_ecu in PLATFORM_CODE_ECUS:
if platform_code_ecu in (Ecu.fwdRadar, Ecu.eps) and car_model == CAR.HYUNDAI_GENESIS:
continue
if platform_code_ecu == Ecu.eps and car_model in no_eps_platforms:
continue
assert platform_code_ecu in [e[0] for e in ecus]
def test_fw_format(self, subtests):
# Asserts:
# - every supported ECU FW version returns one platform code
# - every supported ECU FW version has a part number
# - expected parsing of ECU FW dates
for car_model, ecus in FW_VERSIONS.items():
with subtests.test(car_model=car_model.value):
for ecu, fws in ecus.items():
if ecu[0] not in PLATFORM_CODE_ECUS:
continue
codes = set()
for fw in fws:
result = get_platform_codes([fw])
assert 1 == len(result), f"Unable to parse FW: {fw}"
codes |= result
if ecu[0] not in DATE_FW_ECUS or car_model in NO_DATES_PLATFORMS:
assert all(date is None for _, date in codes)
else:
assert all(date is not None for _, date in codes)
if car_model != CAR.HYUNDAI_GENESIS:
assert all(b"-" in code for code, _ in codes), \
f"FW does not have part number: {fw}"
def test_platform_codes_spot_check(self):
# Asserts basic platform code parsing behavior for a few cases
results = get_platform_codes([b"\xf1\x00DH LKAS 1.1 -150210"])
assert results == {(b"DH", b"150210")}
# Some cameras and all radars do not have dates
results = get_platform_codes([b"\xf1\x00AEhe SCC H-CUP 1.01 1.01 96400-G2000 "])
assert results == {(b"AEhe-G2000", None)}
results = get_platform_codes([b"\xf1\x00CV1_ RDR ----- 1.00 1.01 99110-CV000 "])
assert results == {(b"CV1-CV000", None)}
results = get_platform_codes([
b"\xf1\x00DH LKAS 1.1 -150210",
b"\xf1\x00AEhe SCC H-CUP 1.01 1.01 96400-G2000 ",
b"\xf1\x00CV1_ RDR ----- 1.00 1.01 99110-CV000 ",
])
assert results == {(b"DH", b"150210"), (b"AEhe-G2000", None), (b"CV1-CV000", None)}
results = get_platform_codes([
b"\xf1\x00LX2 MFC AT USA LHD 1.00 1.07 99211-S8100 220222",
b"\xf1\x00LX2 MFC AT USA LHD 1.00 1.08 99211-S8100 211103",
b"\xf1\x00ON MFC AT USA LHD 1.00 1.01 99211-S9100 190405",
b"\xf1\x00ON MFC AT USA LHD 1.00 1.03 99211-S9100 190720",
])
assert results == {(b"LX2-S8100", b"220222"), (b"LX2-S8100", b"211103"),
(b"ON-S9100", b"190405"), (b"ON-S9100", b"190720")}
def test_fuzzy_excluded_platforms(self):
platforms_with_shared_codes = set()
for platform, fw_by_addr in FW_VERSIONS.items():
car_fw = []
for ecu, fw_versions in fw_by_addr.items():
ecu_name, addr, sub_addr = ecu
for fw in fw_versions:
car_fw.append(CarParams.CarFw(ecu=ecu_name, fwVersion=fw, address=addr,
subAddress=0 if sub_addr is None else sub_addr))
CP = CarParams(carFw=car_fw)
matches = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(build_fw_dict(CP.carFw), CP.carVin, FW_VERSIONS)
if len(matches) == 1:
assert list(matches)[0] == platform
else:
platforms_with_shared_codes.add(platform)
# A unique fuzzy match must always resolve back to the platform that supplied the firmware.
# Ambiguity is expected for shared platform codes and for platforms without parseable dates.
assert {CAR.GENESIS_G70, CAR.GENESIS_G70_2020} <= platforms_with_shared_codes

View File

@@ -0,0 +1,53 @@
import pytest
from iqdbc.car import gen_empty_fingerprint, structs
from iqdbc.car.hyundai.interface import CarInterface
from iqdbc.car.hyundai.values import CAR, HyundaiFlagsIQ, HyundaiSafetyFlagsIQ
@pytest.mark.parametrize("candidate", list(CAR), ids=lambda candidate: candidate.value)
@pytest.mark.parametrize("alpha_long", (False, True), ids=("stock_long", "openpilot_long"))
def test_all_platform_state_controller_and_radar(candidate, alpha_long, monkeypatch, tmp_path):
"""Every declared HKG platform must initialize and execute one complete interface cycle."""
monkeypatch.setenv("PARAMS_ROOT", str(tmp_path / candidate.value))
fingerprint = gen_empty_fingerprint()
cp = CarInterface.get_params(candidate, fingerprint, [], alpha_long, False, False)
cp_iq = CarInterface.get_params_iq(cp, candidate, fingerprint, [], alpha_long, False, False)
interface = CarInterface(cp, cp_iq)
state, state_iq = interface.update([])
actuators, can_sends = interface.apply(structs.CarControl().as_reader(), structs.IQCarControl())
radar = interface.RadarInterface(cp, cp_iq)
radar_result = radar.update([])
assert state.vEgo == 0.0
assert state_iq is not None
assert actuators is not None
assert isinstance(can_sends, list)
assert radar_result is None
def test_parameter_defaults_do_not_require_persisted_fingerprint(monkeypatch, tmp_path):
"""A clean installation must not crash when carrotpilot-specific Params have not been written yet."""
monkeypatch.setenv("PARAMS_ROOT", str(tmp_path))
fingerprint = gen_empty_fingerprint()
cp = CarInterface.get_params(CAR.HYUNDAI_SONATA, fingerprint, [], False, False, False)
cp_iq = CarInterface.get_params_iq(cp, CAR.HYUNDAI_SONATA, fingerprint, [], False, False, False)
interface = CarInterface(cp, cp_iq)
state, _ = interface.update([])
assert state.vEgo == 0.0
def test_classic_lfa_button_capability_survives_iq_module_removal(monkeypatch, tmp_path):
monkeypatch.setenv("PARAMS_ROOT", str(tmp_path))
fingerprint = gen_empty_fingerprint()
fingerprint[0][0x391] = 8
cp = CarInterface.get_params(CAR.HYUNDAI_SONATA, fingerprint, [], False, False, False)
cp_iq = CarInterface.get_params_iq(cp, CAR.HYUNDAI_SONATA, fingerprint, [], False, False, False)
assert cp_iq.flags & HyundaiFlagsIQ.HAS_LFA_BUTTON
assert cp_iq.iqSafetyFlags & HyundaiSafetyFlagsIQ.HAS_LDA_BUTTON

View File

@@ -0,0 +1,326 @@
import math
import pytest
from iqdbc.can import CANParser
from iqdbc.car import Bus, structs
import iqdbc.car.hyundai.hyundaicanfd as hyundaicanfd
import iqdbc.car.hyundai.radar_interface as radar_interface_module
from iqdbc.car.hyundai.radar_interface import (
CORNER_OBJECT_STABLE_TRACK_ID_START,
RADAR_MSG_COUNT3,
RADAR_MSG_COUNT4,
RADAR_START_ADDR_CANFD3,
CornerObjectTrackIdManager,
RadarInterface,
corner_object_position_valid,
)
from iqdbc.car.hyundai.values import CAR, HyundaiExtFlags, HyundaiFlags
class TestDensoRadar:
@staticmethod
def parse(addr, dat):
name = f"RADAR_TRACK_{addr:x}"
parser = CANParser("hyundai_kia_denso_front_radar_generated", [(name, 20)], 1)
parser.update([0, [(addr, bytes.fromhex(dat), 1)]])
return parser.vl[name]
def test_active_track_signals(self):
# Person walking toward the parked car, left of the camera center.
track = self.parse(0x503, "bc047efcc1fe8b00")
assert track["LONG_DIST"] == pytest.approx(7.1875)
assert track["LAT_DIST"] == pytest.approx(-1.625)
assert track["REL_SPEED"] == pytest.approx(-0.734375)
assert track["OBJECT_STATE"] == 3
def test_empty_track(self):
track = self.parse(0x507, "53fff80000000081")
assert track["LONG_DIST"] == pytest.approx(409.55)
assert track["LAT_DIST"] == 0
assert track["REL_SPEED"] == 0
assert track["OBJECT_STATE"] == 0
def test_long_range_lateral_distance(self):
# Real driving sample: treating the signed field as -12 degrees would put
# this target about 34 m sideways at 161 m. It is instead -3.0 m lateral.
track = self.parse(0x506, "b664eafa00cd230b")
assert track["LONG_DIST"] == pytest.approx(161.4625)
assert track["LAT_DIST"] == pytest.approx(-3.0)
assert track["OBJECT_STATE"] == 3
def test_parser_selection_and_point_conversion(self, monkeypatch):
class FakeParams:
def get_int(self, key):
return 1 if key == "EnableRadarTracks" else 0
monkeypatch.setattr(radar_interface_module, "Params", FakeParams)
cp = structs.CarParams()
cp.carFingerprint = CAR.KIA_SORENTO
cp.flags = 0
cp.extFlags = HyundaiExtFlags.RADAR_GROUP4.value
cp.radarUnavailable = False
cp.safetyConfigs = [structs.CarParams.SafetyConfig()]
radar_interface = RadarInterface(cp)
assert radar_interface.radar_group4
assert RADAR_MSG_COUNT4 == 8
assert radar_interface.radar_msg_count == RADAR_MSG_COUNT4
assert radar_interface.trigger_msg_tracks == 0x507
active_dat = bytes.fromhex("bc047efcc1fe8b00")
empty_dat = bytes.fromhex("bcfff80000000081")
packets = [(addr, active_dat if addr == 0x503 else empty_dat, 1) for addr in range(0x500, 0x508)]
radar_data = radar_interface.update([0, packets])
point = next(point for point in radar_data.points if point.trackId == 35)
assert point.measured
assert point.dRel == pytest.approx(7.1875)
assert point.yRel == pytest.approx(1.625)
assert point.vRel == pytest.approx(-0.734375)
assert math.isnan(point.aRel)
# EN: Confirm that the long-range sample survives the filter and converts
# radar-left-negative to openpilot-left-positive coordinates.
# KO: 장거리 샘플의 필터 통과와 레이더 좌측 음수 좌표가 openpilot 좌측
# 양수 좌표로 변환되는지 확인함.
long_range_dat = bytes.fromhex("b664eafa00cd230b")
packets = [(addr, long_range_dat if addr == 0x506 else empty_dat, 1) for addr in range(0x500, 0x508)]
radar_data = radar_interface.update([0, packets])
point = next(point for point in radar_data.points if point.trackId == 38)
assert point.dRel == pytest.approx(161.4625)
assert point.yRel == pytest.approx(3.0)
# EN: A state-0 raw detection must not enter a stable tracked-object slot.
# KO: 상태 0인 raw detection이 안정적인 추적 객체 슬롯에 들어오지 않음을 확인함.
raw_detection = bytes.fromhex("d702f4fc200000e4")
packets = [(addr, raw_detection if addr == 0x503 else empty_dat, 1) for addr in range(0x500, 0x508)]
radar_data = radar_interface.update([0, packets])
assert not radar_data.points
# EN: A real confirmed track beyond the former 205 m limit remains valid.
# KO: 기존 205m 상한을 넘는 실제 확정 트랙도 유효하게 유지됨.
confirmed_213m_track = bytes.fromhex("35854c0780f163e0")
packets = [(addr, confirmed_213m_track if addr == 0x503 else empty_dat, 1) for addr in range(0x500, 0x508)]
radar_data = radar_interface.update([0, packets])
point = next(point for point in radar_data.points if point.trackId == 35)
assert point.dRel == pytest.approx(213.275)
assert point.yRel == pytest.approx(-3.75)
# EN: The 325 m boundary is rejected, leaving ample separation from the
# 409.55 m empty-slot sentinel.
# KO: 325m 경계값을 제외해 409.55m 빈 슬롯 값과 충분한 간격을 확보함.
boundary_track = bytes.fromhex("bccb200000000300")
packets = [(addr, boundary_track if addr == 0x503 else empty_dat, 1) for addr in range(0x500, 0x508)]
radar_data = radar_interface.update([0, packets])
assert not radar_data.points
# EN: The wider profile keeps a real stable track at 4.875 m, covering more
# of the outer adjacent lane than the conservative 4.5 m profile.
# KO: 넓어진 필터에서 4.875m의 실제 안정 트랙을 유지해 보수적인 4.5m
# 설정보다 바깥쪽 인접 차선을 더 넓게 포함함.
outer_lane_track = bytes.fromhex("d80b66f640000300")
packets = [(addr, outer_lane_track if addr == 0x503 else empty_dat, 1) for addr in range(0x500, 0x508)]
radar_data = radar_interface.update([0, packets])
point = next(point for point in radar_data.points if point.trackId == 35)
assert point.yRel == pytest.approx(4.875)
# EN: Tracks beyond the widened envelope are rejected as roadside clutter;
# this payload differs only in lateral distance (-7.0 m).
# KO: 넓어진 범위를 벗어난 트랙은 도로변 잡음으로 제외함. 이 payload는
# 횡방향 거리(-7.0m)만 다름.
far_side_reflection = bytes.fromhex("d80b66f200000300")
packets = [(addr, far_side_reflection if addr == 0x503 else empty_dat, 1) for addr in range(0x500, 0x508)]
radar_data = radar_interface.update([0, packets])
assert not radar_data.points
class TestRadarGroup3:
@staticmethod
def parse(addr, dat):
name = f"RADAR_TRACK_{addr:x}"
parser = CANParser("hyundai_canfd_radar_generated", [(name, 20)], 1)
parser.update([0, [(addr, bytes.fromhex(dat), 1)]])
return parser.vl[name]
def test_group3_active_track(self):
track = self.parse(0x406, "e1043b0f02590e692a227e16f80fe00f28fcc753a20a0000")
assert track["OBJECT_LENGTH"] == pytest.approx(4.4)
assert track["LONG_DIST"] == pytest.approx(55.4)
assert track["LAT_DIST"] == pytest.approx(-3.0)
assert track["REL_SPEED"] == pytest.approx(4.4)
def test_group3_empty_track(self):
track = self.parse(0x407, "c03d3b0000000000ff0700000000000000d0020000000000")
assert track["OBJECT_LENGTH"] == 0
assert track["LONG_DIST"] == pytest.approx(204.7)
assert track["LAT_DIST"] == 0
assert track["REL_SPEED"] == 0
def test_group3_parser_selection(self, monkeypatch):
class FakeParams:
def get_int(self, key):
return 1 if key == "EnableRadarTracks" else 0
monkeypatch.setattr(radar_interface_module, "Params", FakeParams)
monkeypatch.setattr(hyundaicanfd, "Params", FakeParams)
cp = structs.CarParams()
cp.carFingerprint = next(car for car, dbc in radar_interface_module.DBC.items() if "hyundai_canfd" in dbc[Bus.pt])
cp.flags = HyundaiFlags.CANFD.value
cp.extFlags = HyundaiExtFlags.RADAR_GROUP3.value
cp.radarUnavailable = False
cp.safetyConfigs = [structs.CarParams.SafetyConfig()]
radar_interface = RadarInterface(cp)
assert radar_interface.radar_group3
assert radar_interface.radar_start_addr == RADAR_START_ADDR_CANFD3
assert radar_interface.radar_msg_count == RADAR_MSG_COUNT3
assert radar_interface.trigger_msg_tracks == 0x41D
active_dat = bytes.fromhex("e1043b0f02590e692a227e16f80fe00f28fcc753a20a0000")
empty_dat = bytes.fromhex("c03d3b0000000000ff0700000000000000d0020000000000")
packets = [(addr, active_dat if addr == 0x406 else empty_dat, 1) for addr in range(0x400, 0x41E)]
radar_data = radar_interface.update([0, packets])
point = next(point for point in radar_data.points if point.trackId == 38)
assert point.measured
assert point.dRel == pytest.approx(53.1)
assert point.yRel == pytest.approx(-3.0)
assert point.vRel == pytest.approx(4.4)
class TestCornerRadarObjectIdentity:
@staticmethod
def set_bits(data, start, size, value):
for offset in range(size):
bit = start + offset
data[bit // 8] |= ((value >> offset) & 1) << (bit % 8)
@pytest.mark.parametrize(
"dbc,msg_name,addr,age_signal,id_signal,age_start,id_start",
(
("hyundai_canfd_corner_radar_180_generated", "CORNER_RADAR_180_OBJECTS_180", 0x180,
"SLOT1_AGE", "SLOT1_OBJECT_ID", 32, 44),
("hyundai_canfd_corner_radar_235_generated", "CORNER_RADAR_235_OBJECTS_235", 0x235,
"OBJ_AGE", "OBJ_OBJECT_ID", 32, 44),
),
)
def test_object_identity_signals(self, dbc, msg_name, addr, age_signal, id_signal, age_start, id_start):
data = bytearray(32)
self.set_bits(data, age_start, 8, 23)
self.set_bits(data, id_start, 7, 46)
parser = CANParser(dbc, [(msg_name, 33)], 1)
parser.update([0, [(addr, bytes(data), 1)]])
assert parser.vl[msg_name][age_signal] == 23
assert parser.vl[msg_name][id_signal] == 46
def test_track_id_survives_slot_move_and_resets_with_age(self):
manager = CornerObjectTrackIdManager()
first_id = manager.get_track_id("corner180", object_id=108, age=240)
assert first_id == CORNER_OBJECT_STABLE_TRACK_ID_START
assert manager.get_track_id("corner180", object_id=108, age=241) == first_id
assert manager.get_track_id("corner235", object_id=108, age=241) != first_id
assert manager.get_track_id("corner180", object_id=108, age=2) != first_id
def test_clipped_side_object_position_is_valid(self):
assert corner_object_position_valid(0.0, 2.8)
assert corner_object_position_valid(25.0, 0.2)
assert not corner_object_position_valid(0.0, 0.0)
assert not corner_object_position_valid(0.0, 5.0)
class TestCornerRadar430CandidateFilter:
@staticmethod
def slot_word(distance_raw, meta13=0, b2=10, b3=2):
return distance_raw | (meta13 << 13) | (b2 << 16) | (b3 << 24)
@classmethod
def message(cls, slots):
words = [0x010d1f40] * 7
for slot, word in slots.items():
words[slot - 1] = word
dat = bytearray(32)
for idx, word in enumerate(words):
dat[4 + idx * 4:8 + idx * 4] = int(word).to_bytes(4, "little")
return bytes(dat)
@staticmethod
def build_interface(monkeypatch):
class FakeParams:
def get_int(self, key):
return 1 if key == "EnableCornerRadar" else 0
monkeypatch.setattr(radar_interface_module, "Params", FakeParams)
monkeypatch.setattr(hyundaicanfd, "Params", FakeParams)
cp = structs.CarParams()
cp.carFingerprint = next(car for car, dbc in radar_interface_module.DBC.items() if "hyundai_canfd" in dbc[Bus.pt])
cp.flags = HyundaiFlags.CANFD.value
cp.extFlags = HyundaiExtFlags.CORNER_RADAR_OBJECTS_430.value
cp.radarUnavailable = True
cp.safetyConfigs = [structs.CarParams.SafetyConfig()]
return RadarInterface(cp)
@staticmethod
def update_frames(radar_interface, packets, frames=5):
radar_data = None
for _ in range(frames):
radar_data = radar_interface.update([0, packets])
return radar_data
def test_430_promotes_supported_neighbor_bins(self, monkeypatch):
radar_interface = self.build_interface(monkeypatch)
empty = self.message({})
supported_bins = self.message({
6: self.slot_word(1000),
7: self.slot_word(1004),
})
packets = [(addr, supported_bins if addr == 0x436 else empty, 1) for addr in range(0x430, 0x438)]
packets += [(addr, empty, 1) for addr in range(0x440, 0x448)]
radar_data = self.update_frames(radar_interface, packets)
points = {point.trackId: point for point in radar_data.points}
assert points[300].measured
assert points[300].dRel == pytest.approx(50.1)
assert points[300].yRel == pytest.approx(2.0)
assert points[300].yvRel == 0.0
def test_430_expires_noncenter_inward_yvrel(self, monkeypatch):
radar_interface = self.build_interface(monkeypatch)
empty = self.message({})
frame_defs = (
(0x431, 4, 5),
(0x433, 3, 4),
(0x435, 2, 3),
(0x430, 5, 6),
(0x432, 4, 5),
(0x434, 3, 4),
(0x436, 2, 3),
)
radar_data = None
for addr, first_slot, second_slot in frame_defs:
msg = self.message({
first_slot: self.slot_word(1000),
second_slot: self.slot_word(1004),
})
packets = [(a, msg if a == addr else empty, 1) for a in range(0x430, 0x438)]
packets += [(a, empty, 1) for a in range(0x440, 0x448)]
radar_data = radar_interface.update([0, packets])
radar_data = self.update_frames(radar_interface, packets, frames=3)
points = {point.trackId: point for point in radar_data.points}
assert points[300].measured
assert points[300].yRel == pytest.approx(2.0)
assert points[300].yvRel == 0.0