diff --git a/opendbc_repo/opendbc/car/hyundai/carcontroller.py b/opendbc_repo/opendbc/car/hyundai/carcontroller.py index ea4d3cd4b..b7e360aa1 100644 --- a/opendbc_repo/opendbc/car/hyundai/carcontroller.py +++ b/opendbc_repo/opendbc/car/hyundai/carcontroller.py @@ -23,8 +23,6 @@ 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 @@ -344,8 +342,6 @@ 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 @@ -765,22 +761,9 @@ 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 + # gated + latched in carstate (_update_ioniq_6_bsm_gate): only asserts once a front + # radar track confirms the side-detect car is actually in the adjacent lane + left_blindspot, right_blindspot = CS.left_blindspot_gated, CS.right_blindspot_gated 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 diff --git a/opendbc_repo/opendbc/car/hyundai/carstate.py b/opendbc_repo/opendbc/car/hyundai/carstate.py index 39e56df6a..1ce875fa8 100644 --- a/opendbc_repo/opendbc/car/hyundai/carstate.py +++ b/opendbc_repo/opendbc/car/hyundai/carstate.py @@ -24,6 +24,13 @@ BUTTONS_DICT = {Buttons.RES_ACCEL: ButtonType.accelCruise, Buttons.SET_DECEL: Bu IONIQ_6_BLINDSPOT_RIGHT_MASK = 0x08 IONIQ_6_BLINDSPOT_LEFT_MASK = 0x10 +# BSM gate: SIDE_DETECT_STATE fires on far and two-lanes-over traffic, so a side only +# asserts after a front radar track confirms a car in the adjacent lane, then latches +# while side-detect stays on (rear-quarter hold). Frame counts assume the ~100Hz +# carstate update rate. Validated on lanes rlogs 2026-07-16. +IONIQ_6_BSM_NEAR_MEMORY_FRAMES = 100 # 1.0s: in-zone track counts toward arming this long +IONIQ_6_BSM_SD_RELEASE_FRAMES = 50 # 0.5s: side-detect must stay quiet this long to release +IONIQ_6_BSM_STASH_STALE_FRAMES = 100 # 1.0s without a radar stash update -> fail open to raw CANFD_CAMERA_LEAD_MIN_DISTANCE = 0.1 ALT_BUS_LDA_BUTTON_BURST_DEBOUNCE_NS = int(1.3e9) @@ -68,6 +75,35 @@ class CarState(CarStateBase): return "LEFT_LAMP_ALT", "RIGHT_LAMP_ALT" return "LEFT_LAMP", "RIGHT_LAMP" + def _update_ioniq_6_bsm_gate(self): + # Arm a side once side-detect and a recent adjacent-lane radar track agree, hold while + # side-detect stays asserted (front radar loses cars that slide into the rear quarter), + # release only after side-detect stays quiet for the debounce window (raw bit has + # single-frame dropouts, seen in standstillmirror rlog). Fail open to raw side-detect + # when the track stash stops updating (radar dead / ECU disable failed). + if self.blindspot_track_stash_seq != self._bsm_stash_seq_prev: + self._bsm_stash_seq_prev = self.blindspot_track_stash_seq + self._bsm_stash_stale_frames = 0 + else: + self._bsm_stash_stale_frames = min(self._bsm_stash_stale_frames + 1, IONIQ_6_BSM_STASH_STALE_FRAMES + 1) + tracks_fresh = self.blindspot_track_stash_seq > 0 and self._bsm_stash_stale_frames <= IONIQ_6_BSM_STASH_STALE_FRAMES + + for side, sd, near in (("left", self.left_blindspot_from_radar, self.left_blindspot_track_near), + ("right", self.right_blindspot_from_radar, self.right_blindspot_track_near)): + self._bsm_near_gap_frames[side] = 0 if near else min(self._bsm_near_gap_frames[side] + 1, + IONIQ_6_BSM_NEAR_MEMORY_FRAMES + 1) + self._bsm_sd_gap_frames[side] = 0 if sd else min(self._bsm_sd_gap_frames[side] + 1, + IONIQ_6_BSM_SD_RELEASE_FRAMES + 1) + if not tracks_fresh: + self._bsm_latched[side] = sd + elif sd and self._bsm_near_gap_frames[side] <= IONIQ_6_BSM_NEAR_MEMORY_FRAMES: + self._bsm_latched[side] = True + elif self._bsm_sd_gap_frames[side] > IONIQ_6_BSM_SD_RELEASE_FRAMES: + self._bsm_latched[side] = False + + self.left_blindspot_gated = self._bsm_latched["left"] + self.right_blindspot_gated = self._bsm_latched["right"] + def __init__(self, CP, FPCP): super().__init__(CP, FPCP) can_define = CANDefine(DBC[CP.carFingerprint][Bus.pt]) @@ -136,12 +172,22 @@ 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. + # Radar-track adjacency stashed by card from RadarData (front radar tracks); + # card bumps stash_seq on every radar update so staleness is clock-free. self.left_blindspot_track_near = False self.right_blindspot_track_near = False - self.blindspot_track_update_ts = 0 + self.blindspot_track_stash_seq = 0 + # Gated + latched BSM state (single source for ret.left/rightBlindspot and the + # carcontroller mirror/cluster spoofs) + self.left_blindspot_gated = False + self.right_blindspot_gated = False + self._bsm_stash_seq_prev = 0 + self._bsm_stash_stale_frames = IONIQ_6_BSM_STASH_STALE_FRAMES + 1 + self._bsm_near_gap_frames = {"left": IONIQ_6_BSM_NEAR_MEMORY_FRAMES + 1, + "right": IONIQ_6_BSM_NEAR_MEMORY_FRAMES + 1} + self._bsm_sd_gap_frames = {"left": IONIQ_6_BSM_SD_RELEASE_FRAMES + 1, + "right": IONIQ_6_BSM_SD_RELEASE_FRAMES + 1} + self._bsm_latched = {"left": False, "right": False} # On some cars, CLU15->CF_Clu_VehicleSpeed can oscillate faster than the dash updates. Sample at 5 Hz self.cluster_speed = 0 @@ -472,12 +518,16 @@ class CarState(CarStateBase): if self.CP.carFingerprint == CAR.HYUNDAI_IONIQ_6: self.left_blindspot_from_radar, self.right_blindspot_from_radar = decode_ioniq_6_blindspot_radar_state( cp.vl["BLINDSPOTS_FRONT_CORNER_2"]["SIDE_DETECT_STATE"]) + self._update_ioniq_6_bsm_gate() if self.CP.enableBsm: if self.CP.carFingerprint == CAR.HYUNDAI_IONIQ_6: + # gated side-detect: raw bit also fires on far and two-lanes-over traffic. With OP + # long the ADAS ECU is disabled so the lane-filtered BCW verdict is gone entirely; + # with stock long the BCW verdict stays primary and the gate only cleans the OR-in. ret.leftBlindspot = (bool(cp.vl["BLINDSPOTS_REAR_CORNERS"]["BCW_LtIndSta"]) or - self.left_blindspot_from_radar) + self.left_blindspot_gated) ret.rightBlindspot = (bool(cp.vl["BLINDSPOTS_REAR_CORNERS"]["BCW_RtIndSta"]) or - self.right_blindspot_from_radar) + self.right_blindspot_gated) elif self.CP.flags & HyundaiFlags.CCNC: ret.leftBlindspot = bool(cp.vl["BLINDSPOTS_REAR_CORNERS"]["BCW_LtIndSta"]) ret.rightBlindspot = bool(cp.vl["BLINDSPOTS_REAR_CORNERS"]["BCW_RtIndSta"]) diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index d035aea5a..e9d68df0c 100644 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -238,7 +238,6 @@ 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 = ( @@ -389,10 +388,9 @@ class Car: 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. + # Stash radar-track adjacency for the Hyundai CAN-FD BSM gate (yRel > 0 is left). + # Skipped frames leave the previous stash in place; the seq bump lets carstate + # detect staleness without cross-clock math and fail open to raw side-detect. if RD is None or any(RD.errors.to_dict().values()): return @@ -406,7 +404,7 @@ class Car: 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) + self.CI.CS.blindspot_track_stash_seq = getattr(self.CI.CS, "blindspot_track_stash_seq", 0) + 1 def _update_redneck_cruise(self, CS: car.CarState, CC: car.CarControl) -> None: if self.redneck_cruise is None: