Ioniq 6 BSM: gate side-detect on front-radar lane position

The raw SIDE_DETECT_STATE bit lit the mirrors and UI for cars two lanes
over and for far-ahead traffic, because it has no lane discrimination.
Gate it on a front-radar track actually in the adjacent lane (LONG<15m,
1.9<=|LAT|<=5.5m), latched while side-detect stays asserted with a 0.5s
release debounce and 1s track-near memory, computed in carstate so the
UI path, lane-change veto, and mirror/cluster spoof all consume the same
gated signal. card stashes track adjacency from RadarData; fails open to
the raw bit if the stash goes stale. Validated on onelane/twolanes/
trafficonleft/standstillmirror rlogs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
whoisdomi
2026-07-16 14:03:22 -05:00
parent 6b21ab83a4
commit 119e175978
3 changed files with 63 additions and 32 deletions
@@ -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
+56 -6
View File
@@ -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"])
+4 -6
View File
@@ -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: