1
0
forked from IQ.Lvbs/IQ.Pilot
Files
IQ.Pilot/iqdbc_repo/iqdbc/car/hyundai/tests/test_radar.py
2026-08-22 23:42:42 -05:00

327 lines
13 KiB
Python

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