openpilot is an open source driver assistance system. openpilot performs the functions of Automated Lane Centering and Adaptive Cruise Control for over 200 supported car makes and models.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

342 lines
13 KiB

import numpy as np
from typing import cast
from collections import defaultdict
from math import cos, sin
from dataclasses import dataclass
from opendbc.can.parser import CANParser
from opendbc.car import Bus, structs
from opendbc.car.common.conversions import Conversions as CV
from opendbc.car.ford.fordcan import CanBus
from opendbc.car.ford.values import DBC, RADAR
from opendbc.car.interfaces import RadarInterfaceBase
DELPHI_ESR_RADAR_MSGS = list(range(0x500, 0x540))
DELPHI_MRR_RADAR_START_ADDR = 0x120
DELPHI_MRR_RADAR_HEADER_ADDR = 0x174 # MRR_Header_SensorCoverage
DELPHI_MRR_RADAR_MSG_COUNT = 64
DELPHI_MRR_RADAR_MSG_COUNT_64 = 22 # 22 messages in CANFD
DELPHI_MRR_RADAR_RANGE_COVERAGE = {0: 42, 1: 164, 2: 45, 3: 175} # scan index to detection range (m)
DELPHI_MRR_MIN_LONG_RANGE_DIST = 30 # meters
DELPHI_MRR_CLUSTER_THRESHOLD = 5 # meters, lateral distance and relative velocity are weighted
@dataclass
class Cluster:
dRel: float = 0.0
yRel: float = 0.0
vRel: float = 0.0
trackId: int = 0
def cluster_points(pts_l: list[list[float]], pts2_l: list[list[float]], max_dist: float) -> list[int]:
"""
Clusters a collection of points based on another collection of points. This is useful for correlating clusters through time.
Points in pts2 not close enough to any point in pts are assigned -1.
Args:
pts_l: List of points to base the new clusters on
pts2_l: List of points to cluster using pts
max_dist: Max distance from cluster center to candidate point
Returns:
List of cluster indices for pts2 that correspond to pts
"""
if not len(pts2_l):
return []
if not len(pts_l):
return [-1] * len(pts2_l)
max_dist_sq = max_dist ** 2
pts = np.array(pts_l)
pts2 = np.array(pts2_l)
# Compute squared norms
pts_norm_sq = np.sum(pts ** 2, axis=1)
pts2_norm_sq = np.sum(pts2 ** 2, axis=1)
# Compute squared Euclidean distances using the identity
# dist_sq[i, j] = ||pts2[i]||^2 + ||pts[j]||^2 - 2 * pts2[i] . pts[j]
dist_sq = pts2_norm_sq[:, np.newaxis] + pts_norm_sq[np.newaxis, :] - 2 * np.dot(pts2, pts.T)
dist_sq = np.maximum(dist_sq, 0.0)
# Find the closest cluster for each point and assign its index
closest_clusters = np.argmin(dist_sq, axis=1)
closest_dist_sq = dist_sq[np.arange(len(pts2)), closest_clusters]
cluster_idxs = np.where(closest_dist_sq < max_dist_sq, closest_clusters, -1)
return cast(list[int], cluster_idxs.tolist())
def _create_delphi_esr_radar_can_parser(CP) -> CANParser:
msg_n = len(DELPHI_ESR_RADAR_MSGS)
messages = list(zip(DELPHI_ESR_RADAR_MSGS, [20] * msg_n, strict=True))
return CANParser(RADAR.DELPHI_ESR, messages, CanBus(CP).radar)
def _create_delphi_mrr_radar_can_parser(CP) -> CANParser:
messages = [
("MRR_Header_InformationDetections", 33),
("MRR_Header_SensorCoverage", 33),
]
for i in range(1, DELPHI_MRR_RADAR_MSG_COUNT + 1):
msg = f"MRR_Detection_{i:03d}"
messages += [(msg, 33)]
return CANParser(RADAR.DELPHI_MRR, messages, CanBus(CP).radar)
def _create_delphi_mrr_radar_can_parser_64(CP) -> CANParser:
messages = []
for i in range(1, DELPHI_MRR_RADAR_MSG_COUNT_64 + 1):
msg = f"MRR_Detection_{i:03d}"
messages += [(msg, 20)]
return CANParser(RADAR.DELPHI_MRR_64, messages, CanBus(CP).radar)
class RadarInterface(RadarInterfaceBase):
def __init__(self, CP):
super().__init__(CP)
self.points: list[list[float]] = []
self.clusters: list[Cluster] = []
self.updated_messages = set()
self.track_id = 0
self.radar = DBC[CP.carFingerprint].get(Bus.radar)
self.scan_index_invalid_cnt = 0
self.radar_unavailable_cnt = 0
self.prev_headerScanIndex = 0
if CP.radarUnavailable:
self.rcp = None
elif self.radar == RADAR.DELPHI_ESR:
self.rcp = _create_delphi_esr_radar_can_parser(CP)
self.trigger_msg = DELPHI_ESR_RADAR_MSGS[-1]
self.valid_cnt = {key: 0 for key in DELPHI_ESR_RADAR_MSGS}
elif self.radar == RADAR.DELPHI_MRR:
self.rcp = _create_delphi_mrr_radar_can_parser(CP)
self.trigger_msg = DELPHI_MRR_RADAR_HEADER_ADDR
elif self.radar == RADAR.DELPHI_MRR_64:
self.rcp = _create_delphi_mrr_radar_can_parser_64(CP)
self.trigger_msg = DELPHI_MRR_RADAR_START_ADDR + DELPHI_MRR_RADAR_MSG_COUNT_64 - 1
else:
raise ValueError(f"Unsupported radar: {self.radar}")
def update(self, can_strings):
if self.rcp is None:
return super().update(None)
vls = self.rcp.update_strings(can_strings)
self.updated_messages.update(vls)
if self.trigger_msg not in self.updated_messages:
return None
self.updated_messages.clear()
ret = structs.RadarData()
if not self.rcp.can_valid:
ret.errors.canError = True
if self.radar == RADAR.DELPHI_ESR:
self._update_delphi_esr()
elif self.radar == RADAR.DELPHI_MRR:
_update = self._update_delphi_mrr(ret)
if not _update:
return None
elif self.radar == RADAR.DELPHI_MRR_64:
_update = self._update_delphi_mrr_64(ret)
if not _update:
return None
ret.points = list(self.pts.values())
return ret
def _update_delphi_esr(self):
for ii in sorted(self.updated_messages):
cpt = self.rcp.vl[ii]
if cpt['X_Rel'] > 0.00001:
self.valid_cnt[ii] = 0 # reset counter
if cpt['X_Rel'] > 0.00001:
self.valid_cnt[ii] += 1
else:
self.valid_cnt[ii] = max(self.valid_cnt[ii] - 1, 0)
#print ii, self.valid_cnt[ii], cpt['VALID'], cpt['X_Rel'], cpt['Angle']
# radar point only valid if there have been enough valid measurements
if self.valid_cnt[ii] > 0:
if ii not in self.pts:
self.pts[ii] = structs.RadarData.RadarPoint()
self.pts[ii].trackId = self.track_id
self.track_id += 1
self.pts[ii].dRel = cpt['X_Rel'] # from front of car
self.pts[ii].yRel = cpt['X_Rel'] * cpt['Angle'] * CV.DEG_TO_RAD # in car frame's y axis, left is positive
self.pts[ii].vRel = cpt['V_Rel']
self.pts[ii].aRel = float('nan')
self.pts[ii].yvRel = float('nan')
self.pts[ii].measured = True
else:
if ii in self.pts:
del self.pts[ii]
def _update_delphi_mrr(self, ret: structs.RadarData):
headerScanIndex = int(self.rcp.vl["MRR_Header_InformationDetections"]['CAN_SCAN_INDEX']) & 0b11
# In reverse, the radar continually sends the last messages. Mark this as invalid
if (self.prev_headerScanIndex + 1) % 4 != headerScanIndex:
self.radar_unavailable_cnt += 1
else:
self.radar_unavailable_cnt = 0
self.prev_headerScanIndex = headerScanIndex
if self.radar_unavailable_cnt >= 5:
self.pts.clear()
self.points.clear()
self.clusters.clear()
ret.errors.radarUnavailableTemporary = True
return True
# Use points with Doppler coverage of +-60 m/s, reduces similar points
if headerScanIndex not in (2, 3):
return False
if DELPHI_MRR_RADAR_RANGE_COVERAGE[headerScanIndex] != int(self.rcp.vl["MRR_Header_SensorCoverage"]["CAN_RANGE_COVERAGE"]):
self.scan_index_invalid_cnt += 1
else:
self.scan_index_invalid_cnt = 0
# Rarely MRR_Header_InformationDetections can fail to send a message. The scan index is skipped in this case
if self.scan_index_invalid_cnt >= 5:
ret.errors.wrongConfig = True
for ii in range(1, DELPHI_MRR_RADAR_MSG_COUNT + 1):
msg = self.rcp.vl[f"MRR_Detection_{ii:03d}"]
# SCAN_INDEX rotates through 0..3 on each message for different measurement modes
# Indexes 0 and 2 have a max range of ~40m, 1 and 3 are ~170m (MRR_Header_SensorCoverage->CAN_RANGE_COVERAGE)
# Indexes 0 and 1 have a Doppler coverage of +-71 m/s, 2 and 3 have +-60 m/s
scanIndex = msg[f"CAN_SCAN_INDEX_2LSB_{ii:02d}"]
# Throw out old measurements. Very unlikely to happen, but is proper behavior
if scanIndex != headerScanIndex:
continue
valid = bool(msg[f"CAN_DET_VALID_LEVEL_{ii:02d}"])
# TODO: verify this is correct for CANFD as well - copied from CAN version
# Long range measurement mode is more sensitive and can detect the road surface
dist = msg[f"CAN_DET_RANGE_{ii:02d}"] # m [0|255.984]
if scanIndex in (1, 3) and dist < DELPHI_MRR_MIN_LONG_RANGE_DIST:
valid = False
if valid:
azimuth = msg[f"CAN_DET_AZIMUTH_{ii:02d}"] # rad [-3.1416|3.13964]
distRate = msg[f"CAN_DET_RANGE_RATE_{ii:02d}"] # m/s [-128|127.984]
dRel = cos(azimuth) * dist # m from front of car
yRel = -sin(azimuth) * dist # in car frame's y axis, left is positive
self.points.append([dRel, yRel * 2, distRate * 2])
# Cluster and publish using stored points once we've cycled through all 4 scan modes
if headerScanIndex != 3:
return False
self.do_clustering()
return True
def _update_delphi_mrr_64(self, ret: structs.RadarData):
# Ensure all point IDs match. Note that this message is sent first, but trigger_msg waits for the last message to come in
headerScanIndex = int(self.rcp.vl["MRR_Detection_001"]['CAN_SCAN_INDEX_2LSB_01_01'])
# TODO: Verify the below is correct for CANFD as well - copied from CAN version
# Use points with Doppler coverage of +-60 m/s, reduces similar points
if headerScanIndex in (0, 1):
return False
for ii in range(1, DELPHI_MRR_RADAR_MSG_COUNT_64 + 1):
msg = self.rcp.vl[f"MRR_Detection_{ii:03d}"]
# all messages have 6 points except the last one
maxRangeID = 6 if ii < DELPHI_MRR_RADAR_MSG_COUNT_64 else 3
for iii in range(1, maxRangeID + 1):
# SCAN_INDEX rotates through 0..3
# TODO: Verify the below is correct for CANFD as well - copied from CAN version
# Indexes 0 and 2 have a max range of ~40m, 1 and 3 are ~170m (MRR_Header_SensorCoverage->CAN_RANGE_COVERAGE)
# Indexes 0 and 1 have a Doppler coverage of +-71 m/s, 2 and 3 have +-60 m/s
scanIndex = msg[f"CAN_SCAN_INDEX_2LSB_{ii:02d}_{iii:02d}"]
# Throw out old measurements. Very unlikely to happen, but is proper behavior
if scanIndex != headerScanIndex:
continue
valid = bool(msg[f"CAN_DET_VALID_LEVEL_{ii:02d}_{iii:02d}"])
# Long range measurement mode is more sensitive and can detect the road surface
dist = msg[f"CAN_DET_RANGE_{ii:02d}_{iii:02d}"] # m [0|255.984]
if scanIndex in (1, 3) and dist < DELPHI_MRR_MIN_LONG_RANGE_DIST:
valid = False
if valid:
azimuth = msg[f"CAN_DET_AZIMUTH_{ii:02d}_{iii:02d}"] # rad [-3.1416|3.13964]
distRate = msg[f"CAN_DET_RANGE_RATE_{ii:02d}_{iii:02d}"] # m/s [-128|127.984]
dRel = cos(azimuth) * dist # m from front of car
yRel = sin(azimuth) * dist # in car frame's y axis, right is positive
self.points.append([dRel, yRel * 2, distRate * 2])
if headerScanIndex != 3:
return True
# Update the points once we've cycled through all 4 scan modes
self.do_clustering()
return True
# Do the common work for CAN and CANFD clustering and prepare the points to be used for liveTracks
def do_clustering(self):
# Cluster points from this cycle against the centroids from the previous cycle
prev_keys = [[p.dRel, p.yRel * 2, p.vRel * 2] for p in self.clusters]
labels = cluster_points(prev_keys, self.points, DELPHI_MRR_CLUSTER_THRESHOLD)
points_by_track_id = defaultdict(list)
for idx, label in enumerate(labels):
if label != -1:
points_by_track_id[self.clusters[label].trackId].append(self.points[idx])
else:
points_by_track_id[self.track_id].append(self.points[idx])
self.track_id += 1
self.clusters = []
for idx, (track_id, pts) in enumerate(points_by_track_id.items()):
dRel = [p[0] for p in pts]
min_dRel = min(dRel)
dRel = sum(dRel) / len(dRel)
yRel = [p[1] for p in pts]
yRel = sum(yRel) / len(yRel) / 2
vRel = [p[2] for p in pts]
vRel = sum(vRel) / len(vRel) / 2
# FIXME: creating capnp RadarPoint and accessing attributes are both expensive, so we store a dataclass and reuse the RadarPoint
self.clusters.append(Cluster(dRel=dRel, yRel=yRel, vRel=vRel, trackId=track_id))
if idx not in self.pts:
self.pts[idx] = structs.RadarData.RadarPoint(measured=True, aRel=float('nan'), yvRel=float('nan'))
self.pts[idx].dRel = min_dRel
self.pts[idx].yRel = yRel
self.pts[idx].vRel = vRel
self.pts[idx].trackId = track_id
for idx in range(len(points_by_track_id), len(self.pts)):
del self.pts[idx]
self.points = []