diff --git a/opendbc_repo/opendbc/car/hyundai/carcontroller.py b/opendbc_repo/opendbc/car/hyundai/carcontroller.py index baa4d2810..ea4d3cd4b 100644 --- a/opendbc_repo/opendbc/car/hyundai/carcontroller.py +++ b/opendbc_repo/opendbc/car/hyundai/carcontroller.py @@ -23,6 +23,8 @@ MAX_ANGLE = 85 MAX_ANGLE_FRAMES = 89 MAX_ANGLE_CONSECUTIVE_FRAMES = 2 CANFD_BLINDSPOT_STATUS_STALE_NS = 200_000_000 +# fail-open window for the radar-track BSM gate; tracks refresh at 20Hz from card +IONIQ_6_BLINDSPOT_TRACK_STALE_NS = 1_000_000_000 CANFD_CAMERA_LEAD_STALE_NS = 300_000_000 CANFD_LEAD_MIN_DISTANCE = 0.1 CANFD_FALLBACK_LEAD_DISTANCE = 20.0 @@ -342,6 +344,8 @@ class CarController(CarControllerBase): self.long_active_ecu = self.CP.openpilotLongitudinalControl self._ioniq_6_lane_change_ui_side = None self._ioniq_6_lane_change_ui_frames = 0 + self._ioniq_6_bsm_gate_left = False + self._ioniq_6_bsm_gate_right = False self._ioniq_6_long_tuning = Ioniq6LongitudinalTuningState() self._genesis_g90_long_tuning = GenesisG90LongitudinalTuningState() self._dash_lat_disengage_blink_frame = 0 @@ -761,6 +765,22 @@ class CarController(CarControllerBase): CS.out.gasPressed, self.CP.carFingerprint)) elif not ccnc_non_hda2: can_sends.extend(hyundaicanfd.create_fca_warning_light(self.packer, self.CAN, self.frame)) + left_blindspot, right_blindspot = CS.left_blindspot_from_radar, CS.right_blindspot_from_radar + if self.CP.carFingerprint == CAR.HYUNDAI_IONIQ_6: + # The raw SIDE_DETECT_STATE bit also fires on far and two-lanes-over traffic, so gate + # the mirror/cluster spoof on a front radar track actually in the adjacent lane, then + # latch while side-detect stays asserted so the lamp holds as a passed car slides into + # the rear quarter (validated on onelane/twolanes rlogs 2026-07-16). Fails open to the + # ungated bit if the track stash goes stale (radar dead / ECU disable failed). + tracks_fresh = CS.blindspot_track_update_ts > 0 and \ + now_nanos - CS.blindspot_track_update_ts < IONIQ_6_BLINDSPOT_TRACK_STALE_NS + if tracks_fresh: + self._ioniq_6_bsm_gate_left = left_blindspot and (self._ioniq_6_bsm_gate_left or CS.left_blindspot_track_near) + self._ioniq_6_bsm_gate_right = right_blindspot and (self._ioniq_6_bsm_gate_right or CS.right_blindspot_track_near) + left_blindspot, right_blindspot = self._ioniq_6_bsm_gate_left, self._ioniq_6_bsm_gate_right + else: + self._ioniq_6_bsm_gate_left = False + self._ioniq_6_bsm_gate_right = False if self.CP.carFingerprint == CAR.HYUNDAI_IONIQ_6 and self.frame % 5 == 0: rear_stale = now_nanos - CS.blindspots_rear_corners_ts > CANFD_BLINDSPOT_STATUS_STALE_NS front_stale = now_nanos - CS.blindspots_front_corner_1_ts > CANFD_BLINDSPOT_STATUS_STALE_NS @@ -768,14 +788,14 @@ class CarController(CarControllerBase): can_sends.extend(hyundaicanfd.create_blindspot_status_messages(self.packer, self.CAN, CS.blindspots_rear_corners, CS.blindspots_front_corner_1, - CS.left_blindspot_from_radar, - CS.right_blindspot_from_radar, + left_blindspot, + right_blindspot, CC.leftBlinker, CC.rightBlinker)) if self.CP.carFingerprint == CAR.HYUNDAI_IONIQ_6 and lane_change_ui_side is None: can_sends.extend(hyundaicanfd.create_ioniq_6_cluster_blindspot_messages(self.CAN, self.frame, - CS.left_blindspot_from_radar, - CS.right_blindspot_from_radar, + left_blindspot, + right_blindspot, CC.leftBlinker, CC.rightBlinker)) if self.frame % 2 == 0: diff --git a/opendbc_repo/opendbc/car/hyundai/carstate.py b/opendbc_repo/opendbc/car/hyundai/carstate.py index aa29e673e..39e56df6a 100644 --- a/opendbc_repo/opendbc/car/hyundai/carstate.py +++ b/opendbc_repo/opendbc/car/hyundai/carstate.py @@ -136,6 +136,12 @@ class CarState(CarStateBase): self.blindspots_front_corner_1_ts = 0 self.left_blindspot_from_radar = False self.right_blindspot_from_radar = False + # Radar-track adjacency for the spoofed BSM mirror gate, stashed by card from + # RadarData (front radar tracks). ts stays 0 when never updated, which makes + # the carcontroller gate fail open to the ungated side-detect bit. + self.left_blindspot_track_near = False + self.right_blindspot_track_near = False + self.blindspot_track_update_ts = 0 # On some cars, CLU15->CF_Clu_VehicleSpeed can oscillate faster than the dash updates. Sample at 5 Hz self.cluster_speed = 0 diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index 6570f5674..d035aea5a 100644 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -36,6 +36,14 @@ REPLAY = "REPLAY" in os.environ OPENPILOT_LEAD_MIN_DISTANCE = 0.1 REDNECK_DECREASE_LOOKAHEAD_POINTS = 10 +# Adjacent-lane zone for the spoofed-BSM mirror gate (Hyundai CAN-FD): a front radar +# track counts as "car one lane over" only inside this envelope. Lat band excludes +# own-lane leads (<1.9 m) and two-lanes-over traffic (>5.5 m); thresholds validated +# on Ioniq 6 onelane/twolanes rlogs 2026-07-16. +BSM_TRACK_LONG_MAX = 15.0 +BSM_TRACK_LAT_MIN = 1.9 +BSM_TRACK_LAT_MAX = 5.5 + EventName = log.OnroadEvent.EventName # forward @@ -230,6 +238,9 @@ class Car: if can_rcv_valid and REPLAY: self.can_log_mono_time = messaging.log_from_bytes(can_strs[0]).logMonoTime + # RD is only non-None on frames with valid CAN, so can_log_mono_time is set by here + self._update_blindspot_track_state(RD) + preap_software_cruise = ( self.CP.brand == "tesla" and self.CP.carFingerprint == "TESLA_MODEL_S_PREAP" and self.CP.openpilotLongitudinalControl and not self.CP.pcmCruise @@ -377,6 +388,26 @@ class Car: self.CI.CS.openpilot_lead_distance = lead_distance self.CI.CS.openpilot_lead_rel_speed = lead_rel_speed + def _update_blindspot_track_state(self, RD: structs.RadarDataT | None) -> None: + # Stash radar-track adjacency for the Hyundai CAN-FD spoofed-BSM mirror gate + # (yRel > 0 is left). Skipped frames leave the previous stash in place; if the + # radar stops updating entirely, the timestamp goes stale and the carcontroller + # gate fails open to its ungated side-detect input. + if RD is None or any(RD.errors.to_dict().values()): + return + + left = right = False + for pt in RD.points: + if 0.0 < pt.dRel < BSM_TRACK_LONG_MAX and BSM_TRACK_LAT_MIN <= abs(pt.yRel) <= BSM_TRACK_LAT_MAX: + if pt.yRel > 0.0: + left = True + else: + right = True + + self.CI.CS.left_blindspot_track_near = left + self.CI.CS.right_blindspot_track_near = right + self.CI.CS.blindspot_track_update_ts = self.can_log_mono_time if REPLAY else int(time.monotonic() * 1e9) + def _update_redneck_cruise(self, CS: car.CarState, CC: car.CarControl) -> None: if self.redneck_cruise is None: return