327 lines
13 KiB
Python
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
|