mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-18 15:52:09 +08:00
Add Ioniq 6 OEM rear-BSM UDS probe script
Standalone diagnostic (run on device over SSH, car IGN-ON/engine-off/ parked, openpilot stopped) that sweeps UDS CommunicationControl message classes on the ADAS DRV ECU (0x730, ECAN) to find a combo that silences SCC actuation (0x1A0) while leaving the rear-radar BSM verdict (0x1BA) transmitting. A winner would restore OEM from-behind, lane-filtered blind spot monitoring under openpilot long via a one-byte change to the disable command. Does not alter any openpilot code path; only runs when invoked. Restores the ECU after each combo and sets the panda to SILENT on exit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Ioniq 6 ADAS-DRV BSM/SCC message-class probe.
|
||||
|
||||
GOAL: find a CommunicationControl (UDS 0x28) variant that silences the ADAS DRV
|
||||
ECU's SCC actuation (0x1A0) so openpilot long can take over, while LEAVING the
|
||||
rear-radar BSM verdict (0x1BA, BLINDSPOTS_REAR_CORNERS) still transmitting — which
|
||||
would give us OEM-grade, from-behind, lane-filtered blind-spot monitoring for free.
|
||||
|
||||
Background (from on-car UDS work, not LLM guesswork):
|
||||
- ADAS DRV ECU phys addr 0x730 on ECAN (bus 1), rx resp 0x738. Ioniq 6 is
|
||||
LKA-steering so ECAN = bus 1 (confirmed: hyundaicanfd.CanBus, disable_ecu path).
|
||||
- Today's disable sends 0x28 with message-type NORMAL (0x01), which takes down
|
||||
ALL normal-class TX at once: SCC 0x1A0 *and* BSM 0x1BA together.
|
||||
- The ECU rejects with 7F 28 22 (conditionsNotCorrect), NOT 7F 27 -> it's a
|
||||
STATE/SEQUENCE gate, not a SecurityAccess key. So this must run in IGN-ON,
|
||||
engine OFF, BEFORE the car enters READY (same precondition as the working
|
||||
disable). See Disable-ECU notes.
|
||||
|
||||
WHAT THIS DOES (read-mostly, reversible):
|
||||
For each candidate CommunicationControl (control-type x message-type) combo:
|
||||
1. Snapshot which of {0x1A0, 0x1BA} are live on the bus (baseline).
|
||||
2. Enter extended diagnostic session (10 03).
|
||||
3. Send the CommunicationControl combo, log the raw response.
|
||||
4. Sample the bus ~2 s; report whether 0x1A0 and 0x1BA are still transmitting.
|
||||
5. RESTORE: send ENABLE_RX_ENABLE_TX so nothing stays disabled between combos.
|
||||
The winning combo is one where 0x1A0 stops but 0x1BA keeps flowing.
|
||||
|
||||
SAFETY / PRECONDITIONS:
|
||||
* Car in IGN-ON (start button, NO brake), engine off, PARKED. Do not drive.
|
||||
* Stop openpilot first so it doesn't fight for the panda: (on device)
|
||||
tmux kill-session -t comma || sudo systemctl stop comma || pkill -f manager
|
||||
* This talks straight to the panda; on exit it restores the ECU and sets the
|
||||
panda back to SILENT so it won't transmit on its own.
|
||||
* It DOES momentarily disable SCC/AEB during each combo. Engine off + parked =
|
||||
no motion risk. Everything is re-enabled after each step and on exit.
|
||||
|
||||
USAGE (on the comma device over SSH):
|
||||
cd /data/openpilot && python scripts/ioniq6_bsm_uds_probe.py
|
||||
Logs to stdout and to /data/ioniq6_bsm_uds_probe.log
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import datetime
|
||||
|
||||
# ---- config ---------------------------------------------------------------
|
||||
ECAN_BUS = 1 # Ioniq 6 (LKA steering) ECAN
|
||||
DRV_TX = 0x730 # ADAS DRV physical request addr
|
||||
DRV_RX = 0x738 # response addr (tx + 8)
|
||||
SCC_ADDR = 0x1A0 # SCC actuation the CCU consumes (must go silent for OP long)
|
||||
BSM_ADDR = 0x1BA # BLINDSPOTS_REAR_CORNERS (the OEM rear BSM we want to keep)
|
||||
FRONT_BSM_ADDR = 0x36A # front-corner side-detect, for reference
|
||||
SAMPLE_S = 2.0 # bus sample window after each command
|
||||
LOGFILE = "/data/ioniq6_bsm_uds_probe.log"
|
||||
|
||||
# UDS bytes
|
||||
EXT_DIAG = bytes([0x10, 0x03])
|
||||
COMM_CONTROL = 0x28
|
||||
# control types (ISO 14229 §CommunicationControl subfunction)
|
||||
CT_ENABLE_RX_ENABLE_TX = 0x00
|
||||
CT_ENABLE_RX_DISABLE_TX = 0x01
|
||||
CT_DISABLE_RX_ENABLE_TX = 0x02
|
||||
CT_DISABLE_RX_DISABLE_TX = 0x03
|
||||
# message types (communicationType byte, bit0=normal, bit1=network-management)
|
||||
MT_NORMAL = 0x01
|
||||
MT_NM = 0x02
|
||||
MT_NM_AND_NORMAL = 0x03
|
||||
|
||||
# Combos to sweep. Each: (label, control_type, message_type).
|
||||
# The interesting hypothesis is that BSM (0x1BA) rides a different class than SCC,
|
||||
# so disabling only NORMAL tx (what we do today) vs NM vs both changes what dies.
|
||||
COMBOS = [
|
||||
("disable TX, NORMAL (current)", CT_ENABLE_RX_DISABLE_TX, MT_NORMAL),
|
||||
("disable TX, NM only", CT_ENABLE_RX_DISABLE_TX, MT_NM),
|
||||
("disable TX, NM+NORMAL", CT_ENABLE_RX_DISABLE_TX, MT_NM_AND_NORMAL),
|
||||
("disable RX+TX, NORMAL", CT_DISABLE_RX_DISABLE_TX, MT_NORMAL),
|
||||
("disable RX+TX, NM only", CT_DISABLE_RX_DISABLE_TX, MT_NM),
|
||||
]
|
||||
|
||||
|
||||
def log(msg):
|
||||
line = f"[{datetime.datetime.now().strftime('%H:%M:%S.%f')[:-3]}] {msg}"
|
||||
print(line, flush=True)
|
||||
try:
|
||||
with open(LOGFILE, "a") as f:
|
||||
f.write(line + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def isotp_single_frame(payload: bytes) -> bytes:
|
||||
"""Build a classic ISO-TP single frame (fits all our short requests).
|
||||
MUST be exactly 8 bytes: the panda's ELM327 tx hook rejects any diagnostic
|
||||
frame where len != 8 (opendbc/safety/modes/elm327.h), so all requests are
|
||||
padded to 8. All our UDS requests are <= 3 bytes, so a single frame is enough
|
||||
and no multi-frame/flow-control handling is needed."""
|
||||
assert len(payload) <= 7, "request too long for a single frame"
|
||||
return bytes([len(payload)]) + payload + b"\x00" * (7 - len(payload))
|
||||
|
||||
|
||||
def parse_isotp_response(dat: bytes) -> bytes:
|
||||
"""Strip the ISO-TP single-frame length byte; return the UDS payload."""
|
||||
if not dat:
|
||||
return b""
|
||||
n = dat[0] & 0x0F
|
||||
return bytes(dat[1:1 + n])
|
||||
|
||||
|
||||
def sample_bus(panda, want_addrs, duration):
|
||||
"""Sample the bus for `duration` s; return set of addrs seen on ECAN and a
|
||||
per-addr frame count."""
|
||||
seen = {a: 0 for a in want_addrs}
|
||||
panda.can_clear(0xFFFF)
|
||||
t_end = time.monotonic() + duration
|
||||
while time.monotonic() < t_end:
|
||||
for addr, dat, src in panda.can_recv():
|
||||
if src == ECAN_BUS and addr in seen:
|
||||
seen[addr] += 1
|
||||
return seen
|
||||
|
||||
|
||||
def send_uds(panda, payload, wait=0.3):
|
||||
"""Send one UDS request as an ISO-TP single frame on ECAN, wait for a response
|
||||
from DRV_RX. Returns the decoded UDS payload bytes (b'' if none)."""
|
||||
panda.can_clear(0xFFFF)
|
||||
frame = isotp_single_frame(payload)
|
||||
# ECAN is CAN-FD; send as FD frame
|
||||
panda.can_send(DRV_TX, frame, ECAN_BUS, fd=True)
|
||||
t_end = time.monotonic() + wait
|
||||
while time.monotonic() < t_end:
|
||||
for addr, dat, src in panda.can_recv():
|
||||
if src == ECAN_BUS and addr == DRV_RX:
|
||||
return parse_isotp_response(bytes(dat))
|
||||
return b""
|
||||
|
||||
|
||||
def describe_resp(resp: bytes) -> str:
|
||||
if not resp:
|
||||
return "no response"
|
||||
if resp[0] == 0x7F and len(resp) >= 3:
|
||||
nrc = resp[2]
|
||||
names = {0x12: "subFunctionNotSupported", 0x13: "invalidFormat",
|
||||
0x22: "conditionsNotCorrect (need IGN-ON, not READY)",
|
||||
0x31: "requestOutOfRange", 0x33: "securityAccessDenied"}
|
||||
return f"NEGATIVE 7F {resp[1]:02X} {nrc:02X} = {names.get(nrc, 'unknown')}"
|
||||
if resp[0] == 0x68:
|
||||
return f"POSITIVE 68 {resp[1]:02X} (CommunicationControl accepted)"
|
||||
if resp[0] == 0x50:
|
||||
return f"POSITIVE 50 {resp[1]:02X} (diag session)"
|
||||
return "0x" + resp.hex()
|
||||
|
||||
|
||||
def enter_session(panda) -> bool:
|
||||
resp = send_uds(panda, EXT_DIAG)
|
||||
log(f" diag session (10 03) -> {describe_resp(resp)}")
|
||||
return bool(resp) and resp[0] == 0x50
|
||||
|
||||
|
||||
def comm_control(panda, control_type, message_type):
|
||||
return send_uds(panda, bytes([COMM_CONTROL, control_type, message_type]))
|
||||
|
||||
|
||||
def restore(panda):
|
||||
"""Best-effort: re-enable all comms on the DRV ECU."""
|
||||
enter_session(panda)
|
||||
resp = comm_control(panda, CT_ENABLE_RX_ENABLE_TX, MT_NM_AND_NORMAL)
|
||||
log(f" RESTORE enable rx+tx -> {describe_resp(resp)}")
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
from panda import Panda
|
||||
except ImportError:
|
||||
# device path
|
||||
sys.path.insert(0, "/data/openpilot")
|
||||
from panda import Panda
|
||||
from opendbc.car.structs import CarParams
|
||||
|
||||
SAFETY_ELM327 = int(CarParams.SafetyModel.elm327) # 3
|
||||
SAFETY_SILENT = int(CarParams.SafetyModel.silent) # 0
|
||||
|
||||
log("=" * 70)
|
||||
log("Ioniq 6 BSM/SCC message-class probe starting")
|
||||
log("PRECONDITION: IGN-ON, engine OFF, parked, openpilot stopped.")
|
||||
log("=" * 70)
|
||||
|
||||
panda = Panda()
|
||||
# ELM327 with param=1 is exactly the mode openpilot uses for fingerprint/UDS
|
||||
# diagnostic traffic (see selfdrive/pandad/panda_safety.cc). It permits our
|
||||
# diagnostic tx on the requested bus without letting the panda emit car control.
|
||||
panda.set_safety_mode(SAFETY_ELM327, 1)
|
||||
log(f"panda connected: {panda.get_serial()[0]}, safety=ELM327")
|
||||
|
||||
# tester-present keepalive is not needed for the short window per combo; each
|
||||
# combo re-enters the session fresh.
|
||||
results = []
|
||||
try:
|
||||
base = sample_bus(panda, [SCC_ADDR, BSM_ADDR, FRONT_BSM_ADDR], SAMPLE_S)
|
||||
log(f"BASELINE (nothing disabled): SCC 0x1A0={base[SCC_ADDR]} "
|
||||
f"BSM 0x1BA={base[BSM_ADDR]} frontSD 0x36A={base[FRONT_BSM_ADDR]} frames/{SAMPLE_S:.0f}s")
|
||||
if base[SCC_ADDR] == 0 and base[BSM_ADDR] == 0:
|
||||
log("WARNING: neither SCC nor BSM seen at baseline. Wrong bus, or car not "
|
||||
"in IGN-ON, or openpilot still running and already disabled the ECU.")
|
||||
|
||||
for label, ct, mt in COMBOS:
|
||||
log("-" * 70)
|
||||
log(f"COMBO: {label} (0x28 {ct:02X} {mt:02X})")
|
||||
if not enter_session(panda):
|
||||
log(" could not enter diag session; skipping combo")
|
||||
restore(panda)
|
||||
continue
|
||||
time.sleep(0.05)
|
||||
resp = comm_control(panda, ct, mt)
|
||||
log(f" comm control -> {describe_resp(resp)}")
|
||||
after = sample_bus(panda, [SCC_ADDR, BSM_ADDR, FRONT_BSM_ADDR], SAMPLE_S)
|
||||
scc_off = after[SCC_ADDR] == 0
|
||||
bsm_on = after[BSM_ADDR] > 0
|
||||
verdict = "*** WINNER: SCC off, BSM alive ***" if (scc_off and bsm_on) else (
|
||||
"SCC off, BSM also off" if scc_off else "SCC still transmitting")
|
||||
log(f" AFTER: SCC 0x1A0={after[SCC_ADDR]} BSM 0x1BA={after[BSM_ADDR]} "
|
||||
f"frontSD 0x36A={after[FRONT_BSM_ADDR]} -> {verdict}")
|
||||
results.append((label, ct, mt, after[SCC_ADDR], after[BSM_ADDR], scc_off and bsm_on))
|
||||
restore(panda)
|
||||
time.sleep(0.3)
|
||||
|
||||
log("=" * 70)
|
||||
log("SUMMARY")
|
||||
for label, ct, mt, scc, bsm, win in results:
|
||||
flag = "WIN" if win else " "
|
||||
log(f" {flag} {label:34s} 0x28 {ct:02X} {mt:02X} -> SCC={scc:4d} BSM={bsm:4d}")
|
||||
winners = [r for r in results if r[5]]
|
||||
if winners:
|
||||
log("A winning combo exists: set the disable message-type to it in interface.py "
|
||||
"and 0x1BA (real rear BSM) will keep flowing with OP long.")
|
||||
else:
|
||||
log("No combo kept BSM alive while killing SCC. SCC and BSM share a class on this "
|
||||
"ECU -> OEM rear BSM via UDS is not separable; hardware rear-radar tap is the "
|
||||
"remaining path.")
|
||||
finally:
|
||||
restore(panda)
|
||||
panda.set_safety_mode(SAFETY_SILENT)
|
||||
log("panda set back to SILENT. Probe done. Reboot the car ign to fully reset the ECU.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,41 @@
|
||||
# Ioniq 6 OEM rear-BSM probe
|
||||
|
||||
Finds whether we can keep the OEM rear blind-spot verdict (`0x1BA`,
|
||||
`BLINDSPOTS_REAR_CORNERS`) alive while still silencing SCC (`0x1A0`) so openpilot
|
||||
long takes over. If yes → from-behind, lane-filtered OEM BSM comes back for free
|
||||
with a one-byte change to the ECU-disable command.
|
||||
|
||||
## What it does
|
||||
Sweeps UDS CommunicationControl (`0x28`) control-type × message-type combos on the
|
||||
ADAS DRV ECU (`0x730`, ECAN/bus 1). For each combo it samples the bus and reports
|
||||
whether `0x1A0` (SCC) stopped and `0x1BA` (BSM) kept transmitting. Restores the ECU
|
||||
after every combo and on exit.
|
||||
|
||||
## Preconditions (important)
|
||||
1. Car in **IGN-ON, engine OFF, parked** (start button, NO brake). Same state that
|
||||
makes the normal ECU-disable succeed. Do **not** drive.
|
||||
2. **Stop openpilot first** so it doesn't fight for the panda:
|
||||
```
|
||||
sudo systemctl stop comma # or: tmux kill-session -t comma # or: pkill -f manager
|
||||
```
|
||||
3. The probe momentarily disables SCC/AEB per combo — safe because engine is off &
|
||||
parked, and it re-enables after each step.
|
||||
|
||||
## Run
|
||||
```
|
||||
cd /data/openpilot
|
||||
python scripts/ioniq6_bsm_uds_probe.py
|
||||
```
|
||||
Watch the console and `/data/ioniq6_bsm_uds_probe.log`.
|
||||
|
||||
## Reading the result
|
||||
- `*** WINNER: SCC off, BSM alive ***` on any combo → tell me which `0x28 CT MT`
|
||||
bytes won; we set the ECU-disable `communication_control` in
|
||||
`opendbc/car/hyundai/interface.py` to that message-type, and `0x1BA` (real rear
|
||||
BSM) will keep flowing under OP long.
|
||||
- No winner → SCC and BSM share a message class on this ECU; UDS can't separate
|
||||
them. The remaining path is a hardware tap into the rear-corner-radar CAN branch.
|
||||
|
||||
## After running
|
||||
Cycle the ignition (full off → on) to make sure the ECU is back to a clean state
|
||||
before a normal drive. The script sets the panda back to SILENT on exit.
|
||||
Reference in New Issue
Block a user