diff --git a/opendbc_repo/opendbc/car/hyundai/blended_disable_ecu.py b/opendbc_repo/opendbc/car/hyundai/blended_disable_ecu.py new file mode 100644 index 0000000000..269e210e5f --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/blended_disable_ecu.py @@ -0,0 +1,142 @@ +"""Experimental mixed longitudinal diagnostic protocol.""" +import time +from opendbc.car.carlog import carlog +from opendbc.car.isotp_parallel_query import IsoTpParallelQuery + +EXT_DIAG_REQUEST = b'\x10\x03' +EXT_DIAG_RESPONSE = b'\x50\x03' +RESET_REQUEST = b'\x11\x01' +RESET_RESPONSE = b'' +ecu_log = carlog.warning + + +def disable_ecu(can_recv, can_send, bus=0, addr=0x7d0, sub_addr=None, com_cont_req=b'\x28\x83\x01', timeout=0.1, retry=10, reset=False, + require_response=False, diag_request=EXT_DIAG_REQUEST, diag_response=EXT_DIAG_RESPONSE, response_offset=0x8): + """Silence an ECU by disabling sending and receiving messages using UDS 0x28. + The ECU will stay silent as long as openpilot keeps sending Tester Present. + Set require_response for takeovers that must fail closed unless the ECU confirms communication control. + + This is used to disable the radar in some cars. Openpilot will emulate the radar. + WARNING: THIS DISABLES AEB!""" + ecu_log(f"=== ECU DISABLE START === addr={hex(addr)}, bus={bus}") + + if reset: + try: + ecu_log("sending ECU reset before communication control...") + reset_query = IsoTpParallelQuery(can_send, can_recv, bus, [(addr, sub_addr)], [RESET_REQUEST], [RESET_RESPONSE], + response_offset=response_offset) + reset_query.get_data(timeout=timeout) + time.sleep(0.2) + except Exception as e: + ecu_log(f"reset exception: {e}") + + # Try multiple times with different approaches + for i in range(retry): + try: + # Enter extended diagnostic session + ecu_log(f"attempt {i+1}/{retry}: diag session...") + query = IsoTpParallelQuery(can_send, can_recv, bus, [(addr, sub_addr)], [diag_request], [diag_response], + response_offset=response_offset) + + for _, _ in query.get_data(timeout).items(): + ecu_log("diag session OK") + + # Small delay to let ECU fully enter diagnostic mode + time.sleep(0.05) + + # Send CC command and log the response + ecu_log("sending CC...") + cc_query = IsoTpParallelQuery(can_send, can_recv, bus, [(addr, sub_addr)], [com_cont_req], [b''], + response_offset=response_offset) + cc_response = cc_query.get_data(timeout) + + # Log what we got back + cc_success = False + cc_rejected = False + cc_nrc = None + for (_rx_addr, _), data in cc_response.items(): + ecu_log(f"CC response: {data.hex() if data else 'empty'}") + # Check for positive response (0x68 = 0x28 + 0x40) + if len(data) >= 1 and data[0] == 0x68: + ecu_log("=== ECU DISABLE CONFIRMED ===") + cc_success = True + # Check for negative response + elif len(data) >= 3 and data[0] == 0x7F: + nrc = data[2] + cc_nrc = nrc + nrc_meanings = { + 0x12: "subFunctionNotSupported", + 0x13: "incorrectMessageLengthOrInvalidFormat", + 0x22: "conditionsNotCorrect (car must be in IGN-ON, not READY)", + 0x31: "requestOutOfRange", + 0x33: "securityAccessDenied", + } + nrc_name = nrc_meanings.get(nrc, "unknown") + ecu_log(f"CC rejected: NRC 0x{nrc:02x} = {nrc_name}") + cc_rejected = True + + if cc_success: + return True + elif cc_rejected: + if reset and cc_nrc == 0x22 and i < retry - 1: + ecu_log("CC rejected with NRC 0x22 after reset; retrying...") + time.sleep(0.2) + continue + # ECU explicitly rejected - don't retry, it won't work + ecu_log("=== ECU DISABLE REJECTED ===") + return False + elif require_response: + ecu_log("CC response required but none received; retrying...") + continue + else: + # No response - consider it sent (ECU might have stopped responding) + ecu_log("=== ECU DISABLE SENT (no response) ===") + return True + + except Exception as e: + ecu_log(f"attempt {i+1} exception: {e}") + + time.sleep(0.1) + + ecu_log("=== ECU DISABLE FAILED ===") + return False + + + + +def communication_control(can_recv, can_send, *, bus, addr, control_type, reset=False, timeout=.1, retry=10): + """Matched unsuppressed SID/subfunction acknowledgment, following UdsClient. + + Original suppressed28 83/80 are preserved above as source sent-request + behavior, never reused as confirmed takeover/restoration proof. + """ + if reset: + try: + query = IsoTpParallelQuery(can_send, can_recv, bus, [(addr, None)], [RESET_REQUEST], [RESET_RESPONSE]) + query.get_data(timeout) + time.sleep(.2) + except Exception: + carlog.exception('Mixed-CAN pre-takeover reset query failed') + for _ in range(retry): + try: + diagnostic = IsoTpParallelQuery(can_send, can_recv, bus, [(addr, None)], + [EXT_DIAG_REQUEST], [EXT_DIAG_RESPONSE]) + if not diagnostic.get_data(timeout): + continue + control = IsoTpParallelQuery(can_send, can_recv, bus, [(addr, None)], + [bytes((0x28, control_type, 1))], [bytes((0x68, control_type))]) + if control.get_data(timeout): + return True + except Exception: + carlog.exception('Mixed-CAN communication-control acknowledgment failed') + return False + + +def confirm_disable_ecu(can_recv, can_send, *, bus, addr, com_cont_req, reset): + if com_cont_req != b'\x28\x03\x01' or reset is not True: + raise ValueError('Unexpected mixed-CAN takeover request') + return communication_control(can_recv, can_send, bus=bus, addr=addr, control_type=3, reset=True) + + +def restore_ecu(can_recv, can_send, *, bus, addr, timeout=.1, retry=10): + return communication_control(can_recv, can_send, bus=bus, addr=addr, control_type=0, timeout=timeout, retry=retry) diff --git a/opendbc_repo/opendbc/car/hyundai/blended_longitudinal.py b/opendbc_repo/opendbc/car/hyundai/blended_longitudinal.py new file mode 100644 index 0000000000..58c716fb0d --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/blended_longitudinal.py @@ -0,0 +1,513 @@ +"""Experimental mixed-CAN longitudinal ownership; native admission is mandatory. + +A caller must finish this transaction before publishing/construing active CP. +Shared Ioniq and generic Hyundai initialization are intentionally independent. +""" +from dataclasses import dataclass +from enum import Enum + +from opendbc.car.hyundai.values import CAR, is_blended, HyundaiFlags, HyundaiSafetyFlags + +DISABLE = b'\x28\x03\x01' # Unsuppressed takeover requires a matching68 03 acknowledgment. +ENABLE = b'\x28\x00\x01' # Unsuppressed restoration: source-equivalent sent is not confirmation. + + +class Phase(Enum): + IDLE = 'idle' + PENDING = 'pending' + OWNED = 'owned' + FAILED = 'failed' + RESTORING = 'restoring' + RESTORED = 'restored' + + +class Outcome(Enum): + OWNED = 'owned' + STOCK = 'stock' + STOCK_UNTOUCHED = 'stock_untouched' + ABORT_UNCERTAIN = 'abort_uncertain' + + +@dataclass(frozen=True) +class TakeoverResult: + outcome: Outcome + + def __bool__(self): + raise TypeError('Inspect explicit takeover outcome; uncertain ECU state is not stock admission') + + @property + def admissible(self): + return self.outcome in (Outcome.OWNED, Outcome.STOCK, Outcome.STOCK_UNTOUCHED) + + +def alpha_eligible(cp): + # Pinned original: classic non-legacy/non-unsupported eligibility plus the + # dedicated HDAII allowlist, whose only member is this exact platform. + return (is_blended(cp) and + not cp.flags & (HyundaiFlags.CANFD | HyundaiFlags.LEGACY | HyundaiFlags.UNSUPPORTED_LONGITUDINAL | HyundaiFlags.NON_SCC) and + not (cp.flags & HyundaiFlags.USE_FCA and not cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG)) + + +def candidate_from_stock(cp, *, alpha_requested, native_qualified=False): + """Only a separately qualified mixed namespace may construct a long candidate.""" + if not alpha_requested or not native_qualified or not alpha_eligible(cp) or cp.passive: + return None + if len(cp.safetyConfigs) != 1 or cp.openpilotLongitudinalControl or not cp.pcmCruise: + return None + stock_word = 0x2010 if cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG else 0x2000 + if cp.safetyConfigs[0].safetyParam != stock_word: + return None + # get_params returns a mutable builder; convert to a reader then own a copy. + candidate = cp.as_reader().as_builder() + candidate.alphaLongitudinalAvailable = True + candidate.openpilotLongitudinalControl = True + candidate.pcmCruise = False + from opendbc.car.hyundai.radar_interface import blended_track_radar + candidate.radarUnavailable = cp.radarUnavailable if blended_track_radar(cp) else True + candidate.safetyConfigs[0].safetyParam |= HyundaiSafetyFlags.LONG.value + # Modern mixed alpha namespace2004/2014; cancel/LDA semantics are local. + candidate.stopAccel = -.85 + return candidate + + +class BlendedLongitudinalOwner: + """Explicit scoped takeover/restore; never consumes another car's saved failure.""" + def __init__(self, cp, CAN, exchange, restore_exchange=None): + if not is_blended(cp): + raise ValueError('Mixed-CAN owner requires the exact platform') + self.cp = cp + self.exchange = exchange + if restore_exchange is None: + from opendbc.car.hyundai.blended_disable_ecu import restore_ecu + restore_exchange = restore_ecu + self.restore_exchange = restore_exchange + self.bus = CAN.ECAN + self.address = 0x730 if cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG else 0x7d0 + self.phase = Phase.IDLE + self.cancelled = False + self.restore_confirmed = False + self.result = None + self.published = False + self.takeover_attempted = False + + def _fallback(self): + if self.published: + return + self.cp.safetyConfigs[-1].safetyParam &= ~HyundaiSafetyFlags.LONG.value + self.cp.openpilotLongitudinalControl = False + self.cp.pcmCruise = True + + def begin(self, can_recv, can_send, *, unpublished, admission): + if self.phase is not Phase.IDLE or self.result is not None: + raise RuntimeError('Takeover is single-use') + if unpublished is not True: + raise RuntimeError('Takeover must precede active CP publication') + if admission is not True or not self.cp.openpilotLongitudinalControl: + self._fallback() + self.result = TakeoverResult(Outcome.STOCK_UNTOUCHED) + return self.result + self.phase = Phase.PENDING + self.takeover_attempted = True + try: + accepted = self.exchange(can_recv, can_send, bus=self.bus, addr=self.address, + com_cont_req=DISABLE, reset=True) + except BaseException: + self._fallback() + self.phase = Phase.FAILED + self.restore(can_recv, can_send) + raise + if accepted is True and not self.cancelled: + self.phase = Phase.OWNED + self.result = TakeoverResult(Outcome.OWNED) + return self.result + self._fallback() + self.phase = Phase.FAILED + # A missing/rejected response can still follow a partially completed disable. + restored = self.restore(can_recv, can_send) + self.result = TakeoverResult(Outcome.STOCK if restored else Outcome.ABORT_UNCERTAIN) + return self.result + + def cancel(self): + self.cancelled = True + + def restore(self, can_recv, can_send): + if self.phase is Phase.RESTORED: + return self.restore_confirmed + self.phase = Phase.RESTORING + self.result = TakeoverResult(Outcome.ABORT_UNCERTAIN) + self._fallback() + try: + accepted = self.restore_exchange(can_recv, can_send, bus=self.bus, addr=self.address) + except BaseException: + self.phase = Phase.FAILED + raise + self.restore_confirmed = accepted is True + self.result = TakeoverResult(Outcome.STOCK if self.restore_confirmed else Outcome.ABORT_UNCERTAIN) + self.phase = Phase.RESTORED if self.restore_confirmed else Phase.FAILED + return self.restore_confirmed + + def publication_cp(self): + if self.result is None or not self.result.admissible or (self.cancelled and self.phase is not Phase.RESTORED): + raise RuntimeError("Uncertain ECU ownership forbids constructor/publication continuation") + return self.cp + + def active(self): + return self.phase is Phase.OWNED and not self.cancelled and self.cp.openpilotLongitudinalControl + + +class BlendedLongitudinalController: + """Platform extension for owned diagnostic keepalive and original ACC cadence.""" + def __init__(self, owner, packer, CAN): + self.owner = owner + self.packer = packer + self.CAN = CAN + self.accel_last = 0.0 + self.normal_sent = False + + def tester_present(self, frame): + if not self.owner.active() or self.normal_sent and frame % 100: + return [] + from opendbc.car import make_tester_present_msg + return [make_tester_present_msg(self.owner.address, self.owner.bus, suppress_response=True)] + + def commands(self, frame, CC, CS, accel, stopping, set_speed): + if not self.owner.active(): + return [] + from opendbc.car.hyundai import blended_longitudinal_can as can + cp = self.owner.cp + hda2 = bool(cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG) + messages = can.create_blended_adrv_messages(self.packer, self.CAN, frame) if hda2 else [] + messages.extend(can.create_radar_aux_messages(self.packer, self.CAN, frame, hda2=hda2)) + if frame % 2 == 0: + from opendbc.car import structs + jerk = 3.0 if CC.actuators.longControlState == structs.CarControl.Actuators.LongControlState.pid else 1.0 + use_fca = cp.flags & HyundaiFlags.USE_FCA.value + if hda2: + messages.extend(can.create_acc_commands_can_canfd_blended_hda2( + self.packer, CC.enabled, accel, self.accel_last, jerk, frame // 2, CC.hudControl, + set_speed, stopping and CS.out.vEgoRaw < .1, CC.cruiseControl.override, use_fca, cp)) + self.accel_last = accel + else: + messages.extend(can.create_acc_commands_can_canfd_blended( + self.packer, CC.enabled, accel, jerk, frame // 2, CC.hudControl, set_speed, + stopping, CC.cruiseControl.override, use_fca, cp)) + return messages + + +def mixed_startup_qualified(cp, *, is_release=False, allow_marked=False): + """Exact prepared mixed owner; stock topology remains immutable.""" + from opendbc.car import structs + from opendbc.car.hyundai.hyundaicanfd import CanBus + declared = int(CAR.HYUNDAI_PALISADE_2023.config.flags) + hda2 = bool(cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG) + dynamic = int(HyundaiFlags.HAS_LDA_BUTTON | HyundaiFlags.SEND_LFA) + if hda2: + dynamic |= int(HyundaiFlags.CANFD_LKA_STEER_MSG | HyundaiFlags.USE_FCA) + if (is_release or int(cp.flags) & ~dynamic != declared or not alpha_eligible(cp) or cp.passive or cp.dashcamOnly or cp.notCar or + cp.brand != 'hyundai' or cp.steerControlType != structs.CarParams.SteerControlType.torque or + cp.alternativeExperience not in ((0, 32) if allow_marked else (0,)) or + len(cp.safetyConfigs) != 1): + return False + safety = cp.safetyConfigs[0] + word = (0x2014 if hda2 else 0x2004) if cp.openpilotLongitudinalControl and not cp.pcmCruise else (0x2010 if hda2 else 0x2000) + buses = CanBus(cp) + return (safety.safetyModel == structs.CarParams.SafetyModel.hyundai and safety.safetyParam == word and + (cp.openpilotLongitudinalControl != cp.pcmCruise) and + (buses.ECAN, buses.ACAN, buses.CAM) == ((1, 0, 2) if hda2 else (0, 1, 2))) + + +def hdai_startup_qualified(cp, *, is_release=False, allow_marked=False): + return (not cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG and + mixed_startup_qualified(cp, is_release=is_release, allow_marked=allow_marked)) + + +def startup_required(cp): + # Active mixed CP must never bypass its car-owned prepublication transaction. + return is_blended(cp) and cp.openpilotLongitudinalControl + + +class BlendedStartup: + def __init__(self, stock_cp, candidate, callbacks): + import threading + import time + from opendbc.car.hyundai.blended_disable_ecu import confirm_disable_ecu + from opendbc.car.hyundai.hyundaicanfd import CanBus + self.stock_cp = stock_cp + self.candidate_snapshot = candidate.to_dict() + self.final_snapshot = None + self.configured_ci = None + self.callbacks = callbacks + self.owner = BlendedLongitudinalOwner(candidate, CanBus(candidate), confirm_disable_ecu) + self.stop_event = threading.Event() + self.thread = None + self.clock = time.monotonic + self.started_at = None + self.fault = None + self.handed_off = False + self.closed = False + self.diagnostic_admission = None + self.source_floor_ns = 0 + self.controller = None + self.last_tester_at = None + self.enable_gesture = None + self.enable_credit = None + self.enable_generation = 0 + self.enable_button = 0 + self.native_off_witness = False + self.cruise_resume_hint = None + + def prepare(self, *, admission): + self.diagnostic_admission = admission + result = self.owner.begin(*self.callbacks, unpublished=True, admission=admission()) + if not result.admissible: + raise RuntimeError('Mixed-CAN ECU state uncertain; abort before constructors/publication') + if result.outcome in (Outcome.STOCK, Outcome.STOCK_UNTOUCHED): + return self.stock_cp + import time + self.source_floor_ns = time.clock_gettime_ns(time.CLOCK_BOOTTIME) + self.started_at = self.clock() + self._tester() # Continuity starts before parser/controller construction. + import threading + + def loop(): + while not self.stop_event.wait(.5): + self._tester() + self.thread = threading.Thread(target=loop, name='blended-startup-tester', daemon=True) + self.thread.start() + return self.owner.publication_cp() + + def _tester(self): + from opendbc.car import make_tester_present_msg + if self.started_at is None or self.fault is not None: + return + if not self.handed_off and self.clock() - self.started_at >= 60.: + self.fault = 'Startup keepalive bound exceeded' + self.owner.cancel() + self.stop_event.set() + return + try: + self.callbacks[1]([make_tester_present_msg(self.owner.address, self.owner.bus, suppress_response=True)]) + self.last_tester_at = self.clock() + except Exception: + self.fault = 'Startup diagnostic sender failed' + self.owner.cancel() + self.stop_event.set() + + def prepared_for(self, cp): + return (not self.closed and self.fault is None and self.owner.active() and self.owner.result is not None and + self.owner.result.outcome is Outcome.OWNED and self.source_floor_ns > 0 and + self.owner.cp.to_dict() == (self.final_snapshot or self.candidate_snapshot) == cp.to_dict()) + + def configure(self, ci): + self.check() + if self.closed or self.owner.published: + raise RuntimeError("Mixed startup configuration is closed") + if self.owner.active(): + self.controller = BlendedLongitudinalController(self.owner, ci.CC.packer, ci.CC.CAN) + ci.CC.blended_longitudinal = self.controller + self.configured_ci = ci + + def finalize_aol_configuration(self, ci): + self.check() + if self.closed or self.owner.published or ci is not self.configured_ci: + raise RuntimeError('Mixed AOL finalization requires configured unpublished startup') + if self.owner.active() and (self.controller is None or ci.CC.blended_longitudinal is not self.controller): + raise RuntimeError('Mixed AOL finalization requires its installed controller') + if self.final_snapshot is not None: + if not self.prepared_for(ci.CP): + raise RuntimeError('Mixed AOL final configuration is frozen') + return + if not self.owner.active(): + return + expected = dict(self.candidate_snapshot) + if ci.CP.alternativeExperience == 32: + from opendbc.car.hyundai.blended_stock_aol import qualified_alpha + if not qualified_alpha(ci.CP, marked_only=True): + raise RuntimeError('Unsupported mixed alpha AOL composition') + expected['alternativeExperience'] = 32 + if ci.CP.to_dict() != expected or self.owner.cp.to_dict() != expected: + raise RuntimeError('Mixed alpha final CP differs from prepared transaction') + self.final_snapshot = expected + + def seal_publication(self): + self.check() + if self.owner.active() and not self.prepared_for(self.owner.cp): + raise RuntimeError('Mixed alpha publication requires exact prepared final CP') + self.owner.published = True + + def check(self): + if self.fault is not None: + raise RuntimeError(self.fault) + if self.owner.result is None or not self.owner.result.admissible: + raise RuntimeError('Uncertain mixed-CAN ECU ownership') + + def sources_current(self, ci, now_ns, *, health_checked=False): + from opendbc.car import Bus + from opendbc.can.parser import MAX_BAD_COUNTER + if not self.owner.active(): + return True + if self.source_floor_ns <= 0: + return False + # Independent sources consumed after ECU suppression; SCC output is not a + # takeover proof. Disabled SCC parser/source selection remains separate. + pt_names = ('MDPS12', 'TCS11', 'TCS13', 'TCS15', 'CLU11', 'CLU15', 'ESP12', + 'CGW1', 'CGW2', 'WHL_SPD11', 'SAS11', 'EMS12', 'EMS16', 'LVR12') + camera = 'CAM_0x2a4' if self.owner.cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG else 'LKAS11' + from opendbc.car.hyundai.hyundaicanfd import CanBus + CAN = CanBus(self.owner.cp) + for bus, names, expected_bus in ((Bus.pt, pt_names, CAN.ECAN), (Bus.cam, (camera,), CAN.CAM)): + parser = ci.can_parsers[bus] + if parser.bus != expected_bus or (not health_checked and (not parser.can_valid or parser.bus_timeout)): + return False + for name in names: + address = parser.dbc.name_to_msg[name].address + state = parser.message_states.get(address) + if state is None or not state.timestamps or state.counter_fail >= MAX_BAD_COUNTER: + return False + stamp = int(state.timestamps[-1]) + if not self.source_floor_ns < stamp <= now_ns or now_ns - stamp > state.timeout_threshold: + return False + return True + + def after_state(self, *, ci, state, now_ns, control_enabled, control_current, pandas, + panda_log_ns, panda_recv_ns, panda_current): + from opendbc.car import Bus + from opendbc.car.hyundai.values import Buttons + cp = self.owner.cp + if not self.owner.active(): + self.cruise_resume_hint = None + return + state.buttonEnable = False + exact = (is_blended(cp) and cp.openpilotLongitudinalControl and not cp.pcmCruise and + len(cp.safetyConfigs) == 1 and cp.safetyConfigs[0].safetyParam in (0x2004, 0x2014)) + matched = False + if exact and panda_current and len(pandas) == 1: + panda = pandas[0] + config = cp.safetyConfigs[0] + matched = (panda.safetyModel == config.safetyModel and panda.safetyParam == config.safetyParam and + panda.alternativeExperience == cp.alternativeExperience and not panda.safetyRxChecksInvalid and + not panda.faults and (panda.ignitionLine or panda.ignitionCan) and + 0 < panda_log_ns <= now_ns and now_ns - panda_log_ns <= 300_000_000 and + 0 < panda_recv_ns <= now_ns and now_ns - panda_recv_ns <= 300_000_000) + transport_guard = (matched and control_current and state.canValid and not state.canTimeout and + state.cruiseState.available and not state.brakePressed and not state.gasPressed and + self.sources_current(ci, now_ns, health_checked=True)) + base_guard = transport_guard and not control_enabled + parser = ci.can_parsers[Bus.pt] + samples = list(parser.vl_all['CLU11']['CF_Clu_CruiseSwState']) + edges = [] + for sample in samples: + button = int(sample) + if button != self.enable_button: + edges.append((self.enable_button, button)) + self.enable_button = button + if self.cruise_resume_hint is not None: + _, hint_ns = self.cruise_resume_hint + if (not transport_guard or edges or now_ns < hint_ns or now_ns - hint_ns > 500_000_000 or + not ci.CS.blended_cancel_sources_current(parser, cancel=False)): + self.cruise_resume_hint = None + native_on = bool(matched and pandas[0].controlsAllowed) + previous_off = self.native_off_witness + self.native_off_witness = bool(base_guard and not native_on) + if not base_guard: + self.enable_gesture = None + self.enable_credit = None + return + if self.enable_credit is not None: + generation, release_ns, created_ns, button = self.enable_credit + cancel = button == Buttons.CANCEL + if (edges or now_ns < created_ns or now_ns - created_ns > 500_000_000 or + not ci.CS.blended_cancel_sources_current(parser, cancel=cancel)): + self.enable_credit = None + elif native_on and panda_log_ns > release_ns: + state.buttonEnable = True + self.cruise_resume_hint = (button in (Buttons.CANCEL, Buttons.RES_ACCEL), now_ns) + self.enable_credit = None + for previous, button in edges: + self.enable_credit = None + gesture = self.enable_gesture + self.enable_gesture = None + if (button == Buttons.NONE and gesture is not None and previous == gesture[1] and + ci.CS.blended_cancel_sources_current(parser, cancel=gesture[1] == Buttons.CANCEL)): + # No immediate host event; require a subsequent fresh native ON sample. + release_ns = parser.ts_nanos['CLU11']['CF_Clu_CruiseSwState'] + self.enable_credit = (gesture[0], release_ns, now_ns, gesture[1]) + elif (previous == Buttons.NONE and button in (Buttons.CANCEL, Buttons.RES_ACCEL, Buttons.SET_DECEL) and + previous_off and not native_on and + ci.CS.blended_cancel_sources_current(parser, cancel=button == Buttons.CANCEL)): + self.enable_generation += 1 + self.enable_gesture = (self.enable_generation, button) + if self.enable_gesture is not None: + cancel = self.enable_gesture[1] == Buttons.CANCEL + if not ci.CS.blended_cancel_sources_current(parser, cancel=cancel): + self.enable_gesture = None + + def consume_cruise_resume(self): + hint = self.cruise_resume_hint + self.cruise_resume_hint = None + return None if hint is None else hint[0] + + def before_control(self, *, configured, sources_current, control_current): + self.check() + if not self.owner.active(): + return True + if not configured or not sources_current or not control_current: + return False + # Keep the worker alive until a valid controller tester actually reaches + # PubSocket.send. Merely reaching CI.apply is not a handoff receipt. + return True + + def maintain(self, *, configured): + self.check() + if not self.handed_off or not self.owner.active() or not configured: + return + # A missing control/input sample inhibits actuation, not ECU keepalive. + # This runs from Card's main loop even when controls are unavailable. + if self.last_tester_at is None or self.clock() - self.last_tester_at >= 1.: + self._tester() + + def sent(self, frames, *, valid): + if not valid or not self.owner.active(): + return + from opendbc.car import make_tester_present_msg + tester = make_tester_present_msg(self.owner.address, self.owner.bus, suppress_response=True) + if tester not in frames: + return + self.last_tester_at = self.clock() + if self.handed_off: + return + # Caller has released send_lock; join cannot deadlock the worker sender. + self._stop() + self.check() + self.handed_off = True + if self.controller is not None: + self.controller.normal_sent = True + + def _stop(self): + import threading + self.stop_event.set() + if self.thread is not None and self.thread is not threading.current_thread(): + self.thread.join() + + def close(self): + if self.closed: + if self.owner.result is not None and self.owner.result.outcome is Outcome.ABORT_UNCERTAIN: + raise RuntimeError("Stock ECU restoration remains unverified") + return + self.closed = True + self._stop() + if not self.owner.takeover_attempted: + return + if self.diagnostic_admission is None or self.diagnostic_admission() is not True: + self.owner.result = TakeoverResult(Outcome.ABORT_UNCERTAIN) + self.owner.phase = Phase.FAILED + raise RuntimeError("Diagnostic admission unavailable; stock ECU restoration remains unverified") + # The output loop is stopped. Published CP is never relabeled on teardown. + if not self.owner.restore(*self.callbacks): + raise RuntimeError('Mixed-CAN stock ECU restoration is unverified') + + +def startup_owner(cp, callbacks, *, requested): + candidate = candidate_from_stock(cp, alpha_requested=requested, + native_qualified=cp.alphaLongitudinalAvailable and mixed_startup_qualified(cp)) + return None if candidate is None else BlendedStartup(cp, candidate, callbacks) diff --git a/opendbc_repo/opendbc/car/hyundai/blended_longitudinal_can.py b/opendbc_repo/opendbc/car/hyundai/blended_longitudinal_can.py new file mode 100644 index 0000000000..0cc1e122e1 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/blended_longitudinal_can.py @@ -0,0 +1,144 @@ +"""Mixed longitudinal CAN message builders.""" +from opendbc.car.hyundai.hyundaican import hyundai_checksum +from opendbc.car.hyundai.hyundaicanfd import CanBus +from opendbc.car.hyundai.values import HyundaiFlags + + +def create_checksum_can_canfd_blended(packer, bus, addr, values): + dat = packer.make_can_msg(addr, bus, values)[1] + return hyundai_checksum(dat[1:8]) + + +def create_acc_commands_can_canfd_blended(packer, enabled, accel, upper_jerk, idx, hud_control, set_speed, + stopping, long_override, use_fca, CP): + commands = [] + bus = CanBus(CP).ECAN + + scc11_values = { + "aReqRaw": accel, + "aReqValue": accel, + "JerkUpperLimit": upper_jerk, + "JerkLowerLimit": 5.0, + "ComfortBandUpper": 0.0, + "ComfortBandLower": 0.0, + "COUNTER": idx % 0x10, + } + scc11_values["CHECKSUM"] = create_checksum_can_canfd_blended(packer, bus, "SCC11", scc11_values) + commands.append(packer.make_can_msg("SCC11", bus, scc11_values)) + + scc12_values = { + "MainMode_ACC": 1, + "ACCMode_Inactive": 0 if enabled else 1, + "TauGapSet": hud_control.leadDistanceBars, + "VSetDis": set_speed if enabled else 0, + "ACC_ObjDist": 1, + "ACCMode": 2 if enabled and long_override else 1 if enabled else 0, + "StopReq": 1 if stopping else 0, + "ACC_ObjDist_Ref": 1, + "COUNTER": idx % 0x10, + } + scc12_values["CHECKSUM"] = create_checksum_can_canfd_blended(packer, bus, "SCC12", scc12_values) + commands.append(packer.make_can_msg("SCC12", bus, scc12_values)) + + scc14_values = { + "ACC_ObjRelSpd": 0, + "ObjValid": 1, + "ObjStatus": 1, + "COUNTER": idx % 0x10, + } + scc14_values["CHECKSUM"] = create_checksum_can_canfd_blended(packer, bus, "SCC14", scc14_values) + commands.append(packer.make_can_msg("SCC14", bus, scc14_values)) + + if use_fca and not (CP.flags & HyundaiFlags.CAMERA_SCC): + fca11_values = { + "cr_vsm_deccmd": 255, + "cf_vsm_deccmdact": 127, + "COUNTER": idx % 0x10, + } + fca11_values["CHECKSUM"] = create_checksum_can_canfd_blended(packer, bus, "FCA11", fca11_values) + commands.append(packer.make_can_msg("FCA11", bus, fca11_values)) + + return commands + + +def create_acc_commands_can_canfd_blended_hda2(packer, enabled, accel, accel_last, upper_jerk, idx, + hud_control, set_speed, stopping, long_override, use_fca, CP): + commands = [] + bus = CanBus(CP).ECAN + jerk = 5.0 + + if not enabled or long_override: + accel_raw, accel_value = 0.0, 0.0 + else: + accel_raw = accel + accel_value = max(accel_last - jerk / 50.0, min(accel, accel_last + jerk / 50.0)) + + message_values = [ + ("SCC11", { + "aReqRaw": accel_raw, + "aReqValue": accel_value, + "JerkUpperLimit": upper_jerk, + "JerkLowerLimit": jerk if enabled else 1.0, + }), + ("SCC12", { + "MainMode_ACC": 1, + "ACCMode_Inactive": 0 if enabled else 1, + "TauGapSet": hud_control.leadDistanceBars, + "VSetDis": set_speed, + "ACC_ObjDist": 1, + "ACCMode": 2 if enabled and long_override else 1 if enabled else 0, + "StopReq": 1 if stopping else 0, + }), + ("SCC14", { + "ACC_ObjRelSpd": 0, + "ObjValid": 0, + "ObjStatus": 2 if hud_control.leadVisible and enabled else 1 if hud_control.leadVisible else 0, + }), + ] + + if use_fca and not (CP.flags & HyundaiFlags.CAMERA_SCC): + # Retained original bytes; the FCA deceleration sentinel semantics remain unverified. + message_values.append(("FCA11", { + "cr_vsm_deccmd": 255, + "cf_vsm_deccmdact": 0, + })) + + for name, values in message_values: + values["COUNTER"] = idx % 0xF + values["CHECKSUM"] = create_checksum_can_canfd_blended(packer, bus, name, values) + commands.append(packer.make_can_msg(name, bus, values)) + + return commands + + +def create_radar_aux_messages(packer, CAN, frame, hda2=False): + commands = [] + + message_specs = ( + ("RADAR_0x363", 2, {"FCA_ESA": 1}), + ("RADAR_0x398", 5, {"BYTE4": 0x80, "BYTE5": 0x5D}), + ("RADAR_0x399", 5, {"BYTE2": 0x02}), + ("RADAR_0x39a", 5, {"BYTE7": 0xFF}), + ("RADAR_0x39b", 5, {}), + ("RADAR_0x39c", 5, {"BYTE5": 0xE0, "BYTE6": 0x79}), + ("RADAR_0x43a", 20, {"BYTE2": 0x07}), + ) if hda2 else ( + ("RADAR_0x363", 2, {"FCA_ESA": 1}), + ("RADAR_0x398", 5, {"BYTE4": 0x80, "BYTE5": 0x10}), + ) + + for addr, freq, values in message_specs: + if frame % freq != 0: + continue + + msg_values = values | {"COUNTER": frame % (0xF if hda2 else 0x10)} + msg_values["CHECKSUM"] = create_checksum_can_canfd_blended(packer, CAN.ECAN, addr, msg_values) + commands.append(packer.make_can_msg(addr, CAN.ECAN, msg_values)) + + return commands + + + +def create_blended_adrv_messages(packer, CAN, frame): + # Exact reached original create_adrv_messages(..., blended_hda2=True) branch. + return [packer.make_can_msg("ADRV_0x51", CAN.ACAN, {})] diff --git a/opendbc_repo/opendbc/car/hyundai/blended_stock_aol.py b/opendbc_repo/opendbc/car/hyundai/blended_stock_aol.py new file mode 100644 index 0000000000..6c819de276 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/blended_stock_aol.py @@ -0,0 +1,72 @@ +from opendbc.car import structs +from opendbc.car.hyundai.values import CAR, HyundaiFlags +from opendbc.car.hyundai.hyundaicanfd import CanBus +from opendbc.car.hyundai.classic_scc_aol import ClassicSccLkasSources + +AOL_EXPERIENCE = 32 +WORDS = frozenset((0x2000, 0x2010)) + + +def qualified(cp, *, marked_only=False): + if cp.carFingerprint != CAR.HYUNDAI_PALISADE_2023 or cp.brand != 'hyundai': + return False + declared = int(CAR.HYUNDAI_PALISADE_2023.config.flags) + dynamic = int(HyundaiFlags.CANFD_LKA_STEER_MSG | HyundaiFlags.HAS_LDA_BUTTON | HyundaiFlags.USE_FCA | HyundaiFlags.SEND_LFA) + if ( + int(cp.flags) & ~dynamic != declared + or cp.passive + or cp.notCar + or cp.dashcamOnly + or cp.openpilotLongitudinalControl + or not cp.pcmCruise + or cp.steerControlType != structs.CarParams.SteerControlType.torque + or len(cp.safetyConfigs) != 1 + or cp.alternativeExperience not in ((AOL_EXPERIENCE,) if marked_only else (0, AOL_EXPERIENCE)) + ): + return False + hda2 = bool(cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG) + bus = CanBus(cp) + return ( + (bus.ECAN, bus.ACAN, bus.CAM) == ((1, 0, 2) if hda2 else (0, 1, 2)) + and cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.hyundai + and cp.safetyConfigs[0].safetyParam == (0x2010 if hda2 else 0x2000) + ) + + +class MixedStockLkasSources(ClassicSccLkasSources): + def __init__(self, cp): + super().__init__(cp.carFingerprint) + self.cp = cp + self.bus = CanBus(cp).ECAN + + @property + def active(self): + return qualified(self.cp, marked_only=True) or qualified_alpha(self.cp, marked_only=True) + + def update(self, packets): + if not self.active: + return + selected = [(stamp, [(address, data, 0) for address, data, bus in frames if bus == self.bus]) for stamp, frames in packets] + super().update(selected) + + +ALPHA_WORDS = frozenset((0x2004, 0x2014)) + + +def qualified_alpha(cp, *, marked_only=False): + from opendbc.car.hyundai.blended_longitudinal import mixed_startup_qualified + return (mixed_startup_qualified(cp, allow_marked=True) and cp.alphaLongitudinalAvailable and + cp.openpilotLongitudinalControl and not cp.pcmCruise and + cp.safetyConfigs[0].safetyParam == (0x2014 if cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG else 0x2004) and + cp.alternativeExperience in ((32,) if marked_only else (0, 32))) + + +def camera_current(parser): + from opendbc.can.parser import MAX_BAD_COUNTER + source = parser.message_states.get(0x2A4) + now = parser._last_update_nanos + bus_timeout = parser.bus_timeout + healthy = all(state.valid(now, bus_timeout) and state.counter_fail < MAX_BAD_COUNTER for state in parser.message_states.values()) + return bool(healthy and not bus_timeout and source is not None and source.timestamps + and source.counter_fail < MAX_BAD_COUNTER and 0 < int(source.timestamps[-1]) <= now + and now - int(source.timestamps[-1]) <= source.timeout_threshold) diff --git a/opendbc_repo/opendbc/car/hyundai/blended_stopping.py b/opendbc_repo/opendbc/car/hyundai/blended_stopping.py new file mode 100644 index 0000000000..9f524ea049 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/blended_stopping.py @@ -0,0 +1,64 @@ +"""Reached mixed-Palisade stopping behavior from pinned original 86599a27.""" +import numpy as np + +from opendbc.car.structs import car +from opendbc.car.hyundai import blended_longitudinal +from opendbc.car.hyundai.values import HyundaiFlags +from opendbc.car.hyundai.longitudinal_mode import HKGModePolicy + +LongCtrlState = car.CarControl.Actuators.LongControlState + + +def eligible(cp): + if not blended_longitudinal.mixed_startup_qualified(cp, allow_marked=True): + return False + word = 0x2014 if cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG else 0x2004 + if (not cp.openpilotLongitudinalControl or cp.pcmCruise or cp.passive or + len(cp.safetyConfigs) != 1 or cp.safetyConfigs[0].safetyParam != word): + return False + return True + + +def policy_for(cp, dt): + return BlendedStoppingPolicy(dt) if eligible(cp) else None + + +class BlendedStoppingPolicy(HKGModePolicy): + # Preserve the original CarParams Float32 representation. + stopping_decel_rate = float(np.float32(.35)) + + def __init__(self, dt): + self.dt = dt + self.release_frames = int(round(.35 / dt)) + self.init_mode(dt) + self.reset() + + def reset(self): + self.release_counter = 0 + + def transition(self, native_state, previous_state, active, CS, a_target, should_stop, *, has_lead): + # Original mixed platform startingState is false: stopping releases to PID. + if previous_state != LongCtrlState.stopping or not active: + self.reset() + return native_state + if should_stop or CS.brakePressed: + self.reset() + return LongCtrlState.stopping + if (CS.vEgo > .5 or (has_lead and a_target > .15) or + (a_target >= .45 and not CS.cruiseState.standstill)): + self.release_counter = self.release_frames + elif a_target > .15: + self.release_counter = min(self.release_counter + 1, self.release_frames) + else: + self.reset() + return LongCtrlState.pid if self.release_counter >= self.release_frames else LongCtrlState.stopping + + def stopping_output(self, output, a_target, should_stop, CS): + # Original other-vehicle stopping tune branches are no-ops for this identity. + follow_min_speed = max(1.5, .35 + 1.0) + if not should_stop or CS.brakePressed or CS.vEgo <= follow_min_speed: + return output + if a_target >= output - .25: + return output + step = np.interp(CS.vEgo, [follow_min_speed, 3., 6., 10.], [.02, .03, .05, .07]) + return max(float(a_target), output - float(step)) diff --git a/opendbc_repo/opendbc/car/hyundai/canfd_angle_aol.py b/opendbc_repo/opendbc/car/hyundai/canfd_angle_aol.py new file mode 100644 index 0000000000..27cc09ab63 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/canfd_angle_aol.py @@ -0,0 +1,210 @@ +import math + +from opendbc.car import structs +from opendbc.car.hyundai.values import CAR, HyundaiFlags, CANFD_ANGLE_MODEL_BITS + +AOL_EXPERIENCE = 32 +MODELS = frozenset(CANFD_ANGLE_MODEL_BITS) +ANGLE_AOL_WORDS = frozenset( + ( + 0x4008, + 0x4010, + 0x4028, + 0x4090, + 0x4208, + 0x4228, + 0x4408, + 0x440A, + 0x4410, + 0x4412, + 0x4428, + 0x442A, + 0x4490, + 0x4492, + 0x4608, + 0x460A, + 0x4628, + 0x462A, + 0x4809, + 0x4811, + 0x4829, + 0x4891, + 0x4A09, + 0x4A29, + 0x4C08, + 0x4C28, + 0x4E08, + 0x4E28, + 0x5008, + 0x5010, + 0x5028, + 0x5090, + 0x5208, + 0x5228, + 0x5491, + 0x5809, + 0x5811, + 0x5829, + 0x5891, + 0x5A09, + 0x5A29, + 0x5C91, + 0x6008, + 0x600A, + 0x6010, + 0x6012, + 0x6028, + 0x602A, + 0x6090, + 0x6092, + 0x6208, + 0x620A, + 0x6228, + 0x622A, + 0x6808, + 0x680A, + 0x6810, + 0x6812, + 0x6828, + 0x682A, + 0x6890, + 0x6892, + 0x6A08, + 0x6A0A, + 0x6A28, + 0x6A2A, + 0x7008, + 0x700A, + 0x7010, + 0x7012, + 0x7028, + 0x702A, + 0x7090, + 0x7092, + 0x7208, + 0x720A, + 0x7228, + 0x722A, + 0x7809, + 0x7829, + ) +) + + +def stock_word(cp): + flags = int(cp.flags) + word = 0x4000 | CANFD_ANGLE_MODEL_BITS[str(cp.carFingerprint)] + if flags & HyundaiFlags.EV: + word |= 1 + if flags & HyundaiFlags.HYBRID: + word |= 2 + if flags & HyundaiFlags.CANFD_LKA_STEER_MSG: + word |= 16 + if flags & HyundaiFlags.CANFD_LKA_STEER_MSG_ALT: + word |= 128 + else: + word |= 8 + if flags & HyundaiFlags.CANFD_ALT_BUTTONS: + word |= 32 + if flags & HyundaiFlags.SEND_LFA: + word |= 512 + return word + + +def qualified(cp, *, marked_only=False): + identity = str(cp.carFingerprint) + if identity not in MODELS or cp.brand != 'hyundai': + return False + declared = int(CAR(identity).config.flags) + dynamic = int( + HyundaiFlags.HYBRID + | HyundaiFlags.CANFD_LKA_STEER_MSG + | HyundaiFlags.CANFD_LKA_STEER_MSG_ALT + | HyundaiFlags.CANFD_ALT_BUTTONS + | HyundaiFlags.CANFD_CAMERA_SCC + | HyundaiFlags.CANFD_ALT_GEARS + | HyundaiFlags.CANFD_ALT_GEARS_2 + | HyundaiFlags.SEND_LFA + ) + flags = int(cp.flags) + if ( + flags & ~dynamic != declared & ~dynamic + or flags & ~(declared | dynamic) + or cp.passive + or cp.notCar + or cp.dashcamOnly + or cp.openpilotLongitudinalControl + or not cp.pcmCruise + or cp.steerControlType != structs.CarParams.SteerControlType.angle + or cp.alternativeExperience not in ((AOL_EXPERIENCE,) if marked_only else (0, AOL_EXPERIENCE)) + or len(cp.safetyConfigs) != 1 + or cp.safetyConfigs[0].safetyModel != structs.CarParams.SafetyModel.hyundaiCanfd + ): + return False + if not flags & HyundaiFlags.EV and (not flags & HyundaiFlags.CANFD_ALT_GEARS_2 or flags & HyundaiFlags.CANFD_ALT_GEARS): + return False + if flags & HyundaiFlags.EV and flags & HyundaiFlags.HYBRID: + return False + lka = bool(flags & HyundaiFlags.CANFD_LKA_STEER_MSG) + if ( + bool(flags & HyundaiFlags.CANFD_CAMERA_SCC) == lka + or (flags & HyundaiFlags.CANFD_LKA_STEER_MSG_ALT and not lka) + or (lka and flags & HyundaiFlags.CANFD_ALT_BUTTONS) + ): + return False + if identity == 'KIA_SPORTAGE_2026' and (lka or flags & HyundaiFlags.HYBRID): + return False + if identity in ('HYUNDAI_IONIQ_5_PE', 'KIA_EV9'): + expected = 0x5491 if identity == 'HYUNDAI_IONIQ_5_PE' else 0x5C91 + return stock_word(cp) == expected and cp.safetyConfigs[0].safetyParam == expected + if identity == 'KIA_EV6_2025' and stock_word(cp) not in (0x7809, 0x7829): + return False + if flags & HyundaiFlags.HYBRID and identity not in ( + 'HYUNDAI_AZERA_HEV_7TH_GEN', + 'KIA_SORENTO_HEV_4TH_GEN_LFA2', + 'KIA_SPORTAGE_HEV_2026', + 'HYUNDAI_SANTA_FE_HEV_5TH_GEN', + ): + return False + return stock_word(cp) in ANGLE_AOL_WORDS and cp.safetyConfigs[0].safetyParam == stock_word(cp) + + +def command_allowed(cp, state): + return ( + qualified(cp, marked_only=True) + and state.canValid + and not state.canTimeout + and state.gearShifter == structs.CarState.GearShifter.drive + and not state.standstill + and math.isfinite(state.vEgoRaw) + and state.vEgoRaw > 0.3 + and not (state.brakePressed or state.gasPressed or state.steerFaultTemporary or state.steerFaultPermanent) + ) + + +def temporary_restriction(cp, state): + gears = structs.CarState.GearShifter + return bool( + state is not None + and qualified(cp, marked_only=True) + and state.canValid + and not state.canTimeout + and not state.steerFaultPermanent + and state.gearShifter in (gears.drive, gears.reverse, gears.park, gears.neutral) + and math.isfinite(state.vEgoRaw) + and (state.gearShifter != gears.drive or state.standstill or state.vEgoRaw <= 0.3 or state.steerFaultTemporary or state.brakePressed or state.gasPressed) + ) + + +ANGLE_ACK_TIMEOUT_NS = 200_000_000 + + +def native_observation(native, *, latched, panda_ready, restricted, now_ns, pending_since_ns): + lost = not panda_ready or native is None + if not latched or lost or restricted: + return 0, lost, False + if native.requestedLateral and native.lateralAllowed: + return 0, False, False + pending_since_ns = pending_since_ns or now_ns + reset = now_ns - pending_since_ns >= ANGLE_ACK_TIMEOUT_NS + return pending_since_ns, False, reset diff --git a/opendbc_repo/opendbc/car/hyundai/canfd_camera_lead.py b/opendbc_repo/opendbc/car/hyundai/canfd_camera_lead.py new file mode 100644 index 0000000000..ed66504215 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/canfd_camera_lead.py @@ -0,0 +1,75 @@ +"""Optional isolated CANFD camera lead decoding, outside required controls health.""" +import math +import time +from dataclasses import dataclass + +from opendbc.can.parser import CANParser +from opendbc.car import Bus +from opendbc.car.hyundai.hyundaicanfd import hkg_can_fd_checksum +from opendbc.car.hyundai.values import DBC + +CAMERA_ADDRESS = 0x1B5 +CAMERA_MAX_AGE_NS = 300_000_000 + + +@dataclass(frozen=True) +class CameraLeadObservation: + producer_boot_ns: int + visible: bool + distance_m: float + relative_speed_mps: float + + +class CANFDCameraLead: + def __init__(self, cp, bus): + self.bus = bus + self.parser = CANParser(DBC[cp.carFingerprint][Bus.pt], [('FR_CMR_03_50ms', math.nan)], self.bus) + self.observation = None + self.last_packet_ns = 0 + self.last_counter = None + + def update(self, can_packets): + boot_clock = getattr(time, "CLOCK_BOOTTIME", None) + if boot_clock is None: + return + try: + now_boot_ns = time.clock_gettime_ns(boot_clock) + except OSError: + return + for stamp, frames in can_packets: + if type(stamp) is not int or not self.last_packet_ns < stamp <= now_boot_ns: + continue + for address, data, bus in frames: + if address != CAMERA_ADDRESS or bus != self.bus or len(data) != 32 or stamp <= self.last_packet_ns: + continue + self.last_packet_ns = stamp + # Semantic camera field names do not opt the shared parser into integrity + # checks. Keep validation local so existing CCNC controls health is unchanged. + if int.from_bytes(data[:2], 'little') != hkg_can_fd_checksum(CAMERA_ADDRESS, None, bytearray(data)): + continue + counter = data[2] + advancing = self.last_counter is None or counter == (self.last_counter + 1) % 256 + self.last_counter = counter + if not advancing: + continue + previous = self.parser.ts_nanos['FR_CMR_03_50ms']['FR_CMR_Crc3Val'] + accepted = self.parser.update([(stamp, [(address, data, bus)])]) + if CAMERA_ADDRESS not in accepted or stamp <= previous: + continue + if self.observation is not None and stamp <= self.observation.producer_boot_ns: + continue + values = self.parser.vl['FR_CMR_03_50ms'] + distance = float(values['Longitudinal_Distance']) + relative = float(values['Relative_Velocity']) + if not math.isfinite(distance) or not math.isfinite(relative): + continue + visible = distance > .1 + self.observation = CameraLeadObservation(stamp, visible, distance if visible else 0., relative if visible else 0.) + + def current(self, now_boot_ns, floor_boot_ns=0): + observation = self.observation + if (type(now_boot_ns) is not int or type(floor_boot_ns) is not int or observation is None or + not max(0, floor_boot_ns) < observation.producer_boot_ns <= now_boot_ns or + now_boot_ns - observation.producer_boot_ns > CAMERA_MAX_AGE_NS): + return None + return observation diff --git a/opendbc_repo/opendbc/car/hyundai/canfd_dash_icons.py b/opendbc_repo/opendbc/car/hyundai/canfd_dash_icons.py new file mode 100644 index 0000000000..80a6e4a8de --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/canfd_dash_icons.py @@ -0,0 +1,19 @@ +from opendbc.car import DT_CTRL + + +class CanFDDashIcons: + def __init__(self): + self.disengage_frame = 0 + self.disengaging = False + self.previous_lateral = False + + def update(self, frame, enabled, lateral_active): + if lateral_active: + self.disengaging = False + elif self.previous_lateral: + self.disengaging = True + if not self.disengaging: + self.disengage_frame = frame + blinking = self.disengaging and (frame - self.disengage_frame) * DT_CTRL < 1.0 + self.previous_lateral = lateral_active + return 2 if enabled or lateral_active else 3 if blinking else 0 diff --git a/opendbc_repo/opendbc/car/hyundai/canfd_lead.py b/opendbc_repo/opendbc/car/hyundai/canfd_lead.py new file mode 100644 index 0000000000..616808b420 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/canfd_lead.py @@ -0,0 +1,58 @@ +"""Stateless CANFD lead selection; observations carry owner-qualified BOOTTIME receipts.""" +from dataclasses import dataclass +import math + +CAMERA_MAX_AGE_NS = 300_000_000 +MIN_DISTANCE = 0.1 + + +@dataclass(frozen=True) +class CANFDLeadObservation: + visible: bool + distance: float + relative_speed: float + producer_ns: int + + +@dataclass(frozen=True) +class CANFDLeadSelection: + visible: bool + distance: float + relative_speed: float + + +ABSENT = CANFDLeadSelection(False, 0., 0.) + + +def _current(observation: CANFDLeadObservation | None, observed_ns: int, + epoch_floor_ns: int) -> bool: + return (observation is not None and type(observation.producer_ns) is int and + epoch_floor_ns < observation.producer_ns <= observed_ns and + math.isfinite(observation.distance) and math.isfinite(observation.relative_speed)) + + +def _selection(observation: CANFDLeadObservation) -> CANFDLeadSelection: + return CANFDLeadSelection(True, min(max(observation.distance, 0.), 204.7), + min(max(observation.relative_speed, -16.4), 34.7)) + + +def select_lead(radar: CANFDLeadObservation | None, camera: CANFDLeadObservation | None, *, + hud_visible: bool, observed_ns: int, epoch_floor_ns: int) -> CANFDLeadSelection: + """Radar freshness is qualified by transport; camera integrity by its parser owner. + + Owner supplies one BOOTTIME domain and epoch floor. Camera age is bounded here; + no G90 hysteresis or additional radar freshness policy is introduced. + """ + if (type(observed_ns) is not int or type(epoch_floor_ns) is not int or + epoch_floor_ns < 0 or observed_ns <= epoch_floor_ns): + return ABSENT + radar_current = _current(radar, observed_ns, epoch_floor_ns) + radar_visible = radar_current and radar.visible + if radar_visible and radar.distance > MIN_DISTANCE: + return _selection(radar) + if (_current(camera, observed_ns, epoch_floor_ns) and camera.visible and + camera.distance > MIN_DISTANCE and observed_ns-camera.producer_ns <= CAMERA_MAX_AGE_NS): + return _selection(camera) + if radar_visible or hud_visible: + return CANFDLeadSelection(True, 20., 0.) + return ABSENT diff --git a/opendbc_repo/opendbc/car/hyundai/canfd_owner.py b/opendbc_repo/opendbc/car/hyundai/canfd_owner.py new file mode 100644 index 0000000000..ad1d74fa6d --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/canfd_owner.py @@ -0,0 +1,36 @@ +"""Selected safety slot for the exact first-generation captured CANFD owner.""" +from opendbc.car.structs import CarParams +from opendbc.car.hyundai.values import CAR, HyundaiFlags + + +def topology_index(cp): + if len(cp.safetyConfigs) == 1: + return 0 if cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.hyundaiCanfd else None + if len(cp.safetyConfigs) != 2: + return None + auxiliary, owner = cp.safetyConfigs + required = HyundaiFlags.CANFD | HyundaiFlags.EV | HyundaiFlags.CANFD_LKA_STEER_MSG + excluded = (HyundaiFlags.CANFD_ANGLE_STEERING | HyundaiFlags.CANFD_LKA_STEER_MSG_ALT | + HyundaiFlags.CANFD_ALT_BUTTONS | HyundaiFlags.CANFD_CAMERA_SCC | HyundaiFlags.CCNC | HyundaiFlags.HYBRID) + if (cp.brand != 'hyundai' or cp.carFingerprint not in (CAR.KIA_EV6, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN) or + cp.passive or cp.dashcamOnly or cp.notCar or cp.steerControlType != CarParams.SteerControlType.torque or + cp.flags & required != required or cp.flags & excluded or + auxiliary.safetyModel != CarParams.SafetyModel.noOutput or auxiliary.safetyParam != 0 or + owner.safetyModel != CarParams.SafetyModel.hyundaiCanfd): + return None + return 1 + + +def config_index(cp): + owner = topology_index(cp) + if owner != 1: + return owner + raw = int(cp.safetyConfigs[owner].safetyParam) + if cp.openpilotLongitudinalControl and not cp.pcmCruise: + if raw == 0x15 and cp.alternativeExperience == 0: + return 1 + if cp.carFingerprint == CAR.KIA_EV6 and raw == 0x815 and cp.alternativeExperience == 32: + return 1 + elif not cp.openpilotLongitudinalControl and cp.pcmCruise and cp.alternativeExperience == 0 and raw in (0x11, 0x811): + return 1 + return None diff --git a/opendbc_repo/opendbc/car/hyundai/canfd_stock_aol.py b/opendbc_repo/opendbc/car/hyundai/canfd_stock_aol.py new file mode 100644 index 0000000000..f9af41f2b6 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/canfd_stock_aol.py @@ -0,0 +1,114 @@ +"""AOL admission for the ordinary CAN FD torque and physical-button layouts.""" + +from opendbc.car.structs import CarParams +from opendbc.car.hyundai.values import CAR, HyundaiFlags +from opendbc.car.hyundai.hyundaicanfd import CanBus +from opendbc.car.hyundai.canfd_owner import config_index + +STOCK_AOL_MARKER = 0x0800 +STOCK_EV_CARS = frozenset( + ( + CAR.HYUNDAI_KONA_EV_2ND_GEN, + CAR.HYUNDAI_IONIQ_5, + CAR.HYUNDAI_IONIQ_5_N, + CAR.KIA_NIRO_EV_2ND_GEN, + CAR.KIA_EV6, + CAR.GENESIS_GV60_EV_1ST_GEN, + CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, + ) +) +CARNIVAL_CARS = frozenset((CAR.KIA_CARNIVAL_2025, CAR.KIA_CARNIVAL_HEV_4TH_GEN)) +STOCK_AOL_WORDS = frozenset( + 0x0800 | gas | topology | extra + for gas in (0, 1, 2) + for topology in (0, 8, 16, 144, 32, 40, 48, 176) + for extra in (0, 0x0400, 0x2000, 0x2400) + if (topology not in (48, 176) or extra & 0x2000) + and (not extra & 0x0400 or topology in (8, 40)) + and (not extra & 0x2000 or (topology in (32, 40, 48, 176) and gas != 1)) +) + +STOCK_AOL_FLAGS = ( + HyundaiFlags.CANFD + | HyundaiFlags.EV + | HyundaiFlags.HYBRID + | HyundaiFlags.CANFD_LKA_STEER_MSG + | HyundaiFlags.CANFD_LKA_STEER_MSG_ALT + | HyundaiFlags.CANFD_ALT_BUTTONS + | HyundaiFlags.CANFD_CAMERA_SCC + | HyundaiFlags.CANFD_ALT_GEARS + | HyundaiFlags.CANFD_ALT_GEARS_2 + | HyundaiFlags.CANFD_RADAR_SCC + | HyundaiFlags.CANFD_NO_RADAR_DISABLE + | HyundaiFlags.CCNC +) + +def _base_word(cp, *, experiences=(0,)): + owner = config_index(cp) + allowed = STOCK_AOL_FLAGS + try: + factory = CAR(cp.carFingerprint) + except ValueError: + return None + if ( + cp.brand != 'hyundai' + or factory == CAR.HYUNDAI_IONIQ_6 + or not factory.config.flags & HyundaiFlags.CANFD + or factory.config.flags & (HyundaiFlags.CANFD_ANGLE_STEERING | HyundaiFlags.NON_SCC) + or cp.passive + or cp.dashcamOnly + or cp.notCar + or cp.alternativeExperience not in experiences + or cp.steerControlType != CarParams.SteerControlType.torque + or not cp.flags & HyundaiFlags.CANFD + or int(cp.flags) & ~int(allowed) + or owner is None + ): + return None + # EV identity is static; hybrid fuel is selected by the actual 0xFA fingerprint. + if bool(cp.flags & HyundaiFlags.EV) != bool(factory.config.flags & HyundaiFlags.EV): + return None + ev, hybrid = bool(cp.flags & HyundaiFlags.EV), bool(cp.flags & HyundaiFlags.HYBRID) + lka = bool(cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG) + alt_lka = bool(cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG_ALT) + alt_buttons = bool(cp.flags & HyundaiFlags.CANFD_ALT_BUTTONS) + camera = bool(cp.flags & HyundaiFlags.CANFD_CAMERA_SCC) + if (ev and hybrid) or (alt_lka and not lka) or (lka and camera): + return None + bus = CanBus(cp) + offset = 4 * owner + if (bus.ACAN - offset, bus.ECAN - offset, bus.CAM - offset) != ((0, 1, 2) if lka else (1, 0, 2)): + return None + word = (1 if ev else 2 if hybrid else 0) | (16 if lka else 0) | (128 if alt_lka else 0) + word |= (32 if alt_buttons else 0) | (8 if camera else 0) + if cp.flags & HyundaiFlags.CCNC and camera: + word |= 0x0400 + if factory in CARNIVAL_CARS and alt_buttons: + word |= 0x2000 + return word if word | STOCK_AOL_MARKER in STOCK_AOL_WORDS else None + + +def qualified(cp, *, marked_only=False): + word = _base_word(cp) + if word is None or cp.openpilotLongitudinalControl or not cp.pcmCruise: + return False + return cp.safetyConfigs[config_index(cp)].safetyParam in ((word | STOCK_AOL_MARKER,) if marked_only else (word, word | STOCK_AOL_MARKER)) + + +LONG_AOL_WORDS = frozenset((0x0815, 0x0895)) +LONG_EV_CARS = frozenset((CAR.HYUNDAI_IONIQ_5, CAR.HYUNDAI_KONA_EV_2ND_GEN)) + + +def qualified_long(cp, *, marked_only=False): + if cp.carFingerprint == CAR.KIA_EV6: + from opendbc.car.hyundai.ev6_aol import qualified as ev6_qualified + return ev6_qualified(cp, marked_only=marked_only) + if cp.carFingerprint not in LONG_EV_CARS or not cp.openpilotLongitudinalControl or cp.pcmCruise: + return False + word = _base_word(cp, experiences=(0, 32)) + if word not in (0x11, 0x91): + return False + raw = int(cp.safetyConfigs[0].safetyParam) + return (raw == (word | 0x804) and cp.alternativeExperience == 32) or ( + not marked_only and raw == (word | 4) and cp.alternativeExperience == 0 + ) diff --git a/opendbc_repo/opendbc/car/hyundai/captured_adrv.py b/opendbc_repo/opendbc/car/hyundai/captured_adrv.py new file mode 100644 index 0000000000..a3446db003 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/captured_adrv.py @@ -0,0 +1,53 @@ +"""Immutable captured status shared by exact first-generation ADRV senders.""" +from dataclasses import dataclass + +from opendbc.car.can_definitions import CanData +from opendbc.car.crc import CRC16_XMODEM + + +def _checksum(data: bytes | bytearray) -> int: + crc = 0 + for byte in data[2:]: + crc = ((crc << 8) ^ CRC16_XMODEM[(crc >> 8) ^ byte]) & 0xFFFF + for byte in (0x51, 0): + crc = ((crc << 8) ^ CRC16_XMODEM[(crc >> 8) ^ byte]) & 0xFFFF + return crc ^ 0x9F5B + + +@dataclass(frozen=True) +class CapturedADRVTemplate: + data: bytes + + def __post_init__(self): + if not isinstance(self.data, bytes) or len(self.data) != 32: + raise ValueError('Captured ADRV template requires immutable 32-byte capture') + if not any(self.data[3:]): + raise ValueError('Captured ADRV capture has no opaque status body') + if int.from_bytes(self.data[:2], 'little') != _checksum(self.data): + raise ValueError('Captured ADRV capture checksum is invalid') + + @classmethod + def capture(cls, data: bytes): + return cls(data) + + @property + def counter(self) -> int: + return self.data[2] + + def _frame_data(self, frame: int, drive_gear: bool, bus: int) -> bytearray: + if type(frame) is not int or frame < 0: + raise ValueError('Captured ADRV frame must be a nonnegative integer') + if type(drive_gear) is not bool or type(bus) is not int or bus != 0: + raise ValueError('Captured ADRV requires physical Drive state and A-CAN bus zero') + data = bytearray(self.data) + data[2] = (self.counter + frame + 1) & 0xFF + data[3] = (data[3] & ~1) | int(drive_gear) + return data + + @staticmethod + def _finish(data: bytearray, bus: int) -> CanData: + data[:2] = _checksum(data).to_bytes(2, 'little') + return CanData(0x51, bytes(data), bus) + + def frame(self, frame: int, drive_gear: bool, bus: int = 0) -> CanData: + return self._finish(self._frame_data(frame, drive_gear, bus), bus) diff --git a/opendbc_repo/opendbc/car/hyundai/captured_adrv_startup.py b/opendbc_repo/opendbc/car/hyundai/captured_adrv_startup.py new file mode 100644 index 0000000000..4414ec2eff --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/captured_adrv_startup.py @@ -0,0 +1,107 @@ +"""Shared captured ADRV startup for explicitly selected first-generation cars.""" +import time + +from opendbc.car import Bus +from opendbc.can.parser import MAX_BAD_COUNTER +from opendbc.car.hyundai.ecu_startup import HyundaiECUStartup, Outcome +from opendbc.car.hyundai.hyundaicanfd import CanBus +from opendbc.car.hyundai.values import HyundaiFlags +from opendbc.car.hyundai.canfd_owner import topology_index + + +def first_generation(cp): + owner = topology_index(cp) + if owner is None: + return False + buses = CanBus(cp) + offset = 4 * owner + excluded = (HyundaiFlags.CANFD_ANGLE_STEERING | HyundaiFlags.CANFD_LKA_STEER_MSG_ALT | + HyundaiFlags.CANFD_ALT_BUTTONS | HyundaiFlags.CANFD_CAMERA_SCC | HyundaiFlags.CCNC | + HyundaiFlags.HYBRID) + return (cp.flags & HyundaiFlags.CANFD and + cp.flags & HyundaiFlags.EV and cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG and + not cp.flags & excluded and (buses.ACAN, buses.ECAN, buses.CAM) == (offset, offset + 1, offset + 2) and + not cp.passive and not cp.dashcamOnly and not cp.notCar) + + +class CapturedADRVStartup(HyundaiECUStartup): + def __init__(self, cp, callbacks, *, label, template_type): + buses = CanBus(cp) + super().__init__(cp, callbacks, address=0x730, bus=buses.ECAN, label=label) + self.template_bus = buses.ACAN + self.camera_bus = buses.CAM + self.template_type = template_type + self.template = None + + def _capture(self): + floor = time.clock_gettime_ns(time.CLOCK_BOOTTIME) + deadline = time.monotonic() + 1. + previous = None + while time.monotonic() < deadline: + for packet in self.callbacks[0]() or []: + stamp = int(getattr(packet, 'log_mono_time_ns', 0)) + now = time.clock_gettime_ns(time.CLOCK_BOOTTIME) + if not floor < stamp <= now or now - stamp > 150_000_000: + continue + for msg in packet: + if msg.src != self.template_bus or msg.address != 0x51: + continue + try: + candidate = self.template_type.capture(bytes(msg.dat)) + except ValueError: + previous = None + continue + if previous is not None and stamp > previous[0] and candidate.counter == (previous[1].counter + 1) % 256: + return candidate + previous = stamp, candidate + time.sleep(.005) + return None + + def prepare(self, *, admission): + # No ECU request is issued when current-session capture is unavailable. + self.template = self._capture() + return super().prepare(admission=admission if self.template is not None else lambda: False) + + def configure(self, ci): + super().configure(ci) + if self.outcome is Outcome.SENT_UNCONFIRMED: + if self.template is None: + raise RuntimeError(f'{self.label} sent startup lacks its current-instance template') + ci.CC.adrv_template = self.template + + def _sources(self, ci, now, *, floor): + state = ci.CS.out + if not state.canValid or state.canTimeout: + return False + pt = ci.can_parsers[Bus.pt] + cam = ci.can_parsers[Bus.cam] + if pt.bus != self.bus or cam.bus != self.camera_bus: + return False + names = ['ACCELERATOR', 'TCS', 'WHEEL_SPEEDS', 'MDPS', 'CRUISE_BUTTONS', ci.CS.gear_msg_canfd] + if not ci.CP.openpilotLongitudinalControl: + names.append('SCC_CONTROL') + for parser, sources in ((pt, set(names)), (cam, {'CAM_0x2a4'})): + for name in sources: + source = parser.message_states.get(parser.dbc.name_to_msg[name].address) + if source is None or not source.timestamps or source.counter_fail >= MAX_BAD_COUNTER: + return False + stamp = int(source.timestamps[-1]) + # Existing ordinary buttons can pause ~0.5s: retain the 1Hz host policy. + if not floor < stamp <= now or now - stamp > source.timeout_threshold: + return False + return True + + def _warm_stock(self, ci): + deadline = time.monotonic() + 3. + while time.monotonic() < deadline: + packets = self.callbacks[0]() + stamped = [(int(getattr(packet, 'log_mono_time_ns', 0)), list(packet)) for packet in packets] + if any(stamp <= 0 for stamp, _ in stamped): + raise RuntimeError(f'{self.label} stock warmup requires actual timestamped CAN') + ci.update(stamped) + if self._sources(ci, time.clock_gettime_ns(time.CLOCK_BOOTTIME), floor=self.floor_ns): + self.ready = True + return + time.sleep(.005) + self.outcome = Outcome.ABORT_UNCERTAIN + raise RuntimeError(f'{self.label} stock CANFD sources did not become fresh before publication') diff --git a/opendbc_repo/opendbc/car/hyundai/carcontroller.py b/opendbc_repo/opendbc/car/hyundai/carcontroller.py index 16abe2e25a..5cc0b7a4b3 100644 --- a/opendbc_repo/opendbc/car/hyundai/carcontroller.py +++ b/opendbc_repo/opendbc/car/hyundai/carcontroller.py @@ -1,12 +1,37 @@ +from opendbc.car.hyundai.canfd_angle_aol import qualified as qualified_angle_aol, command_allowed as angle_aol_command_allowed +from opendbc.car.hyundai.ev9_longitudinal import ( + EV9LongitudinalPolicy, + LegacyAngleEnvelope, + lateral_request_allowed, + qualified as ev9_long_qualified, + update_blindspot_warning, + BlindspotWarningOutput, +) +from opendbc.car.hyundai import ev9_long_sender +from opendbc.car.common.filter_simple import FirstOrderFilter +from opendbc.car.lateral import get_max_angle_vm, get_max_angle_delta_vm import numpy as np from opendbc.can import CANPacker -from opendbc.car import Bus, DT_CTRL, make_tester_present_msg, structs -from opendbc.car.lateral import apply_driver_steer_torque_limits, common_fault_avoidance +from opendbc.car import Bus, DT_CTRL, make_tester_present_msg, rate_limit, structs +from opendbc.car.lateral import apply_driver_steer_torque_limits, apply_steer_angle_limits_vm, common_fault_avoidance from opendbc.car.common.conversions import Conversions as CV from opendbc.car.hyundai import hyundaicanfd, hyundaican from opendbc.car.hyundai.hyundaicanfd import CanBus -from opendbc.car.hyundai.values import HyundaiFlags, Buttons, CarControllerParams, CAR +from opendbc.car.hyundai.ray_pedal import create_ray_pedal_command, ray_pedal_gas, ray_pedal_enabled +from opendbc.car.hyundai.ioniq6_longitudinal import Ioniq6LongitudinalPolicy +from opendbc.car.hyundai.canfd_dash_icons import CanFDDashIcons +from opendbc.car.hyundai.g90_longitudinal import G90LongitudinalPolicy +from opendbc.car.hyundai.gv70_longitudinal import is_gv70, scc_request, suppress_stock_cancel +from opendbc.car.hyundai.g90_lead import G90LeadState, eligible as g90_lead_eligible +from opendbc.car.hyundai.gv70_camera_lead import eligible as gv70_lead_eligible +from opendbc.car.hyundai.canfd_lead import select_lead, ABSENT +from opendbc.car.hyundai.torque_ev_scc import eligible as torque_ev_scc_eligible, options as torque_ev_scc_options, clock_boot_ns, fallback_lead +from opendbc.car.hyundai.ccnc_ev_stock import replacement_requested +from opendbc.car.hyundai.values import is_blended, HyundaiFlags, HyundaiSafetyFlags, Buttons, CarControllerParams, CAR from opendbc.car.interfaces import CarControllerBase +from opendbc.car.vehicle_model import VehicleModel + +EV9_OP_LONG_ANGLE_TARGET_MAX = 140.0 VisualAlert = structs.CarControl.HUDControl.VisualAlert LongCtrlState = structs.CarControl.Actuators.LongControlState @@ -24,13 +49,30 @@ MAX_ANGLE_CONSECUTIVE_FRAMES = 2 CANCEL_BUTTON_DELAY_FRAMES = 10 +def angle_torque_reduction_gain(steering_torque, v_ego, lat_active, last_gain): + # The frozen ADAS angle protocol scales driver torque into a reduction gain. + if lat_active: + ceiling = np.interp(v_ego, [0.5, 1.5], [1.0, 0.85]) + shelf = np.interp(v_ego, [2.0, 11.0], [0.45, 0.6]) + floor = np.interp(v_ego, [2.0, 22.0], [0.1, 0.3]) + bp1 = np.interp(v_ego, [2.0, 11.0], [75.0, 125.0]) + bp2 = np.interp(v_ego, [2.0, 11.0], [125.0, 150.0]) + bp3 = np.interp(v_ego, [2.0, 11.0], [175.0, 275.0]) + bp4 = np.interp(v_ego, [2.0, 22.0], [400.0, 700.0]) + target = np.interp(abs(steering_torque), [bp1, bp2, bp3, bp4], [ceiling, shelf, shelf, floor]) + else: + target = 0.0 + return round(rate_limit(target, last_gain, -0.014, 0.004) / 0.004) * 0.004 + + def process_hud_alert(enabled, fingerprint, hud_control): sys_warning = (hud_control.visualAlert in (VisualAlert.steerRequired, VisualAlert.ldw)) # initialize to no line visible # TODO: this is not accurate for all cars sys_state = 1 - if hud_control.leftLaneVisible and hud_control.rightLaneVisible or sys_warning: # HUD alert only display when LKAS status is active + if hud_control.leftLaneVisible and hud_control.rightLaneVisible or sys_warning: + # HUD alert only display when LKAS status is active sys_state = 3 if enabled or sys_warning else 4 elif hud_control.leftLaneVisible: sys_state = 5 @@ -55,20 +97,77 @@ class CarController(CarControllerBase): self.params = CarControllerParams(CP) self.packer = CANPacker(dbc_names[Bus.pt]) self.angle_limit_counter = 0 + self.angle_vm = VehicleModel(CP) if CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING else None + self.apply_angle_last = 0.0 + self.angle_gain_last = 0.0 self.accel_last = 0 self.apply_torque_last = 0 self.car_fingerprint = CP.carFingerprint self.last_button_frame = 0 self.cancel_counter = 0 + self.blended_longitudinal = None + self.blended_previous_lateral = False + self.blended_disengage_frame = None + self.last_ccnc_161_ts_ns = 0 + self.last_ccnc_162_ts_ns = 0 + self.last_ccnc_now_ns = 0 + self.ccnc_baselined = False + self.ioniq6_longitudinal = (Ioniq6LongitudinalPolicy() + if CP.carFingerprint == CAR.HYUNDAI_IONIQ_6 and CP.openpilotLongitudinalControl else None) + self.ev9_longitudinal = EV9LongitudinalPolicy() if ev9_long_qualified(CP) else None + self.ev9_lead_inputs = None + self.ev9_legacy_envelope = LegacyAngleEnvelope() if self.ev9_longitudinal is not None else None + self.ev9_angle_filter = FirstOrderFilter(0.0, 0.2, DT_CTRL) if self.ev9_longitudinal is not None else None + self.ioniq6_accel_request = None + self.ioniq6_lead_inputs = None + self.torque_ev_scc_enabled = torque_ev_scc_eligible(CP) + self.torque_ev_lead_inputs = None + self.torque_ev_floor_boot_ns = clock_boot_ns() if self.torque_ev_scc_enabled else 0 + self.adrv_template = None + self.gv70_stock_fallback = False + self.gv70_lead_inputs = None + self.gv70_lead_enabled = gv70_lead_eligible(CP) + self.g90_lead_inputs = None + self.g90_lead_state = G90LeadState() if g90_lead_eligible(CP) else None + self.g90_longitudinal = (G90LongitudinalPolicy() + if CP.carFingerprint == CAR.GENESIS_G90 and CP.openpilotLongitudinalControl else None) + ioniq6_status = (self.ioniq6_longitudinal is not None and + int(CP.safetyConfigs[-1].safetyParam) in (0x8015, 0x8095, 0x8815, 0x8895)) + self.ioniq6_bsm_enabled = ioniq6_status + self.canfd_dash_icons = CanFDDashIcons() if ioniq6_status or self.torque_ev_scc_enabled else None + self.ioniq6_bsm_counter = 0 + self.ioniq6_bsm_last_can_ns = 0 + self._ray_pedal = ray_pedal_enabled(CP) + self._ray_pedal_gas_last = 0.0 + self._ray_pedal_packer = CANPacker("hyundai_kia_ray_pedal") if self._ray_pedal else None + self._ray_lfa_8byte = CP.carFingerprint == CAR.KIA_RAY_EV and bool(CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CAN_REFRESH_MSGS) + self._ray_lfa_packer = CANPacker("hyundai_kia_ray_lfa") if self._ray_lfa_8byte else None + self._ray_lkas11_active = False + self._ray_lfa_icon = 0 + self._ray_prev_lat_active = False + self._ray_disengage_frame = None def update(self, CC, CS, now_nanos): actuators = CC.actuators + if self.CP.carFingerprint == CAR.KIA_EV6_2025 and self.CP.dashcamOnly: + return actuators.as_builder(), [] hud_control = CC.hudControl + if self.CP.carFingerprint == CAR.KIA_RAY_EV: + if CC.latActive: + self._ray_disengage_frame = None + elif self._ray_prev_lat_active: + self._ray_disengage_frame = self.frame - 1 + disengaging = self._ray_disengage_frame is not None and (self.frame - self._ray_disengage_frame) * DT_CTRL < 1.0 + self._ray_lfa_icon = 2 if CC.enabled or CC.latActive else 3 if disengaging else 0 + self._ray_prev_lat_active = CC.latActive # steering torque + if self.CP.flags & HyundaiFlags.CANFD and self.angle_vm is None: + self.params = CarControllerParams(self.CP, CS.out.vEgoRaw) new_torque = int(round(actuators.torque * self.params.STEER_MAX)) - apply_torque = apply_driver_steer_torque_limits(new_torque, self.apply_torque_last, CS.out.steeringTorque, self.params) + apply_torque = (0 if self.angle_vm is not None else + apply_driver_steer_torque_limits(new_torque, self.apply_torque_last, CS.out.steeringTorque, self.params)) # >90 degree steering fault prevention self.angle_limit_counter, apply_steer_req = common_fault_avoidance(abs(CS.out.steeringAngleDeg) >= MAX_ANGLE, CC.latActive, @@ -80,17 +179,38 @@ class CarController(CarControllerBase): self.apply_torque_last = apply_torque + # Original lead hysteresis counts100Hz controller updates, not50Hz SCC. + lead_data = None + if self.g90_lead_state is not None and self.g90_lead_inputs is not None: + lead_data = self.g90_lead_state.update(self.g90_lead_inputs.update(), hud_control.leadVisible) + # accel + longitudinal - accel = float(np.clip(actuators.accel, CarControllerParams.ACCEL_MIN, CarControllerParams.ACCEL_MAX)) + accel = float(np.clip(actuators.accel, self.params.ACCEL_MIN, self.params.ACCEL_MAX)) stopping = actuators.longControlState == LongCtrlState.stopping + if self.ev9_longitudinal is not None: + accel = self.ev9_longitudinal.update(self.frame, float(np.clip(actuators.accel, -3.5, 3.5)), CS.out.vEgo, + CS.out.aEgo, actuators.longControlState, CC.enabled, CC.cruiseControl.override) + if self.ioniq6_longitudinal is not None: + self.ioniq6_accel_request = self.ioniq6_longitudinal.update( + self.frame, active=CC.enabled and CC.longActive, override=CC.cruiseControl.override, + accel=accel, speed=CS.out.vEgo, measured_accel=CS.out.aEgo, + control_state=actuators.longControlState, last_sent_accel=self.accel_last) + accel = self.ioniq6_accel_request.accel + stopping = self.ioniq6_accel_request.stopping + if self.g90_longitudinal is not None: + accel = self.g90_longitudinal.update(accel, CS.out.vEgo, actuators.longControlState, self.CP.openpilotLongitudinalControl) set_speed_in_units = hud_control.setSpeed * (CV.MS_TO_KPH if CS.is_metric else CV.MS_TO_MPH) can_sends = [] # *** common hyundai stuff *** + if self.blended_longitudinal is not None: + can_sends.extend(self.blended_longitudinal.tester_present(self.frame)) + # tester present - w/ no response (keeps relevant ECU disabled) - if self.frame % 100 == 0 and not (self.CP.flags & HyundaiFlags.CANFD_CAMERA_SCC) and self.CP.openpilotLongitudinalControl: + if (self.frame % 100 == 0 and not (self.CP.flags & HyundaiFlags.CANFD_CAMERA_SCC) and + self.CP.openpilotLongitudinalControl and not self._ray_pedal and self.blended_longitudinal is None): # for longitudinal control, either radar or ADAS driving ECU addr, bus = 0x7d0, self.CAN.ECAN if self.CP.flags & HyundaiFlags.CANFD else 0 if self.CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG.value: @@ -108,40 +228,77 @@ class CarController(CarControllerBase): # *** CAN/CAN FD specific *** if self.CP.flags & HyundaiFlags.CANFD: can_sends.extend(self.create_canfd_msgs(apply_steer_req, apply_torque, set_speed_in_units, accel, - stopping, hud_control, CS, CC)) + stopping, hud_control, CS, CC, now_nanos)) else: # Hold torque with induced temporary fault when cutting the actuation bit # FIXME: we don't use this with CAN FD? torque_fault = CC.latActive and not apply_steer_req can_sends.extend(self.create_can_msgs(apply_steer_req, apply_torque, torque_fault, set_speed_in_units, accel, - stopping, hud_control, actuators, CS, CC)) + stopping, hud_control, actuators, CS, CC, lead_data=lead_data)) new_actuators = actuators.as_builder() new_actuators.torque = apply_torque / self.params.STEER_MAX new_actuators.torqueOutputCan = apply_torque + if self.angle_vm is not None: + new_actuators.steeringAngleDeg = self.apply_angle_last new_actuators.accel = accel self.frame += 1 return new_actuators, can_sends - def create_can_msgs(self, apply_steer_req, apply_torque, torque_fault, set_speed_in_units, accel, stopping, hud_control, actuators, CS, CC): + def create_can_msgs(self, apply_steer_req, apply_torque, torque_fault, set_speed_in_units, accel, stopping, + hud_control, actuators, CS, CC, *, lead_data=None): can_sends = [] # HUD messages - sys_warning, sys_state, left_lane_warning, right_lane_warning = process_hud_alert(CC.enabled, self.car_fingerprint, + hud_enabled = CC.enabled or (self.CP.carFingerprint == CAR.KIA_STINGER_2022 and CC.latActive) + sys_warning, sys_state, left_lane_warning, right_lane_warning = process_hud_alert(hud_enabled, self.car_fingerprint, hud_control) - can_sends.append(hyundaican.create_lkas11(self.packer, self.frame, self.CP, apply_torque, apply_steer_req, - torque_fault, CS.lkas11, sys_warning, sys_state, CC.enabled, + if is_blended(self.CP): + if CC.latActive: + self.blended_disengage_frame = None + elif self.blended_previous_lateral: + self.blended_disengage_frame = (self.frame - 1 if self.blended_longitudinal is not None + else self.frame) + self.blended_previous_lateral = CC.latActive + disengaging = self.blended_disengage_frame is not None and self.frame - self.blended_disengage_frame < 100 + icon = 2 if CC.enabled or CC.latActive else 3 if disengaging else 1 + if self.CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG: + can_sends.append(hyundaican.create_blended_steering(self.packer, self.CAN, apply_torque, apply_steer_req, icon)) + if self.blended_longitudinal is not None and self.blended_longitudinal.owner.active(): + can_sends.extend(hyundaican.create_lkas11_can_canfd_blended( + self.packer, self.frame, self.CP, apply_torque, apply_steer_req, torque_fault, CS.lkas11, + sys_warning, sys_state, CC.enabled, hud_control.leftLaneVisible, hud_control.rightLaneVisible, + left_lane_warning, right_lane_warning, {}, include_alerts=False, counter_mod=0xF, + fcw_opt_usm=2 if apply_steer_req or icon == 3 else 1)) + if (self.frame % 5 == 0 and CS.out.canValid and not CS.out.canTimeout and + CS.blended_camera_current): + can_sends.append(hyundaicanfd.create_suppress_lfa(self.packer, self.CAN, CS.lfa_block_msg, False)) + else: + can_sends.extend(hyundaican.create_lkas11_can_canfd_blended( + self.packer, self.frame, self.CP, apply_torque, apply_steer_req, torque_fault, CS.lkas11, + sys_warning, sys_state, CC.enabled, hud_control.leftLaneVisible, hud_control.rightLaneVisible, + left_lane_warning, right_lane_warning, CS.msg_364)) + elif self.CP.carFingerprint != CAR.KIA_RAY_EV or self._ray_lkas11_active: + can_sends.append(hyundaican.create_lkas11(self.packer, self.frame, self.CP, apply_torque, apply_steer_req, + torque_fault, CS.lkas11, sys_warning, sys_state, hud_enabled, hud_control.leftLaneVisible, hud_control.rightLaneVisible, - left_lane_warning, right_lane_warning)) + left_lane_warning, right_lane_warning, + ray_lka_icon=(self._ray_lfa_icon or 1) if self.CP.carFingerprint == CAR.KIA_RAY_EV else None)) + if self.CP.carFingerprint == CAR.KIA_RAY_EV: + self._ray_lkas11_active = True # Button messages - if not self.CP.openpilotLongitudinalControl: - if self.cancel_counter > CANCEL_BUTTON_DELAY_FRAMES: + if not self.CP.openpilotLongitudinalControl or self._ray_pedal: + if self._ray_pedal and CC.enabled and CS.out.cruiseState.enabled: + if (self.frame - self.last_button_frame) * DT_CTRL > 0.04: + can_sends.append(hyundaican.create_clu11(self.packer, self.frame, CS.clu11, Buttons.CANCEL, self.CP)) + self.last_button_frame = self.frame + elif self.cancel_counter > CANCEL_BUTTON_DELAY_FRAMES: can_sends.append(hyundaican.create_clu11(self.packer, self.frame, CS.clu11, Buttons.CANCEL, self.CP)) - elif CC.cruiseControl.resume: + elif CC.cruiseControl.resume and not self._ray_pedal and self.blended_longitudinal is None: # send resume at a max freq of 10Hz if (self.frame - self.last_button_frame) * DT_CTRL > 0.1: # send 25 messages at a time to increases the likelihood of resume being accepted @@ -149,64 +306,268 @@ class CarController(CarControllerBase): if (self.frame - self.last_button_frame) * DT_CTRL >= 0.15: self.last_button_frame = self.frame - if self.frame % 2 == 0 and self.CP.openpilotLongitudinalControl: + if self._ray_pedal and self.frame % 4 == 0: + pedal_active = (CC.longActive and CS.ray_pedal_valid and CS.ray_pedal_state == 0 and + not CC.cruiseControl.override and not CS.out.gasPressed and not CS.out.brakePressed) + self._ray_pedal_gas_last = ray_pedal_gas(self._ray_pedal_gas_last, CS.out.vEgo, accel, hud_control.setSpeed) if pedal_active else 0.0 + can_sends.append(create_ray_pedal_command(self._ray_pedal_packer, self._ray_pedal_gas_last, (self.frame // 4) & 0xF)) + + if self.blended_longitudinal is not None: + can_sends.extend(self.blended_longitudinal.commands(self.frame, CC, CS, accel, stopping, set_speed_in_units)) + + if self.frame % 2 == 0 and self.CP.openpilotLongitudinalControl and not self._ray_pedal and self.blended_longitudinal is None: # TODO: unclear if this is needed jerk = 3.0 if actuators.longControlState == LongCtrlState.pid else 1.0 use_fca = self.CP.flags & HyundaiFlags.USE_FCA.value can_sends.extend(hyundaican.create_acc_commands(self.packer, CC.enabled, accel, jerk, int(self.frame / 2), hud_control, set_speed_in_units, stopping, - CC.cruiseControl.override, use_fca, self.CP)) + CC.cruiseControl.override, use_fca, self.CP, lead_data=lead_data)) # 20 Hz LFA MFA message - if self.frame % 5 == 0 and self.CP.flags & HyundaiFlags.SEND_LFA.value: - can_sends.append(hyundaican.create_lfahda_mfc(self.packer, CC.enabled)) + if self.frame % 5 == 0 and (self.CP.flags & HyundaiFlags.SEND_LFA.value or + self.blended_longitudinal is not None and self.CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG): + if self._ray_lfa_8byte: + can_sends.append(hyundaican.create_ray_lfahda_mfc(self._ray_lfa_packer, CC.latActive, self._ray_lfa_icon)) + elif is_blended(self.CP): + lfa_icon = 2 if CC.enabled or CC.latActive else 3 if disengaging else 0 + can_sends.append(hyundaican.create_blended_lfahda(self.packer, self.CAN, self.frame, lfa_icon)) + else: + can_sends.append(hyundaican.create_lfahda_mfc(self.packer, CC.enabled)) # 5 Hz ACC options - if self.frame % 20 == 0 and self.CP.openpilotLongitudinalControl: + if self.frame % 20 == 0 and self.CP.openpilotLongitudinalControl and not self._ray_pedal and self.blended_longitudinal is None: can_sends.extend(hyundaican.create_acc_opt(self.packer, self.CP)) # 2 Hz front radar options - if self.frame % 50 == 0 and self.CP.openpilotLongitudinalControl: + if self.frame % 50 == 0 and self.CP.openpilotLongitudinalControl and not self._ray_pedal and self.blended_longitudinal is None: can_sends.append(hyundaican.create_frt_radar_opt(self.packer)) return can_sends - def create_canfd_msgs(self, apply_steer_req, apply_torque, set_speed_in_units, accel, stopping, hud_control, CS, CC): + def create_canfd_msgs(self, apply_steer_req, apply_torque, set_speed_in_units, accel, stopping, hud_control, CS, CC, now_nanos): can_sends = [] + canfd_lfa_icon = (self.canfd_dash_icons.update(self.frame, CC.enabled, CC.latActive) + if self.canfd_dash_icons is not None else None) lka_steering = self.CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG lka_steering_long = lka_steering and self.CP.openpilotLongitudinalControl + ccnc_non_lka = bool(self.CP.flags & HyundaiFlags.CCNC and not lka_steering) + if ccnc_non_lka: + # A new controller instance or reversed clock must wait for a fresh pair. + if not self.ccnc_baselined or now_nanos < self.last_ccnc_now_ns: + self.last_ccnc_161_ts_ns = CS.ccnc_161_ts_ns + self.last_ccnc_162_ts_ns = CS.ccnc_162_ts_ns + self.ccnc_baselined = True + self.last_ccnc_now_ns = now_nanos + + ev9_long = self.ev9_longitudinal is not None + angle_aol = qualified_angle_aol(self.CP, marked_only=True) + angle_aol_active = angle_aol and CC.latActive and angle_aol_command_allowed(self.CP, CS.out) + ccnc_ev_stock = self.CP.carFingerprint in (CAR.HYUNDAI_IONIQ_5_PE, CAR.KIA_EV9) and not ev9_long + ccnc_ev_replacement = replacement_requested(self.CP, CC, CS.out) if ccnc_ev_stock else False # steering control - can_sends.extend(hyundaicanfd.create_steering_messages(self.packer, self.CP, self.CAN, CC.enabled, apply_steer_req, apply_torque)) + if ev9_long: + drive = CS.out.gearShifter == structs.CarState.GearShifter.drive + measured = CS.angle_steering_angle + safety_speed = max(CS.out.vEgoRaw - 1.0, 1.0) + bound = min(get_max_angle_vm(safety_speed, self.ev9_legacy_envelope, self.params), + get_max_angle_vm(safety_speed, self.angle_vm, self.params)) + ev9_lateral_allowed = lateral_request_allowed(CC, CS.out, CS.angle_steering_fault) + active = bool(ev9_lateral_allowed and CC.latActive and abs(measured) <= bound and abs(self.apply_angle_last) <= bound) + self.ev9_angle_filter.update_alpha(float(np.interp(CS.out.vEgo, [5.0, 10.0, 20.0], [0.2, 0.1, 0.0]))) + desired = self.ev9_angle_filter.update(float(np.clip(CC.actuators.steeringAngleDeg, + -EV9_OP_LONG_ANGLE_TARGET_MAX, EV9_OP_LONG_ANGLE_TARGET_MAX))) + # Preserve the original two VM passes and its final tolerance-envelope delta. + angle = apply_steer_angle_limits_vm(desired, self.apply_angle_last, CS.out.vEgoRaw, measured, + active, self.params, self.angle_vm) + angle = apply_steer_angle_limits_vm(angle or desired, self.apply_angle_last, CS.out.vEgoRaw, measured, + active, self.params, self.ev9_legacy_envelope) + if active: + delta = min(get_max_angle_delta_vm(safety_speed, self.ev9_legacy_envelope, self.params), + self.params.ANGLE_LIMITS.MAX_ANGLE_RATE) + angle = float(np.clip(angle, self.apply_angle_last - delta, self.apply_angle_last + delta)) + # Exact typed EV9 safety envelope is an additional bound, not a new tune. + self.apply_angle_last = apply_steer_angle_limits_vm(angle, self.apply_angle_last, safety_speed, measured, + active, self.params, self.angle_vm) + self.angle_gain_last = angle_torque_reduction_gain(CS.out.steeringTorque, CS.out.vEgoRaw, active, self.angle_gain_last) + if not active: + self.ev9_angle_filter.x = self.apply_angle_last + # Native checks health even for an inactive CB; do not emit stale measurements. + if CS.out.canValid and not CS.out.canTimeout: + if drive: + can_sends.append(ev9_long_sender.create_angle_adas_cmd(self.packer, self.CAN, self.apply_angle_last, + active, self.angle_gain_last)) + else: + can_sends.extend(ev9_long_sender.create_inactive_angle_steering_messages(self.packer, self.CAN, + self.apply_angle_last)) + elif self.angle_vm is not None: + drive_gear_required = self.CP.carFingerprint in (CAR.HYUNDAI_AZERA_HEV_7TH_GEN, + CAR.KIA_SORENTO_HEV_4TH_GEN_LFA2, + CAR.KIA_SPORTAGE_HEV_2026, + CAR.HYUNDAI_SANTA_FE_HEV_5TH_GEN, + CAR.KIA_SPORTAGE_2026) + angle_active = bool(CC.latActive and CS.out.cruiseState.enabled and not CS.out.brakePressed and + not CS.out.gasPressed and not CS.out.steerFaultTemporary and + (not (self.CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG_ALT or drive_gear_required) or + CS.out.gearShifter == structs.CarState.GearShifter.drive)) + if angle_aol: + angle_active = angle_aol_active + if ccnc_ev_stock: + angle_active = ccnc_ev_replacement + desired_angle = float(np.clip(CC.actuators.steeringAngleDeg, -self.params.ANGLE_LIMITS.STEER_ANGLE_MAX, + self.params.ANGLE_LIMITS.STEER_ANGLE_MAX)) + safety_speed = max(CS.out.vEgoRaw - 1.0, 1.0) + self.apply_angle_last = apply_steer_angle_limits_vm( + desired_angle, self.apply_angle_last, safety_speed, CS.out.steeringAngleDeg, + angle_active, self.params, self.angle_vm) + self.angle_gain_last = angle_torque_reduction_gain(CS.out.steeringTorque, CS.out.vEgoRaw, + angle_active, self.angle_gain_last) + if not ccnc_ev_stock or ccnc_ev_replacement: + can_sends.extend( + hyundaicanfd.create_angle_steering_messages( + self.packer, + self.CP, + self.CAN, + CC.enabled or (angle_aol and angle_active), + angle_active, + self.apply_angle_last, + self.angle_gain_last, + CS.angle_lkas_status, + CS.out.steeringAngleDeg, + ) + ) + else: + steering_status_active = CC.enabled + if self.ioniq6_longitudinal is not None and self.CP.safetyConfigs[-1].safetyParam in (0x8815, 0x8895): + # AOL may steer while ordinary cruise engagement remains off. + steering_status_active |= CC.latActive + can_sends.extend(hyundaicanfd.create_steering_messages(self.packer, self.CP, self.CAN, + steering_status_active, apply_steer_req, apply_torque)) # prevent LFA from activating on LKA steering cars by sending "no lane lines detected" to ADAS ECU - if self.frame % 5 == 0 and lka_steering: + if ( + self.frame % 5 == 0 + and lka_steering + and (not angle_aol or angle_aol_active) + and (not ccnc_ev_stock or ccnc_ev_replacement) + and (not ev9_long or ev9_lateral_allowed) + ): can_sends.append(hyundaicanfd.create_suppress_lfa(self.packer, self.CAN, CS.lfa_block_msg, self.CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG_ALT)) # LFA and HDA icons - if self.frame % 5 == 0 and (not lka_steering or lka_steering_long): - can_sends.append(hyundaicanfd.create_lfahda_cluster(self.packer, self.CAN, CC.enabled)) + if self.frame % 5 == 0 and (not lka_steering or lka_steering_long) and not ev9_long: + if ccnc_non_lka: + # Re-emit one paired camera display sample at most once. Missing or + # held optional frames must not be replaced by a zero-filled payload. + pair_ts = max(CS.ccnc_161_ts_ns, CS.ccnc_162_ts_ns) + fresh_pair = (CS.ccnc_161_ts_ns > self.last_ccnc_161_ts_ns and + CS.ccnc_162_ts_ns > self.last_ccnc_162_ts_ns and + pair_ts <= now_nanos and + now_nanos - min(CS.ccnc_161_ts_ns, CS.ccnc_162_ts_ns) <= 50_000_000 and + abs(CS.ccnc_161_ts_ns - CS.ccnc_162_ts_ns) <= 50_000_000) + if fresh_pair: + support_current = (CS.ccnc_1b5_ts_ns > 0 and CS.ccnc_1b5_ts_ns <= now_nanos and + abs(CS.ccnc_1b5_ts_ns - pair_ts) <= 50_000_000) + can_sends.extend(hyundaicanfd.create_ccnc(self.packer, self.CAN, self.CP.openpilotLongitudinalControl, + CC.enabled, hud_control, CC.leftBlinker, CC.rightBlinker, + CS.ccnc_161.copy(), CS.ccnc_162.copy(), CS.ccnc_1b5 if support_current else None, + CS.is_metric, CS.out, CS.out.cruiseState.available, CC.latActive)) + self.last_ccnc_161_ts_ns = CS.ccnc_161_ts_ns + self.last_ccnc_162_ts_ns = CS.ccnc_162_ts_ns + else: + can_sends.append(hyundaicanfd.create_lfahda_cluster(self.packer, self.CAN, CC.enabled, lfa_icon=canfd_lfa_icon)) # blinkers if lka_steering and self.CP.flags & HyundaiFlags.CANFD_ENABLE_BLINKERS: can_sends.extend(hyundaicanfd.create_spas_messages(self.packer, self.CAN, CC.leftBlinker, CC.rightBlinker)) if self.CP.openpilotLongitudinalControl: - if lka_steering: - can_sends.extend(hyundaicanfd.create_adrv_messages(self.packer, self.CAN, self.frame)) - else: + if ev9_long: + left_escalated = CS.left_blindspot_from_radar and CC.leftBlinker and not CC.rightBlinker + right_escalated = CS.right_blindspot_from_radar and CC.rightBlinker and not CC.leftBlinker + left_warning, right_warning = BlindspotWarningOutput(), BlindspotWarningOutput() + if self.frame % 5 == 0: + left_warning = update_blindspot_warning(self.ev9_longitudinal.left_warning, left_escalated, CC.leftBlinker) + right_warning = update_blindspot_warning(self.ev9_longitudinal.right_warning, right_escalated, CC.rightBlinker) + can_sends.extend(ev9_long_sender.create_ccnc_adrv_messages( + self.packer, self.CP, self.CAN, self.frame, CC.enabled, CS.out.cruiseState.available, hud_control, CS.out, + CS.is_metric, CC.latActive or CC.enabled, active and self.angle_gain_last != 0.0 and not CS.out.steeringPressed, + CS.left_blindspot_from_radar, CS.right_blindspot_from_radar, drive_gear=drive, hba_icon=CS.hba_icon, + left_escalated=left_escalated, right_escalated=right_escalated, + left_warning_lamp=left_warning.mirror_lamp_active, right_warning_lamp=right_warning.mirror_lamp_active, + left_sound_active=left_warning.sound_active, right_sound_active=right_warning.sound_active)) + can_sends.append(ev9_long_sender.create_radar_heartbeat(self.frame, CS.out.brakePressed, CS.out.gasPressed)) + elif lka_steering: + can_sends.extend(hyundaicanfd.create_adrv_messages( + self.packer, self.CAN, self.frame, template=self.adrv_template, + drive_gear=CS.out.gearShifter == structs.CarState.GearShifter.drive, + speed=CS.out.vEgoRaw if self.CP.carFingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN else None)) + if self.ioniq6_longitudinal is not None and self.frame % 4 == 0: + can_sends.append(hyundaicanfd.create_ioniq6_radar_heartbeat(self.frame // 4, CS.out.brakePressed, CS.out.gasPressed)) + if self.ioniq6_bsm_enabled and self.frame % 5 == 0: + sources = getattr(CS, "ioniq6_bsm_sources", None) + can_now_ns = getattr(CS, "ioniq6_bsm_now_ns", 0) + status = (sources.status(can_now_ns) if sources is not None and can_now_ns > self.ioniq6_bsm_last_can_ns else None) + if status is not None: + can_sends.extend(hyundaicanfd.create_ioniq6_blindspot_status(self.ioniq6_bsm_counter, status)) + self.ioniq6_bsm_counter = (self.ioniq6_bsm_counter + 1) & 0xff + self.ioniq6_bsm_last_can_ns = can_now_ns + elif not ccnc_non_lka: can_sends.extend(hyundaicanfd.create_fca_warning_light(self.packer, self.CAN, self.frame)) if self.frame % 2 == 0: - can_sends.append(hyundaicanfd.create_acc_control(self.packer, self.CAN, CC.enabled, self.accel_last, accel, stopping, CC.cruiseControl.override, - set_speed_in_units, hud_control)) + acc_enabled = CC.enabled + acc_options = {} + if self.torque_ev_scc_enabled: + lead = (self.torque_ev_lead_inputs.update(CS.torque_ev_camera_lead, hud_control.leadVisible, now_nanos) + if self.torque_ev_lead_inputs is not None else + fallback_lead(CS.torque_ev_camera_lead, hud_control.leadVisible, self.torque_ev_floor_boot_ns)) + acc_options = torque_ev_scc_options(CS.out.cruiseState.available, CC.actuators.longControlState, lead) + elif is_gv70(self.CP): + request = scc_request(CC.enabled, CC.cruiseControl.override, stopping, accel, self.accel_last) + accel = request.accel + acc_options = {"direct_accel": True, "raw_accel": request.raw_accel, + "jerk_upper": request.jerk_upper, "jerk_lower": request.jerk_lower} + elif self.ioniq6_accel_request is not None: + lead = (self.ioniq6_lead_inputs.update(CS.ioniq6_camera_lead, hud_control.leadVisible, now_nanos) + if self.ioniq6_lead_inputs is not None else + select_lead(None, None, hud_visible=hud_control.leadVisible, observed_ns=now_nanos, epoch_floor_ns=0)) + acc_enabled = CC.enabled + acc_options = {"direct_accel": True, "main_mode_acc": int(CS.out.cruiseState.available), + "jerk_upper": self.ioniq6_accel_request.jerk_upper, "jerk_lower": self.ioniq6_accel_request.jerk_lower, + "lead_visible": lead.visible, "lead_distance": lead.distance, "lead_rel_speed": lead.relative_speed} + if self.gv70_lead_enabled: + lead = self.gv70_lead_inputs.update(CS.gv70_camera_lead, hud_control.leadVisible, now_nanos) \ + if self.gv70_lead_inputs is not None else ABSENT + acc_options.update(lead_distance=lead.distance, lead_rel_speed=lead.relative_speed, lead_visible=lead.visible) + if ev9_long: + tuning = self.ev9_longitudinal.tuning + dynamic = CC.actuators.longControlState in (LongCtrlState.starting, LongCtrlState.pid, LongCtrlState.stopping) + jerk_lower = tuning.jerk_lower if dynamic else (5.0 if CC.enabled else 1.0) + jerk_upper = tuning.jerk_upper if dynamic else (3.0 if CC.actuators.longControlState == LongCtrlState.pid else 1.0) + zero = self.ev9_longitudinal.update_stop(CC.enabled, CC.cruiseControl.override, stopping, CS.out.vEgo) + if zero is not None: + accel = zero + lead = (self.ev9_lead_inputs.update(CS.ev9_camera_lead, hud_control.leadVisible, now_nanos) + if self.ev9_lead_inputs is not None else ABSENT) + lead_visible, lead_distance, lead_rel_speed = lead.visible, lead.distance, lead.relative_speed + accel = float(np.clip(accel, -3.5, 2.2)) + can_sends.append(ev9_long_sender.create_ccnc_acc_control( + self.packer, self.CAN, CC.enabled, accel, self.ev9_longitudinal.stop.stop_request, + self.ev9_longitudinal.stop.cruise_standstill, CC.cruiseControl.override, set_speed_in_units, + int(CS.out.cruiseState.available), lead_distance, lead_rel_speed, lead_visible, CS.out.vEgo, + jerk_lower=jerk_lower, jerk_upper=jerk_upper)) + else: + can_sends.append(hyundaicanfd.create_acc_control(self.packer, self.CAN, acc_enabled, self.accel_last, accel, stopping, CC.cruiseControl.override, + set_speed_in_units, hud_control, **acc_options)) self.accel_last = accel else: # button presses if (self.frame - self.last_button_frame) * DT_CTRL > 0.25: # cruise cancel - if CC.cruiseControl.cancel: + if CC.cruiseControl.cancel and not suppress_stock_cancel( + self.CP, CS.out.brakePressed, CC.latActive, stock_fallback=self.gv70_stock_fallback): # Here we send ACC message to cancel, not buttons. Don't delay if self.CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS: can_sends.append(hyundaicanfd.create_acc_cancel(self.packer, self.CP, self.CAN, CS.cruise_info)) @@ -218,8 +579,16 @@ class CarController(CarControllerBase): # cruise standstill resume elif CC.cruiseControl.resume: - if self.CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS: - # TODO: resume for alt button cars + if (not self.CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING and + self.CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CARNIVAL_ALT_RESUME): + source = CS.cruise_buttons_alt_msg + source_age = now_nanos - CS.cruise_buttons_alt_ts_ns + if (source and CS.cruise_buttons_alt_ts_ns > 0 and 0 <= source_age <= 100_000_000 and + source["CRUISE_BUTTONS"] == 0 and source["ADAPTIVE_CRUISE_MAIN_BTN"] == 0 and + source["NORMAL_CRUISE_MAIN_BTN"] == 0 and source["LDA_BTN"] == 0): + can_sends.append(hyundaicanfd.create_carnival_alt_resume(self.packer, self.CP, self.CAN, source)) + self.last_button_frame = self.frame + elif self.CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS: pass else: for _ in range(20): diff --git a/opendbc_repo/opendbc/car/hyundai/carstate.py b/opendbc_repo/opendbc/car/hyundai/carstate.py index 05fca0bfc3..b176a2bcff 100644 --- a/opendbc_repo/opendbc/car/hyundai/carstate.py +++ b/opendbc_repo/opendbc/car/hyundai/carstate.py @@ -1,3 +1,10 @@ +from opendbc.car.hyundai.blended_stock_aol import qualified as qualified_blended_stock, qualified_alpha as qualified_blended_alpha, MixedStockLkasSources +from opendbc.car.hyundai.classic_long_aol import qualified as qualified_classic_long +from opendbc.car.hyundai.classic_scc_aol import qualified as qualified_classic_scc, ClassicSccLkasSources +from opendbc.car.hyundai.ev9_camera_lead import EV9CameraLead +from opendbc.car.hyundai.canfd_camera_lead import CANFDCameraLead +from opendbc.car.hyundai.torque_ev_scc import eligible as torque_ev_scc_eligible +from opendbc.car.hyundai.ev9_longitudinal import qualified as ev9_long_qualified from collections import deque import copy import math @@ -6,14 +13,30 @@ from opendbc.can import CANDefine, CANParser from opendbc.car import Bus, create_button_events, structs from opendbc.car.common.conversions import Conversions as CV from opendbc.car.hyundai.hyundaicanfd import CanBus -from opendbc.car.hyundai.values import HyundaiFlags, CAR, DBC, Buttons, CarControllerParams +from opendbc.car.hyundai.non_scc_aol import NonSccLkasSources, qualified as qualified_non_scc +from opendbc.car.hyundai.gv70_camera_lead import GV70CameraLead, eligible as gv70_lead_eligible +from opendbc.car.hyundai.ioniq6_bsm import Ioniq6BlindspotSources +from opendbc.car.hyundai.ray_pedal import ray_pedal_enabled +from opendbc.car.hyundai.values import is_blended, HyundaiFlags, HyundaiSafetyFlags, CAR, DBC, Buttons, CarControllerParams from opendbc.car.interfaces import CarStateBase +from opendbc.car.dashboard_speed_limit import Tracker as LimitTracker, hyundai_canfd_sign, parser_expiry ButtonType = structs.CarState.ButtonEvent.Type PREV_BUTTON_SAMPLES = 8 CLUSTER_SAMPLE_RATE = 20 # frames STANDSTILL_THRESHOLD = 12 * 0.03125 +IONIQ_6_BSM_MAX_AGE_NS = 200_000_000 +IONIQ_6_BSM_LEFT_MASK = 0x10 +IONIQ_6_BSM_RIGHT_MASK = 0x08 + + +def ioniq_6_bsm_source_fresh(now_ns: int, source_ns: int) -> bool: + return source_ns > 0 and 0 <= now_ns - source_ns <= IONIQ_6_BSM_MAX_AGE_NS + + +def decode_ioniq_6_corner_bsm(state: int) -> tuple[bool, bool]: + return bool(state & IONIQ_6_BSM_LEFT_MASK), bool(state & IONIQ_6_BSM_RIGHT_MASK) # Cancel button can sometimes be ACC pause/resume button, main button can also enable on some cars ENABLE_BUTTONS = (Buttons.RES_ACCEL, Buttons.SET_DECEL, Buttons.CANCEL) @@ -21,14 +44,45 @@ BUTTONS_DICT = {Buttons.RES_ACCEL: ButtonType.accelCruise, Buttons.SET_DECEL: Bu Buttons.GAP_DIST: ButtonType.gapAdjustCruise, Buttons.CANCEL: ButtonType.cancel} +def get_non_scc_cruise_signals(flags: int, car=None) -> tuple[str, str, str, str, str, str]: + if car == CAR.KIA_RAY_EV: + return "LABEL11", "CC_React", "LABEL11", "CC_Engaged", "E_EMS11", "Cruise_Limit_Target" + if flags & HyundaiFlags.EV: + return "LABEL11", "CC_React", "EMS12", "ACC_ACT", "E_EMS11", "Cruise_Limit_Target" + if flags & HyundaiFlags.HYBRID: + return "E_CRUISE_CONTROL", "CRUISE_LAMP_M", "E_CRUISE_CONTROL", "CRUISE_LAMP_S", "ELECT_GEAR", "SLC_SET_SPEED" + return "EMS16", "CRUISE_LAMP_M", "EMS16", "CRUISE_LAMP_S", "LVR12", "CF_Lvr_CruiseSet" + + class CarState(CarStateBase): def __init__(self, CP): super().__init__(CP) + self.forte_lkas_sources = NonSccLkasSources() if qualified_non_scc(CP) and CP.flags & HyundaiFlags.HAS_LDA_BUTTON else None + if (qualified_classic_scc(CP) or qualified_classic_long(CP)) and CP.flags & HyundaiFlags.HAS_LDA_BUTTON: + self.forte_lkas_sources = ClassicSccLkasSources(CP.carFingerprint) + if qualified_blended_stock(CP) or qualified_blended_alpha(CP): + self.forte_lkas_sources = MixedStockLkasSources(CP) + self.ev9_long = ev9_long_qualified(CP) + self.ev9_camera_lead = EV9CameraLead(CP) if self.ev9_long else None + self.ioniq6_camera_lead = (CANFDCameraLead(CP, CanBus(CP).ECAN) + if CP.carFingerprint == CAR.HYUNDAI_IONIQ_6 and CP.openpilotLongitudinalControl else None) + self.torque_ev_camera_lead = CANFDCameraLead(CP, CanBus(CP).ECAN) if torque_ev_scc_eligible(CP) else None + self.angle_steering_angle = 0.0 + self.angle_steering_fault = False + self.hba_icon = 0 + self.left_blindspot_from_radar = False + self.right_blindspot_from_radar = False + self.gv70_camera_lead = GV70CameraLead(CP) if gv70_lead_eligible(CP) else None can_define = CANDefine(DBC[CP.carFingerprint][Bus.pt]) self.cruise_buttons: deque = deque([Buttons.NONE] * PREV_BUTTON_SAMPLES, maxlen=PREV_BUTTON_SAMPLES) self.main_buttons: deque = deque([Buttons.NONE] * PREV_BUTTON_SAMPLES, maxlen=PREV_BUTTON_SAMPLES) self.lda_button = 0 + self.ev6_aol_samples = () + self.ev6_aol_sample_stamp_ns = 0 + self.ev6_aol_timeout_ns = 0 + self.ev6_aol_source_healthy = False + self.left_paddle = 0 self.gear_msg_canfd = "ACCELERATOR" if CP.flags & HyundaiFlags.EV else \ "GEAR_ALT" if CP.flags & HyundaiFlags.CANFD_ALT_GEARS else \ @@ -54,14 +108,30 @@ class CarState(CarStateBase): "CRUISE_BUTTONS" self.is_metric = False self.buttons_counter = 0 + self.cruise_buttons_alt_msg = {} + self.cruise_buttons_alt_ts_ns = 0 self.cruise_info = {} + self.angle_lkas_status = {} + self.ccnc_161 = {} + self.ccnc_162 = {} + self.ccnc_1b5 = {} + self.ccnc_161_ts_ns = 0 + self.ccnc_162_ts_ns = 0 + self.ccnc_1b5_ts_ns = 0 + self.dashboard_limit = LimitTracker() + self.ioniq6_bsm_sources = (Ioniq6BlindspotSources() + if CP.carFingerprint == CAR.HYUNDAI_IONIQ_6 and CP.openpilotLongitudinalControl else None) + self.ioniq6_bsm_now_ns = 0 # On some cars, CLU15->CF_Clu_VehicleSpeed can oscillate faster than the dash updates. Sample at 5 Hz self.cluster_speed = 0 self.cluster_speed_counter = CLUSTER_SAMPLE_RATE self.params = CarControllerParams(CP) + self.blended_camera_current = False + self.ray_pedal_valid = False + self.ray_pedal_state = 5 def recent_button_interaction(self) -> bool: # On some newer model years, the CANCEL button acts as a pause/resume button based on the PCM state @@ -69,8 +139,42 @@ class CarState(CarStateBase): # Main button also can trigger an engagement on these cars return any(btn in ENABLE_BUTTONS for btn in self.cruise_buttons) or any(self.main_buttons) + def blended_cancel_sources_current(self, cp, *, cancel=True): + if not (is_blended(self.CP) and self.CP.openpilotLongitudinalControl): + return False + now = cp._last_update_nanos + for name in ('TCS13', 'EMS16', 'CLU11'): + source = cp.message_states.get(cp.dbc.name_to_msg[name].address) + if source is None or not source.timestamps: + return False + stamp = source.timestamps[-1] + if not 0 < stamp <= now or now - stamp > source.timeout_threshold: + return False + signals = [('TCS13', 'ACCEnable'), ('TCS13', 'DriverOverride'), ('EMS16', 'CF_Ems_AclAct')] + if cancel: + signals.append(('TCS13', 'ACC_REQ')) + return not any(cp.vl[name][signal] != 0 or any(value != 0 for value in cp.vl_all[name][signal]) + for name, signal in signals) + + def create_cruise_button_events(self, cur_button, prev_button, samples=()): + # Preserve every real edge in an alpha parser batch, including a complete + # Cancel press/release while the host is already enabled. + if is_blended(self.CP) and self.CP.openpilotLongitudinalControl: + events = [] + for button in samples: + events.extend(create_button_events(int(button), prev_button, BUTTONS_DICT)) + prev_button = int(button) + return events + return create_button_events(cur_button, prev_button, BUTTONS_DICT) + + def finalize_blended_cancel(self, ret): + if is_blended(self.CP) and self.CP.openpilotLongitudinalControl: + # Card's after-state owner can grant one later acknowledged enable credit. + ret.buttonEnable = False + def update(self, can_parsers) -> structs.CarState: cp = can_parsers[Bus.pt] + cp_pedal = can_parsers.get(Bus.party) cp_cam = can_parsers[Bus.cam] if self.CP.flags & HyundaiFlags.CANFD: @@ -117,25 +221,34 @@ class CarState(CarStateBase): ret.steerFaultTemporary = cp.vl["MDPS12"]["CF_Mdps_ToiUnavail"] != 0 or cp.vl["MDPS12"]["CF_Mdps_ToiFlt"] != 0 # cruise state - if self.CP.openpilotLongitudinalControl: + non_scc = bool(self.CP.flags & HyundaiFlags.NON_SCC) + if non_scc: + available_msg, available_sig, enabled_msg, enabled_sig, speed_msg, speed_sig = get_non_scc_cruise_signals(self.CP.flags, self.CP.carFingerprint) + ret.cruiseState.available = cp.vl[available_msg][available_sig] != 0 + ret.cruiseState.enabled = cp.vl[enabled_msg][enabled_sig] != 0 + ret.cruiseState.standstill = False + ret.cruiseState.nonAdaptive = False + ret.cruiseState.speed = cp.vl[speed_msg][speed_sig] * speed_conv + elif self.CP.openpilotLongitudinalControl: # These are not used for engage/disengage since openpilot keeps track of state using the buttons ret.cruiseState.available = cp.vl["TCS13"]["ACCEnable"] == 0 ret.cruiseState.enabled = cp.vl["TCS13"]["ACC_REQ"] == 1 ret.cruiseState.standstill = False ret.cruiseState.nonAdaptive = False else: - ret.cruiseState.available = cp_cruise.vl["SCC11"]["MainMode_ACC"] == 1 + scc_status = "SCC12" if is_blended(self.CP) else "SCC11" + ret.cruiseState.available = cp_cruise.vl[scc_status]["MainMode_ACC"] == 1 ret.cruiseState.enabled = cp_cruise.vl["SCC12"]["ACCMode"] != 0 - ret.cruiseState.standstill = cp_cruise.vl["SCC11"]["SCCInfoDisplay"] == 4. - ret.cruiseState.nonAdaptive = cp_cruise.vl["SCC11"]["SCCInfoDisplay"] == 2. # Shows 'Cruise Control' on dash - ret.cruiseState.speed = cp_cruise.vl["SCC11"]["VSetDis"] * speed_conv + ret.cruiseState.standstill = cp_cruise.vl[scc_status]["SCCInfoDisplay"] == 4. + ret.cruiseState.nonAdaptive = cp_cruise.vl[scc_status]["SCCInfoDisplay"] == 2. # Shows 'Cruise Control' on dash + ret.cruiseState.speed = cp_cruise.vl[scc_status]["VSetDis"] * speed_conv ret.brakePressed = cp.vl["TCS13"]["DriverOverride"] == 2 # 2 includes regen braking by user on HEV/EV ret.brakeHoldActive = cp.vl["TCS15"]["AVH_LAMP"] == 2 # 0 OFF, 1 ERROR, 2 ACTIVE, 3 READY ret.parkingBrake = cp.vl["TCS13"]["PBRAKE_ACT"] == 1 ret.espDisabled = cp.vl["TCS11"]["TCS_PAS"] == 1 ret.espActive = cp.vl["TCS11"]["ABS_ACT"] == 1 - ret.accFaulted = cp.vl["TCS13"]["ACCEnable"] != 0 # 0 ACC CONTROL ENABLED, 1-3 ACC CONTROL DISABLED + ret.accFaulted = False if non_scc else cp.vl["TCS13"]["ACCEnable"] != 0 if self.CP.flags & (HyundaiFlags.HYBRID | HyundaiFlags.EV | HyundaiFlags.FCEV): if self.CP.flags & HyundaiFlags.FCEV: @@ -147,6 +260,17 @@ class CarState(CarStateBase): else: ret.gasPressed = bool(cp.vl["EMS16"]["CF_Ems_AclAct"]) + if ray_pedal_enabled(self.CP): + self.ray_pedal_valid = bool(cp_pedal is not None and cp_pedal.can_valid and + cp_pedal.ts_nanos["GAS_SENSOR"]["STATE"] > 0) + self.ray_pedal_state = int(cp_pedal.vl["GAS_SENSOR"]["STATE"]) if cp_pedal is not None else 5 + ret.accFaulted = not self.ray_pedal_valid or self.ray_pedal_state != 0 + if self.ray_pedal_valid: + driver_pedal = cp_pedal.vl["GAS_SENSOR"] + track1 = round((driver_pedal["INTERCEPTOR_GAS"] + 177.408) / 0.672) + track2 = round((driver_pedal["INTERCEPTOR_GAS2"] + 165.004) / 0.332) + ret.gasPressed = track1 > 272 or track2 > 513 + # Gear Selection via Cluster - For those Kia/Hyundai which are not fully discovered, we can use the Cluster Indicator for Gear Selection, # as this seems to be standard over all cars, but is not the preferred method. if self.CP.flags & (HyundaiFlags.HYBRID | HyundaiFlags.EV): @@ -162,21 +286,40 @@ class CarState(CarStateBase): ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(gear)) - if not self.CP.openpilotLongitudinalControl or self.CP.flags & HyundaiFlags.CAMERA_SCC: - aeb_src = "FCA11" if self.CP.flags & HyundaiFlags.USE_FCA.value else "SCC12" - aeb_sig = "FCA_CmdAct" if self.CP.flags & HyundaiFlags.USE_FCA.value else "AEB_CmdAct" - aeb_warning = cp_cruise.vl[aeb_src]["CF_VSM_Warn"] != 0 - scc_warning = cp_cruise.vl["SCC12"]["TakeOverReq"] == 1 # sometimes only SCC system shows an FCW - aeb_braking = cp_cruise.vl[aeb_src]["CF_VSM_DecCmdAct"] != 0 or cp_cruise.vl[aeb_src][aeb_sig] != 0 - ret.stockFcw = (aeb_warning or scc_warning) and not aeb_braking - ret.stockAeb = aeb_warning and aeb_braking + if not is_blended(self.CP) and (not self.CP.openpilotLongitudinalControl or self.CP.flags & HyundaiFlags.CAMERA_SCC): + if non_scc: + if not self.CP.flags & HyundaiFlags.NON_SCC_NO_FCA: + fca_parser = cp if self.CP.flags & HyundaiFlags.NON_SCC_RADAR_FCA else cp_cam + fca = fca_parser.vl["FCA11"] + warning = fca["CF_VSM_Warn"] != 0 + braking = fca["CF_VSM_DecCmdAct"] != 0 or fca["FCA_CmdAct"] != 0 + ret.stockFcw = warning and not braking + ret.stockAeb = warning and braking + else: + aeb_src = "FCA11" if self.CP.flags & HyundaiFlags.USE_FCA.value else "SCC12" + aeb_sig = "FCA_CmdAct" if self.CP.flags & HyundaiFlags.USE_FCA.value else "AEB_CmdAct" + aeb_warning = cp_cruise.vl[aeb_src]["CF_VSM_Warn"] != 0 + scc_warning = cp_cruise.vl["SCC12"]["TakeOverReq"] == 1 + aeb_braking = cp_cruise.vl[aeb_src]["CF_VSM_DecCmdAct"] != 0 or cp_cruise.vl[aeb_src][aeb_sig] != 0 + ret.stockFcw = (aeb_warning or scc_warning) and not aeb_braking + ret.stockAeb = aeb_warning and aeb_braking - if self.CP.flags & HyundaiFlags.HAS_BSM: + if self.CP.deprecated.enableBsm: ret.leftBlindspot = cp.vl["LCA11"]["CF_Lca_IndLeft"] != 0 ret.rightBlindspot = cp.vl["LCA11"]["CF_Lca_IndRight"] != 0 # save the entire LKAS11 and CLU11 - self.lkas11 = copy.copy(cp_cam.vl["LKAS11"]) + if is_blended(self.CP): + if self.CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG: + self.lfa_block_msg = copy.copy(cp_cam.vl["CAM_0x2a4"]) + from opendbc.car.hyundai.blended_stock_aol import camera_current + self.blended_camera_current = camera_current(cp_cam) + self.lkas11 = {} + else: + self.msg_364 = copy.copy(cp_cam.vl["ALERTS_364"]) + self.lkas11 = copy.copy(cp_cam.vl["LKAS11"]) + else: + self.lkas11 = copy.copy(cp_cam.vl["LKAS11"]) self.clu11 = copy.copy(cp.vl["CLU11"]) self.steer_state = cp.vl["MDPS12"]["CF_Mdps_ToiActive"] # 0 NOT ACTIVE, 1 ACTIVE prev_cruise_buttons = self.cruise_buttons[-1] @@ -184,13 +327,32 @@ class CarState(CarStateBase): prev_lda_button = self.lda_button self.cruise_buttons.extend(cp.vl_all["CLU11"]["CF_Clu_CruiseSwState"]) self.main_buttons.extend(cp.vl_all["CLU11"]["CF_Clu_CruiseSwMain"]) - if self.CP.flags & HyundaiFlags.HAS_LDA_BUTTON: + if self.forte_lkas_sources is not None and getattr(self.forte_lkas_sources, 'active', True): + self.lda_button = int(self.forte_lkas_sources.held) + elif self.CP.carFingerprint == CAR.HYUNDAI_ELANTRA_HEV_2024: + # Both signals can pulse within one parser update; preserve a short press. + lda_samples = [*cp.vl_all["CLU13"]["CF_Clu_LdwsLkasSW"], *cp.vl_all["BCM_PO_11"]["LDA_BTN"]] + if lda_samples: + self.lda_button = int(any(lda_samples)) + elif is_blended(self.CP): + self.lda_button = int(any(( + cp.vl['CLU13']['CF_Clu_LdwsLkasSW'] if cp.ts_nanos['CLU13']['CF_Clu_LdwsLkasSW'] > 0 else 0, + cp.vl['BCM_PO_11']['LDA_BTN'] if cp.ts_nanos['BCM_PO_11']['LDA_BTN'] > 0 else 0))) + elif self.CP.flags & HyundaiFlags.HAS_LDA_BUTTON: self.lda_button = cp.vl["BCM_PO_11"]["LDA_BTN"] - ret.buttonEvents = [*create_button_events(self.cruise_buttons[-1], prev_cruise_buttons, BUTTONS_DICT), + button_events = [*self.create_cruise_button_events(self.cruise_buttons[-1], prev_cruise_buttons, + cp.vl_all["CLU11"]["CF_Clu_CruiseSwState"]), *create_button_events(self.main_buttons[-1], prev_main_buttons, {1: ButtonType.mainCruise}), *create_button_events(self.lda_button, prev_lda_button, {1: ButtonType.lkas})] + if self.forte_lkas_sources is not None and getattr(self.forte_lkas_sources, 'active', True): + button_events = [event for event in button_events if event.type != ButtonType.lkas] + button_events.extend(structs.CarState.ButtonEvent(type=ButtonType.lkas, pressed=pressed) + for pressed in self.forte_lkas_sources.edges) + self.lda_button = int(self.forte_lkas_sources.held) + ret.buttonEvents = button_events + ret.blockPcmEnable = not self.recent_button_interaction() # low speed steer alert hysteresis logic (only for cars with steer cut off above 10 m/s) @@ -240,16 +402,63 @@ class CarState(CarStateBase): ret.steeringTorqueEps = cp.vl["MDPS"]["MDPS_OutTqVal"] ret.steeringPressed = self.update_steering_pressed(abs(ret.steeringTorque) > self.params.STEER_THRESHOLD, 5) ret.steerFaultTemporary = cp.vl["MDPS"]["MDPS_LkaFailSta"] != 0 + if self.CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING: + angle_fault = int(cp.vl["MDPS"]["MDPS_ADAS_AciFltSig_Lv2"]) + if self.ev9_long: + self.angle_steering_angle = cp.vl["MDPS"]["MDPS_EstStrAnglVal"] + self.angle_steering_fault = bool(angle_fault & 2) + ret.steerFaultTemporary |= self.angle_steering_fault + else: + ret.steerFaultTemporary |= bool(angle_fault if self.CP.carFingerprint in (CAR.HYUNDAI_IONIQ_5_PE, CAR.KIA_EV9) else angle_fault & 2) + + if self.CP.flags & HyundaiFlags.CCNC and not self.CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG: + self.ccnc_161 = copy.copy(cp_cam.vl["CCNC_0x161"]) + self.ccnc_162 = copy.copy(cp_cam.vl["CCNC_0x162"]) + self.ccnc_1b5 = copy.copy(cp_cam.vl["FR_CMR_03_50ms"]) + self.ccnc_161_ts_ns = cp_cam.ts_nanos[0x161]["COUNTER"] + self.ccnc_162_ts_ns = cp_cam.ts_nanos[0x162]["COUNTER"] + self.ccnc_1b5_ts_ns = cp_cam.ts_nanos[0x1b5]["Info_LftLnPosVal"] - # TODO: alt signal usage may be described by cp.vl['BLINKERS']['USE_ALT_LAMP'] left_blinker_sig, right_blinker_sig = "LEFT_LAMP", "RIGHT_LAMP" - if self.CP.carFingerprint == CAR.HYUNDAI_KONA_EV_2ND_GEN: + if (self.CP.carFingerprint == CAR.HYUNDAI_KONA_EV_2ND_GEN or self.CP.flags & HyundaiFlags.CCNC or + cp.vl["BLINKERS"]["USE_ALT_LAMP"] == 1): left_blinker_sig, right_blinker_sig = "LEFT_LAMP_ALT", "RIGHT_LAMP_ALT" ret.leftBlinker, ret.rightBlinker = self.update_blinker_from_lamp(50, cp.vl["BLINKERS"][left_blinker_sig], cp.vl["BLINKERS"][right_blinker_sig]) - if self.CP.flags & HyundaiFlags.HAS_BSM: - ret.leftBlindspot = bool(cp.vl["ADAS_CMD_50_50ms"]["BCW_LtIndSta"]) - ret.rightBlindspot = bool(cp.vl["ADAS_CMD_50_50ms"]["BCW_RtIndSta"]) + if self.CP.deprecated.enableBsm: + rear = cp.vl["ADAS_CMD_50_50ms"] + ret.leftBlindspot = bool(rear["BCW_LtIndSta"]) + ret.rightBlindspot = bool(rear["BCW_RtIndSta"]) + if self.CP.carFingerprint == CAR.KIA_EV9: + corner_left, corner_right = decode_ioniq_6_corner_bsm(int(cp.vl["BLINDSPOTS_FRONT_CORNER_2"]["SIDE_DETECT_STATE"])) + if self.ev9_long: + self.left_blindspot_from_radar, self.right_blindspot_from_radar = corner_left, corner_right + ret.leftBlindspot |= corner_left + ret.rightBlindspot |= corner_right + if self.CP.carFingerprint == CAR.HYUNDAI_IONIQ_6: + now_ns = cp._last_update_nanos + rear_ts = cp.ts_nanos["ADAS_CMD_50_50ms"]["ADAS_CMD_Crc50Val"] + front_ts = cp.ts_nanos["BLINDSPOTS_FRONT_CORNER_2"]["CHECKSUM"] + rear_fresh = ioniq_6_bsm_source_fresh(now_ns, rear_ts) + front_fresh = ioniq_6_bsm_source_fresh(now_ns, front_ts) + corner_left, corner_right = decode_ioniq_6_corner_bsm(int(cp.vl["BLINDSPOTS_FRONT_CORNER_2"]["SIDE_DETECT_STATE"])) + ret.leftBlindspot = (rear_fresh and ret.leftBlindspot) or (front_fresh and corner_left) + ret.rightBlindspot = (rear_fresh and ret.rightBlindspot) or (front_fresh and corner_right) + if self.ioniq6_bsm_sources is not None: + # CANParser timestamps and this receive clock are both BOOTTIME. + # CarController's now_nanos is MONOTONIC and cannot age them. + self.ioniq6_bsm_now_ns = now_ns + self.ioniq6_bsm_sources.observe( + corner_source_ns=front_ts, + corner_state=int(cp.vl["BLINDSPOTS_FRONT_CORNER_2"]["SIDE_DETECT_STATE"]), + lamp_source_ns=cp.ts_nanos["BLINKERS"]["USE_ALT_LAMP"], + left_lamp=bool(cp.vl["BLINKERS"][left_blinker_sig]), + right_lamp=bool(cp.vl["BLINKERS"][right_blinker_sig]), + ) + + if self.ev9_long and cp.ts_nanos["FR_CMR_01_10ms"]["FR_CMR_Crc1Val"] > 0: + hba = int(cp.vl["FR_CMR_01_10ms"]["HBA_IndLmpReq"]) + self.hba_icon = hba if hba in (1, 2) else 0 # cruise state # CAN FD cars enable on main button press, set available if no TCS faults preventing engagement @@ -272,45 +481,194 @@ class CarState(CarStateBase): if self.CP.flags & HyundaiFlags.EV: ret.cruiseState.nonAdaptive = cp.vl["MANUAL_SPEED_LIMIT_ASSIST"]["MSLA_ENABLED"] == 1 + from opendbc.car.hyundai.ev6_aol import qualified as ev6_aol_qualified + self.ev6_aol_samples = () + self.ev6_aol_source_healthy = False + if ev6_aol_qualified(self.CP): + from opendbc.can.parser import MAX_BAD_COUNTER + source = cp.message_states.get(cp.dbc.name_to_msg[self.cruise_btns_msg_canfd].address) + self.ev6_aol_sample_stamp_ns = int(source.timestamps[-1]) if source is not None and source.timestamps else 0 + self.ev6_aol_timeout_ns = int(source.timeout_threshold) if source is not None else 0 + now = cp._last_update_nanos + bus_timeout = cp.bus_timeout + self.ev6_aol_source_healthy = bool( + not bus_timeout and source is not None and source.timestamps + and source.valid(now, bus_timeout) and source.counter_fail < MAX_BAD_COUNTER + and 0 < self.ev6_aol_sample_stamp_ns <= now + and now - self.ev6_aol_sample_stamp_ns <= source.timeout_threshold) + values = cp.vl_all[self.cruise_btns_msg_canfd] + self.ev6_aol_samples = tuple(zip(values['ADAPTIVE_CRUISE_MAIN_BTN'], values['LDA_BTN'], values['CRUISE_BUTTONS'], strict=True)) prev_cruise_buttons = self.cruise_buttons[-1] prev_main_buttons = self.main_buttons[-1] prev_lda_button = self.lda_button self.cruise_buttons.extend(cp.vl_all[self.cruise_btns_msg_canfd]["CRUISE_BUTTONS"]) self.main_buttons.extend(cp.vl_all[self.cruise_btns_msg_canfd]["ADAPTIVE_CRUISE_MAIN_BTN"]) self.lda_button = cp.vl[self.cruise_btns_msg_canfd]["LDA_BTN"] + prev_left_paddle = self.left_paddle + self.left_paddle = cp.vl["CRUISE_BUTTONS"]["LEFT_PADDLE"] if self.CP.carFingerprint == CAR.HYUNDAI_IONIQ_6 else 0 self.buttons_counter = cp.vl[self.cruise_btns_msg_canfd]["COUNTER"] + if not self.CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING and self.CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CARNIVAL_ALT_RESUME: + self.cruise_buttons_alt_ts_ns = cp.ts_nanos["CRUISE_BUTTONS_ALT"]["COUNTER"] + if self.cruise_buttons_alt_ts_ns > 0: + self.cruise_buttons_alt_msg = copy.copy(cp.vl["CRUISE_BUTTONS_ALT"]) ret.accFaulted = cp.vl["TCS"]["ACCEnable"] != 0 # 0 ACC CONTROL ENABLED, 1-3 ACC CONTROL DISABLED if self.CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG: self.lfa_block_msg = copy.copy(cp_cam.vl["CAM_0x362"] if self.CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG_ALT else cp_cam.vl["CAM_0x2a4"]) + if self.CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING: + self.angle_lkas_status = copy.copy(cp_cam.vl["LKAS_ALT" if self.CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG_ALT else "LKAS"]) ret.buttonEvents = [*create_button_events(self.cruise_buttons[-1], prev_cruise_buttons, BUTTONS_DICT), *create_button_events(self.main_buttons[-1], prev_main_buttons, {1: ButtonType.mainCruise}), - *create_button_events(self.lda_button, prev_lda_button, {1: ButtonType.lkas})] + *create_button_events(self.lda_button, prev_lda_button, {1: ButtonType.lkas}), + *create_button_events(self.left_paddle, prev_left_paddle, {1: ButtonType.altButton2})] ret.blockPcmEnable = not self.recent_button_interaction() + # The front camera's posted sign is distinct from SCC set speed and from + # the CCNC cluster's forwarded display value. Only a received packet can + # establish valid or absent evidence; the subscription is non-gating. + sign_bus = cp if self.CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG else cp_cam + timestamp, expiry = parser_expiry(sign_bus, "FR_CMR_02_100ms", "ISLW_SpdCluMainDis") + if timestamp > self.dashboard_limit.observation.observed_ns: + speed = int(sign_bus.vl["FR_CMR_02_100ms"]["ISLW_SpdCluMainDis"]) + system_status = int(sign_bus.vl["FR_CMR_02_100ms"]["ISLW_SysSta"]) + status, value_mps = hyundai_canfd_sign(speed, system_status, self.is_metric) + self.dashboard_limit.update(timestamp, status, value_mps, valid_until_ns=expiry) + return ret def get_can_parsers_canfd(self, CP): msgs = [] + if ev9_long_qualified(CP): + msgs.append(("FR_CMR_01_10ms", math.nan)) + cam_msgs: list[tuple[str | int, int | float]] = [] + if CP.carFingerprint in (CAR.HYUNDAI_IONIQ_6, CAR.KIA_EV9) and CP.deprecated.enableBsm: + # Receive-only corner sources do not poison stock SCC CAN validity. + # The separately gated exact-long controller may use a fresh source for + # paired dashboard status; parsing alone never authorizes TX. + msgs.extend((("ADAS_CMD_50_50ms", math.nan), ("BLINDSPOTS_FRONT_CORNER_2", math.nan))) + if CP.flags & HyundaiFlags.EV: + msgs.append(("MANUAL_SPEED_LIMIT_ASSIST", math.nan)) + sign_subscription = ("FR_CMR_02_100ms", math.nan) + if CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG: + msgs.append(sign_subscription) + if CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING: + cam_msgs.append(("LKAS_ALT" if CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG_ALT else "LKAS", math.nan)) + cam_msgs.append(("CAM_0x362" if CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG_ALT else "CAM_0x2a4", math.nan)) + else: + cam_msgs.append(sign_subscription) + if CP.flags & HyundaiFlags.CCNC and not CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG: + cam_msgs += [("CCNC_0x161", math.nan), ("CCNC_0x162", math.nan), ("FR_CMR_03_50ms", math.nan)] if not (CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS): # TODO: this can be removed once we add dynamic support to vl_all msgs += [ # this message is 50Hz but the ECU frequently stops transmitting for ~0.5s - ("CRUISE_BUTTONS", 1) + ("CRUISE_BUTTONS", 50 if CP.carFingerprint == CAR.HYUNDAI_IONIQ_6 and CP.openpilotLongitudinalControl else + math.nan if CP.carFingerprint in (CAR.HYUNDAI_IONIQ_9, CAR.KIA_EV9) else + 50 if CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING else 1) ] + elif not CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING and CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CARNIVAL_ALT_RESUME: + msgs.append(("CRUISE_BUTTONS_ALT", math.nan)) + from opendbc.car.hyundai.canfd_stock_aol import qualified as stock_aol_qualified + if stock_aol_qualified(CP): + msgs = [(name, frequency) for name, frequency in msgs if name != "CRUISE_BUTTONS_ALT"] + msgs.append(("CRUISE_BUTTONS_ALT", 50 if CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS else math.nan)) + if CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING: + # These are actual state and safety inputs, not DBC defaults. Keep this + # subscription expansion scoped to the new angle profile. + fuel = "ACCELERATOR" if CP.flags & HyundaiFlags.EV else \ + "ACCELERATOR_ALT" if CP.flags & HyundaiFlags.HYBRID else "ACCELERATOR_BRAKE_ALT" + required = [(fuel, 100), ("TCS", 50), ("WHEEL_SPEEDS", 100), ("MDPS", 100), + ("STEERING_SENSORS", 100), ("DOORS_SEATBELTS", 10), ("BLINKERS", 10), + (self.gear_msg_canfd, 100)] + existing = {name for name, _ in msgs} + for name, frequency in required: + if name not in existing: + msgs.append((name, frequency)) + existing.add(name) + if CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS: + msgs.append(("CRUISE_BUTTONS_ALT", 50)) + else: + # update_canfd reads DISTANCE_UNIT from this alternate definition even + # on standard-button cars; prevent the absent source gating CAN-valid. + msgs.append(("CRUISE_BUTTONS_ALT", math.nan)) + if not ev9_long_qualified(CP): + (cam_msgs if CP.flags & HyundaiFlags.CANFD_CAMERA_SCC else msgs).append(("SCC_CONTROL", 50)) + elif torque_ev_scc_eligible(CP): + msgs.extend((("ACCELERATOR", 100), ("TCS", 50), ("WHEEL_SPEEDS", 100), ("MDPS", 100))) + msgs.extend((("CRUISE_BUTTONS_ALT", math.nan), ("DOORS_SEATBELTS", math.nan), + ("STEERING_SENSORS", math.nan), ("BLINKERS", math.nan))) + if self.gear_msg_canfd != "ACCELERATOR": + msgs.append((self.gear_msg_canfd, 100)) + cam_msgs.append(("CAM_0x362" if CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG_ALT else "CAM_0x2a4", 20)) + elif CP.carFingerprint == CAR.HYUNDAI_IONIQ_6 and CP.openpilotLongitudinalControl: + # The acknowledged ADAS takeover removes stock SCC from E-CAN. Require + # every independent native input and the matching camera source instead. + msgs.extend((("ACCELERATOR", 100), ("TCS", 50), ("WHEEL_SPEEDS", 100), ("MDPS", 100))) + msgs.extend((("CRUISE_BUTTONS_ALT", math.nan), ("DOORS_SEATBELTS", math.nan), + ("STEERING_SENSORS", math.nan), ("BLINKERS", math.nan))) + cam_msgs.append(("CAM_0x362" if CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG_ALT else "CAM_0x2a4", 20)) + elif CP.carFingerprint == CAR.HYUNDAI_IONIQ_6 and not CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS: + # Stock standard-button Ioniq 6 reads alternate DISTANCE_UNIT too, but + # that frame may be absent. Subscribe before the lazy lookup makes it + # required; existing native and CarState safety inputs remain required. + msgs.append(("CRUISE_BUTTONS_ALT", math.nan)) + from opendbc.car.hyundai.ev6_startup import eligible as ev6_eligible + from opendbc.car.hyundai.gv70_startup import eligible as gv70_eligible + if ev6_eligible(CP) or gv70_eligible(CP): + # Standard 1CF supplies buttons; alternate 1AA only holds optional units. + if not any(name == "CRUISE_BUTTONS_ALT" for name, _ in msgs): + msgs.append(("CRUISE_BUTTONS_ALT", math.nan)) + existing = {name for name, _ in msgs} + for name, frequency in (("ACCELERATOR", 100), ("TCS", 50), ("WHEEL_SPEEDS", 100), ("MDPS", 100), + (self.gear_msg_canfd, 100)): + if name not in existing: + msgs.append((name, frequency)) + existing.add(name) + if not CP.openpilotLongitudinalControl: + msgs.append(("SCC_CONTROL", 50)) + if ("CAM_0x2a4", 20) not in cam_msgs: + cam_msgs.append(("CAM_0x2a4", 20)) return { Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], msgs, CanBus(CP).ECAN), - Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus(CP).CAM), + # Native CANParser accepts NaN to subscribe without a CAN-valid frequency gate. + Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], cam_msgs, CanBus(CP).CAM), } def get_can_parsers(self, CP): if CP.flags & HyundaiFlags.CANFD: return self.get_can_parsers_canfd(CP) - return { - Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], 0), - Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], [], 2), + if is_blended(CP): + msgs = [("MDPS12", 100), ("TCS11", 100), ("TCS13", 50), ("TCS15", 10), + ("CLU11", 50), ("CLU15", 5), ("ESP12", 100), ("CGW1", 10), ("CGW2", 5), + ("WHL_SPD11", 50), ("SAS11", 100), + ("EMS12", 100), ("EMS16", 100), ("LVR12", 100), ("BCM_PO_11", math.nan), ("CLU13", math.nan)] + if not CP.openpilotLongitudinalControl: + msgs.extend((("SCC11", 50), ("SCC12", 50))) + elif CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG: + msgs.append(("SCC12", math.nan)) + if CP.deprecated.enableBsm: + msgs.append(("LCA11", 20)) + cam_msgs = [("CAM_0x2a4", 20)] if CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG else [ + ("LKAS11", 100), ("ALERTS_364", math.nan)] + return {Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], msgs, CanBus(CP).ECAN), + Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], cam_msgs, CanBus(CP).CAM)} + + msgs = [("CLU13", math.nan), ("BCM_PO_11", math.nan)] if CP.carFingerprint == CAR.HYUNDAI_ELANTRA_HEV_2024 else [] + cam_msgs = [] + if CP.flags & HyundaiFlags.NON_SCC: + available_msg, _, enabled_msg, _, speed_msg, _ = get_non_scc_cruise_signals(CP.flags, CP.carFingerprint) + msgs.extend((name, 0) for name in {available_msg, enabled_msg, speed_msg}) + if not CP.flags & HyundaiFlags.NON_SCC_NO_FCA: + (msgs if CP.flags & HyundaiFlags.NON_SCC_RADAR_FCA else cam_msgs).append(("FCA11", math.nan)) + parsers = { + Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], msgs, 0), + Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], cam_msgs, 2), } + + if ray_pedal_enabled(CP): + parsers[Bus.party] = CANParser("hyundai_kia_ray_pedal", [("GAS_SENSOR", 50)], 0) + return parsers diff --git a/opendbc_repo/opendbc/car/hyundai/ccnc_ev_stock.py b/opendbc_repo/opendbc/car/hyundai/ccnc_ev_stock.py new file mode 100644 index 0000000000..da4601b5bc --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/ccnc_ev_stock.py @@ -0,0 +1,33 @@ +"""Exact stock-SCC CCNC EV ordinary steering transport contract.""" + +from opendbc.car import structs +from opendbc.car.hyundai.values import CAR, HyundaiFlags + +STOCK_SAFETY_PARAMS = {CAR.HYUNDAI_IONIQ_5_PE: 0x5491, CAR.KIA_EV9: 0x5c91} +_REQUIRED = (HyundaiFlags.CANFD | HyundaiFlags.EV | HyundaiFlags.CCNC | HyundaiFlags.CANFD_ANGLE_STEERING | + HyundaiFlags.CANFD_LKA_STEER_MSG | HyundaiFlags.CANFD_LKA_STEER_MSG_ALT) + +# These generic metadata flags never select the EV gear decoder. +_IGNORED_EV_GEAR_FLAGS = HyundaiFlags.CANFD_ALT_GEARS | HyundaiFlags.CANFD_ALT_GEARS_2 + + +def qualified(cp): + return (cp.carFingerprint in STOCK_SAFETY_PARAMS and cp.brand == "hyundai" and + not (cp.notCar or cp.passive or cp.dashcamOnly or cp.openpilotLongitudinalControl) and cp.pcmCruise and + cp.alternativeExperience == 0 and int(cp.flags) & ~int(_IGNORED_EV_GEAR_FLAGS) == int(_REQUIRED) and + len(cp.safetyConfigs) == 1 and cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.hyundaiCanfd and + cp.safetyConfigs[0].safetyParam == STOCK_SAFETY_PARAMS[cp.carFingerprint]) + + +def request_allowed(cp, state): + return (qualified(cp) and state.canValid and not state.canTimeout and state.cruiseState.enabled and + state.gearShifter == structs.CarState.GearShifter.drive and not state.standstill and + not (state.brakePressed or state.gasPressed or state.steerFaultTemporary or state.steerFaultPermanent)) + + +def replacement_requested(cp, control, state): + from opendbc.car.hyundai.canfd_angle_aol import qualified as angle_qualified, command_allowed + + if angle_qualified(cp, marked_only=True): + return command_allowed(cp, state) and control.latActive + return request_allowed(cp, state) and control.latActive diff --git a/opendbc_repo/opendbc/car/hyundai/classic_long_aol.py b/opendbc_repo/opendbc/car/hyundai/classic_long_aol.py new file mode 100644 index 0000000000..cc08ceebb2 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/classic_long_aol.py @@ -0,0 +1,71 @@ +from opendbc.car import structs +from opendbc.car.hyundai.values import CAR, HyundaiFlags, HyundaiSafetyFlags, is_blended +from opendbc.car.hyundai.classic_scc_aol import CLASSIC_SCC_IDS, stock_word + +AOL_MARKER = 0x0400 +AOL_EXPERIENCE = 32 +_EXCLUDED = int(HyundaiFlags.LEGACY | HyundaiFlags.UNSUPPORTED_LONGITUDINAL | HyundaiFlags.ALT_LIMITS_2 | HyundaiFlags.CAMERA_SCC | HyundaiFlags.FCEV) +CLASSIC_LONG_IDS = frozenset(identity for identity in CLASSIC_SCC_IDS if not int(identity.config.flags) & _EXCLUDED) +LONG_AOL_WORDS = frozenset(AOL_MARKER | 4 | gas | limits | lda for gas in (0, 1, 2) for limits in (0, 64) for lda in (0, 2048)) + + +def ordinary_word(cp): + from opendbc.car.hyundai.legacy_long_aol import CARS, ordinary_word as legacy_word + if cp.carFingerprint in CARS: + return legacy_word(cp) + return stock_word(cp) | int(HyundaiSafetyFlags.LONG) + + +def aol_word(cp): + from opendbc.car.hyundai.legacy_long_aol import CARS, aol_word as legacy_word + if cp.carFingerprint in CARS: + return legacy_word(cp) + return ordinary_word(cp) | AOL_MARKER | (int(HyundaiSafetyFlags.HAS_LDA_BUTTON) if int(cp.flags) & int(HyundaiFlags.HAS_LDA_BUTTON) else 0) + + +def qualified(cp, *, marked_only=False): + from opendbc.car.hyundai.legacy_long_aol import CARS, qualified as legacy_qualified + if cp.carFingerprint in CARS: + return legacy_qualified(cp, marked_only=marked_only) + if cp.brand != 'hyundai' or cp.carFingerprint not in CLASSIC_LONG_IDS: + return False + declared = int(CAR[cp.carFingerprint].config.flags) + allowed = declared | int(HyundaiFlags.HAS_LDA_BUTTON | HyundaiFlags.USE_FCA | HyundaiFlags.SEND_LFA) + identity_mask = int( + HyundaiFlags.CANFD + | HyundaiFlags.NON_SCC + | HyundaiFlags.FCEV + | HyundaiFlags.LEGACY + | HyundaiFlags.EV + | HyundaiFlags.HYBRID + | HyundaiFlags.ALT_LIMITS + | HyundaiFlags.ALT_LIMITS_2 + | HyundaiFlags.CAMERA_SCC + ) + if ( + int(cp.flags) & identity_mask != declared & identity_mask + or int(cp.flags) & ~allowed + or is_blended(cp) + or cp.passive + or cp.notCar + or cp.dashcamOnly + or not cp.openpilotLongitudinalControl + or cp.pcmCruise + or cp.steerControlType != structs.CarParams.SteerControlType.torque + or not cp.alphaLongitudinalAvailable + or cp.alternativeExperience not in (0, AOL_EXPERIENCE) + or len(cp.safetyConfigs) != 1 + ): + return False + safety = cp.safetyConfigs[0] + accepted = (aol_word(cp),) if marked_only else (ordinary_word(cp), aol_word(cp)) + return safety.safetyModel == structs.CarParams.SafetyModel.hyundai and int(safety.safetyParam) in accepted + + +def native_accepts(cp, model, param): + return ( + qualified(cp, marked_only=True) + and cp.alternativeExperience == AOL_EXPERIENCE + and model == int(cp.safetyConfigs[0].safetyModel.raw) + and param == aol_word(cp) + ) diff --git a/opendbc_repo/opendbc/car/hyundai/classic_scc_aol.py b/opendbc_repo/opendbc/car/hyundai/classic_scc_aol.py new file mode 100644 index 0000000000..7115f9b372 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/classic_scc_aol.py @@ -0,0 +1,188 @@ +"""Classic stock-SCC AOL identity and transport contract. + +Gas, checksum, SCC routing and steering limits remain platform-owned. +Independent lateral authorization is a separate native profile, never LONG. +""" + +import time + +from opendbc.car import structs +from opendbc.car.hyundai.values import CAR, HyundaiFlags, HyundaiSafetyFlags, is_blended + +AOL_MARKER = 0x0400 +AOL_EXPERIENCE = 32 +_EXCLUDED = int(HyundaiFlags.CANFD | HyundaiFlags.NON_SCC | HyundaiFlags.FCEV) +CLASSIC_SCC_IDS = frozenset(identity for identity in CAR if not int(identity.config.flags) & _EXCLUDED and identity != CAR.HYUNDAI_PALISADE_2023) | frozenset( + (CAR.HYUNDAI_NEXO_1ST_GEN,) +) +_MODELS = (structs.CarParams.SafetyModel.hyundai, structs.CarParams.SafetyModel.hyundaiLegacy) +FCEV_AOL_WORDS = frozenset((0x0500, 0x0D00)) +NORMAL_AOL_WORDS = ( + FCEV_AOL_WORDS + | frozenset(AOL_MARKER | gas | limit | camera | lda for gas in (0, 1, 2) for limit in (0, 64, 512) for camera in (0, 8) for lda in (0, 2048)) + | frozenset((0x8408, 0x840A, 0x8C08, 0x8C0A)) +) +LEGACY_AOL_WORDS = frozenset(word for word in NORMAL_AOL_WORDS if not word & (8 | 256 | 32768)) + + +def native_profile_supported(model, param): + return (model == int(_MODELS[0]) and param in NORMAL_AOL_WORDS) or (model == int(_MODELS[1]) and param in LEGACY_AOL_WORDS) + + +def stock_word(cp): + word = int(HyundaiSafetyFlags.CAN_REFRESH_MSGS) if cp.carFingerprint in (CAR.HYUNDAI_ELANTRA_2024, CAR.HYUNDAI_ELANTRA_HEV_2024) else 0 + for flag, safety in ( + (HyundaiFlags.FCEV, HyundaiSafetyFlags.FCEV_GAS), + (HyundaiFlags.EV, HyundaiSafetyFlags.EV_GAS), + (HyundaiFlags.HYBRID, HyundaiSafetyFlags.HYBRID_GAS), + (HyundaiFlags.ALT_LIMITS, HyundaiSafetyFlags.ALT_LIMITS), + (HyundaiFlags.ALT_LIMITS_2, HyundaiSafetyFlags.ALT_LIMITS_2), + (HyundaiFlags.CAMERA_SCC, HyundaiSafetyFlags.CAMERA_SCC), + ): + if int(cp.flags) & int(flag): + word |= int(safety) + return word + + +def aol_word(cp): + return stock_word(cp) | AOL_MARKER | (int(HyundaiSafetyFlags.HAS_LDA_BUTTON) if int(cp.flags) & int(HyundaiFlags.HAS_LDA_BUTTON) else 0) + + +def qualified(cp, *, marked_only=False): + if cp.brand != "hyundai" or cp.carFingerprint not in CLASSIC_SCC_IDS: + return False + declared = int(CAR[cp.carFingerprint].config.flags) + allowed = declared | int(HyundaiFlags.HAS_LDA_BUTTON | HyundaiFlags.USE_FCA | HyundaiFlags.SEND_LFA) + identity_mask = _EXCLUDED | int( + HyundaiFlags.EV | HyundaiFlags.HYBRID | HyundaiFlags.ALT_LIMITS | HyundaiFlags.ALT_LIMITS_2 | HyundaiFlags.CAMERA_SCC | HyundaiFlags.LEGACY + ) + if ( + int(cp.flags) & identity_mask != declared & identity_mask + or int(cp.flags) & ~allowed + or is_blended(cp) + or cp.passive + or cp.notCar + or cp.dashcamOnly + or cp.openpilotLongitudinalControl + or not cp.pcmCruise + or cp.alternativeExperience not in (0, AOL_EXPERIENCE) + or len(cp.safetyConfigs) != 1 + ): + return False + safety = cp.safetyConfigs[0] + model = _MODELS[bool(declared & int(HyundaiFlags.LEGACY))] + accepted = (aol_word(cp),) if marked_only else (stock_word(cp), aol_word(cp)) + return safety.safetyModel == model and int(safety.safetyParam) in accepted + + +def native_accepts(cp, model, param): + return ( + qualified(cp, marked_only=True) + and cp.alternativeExperience == AOL_EXPERIENCE + and model == int(cp.safetyConfigs[0].safetyModel.raw) + and param == aol_word(cp) + ) + + +class ClassicSccLkasSources: + """Original per-frame button selection over current-session raw observations. + + The Sonata hybrid's SWL status can affect host intent, but cannot by itself + authorize native steering. Native authorization still observes physical + BCM/CLU button bits. Expiry and held-at-start protection remain fail closed. + """ + + def __init__(self, identity): + self.identity = identity + self.sources = {} + self.selected = None + self.previous = {"bcm": 0, "clu13": 0, "swl_stat": 0} + self.held = False + self.neutral_seen = False + self.edges = [] + self.neutral_sources = set() + self.blocked_sources = set() + + def update(self, can_packets): + self.edges = [] + try: + now = time.clock_gettime_ns(time.CLOCK_BOOTTIME) + except (AttributeError, OSError): + self.sources.clear() + self.neutral_sources.clear() + self.blocked_sources.clear() + self.neutral_seen = False + self.held = False + self.previous = dict.fromkeys(self.previous, 0) + self.selected = None + return + age = 300_000_000 + current = {address: value for address, value in self.sources.items() if 0 <= now - value[0] <= age} + if len(current) != len(self.sources): + for address, (_, data) in self.sources.items(): + held_source = ( + bool(data[0] & 0x10) if address == 0x391 else bool(data[7] & 1 or (self.identity == CAR.HYUNDAI_SONATA_HYBRID and ((data[4] >> 4) & 7) == 4)) + ) + if address not in current: + self.neutral_sources.discard(address) + if held_source and (self.identity != CAR.HYUNDAI_SONATA or address == 0x391): + self.blocked_sources.add(address) + self.neutral_seen = False + self.sources = current + samples = [] + accepted = set() + for stamp, packets in can_packets: + if type(stamp) is not int or not 0 < stamp <= now or now - stamp > age: + continue + for address, data, bus in packets: + if bus != 0 or address not in (0x391, 0x50C) or len(data) != 8: + continue + previous = self.sources.get(address) + if previous is not None and stamp <= previous[0]: + continue + self.sources[address] = (stamp, bytes(data)) + accepted.add(address) + source_held = ( + bool(data[0] & 0x10) if address == 0x391 else bool(data[7] & 1 or (self.identity == CAR.HYUNDAI_SONATA_HYBRID and ((data[4] >> 4) & 7) == 4)) + ) + if self.identity == CAR.HYUNDAI_SONATA and address != 0x391: + continue + if source_held: + if address not in self.neutral_sources: + self.blocked_sources.add(address) + else: + self.neutral_sources.add(address) + self.blocked_sources.discard(address) + samples.append(bool(data[0] & 0x10) if address == 0x391 else bool(data[7] & 1)) + bcm = self.sources.get(0x391) + clu = self.sources.get(0x50C) + states = {"bcm": int(bool(bcm and bcm[1][0] & 0x10)), "clu13": int(bool(clu and clu[1][7] & 1)), "swl_stat": int(bool(clu and ((clu[1][4] >> 4) & 7) == 4))} + if self.identity == CAR.HYUNDAI_SONATA: + held = bool(states["bcm"]) + elif self.identity == CAR.HYUNDAI_SONATA_HYBRID: + changed = [source for source, state in states.items() if state != self.previous[source]] + active = [source for source, state in states.items() if state] + if self.selected in changed: + selected = self.selected + elif active: + selected = active[0] + elif changed: + selected = changed[0] + else: + selected = self.selected + self.previous = states + if selected is not None: + self.selected = selected + held = bool(states[selected]) if selected is not None else False + elif self.identity == CAR.HYUNDAI_ELANTRA_HEV_2024: + held = any(samples) if samples else bool(states["bcm"] or states["clu13"]) + else: + held = bool(states["bcm"] or states["clu13"]) + fresh_neutral = bool(accepted) and not self.blocked_sources and not held and (self.identity != CAR.HYUNDAI_SONATA or 0x391 in accepted) + if fresh_neutral: + if not self.neutral_seen and not self.held: + self.edges.append(False) + self.neutral_seen = True + if held != self.held and self.neutral_seen and not self.blocked_sources and (held or fresh_neutral): + self.edges.append(held) + self.held = held diff --git a/opendbc_repo/opendbc/car/hyundai/ecu_startup.py b/opendbc_repo/opendbc/car/hyundai/ecu_startup.py new file mode 100644 index 0000000000..5e017aeb1d --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/ecu_startup.py @@ -0,0 +1,209 @@ +"""Hyundai prepublication ECU transaction; sent disable is not confirmed ownership.""" +from enum import Enum +from collections.abc import Callable +from typing import TypedDict, cast +from copy import deepcopy +import time + +from opendbc.car import structs +from opendbc.car.disable_ecu import disable_ecu +from opendbc.car.hyundai.values import HyundaiSafetyFlags +from opendbc.car.hyundai.canfd_owner import config_index +from opendbc.car.isotp_parallel_query import IsoTpParallelQuery + + +class PreparedSafetyConfig(TypedDict): + safetyParam: int + + +class PreparedParams(TypedDict): + safetyConfigs: list[PreparedSafetyConfig] + alternativeExperience: int + + +class Outcome(Enum): + STOCK_UNTOUCHED = 'stock_untouched' + SENT_UNCONFIRMED = 'sent_unconfirmed' + STOCK_RESTORED = 'stock_restored' + ABORT_UNCERTAIN = 'abort_uncertain' + + +def stock_copy(cp): + with structs.CarParams.from_bytes(cp.to_bytes()) as reader: + stock = reader.as_builder() + stock.openpilotLongitudinalControl = False + stock.pcmCruise = True + for config in stock.safetyConfigs: + config.safetyParam &= ~HyundaiSafetyFlags.LONG.value + return stock + + +class HyundaiECUStartup: + def __init__(self, cp, callbacks, *, address, bus, label): + self.address, self.bus, self.label = address, bus, label + self.cp = cp + self.callbacks = callbacks + self.outcome = None + self.disable_attempted = False + self.restored = False + self.admission = None + self.floor_ns = 0 + self.published = False + self.ci = None + self.closed = False + self.ready = False + self.prepared_cp = None + + def _disable(self, *args, **kwargs): + return disable_ecu(*args, **kwargs) + + def _query(self, *args, **kwargs): + return IsoTpParallelQuery(*args, **kwargs) + + def _warm_stock(self, ci): + raise NotImplementedError + + def _send(self, frames): + # The exact3-byte UDS request fits an ISO-TP single frame. Mark before + # invoking transport: a callback exception cannot prove no ECU mutation. + for address, data, bus in frames: + if address == self.address and bus == self.bus and len(data) >= 4 and data[:4] == b'\x03\x28\x83\x01': + self.disable_attempted = True + self.callbacks[1](frames) + + def _restore(self): + admission = cast(Callable[[], bool], self.admission) + if not admission(): + return False + recv, send = self.callbacks + session = self._query(send, recv, self.bus, [(self.address, None)], [b'\x10\x03'], [b'\x50\x03']) + if not session.get_data(.1): + return False + # Restore is deliberately unsuppressed; a sent288001 cannot prove stock. + restore = self._query(send, recv, self.bus, [(self.address, None)], [b'\x28\x00\x01'], [b'\x68\x00']) + if not restore.get_data(.1): + return False + self.restored = True + self.floor_ns = time.clock_gettime_ns(time.CLOCK_BOOTTIME) + return True + + def prepare(self, *, admission): + self.admission = admission + self.floor_ns = time.clock_gettime_ns(time.CLOCK_BOOTTIME) + if not admission(): + self.outcome = Outcome.STOCK_UNTOUCHED + self.cp = stock_copy(self.cp) + self.prepared_cp = deepcopy(self.cp.to_dict()) + return self.cp + try: + success = self._disable(self.callbacks[0], self._send, bus=self.bus, addr=self.address, com_cont_req=b'\x28\x83\x01') + except Exception: + success = False + if success and self.disable_attempted: + self.outcome = Outcome.SENT_UNCONFIRMED + self.prepared_cp = deepcopy(self.cp.to_dict()) + return self.cp + if not self.disable_attempted: + self.outcome = Outcome.STOCK_UNTOUCHED + else: + try: + restored = self._restore() + except Exception: + restored = False + if not restored: + self.outcome = Outcome.ABORT_UNCERTAIN + raise RuntimeError(f'{self.label} disable failed; stock ECU restoration unverified') + self.outcome = Outcome.STOCK_RESTORED + self.cp = stock_copy(self.cp) + self.prepared_cp = deepcopy(self.cp.to_dict()) + return self.cp + + def prepared_for(self, cp): + return (self.outcome is not None and self.outcome is not Outcome.ABORT_UNCERTAIN and + self.prepared_cp is not None and cp.to_dict() == self.prepared_cp) + + def configure(self, ci): + if not self.prepared_for(ci.CP): + raise RuntimeError(f'{self.label} prepared startup does not match constructed CP') + self.ci = ci + if self.outcome is Outcome.SENT_UNCONFIRMED: + self.ready = True + return # Preserve ordinary controller commands and1Hz tester cadence. + self._warm_stock(ci) + + def finalize_aol_configuration(self, ci): + """Finalize only the supported stock AOL marker before publication.""" + if self.published or self.closed or ci is not self.ci or not self.ready: + raise RuntimeError('Hyundai AOL finalization requires configured startup') + if self.prepared_for(ci.CP): + return + from opendbc.car.hyundai.ev6_aol import qualified as ev6_aol_qualified + if self.outcome is Outcome.SENT_UNCONFIRMED and ev6_aol_qualified(ci.CP, marked_only=True): + expected = deepcopy(cast(PreparedParams, self.prepared_cp)) + configs = expected.get('safetyConfigs', []) + owner = config_index(ci.CP) + if owner is None or len(configs) != owner + 1 or configs[owner]['safetyParam'] != 0x15 or expected['alternativeExperience'] != 0: + raise RuntimeError('EV6 prepared LONG profile cannot be finalized') + configs[owner]['safetyParam'] |= 0x0800 + expected['alternativeExperience'] = 32 + if ci.CP.to_dict() != expected: + raise RuntimeError('EV6 AOL finalization changed unrelated CarParams') + self.prepared_cp = expected + return + from opendbc.car.hyundai.canfd_stock_aol import qualified as stock_aol_qualified + if (self.outcome not in (Outcome.STOCK_UNTOUCHED, Outcome.STOCK_RESTORED) or + not stock_aol_qualified(ci.CP, marked_only=True)): + raise RuntimeError('Hyundai startup does not admit this AOL configuration') + expected = deepcopy(cast(PreparedParams, self.prepared_cp)) + configs = expected.get('safetyConfigs', []) + owner = config_index(ci.CP) + if owner is None or len(configs) != owner + 1 or configs[owner]['safetyParam'] not in (0x11, 0x91): + raise RuntimeError('Hyundai startup stock profile cannot be finalized') + configs[owner]['safetyParam'] |= 0x0800 + if ci.CP.to_dict() != expected: + raise RuntimeError('Hyundai AOL finalization changed unrelated CarParams') + self.prepared_cp = expected + + def seal_publication(self): + self.check() + if self.ci is None or not self.ready: + raise RuntimeError(f'{self.label} startup requires configure before publication') + if not self.prepared_for(self.ci.CP): + raise RuntimeError(f'{self.label} CP differs from its prepared startup decision') + self.published = True + + def check(self): + if self.outcome is None or self.outcome is Outcome.ABORT_UNCERTAIN: + raise RuntimeError('Unresolved Hyundai startup') + + def close(self): + if self.closed: + return + self.closed = True + if self.disable_attempted and not self.published and not self.restored: + try: + restored = self._restore() + except Exception: + restored = False + if not restored: + self.outcome = Outcome.ABORT_UNCERTAIN + raise RuntimeError(f'{self.label} unpublished startup cleanup could not confirm stock restoration') + + def after_state(self, **context): + pass + + def consume_cruise_resume(self): + return None + + def before_control(self, **context): + self.check() + return True + + def sources_current(self, ci, now_ns): + return True + + def maintain(self, *, configured): + pass + + def sent(self, frames, *, valid): + pass diff --git a/opendbc_repo/opendbc/car/hyundai/ev6_aol.py b/opendbc_repo/opendbc/car/hyundai/ev6_aol.py new file mode 100644 index 0000000000..62fa641036 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/ev6_aol.py @@ -0,0 +1,42 @@ +"""First-generation EV6 physical authorization for the existing torque lease.""" +from opendbc.car.hyundai.canfd_stock_aol import _base_word +from opendbc.car.hyundai.canfd_owner import config_index +from opendbc.car.hyundai.ev6_startup import eligible +from opendbc.car.hyundai.values import Buttons + + +def qualified(cp, *, marked_only=False): + if not eligible(cp) or not cp.openpilotLongitudinalControl or cp.pcmCruise: + return False + if _base_word(cp, experiences=(0, 32)) != 0x11: + return False + word = cp.safetyConfigs[config_index(cp)].safetyParam + return (word == 0x815 and cp.alternativeExperience == 32) or ( + not marked_only and word == 0x15 and cp.alternativeExperience == 0) + + +class Ev6PhysicalAuthorization: + def __init__(self, *, lkas_on_engage): + self.lkas_on_engage = bool(lkas_on_engage) + self.main_on = self.lkas_on = False + self.previous_main = self.previous_lkas = False + self.previous_cruise = Buttons.NONE + + def revoke(self): + # Retain actual held-button history: withdrawal is not a synthetic new edge. + self.main_on = self.lkas_on = False + + def observe(self, main, lkas, cruise, *, main_available): + if main and not self.previous_main: + self.main_on = not self.main_on + if lkas and not self.previous_lkas: + self.lkas_on = not self.lkas_on + if self.lkas_on_engage and cruise != self.previous_cruise and self.previous_cruise in (Buttons.SET_DECEL, Buttons.RES_ACCEL): + self.lkas_on = True + self.previous_main, self.previous_lkas, self.previous_cruise = bool(main), bool(lkas), cruise + if not main_available: + self.main_on = False + + @property + def authorized(self): + return self.main_on or self.lkas_on diff --git a/opendbc_repo/opendbc/car/hyundai/ev6_startup.py b/opendbc_repo/opendbc/car/hyundai/ev6_startup.py new file mode 100644 index 0000000000..7a5dbda04b --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/ev6_startup.py @@ -0,0 +1,17 @@ +"""Exact first-generation EV6 startup compatibility wrapper.""" +from opendbc.car.hyundai.captured_adrv_startup import CapturedADRVStartup, first_generation +from opendbc.car.hyundai.ev6_template import EV6Template +from opendbc.car.hyundai.values import CAR + + +def eligible(cp): + return cp.carFingerprint == CAR.KIA_EV6 and first_generation(cp) + + +def required(cp): + return eligible(cp) and cp.openpilotLongitudinalControl + + +class EV6Startup(CapturedADRVStartup): + def __init__(self, cp, callbacks): + super().__init__(cp, callbacks, label='EV6', template_type=EV6Template) diff --git a/opendbc_repo/opendbc/car/hyundai/ev6_template.py b/opendbc_repo/opendbc/car/hyundai/ev6_template.py new file mode 100644 index 0000000000..4822e4cec3 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/ev6_template.py @@ -0,0 +1,8 @@ +"""Exact EV6 compatibility descriptor; speed never changes its captured body.""" +from dataclasses import dataclass +from opendbc.car.hyundai.captured_adrv import CapturedADRVTemplate + + +@dataclass(frozen=True) +class EV6Template(CapturedADRVTemplate): + pass diff --git a/opendbc_repo/opendbc/car/hyundai/ev9_camera_lead.py b/opendbc_repo/opendbc/car/hyundai/ev9_camera_lead.py new file mode 100644 index 0000000000..85a5d96fe0 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/ev9_camera_lead.py @@ -0,0 +1,11 @@ +"""Exact EV9 longitudinal owner for optional camera lead data.""" +from opendbc.car.hyundai.canfd_camera_lead import CANFDCameraLead +from opendbc.car.hyundai.ev9_longitudinal import qualified +from opendbc.car.hyundai.hyundaicanfd import CanBus + + +class EV9CameraLead(CANFDCameraLead): + def __init__(self, cp): + if not qualified(cp): + raise ValueError('Camera lead owner requires the exact active EV9 longitudinal profile') + super().__init__(cp, CanBus(cp).ECAN) diff --git a/opendbc_repo/opendbc/car/hyundai/ev9_keeper.py b/opendbc_repo/opendbc/car/hyundai/ev9_keeper.py new file mode 100644 index 0000000000..17b3314425 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/ev9_keeper.py @@ -0,0 +1,57 @@ +"""Bounded EV9 startup tester sender; never receives CAN or restores ECUs.""" +import threading +import time + +STARTUP_DEADLINE_S = 60.0 # Shared software guard convention, not measured ECU timing. +TESTER_PERIOD_S = 1.0 # Original controller sends Tester Present every100 control frames. + + +class EV9Keeper: + def __init__(self, send, *, clock=time.monotonic, event=None, thread_factory=threading.Thread): + self.send = send + self.clock = clock + self.event = event if event is not None else threading.Event() + self.thread_factory = thread_factory + self.thread = None + self.started = None + self.next_send = None + self.abort_reason = None + + def tick(self): + if self.event.is_set() or self.abort_reason is not None: + return False + now = self.clock() + if self.started is None: + self.started = now + self.next_send = now + if now < self.started or now - self.started >= STARTUP_DEADLINE_S: + self.abort_reason = 'startup software deadline or clock reversal' + self.event.set() + return False + if now >= self.next_send: + try: + self.send() + except Exception: + self.abort_reason = 'startup tester send failed' + self.event.set() + return False + self.next_send = now + TESTER_PERIOD_S + return True + + def start(self): + if self.thread is not None: + raise RuntimeError('EV9 keeper already started') + self.started = self.clock() + self.next_send = self.started + def loop(): + while self.tick(): + self.event.wait(TESTER_PERIOD_S) + self.thread = self.thread_factory(target=loop, name='ev9-startup-tester', daemon=True) + self.thread.start() + + def stop(self): + self.event.set() + # Called outside VehicleStartupOwner.send_lock; the sender may hold it. + if self.thread is not None: + self.thread.join() + self.thread = None diff --git a/opendbc_repo/opendbc/car/hyundai/ev9_long_sender.py b/opendbc_repo/opendbc/car/hyundai/ev9_long_sender.py new file mode 100644 index 0000000000..a2a51c897e --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/ev9_long_sender.py @@ -0,0 +1,234 @@ +"""EV9 CCNC LONG wire recipes from Dom2efe, with explicit current DBC aliases.""" +import numpy as np +from opendbc.car import CanData +from opendbc.car.common.conversions import Conversions as CV +from opendbc.car.hyundai.values import CAR +from opendbc.car.hyundai.hyundaicanfd import hkg_can_fd_checksum + +def _set_value(msg: bytearray, sig, ival: int) -> None: + i = sig.lsb // 8 + bits = sig.size + if sig.size < 64: + ival &= (1 << sig.size) - 1 + while 0 <= i < len(msg) and bits > 0: + shift = sig.lsb % 8 if (sig.lsb // 8) == i else 0 + size = min(bits, 8 - shift) + mask = ((1 << size) - 1) << shift + msg[i] &= ~mask + msg[i] |= (ival & ((1 << size) - 1)) << shift + bits -= size + ival >>= size + i = i + 1 if sig.is_little_endian else i - 1 + +def _create_angle_adas_cmd_msg(packer, CAN, apply_angle: float, lat_active: bool, torque_reduction_gain: float): + values = { + "ADAS_ActvACISta": 0, + "ADAS_ActvACILvl2Sta": 2 if lat_active else 1, + "ADAS_StrAnglReqVal": apply_angle, + "ADAS_ACIAnglTqRedcGainVal": torque_reduction_gain if lat_active else 0.0, + "FCA_ESA_ActvSta": 0, + "FCA_ESA_TqBstGainVal": 0.0, + } + return packer.make_can_msg("LFA_ALT", CAN.ECAN, values) + +def create_angle_adas_cmd(packer, CAN, apply_angle: float, lat_active: bool, torque_reduction_gain: float): + return _create_angle_adas_cmd_msg(packer, CAN, apply_angle, lat_active, torque_reduction_gain) + +def create_inactive_angle_steering_messages(packer, CAN, steering_angle: float): + # Explicit nonzero original aliases: LKA_MODE/ICON/TORQUE_REQUEST/DAMP_FACTOR. + neutral = packer.make_can_msg("LFA", CAN.ECAN, {"LKA_OptUsmSta": 2, "LKA_SysIndReq": 1, + "StrTqReqVal": 0, "Damping_Gain": 100}) + return [neutral, create_angle_adas_cmd(packer, CAN, steering_angle, False, 0.0)] + + +def create_ccnc_blindspot_status_messages(packer, CP, CAN, counter, left_blindspot=False, right_blindspot=False, + left_escalated=False, right_escalated=False, drive_gear=False, + left_warning_lamp=False, right_warning_lamp=False, + left_sound_active=False, right_sound_active=False): + left_state = 2 if left_blindspot and left_escalated else (1 if left_blindspot else 0) + right_state = 2 if right_blindspot and right_escalated else (1 if right_blindspot else 0) + left_osm_state = 2 if left_warning_lamp else 1 if left_state == 1 else 0 + right_osm_state = 2 if right_warning_lamp else 1 if right_state == 1 else 0 + desired_fields = { + "BCW_IndSta": 1, + "BCA_OnOffEquip2Sta": 2, + "BCA_Sta": int(drive_gear), + "BCW_LtIndSta": left_state, + "BCW_RtIndSta": right_state, + "BCW_LtSndWrngSta": int(left_sound_active), + "BCW_RtSndWrngSta": int(right_sound_active), + "OSMrrLamp_LtIndSta": left_osm_state, + "OSMrrLamp_RtIndSta": right_osm_state, + } + + return [ + _create_ccnc_adrv_message_with_signals( + packer, CP, CAN, 0x1BA, counter, "ADAS_CMD_50_50ms", desired_fields, + ), + # No retained radar input reproduces the stock RCTA target decision across routes. + _create_ccnc_adrv_message(CP.carFingerprint, 0x1E5, CAN.ECAN, counter), + ] + +def create_ccnc_adrv_messages(packer, CP, CAN, frame, enabled, main_cruise_enabled, hud, out, is_metric, + steering_available, steering_active, left_blindspot, right_blindspot, + drive_gear=False, + hba_icon=0, + left_escalated=False, right_escalated=False, + left_warning_lamp=False, right_warning_lamp=False, + left_sound_active=False, right_sound_active=False): + ret = [ + _create_ccnc_adrv_message(CP.carFingerprint, address, CAN.ECAN, frame // period) + for address, period in _CCNC_ADRV_PERIODS[CP.carFingerprint].items() if frame % period == 0 + ] + if frame % 5 == 0: + ret.extend(create_ccnc_angle_long_status_messages( + packer, CP, CAN, frame // 5, enabled, main_cruise_enabled, hud, out, is_metric, + steering_available, steering_active, hba_icon, + )) + ret.extend(create_ccnc_blindspot_status_messages( + packer, CP, CAN, frame // 5, left_blindspot, right_blindspot, left_escalated, right_escalated, + drive_gear, + left_warning_lamp, right_warning_lamp, left_sound_active, right_sound_active, + )) + return ret + +_CCNC_ADRV_TEMPLATES = {CAR.KIA_EV9: { + 0x160: bytes.fromhex("0000000100000000fffc0100a8001000"), + 0x1DA: bytes.fromhex("0000002200110000000000000000000000000000000000000000000000000000"), + 0x1EA: bytes.fromhex("000000080000000000000000000000ff000000000000000000000000000f0f00"), + 0x200: bytes.fromhex("00000014801a0000"), + 0x345: bytes.fromhex("0000001500560000"), + 0x161: bytes.fromhex("0000000000000000c0fff0c003000040000000000000000000ff000000000000"), + 0x162: bytes.fromhex("0000002700000000000000000000000000000000000000000000000000000000"), + 0x1BA: bytes.fromhex("00000000000000880200000000000000000100000000000f"), + 0x1E5: bytes.fromhex("00000000000000000000220300000080"), + 0x1E0: bytes.fromhex("00000002000000000000000000000000"), + 0x38C: bytes.fromhex("000000f71f000000000000000000000000000000000000000000000000000000"), + }} + +_CCNC_ADRV_PERIODS = {CAR.KIA_EV9: { + 0x160: 2, + 0x1DA: 100, + 0x1EA: 5, + 0x200: 5, + 0x345: 20, + 0x1E0: 5, + 0x38C: 20, + }} + +def _create_ccnc_adrv_message(car_fingerprint, address: int, bus: int, counter: int) -> CanData: + d = bytearray(_CCNC_ADRV_TEMPLATES[car_fingerprint][address]) + d[2] = counter & 0xFF + crc = hkg_can_fd_checksum(address, None, d) + d[0] = crc & 0xFF + d[1] = (crc >> 8) & 0xFF + return CanData(address, bytes(d), bus) + +def _set_ccnc_message_signals(packer, message_name: str, dat: bytearray, values: dict) -> None: + dbc_msg = packer.dbc.name_to_msg[message_name] + for name, value in values.items(): + sig = dbc_msg.sigs[name] + ival = int(np.floor((value - sig.offset) / sig.factor + 0.5)) + if ival < 0: + ival = (1 << sig.size) + ival + _set_value(dat, sig, ival) + +def _create_ccnc_adrv_message_with_signals(packer, CP, CAN, address: int, counter: int, + message_name: str, values: dict) -> CanData: + msg = _create_ccnc_adrv_message(CP.carFingerprint, address, CAN.ECAN, counter) + dat = bytearray(msg.dat) + # Update decoded fields in the verified neutral payload. + _set_ccnc_message_signals(packer, message_name, dat, values) + crc = hkg_can_fd_checksum(address, None, dat) + dat[0] = crc & 0xFF + dat[1] = (crc >> 8) & 0xFF + return CanData(address, bytes(dat), CAN.ECAN) + +def create_ccnc_acc_control(packer, CAN, enabled: bool, accel: float, + stop_request: bool, cruise_standstill: bool, gas_override: bool, set_speed: float, + main_mode_acc: int, lead_distance: float, lead_rel_speed: float, lead_visible: bool, + v_ego: float, jerk_lower: float = 0.7, jerk_upper: float = 0.7): + if not enabled or gas_override or stop_request: + accel = 0.0 + + lead_visible = bool(enabled and lead_visible) + desired_headway = min(max(round(1.625 * max(v_ego, 0.0), 1), 3.5), 204.6) if enabled else 204.6 + values = { + "ACCMode": 0 if not enabled else (2 if gas_override else 1), + "MainMode_ACC": int(bool(main_mode_acc)), + "StopReq": 1 if stop_request and enabled else 0, + "CRUISE_STANDSTILL": 1 if cruise_standstill and stop_request and enabled else 0, + "aReqValue": accel, + "aReqRaw": accel, + "VSetDis": set_speed, + "JerkLowerLimit": jerk_lower if enabled else 1.0, + "JerkUpperLimit": jerk_upper if enabled else 3.0, + "ACC_ObjDist": float(np.clip(lead_distance, 0.0, 204.7)) if lead_visible else 204.6, + "ACC_ObjRelSpd": float(np.clip(lead_rel_speed, -16.4, 34.7)) if lead_visible else 34.6, + "ObjValid": 0 if lead_visible else 1, + "OBJ_STATUS": 2 if enabled and lead_visible else 0, + "NEW_SIGNAL_15": desired_headway, + "SET_ME_2": 4, + "SET_ME_3": 3, + "SET_ME_TMP_64": 0x64, + # Stock CCNC LKA-long routes use raw 7. The DBC's physical range is stale. + "DISTANCE_SETTING": 7 if enabled else 0, + } + + address, payload, bus = packer.make_can_msg("SCC_CONTROL", CAN.ECAN, values) + dat = bytearray(payload) + # Original NEW_SIGNAL_3 is bits108..109 (Motorola109|2); current SCC_ObjSta + # additionally names bit110. Preserve the literal two-bit value, leaving110 zero. + dat[13] = (dat[13] & ~0x30) | ((2 if lead_visible else 0) << 4) + dat[0:2] = hkg_can_fd_checksum(address, None, dat).to_bytes(2, "little") + return CanData(address, bytes(dat), bus) + +def create_ccnc_angle_long_status_messages(packer, CP, CAN, counter: int, enabled: bool = False, + main_cruise_enabled: bool = False, hud=None, out=None, + is_metric: bool = True, steering_available: bool = False, + steering_active: bool = False, hba_icon: int = 0) -> list[CanData]: + cruise_speed = round(out.vCruiseCluster * (1 if is_metric else CV.KPH_TO_MPH)) if out is not None else 0 + display_speed = (40 if is_metric else 25) if cruise_speed > (145 if is_metric else 90) else max(cruise_speed, 0) + main_standby = bool(main_cruise_enabled and not enabled) + values_161 = { + "FCA_ICON": 1, # orange: FCA unavailable + "FCA_ALT_ICON": 0, + "FCA_IMAGE": 0, + "ALERTS_1": 0, + "ALERTS_2": 0, + "ALERTS_3": 0, + "ALERTS_4": 0, + "ALERTS_5": 0, + "SOUNDS_1": 0, + "SOUNDS_2": 0, + "SOUNDS_3": 0, + "SOUNDS_4": 0, + "LFA_ICON": (2 if steering_active else 1) if steering_available else 0, + "HBA_ICON": hba_icon if hba_icon in (1, 2) else 0, + "HDA_ICON": 2 if enabled else 1 if main_standby else 0, + "TARGET": 3 if enabled else 0, + "SETSPEED": 3 if enabled else 1 if main_standby else 0, + "SETSPEED_HUD": 2 if enabled else 1 if main_standby else 0, + "SETSPEED_SPEED": display_speed if enabled or main_standby else 255, + "DISTANCE": hud.leadDistanceBars if enabled and hud is not None else 0, + "DISTANCE_SPACING": 3 if enabled or main_standby else 0, + "DISTANCE_CAR": 2 if enabled else 1 if main_standby else 0, + } + values_162 = {fault: 0 for fault in ( + "FAULT_FSS", "FAULT_FCA", "FAULT_LSS", "FAULT_SLA", "FAULT_HDA", "FAULT_DAS", "FAULT_LFA", "FAULT_DAW", + "FAULT_HBA", "FAULT_ESS", + )} + values_162["VIBRATE"] = 0 + return [ + _create_ccnc_adrv_message_with_signals(packer, CP, CAN, 0x161, counter, "CCNC_0x161", values_161), + _create_ccnc_adrv_message_with_signals(packer, CP, CAN, 0x162, counter, "CCNC_0x162", values_162), + ] + +def create_radar_heartbeat(counter, brake_pressed, accelerator_pressed): + d = bytearray.fromhex("00000000ff006f00e80400001201030055ffff0000000000") + d[2] = counter & 0xff + d[4] = (d[4] & ~1) | int(brake_pressed) + d[22] = (d[22] & ~1) | int(accelerator_pressed) + crc = hkg_can_fd_checksum(0x100, None, d) + d[0], d[1] = crc & 0xff, crc >> 8 + return CanData(0x100, bytes(d), 0) diff --git a/opendbc_repo/opendbc/car/hyundai/ev9_longitudinal.py b/opendbc_repo/opendbc/car/hyundai/ev9_longitudinal.py new file mode 100644 index 0000000000..3efbd62bfa --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/ev9_longitudinal.py @@ -0,0 +1,336 @@ +# EV9 calibration extracted from the reached Dom controller; isolated from Ioniq6 policy. +# Preserve source calibration, 20Hz update and 50Hz stop latch separately. +from dataclasses import dataclass +import numpy as np +from opendbc.car import DT_CTRL, structs +from opendbc.car.hyundai.values import CAR +from opendbc.car.hyundai.ccnc_ev_stock import qualified as stock_qualified + +class CarControllerParams: + ACCEL_MIN = -3.5 + ACCEL_MAX = 3.5 + +LongCtrlState = structs.CarControl.Actuators.LongControlState + +IONIQ_6_RESPONSE_MULTIPLIER = 1.2 + +IONIQ_6_LONG_MIN_JERK = 0.5 * IONIQ_6_RESPONSE_MULTIPLIER + +IONIQ_6_LONG_JERK_LIMIT = 4.8 * IONIQ_6_RESPONSE_MULTIPLIER + +EV9_LONG_DECEL_JERK = 2.0 + +IONIQ_6_LONG_LOOKAHEAD_JERK_BP = [2.0, 5.0, 20.0] + +IONIQ_6_LONG_LOOKAHEAD_JERK_V = [0.3 / IONIQ_6_RESPONSE_MULTIPLIER, + 0.45 / IONIQ_6_RESPONSE_MULTIPLIER, + 0.6 / IONIQ_6_RESPONSE_MULTIPLIER] + +IONIQ_6_DYNAMIC_LOWER_JERK_BP = [-2.0, -1.5, -1.0, -0.25, -0.1, -0.025, -0.01, -0.005] + +IONIQ_6_DYNAMIC_LOWER_JERK_V = [3.3, 1.5, 1.0, 0.8, 0.7, 0.65, 0.55, 0.5] + +IONIQ_6_LAUNCH_HOLD_SPEED_BP = [0.0, 0.6, 1.25, 2.5] + +IONIQ_6_LAUNCH_HOLD_SPEED_V = [0.75, 0.6, 0.4, 0.0] + +IONIQ_6_STOP_BRAKE_CAP_MAX_SPEED = 2.0 + +IONIQ_6_STOP_BRAKE_CAP_SPEED_BP = [0.0, 0.08, 0.25, 0.6, 1.2, 2.0, 3.0] + +IONIQ_6_STOP_BRAKE_CAP_ACCEL_V = [-0.15, -0.16, -0.22, -0.42, -0.78, -1.15, -1.40] + +EV6_GT_LINE_STOP_BRAKE_CAP_MAX_SPEED = 1.2 + +IONIQ_6_STOP_HOLD_JERK_BP = [0.0, 0.15, 0.6, 1.2, 2.0, 3.0] + +IONIQ_6_STOP_HOLD_JERK_V = [0.35, 0.40, 0.48, 0.65, 0.85, 1.10] + +IONIQ_6_STOP_RELEASE_JERK_BP = [0.0, 0.15, 0.5] + +IONIQ_6_STOP_RELEASE_JERK_V = [3.6 * IONIQ_6_RESPONSE_MULTIPLIER, + 4.2 * IONIQ_6_RESPONSE_MULTIPLIER, + 4.8 * IONIQ_6_RESPONSE_MULTIPLIER] + +EV9_STOP_REQUEST_SPEED = 0.47 + +EV9_STANDSTILL_DELAY_FRAMES = 178 + +EV9_STOP_RELEASE_DELAY_FRAMES = 6 + +@dataclass +class Ioniq6LongitudinalTuningState: + desired_accel: float = 0.0 + actual_accel: float = 0.0 + accel_last: float = 0.0 + jerk_upper: float = 0.0 + jerk_lower: float = 0.0 + launch_active: bool = False + stopping: bool = False + stopping_count: int = 0 + long_control_state_last: LongCtrlState = LongCtrlState.off + +@dataclass(frozen=True) +class EV9LongitudinalTuningState: + stop_request: bool = False + cruise_standstill: bool = False + stop_request_frames: int = 0 + release_frames: int = 0 + +def _jerk_limited_integrator(desired_accel: float, last_accel: float, jerk_upper: float, jerk_lower: float) -> float: + step = (jerk_upper if desired_accel >= last_accel else jerk_lower) * DT_CTRL * 5.0 + return float(np.clip(desired_accel, last_accel - step, last_accel + step)) + +def _calculate_ioniq_6_dynamic_lower_jerk(accel_error: float) -> float: + if accel_error < 0.0: + scaled_values = np.array(IONIQ_6_DYNAMIC_LOWER_JERK_V) * (IONIQ_6_LONG_JERK_LIMIT / IONIQ_6_DYNAMIC_LOWER_JERK_V[0]) + return float(np.interp(accel_error, IONIQ_6_DYNAMIC_LOWER_JERK_BP, scaled_values)) + return IONIQ_6_LONG_MIN_JERK + +def update_ev9_longitudinal_tuning(state: EV9LongitudinalTuningState, enabled: bool, + stopping: bool, v_ego: float) -> EV9LongitudinalTuningState: + if not enabled: + return EV9LongitudinalTuningState() + + if stopping: + if not state.stop_request and v_ego > EV9_STOP_REQUEST_SPEED: + return EV9LongitudinalTuningState() + frames = state.stop_request_frames + 1 if state.stop_request else 0 + return EV9LongitudinalTuningState( + stop_request=True, + cruise_standstill=frames >= EV9_STANDSTILL_DELAY_FRAMES, + stop_request_frames=frames, + ) + + if state.stop_request: + release_frames = state.release_frames + 1 + if release_frames <= EV9_STOP_RELEASE_DELAY_FRAMES: + return EV9LongitudinalTuningState( + stop_request=True, + cruise_standstill=False, + stop_request_frames=state.stop_request_frames, + release_frames=release_frames, + ) + + return EV9LongitudinalTuningState() + +def reset_egmp_longitudinal_tuning(state: Ioniq6LongitudinalTuningState) -> Ioniq6LongitudinalTuningState: + state.desired_accel = 0.0 + state.actual_accel = 0.0 + state.accel_last = 0.0 + state.jerk_upper = 0.0 + state.jerk_lower = 0.0 + state.launch_active = False + return state + +def update_ioniq_6_longitudinal_tuning(state: Ioniq6LongitudinalTuningState, accel_cmd: float, v_ego: float, a_ego: float, + long_control_state: LongCtrlState, long_active: bool, + ev6_gt_line: bool = False, low_speed_stop_brake_cap: bool = False, + ev9: bool = False) -> Ioniq6LongitudinalTuningState: + starting = long_control_state == LongCtrlState.starting + stopping = long_control_state == LongCtrlState.stopping + restart_from_stop = state.long_control_state_last in (LongCtrlState.stopping, LongCtrlState.starting) and \ + long_control_state in (LongCtrlState.starting, LongCtrlState.pid) and accel_cmd > 0.0 and v_ego < 0.5 + + state.stopping = long_active and stopping + state.stopping_count = state.stopping_count + 1 if state.stopping else 0 + + if not long_active: + state.desired_accel = 0.0 + state.actual_accel = 0.0 + state.accel_last = 0.0 + state.jerk_upper = 0.0 + state.jerk_lower = 0.0 + state.launch_active = False + state.long_control_state_last = long_control_state + return state + + if accel_cmd <= 0.0 or v_ego >= IONIQ_6_LAUNCH_HOLD_SPEED_BP[-1]: + state.launch_active = False + elif starting or (state.launch_active and v_ego < IONIQ_6_LAUNCH_HOLD_SPEED_BP[-1]) or \ + (state.long_control_state_last == LongCtrlState.starting and long_control_state == LongCtrlState.pid and v_ego < IONIQ_6_LAUNCH_HOLD_SPEED_BP[-1]): + state.launch_active = True + + upper_speed_limit = float(np.interp(v_ego, [0.0, 5.0, 20.0], [2.0, 3.0, 2.0])) * IONIQ_6_RESPONSE_MULTIPLIER if long_control_state == LongCtrlState.pid else IONIQ_6_LONG_MIN_JERK + lower_speed_limit = float(np.interp(v_ego, [0.0, 5.0, 20.0], [5.0, 3.5, 3.0])) * IONIQ_6_RESPONSE_MULTIPLIER + + future_t_upper = float(np.interp(v_ego, IONIQ_6_LONG_LOOKAHEAD_JERK_BP, IONIQ_6_LONG_LOOKAHEAD_JERK_V)) + + accel_error = accel_cmd - state.accel_last + j_ego_upper = float(np.clip(accel_error / future_t_upper, -IONIQ_6_LONG_JERK_LIMIT, IONIQ_6_LONG_JERK_LIMIT)) + desired_jerk_upper = min(max(j_ego_upper, IONIQ_6_LONG_MIN_JERK), upper_speed_limit) + + dynamic_accel_error = a_ego - state.accel_last + dynamic_lower_jerk = _calculate_ioniq_6_dynamic_lower_jerk(dynamic_accel_error) + state.jerk_upper = desired_jerk_upper + state.jerk_lower = min(dynamic_lower_jerk, lower_speed_limit) + if ev9: + state.jerk_lower = min(state.jerk_lower, EV9_LONG_DECEL_JERK) + + if state.stopping: + stop_brake_cap_max_speed = EV6_GT_LINE_STOP_BRAKE_CAP_MAX_SPEED if ev6_gt_line or low_speed_stop_brake_cap else \ + IONIQ_6_STOP_BRAKE_CAP_MAX_SPEED + if v_ego <= stop_brake_cap_max_speed: + stop_brake_cap = float(np.interp(v_ego, IONIQ_6_STOP_BRAKE_CAP_SPEED_BP, IONIQ_6_STOP_BRAKE_CAP_ACCEL_V)) + state.desired_accel = min(0.0, max(accel_cmd, stop_brake_cap)) + state.jerk_upper = min(state.jerk_upper, float(np.interp(v_ego, IONIQ_6_STOP_HOLD_JERK_BP, IONIQ_6_STOP_HOLD_JERK_V)) * IONIQ_6_RESPONSE_MULTIPLIER) + else: + state.desired_accel = float(np.clip(accel_cmd, CarControllerParams.ACCEL_MIN, 0.0)) + else: + state.desired_accel = float(np.clip(accel_cmd, CarControllerParams.ACCEL_MIN, CarControllerParams.ACCEL_MAX)) + if state.launch_active: + state.desired_accel = max(state.desired_accel, float(np.interp(v_ego, IONIQ_6_LAUNCH_HOLD_SPEED_BP, IONIQ_6_LAUNCH_HOLD_SPEED_V))) + state.jerk_upper = max(state.jerk_upper, float(np.interp(v_ego, [0.0, 2.5], [4.8, 3.2])) * IONIQ_6_RESPONSE_MULTIPLIER) + state.jerk_lower = max(state.jerk_lower, 1.0) + if restart_from_stop: + state.jerk_upper = min(state.jerk_upper, float(np.interp(v_ego, IONIQ_6_STOP_RELEASE_JERK_BP, IONIQ_6_STOP_RELEASE_JERK_V))) + + state.actual_accel = _jerk_limited_integrator(state.desired_accel, state.accel_last, state.jerk_upper, state.jerk_lower) + state.accel_last = state.actual_accel + state.long_control_state_last = long_control_state + return state + + +BLINDSPOT_WARNING_FLASH_SAMPLES = 20 + +BLINDSPOT_WARNING_FLASH_ON_SAMPLES = 16 + +BLINDSPOT_WARNING_SOUND_SAMPLES = 36 + +@dataclass(frozen=True) +class BlindspotWarningOutput: + mirror_lamp_active: bool = False + sound_active: bool = False + +@dataclass +class BlindspotWarningState: + flash_phase: int = 0 + mirror_warning_active: bool = False + escalated_prev: bool = False + sound_remaining: int = 0 + sound_armed: bool = True + +def update_blindspot_warning(state: BlindspotWarningState, escalated: bool, + blinker: bool) -> BlindspotWarningOutput: + if not blinker: + state.flash_phase = 0 + state.mirror_warning_active = False + state.escalated_prev = False + state.sound_remaining = 0 + state.sound_armed = True + return BlindspotWarningOutput() + + rising = escalated and not state.escalated_prev + if rising: + state.flash_phase = 0 + state.mirror_warning_active = True + if state.sound_armed: + state.sound_remaining = BLINDSPOT_WARNING_SOUND_SAMPLES + state.sound_armed = False + elif escalated: + state.flash_phase = (state.flash_phase + 1) % BLINDSPOT_WARNING_FLASH_SAMPLES + state.mirror_warning_active = True + elif state.mirror_warning_active and state.flash_phase < BLINDSPOT_WARNING_FLASH_ON_SAMPLES - 1: + state.flash_phase += 1 + else: + state.flash_phase = 0 + state.mirror_warning_active = False + + state.escalated_prev = escalated + sound_active = state.sound_remaining > 0 + if state.sound_remaining > 0: + state.sound_remaining -= 1 + return BlindspotWarningOutput( + mirror_lamp_active=state.mirror_warning_active and state.flash_phase < BLINDSPOT_WARNING_FLASH_ON_SAMPLES, + sound_active=sound_active, + ) + + + +class EV9LongitudinalPolicy: + def __init__(self): + self.tuning = Ioniq6LongitudinalTuningState() + self.stop = EV9LongitudinalTuningState() + self.left_warning = BlindspotWarningState() + self.right_warning = BlindspotWarningState() + + def update(self, frame, accel, speed, measured_accel, control_state, enabled, override): + # Original reset runs at100Hz, calibration only every fifth controller frame. + if self.stop.stop_request or not enabled or override: + reset_egmp_longitudinal_tuning(self.tuning) + active = control_state in (LongCtrlState.starting, LongCtrlState.pid, LongCtrlState.stopping) + if active and frame % 5 == 0: + update_ioniq_6_longitudinal_tuning(self.tuning, accel, speed, measured_accel, control_state, True, + low_speed_stop_brake_cap=True, ev9=True) + return self.tuning.actual_accel if active else accel + + def update_stop(self, enabled, override, stopping, speed): + self.stop = update_ev9_longitudinal_tuning(self.stop, enabled and not override, stopping, speed) + if self.stop.stop_request or not enabled or override: + reset_egmp_longitudinal_tuning(self.tuning) + return 0.0 + return None + + + +STOCK_PARAM = 0x5c91 +LONG_PARAM = 0x5c95 + + +def copy_cp(cp): + with structs.CarParams.from_bytes(structs.CarParams.new_message(**cp.to_dict()).to_bytes()) as reader: + return reader.as_builder() + + +def stock_copy(cp): + stock = copy_cp(cp) + stock.safetyConfigs[0].safetyParam = STOCK_PARAM + stock.openpilotLongitudinalControl = False + stock.pcmCruise = True + stock.longitudinalActuatorDelay = 0.5 + return stock + + +def candidate(stock, *, enabled, is_release): + if not enabled or is_release or stock.carFingerprint != CAR.KIA_EV9 or not stock_qualified(stock): + return stock + result = copy_cp(stock) + result.alphaLongitudinalAvailable = True + result.openpilotLongitudinalControl = True + result.pcmCruise = False + result.safetyConfigs[0].safetyParam = LONG_PARAM + result.longitudinalActuatorDelay = 0.3 + return result + + +def qualified(cp): + if (cp.carFingerprint != CAR.KIA_EV9 or not cp.openpilotLongitudinalControl or cp.pcmCruise or + not cp.alphaLongitudinalAvailable or len(cp.safetyConfigs) != 1 or cp.safetyConfigs[0].safetyParam != LONG_PARAM): + return False + return stock_qualified(stock_copy(cp)) + + +class LegacyAngleEnvelope: + """Original Sportage safety envelope, never vehicle identity or metadata. + + Original source VM: wheelbase2.756, steerRatio13.7, zero rear steering, + slip factor -.0006085930193026732 (original native mode mirrors that CP). + Only the EV9 LONG owner's original host calibration consumes this model. + """ + def get_steer_from_curvature(self, curvature, speed, roll): + assert roll == 0 + # Original CarParams Float32 stores precede VehicleModel construction. + return curvature * 2.75600004196167 * 13.699999809265137 * (1.0 - (-0.0006085930193026732) * speed ** 2) + + + +def lateral_request_allowed(control, state, angle_fault): + """Host half of the existing exact5c95 native actuation predicates. + + Independent AOL cannot supply authority. Unlike the original reached caller, + stale latActive alone does not bypass engagement, pedals, standstill or health. + CAN validity is the host's observable health; native RX validation remains final. + """ + return (control.enabled and state.canValid and not state.canTimeout and not state.standstill and + state.gearShifter == structs.CarState.GearShifter.drive and + not (state.brakePressed or state.gasPressed or state.steerFaultTemporary or state.steerFaultPermanent or angle_fault)) diff --git a/opendbc_repo/opendbc/car/hyundai/ev9_startup.py b/opendbc_repo/opendbc/car/hyundai/ev9_startup.py new file mode 100644 index 0000000000..956bebbdb1 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/ev9_startup.py @@ -0,0 +1,168 @@ +"""EV9 source-selected communication-control startup, resolved before CarParams publication.""" +from copy import deepcopy +import time + +from opendbc.car import Bus, make_tester_present_msg +from opendbc.can.parser import MAX_BAD_COUNTER +from opendbc.car.hyundai.ecu_startup import HyundaiECUStartup, Outcome +from opendbc.car.hyundai.ev9_longitudinal import qualified, copy_cp +from opendbc.car.hyundai.ccnc_ev_stock import qualified as stock_qualified +from opendbc.car.hyundai.values import CAR +from opendbc.car.hyundai.ev9_keeper import EV9Keeper + + +def required(cp): + # A malformed LONG CP still requires the owner; it must never bypass it. + return cp.carFingerprint == CAR.KIA_EV9 and cp.openpilotLongitudinalControl + + +class EV9Startup(HyundaiECUStartup): + def __init__(self, cp, callbacks, *, stock_cp, clock=time.monotonic, keeper_factory=EV9Keeper): + super().__init__(cp, callbacks, address=0x730, bus=1, label='EV9') + if stock_cp.carFingerprint != CAR.KIA_EV9 or not stock_qualified(stock_cp): + raise ValueError('EV9 startup requires exact original stock CP') + self.clock = clock + self.stock_cp = copy_cp(stock_cp) + self.keeper_factory = keeper_factory + self.keeper = None + self.handed_off = False + self.disable_response = None + + + def _send(self, frames): + # Source-selected EV9 request is unsuppressed enable-RX/disable-TX. + for address, data, bus in frames: + if address == self.address and bus == self.bus and len(data) >= 4 and data[:4] == b"\x03\x28\x01\x01": + self.disable_attempted = True + self.callbacks[1](frames) + + def _disable(self, can_recv, can_send, **kwargs): + # Keep the original outcome distinction: command silence is sent, + # unconfirmed; an explicit negative reply is failure. No SecurityAccess. + for _ in range(10): + try: + session = self._query(can_send, can_recv, self.bus, [(self.address, None)], [b"\x10\x03"], [b"\x50\x03"]) + if not session.get_data(.1): + time.sleep(.1) + continue + time.sleep(.05) + command = self._query(can_send, can_recv, self.bus, [(self.address, None)], [b"\x28\x01\x01"], [b""]) + replies = command.get_data(.1) + if not replies: + self.disable_response = 'no_reply_sent_unconfirmed' + return True + for payload in replies.values(): + if payload == b"\x68\x01": + self.disable_response = 'positive_communication_control' + elif len(payload) == 3 and payload[:2] == b"\x7f\x28": + self.disable_response = 'negative_communication_control' + return False + else: + self.disable_response = 'unexpected_or_malformed_reply' + return False + return True + except Exception: + # Once the command was emitted, do not retry into uncertain ownership. + # The shared owner restores before attempting stock publication. + if self.disable_attempted: + raise + time.sleep(.1) + return False + + def _ready(self): + deadline = self.clock() + 0.5 + while self.clock() < deadline: + packets = self.callbacks[0](wait_for_one=False) + for packet in packets: + for message in packet: + if message.address == 0x35 and message.src == self.bus and len(message.dat) > 3 and message.dat[3] & 0x40: + return True + time.sleep(.005) + return False + + def prepare(self, *, admission): + if not qualified(self.cp): + raise ValueError('Unqualified EV9 LONG startup profile') + # A READY vehicle retains stock SCC and never receives communication control. + skip = not admission() or self._ready() + super().prepare(admission=(lambda: False) if skip else admission) + if self.outcome in (Outcome.STOCK_UNTOUCHED, Outcome.STOCK_RESTORED): + self.cp = copy_cp(self.stock_cp) + self.prepared_cp = deepcopy(self.cp.to_dict()) + elif self.outcome is Outcome.SENT_UNCONFIRMED: + self.keeper = self.keeper_factory( + lambda: self.callbacks[1]([make_tester_present_msg(self.address, self.bus, suppress_response=True)]), clock=self.clock) + self.keeper.start() + return self.cp + + def check(self): + super().check() + if self.keeper is not None and self.keeper.abort_reason is not None: + self.keeper.stop() + # Main thread alone receives CAN/restores. Never mutate published CP. + if self.disable_attempted and not self.restored: + try: + self._restore() + except Exception: + pass + self.outcome = Outcome.ABORT_UNCERTAIN + raise RuntimeError('EV9 startup keeper aborted; session must stop') + + def sources_current(self, ci, now_ns): + parser = ci.can_parsers[Bus.pt] + for name in ('MDPS', 'ACCELERATOR', 'WHEEL_SPEEDS', 'TCS'): + source = parser.message_states.get(parser.dbc.name_to_msg[name].address) + if source is None or not source.timestamps or source.counter_fail >= MAX_BAD_COUNTER: + return False + stamp = int(source.timestamps[-1]) + if not self.floor_ns < stamp <= now_ns or now_ns - stamp > source.timeout_threshold: + return False + return True + + def before_control(self, *, configured, sources_current, control_current): + self.check() + if self.outcome is not Outcome.SENT_UNCONFIRMED: + return True + if not (configured and sources_current and control_current): + return False + if not self.handed_off: + self.keeper.stop() # No sender lock held; join before controller handoff. + self.check() + self.handed_off = True + return True + + def close(self): + if self.keeper is not None: + self.keeper.stop() + if self.keeper.abort_reason is not None and self.disable_attempted and not self.restored: + try: + self._restore() + except Exception: + pass + super().close() + + def _warm_stock(self, ci): + deadline = self.clock() + 3. + while self.clock() < deadline: + packets = self.callbacks[0]() + stamped = [(int(getattr(packet, 'log_mono_time_ns', 0)), list(packet)) for packet in packets] + if any(stamp <= 0 for stamp, _ in stamped): + raise RuntimeError('EV9 stock warmup requires timestamped CAN') + state = ci.update(stamped) + now = time.clock_gettime_ns(time.CLOCK_BOOTTIME) + parser = ci.can_parsers[Bus.pt] + ready = state.canValid and not state.canTimeout and parser.bus == 1 + for name in ('SCC_CONTROL', 'MDPS', 'ACCELERATOR', 'WHEEL_SPEEDS', 'TCS'): + source = parser.message_states.get(parser.dbc.name_to_msg[name].address) + if source is None or not source.timestamps or source.counter_fail >= MAX_BAD_COUNTER: + ready = False + continue + stamp = int(source.timestamps[-1]) + if not self.floor_ns < stamp <= now or now - stamp > source.timeout_threshold: + ready = False + if ready: + self.ready = True + return + time.sleep(.005) + self.outcome = Outcome.ABORT_UNCERTAIN + raise RuntimeError('EV9 stock sources did not resume before publication') diff --git a/opendbc_repo/opendbc/car/hyundai/ev9_stopping.py b/opendbc_repo/opendbc/car/hyundai/ev9_stopping.py new file mode 100644 index 0000000000..0bdd2c9e51 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/ev9_stopping.py @@ -0,0 +1,73 @@ +"""Original EV9 normal LongControl start/release policy in the modern extension.""" +import numpy as np +from opendbc.car.structs import car +from opendbc.car.hyundai.ev9_longitudinal import qualified +from opendbc.car.hyundai.longitudinal_mode import HKGModePolicy + +LongCtrlState = car.CarControl.Actuators.LongControlState +START_ACCEL = float(np.float32(.2)) +START_SPEED = float(np.float32(.5)) +STOP_SPEED = float(np.float32(.3)) + + +def policy_for(cp, dt): + return EV9StoppingPolicy(dt) if qualified(cp) else None + + +class EV9StoppingPolicy(HKGModePolicy): + stopping_decel_rate = float(np.float32(.4)) + + def __init__(self, dt): + self.init_mode(dt) + self.release_frames = int(round(.35 / dt)) + self.reset() + + def reset(self): + self.release_counter = 0 + + def transition(self, native_state, previous_state, active, CS, a_target, should_stop, *, has_lead): + # Original Toyota stopped-lead hold branch is unreachable for this identity. + if previous_state != LongCtrlState.stopping: + self.release_counter = 0 + ready = True + elif should_stop or CS.brakePressed: + self.release_counter = 0 + ready = False + elif (CS.vEgo > START_SPEED or (has_lead and a_target > .15) or + (a_target >= .45 and not CS.cruiseState.standstill)): + self.release_counter = self.release_frames + ready = True + else: + self.release_counter = min(self.release_counter + 1, self.release_frames) if a_target > .15 else 0 + ready = self.release_counter >= self.release_frames + if not active: + return LongCtrlState.off + if previous_state == LongCtrlState.off: + return LongCtrlState.starting if not (should_stop or CS.brakePressed or CS.cruiseState.standstill) else LongCtrlState.stopping + if previous_state == LongCtrlState.stopping: + return LongCtrlState.starting if not should_stop and not CS.brakePressed and ready else LongCtrlState.stopping + if should_stop: + return LongCtrlState.stopping + return LongCtrlState.pid if CS.vEgo > START_SPEED else previous_state + + def starting_output(self, a_target, accel_limits, context): + # Missing carrier data cannot be interpreted as a request for a launch shove. + if (context.traffic_mode is None or context.custom_acceleration is None or context.has_lead is None or + context.profile_max_accel is None): + output = float(np.clip(a_target, 0., START_ACCEL)) + elif context.traffic_mode or context.custom_acceleration or (context.has_lead and a_target <= .25): + output = float(np.clip(a_target, 0., START_ACCEL)) + elif context.profile_max_accel > 0.: + output = min(START_ACCEL, context.profile_max_accel) + else: + output = START_ACCEL + self.reset() # Original starting branch resets the release hysteresis. + return float(np.clip(output, *accel_limits)) + + def stopping_output(self, output, a_target, should_stop, CS): + follow_min_speed = max(1.5, STOP_SPEED + 1.) + if not should_stop or CS.brakePressed or CS.vEgo <= follow_min_speed or a_target >= output - .25: + return output + step = np.interp(CS.vEgo, [follow_min_speed, 3., 6., 10.], [.02, .03, .05, .07]) + return max(float(a_target), output - float(step)) + diff --git a/opendbc_repo/opendbc/car/hyundai/fingerprints.py b/opendbc_repo/opendbc/car/hyundai/fingerprints.py index ad6d9ed33d..756f7538d3 100644 --- a/opendbc_repo/opendbc/car/hyundai/fingerprints.py +++ b/opendbc_repo/opendbc/car/hyundai/fingerprints.py @@ -9,6 +9,240 @@ Ecu = CarParams.Ecu FW_VERSIONS = { + CAR.HYUNDAI_AZERA_HEV_7TH_GEN: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00GN7HMFC AT KOR LHD 1.00 1.01 99211-N1110 240423', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00GN7_ RDR ----- 1.00 1.00 99110-N1100 ', + ], + }, + CAR.KIA_SPORTAGE_HEV_2026: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00NQ51.011.021.012551000HKP_NQ524_50509099211P1110', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00NQ5__ 1.00 1.04 99110CH100 ', + ], + }, + CAR.HYUNDAI_SANTA_FE_HEV_5TH_GEN: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00MX5HMFC AT CAN LHD 1.00 1.07 99211-P6000 231218', + b'\xf1\x00MX5HMFC AT KOR LHD 1.00 1.07 99211-P6000 231218', + b'\xf1\x00MX5HMFC AT USA LHD 1.00 1.06 99211-R6000 231218', + b'\xf1\x00MX5HMFC AT USA LHD 1.00 1.09 99211-R6200 250519', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00MX5_ RDR ----- 1.00 1.01 99110-P6000 ', + b'\xf1\x00MX5_ RDR ----- 1.00 1.01 99110-R6000 ', + ], + }, + CAR.HYUNDAI_IONIQ_9: { + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00MEev RDR ----- 1.00 1.00 99110-GO000 ', + ], + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00ME MFC AT KOR LHD 1.00 1.00 99211-GO000 241007', + b'\xf1\x00ME MFC AT KOR LHD 1.00 1.01 99211-GO000 250103', + ], + }, + CAR.KIA_SPORTAGE_2026: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00NQ51.011.021.012551000HKP_NQ524_50509099211P1110', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00NQ5__ 1.00 1.04 99110P1100 ', + ], + }, + CAR.KIA_SORENTO_HEV_4TH_GEN_LFA2: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00MQ4HMFC AT USA LHD 1.00 1.00 99210-P2600 250617', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00MQ4_ RDR ----- 1.00 1.01 99110-P2500 ', + ], + }, + CAR.GENESIS_GV70_ELECTRIFIED_2ND_GEN: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00JK MFC AT USA LHD 1.00 1.03 99211-DS600 241125', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00JK__ RDR ----- 1.00 1.01 99110-DS500 ', + ], + }, + CAR.GENESIS_GV70_2026: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00JK MFC AT USA LHD 1.00 1.11 99211-IY600 241125', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00JK__ RDR ----- 1.00 1.01 99110-AR600 ', + ], + }, + CAR.GENESIS_GV80_2025: { + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00JX__ RDR ----- 1.00 1.03 99110-T6500 ', + ], + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00JX MFC AT USA LHD 1.00 1.03 99211-T6510 240124', + ], + }, + + CAR.HYUNDAI_PALISADE_2023: { + (Ecu.eps, 0x7d4, None): [ + b'\xf1\x00ON MDPS C 1.00 1.01 56300-S9500 2922', + b'\xf1\x00LXP MDPS C 1.00 1.00 56310-S8620 4LXPC100', + ], + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00ON MFC AT USA LHD 1.00 1.00 99211-S9170 240531', + b'\xf1\x00ON MFC AT USA LHD 1.00 1.01 99211-S9160 230802', + b'\xf1\x00LX2 MFC AT USA LHD 1.00 1.04 99211-S8150 220622', + b'\xf1\x00LX2 MFC AT RUS LHD 1.00 1.04 99211-S8150 220622', + b'\xf1\x00ON MFC AT USA LHD 1.00 1.01 99211-S9150 220708', + b'\xf1\x00ON MFC AT USA LHD 1.00 1.00 99211-S9160 230303', + b'\xf1\x00LX2 MFC AT USA LHD 1.00 1.01 99211-S8600 230817', + b'\xf1\x00LX2 MFC AT USA LHD 1.00 1.00 99211-S8700 240221', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00ON__ SCC FHCUP 1.00 1.00 99110-S9170 ', + b'\xf1\x00ON__ SCC FHCUP 1.00 1.00 99110-S9160 ', + b'\xf1\x00LX2_ SCC ----- 1.00 1.01 99110-S8150 ', + b'\xf1\x00ON__ SCC ----- 1.00 1.01 99110-S9150 ', + b'\xf1\x00LX2_ SCC FHCUP 1.00 1.01 99110-S8150 ', + b'\xf1\x00ON__ SCC FHCUP 1.00 1.01 99110-S9150 ', + b'\xf1\x00LX2 SCC FHCUP 1.00 1.00 99110-S8600 ', + b'\xf1\x00LX2_ SCC FHCUP 1.00 1.01 99110-S8700 ', + b'\xf1\x00LX2_ SCC F-CUP 1.00 1.01 99110-S8150 ', + ], + }, + CAR.HYUNDAI_IONIQ_5_N: { + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00NE1N RDR ----- 1.00 1.00 99110-NI000 ', + ], + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00NE1NMFC AT KOR LHD 1.00 1.04 99211-NI000 231219', + b'\xf1\x00NE1NMFC AT KOR LHD 1.00 1.00 99211-NI010 240712', + b'\xf1\x00NE1NMFC AT USA LHD 1.00 1.04 99211-NI000 231219', + ], + }, + CAR.HYUNDAI_TUCSON_PHEV_2025: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00NX4 FR_CMR AT CAN LHD 1.00 1.00 99211-N7030 C55', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00NX4__ 1.00 1.02 99110N7100 ', + ], + }, + CAR.KIA_K4_2025: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00CL4 MFC AT CAN LHD 1.00 1.02 99210-GG000 240708', + b'\xf1\x00CL4 MFC AT USA LHD 1.00 1.02 99210-GG000 240708', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00CL4_ RDR ----- 1.00 1.01 99110-GG000 ', + b'\xf1\x00CL4_ RDR ----- 1.00 1.01 99110-GG100 ', + ], + }, + CAR.KIA_SORENTO_2024: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00MQ4 MFC AT AUS RHD 1.01 1.04 99210-P2550 231127', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00MQ4_ RDR ----- 1.00 1.01 99110-P2500 ', + ], + }, + CAR.KIA_CARNIVAL_2025: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00KA4 MFC AT CAN LHD 1.00 1.00 99210-R0700 250324', + b'\xf1\x00KA4 MFC AT USA LHD 1.00 1.05 99210-R0500 240305', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00KA4_ SCC FHCUP 1.00 1.01 99110-R0510 ', + b'\xf1\x00KA4_ RDR ----- 1.00 1.01 99110-R0510 ', + ], + }, + CAR.KIA_CARNIVAL_HEV_4TH_GEN: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00KA4HMFC AT USA LHD 1.00 1.05 99210-R0500 240305', + b'\xf1\x00KA4HMFC AT KOR LHD 1.00 1.00 99210-R0600 240924', + b'\xf1\x00KA4HMFC AT USA LHD 1.00 1.00 99210-R0700 250324', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00KAhe RDR ----- 1.00 1.01 99110-ES500 ', + ], + }, + CAR.HYUNDAI_KONA_2ND_GEN: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00SX2 MFC AT USA LHD 1.00 1.03 99211-BE000 230517', + b'\xf1\x00SX2 MFC AT USA LHD 1.00 1.07 99211-BE000 240611', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00SX2_ RDR ----- 1.00 1.02 99110-BE000 ', + b'\xf1\x00SX2_ RDR ----- 1.00 1.02 99110-BE500 ', + ], + }, + CAR.HYUNDAI_KONA_HEV_2ND_GEN: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00SX2HMFC AT AUS RHD 1.00 1.00 99211-BE001 241015', + b'\xf1\x00SX2HMFC AT EUR RHD 1.00 1.04 99211-BE000 231010', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00SX2_ RDR ----- 1.00 1.02 99110-BE000 ', + b'\xf1\x00SX2_ RDR ----- 1.00 1.02 99110-BE500 ', + ], + }, + CAR.HYUNDAI_SONATA_2024: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00DN8 MFC AT KOR LHD 1.00 1.01 99211-L1800 230512', + b'\xf1\x00DN8 MFC AT USA LHD 1.00 1.01 99211-L1800 230512', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00DN8_ RDR ----- 1.00 1.00 99110-L1800 ', + ], + }, + CAR.HYUNDAI_SONATA_HEV_2024: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00DN8HMFC AT KOR LHD 1.00 1.01 99211-L1800 230512', + b'\xf1\x00DN8HMFC AT USA LHD 1.00 1.01 99211-L1800 230512', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00DN8_ RDR ----- 1.00 1.00 99110-L1800 ', + ], + }, + CAR.HYUNDAI_TUCSON_2025: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.01 99211-N7050 C5A', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00NX4__ 1.00 1.03 99110N7100 ', + ], + }, + CAR.HYUNDAI_TUCSON_HEV_2025: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.00 99211-N7030 C55', + b'\xf1\x00NX4 FR_CMR AT EUR LHD 1.00 1.00 99211-N7030 C55', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00NX4__ 1.00 1.02 99110N7000 ', + b'\xf1\x00NX4__ 1.00 1.02 99110N7100 ', + b'\xf1\x00NX4__ 1.00 1.03 99110N7100 ', + ], + }, + CAR.HYUNDAI_SANTA_CRUZ_2025: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.00 99211-N7030 C55', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00NX4__ 1.00 1.00 99110K5500 ', + ], + }, + CAR.KIA_K5_2025: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00DL3 MFC AT USA LHD 1.00 1.04 99210-L2500 240117', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00DL3_ RDR ----- 1.00 1.01 99110-L2500 ', + ], + }, CAR.HYUNDAI_AZERA_6TH_GEN: { (Ecu.fwdRadar, 0x7d0, None): [ b'\xf1\x00IG__ SCC F-CU- 1.00 1.00 99110-G8100 ', @@ -588,6 +822,17 @@ FW_VERSIONS = { b'\xf1\x00CD ESC \x0b 101 \x10\x03 58910-J7AC0', ], }, + CAR.KIA_XCEED_PHEV: { + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00CDph SCC F-CUP 1.00 1.01 99110-CR100 ', + ], + (Ecu.eps, 0x7d4, None): [ + b'\xf1\x00CDe MDPS C 1.00 1.01 56310-XX000 4CDHC101', + ], + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00CD2 LKAS AT EUR LHD 1.00 1.01 99211-CR010 621', + ], + }, CAR.KIA_CEED_PHEV: { (Ecu.fwdRadar, 0x7d0, None): [ b'\xf1\x00CDph SCC F-CUP 1.00 1.01 99110-CR100 ', @@ -958,6 +1203,21 @@ FW_VERSIONS = { b'\xf1\x00CN ESC \t 105 \x10\x03 58910-AA800', ], }, + CAR.HYUNDAI_ELANTRA_2024: { + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00CN7_ RDR ----- 1.00 1.01 99110-AA500 ', + ], + (Ecu.eps, 0x7d4, None): [ + b'\xf1\x00CN7 MDPS C 1.00 1.02 56300AA670\x00 4CSDC102', + ], + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00CN7 MFC AT USA LHD 1.00 1.02 99210-AA500 230420', + b'\xf1\x00CN7 MFC AT USA LHD 1.00 1.03 99210-AA500 230918', + ], + (Ecu.abs, 0x7d1, None): [ + b'\xf1\x00CN ESC \t 104#\x07\x03 58910-AA850', + ], + }, CAR.HYUNDAI_ELANTRA_HEV_2021: { (Ecu.fwdCamera, 0x7c4, None): [ b'\xf1\x00CN7HMFC AT USA LHD 1.00 1.03 99210-AA000 200819', @@ -977,6 +1237,22 @@ FW_VERSIONS = { b'\xf1\x00CN7 MDPS C 1.00 1.04 56310BY050\x00 4CNHC104', ], }, + CAR.HYUNDAI_ELANTRA_HEV_2024: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00CN7HMFC AT AUS RHD 1.00 1.02 99210-AA500 230420', + b'\xf1\x00CN7HMFC AT CAN LHD 1.00 1.05 99210-AA510 240509', + b'\xf1\x00CN7HMFC AT USA LHD 1.00 1.03 99210-AA500 230918', + b'\xf1\x00CN7HMFC AT USA LHD 1.00 1.05 99210-AA510 240509', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00CN7_ RDR ----- 1.00 1.01 99110-AA500 ', + ], + (Ecu.eps, 0x7d4, None): [ + b'\xf1\x00CN7 MDPS C 1.00 1.00 56300BY670\x00 4CSHC100', + b'\xf1\x00CN7 MDPS C 1.00 1.00 56300BY680\x00 4CSHC100', + b'\xf1\x00CN7 MDPS C 1.00 1.03 56300BY670\x00 4CSHC103', + ], + }, CAR.HYUNDAI_KONA_HEV: { (Ecu.abs, 0x7d1, None): [ b'\xf1\x00OS IEB \x01 104 \x11 58520-CM000', @@ -1083,6 +1359,18 @@ FW_VERSIONS = { b'\xf1\x00NE1 MFC AT USA LHD 1.00 1.06 99211-GI010 230110', ], }, + CAR.HYUNDAI_IONIQ_5_PE: { + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00NE__ RDR ----- 1.00 1.00 99110-PI000 ', + b'\xf1\x00NE__ RDR ----- 1.00 1.01 99110-GI500 ', + ], + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00NE MFC AT USA LHD 1.00 1.01 99211-PI000 240905', + b'\xf1\x00NE MFC AT EUR LHD 1.00 1.03 99211-GI500 240809', + b'\xf1\x00NE MFC AT USA LHD 1.00 1.00 99211-PI010 250407', + b'\xf1\x00NE MFC AT EUR LHD 1.00 1.00 99211-GI510 250513', + ], + }, CAR.HYUNDAI_IONIQ_6: { (Ecu.fwdRadar, 0x7d0, None): [ b'\xf1\x00CE__ RDR ----- 1.00 1.01 99110-KL000 ', @@ -1238,6 +1526,32 @@ FW_VERSIONS = { b'\xf1\x00JX1_ SCC FHCUP 1.00 1.01 99110-T6100 ', ], }, + CAR.KIA_EV6_2025: { + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00CV__ RDR ----- 1.00 1.00 99110-XG500 ', + b'\xf1\x00CV__ RDR ----- 1.00 1.01 99110-CV500 ', + ], + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00CV MFC AT KOR LHD 1.00 1.01 99210-CV500 240405', + b'\xf1\x00CV MFC AT USA LHD 1.00 1.02 99210-XG500 241223', + ], + }, + CAR.KIA_EV9: { + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00MV__ RDR ----- 1.00 1.02 99110-DO000 ', + b'\xf1\x00MV__ RDR ----- 1.00 1.03 99110-DO000 ', + b'\xf1\x00MV__ RDR ----- 1.00 1.04 99110-DO000 ', + b'\xf1\x00MV__ RDR ----- 1.00 1.02 99110-DO700 ', + ], + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00MV MFC AT KOR LHD 1.00 1.01 99211-DO000 230419', + b'\xf1\x00MV MFC AT USA LHD 1.00 1.02 99211-DO000 230616', + b'\xf1\x00MV MFC AT EUR LHD 1.00 1.02 99211-DO000 230616', + b'\xf1\x00MV MFC AT CAN LHD 1.00 1.00 99211-DO100 240403', + b'\xf1\x00MV MFC AT USA LHD 1.00 1.01 99211-XA000 241023', + b'\xf1\x00MV MFC AT CAN LHD 1.00 1.01 99211-DO100 241023', + ], + }, CAR.KIA_CARNIVAL_4TH_GEN: { (Ecu.fwdCamera, 0x7c4, None): [ b'\xf1\x00KA4 MFC AT EUR LHD 1.00 1.06 99210-R0000 220221', @@ -1316,4 +1630,114 @@ FW_VERSIONS = { b'\xf1\x00T01G00BL T01I00A1 DOS2T16X4XI00NS0\x99L\xeeq', ], }, + CAR.GENESIS_G70_2021_NON_SCC: { + (Ecu.eps, 0x7d4, None): [ + b'\xf1\x00IK MDPS R 1.00 1.08 57700-G9200 4I2CL108', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00IK__ SCC --CUP 1.00 1.02 96400-G9100 ', + ], + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00IK MFC MT USA LHD 1.00 1.01 95740-G9000 170920', + ], + }, + CAR.HYUNDAI_BAYON_1ST_GEN_NON_SCC: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00BC3 LKA AT EUR LHD 1.00 1.01 99211-Q0100 261', + ], + }, + CAR.HYUNDAI_ELANTRA_2022_NON_SCC: { + (Ecu.eps, 0x7d4, None): [ + b'\xf1\x00CN7 MDPS R 1.00 1.04 57700-IB000 4CNNP104', + ], + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00CN7 MFC AT USA LHD 1.00 1.01 99210-AB000 210205', + b'\xf1\x00CN7 MFC AT USA LHD 1.00 1.00 99210-IB000 210531', + ], + (Ecu.abs, 0x7d1, None): [ + b'\xf1\x00CN ESC \t 100!\x05\x01 58910-IB000', + ], + (Ecu.transmission, 0x7e1, None): [ + b'\xf1\x00T02601BL T02900A1 WCN7T20XXX900NS4\xf7\xccz\xf6', + ], + }, + CAR.HYUNDAI_ELANTRA_HEV_2022_NON_SCC: { + (Ecu.eps, 0x7d4, None): [ + b'\xf1\x00CN7 MDPS C 1.00 1.02 56310/BY050 4CNHC102', + ], + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00CN7HMFC AT USA LHD 1.00 1.04 99210-AA000 210205', + ], + (Ecu.transmission, 0x7e1, None): [ + b'\xf1\x006U3L0_C2\x00\x006U3K3051\x00\x00HCN0G16NS0\x00\x00\x00\x00', + ], + }, + CAR.HYUNDAI_KONA_NON_SCC: { + (Ecu.eps, 0x7d4, None): [ + b'\xf1\x00OS MDPS C 1.00 1.05 56310J9030\x00 4OSDC105', + b'\xf1\x00OS MDPS C 1.00 1.04 56310J9030\x00 4OSDC104', + b'\xf1\x00OS MDPS C 1.00 1.05 56310/J9500 4OSDC105', + ], + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00OS9 LKAS AT USA LHD 1.00 1.00 95740-J9200 g30', + b'\xf1\x00OS9 LKAS AT AUS RHD 1.00 1.00 95740-J9200 g30', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00OS__ FCA --CUP 1.00 1.00 95655-J9100 ', + ], + (Ecu.transmission, 0x7e1, None): [ + b'\xf1\x006T6J0_C2\x00\x006T6K1051\x00\x00TOS4N20NS2\x00\x00\x00\x00', + b'\xf1\x006U2V0_C2\x00\x006U2V1051\x00\x00DOS4T16AS2\x00\x00\x00\x00', + ], + }, + CAR.KIA_CEED_PHEV_2022_NON_SCC: { + (Ecu.eps, 0x7D4, None): [ + b'\xf1\x00CD MDPS C 1.00 1.01 56310-XX000 4CPHC101', + ], + (Ecu.fwdCamera, 0x7C4, None): [ + b'\xf1\x00CDH LKAS AT EUR LHD 1.00 1.01 99211-CR700 931', + ], + }, + CAR.KIA_FORTE_2019_NON_SCC: { + (Ecu.eps, 0x7D4, None): [ + b'\xf1\x00BD MDPS C 1.00 1.04 56310/M6000 4BDDC104', + b'\xf1\x00BD MDPS C 1.00 1.05 56310/M6000 4BDDC105', + ], + (Ecu.fwdCamera, 0x7C4, None): [ + b'\xf1\x00BD LKAS AT USA LHD 1.00 1.02 95740-M6000 J31', + ], + }, + CAR.KIA_FORTE_2021_NON_SCC: { + (Ecu.eps, 0x7D4, None): [ + b'\xf1\x00BD MDPS C 1.00 1.07 56310/M6300 4BDDC107', + b'\xf1\x00BD MDPS C 1.00 1.08 56310M6000\x00 4BDDC108', + ], + (Ecu.fwdCamera, 0x7C4, None): [ + b'\xf1\x00BD LKAS AT USA LHD 1.00 1.02 95740-M6000 J31', + b'\xf1\x00BD LKAS AT USA LHD 1.00 1.04 95740-M6000 J33', + ], + }, + CAR.KIA_SELTOS_2023_NON_SCC: { + (Ecu.abs, 0x7d1, None): [ + b'\xf1\x00SP ESC \t 101\"\t\x01 58910-Q5510', + b'\xf1\x00SP ESC \r 100\"\x04\x01 58910-Q5510', + ], + (Ecu.eps, 0x7d4, None): [ + b'\xf1\x00SP2 MDPS C 1.00 1.04 56310Q5240 4SPSC104', + b'\xf1\x00SP2 MDPS C 1.00 1.01 56300Q5920 ', + ], + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00SP2 MFC AT USA LHD 1.00 1.03 99210-Q5500 230208', + b'\xf1\x00SP2 MFC AT AUS RHD 1.00 1.02 99210-Q5500 220624', + ], + (Ecu.transmission, 0x7e1, None): [ + b'\xf1\x006V2B0_C2\x00\x006V2D5051\x00\x00CSP2N20NL0\x00\x00\x00\x00', + b'\xf1\x006V2B0_C2\x00\x006V2D4051\x00\x00CSP2N20KL1\x00\x00\x00\x00', + ], + }, + CAR.KIA_RAY_EV: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00TAM MFC AT KOR LHD 1.00 1.02 99211-E2000 230901', + ], + }, } diff --git a/opendbc_repo/opendbc/car/hyundai/forte_aol.py b/opendbc_repo/opendbc/car/hyundai/forte_aol.py new file mode 100644 index 0000000000..5fc35e0f54 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/forte_aol.py @@ -0,0 +1,26 @@ +"""Exact Forte non-SCC AOL identity and physical-button observations.""" +from opendbc.car.structs import CarParams +from opendbc.car.hyundai.values import CAR, HyundaiFlags + +FORTE_IDS = (CAR.KIA_FORTE_2019_NON_SCC, CAR.KIA_FORTE_2021_NON_SCC) +FORTE_AOL_MARKER = 0x0400 +FORTE_AOL_EXPERIENCE = 32 +FORTE_AOL_WORDS = frozenset((0x1400, 0x1c00)) +SOURCE_MAX_AGE_NS = 300_000_000 + + +def qualified(cp): + physical = bool(cp.flags & HyundaiFlags.HAS_LDA_BUTTON) + words = (0x1000, 0x1800, 0x1c00) if physical else (0x1000, 0x1400) + allowed_flags = (HyundaiFlags.NON_SCC | HyundaiFlags.NON_SCC_NO_FCA | HyundaiFlags.NON_SCC_RADAR_FCA | + HyundaiFlags.HAS_LDA_BUTTON | HyundaiFlags.USE_FCA | HyundaiFlags.SEND_LFA) + return (cp.carFingerprint in FORTE_IDS and cp.brand == 'hyundai' and cp.alternativeExperience in (0, FORTE_AOL_EXPERIENCE) and + not cp.passive and not cp.dashcamOnly and + not cp.notCar and not cp.openpilotLongitudinalControl and cp.pcmCruise and + bool(cp.flags & HyundaiFlags.NON_SCC) and not int(cp.flags) & ~int(allowed_flags) and + len(cp.safetyConfigs) == 1 and cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.hyundai and + cp.safetyConfigs[0].safetyParam in words) + + +# Keep the public Forte fixture/provider API while sharing the same state machine. +from opendbc.car.hyundai.non_scc_aol import NonSccLkasSources as ForteLkasSources diff --git a/opendbc_repo/opendbc/car/hyundai/g90_lead.py b/opendbc_repo/opendbc/car/hyundai/g90_lead.py new file mode 100644 index 0000000000..3495a62c06 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/g90_lead.py @@ -0,0 +1,106 @@ +"""Original G90 CAN lead hysteresis, fed by a scoped fresh radar provider.""" +import math +from dataclasses import dataclass + + +@dataclass(frozen=True) +class CanLeadData: + object_gap: int = 0 + lead_distance: float = 0.0 + lead_rel_speed: float = 0.0 + lead_visible: bool = False + + @property + def object_rel_gap(self) -> int: + return 0 if self.lead_distance == 0 else 2 if self.lead_rel_speed < -0.2 else 1 + + +def _hysteresis_update(current, new_value, counter, threshold): + if new_value == current: + return current, 0 + + counter += 1 + return (new_value, 0) if counter >= threshold else (current, counter) + + +class CanLeadDataState: + LEAD_HYSTERESIS_FRAMES = 50 + + def __init__(self): + self._lead_on_counter = 0 + self._lead_off_counter = 0 + self._gap_counter = 0 + self._lead_visible = False + self._object_gap = 0 + + @staticmethod + def _get_object_gap(lead_distance: float) -> int: + if lead_distance == 0: + return 0 + if lead_distance < 20: + return 2 + if lead_distance < 25: + return 3 + if lead_distance < 30: + return 4 + return 5 + + def update(self, lead_distance: float, lead_rel_speed: float, lead_visible: bool) -> CanLeadData: + counter = self._lead_on_counter if lead_visible else self._lead_off_counter + self._lead_visible, counter = _hysteresis_update( + self._lead_visible, lead_visible, counter, self.LEAD_HYSTERESIS_FRAMES, + ) + if lead_visible: + self._lead_on_counter = counter + self._lead_off_counter = 0 + else: + self._lead_off_counter = counter + self._lead_on_counter = 0 + + object_gap = self._get_object_gap(lead_distance) + self._object_gap, self._gap_counter = _hysteresis_update( + self._object_gap, object_gap, self._gap_counter, self.LEAD_HYSTERESIS_FRAMES, + ) + + return CanLeadData(self._object_gap, lead_distance, lead_rel_speed, self._lead_visible) + + +@dataclass(frozen=True) +class LeadObservation: + drive_id: int + producer_ns: int + receipt_ns: int + observed_ns: int + present: bool + distance_m: float + relative_speed_mps: float + + +def eligible(cp): + from opendbc.car.hyundai.values import CAR + return (cp.carFingerprint == CAR.GENESIS_G90 and cp.openpilotLongitudinalControl and + not cp.passive and not cp.dashcamOnly and not cp.notCar) + + +class G90LeadState: + def __init__(self): + self.state = CanLeadDataState() + self.drive_id = None + + def update(self, observation, hud_visible): + if observation is None: + self.state = CanLeadDataState() + self.drive_id = None + return CanLeadData() + if self.drive_id != observation.drive_id: + self.state = CanLeadDataState() + self.drive_id = observation.drive_id + visible = observation.present or bool(hud_visible) + distance = min(max(observation.distance_m, 0.), 204.7) + relative = min(max(observation.relative_speed_mps, -170.), 239.5) + if not math.isfinite(distance) or not math.isfinite(relative): + self.state = CanLeadDataState() + return CanLeadData() + if visible and distance <= .1: + distance, relative = 20., 0. + return self.state.update(distance, relative, visible) diff --git a/opendbc_repo/opendbc/car/hyundai/g90_longitudinal.py b/opendbc_repo/opendbc/car/hyundai/g90_longitudinal.py new file mode 100644 index 0000000000..e4b29563a8 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/g90_longitudinal.py @@ -0,0 +1,67 @@ +import numpy as np + +from opendbc.car.structs import CarControl +from opendbc.car.hyundai.values import CAR + +LongCtrlState = CarControl.Actuators.LongControlState + +HOLD_SPEED = (0.0, 0.03, 0.08, 0.16, 0.3, 0.5, 0.8, 1.2, 2.0, 3.0) +HOLD_ACCEL = (-0.10, -0.10, -0.12, -0.18, -0.30, -0.50, -0.75, -1.00, -1.40, -1.80) +RELAX_SPEED = (0.0, 0.08, 0.16, 0.3, 0.5, 0.8, 1.2, 2.0, 3.0) +RELAX_STEP = (0.10, 0.10, 0.08, 0.06, 0.04, 0.035, 0.03, 0.022, 0.018) +RELEASE_SPEED = (0.0, 0.3, 0.6) +RELEASE_ACCEL_STEP = (0.05, 0.07, 0.11) +RELEASE_DECEL_STEP = (0.16, 0.18, 0.18) +RELEASE_MAX_SPEED = 0.8 + + +def stopping_decel_rate(cp): + return 0.55 if cp.carFingerprint == CAR.GENESIS_G90 and cp.openpilotLongitudinalControl else None + + +def forecast_should_stop(cp, speeds, time_indices, action_t): + """G90's original two-horizon stopping decision; no acceleration shaping.""" + if cp.carFingerprint != CAR.GENESIS_G90 or not cp.openpilotLongitudinalControl: + return None + if len(speeds) == len(time_indices): + target = np.interp(action_t, time_indices, speeds) + target_one_second = np.interp(action_t + 1.0, time_indices, speeds) + else: + target = target_one_second = 0.0 + # The original CarParams field was Float32 before its planner toggle copy. + threshold = float(np.float32(0.8)) + return bool(target < threshold and target_one_second < threshold) + + +class G90LongitudinalPolicy: + """G90 stop hold and launch shaping at the controller's 100 Hz cadence.""" + + def __init__(self): + self.actual_accel = 0.0 + self.release_active = False + self.last_state = LongCtrlState.off + + def update(self, accel, speed, state, active): + if not active: + self.actual_accel = 0.0 + self.release_active = False + elif state == LongCtrlState.stopping and speed <= HOLD_SPEED[-1]: + self.release_active = False + target = min(0.0, max(accel, float(np.interp(speed, HOLD_SPEED, HOLD_ACCEL)))) + if self.actual_accel < target: + self.actual_accel = min(self.actual_accel + float(np.interp(speed, RELAX_SPEED, RELAX_STEP)), target) + else: + self.actual_accel = target + else: + if self.last_state == LongCtrlState.stopping and state == LongCtrlState.pid and accel > 0.0 and speed < RELEASE_MAX_SPEED: + self.release_active = True + if self.release_active: + accel_step = float(np.interp(speed, RELEASE_SPEED, RELEASE_ACCEL_STEP)) + decel_step = float(np.interp(speed, RELEASE_SPEED, RELEASE_DECEL_STEP)) + self.actual_accel = float(np.clip(accel, self.actual_accel - decel_step, self.actual_accel + accel_step)) + if speed >= RELEASE_MAX_SPEED or accel <= 0.0 or self.actual_accel >= accel - 1e-3: + self.release_active = False + else: + self.actual_accel = accel + self.last_state = state + return self.actual_accel diff --git a/opendbc_repo/opendbc/car/hyundai/g90_startup.py b/opendbc_repo/opendbc/car/hyundai/g90_startup.py new file mode 100644 index 0000000000..6a5dac2831 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/g90_startup.py @@ -0,0 +1,78 @@ +"""Exact G90 prepublication failure fallback; sent disable is not confirmed ownership.""" +from copy import deepcopy +import time + +from opendbc.car import Bus +from opendbc.car.disable_ecu import disable_ecu +from opendbc.car.hyundai.values import CAR, HyundaiFlags +from opendbc.car.isotp_parallel_query import IsoTpParallelQuery +from opendbc.can.parser import MAX_BAD_COUNTER +from opendbc.car.hyundai.ecu_startup import HyundaiECUStartup, Outcome, stock_copy as stock_copy + + +def required(cp): + return (cp.carFingerprint == CAR.GENESIS_G90 and cp.openpilotLongitudinalControl and + not cp.passive and not cp.dashcamOnly and + not cp.flags & (HyundaiFlags.CANFD | HyundaiFlags.CAMERA_SCC | HyundaiFlags.CANFD_CAMERA_SCC)) + + +class G90Startup(HyundaiECUStartup): + def __init__(self, cp, callbacks): + super().__init__(cp, callbacks, address=0x7d0, bus=0, label='G90') + + def _disable(self, *args, **kwargs): + return disable_ecu(*args, **kwargs) + + def _query(self, *args, **kwargs): + return IsoTpParallelQuery(*args, **kwargs) + + def finalize_aol_configuration(self, ci): + if self.published or self.closed or ci is not self.ci or not self.ready: + raise RuntimeError('G90 AOL finalization requires configured startup') + if self.prepared_for(ci.CP): + return + from opendbc.car.hyundai.classic_long_aol import qualified as long_qualified, ordinary_word, aol_word as long_word + from opendbc.car.hyundai.classic_scc_aol import qualified as stock_qualified, stock_word, aol_word as stock_aol_word + if self.outcome is Outcome.SENT_UNCONFIRMED and long_qualified(ci.CP, marked_only=True): + before_word, after_word = ordinary_word(ci.CP), long_word(ci.CP) + elif self.outcome in (Outcome.STOCK_UNTOUCHED, Outcome.STOCK_RESTORED) and stock_qualified(ci.CP, marked_only=True): + before_word, after_word = stock_word(ci.CP), stock_aol_word(ci.CP) + else: + raise RuntimeError('G90 startup does not admit this AOL configuration') + if ci.CP.carFingerprint != CAR.GENESIS_G90 or ci.CP.alternativeExperience != 32: + raise RuntimeError('G90 AOL configuration identity does not match') + expected = deepcopy(self.prepared_cp) + configs = expected.get('safetyConfigs', []) + if len(configs) != 1 or configs[0]['safetyModel'] != 'hyundai' or configs[0]['safetyParam'] != before_word or expected['alternativeExperience'] != 0: + raise RuntimeError('G90 prepared safety profile cannot be finalized') + configs[0]['safetyParam'] = after_word + expected['alternativeExperience'] = 32 + if ci.CP.to_dict() != expected: + raise RuntimeError('G90 AOL finalization changed unrelated CarParams') + self.prepared_cp = expected + + def _warm_stock(self, ci): + deadline = time.monotonic() + 3. + while time.monotonic() < deadline: + packets = self.callbacks[0]() + stamped = [(int(getattr(packet, 'log_mono_time_ns', 0)), list(packet)) for packet in packets] + if any(stamp <= 0 for stamp, _ in stamped): + raise RuntimeError(f'{self.label} stock warmup requires actual timestamped CAN') + state = ci.update(stamped) + now = time.clock_gettime_ns(time.CLOCK_BOOTTIME) + parser = ci.can_parsers[Bus.pt] + ready = state.canValid and not state.canTimeout and parser.bus == 0 + for name in ('SCC11', 'SCC12'): + source = parser.message_states.get(parser.dbc.name_to_msg[name].address) + if source is None or not source.timestamps or source.counter_fail >= MAX_BAD_COUNTER: + ready = False + continue + stamp = int(source.timestamps[-1]) + if not self.floor_ns < stamp <= now or now - stamp > source.timeout_threshold: + ready = False + if ready: + self.ready = True + return + time.sleep(.005) + self.outcome = Outcome.ABORT_UNCERTAIN + raise RuntimeError(f'{self.label} stock SCC sources did not become fresh before publication') diff --git a/opendbc_repo/opendbc/car/hyundai/gv70_camera_lead.py b/opendbc_repo/opendbc/car/hyundai/gv70_camera_lead.py new file mode 100644 index 0000000000..3b196a676e --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/gv70_camera_lead.py @@ -0,0 +1,18 @@ +"""Exact GV70 owner for the optional camera lead decoder.""" +from opendbc.car.hyundai.canfd_camera_lead import CANFDCameraLead +from opendbc.car.hyundai.hyundaicanfd import CanBus +from opendbc.car.hyundai.values import CAR, HyundaiFlags + + +def eligible(cp): + return (cp.carFingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN and + cp.flags & HyundaiFlags.CANFD and cp.openpilotLongitudinalControl and + not cp.passive and not cp.dashcamOnly and not cp.notCar) + + +class GV70CameraLead(CANFDCameraLead): + def __init__(self, cp): + if not eligible(cp): + raise ValueError('Camera lead owner requires the exact active GV70 profile') + buses = CanBus(cp) + super().__init__(cp, buses.ECAN if cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG else buses.CAM) diff --git a/opendbc_repo/opendbc/car/hyundai/gv70_costs.py b/opendbc_repo/opendbc/car/hyundai/gv70_costs.py new file mode 100644 index 0000000000..2f12a12487 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/gv70_costs.py @@ -0,0 +1,72 @@ +"""Electrified GV70's absolute planner cost projection. + +Input context belongs to the current-drive planner owner. This module does not +change the solver recipe, actuator limits, or other vehicles' default costs. +""" +from dataclasses import dataclass +import math + +from opendbc.car.hyundai.values import CAR + + +@dataclass(frozen=True) +class CostContext: + speed_mps: float + lead_distance_m: float + uncertainty: float + mode: str + acceleration_factor: float + speed_factor: float + danger_factor: float + stop_approach: bool + tracked_lead: bool + previous_accel_constraint: bool + + +@dataclass(frozen=True) +class CostWeights: + stage: tuple[float, ...] + constraints: tuple[float, ...] + + +def eligible(cp): + return (cp.carFingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN and + cp.openpilotLongitudinalControl and not cp.passive and + not cp.dashcamOnly and not cp.notCar) + + +def _interpolate(speed_mph, values): + points = (0., 35., 55., 70.) + if speed_mph <= points[0]: + return values[0] + for i in range(1, len(points)): + if speed_mph <= points[i]: + fraction = (speed_mph - points[i - 1]) / (points[i] - points[i - 1]) + return values[i - 1] + fraction * (values[i] - values[i - 1]) + return values[-1] + + +def project(context): + numeric = (context.speed_mps, context.lead_distance_m, context.uncertainty, + context.acceleration_factor, context.speed_factor, context.danger_factor) + if not all(type(v) in (int, float) and math.isfinite(v) for v in numeric): + raise ValueError('invalid GV70 cost context') + if context.speed_mps < 0 or min(context.acceleration_factor, context.speed_factor, context.danger_factor) <= 0: + raise ValueError('invalid GV70 cost context') + mph = context.speed_mps * 3.6 / 1.609344 + distance_factor = 1. + _interpolate(mph, (0., .06, .06, .05)) * (20. / max(context.lead_distance_m, 5.)) + # Original publisher gives stop/approach priority over tracked following. + scale = .20 if context.stop_approach else 1.75 if context.tracked_lead else 1. + acceleration = 250. * context.acceleration_factor * scale * distance_factor + speed = 5.5 * context.speed_factor * distance_factor + danger = 100. * context.danger_factor * distance_factor + if .45 <= context.uncertainty < .60: + speed *= 1.2 + (context.uncertainty - .45) / .15 * .3 + if context.mode == 'acc': + stage = (_interpolate(mph, (3., 3., 2.5, 2.)), 0., 0., 0., + acceleration if context.previous_accel_constraint else 0., speed) + elif context.mode == 'blended': + stage = (0., .1, .2, 5., 40. if context.previous_accel_constraint else 0., 1.) + else: + raise ValueError('unsupported GV70 planner mode') + return CostWeights(stage, (1e6, 1e6, 1e6, danger)) diff --git a/opendbc_repo/opendbc/car/hyundai/gv70_longitudinal.py b/opendbc_repo/opendbc/car/hyundai/gv70_longitudinal.py new file mode 100644 index 0000000000..5194e8b380 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/gv70_longitudinal.py @@ -0,0 +1,41 @@ +"""First-generation Electrified GV70 SCC demand and stock-cancel policy.""" +from dataclasses import dataclass +import numpy as np + +from opendbc.car.hyundai.values import CAR + +JERK_UPPER = 1.5 +JERK_LOWER = 2.0 +URGENT_JERK_LOWER = 5.0 +URGENT_ACCEL = -1.0 +SCC_FREQUENCY = 50.0 + + +def is_gv70(cp): + return cp.carFingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN + + +@dataclass(frozen=True) +class SCCRequest: + raw_accel: float + accel: float + jerk_upper: float + jerk_lower: float + + +def scc_request(enabled, gas_override, stopping, accel, accel_last): + lower = URGENT_JERK_LOWER if stopping or accel <= URGENT_ACCEL else JERK_LOWER + shaped = float(np.clip(accel, accel_last - lower / SCC_FREQUENCY, accel_last + JERK_UPPER / SCC_FREQUENCY)) \ + if enabled and not gas_override else 0.0 + return SCCRequest(accel, shaped, JERK_UPPER, lower) + + +def suppress_stock_cancel(cp, brake_pressed, lat_active, *, stock_fallback=False): + return bool(is_gv70(cp) and not cp.openpilotLongitudinalControl and + (stock_fallback or (brake_pressed and lat_active))) + + +def tracked_lead_scale(cp): + eligible = (cp.carFingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN and cp.openpilotLongitudinalControl and + not cp.passive and not cp.dashcamOnly and not cp.notCar) + return 1.75 if eligible else 1.0 diff --git a/opendbc_repo/opendbc/car/hyundai/gv70_startup.py b/opendbc_repo/opendbc/car/hyundai/gv70_startup.py new file mode 100644 index 0000000000..597172afa9 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/gv70_startup.py @@ -0,0 +1,24 @@ +"""Exact first-generation GV70 Electrified captured-ADRV startup.""" +from opendbc.car.hyundai.captured_adrv_startup import CapturedADRVStartup, first_generation +from opendbc.car.hyundai.gv70_template import GV70Template +from opendbc.car.hyundai.ecu_startup import Outcome +from opendbc.car.hyundai.values import CAR + + +def eligible(cp): + return cp.carFingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN and first_generation(cp) + + +def required(cp): + return eligible(cp) and cp.openpilotLongitudinalControl + + +class GV70Startup(CapturedADRVStartup): + def __init__(self, cp, callbacks): + super().__init__(cp, callbacks, label='GV70', template_type=GV70Template) + + def configure(self, ci): + super().configure(ci) + # This owner exists only for a requested exact-model takeover. The original + # failed-disable controller avoided fighting stock cruise with Cancel. + ci.CC.gv70_stock_fallback = self.outcome is not Outcome.SENT_UNCONFIRMED diff --git a/opendbc_repo/opendbc/car/hyundai/gv70_template.py b/opendbc_repo/opendbc/car/hyundai/gv70_template.py new file mode 100644 index 0000000000..4e5fa6f547 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/gv70_template.py @@ -0,0 +1,16 @@ +"""Exact first-generation Electrified GV70 captured ADRV speed recipe.""" +from dataclasses import dataclass +import math + +from opendbc.car.can_definitions import CanData +from opendbc.car.hyundai.captured_adrv import CapturedADRVTemplate + + +@dataclass(frozen=True) +class GV70Template(CapturedADRVTemplate): + def frame(self, frame: int, drive_gear: bool, bus: int = 0, *, speed: float | None = None) -> CanData: + data = self._frame_data(frame, drive_gear, bus) + if speed is not None and math.isfinite(speed): + raw = min(max(round(speed * 100.0), 0), 65534) + data[8:10] = raw.to_bytes(2, 'little') + return self._finish(data, bus) diff --git a/opendbc_repo/opendbc/car/hyundai/hyundaican.py b/opendbc_repo/opendbc/car/hyundai/hyundaican.py index e325b32306..c6001e3b31 100644 --- a/opendbc_repo/opendbc/car/hyundai/hyundaican.py +++ b/opendbc_repo/opendbc/car/hyundai/hyundaican.py @@ -1,5 +1,7 @@ +from opendbc.car.hyundai.hyundaicanfd import CanBus +from opendbc.car.hyundai.g90_lead import eligible as g90_lead_eligible from opendbc.car.crc import CRC8J1850, mk_crc8_fun -from opendbc.car.hyundai.values import CAR, HyundaiFlags +from opendbc.car.hyundai.values import is_blended, CAR, HyundaiFlags hyundai_checksum = mk_crc8_fun(CRC8J1850, init_crc=0xFD, xor_out=0xDF) @@ -7,7 +9,7 @@ hyundai_checksum = mk_crc8_fun(CRC8J1850, init_crc=0xFD, xor_out=0xDF) def create_lkas11(packer, frame, CP, apply_torque, steer_req, torque_fault, lkas11, sys_warning, sys_state, enabled, left_lane, right_lane, - left_lane_depart, right_lane_depart): + left_lane_depart, right_lane_depart, ray_lka_icon=None): values = {s: lkas11[s] for s in [ "CF_Lkas_LdwsActivemode", "CF_Lkas_LdwsSysState", @@ -38,8 +40,9 @@ def create_lkas11(packer, frame, CP, apply_torque, steer_req, CAR.HYUNDAI_IONIQ_EV_2020, CAR.HYUNDAI_IONIQ_PHEV, CAR.KIA_SELTOS, CAR.HYUNDAI_ELANTRA_2021, CAR.GENESIS_G70_2020, CAR.HYUNDAI_ELANTRA_HEV_2021, CAR.HYUNDAI_SONATA_HYBRID, CAR.HYUNDAI_KONA_EV, CAR.HYUNDAI_KONA_HEV, CAR.HYUNDAI_KONA_EV_2022, CAR.HYUNDAI_SANTA_FE_2022, CAR.KIA_K5_2021, CAR.HYUNDAI_IONIQ_HEV_2022, CAR.HYUNDAI_SANTA_FE_HEV_2022, - CAR.HYUNDAI_SANTA_FE_PHEV_2022, CAR.KIA_STINGER_2022, CAR.KIA_K5_HEV_2020, CAR.KIA_CEED, CAR.KIA_CEED_PHEV, - CAR.HYUNDAI_AZERA_6TH_GEN, CAR.HYUNDAI_AZERA_HEV_6TH_GEN, CAR.HYUNDAI_CUSTIN_1ST_GEN, CAR.HYUNDAI_KONA_2022): + CAR.HYUNDAI_SANTA_FE_PHEV_2022, CAR.KIA_STINGER_2022, CAR.KIA_K5_HEV_2020, CAR.KIA_CEED, CAR.KIA_XCEED_PHEV, CAR.KIA_CEED_PHEV, + CAR.HYUNDAI_AZERA_6TH_GEN, CAR.HYUNDAI_AZERA_HEV_6TH_GEN, CAR.HYUNDAI_CUSTIN_1ST_GEN, CAR.HYUNDAI_KONA_2022, + CAR.HYUNDAI_ELANTRA_2024, CAR.HYUNDAI_ELANTRA_HEV_2024, CAR.KIA_RAY_EV): values["CF_Lkas_LdwsActivemode"] = int(left_lane) + (int(right_lane) << 1) values["CF_Lkas_LdwsOpt_USM"] = 2 @@ -50,6 +53,8 @@ def create_lkas11(packer, frame, CP, apply_torque, steer_req, # FcwOpt_USM 1 = White car + lanes # FcwOpt_USM 0 = No car + lanes values["CF_Lkas_FcwOpt_USM"] = 2 if enabled else 1 + if CP.carFingerprint == CAR.KIA_RAY_EV and ray_lka_icon is not None: + values["CF_Lkas_FcwOpt_USM"] = ray_lka_icon # SysWarning 4 = keep hands on wheel # SysWarning 5 = keep hands on wheel (red) @@ -58,7 +63,7 @@ def create_lkas11(packer, frame, CP, apply_torque, steer_req, values["CF_Lkas_SysWarning"] = 4 if sys_warning else 0 # Likely cars lacking the ability to show individual lane lines in the dash - elif CP.carFingerprint in (CAR.KIA_OPTIMA_G4, CAR.KIA_OPTIMA_G4_FL): + elif CP.carFingerprint in (CAR.KIA_OPTIMA_G4, CAR.KIA_OPTIMA_G4_FL, CAR.HYUNDAI_KONA_NON_SCC): # SysWarning 4 = keep hands on wheel + beep values["CF_Lkas_SysWarning"] = 4 if sys_warning else 0 @@ -66,7 +71,7 @@ def create_lkas11(packer, frame, CP, apply_torque, steer_req, # SysState 1-2 = white car + lanes # SysState 3 = green car + lanes, green steering wheel # SysState 4 = green car + lanes - values["CF_Lkas_LdwsSysState"] = 3 if enabled else 1 + values["CF_Lkas_LdwsSysState"] = sys_state if CP.carFingerprint == CAR.HYUNDAI_KONA_NON_SCC else 3 if enabled else 1 values["CF_Lkas_LdwsOpt_USM"] = 2 # non-2 changes above SysState definition # these have no effect @@ -114,7 +119,7 @@ def create_clu11(packer, frame, clu11, button, CP): values["CF_Clu_CruiseSwState"] = button values["CF_Clu_AliveCnt1"] = frame % 0x10 # send buttons to camera on camera-scc based cars - bus = 2 if CP.flags & HyundaiFlags.CAMERA_SCC else 0 + bus = CanBus(CP).ECAN if is_blended(CP) else 2 if CP.flags & HyundaiFlags.CAMERA_SCC else 0 return packer.make_can_msg("CLU11", bus, values) @@ -125,8 +130,10 @@ def create_lfahda_mfc(packer, enabled): return packer.make_can_msg("LFAHDA_MFC", 0, values) -def create_acc_commands(packer, enabled, accel, upper_jerk, idx, hud_control, set_speed, stopping, long_override, use_fca, CP): +def create_acc_commands(packer, enabled, accel, upper_jerk, idx, hud_control, set_speed, stopping, long_override, use_fca, CP, *, lead_data=None): commands = [] + if lead_data is not None and not g90_lead_eligible(CP): + lead_data = None scc11_values = { "MainMode_ACC": 1, @@ -139,6 +146,9 @@ def create_acc_commands(packer, enabled, accel, upper_jerk, idx, hud_control, se "ACC_ObjRelSpd": 0, "ACC_ObjDist": 1, # close lead makes controls tighter } + if lead_data is not None: + scc11_values.update(ObjValid=int(lead_data.lead_visible), ACC_ObjStatus=int(lead_data.lead_visible), + ACC_ObjRelSpd=lead_data.lead_rel_speed, ACC_ObjDist=int(lead_data.lead_distance)) commands.append(packer.make_can_msg("SCC11", 0, scc11_values)) scc12_values = { @@ -168,6 +178,8 @@ def create_acc_commands(packer, enabled, accel, upper_jerk, idx, hud_control, se "ACCMode": 2 if enabled and long_override else 1 if enabled else 4, # stock will always be 4 instead of 0 after first disengage "ObjGap": 2 if hud_control.leadVisible else 0, # 5: >30, m, 4: 25-30 m, 3: 20-25 m, 2: < 20 m, 0: no lead } + if lead_data is not None: + scc14_values.update(ObjGap=lead_data.object_gap, ObjDistStat=lead_data.object_rel_gap) commands.append(packer.make_can_msg("SCC14", 0, scc14_values)) # Only send FCA11 on cars where it exists on the bus @@ -215,3 +227,67 @@ def create_frt_radar_opt(packer): "CF_FCA_Equip_Front_Radar": 1, } return packer.make_can_msg("FRT_RADAR11", 0, frt_radar11_values) + + +def create_ray_lfahda_mfc(packer, lat_active, lfa_icon): + values = { + "HDA_USM": 2, + "HDA_Icon_State": 2 if lfa_icon else 0, + "HDA_VSetReq": 0, + "HDA_Icon_Wheel": int(lat_active), + "LFA_Icon_State": lfa_icon, + } + return packer.make_can_msg("LFAHDA_MFC", 0, values) + + +def create_checksum_can_canfd_blended(packer, bus, addr, values): + dat = packer.make_can_msg(addr, bus, values)[1] + return hyundai_checksum(dat[1:8]) + +def create_lkas11_can_canfd_blended(packer, frame, CP, apply_steer, steer_req, + torque_fault, lkas11, sys_warning, sys_state, enabled, + left_lane, right_lane, + left_lane_depart, right_lane_depart, msg_364, + include_alerts=True, counter_mod=0x10, fcw_opt_usm=None): + bus = CanBus(CP).ECAN + values = { + "CF_Lkas_LdwsActivemode": int(left_lane) + (int(right_lane) << 1), + "CF_Lkas_LdwsLHWarning": left_lane_depart, + "CF_Lkas_LdwsRHWarning": right_lane_depart, + "CF_Lkas_FcwOpt_USM": (2 if enabled else 1) if fcw_opt_usm is None else fcw_opt_usm, + "CR_Lkas_StrToqReq": apply_steer, + "CF_Lkas_ActToi": steer_req, + "CF_Lkas_ToiFlt": torque_fault, + "CF_Lkas_MsgCount": frame % counter_mod, + "NEW_SIGNAL_1": 0, + "NEW_SIGNAL_5": 100, + } + values["CF_Lkas_Chksum"] = create_checksum_can_canfd_blended(packer, bus, "LKAS11", values) + + alerts_364 = {k: v for k, v in msg_364.items() if k not in ("CHECKSUM", "COUNTER")} if msg_364 else {} + alerts_364.setdefault("BYTE2", 0) + alerts_364.setdefault("BYTE3", 0) + alerts_364.setdefault("DAW_Status", 0) + alerts_364["DAW_Warning"] = 0 + alerts_364.setdefault("BYTE5", 0) + alerts_364.setdefault("BYTE6", 0) + alerts_364.setdefault("BYTE7", 0) + alerts_364["COUNTER"] = frame % counter_mod + alerts_364["CHECKSUM"] = create_checksum_can_canfd_blended(packer, bus, "ALERTS_364", alerts_364) + + ret = [packer.make_can_msg("LKAS11", bus, values)] + if include_alerts: + ret.append(packer.make_can_msg("ALERTS_364", bus, alerts_364)) + return ret + + +def create_blended_steering(packer, CAN, torque, requested, icon): + values = {"LKA_MODE": 2, "LKA_ICON": icon, "TORQUE_REQUEST": torque, + "LKA_ASSIST": 0, "STEER_REQ": int(requested), "STEER_MODE": 0, "LKA_AVAILABLE": 0} + return packer.make_can_msg("LKAS", CAN.ACAN, values) + + +def create_blended_lfahda(packer, CAN, frame, icon): + values = {"LFA_Icon_State": icon, "COUNTER": frame % 16} + values['CHECKSUM'] = create_checksum_can_canfd_blended(packer, CAN.ECAN, 'LFAHDA_MFC', values) + return packer.make_can_msg('LFAHDA_MFC', CAN.ECAN, values) diff --git a/opendbc_repo/opendbc/car/hyundai/hyundaicanfd.py b/opendbc_repo/opendbc/car/hyundai/hyundaicanfd.py index 1d12549deb..3f303912ad 100644 --- a/opendbc_repo/opendbc/car/hyundai/hyundaicanfd.py +++ b/opendbc_repo/opendbc/car/hyundai/hyundaicanfd.py @@ -1,7 +1,12 @@ +import math +from binascii import crc_hqx +from typing import cast + import numpy as np -from opendbc.car import CanBusBase -from opendbc.car.crc import CRC16_XMODEM -from opendbc.car.hyundai.values import HyundaiFlags +from opendbc.car import CanBusBase, CanData +from opendbc.car.common.conversions import Conversions as CV +from opendbc.car.hyundai.ioniq6_bsm import BlindspotStatus, FRONT_NEUTRAL_BODY, rear_body +from opendbc.car.hyundai.values import CAR, HyundaiFlags class CanBus(CanBusBase): @@ -52,13 +57,72 @@ def create_steering_messages(packer, CP, CAN, enabled, lat_active, apply_torque) lkas_msg = "LKAS_ALT" if CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG_ALT else "LKAS" if CP.openpilotLongitudinalControl: ret.append(packer.make_can_msg("LFA", CAN.ECAN, values)) - ret.append(packer.make_can_msg(lkas_msg, CAN.ACAN, values)) + lkas_values = values + if (CP.carFingerprint == CAR.HYUNDAI_IONIQ_6 and CP.openpilotLongitudinalControl and + not CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING): + lkas_values = {**values, "Damping_Gain": 0} + ret.append(packer.make_can_msg(lkas_msg, CAN.ACAN, lkas_values)) else: ret.append(packer.make_can_msg("LFA", CAN.ECAN, values)) return ret +def _create_angle_frame(packer, name, bus, enabled, lat_active, apply_angle, reduction_gain, + stock_values=None, original_ccnc_ev_wire=False): + # Frozen angle protocol stores a signed 0.1-degree request in bits 82..95 + # on LKAS/LKAS_ALT/LFA, including the variants whose current DBC names only + # the torque fields. Keep the original status fields where they are observed. + # The original CCNC EV active command replaces stock warning/fault/UI fields. + # Other angle profiles retain their existing status-preservation behavior. + source_status = {} if original_ccnc_ev_wire and lat_active else (stock_values or {}) + values = {key: value for key, value in source_status.items() if key not in ("CHECKSUM", "COUNTER")} + values.update({ + "LKA_OptUsmSta": 0, + "LKA_SysIndReq": 2 if enabled else 1, + "StrTqReqVal": 0, + "ActToiSta": 0, + "LKA_SysWrn": 0, + "LKA_RcgSta": 3 if lat_active else 0, + "Damping_Gain": 100, + }) + address, packed, _ = packer.make_can_msg(name, bus, values) + data = bytearray(packed) + angle = float(np.clip(apply_angle, -360.0, 360.0)) + # Match the original DBC packer's signed 0.1-degree quantization for the exact CCNC EV stock identities. + angle_raw = (int(math.floor(angle / 0.1 + 0.5)) if original_ccnc_ev_wire else int(round(angle * 10.0))) & 0x3FFF + data[9] = (data[9] & ~0x30) | ((2 if lat_active else 1) << 4) + data[10] = (data[10] & 0x03) | ((angle_raw & 0x3F) << 2) + data[11] = (angle_raw >> 6) & 0xFF + data[12] = int(np.clip(round(reduction_gain / 0.004), 0, 250)) if lat_active else 0 + data[0:2] = hkg_can_fd_checksum(address, None, data).to_bytes(2, "little") + return address, bytes(data), bus + + +def create_angle_steering_messages(packer, CP, CAN, enabled, lat_active, apply_angle, reduction_gain, + stock_lkas_status=None, measured_angle=0.0): + if CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG: + lkas = "LKAS_ALT" if CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG_ALT else "LKAS" + return [_create_angle_frame(packer, lkas, CAN.ACAN, enabled, lat_active, apply_angle, + reduction_gain, stock_lkas_status, CP.carFingerprint in (CAR.HYUNDAI_IONIQ_5_PE, CAR.KIA_EV9))] + + if CP.flags & HyundaiFlags.SEND_LFA: + # Camera 0xCB topology retains a neutral LFA status frame; angle actuation + # uses the same 24-byte 0xCB payload that current DBC calls LFA_ALT. + status = _create_angle_frame(packer, "LFA", CAN.ECAN, enabled, False, measured_angle, 0.0) + command = packer.make_can_msg("LFA_ALT", CAN.ECAN, { + "ADAS_ActvACISta": 0, + "ADAS_ActvACILvl2Sta": 2 if lat_active else 1, + "ADAS_StrAnglReqVal": apply_angle, + "ADAS_ACIAnglTqRedcGainVal": reduction_gain if lat_active else 0.0, + "FCA_ESA_ActvSta": 0, + "FCA_ESA_TqBstGainVal": 0.0, + }) + return [status, command] + + return [_create_angle_frame(packer, "LFA", CAN.ECAN, enabled, lat_active, apply_angle, reduction_gain)] + + def create_suppress_lfa(packer, CAN, lfa_block_msg, lka_steering_alt): suppress_msg = "CAM_0x362" if lka_steering_alt else "CAM_0x2a4" msg_bytes = 32 if lka_steering_alt else 24 @@ -83,6 +147,17 @@ def create_buttons(packer, CP, CAN, cnt, btn): return packer.make_can_msg("CRUISE_BUTTONS", bus, values) +def create_carnival_alt_resume(packer, CP, CAN, source): + values = {name: value for name, value in source.items() if name not in ("CHECKSUM", "COUNTER")} + values.update({"COUNTER": (int(source["COUNTER"]) + 1) % 256, "SET_ME_1": 1, "CRUISE_BUTTONS": 1}) + bus = CAN.ECAN if CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG else CAN.CAM + address, data, bus = packer.make_can_msg("CRUISE_BUTTONS_ALT", bus, values) + data = bytearray(data) + checksum = hkg_can_fd_checksum(address, None, data) + data[0:2] = checksum.to_bytes(2, "little") + return address, bytes(data), bus + + def create_acc_cancel(packer, CP, CAN, cruise_info_copy): # CAN FD camera-based SCC requires additional signals to be preserved # verbatim from the previous SCC_CONTROL frame to avoid checksum or @@ -116,45 +191,164 @@ def create_acc_cancel(packer, CP, CAN, cruise_info_copy): return packer.make_can_msg("SCC_CONTROL", CAN.ECAN, values) -def create_lfahda_cluster(packer, CAN, enabled): +def create_lfahda_cluster(packer, CAN, enabled, *, lfa_icon=None): values = { "HDA_ICON": 1 if enabled else 0, - "LFA_ICON": 2 if enabled else 0, + "LFA_ICON": (2 if enabled else 0) if lfa_icon is None else lfa_icon, } return packer.make_can_msg("LFAHDA_CLUSTER", CAN.ECAN, values) -def create_acc_control(packer, CAN, enabled, accel_last, accel, stopping, gas_override, set_speed, hud_control): +def create_acc_control(packer, CAN, enabled, accel_last, accel, stopping, gas_override, set_speed, hud_control, + *, direct_accel=False, main_mode_acc=1, jerk_upper=3.0, jerk_lower=None, raw_accel=None, + lead_distance=None, lead_rel_speed=None, lead_visible=None): jerk = 5 jn = jerk / 50 if not enabled or gas_override: a_val, a_raw = 0, 0 else: - a_raw = accel - a_val = np.clip(accel, accel_last - jn, accel_last + jn) + a_raw = accel if not direct_accel or raw_accel is None else raw_accel + a_val = accel if direct_accel else np.clip(accel, accel_last - jn, accel_last + jn) + + if lead_distance is None and lead_rel_speed is None and lead_visible is None: + object_distance, object_relative, object_valid, object_status = 1., 0., 0, 2 + else: + visible = bool(lead_visible) + object_distance = float(np.clip(cast(float, lead_distance) if visible else 0., 0., 204.7)) + object_relative = float(np.clip(cast(float, lead_rel_speed) if visible else 0., -16.4, 34.7)) + object_valid = int(not visible) + object_status = 0 if not (enabled and visible) else 1 if gas_override else 2 values = { "ACCMode": 0 if not enabled else (2 if gas_override else 1), - "MainMode_ACC": 1, + "MainMode_ACC": main_mode_acc, "StopReq": 1 if stopping else 0, "aReqValue": a_val, "aReqRaw": a_raw, "VSetDis": set_speed, - "JerkLowerLimit": jerk if enabled else 1, - "JerkUpperLimit": 3.0, + "JerkLowerLimit": (jerk if enabled else 1) if jerk_lower is None else jerk_lower, + "JerkUpperLimit": jerk_upper, - "ACC_ObjDist": 1, - "ObjValid": 0, - "OBJ_STATUS": 2, + "ACC_ObjDist": object_distance, + "ObjValid": object_valid, + "OBJ_STATUS": object_status, "SET_ME_2": 0x4, "SET_ME_3": 0x3, "SET_ME_TMP_64": 0x64, "DISTANCE_SETTING": hud_control.leadDistanceBars, } + if lead_distance is not None or lead_rel_speed is not None or lead_visible is not None: + values["ACC_ObjRelSpd"] = object_relative return packer.make_can_msg("SCC_CONTROL", CAN.ECAN, values) +def create_ioniq6_radar_heartbeat(counter: int, brake_pressed: bool, gas_pressed: bool) -> CanData: + """Recorded Ioniq 6 ADAS replacement heartbeat, 24 bytes on A-CAN. + + Preserve the observed non-pedal body; this is not the 32-byte ICE message + sharing address 0x100. Only rolling integrity and actual pedal state vary. + """ + data = bytearray.fromhex("000000020000fcff000000000020000055ff000068000000") + data[2] = counter & 0xff + data[4] = int(brake_pressed) + data[22] = int(gas_pressed) + data[:2] = hkg_can_fd_checksum(0x100, None, data).to_bytes(2, 'little') + return CanData(0x100, bytes(data), 0) + + +def create_ioniq6_blindspot_status(counter: int, status: BlindspotStatus) -> list[CanData]: + """Paired HDA-II dashboard status from the frozen Ioniq 6 indicator layout.""" + frames = [] + for address, body in ((0x1BA, rear_body(status)), (0x1E5, FRONT_NEUTRAL_BODY)): + data = bytearray(body) + data[2] = counter & 0xff + data[:2] = hkg_can_fd_checksum(address, None, data).to_bytes(2, 'little') + frames.append(CanData(address, bytes(data), 1)) + return frames + + +# Source-derived CCNC camera display transform; the repository MIT notice applies. +def create_ccnc(packer, CAN, openpilot_longitudinal, enabled, hud, left_blinker, right_blinker, msg_161, msg_162, msg_1b5, + is_metric, out, main_cruise_enabled, lfa_icon): + if lfa_icon: + lane_change_speed_min = 8.9408 + any_blinker = left_blinker or right_blinker + curvature = {i: (31 if i == -1 else 13 - abs(i + 15)) if i < 0 else 15 + i for i in range(-15, 16)} + + msg_161.update({ + "LKA_ICON": 0, + "LFA_ICON": 2 if lfa_icon else 0, + "CENTERLINE": 1 if lfa_icon else 0, + "LANELINE_CURVATURE": curvature.get(max(-15, min(int(out.steeringAngleDeg / 4.5), 15)), 14) if lfa_icon and not any_blinker else 15, + "LANELINE_LEFT": 0 if not lfa_icon else 1 if not hud.leftLaneVisible else 4 if hud.leftLaneDepart else 6 if any_blinker else 2, + "LANELINE_RIGHT": 0 if not lfa_icon else 1 if not hud.rightLaneVisible else 4 if hud.rightLaneDepart else 6 if any_blinker else 2, + "LCA_LEFT_ICON": 0 if not lfa_icon or out.vEgo < lane_change_speed_min else 1 if out.leftBlindspot else 2 if any_blinker else 4, + "LCA_RIGHT_ICON": 0 if not lfa_icon or out.vEgo < lane_change_speed_min else 1 if out.rightBlindspot else 2 if any_blinker else 4, + "LCA_LEFT_ARROW": 2 if left_blinker else 0, + "LCA_RIGHT_ARROW": 2 if right_blinker else 0, + }) + + if any_blinker and msg_1b5 is not None: + left_lane_raw = msg_1b5["Info_LftLnPosVal"] + right_lane_raw = msg_1b5["Info_RtLnPosVal"] + scale_per_m = 15 / 1.7 + left_lane = abs(int(round(15 + (left_lane_raw - 1.7) * scale_per_m))) + right_lane = abs(int(round(15 + (right_lane_raw - 1.7) * scale_per_m))) + + if msg_1b5["Info_LftLnQualSta"] not in (2, 3): + left_lane = 0 + if msg_1b5["Info_RtLnQualSta"] not in (2, 3): + right_lane = 0 + + if left_lane_raw == -2.0248375: + left_lane = 30 - right_lane + if right_lane_raw == 2.0248375: + right_lane = 30 - left_lane + + if left_lane_raw == right_lane_raw == 0: + left_lane = right_lane = 15 + elif left_lane_raw == 0: + left_lane = 30 - right_lane + elif right_lane_raw == 0: + right_lane = 30 - left_lane + + total = left_lane + right_lane + if total == 0: + left_lane = right_lane = 15 + else: + left_lane = round((left_lane / total) * 30) + right_lane = 30 - left_lane + + msg_161["LANELINE_LEFT_POSITION"] = left_lane + msg_161["LANELINE_RIGHT_POSITION"] = right_lane + + if hud.leftLaneDepart or hud.rightLaneDepart: + msg_162["VIBRATE"] = 1 + else: + # Stock steering commands are suppressed in this topology. Its active + # lane indicators cannot describe the host's inactive lateral command. + msg_161.update({"LKA_ICON": 0, "LFA_ICON": 0, "CENTERLINE": 0, "LANELINE_LEFT": 0, "LANELINE_RIGHT": 0, + "LCA_LEFT_ICON": 0, "LCA_RIGHT_ICON": 0, "LCA_LEFT_ARROW": 0, "LCA_RIGHT_ARROW": 0}) + + if openpilot_longitudinal: + cruise_speed = round(out.vCruiseCluster * (1 if is_metric else CV.KPH_TO_MPH)) + msg_161.update({ + "SETSPEED": 3 if enabled else 1, + "SETSPEED_HUD": 0 if not main_cruise_enabled else 2 if enabled else 1, + "SETSPEED_SPEED": 255 if not main_cruise_enabled else (40 if is_metric else 25) if cruise_speed > (145 if is_metric else 90) else cruise_speed, + "DISTANCE": hud.leadDistanceBars, + "DISTANCE_SPACING": 0 if not main_cruise_enabled else 1 if enabled else 3, + "DISTANCE_LEAD": 0 if not main_cruise_enabled else 2 if enabled and hud.leadVisible else 1 if hud.leadVisible else 0, + "DISTANCE_CAR": 0 if not main_cruise_enabled else 2 if enabled else 1, + }) + msg_162["LEAD"] = 0 if not main_cruise_enabled else 2 if enabled else 1 + if msg_1b5 is not None: + msg_162["LEAD_DISTANCE"] = msg_1b5["Longitudinal_Distance"] + + return [packer.make_can_msg(msg, CAN.ECAN, values) for msg, values in (("CCNC_0x161", msg_161), ("CCNC_0x162", msg_162))] + + def create_spas_messages(packer, CAN, left_blink, right_blink): ret = [] @@ -190,7 +384,7 @@ def create_fca_warning_light(packer, CAN, frame): return ret -def create_adrv_messages(packer, CAN, frame): +def create_adrv_messages(packer, CAN, frame, *, template=None, drive_gear=False, speed=None): # messages needed to car happy after disabling # the ADAS Driving ECU to do longitudinal control @@ -198,7 +392,16 @@ def create_adrv_messages(packer, CAN, frame): values = { } - ret.append(packer.make_can_msg("ADRV_0x51", CAN.ACAN, values)) + from opendbc.car.hyundai.gv70_template import GV70Template + if template is not None: + if type(CAN.ACAN) is not int or type(CAN.offset) is not int: + raise ValueError('Captured ADRV requires integer CAN routing') + local_bus = CAN.ACAN - CAN.offset + packet = (template.frame(frame, drive_gear, local_bus, speed=speed) if isinstance(template, GV70Template) else + template.frame(frame, drive_gear, local_bus)) + ret.append(CanData(packet.address, packet.dat, CAN.ACAN)) + else: + ret.append(packer.make_can_msg("ADRV_0x51", CAN.ACAN, values)) ret.extend(create_fca_warning_light(packer, CAN, frame)) @@ -234,11 +437,7 @@ def create_adrv_messages(packer, CAN, frame): def hkg_can_fd_checksum(address: int, sig, d: bytearray) -> int: - crc = 0 - for i in range(2, len(d)): - crc = ((crc << 8) ^ CRC16_XMODEM[(crc >> 8) ^ d[i]]) & 0xFFFF - crc = ((crc << 8) ^ CRC16_XMODEM[(crc >> 8) ^ ((address >> 0) & 0xFF)]) & 0xFFFF - crc = ((crc << 8) ^ CRC16_XMODEM[(crc >> 8) ^ ((address >> 8) & 0xFF)]) & 0xFFFF + crc = crc_hqx(d[2:] + (address & 0xFFFF).to_bytes(2, "little"), 0) if len(d) == 8: crc ^= 0x5F29 elif len(d) == 16: diff --git a/opendbc_repo/opendbc/car/hyundai/interface.py b/opendbc_repo/opendbc/car/hyundai/interface.py index 25503f38a9..d0c6ea5bec 100644 --- a/opendbc_repo/opendbc/car/hyundai/interface.py +++ b/opendbc_repo/opendbc/car/hyundai/interface.py @@ -1,18 +1,30 @@ +from opendbc.car.pedal import supported_pedal_detected +from opendbc.car.hyundai.classic_scc_aol import CLASSIC_SCC_IDS from opendbc.car import Bus, get_safety_config, structs, uds from opendbc.car.hyundai.hyundaicanfd import CanBus -from opendbc.car.hyundai.values import HyundaiFlags, CAR, DBC, HyundaiSafetyFlags -from opendbc.car.hyundai.radar_interface import RADAR_START_ADDR +from opendbc.car.hyundai.non_scc_aol import NON_SCC_IDS +from opendbc.car.hyundai.values import (is_blended, is_blended_alpha, HyundaiFlags, CAR, DBC, HyundaiSafetyFlags, CANFD_ANGLE_MODEL_BITS, + CANFD_ANGLE_OBSERVED_ADAS_BIT, HYUNDAI_MRR35_RADAR_DBC, HYUNDAI_MRR30_RADAR_DBC, + HYUNDAI_GV70_RADAR_DBC, HYUNDAI_G90_RADAR_DBC) +from opendbc.car.hyundai.radar_interface import RADAR_START_ADDR, MRR35_RADAR_START_ADDR, MRR30_RADAR_START_ADDR, radar_bus from opendbc.car.interfaces import CarInterfaceBase from opendbc.car.disable_ecu import disable_ecu from opendbc.car.hyundai.carcontroller import CarController from opendbc.car.hyundai.carstate import CarState -from opendbc.car.hyundai.radar_interface import RadarInterface +from opendbc.car.hyundai.radar_interface import RadarInterface, blended_track_radar ButtonType = structs.CarState.ButtonEvent.Type Ecu = structs.CarParams.Ecu # Cancel button can sometimes be ACC pause/resume button, main button can also enable on some cars ENABLE_BUTTONS = (ButtonType.accelCruise, ButtonType.decelCruise, ButtonType.cancel, ButtonType.mainCruise) +KONA_NON_SCC_FCA_RADAR_ADDR = 0x602 + + +def detect_kona_non_scc_radar_fca(candidate, fingerprint, car_fw) -> bool: + return candidate == CAR.HYUNDAI_KONA_NON_SCC and ( + any(fw.ecu == Ecu.fwdRadar for fw in car_fw) or KONA_NON_SCC_FCA_RADAR_ADDR in fingerprint[1] + ) class CarInterface(CarInterfaceBase): @@ -20,12 +32,66 @@ class CarInterface(CarInterfaceBase): CarController = CarController RadarInterface = RadarInterface + @staticmethod + def startup_required(cp): + from opendbc.car.hyundai.g90_startup import required + from opendbc.car.hyundai.ev6_startup import required as ev6_required + from opendbc.car.hyundai.gv70_startup import required as gv70_required + from opendbc.car.hyundai.ev9_startup import required as ev9_required + from opendbc.car.hyundai.torque_ev_startup import required as torque_ev_required + from opendbc.car.hyundai.blended_longitudinal import startup_required as blended_required + from opendbc.car.hyundai.legacy_long_startup import required as legacy_required + return legacy_required(cp) or blended_required(cp) or required(cp) or ev6_required(cp) or gv70_required(cp) or ev9_required(cp) or torque_ev_required(cp) + + @staticmethod + def startup_owner(cp, callbacks, *, requested): + from opendbc.car.hyundai.legacy_long_aol import candidate as legacy_candidate + from opendbc.car.hyundai.legacy_long_startup import LegacyLongStartup + legacy_cp = legacy_candidate(cp, requested=requested) + if legacy_cp is not None: + return LegacyLongStartup(legacy_cp, callbacks, stock_cp=cp) + from opendbc.car.hyundai.g90_startup import G90Startup, required + if required(cp): + return G90Startup(cp, callbacks) if requested else None + from opendbc.car.hyundai.ev6_startup import EV6Startup, required as ev6_required + if ev6_required(cp): + return EV6Startup(cp, callbacks) if requested else None + from opendbc.car.hyundai.gv70_startup import GV70Startup, required as gv70_required + if gv70_required(cp): + return GV70Startup(cp, callbacks) if requested else None + from opendbc.car.hyundai.ev9_longitudinal import candidate as ev9_candidate, qualified as ev9_qualified + from opendbc.car.hyundai.ev9_startup import EV9Startup + ev9_cp = ev9_candidate(cp, enabled=requested, is_release=False) + if ev9_qualified(ev9_cp): + return EV9Startup(ev9_cp, callbacks, stock_cp=cp) if requested else None + from opendbc.car.hyundai.torque_ev_startup import candidate as torque_ev_candidate, TorqueEVStartup + torque_cp = torque_ev_candidate(cp, requested=requested, is_release=False) + if torque_cp is not None: + return TorqueEVStartup(torque_cp, callbacks, stock_cp=cp) + from opendbc.car.hyundai.blended_longitudinal import startup_owner + return startup_owner(cp, callbacks, requested=requested) + DRIVABLE_GEARS = (structs.CarState.GearShifter.sport, structs.CarState.GearShifter.manumatic) + @staticmethod + def get_pid_accel_limits(CP, current_speed, cruise_speed): + from opendbc.car.hyundai.ev9_longitudinal import qualified as ev9_long_qualified + if ev9_long_qualified(CP): + return -3.5, 2.2 + if is_blended_alpha(CP): + return -3.5, 3.5 + return CarInterfaceBase.get_pid_accel_limits(CP, current_speed, cruise_speed) + @staticmethod def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams: ret.brand = "hyundai" + if is_blended(ret): + cam_can = CanBus(None, fingerprint).CAM + lka_steering = any(fw.ecu == Ecu.adas for fw in car_fw) or 0x50 in fingerprint[cam_can] + if lka_steering: + ret.flags |= HyundaiFlags.CANFD_LKA_STEER_MSG.value + if ret.flags & HyundaiFlags.CANFD: # Shared configuration for CAN-FD cars @@ -38,15 +104,20 @@ class CarInterface(CarInterfaceBase): CAN = CanBus(None, fingerprint, lka_steering) ret.alphaLongitudinalAvailable = not (ret.flags & HyundaiFlags.CANFD_NO_RADAR_DISABLE) + if ret.flags & HyundaiFlags.CANFD_ANGLE_STEERING: + # These manual angle profiles retain stock SCC in both builds. + ret.alphaLongitudinalAvailable = False if lka_steering and Ecu.adas not in [fw.ecu for fw in car_fw]: # this needs to be figured out for cars without an ADAS ECU ret.alphaLongitudinalAvailable = False - if 0x1ba in fingerprint[CAN.ECAN]: - ret.flags |= HyundaiFlags.HAS_BSM.value + ret.deprecated.enableBsm = 0x1ba in fingerprint[CAN.ECAN] or candidate == CAR.KIA_EV9 + if candidate == CAR.HYUNDAI_IONIQ_6: + ret.deprecated.enableBsm = fingerprint[CAN.ECAN].get(0x1ba) == 24 + ret.steerAtStandstill = True # Check if the car is hybrid. Only HEV/PHEV cars have 0xFA on E-CAN. - if 0xFA in fingerprint[CAN.ECAN]: + if 0xFA in fingerprint[CAN.ECAN] or candidate == CAR.KIA_CARNIVAL_HEV_4TH_GEN: ret.flags |= HyundaiFlags.HYBRID.value if lka_steering: @@ -54,12 +125,22 @@ class CarInterface(CarInterfaceBase): ret.flags |= HyundaiFlags.CANFD_LKA_STEER_MSG.value if 0x110 in fingerprint[CAN.CAM]: ret.flags |= HyundaiFlags.CANFD_LKA_STEER_MSG_ALT.value + if (candidate in (CAR.KIA_CARNIVAL_2025, CAR.KIA_CARNIVAL_HEV_4TH_GEN) and + 0x1aa in fingerprint[CAN.ECAN] and 0x1cf not in fingerprint[CAN.ECAN]): + ret.flags |= HyundaiFlags.CANFD_ALT_BUTTONS.value else: # no LKA steering if 0x1cf not in fingerprint[CAN.ECAN]: ret.flags |= HyundaiFlags.CANFD_ALT_BUTTONS.value if not ret.flags & HyundaiFlags.CANFD_RADAR_SCC: ret.flags |= HyundaiFlags.CANFD_CAMERA_SCC.value + if ret.flags & HyundaiFlags.CANFD_ANGLE_STEERING and candidate != CAR.KIA_EV6_2025 and 0xCB in fingerprint[CAN.CAM]: + ret.flags |= HyundaiFlags.SEND_LFA.value + + # The Sportage 2026 identity is documented only without HDA II. A + # hybrid fuel source belongs to its separate HEV identity. + if candidate == CAR.KIA_SPORTAGE_2026 and (lka_steering or 0xFA in fingerprint[CAN.ECAN]): + ret.dashcamOnly = True # Some LKA steering cars have alternative messages for gear checks # ICE cars do not have 0x130; GEARS message on 0x40 or 0x70 instead @@ -69,6 +150,8 @@ class CarInterface(CarInterfaceBase): else: ret.flags |= HyundaiFlags.CANFD_ALT_GEARS.value + if candidate in (CAR.KIA_EV6, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN) and CAN.offset >= 8: + ret.dashcamOnly = True cfgs = [get_safety_config(structs.CarParams.SafetyModel.hyundaiCanfd), ] if CAN.ECAN >= 4: cfgs.insert(0, get_safety_config(structs.CarParams.SafetyModel.noOutput)) @@ -82,20 +165,32 @@ class CarInterface(CarInterfaceBase): ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.CANFD_ALT_BUTTONS.value if ret.flags & HyundaiFlags.CANFD_CAMERA_SCC: ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.CAMERA_SCC.value - + if ret.flags & HyundaiFlags.CCNC and not lka_steering and not ret.flags & HyundaiFlags.CANFD_ANGLE_STEERING: + ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.CCNC.value + if ret.flags & HyundaiFlags.CANFD_ANGLE_STEERING: + ret.steerControlType = structs.CarParams.SteerControlType.angle + ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.CANFD_ANGLE_STEERING.value + ret.safetyConfigs[-1].safetyParam |= CANFD_ANGLE_MODEL_BITS[str(candidate)] + if ret.flags & HyundaiFlags.SEND_LFA: + ret.safetyConfigs[-1].safetyParam |= CANFD_ANGLE_OBSERVED_ADAS_BIT else: # Shared configuration for non CAN-FD cars - ret.alphaLongitudinalAvailable = not (ret.flags & (HyundaiFlags.LEGACY | HyundaiFlags.UNSUPPORTED_LONGITUDINAL)) - if 0x58b in fingerprint[0]: - ret.flags |= HyundaiFlags.HAS_BSM.value + ret.alphaLongitudinalAvailable = not (ret.flags & (HyundaiFlags.LEGACY | HyundaiFlags.UNSUPPORTED_LONGITUDINAL | HyundaiFlags.NON_SCC)) + if is_blended(ret): + # A prepublication owner alone may promote this exact stock CP. + ret.alphaLongitudinalAvailable = False + pt_bus = CanBus(None, fingerprint, bool(ret.flags & HyundaiFlags.CANFD_LKA_STEER_MSG)).ECAN if is_blended(ret) else 0 + ret.deprecated.enableBsm = 0x58b in fingerprint[pt_bus] # Send LFA message on cars with HDA if 0x485 in fingerprint[2]: ret.flags |= HyundaiFlags.SEND_LFA.value # These cars use the FCA11 message for the AEB and FCW signals, all others use SCC12 - if 0x38d in fingerprint[0] or 0x38d in fingerprint[2]: + if 0x38d in fingerprint[pt_bus] or 0x38d in fingerprint[2]: ret.flags |= HyundaiFlags.USE_FCA.value + if detect_kona_non_scc_radar_fca(candidate, fingerprint, car_fw): + ret.flags |= HyundaiFlags.NON_SCC_RADAR_FCA.value if ret.flags & HyundaiFlags.LEGACY: # these cars require a special panda safety mode due to missing counters and checksums in the messages @@ -103,19 +198,43 @@ class CarInterface(CarInterfaceBase): else: ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.hyundai, 0)] + if is_blended(ret): + ret.safetyConfigs[0].safetyParam |= HyundaiSafetyFlags.CAN_CANFD_BLENDED.value + if lka_steering: + ret.safetyConfigs[0].safetyParam |= HyundaiSafetyFlags.CANFD_LKA_STEER_MSG.value + if ret.flags & HyundaiFlags.CAMERA_SCC: ret.safetyConfigs[0].safetyParam |= HyundaiSafetyFlags.CAMERA_SCC.value + if candidate in (CAR.HYUNDAI_ELANTRA_2024, CAR.HYUNDAI_ELANTRA_HEV_2024): + ret.safetyConfigs[0].safetyParam |= HyundaiSafetyFlags.CAN_REFRESH_MSGS.value # These cars have the LFA button on the steering wheel - if 0x391 in fingerprint[0]: + if 0x391 in fingerprint[0] or (candidate in NON_SCC_IDS | CLASSIC_SCC_IDS and + 0x50c in fingerprint[0]): ret.flags |= HyundaiFlags.HAS_LDA_BUTTON.value + if ret.flags & HyundaiFlags.NON_SCC: + ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.NON_SCC.value + if ret.flags & HyundaiFlags.HAS_LDA_BUTTON and (candidate not in NON_SCC_IDS or + 0x391 in fingerprint[0]): + # Ordinary safety still requires its existing BCM source. The AOL + # profile alone may select the observed alternative CLU source. + ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.HAS_LDA_BUTTON.value + + if candidate == CAR.KIA_RAY_EV and fingerprint[2].get(0x485) == 8: + ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.CAN_REFRESH_MSGS.value + + if is_blended(ret) and 0x2AA in fingerprint[0]: + ret.minSteerSpeed = 0.0 + ret.flags &= ~HyundaiFlags.MIN_STEER_32_MPH.value + ret.steerAtStandstill = True # Common lateral control setup ret.centerToFront = ret.wheelbase * 0.4 ret.steerActuatorDelay = 0.1 ret.steerLimitTimer = 0.4 - CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) + if not (ret.flags & HyundaiFlags.CANFD_ANGLE_STEERING): + CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) if ret.flags & HyundaiFlags.ALT_LIMITS: ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.ALT_LIMITS.value @@ -128,10 +247,40 @@ class CarInterface(CarInterfaceBase): # Common longitudinal control setup - ret.radarUnavailable = RADAR_START_ADDR not in fingerprint[1] or Bus.radar not in DBC[ret.carFingerprint] - ret.openpilotLongitudinalControl = alpha_long and ret.alphaLongitudinalAvailable + radar_start = KONA_NON_SCC_FCA_RADAR_ADDR if candidate == CAR.HYUNDAI_KONA_NON_SCC else RADAR_START_ADDR + if DBC[ret.carFingerprint].get(Bus.radar) == HYUNDAI_G90_RADAR_DBC: + ret.radarUnavailable = not all(fingerprint[1].get(addr) == 8 for addr in range(0x500, 0x520)) + elif blended_track_radar(ret) or DBC[ret.carFingerprint].get(Bus.radar) == HYUNDAI_GV70_RADAR_DBC: + ret.radarUnavailable = not all(fingerprint[0].get(addr) == 32 for addr in range(0x210, 0x220)) + elif DBC[ret.carFingerprint].get(Bus.radar) == HYUNDAI_MRR30_RADAR_DBC: + ret.radarUnavailable = fingerprint[radar_bus(ret)].get(MRR30_RADAR_START_ADDR) != 32 + elif DBC[ret.carFingerprint].get(Bus.radar) == HYUNDAI_MRR35_RADAR_DBC: + ret.radarUnavailable = fingerprint[radar_bus(ret)].get(MRR35_RADAR_START_ADDR) != 24 + else: + ret.radarUnavailable = radar_start not in fingerprint[1] or Bus.radar not in DBC[ret.carFingerprint] + from opendbc.car.hyundai.torque_ev_startup import topology_owned as torque_ev_topology + if torque_ev_topology(ret): + # The prepared owner alone promotes the saved developer choice before CI construction. + ret.alphaLongitudinalAvailable = not is_release + alpha_long = False + from opendbc.car.hyundai.legacy_long_aol import CARS as legacy_long_cars + ret.openpilotLongitudinalControl = alpha_long and ret.alphaLongitudinalAvailable and candidate not in legacy_long_cars + if (candidate in (CAR.KIA_CARNIVAL_2025, CAR.KIA_CARNIVAL_HEV_4TH_GEN) and + ret.flags & HyundaiFlags.CANFD_ALT_BUTTONS and 0x1aa in fingerprint[CAN.ECAN] and + not ret.openpilotLongitudinalControl): + ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.CARNIVAL_ALT_RESUME.value ret.pcmCruise = not ret.openpilotLongitudinalControl ret.longitudinalActuatorDelay = 0.5 + if candidate in (CAR.HYUNDAI_IONIQ_5_PE, CAR.KIA_EV9): + if candidate == CAR.HYUNDAI_IONIQ_5_PE: + ret.longitudinalActuatorDelay = 0.35 + ret.steerAtStandstill = True + # Only the source-derived stock-SCC LKA_ALT topology is admitted. + if (CAN.ECAN != 1 or CAN.ACAN != 0 or CAN.CAM != 2 or fingerprint[CAN.CAM].get(0x110) != 32 or + fingerprint[CAN.CAM].get(0x362) != 32 or fingerprint[CAN.ECAN].get(0x1cf) != 8 or + fingerprint[CAN.ECAN].get(0x35) != 32 or fingerprint[CAN.ECAN].get(0x1a0) != 32 or + ret.flags & (HyundaiFlags.HYBRID | HyundaiFlags.CANFD_ALT_BUTTONS | HyundaiFlags.CANFD_CAMERA_SCC | HyundaiFlags.SEND_LFA)): + ret.dashcamOnly = True if ret.openpilotLongitudinalControl: ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.LONG.value @@ -142,8 +291,34 @@ class CarInterface(CarInterfaceBase): elif ret.flags & HyundaiFlags.FCEV: ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.FCEV_GAS.value + if (supported_pedal_detected(fingerprint, 0, supported=candidate == CAR.KIA_RAY_EV) and + ret.safetyConfigs[-1].safetyParam == int(HyundaiSafetyFlags.NON_SCC | HyundaiSafetyFlags.EV_GAS | + HyundaiSafetyFlags.HAS_LDA_BUTTON | HyundaiSafetyFlags.CAN_REFRESH_MSGS)): + ret.alphaLongitudinalAvailable = True + ret.openpilotLongitudinalControl = True + ret.pcmCruise = False + ret.radarUnavailable = True + ret.autoResumeSng = False + ret.minEnableSpeed = -1.0 + ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.LONG.value + ret.longitudinalTuning.kiBP = [0.0, 3.0, 6.0, 35.0] + ret.longitudinalTuning.kiV = [0.07, 0.10, 0.15, 0.24] + + if candidate == CAR.KIA_RAY_EV and not ret.openpilotLongitudinalControl: + ret.dashcamOnly = True + # Car specific configuration overrides + if candidate == CAR.HYUNDAI_ELANTRA_HEV_2024: + ret.longitudinalActuatorDelay = 0.22 + + if is_blended(ret): + ret.openpilotLongitudinalControl = False + ret.pcmCruise = True + ret.stopAccel = -0.85 + from opendbc.car.hyundai.blended_longitudinal import mixed_startup_qualified + ret.alphaLongitudinalAvailable = mixed_startup_qualified(ret, is_release=is_release) + if candidate == CAR.KIA_OPTIMA_G4_FL: ret.steerActuatorDelay = 0.2 @@ -152,6 +327,41 @@ class CarInterface(CarInterfaceBase): if candidate in (CAR.KIA_OPTIMA_H,): ret.dashcamOnly = True + if candidate == CAR.KIA_EV6_2025 and ret.flags & (HyundaiFlags.CANFD_LKA_STEER_MSG | HyundaiFlags.HYBRID): + ret.dashcamOnly = True + ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.noOutput)] + + # Advertise only the exact developer stock profile. Startup still owns the + # saved-choice promotion, so availability never selects LONG during fingerprinting. + if candidate == CAR.KIA_EV9: + from opendbc.car.hyundai.ccnc_ev_stock import qualified as ev9_stock_qualified + ret.alphaLongitudinalAvailable = not is_release and ev9_stock_qualified(ret) + + from opendbc.car.hyundai.legacy_long_aol import CARS as legacy_long_cars, topology_owned + if candidate in legacy_long_cars: + ret.openpilotLongitudinalControl = False + ret.pcmCruise = True + ret.alphaLongitudinalAvailable = not is_release and topology_owned(ret) + + return ret + + def update(self, can_packets): + if (self.CS.gv70_camera_lead is not None or self.CS.ev9_camera_lead is not None or + self.CS.ioniq6_camera_lead is not None or self.CS.torque_ev_camera_lead is not None or self.CS.forte_lkas_sources is not None): + # This isolated optional parser must never join required controls health. + can_packets = list(can_packets) + if self.CS.gv70_camera_lead is not None: + self.CS.gv70_camera_lead.update(can_packets) + if self.CS.ev9_camera_lead is not None: + self.CS.ev9_camera_lead.update(can_packets) + if self.CS.ioniq6_camera_lead is not None: + self.CS.ioniq6_camera_lead.update(can_packets) + if self.CS.torque_ev_camera_lead is not None: + self.CS.torque_ev_camera_lead.update(can_packets) + if self.CS.forte_lkas_sources is not None: + self.CS.forte_lkas_sources.update(can_packets) + ret = super().update(can_packets) + self.CS.finalize_blended_cancel(ret) return ret @staticmethod @@ -160,7 +370,7 @@ class CarInterface(CarInterfaceBase): if communication_control is None: communication_control = bytes([uds.SERVICE_TYPE.COMMUNICATION_CONTROL, 0x80 | uds.CONTROL_TYPE.DISABLE_RX_DISABLE_TX, uds.MESSAGE_TYPE.NORMAL]) - if CP.openpilotLongitudinalControl and not (CP.flags & (HyundaiFlags.CANFD_CAMERA_SCC | HyundaiFlags.CAMERA_SCC)): + if CP.openpilotLongitudinalControl and CP.carFingerprint != CAR.KIA_RAY_EV and not (CP.flags & (HyundaiFlags.CANFD_CAMERA_SCC | HyundaiFlags.CAMERA_SCC)): addr, bus = 0x7d0, CanBus(CP).ECAN if CP.flags & HyundaiFlags.CANFD else 0 if CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG.value: addr, bus = 0x730, CanBus(CP).ECAN diff --git a/opendbc_repo/opendbc/car/hyundai/ioniq5pe.py b/opendbc_repo/opendbc/car/hyundai/ioniq5pe.py new file mode 100644 index 0000000000..dfb38fd90b --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/ioniq5pe.py @@ -0,0 +1,17 @@ +"""Compatibility API retaining the exact Ioniq 5 PE stock contract.""" +from opendbc.car.hyundai import ccnc_ev_stock +from opendbc.car.hyundai.values import CAR + +STOCK_SAFETY_PARAM = 0x5491 + + +def qualified(cp): + return cp.carFingerprint == CAR.HYUNDAI_IONIQ_5_PE and ccnc_ev_stock.qualified(cp) + + +def request_allowed(cp, state): + return qualified(cp) and ccnc_ev_stock.request_allowed(cp, state) + + +def replacement_requested(cp, control, state): + return qualified(cp) and ccnc_ev_stock.replacement_requested(cp, control, state) diff --git a/opendbc_repo/opendbc/car/hyundai/ioniq6_bsm.py b/opendbc_repo/opendbc/car/hyundai/ioniq6_bsm.py new file mode 100644 index 0000000000..0e434dcbaa --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/ioniq6_bsm.py @@ -0,0 +1,78 @@ +"""Exact Ioniq 6 HDA-II blind-spot status from physical corner and lamp sources.""" + +from dataclasses import dataclass + + +CORNER_MAX_AGE_NS = 100_000_000 +LAMP_MAX_AGE_NS = 1_500_000_000 +LAMP_HOLD_NS = 500_000_000 + +REAR_NEUTRAL_BODY = bytes.fromhex("000000000000008000000000000000000000000000000000") +FRONT_NEUTRAL_BODY = bytes.fromhex("00000000000000000000000100000000") + + +@dataclass(frozen=True) +class BlindspotStatus: + left: int + right: int + + +def rear_body(status: BlindspotStatus) -> bytes: + """Return the frozen Ioniq 6 indication body; integrity bytes remain zero.""" + left, right = status.left, status.right + if left not in (0, 1, 2) or right not in (0, 1, 2): + raise ValueError("invalid blind-spot indication") + body = bytearray(REAR_NEUTRAL_BODY) + body[3] = int(bool(left or right)) | (left << 6) + body[4] = right + body[5] = left << 1 + body[6] = right << 5 + body[7] = 0x80 | (max(left, right) << 3) + body[16] = left | (right << 2) + return bytes(body) + + +class Ioniq6BlindspotSources: + """One-source-clock status gate for paired dashboard frames. + + CANParser only updates source timestamps for checksum-valid corner frames. + Repeated parser values do not extend a blinker transition's hold. + """ + + def __init__(self) -> None: + self.lamp_source_ns = 0 + self.left_lamp = False + self.right_lamp = False + self.left_off_ns = 0 + self.right_off_ns = 0 + self.corner_source_ns = 0 + self.corner_state = 0 + + def observe(self, *, corner_source_ns: int, corner_state: int, lamp_source_ns: int, + left_lamp: bool, right_lamp: bool) -> None: + if corner_source_ns > self.corner_source_ns: + self.corner_source_ns = corner_source_ns + self.corner_state = corner_state + if lamp_source_ns > self.lamp_source_ns: + if self.lamp_source_ns > 0 and lamp_source_ns - self.lamp_source_ns > LAMP_MAX_AGE_NS: + # A pre-gap illuminated lamp cannot authorize a new post-gap hold. + self.left_lamp = self.right_lamp = False + self.left_off_ns = self.right_off_ns = 0 + if self.left_lamp and not left_lamp: + self.left_off_ns = lamp_source_ns + if self.right_lamp and not right_lamp: + self.right_off_ns = lamp_source_ns + self.left_lamp = left_lamp + self.right_lamp = right_lamp + self.lamp_source_ns = lamp_source_ns + + def status(self, now_ns: int) -> BlindspotStatus | None: + if not (self.corner_source_ns > 0 and 0 <= now_ns - self.corner_source_ns <= CORNER_MAX_AGE_NS and + self.lamp_source_ns > 0 and 0 <= now_ns - self.lamp_source_ns <= LAMP_MAX_AGE_NS): + return None + left_present = bool(self.corner_state & 0x10) + right_present = bool(self.corner_state & 0x08) + left_blinking = self.left_lamp or (self.left_off_ns > 0 and 0 <= now_ns - self.left_off_ns <= LAMP_HOLD_NS) + right_blinking = self.right_lamp or (self.right_off_ns > 0 and 0 <= now_ns - self.right_off_ns <= LAMP_HOLD_NS) + return BlindspotStatus(1 + int(left_blinking) if left_present else 0, + 1 + int(right_blinking) if right_present else 0) diff --git a/opendbc_repo/opendbc/car/hyundai/ioniq6_handoff.py b/opendbc_repo/opendbc/car/hyundai/ioniq6_handoff.py new file mode 100644 index 0000000000..f2d90aaba6 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/ioniq6_handoff.py @@ -0,0 +1,366 @@ +"""Ioniq 6 HDA-II candidate and ECU-ownership transaction. + +Select the exact opt-in DEBUG CarParams before publication. Verify live sources +and run the reversible diagnostic transaction when Card is ready to replace TX. +""" + +import statistics +import threading +import time +from collections.abc import Callable +from dataclasses import dataclass +from enum import Enum, auto + +from opendbc.car.can_definitions import CanData, CanRecvCallable, CanSendCallable +from opendbc.car import make_tester_present_msg +from opendbc.car.hyundai.hyundaicanfd import hkg_can_fd_checksum +from opendbc.car.isotp_parallel_query import IsoTpParallelQuery + + +IONIQ6_ECAN_BUS = 1 +IONIQ6_ADAS_ADDR = 0x730 +IONIQ6_STOCK_SCC_ADDR = 0x1A0 +IONIQ6_STOCK_HEARTBEAT_ADDR = 0x100 +IONIQ6_STOCK_BSM = {0x1BA: 24, 0x1E5: 16} +IONIQ6_REQUIRED_RX = {0x35: 32, 0x175: 24, 0xA0: 24, 0xEA: 24, 0x1CF: 8} +IONIQ6_LONG_PREARM_ENABLED = True +IONIQ6_DEBUG_LONG_PARAMS = frozenset((0x8015, 0x8095, 0x8815, 0x8895)) + + +class HandoffOutcome(Enum): + NOT_READY = auto() + STOCK = auto() + CONFIRMED = auto() + UNAVAILABLE = auto() + + +@dataclass(frozen=True) +class HandoffResult: + outcome: HandoffOutcome + reason: str + source_period: float | None = None + disable_acknowledged: bool = False + restore_acknowledged: bool = False + + +def build_ioniq6_hda2_long_candidate(stock_cp, fingerprint): + """Prepare a separate CP only for the two observed HDA-II EV forms.""" + from opendbc.car.hyundai.radar_interface import MRR35_RADAR_START_ADDR + from opendbc.car.hyundai.values import CAR + + if stock_cp.carFingerprint != CAR.HYUNDAI_IONIQ_6 or stock_cp.radarUnavailable: + return None + camera = fingerprint[2] + direct = camera.get(0x50) == 16 and 0x110 not in camera + alternate = camera.get(0x110) == 32 and 0x50 not in camera + if not (direct or alternate): + return None + camera_support = (0x362, 32) if alternate else (0x2A4, 24) + if (camera.get(camera_support[0]) != camera_support[1] or + fingerprint[1].get(0x1CF) != 8 or + fingerprint[1].get(0x35) != 32 or fingerprint[1].get(0x175) != 24 or + fingerprint[1].get(0xA0) != 24 or fingerprint[1].get(0xEA) != 24 or + any(fingerprint[1].get(addr) != length for addr, length in {**IONIQ6_STOCK_BSM, 0x36A: 16}.items()) or + fingerprint[0].get(MRR35_RADAR_START_ADDR) != 24): + return None + if int(stock_cp.safetyConfigs[-1].safetyParam) not in (0x11, 0x91): + return None + + long_cp = stock_cp.as_reader().as_builder() + long_cp.alphaLongitudinalAvailable = True + long_cp.openpilotLongitudinalControl = True + long_cp.pcmCruise = False + long_cp.safetyConfigs[-1].safetyParam = 0x8095 if alternate else 0x8015 + return long_cp + + +DiagnosticExchange = Callable[[bytes, bytes], bool] +Clock = Callable[[], float] + + +class TimestampedCanPacket(list[CanData]): + """CAN event frames with an optional producer timestamp for cadence only.""" + + def __init__(self, frames: list[CanData], log_mono_time_ns: int): + super().__init__(frames) + self.log_mono_time_ns = log_mono_time_ns + + +class ObservingCanRecv: + """Keep SCC source evidence while ISO-TP consumes the shared CAN stream.""" + + def __init__(self, can_recv: CanRecvCallable, clock: Clock = time.monotonic): + self.can_recv = can_recv + self.clock = clock + self.source_seen_count = 0 + self.latest_source_time: float | None = None + self.heartbeat_seen_count = 0 + + def __call__(self, wait_for_one: bool = False) -> list[list[CanData]]: + packets = self.can_recv(wait_for_one=wait_for_one) + for packet in packets: + for msg in packet: + if msg.src == IONIQ6_ECAN_BUS and msg.address in (IONIQ6_STOCK_SCC_ADDR, *IONIQ6_STOCK_BSM): + self.source_seen_count += 1 + self.latest_source_time = self.clock() + elif msg.src == 0 and msg.address == IONIQ6_STOCK_HEARTBEAT_ADDR: + self.heartbeat_seen_count += 1 + return packets + + +def query_ioniq6_adas(can_recv: CanRecvCallable, can_send: CanSendCallable, request: bytes, response: bytes) -> bool: + """Validate the service-specific positive response from 0x738 on E-CAN. + + DiagnosticSessionControl may include four P2/P2* timing bytes after 0x50 xx; + CommunicationControl has no corresponding response payload. + """ + try: + query = IsoTpParallelQuery(can_send, can_recv, IONIQ6_ECAN_BUS, [IONIQ6_ADAS_ADDR], [request], [response]) + payload = query.get_data(0.15, total_timeout=0.25).get((IONIQ6_ADAS_ADDR, None)) + if request in (b"\x10\x03", b"\x10\x01") and response == bytes((0x50, request[1])): + return payload is not None and len(payload) in (0, 4) + if request in (b"\x28\x01\x01", b"\x28\x00\x01") and response == bytes((0x68, request[1])): + return payload == b"" + return False + except Exception: + return False + + +def _valid_canfd_crc(msg: CanData) -> bool: + return len(msg.dat) >= 2 and int.from_bytes(msg.dat[:2], "little") == hkg_can_fd_checksum(msg.address, None, bytearray(msg.dat)) + + +def _read_window(can_recv: CanRecvCallable, clock: Clock, duration: float, *, wait_for_one: bool = True) -> tuple[list[tuple[float, int, float]], set[int], set[int], int, bool, set[int]]: + """Collect both stock ECU TX sources and required E-CAN input health.""" + source_samples: list[tuple[float, int, float]] = [] + healthy: set[int] = set() + source_seen: set[int] = set() + status_valid: set[int] = set() + heartbeat_valid = 0 + heartbeat_seen = False + deadline = clock() + duration + while clock() < deadline: + packets = can_recv(wait_for_one=wait_for_one) + if not packets and not wait_for_one: + # Polling preflight must also finish when the bus produces no frames. + time.sleep(min(0.001, max(0.0, deadline - clock()))) + for packet in packets: + received_at = clock() + producer_ns = getattr(packet, "log_mono_time_ns", 0) + sample_time = producer_ns * 1e-9 if producer_ns > 0 else received_at + for msg in packet: + if msg.src == 0 and msg.address == IONIQ6_STOCK_HEARTBEAT_ADDR: + heartbeat_seen = True + if len(msg.dat) == 24 and _valid_canfd_crc(msg): + heartbeat_valid += 1 + continue + if msg.src != IONIQ6_ECAN_BUS: + continue + if msg.address == IONIQ6_STOCK_SCC_ADDR: + # Even a bad CRC proves the stock ECU is still transmitting. + source_seen.add(msg.address) + if len(msg.dat) == 32 and _valid_canfd_crc(msg): + source_samples.append((sample_time, msg.dat[2], received_at)) + elif msg.address in IONIQ6_STOCK_BSM: + source_seen.add(msg.address) + if len(msg.dat) == IONIQ6_STOCK_BSM[msg.address] and _valid_canfd_crc(msg): + status_valid.add(msg.address) + elif msg.address in IONIQ6_REQUIRED_RX and len(msg.dat) == IONIQ6_REQUIRED_RX[msg.address]: + if msg.address == 0x1CF or _valid_canfd_crc(msg): + healthy.add(msg.address) + return source_samples, healthy, source_seen, heartbeat_valid, heartbeat_seen, status_valid + + +def _measured_period(source_samples: list[tuple[float, int, float]]) -> float | None: + if len(source_samples) < 4: + return None + # A queued old burst with valid producer spacing cannot establish current + # stock source health. Fresh packets must reach this process across time. + if source_samples[-1][2] - source_samples[0][2] < 0.08: + return None + if any((b[1] - a[1]) % 256 != 1 for a, b in zip(source_samples, source_samples[1:], strict=False)): + return None + periods = [b[0] - a[0] for a, b in zip(source_samples, source_samples[1:], strict=False)] + period = statistics.median(periods) + if not (0.012 <= period <= 0.030) or any(not (0.008 <= delta <= 0.050) for delta in periods): + return None + return period + + +def inspect_ioniq6_long_sources(can_recv: CanRecvCallable, clock: Clock = time.monotonic) -> HandoffResult: + """Receive-only prerequisite check before selecting a longitudinal profile.""" + # Each attempt stands alone: never combine health or cadence across windows. + # Drain fingerprint/retry backlog before measuring current SCC cadence. + # Post-disable windows deliberately do not drain: any stock source matters. + for _ in range(3): + can_recv(wait_for_one=False) + pre_times, pre_health, pre_sources, pre_heartbeat_valid, _, pre_status_valid = _read_window( + can_recv, clock, 0.25, wait_for_one=False) + period = _measured_period(pre_times) + missing = [] + if period is None: + missing.append('scc_period') + if pre_heartbeat_valid < 4: + missing.append('radar_heartbeat') + if pre_health != IONIQ6_REQUIRED_RX.keys(): + missing.append('required_rx') + if pre_sources != {IONIQ6_STOCK_SCC_ADDR, *IONIQ6_STOCK_BSM}: + missing.append('stock_sources') + if pre_status_valid != IONIQ6_STOCK_BSM.keys(): + missing.append('bsm_status') + if not missing: + return HandoffResult(HandoffOutcome.NOT_READY, 'preflight_ready', period) + return HandoffResult(HandoffOutcome.NOT_READY, 'preflight_' + ','.join(missing)) + + +def run_ioniq6_handoff(can_recv: CanRecvCallable, exchange: DiagnosticExchange, clock: Clock = time.monotonic) -> HandoffResult: + """Resolve stock or long authority before constructing or publishing CarParams. + + The 50 Hz SCC source yields a provisional five-period silence/resumption + window; these bounds require route calibration before feature activation. + """ + preflight = inspect_ioniq6_long_sources(can_recv, clock) + period = preflight.source_period + if period is None: + return preflight + + silence_window = min(max(5.0 * period, 0.10), 0.20) + session_ack = exchange(b"\x10\x03", b"\x50\x03") + disable_ack = session_ack and exchange(b"\x28\x01\x01", b"\x68\x01") + if disable_ack: + _, post_health, post_source_seen, _, post_heartbeat_seen, _ = _read_window(can_recv, clock, silence_window) + if not post_source_seen and not post_heartbeat_seen and post_health == IONIQ6_REQUIRED_RX.keys(): + return HandoffResult(HandoffOutcome.CONFIRMED, "positive UDS response and observed SCC/heartbeat/BSM silence", period, True) + + # Even an unanswered 0x28 request might have taken effect. Restore both + # normal-message TX and the normal diagnostic session before stock fallback. + restore_tx_ack = exchange(b"\x28\x00\x01", b"\x68\x00") + restore_session_ack = exchange(b"\x10\x01", b"\x50\x01") + restored = restore_tx_ack and restore_session_ack + restored_times, restored_health, restored_sources, restored_heartbeat_valid, _, restored_status_valid = _read_window( + can_recv, clock, max(0.25, silence_window)) + if (restored and _measured_period(restored_times) is not None and restored_heartbeat_valid >= 4 and + restored_health == IONIQ6_REQUIRED_RX.keys() and + restored_sources == {IONIQ6_STOCK_SCC_ADDR, *IONIQ6_STOCK_BSM} and restored_status_valid == IONIQ6_STOCK_BSM.keys()): + return HandoffResult(HandoffOutcome.STOCK, "restored ECU TX/session and verified stock SCC/heartbeat/BSM", period, disable_ack, True) + return HandoffResult(HandoffOutcome.UNAVAILABLE, "ECU restore or stock SCC/heartbeat/BSM resumption unverified", period, disable_ack, restored) + + +def restore_ioniq6_adas(can_recv: CanRecvCallable, can_send: CanSendCallable, clock: Clock = time.monotonic) -> bool: + """Abort a post-publication startup without ever claiming stock CP ownership.""" + observed_recv = ObservingCanRecv(can_recv, clock) + restore_tx = query_ioniq6_adas(observed_recv, can_send, b"\x28\x00\x01", b"\x68\x00") + restore_session = query_ioniq6_adas(observed_recv, can_send, b"\x10\x01", b"\x50\x01") + samples, healthy, sources, heartbeat_valid, _, status_valid = _read_window(observed_recv, clock, 0.25) + return (restore_tx and restore_session and _measured_period(samples) is not None and heartbeat_valid >= 4 and + healthy == IONIQ6_REQUIRED_RX.keys() and sources == {IONIQ6_STOCK_SCC_ADDR, *IONIQ6_STOCK_BSM} and + status_valid == IONIQ6_STOCK_BSM.keys()) + + +class Ioniq6StartupKeepalive: + """Maintain the acknowledged diagnostic session until Panda takes TX. + + A conservative 0.5-second diagnostic interval keeps the prepublication + session alive before the controller's normal 1 Hz tester takes ownership. + The 60-second startup bound is a development fail-closed limit, not a measured + ECU session lifetime. Production enablement needs parked/device timing. + """ + + def __init__(self, can_send: CanSendCallable, restore: Callable[[], bool], on_abort: Callable[[], None], + on_restore: Callable[[bool], None], clock: Clock = time.monotonic, max_startup_sec: float = 60.0): + self.can_send = can_send + self.restore = restore + self.on_abort = on_abort + self.on_restore = on_restore + self.clock = clock + self.max_startup_sec = max_startup_sec + self.started_at = clock() + self.next_tester_at = self.started_at + self.stop_event = threading.Event() + self.lock = threading.Lock() + self.thread: threading.Thread | None = None + self.completed = False + + def tick(self): + with self.lock: + if self.completed or self.stop_event.is_set(): + return + now = self.clock() + if now - self.started_at >= self.max_startup_sec: + self._abort_locked() + elif now >= self.next_tester_at: + try: + self.can_send([make_tester_present_msg(IONIQ6_ADAS_ADDR, IONIQ6_ECAN_BUS, suppress_response=True)]) + self.next_tester_at = now + 0.5 + except Exception: + self._abort_locked() + + def _abort_locked(self): + self.completed = True + # The caller permanently suppresses controller TX before restoration. + self.on_abort() + try: + restored = self.restore() + except Exception: + restored = False + self.on_restore(restored) + + def start(self): + def loop(): + while not self.stop_event.wait(0.05) and not self.completed: + self.tick() + + self.tick() + self.thread = threading.Thread(target=loop, name="ioniq6-prearm-tester", daemon=True) + self.thread.start() + + def stop(self): + # This is the TX ownership boundary: no keepalive or restore is in flight + # when the caller is allowed to submit its first controller frame. + self.stop_event.set() + if self.thread is not None and self.thread is not threading.current_thread(): + self.thread.join() + with self.lock: + self.completed = True + + +def finalize_ioniq6_prepublication(stock_cp, long_cp, can_recv: CanRecvCallable, can_send: CanSendCallable, + *, enabled: bool, is_release: bool, clock: Clock = time.monotonic): + """Choose a prepared CP before CarInterface exists; never mutate it after arm. + + The DEBUG candidate must already be built and validated before any UDS request. + """ + if not enabled or is_release or long_cp is None: + return stock_cp, False, None + raw = int(long_cp.safetyConfigs[-1].safetyParam) + if not long_cp.openpilotLongitudinalControl or raw not in IONIQ6_DEBUG_LONG_PARAMS: + raise ValueError("unreviewed Ioniq 6 longitudinal CarParams profile") + + observed_recv = ObservingCanRecv(can_recv, clock) + result = run_ioniq6_handoff( + observed_recv, lambda request, response: query_ioniq6_adas(observed_recv, can_send, request, response), clock) + if result.outcome is HandoffOutcome.CONFIRMED: + return long_cp, True, result + if result.outcome is HandoffOutcome.UNAVAILABLE: + # Card's usual passive setup will publish noOutput; do not advertise stock + # cruise ownership when the stock SCC ECU may still be silent. + stock_cp.dashcamOnly = True + return stock_cp, False, result + + +def prepare_ioniq6_long_candidate(stock_cp, long_cp, *, enabled: bool, is_release: bool): + """Select the immutable long CP without changing ECU communication yet.""" + if not enabled or is_release or long_cp is None: + return stock_cp, False + raw = int(long_cp.safetyConfigs[-1].safetyParam) + if not long_cp.openpilotLongitudinalControl or raw not in IONIQ6_DEBUG_LONG_PARAMS: + raise ValueError("unreviewed Ioniq 6 longitudinal CarParams profile") + return long_cp, True + + +def confirm_ioniq6_prepared_takeover(can_recv: CanRecvCallable, can_send: CanSendCallable, + clock: Clock = time.monotonic) -> HandoffResult: + """Run the source/UDS transaction only when replacement TX can start.""" + observed_recv = ObservingCanRecv(can_recv, clock) + return run_ioniq6_handoff( + observed_recv, lambda request, response: query_ioniq6_adas(observed_recv, can_send, request, response), clock) diff --git a/opendbc_repo/opendbc/car/hyundai/ioniq6_longitudinal.py b/opendbc_repo/opendbc/car/hyundai/ioniq6_longitudinal.py new file mode 100644 index 0000000000..915f5d38c4 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/ioniq6_longitudinal.py @@ -0,0 +1,131 @@ +"""Ioniq 6 acceleration calibration, independent of ECU ownership and CAN packing. + +The active-drive calibration comes from StarPilot 678af783. Inactive control and +pedal override reset its history so a later engagement cannot reuse old demand. +""" + +from dataclasses import dataclass + +import numpy as np + +from opendbc.car import DT_CTRL, structs +from opendbc.car.hyundai.values import CarControllerParams + +LongState = structs.CarControl.Actuators.LongControlState +RESPONSE = 1.2 +MIN_JERK = 0.5 * RESPONSE +MAX_JERK = 4.8 * RESPONSE +ACCEL_STEP = 6.0 / 50.0 * RESPONSE +DECEL_STEP = 15.0 / 50.0 * RESPONSE +LOOKAHEAD_SPEEDS = (2.0, 5.0, 20.0) +LOOKAHEAD_TIMES = (0.3 / RESPONSE, 0.45 / RESPONSE, 0.6 / RESPONSE) +LOWER_JERK_ERRORS = (-2.0, -1.5, -1.0, -0.25, -0.1, -0.025, -0.01, -0.005) +LOWER_JERK_VALUES = tuple(value * (MAX_JERK / 3.3) for value in (3.3, 1.5, 1.0, 0.8, 0.7, 0.65, 0.55, 0.5)) + + +def launch_floor(speed: float) -> float: + return float(np.interp(speed, (0.0, 0.6, 1.25, 2.5), (0.75, 0.6, 0.4, 0.0))) + + +@dataclass +class Ioniq6LongitudinalState: + desired_accel: float = 0.0 + actual_accel: float = 0.0 + accel_last: float = 0.0 + jerk_upper: float = 0.0 + jerk_lower: float = 0.0 + launch_active: bool = False + stopping: bool = False + long_control_state_last: int = LongState.off + + +@dataclass(frozen=True) +class AccelerationRequest: + accel: float = 0.0 + stopping: bool = False + jerk_upper: float = 3.0 + jerk_lower: float = 1.0 + + +def update_calibration(state: Ioniq6LongitudinalState, accel: float, speed: float, measured_accel: float, + control_state: int) -> None: + """Advance the original active-control calibration once per 50 ms.""" + starting = control_state == LongState.starting + state.stopping = control_state == LongState.stopping + restarting = (state.long_control_state_last in (LongState.stopping, LongState.starting) and + control_state in (LongState.starting, LongState.pid) and accel > 0.0 and speed < 0.5) + + if accel <= 0.0 or speed >= 2.5 or accel < launch_floor(speed): + state.launch_active = False + elif starting or state.launch_active or (state.long_control_state_last == LongState.starting and control_state == LongState.pid): + state.launch_active = True + + upper_limit = float(np.interp(speed, (0.0, 5.0, 20.0), (2.0, 3.0, 2.0))) * RESPONSE if control_state == LongState.pid else MIN_JERK + lower_limit = float(np.interp(speed, (0.0, 5.0, 20.0), (5.0, 3.5, 3.0))) * RESPONSE + horizon = float(np.interp(speed, LOOKAHEAD_SPEEDS, LOOKAHEAD_TIMES)) + desired_jerk = float(np.clip((accel - state.accel_last) / horizon, -MAX_JERK, MAX_JERK)) + measured_error = measured_accel - state.accel_last + lower_jerk = float(np.interp(measured_error, LOWER_JERK_ERRORS, LOWER_JERK_VALUES)) if measured_error < 0.0 else MIN_JERK + state.jerk_upper = min(max(desired_jerk, MIN_JERK), upper_limit) + state.jerk_lower = min(lower_jerk, lower_limit) + + if state.stopping: + if speed <= 2.0: + brake_cap = float(np.interp(speed, (0.0, 0.08, 0.25, 0.6, 1.2, 2.0, 3.0), (-0.15, -0.16, -0.22, -0.42, -0.78, -1.15, -1.40))) + state.desired_accel = min(0.0, max(accel, brake_cap)) + hold_jerk = float(np.interp(speed, (0.0, 0.15, 0.6, 1.2, 2.0, 3.0), (0.35, 0.40, 0.48, 0.65, 0.85, 1.10))) * RESPONSE + state.jerk_upper = min(state.jerk_upper, hold_jerk) + else: + state.desired_accel = float(np.clip(accel, CarControllerParams.ACCEL_MIN, 0.0)) + else: + state.desired_accel = float(np.clip(accel, CarControllerParams.ACCEL_MIN, CarControllerParams.ACCEL_MAX)) + if state.launch_active: + state.desired_accel = max(state.desired_accel, launch_floor(speed)) + state.jerk_upper = max(state.jerk_upper, float(np.interp(speed, (0.0, 2.5), (4.8, 3.2))) * RESPONSE) + state.jerk_lower = max(state.jerk_lower, 1.0) + if restarting: + release_jerk = float(np.interp(speed, (0.0, 0.15, 0.5), (3.6 * RESPONSE, 4.2 * RESPONSE, 4.8 * RESPONSE))) + state.jerk_upper = min(state.jerk_upper, release_jerk) + + step = (state.jerk_upper if state.desired_accel >= state.accel_last else state.jerk_lower) * DT_CTRL * 5.0 + state.actual_accel = float(np.clip(state.desired_accel, state.accel_last - step, state.accel_last + step)) + state.accel_last = state.actual_accel + state.long_control_state_last = control_state + + +class Ioniq6LongitudinalPolicy: + def __init__(self): + self.state = Ioniq6LongitudinalState() + self.was_active = False + + def update(self, frame: int, *, active: bool, override: bool, accel: float, speed: float, measured_accel: float, + control_state: int, last_sent_accel: float) -> AccelerationRequest: + enabled = active and not override and control_state in (LongState.starting, LongState.pid, LongState.stopping) + if not enabled: + self.state = Ioniq6LongitudinalState() + self.was_active = False + return AccelerationRequest() + + accel = float(np.clip(accel, CarControllerParams.ACCEL_MIN, CarControllerParams.ACCEL_MAX)) + # The 20 Hz calibration may not run on this 100 Hz call. Withdraw the + # launch floor immediately when a fresh host target becomes softer; the + # ordinary bounded decrease below still handles residual output. + if accel <= 0.0 or speed >= 2.5 or accel < launch_floor(speed): + self.state.launch_active = False + if frame % 5 == 0 or not self.was_active: + update_calibration(self.state, accel, speed, measured_accel, control_state) + self.was_active = True + smoothed = accel >= self.state.actual_accel or self.state.launch_active or self.state.stopping + if smoothed: + return AccelerationRequest(self.state.actual_accel, self.state.stopping, self.state.jerk_upper, self.state.jerk_lower) + + # Preserve the faster response to a new braking request, using the last + # actual 50 Hz SCC output rather than integrating at the 100 Hz host rate. + output = float(np.clip(accel, last_sent_accel - DECEL_STEP, last_sent_accel + ACCEL_STEP)) + self.state.desired_accel = accel + self.state.actual_accel = self.state.accel_last = output + self.state.jerk_upper, self.state.jerk_lower = 3.0, 5.0 + self.state.launch_active = False + self.state.stopping = control_state == LongState.stopping + self.state.long_control_state_last = control_state + return AccelerationRequest(output, self.state.stopping, 3.0 if control_state == LongState.pid else 1.0, 5.0) diff --git a/opendbc_repo/opendbc/car/hyundai/ioniq6_media.py b/opendbc_repo/opendbc/car/hyundai/ioniq6_media.py new file mode 100644 index 0000000000..a715d305e8 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/ioniq6_media.py @@ -0,0 +1,79 @@ +"""Receive-only Ioniq 6 wheel media evidence, independent of CAN validity. + +Only exact physical-bus 0x448/8 packets establish a sample. Times come from +the CAN receive timeline; consumers must not compare them to MONOTONIC. +No gesture, saved assignment, effective mode or CAN transmission lives here. +""" + +from dataclasses import dataclass + + +MEDIA_ADDRESS = 0x448 +MEDIA_MAX_GAP_NS = 300_000_000 + + +@dataclass(frozen=True) +class MediaSample: + mode_held: bool + custom_held: bool + source_boot_ns: int + + +@dataclass(frozen=True) +class MediaObservation: + samples: tuple[MediaSample, ...] + parser_boot_ns: int + source_epoch: int + valid: bool + + +class Ioniq6MediaButtons: + """Preserve packet order without counting a cached held bit as new input. + + The recorded Ioniq neutral source runs at about 5 Hz. A 300 ms bound allows + normal cadence but clears a held sequence across a missing source. Press + transitions and their physical cadence still require vehicle qualification. + The caller must restrict construction to the reviewed Ioniq configuration. + """ + + def __init__(self, bus: int): + if type(bus) is not int or not 0 <= bus <= 3: + raise ValueError("physical CAN bus required") + self.bus = bus + self.epoch = 0 + self.last_batch_ns = 0 + self.last_source_ns = 0 + + def _invalid(self, batch_ns: int) -> MediaObservation: + self.epoch += 1 + self.last_source_ns = 0 + self.last_batch_ns = batch_ns + return MediaObservation((), batch_ns, self.epoch, False) + + def update(self, packets) -> MediaObservation: + samples = [] + batch_ns = self.last_batch_ns + source_ns = self.last_source_ns + if not packets: + return self._invalid(batch_ns) + for stamp, frames in packets: + if type(stamp) is not int or stamp <= 0: + return self._invalid(batch_ns) + if stamp < batch_ns: + return self._invalid(stamp) + batch_ns = stamp + for address, data, bus in frames: + if address != MEDIA_ADDRESS or bus != self.bus: + continue + if (type(address) is not int or type(bus) is not int or not isinstance(data, (bytes, bytearray)) or + len(data) != 8 or stamp <= source_ns or + source_ns and stamp - source_ns > MEDIA_MAX_GAP_NS): + return self._invalid(batch_ns) + # Motorola one-bit signals 22 and 44 in STEERING_WHEEL_MEDIA_BUTTONS. + samples.append(MediaSample(bool(data[2] & 0x40), bool(data[5] & 0x10), stamp)) + source_ns = stamp + if source_ns <= 0 or batch_ns - source_ns > MEDIA_MAX_GAP_NS: + return self._invalid(batch_ns) + self.last_batch_ns = batch_ns + self.last_source_ns = source_ns + return MediaObservation(tuple(samples), batch_ns, self.epoch, True) diff --git a/opendbc_repo/opendbc/car/hyundai/kona_aol.py b/opendbc_repo/opendbc/car/hyundai/kona_aol.py new file mode 100644 index 0000000000..1a4d4d24ba --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/kona_aol.py @@ -0,0 +1,27 @@ +"""Exact main-only classic ICE Kona independent lateral identity.""" +from opendbc.car.structs import CarParams +from opendbc.car.hyundai.values import CAR, HyundaiFlags + +# Exact classic namespace, independently distinct from Forte1400/1c00. +KONA_AOL_MARKER = 0x0400 +KONA_STOCK_WORD = 0x1040 +KONA_AOL_WORD = 0x1440 +KONA_AOL_EXPERIENCE = 32 + + +def qualified(cp): + allowed = (HyundaiFlags.NON_SCC | HyundaiFlags.ALT_LIMITS | HyundaiFlags.NON_SCC_RADAR_FCA | + HyundaiFlags.USE_FCA | HyundaiFlags.SEND_LFA) + required = HyundaiFlags.NON_SCC | HyundaiFlags.ALT_LIMITS + return (cp.carFingerprint == CAR.HYUNDAI_KONA_NON_SCC and cp.brand == 'hyundai' and + cp.alternativeExperience in (0, KONA_AOL_EXPERIENCE) and + not cp.passive and not cp.dashcamOnly and not cp.notCar and + not cp.openpilotLongitudinalControl and cp.pcmCruise and + int(cp.flags) & int(required) == int(required) and not int(cp.flags) & ~int(allowed) and + len(cp.safetyConfigs) == 1 and cp.safetyConfigs[0].safetyModel == CarParams.SafetyModel.hyundai and + cp.safetyConfigs[0].safetyParam in (KONA_STOCK_WORD, KONA_AOL_WORD)) + + +def allow_lateral_onset(*, permitted, normal_enabled, steering_pressed, previous_active): + """Preserve original retry-on-grip-release, after actual native permission.""" + return bool(permitted and not (not normal_enabled and not previous_active and steering_pressed)) diff --git a/opendbc_repo/opendbc/car/hyundai/legacy_long_aol.py b/opendbc_repo/opendbc/car/hyundai/legacy_long_aol.py new file mode 100644 index 0000000000..83820e7ad2 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/legacy_long_aol.py @@ -0,0 +1,50 @@ +"""Exact legacy radar takeover and typed-axis configuration.""" +from opendbc.car import structs +from opendbc.car.hyundai.values import CAR, HyundaiFlags +from opendbc.car.hyundai.classic_scc_aol import qualified as stock_qualified + +CARS = frozenset((CAR.GENESIS_G80, CAR.KIA_XCEED_PHEV)) +LEGACY_LONG_AOL_WORDS = frozenset((0xE902, 0xE903, 0xE912, 0xE913)) + + +def ordinary_word(cp): + return (0xE900 if cp.carFingerprint == CAR.GENESIS_G80 else 0xE901) + (0x10 if cp.flags & HyundaiFlags.HAS_LDA_BUTTON else 0) + + +def aol_word(cp): + return ordinary_word(cp) + 2 + + +def topology_owned(cp): + if cp.carFingerprint not in CARS or cp.brand != 'hyundai': + return False + declared = int(CAR[cp.carFingerprint].config.flags) + dynamic = int(HyundaiFlags.HAS_LDA_BUTTON | HyundaiFlags.USE_FCA | HyundaiFlags.SEND_LFA) + return (int(cp.flags) & ~dynamic == declared and not cp.passive and not cp.notCar and not cp.dashcamOnly + and cp.steerControlType == structs.CarParams.SteerControlType.torque and len(cp.safetyConfigs) == 1 + and cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.hyundaiLegacy) + + +def qualified(cp, *, marked_only=False): + if not topology_owned(cp) or not cp.alphaLongitudinalAvailable or not cp.openpilotLongitudinalControl or cp.pcmCruise: + return False + word = int(cp.safetyConfigs[0].safetyParam) + return ((cp.alternativeExperience == 32 and word == aol_word(cp)) or + (not marked_only and cp.alternativeExperience == 0 and word == ordinary_word(cp))) + + +def candidate(stock, *, requested): + if not requested or not stock.alphaLongitudinalAvailable or not topology_owned(stock) or not stock_qualified(stock): + return None + cp = stock.as_reader().as_builder() + cp.openpilotLongitudinalControl = True + cp.pcmCruise = False + if cp.carFingerprint == CAR.GENESIS_G80: + cp.radarUnavailable = True + cp.safetyConfigs[0].safetyParam = ordinary_word(cp) + cp.alternativeExperience = 0 + return cp + + +def stopping_decel_rate(cp): + return 0.8 if qualified(cp) else None diff --git a/opendbc_repo/opendbc/car/hyundai/legacy_long_startup.py b/opendbc_repo/opendbc/car/hyundai/legacy_long_startup.py new file mode 100644 index 0000000000..07fa75923e --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/legacy_long_startup.py @@ -0,0 +1,73 @@ +"""Legacy radar transaction with exact stock fallback before publication.""" +from copy import deepcopy +import time + +from opendbc.car import Bus +from opendbc.can.parser import MAX_BAD_COUNTER +from opendbc.car.hyundai.values import HyundaiFlags +from opendbc.car.hyundai.ecu_startup import HyundaiECUStartup, Outcome +from opendbc.car.hyundai.g90_startup import G90Startup +from opendbc.car.hyundai.legacy_long_aol import candidate, qualified, CARS, ordinary_word, aol_word +from opendbc.car.hyundai.classic_scc_aol import qualified as stock_qualified, stock_word, aol_word as stock_aol_word + + +def required(cp): + return cp.carFingerprint in CARS and cp.openpilotLongitudinalControl + + +class LegacyLongStartup(G90Startup): + def __init__(self, cp, callbacks, *, stock_cp): + expected = candidate(stock_cp, requested=True) + if expected is None or expected.to_dict() != cp.to_dict(): + raise ValueError('Legacy LONG candidate differs from exact stock configuration') + HyundaiECUStartup.__init__(self, cp, callbacks, address=0x7d0, bus=0, label='Legacy LONG') + self.stock_cp = stock_cp.as_reader().as_builder() + + def prepare(self, *, admission): + super().prepare(admission=admission) + if self.outcome in (Outcome.STOCK_UNTOUCHED, Outcome.STOCK_RESTORED): + self.cp = self.stock_cp.as_reader().as_builder() + self.prepared_cp = deepcopy(self.cp.to_dict()) + elif self.outcome is Outcome.SENT_UNCONFIRMED: + self.floor_ns = time.clock_gettime_ns(time.CLOCK_BOOTTIME) + return self.cp + + def sources_current(self, ci, now_ns): + if self.outcome is not Outcome.SENT_UNCONFIRMED: + return True + if not ci.CS.out.canValid or ci.CS.out.canTimeout: + return False + parser = ci.can_parsers[Bus.pt] + gas = 'E_EMS11' if ci.CP.flags & HyundaiFlags.HYBRID else 'EMS16' + for name in (gas, 'WHL_SPD11', 'TCS13', 'MDPS12', 'CLU11'): + source = parser.message_states.get(parser.dbc.name_to_msg[name].address) + if (source is None or not source.timestamps or source.counter_fail >= MAX_BAD_COUNTER + or not self.floor_ns < int(source.timestamps[-1]) <= now_ns + or now_ns - int(source.timestamps[-1]) > source.timeout_threshold): + return False + return True + + def before_control(self, *, configured, sources_current, control_current): + self.check() + return self.outcome is not Outcome.SENT_UNCONFIRMED or (configured and sources_current and control_current) + + def finalize_aol_configuration(self, ci): + if self.published or self.closed or ci is not self.ci or not self.ready: + raise RuntimeError('Legacy LONG AOL finalization requires configured startup') + if self.prepared_for(ci.CP): + return + if self.outcome is Outcome.SENT_UNCONFIRMED and qualified(ci.CP, marked_only=True): + before_word, after_word = ordinary_word(ci.CP), aol_word(ci.CP) + elif self.outcome in (Outcome.STOCK_UNTOUCHED, Outcome.STOCK_RESTORED) and stock_qualified(ci.CP, marked_only=True): + before_word, after_word = stock_word(ci.CP), stock_aol_word(ci.CP) + else: + raise RuntimeError('Legacy startup does not admit this AOL configuration') + expected = deepcopy(self.prepared_cp) + config = expected['safetyConfigs'][0] + if config['safetyModel'] != 'hyundaiLegacy' or config['safetyParam'] != before_word or expected['alternativeExperience'] != 0: + raise RuntimeError('Legacy prepared profile cannot be finalized') + config['safetyParam'] = after_word + expected['alternativeExperience'] = 32 + if ci.CP.to_dict() != expected: + raise RuntimeError('Legacy AOL finalization changed unrelated CarParams') + self.prepared_cp = expected diff --git a/opendbc_repo/opendbc/car/hyundai/longitudinal_mode.py b/opendbc_repo/opendbc/car/hyundai/longitudinal_mode.py new file mode 100644 index 0000000000..afc1e59fd9 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/longitudinal_mode.py @@ -0,0 +1,43 @@ +"""Reached HKG acceleration PID and ACC/blended transition shaping.""" +import numpy as np + + +class HKGModePolicy: + def init_mode(self, dt): + self.dt = dt + self.prev_mode = 'acc' + self.current_mode = 'acc' + self.mode_timer = 0.0 + self.transitioning = False + + def prepare_pid(self, pid, a_target, error, CS, context): + if type(context.experimental_mode) is bool: + mode = 'blended' if context.experimental_mode else 'acc' + if mode != self.current_mode: + self.prev_mode = self.current_mode + self.current_mode = mode + self.mode_timer = 0.0 + self.transitioning = True + if self.transitioning: + self.mode_timer += self.dt + if self.mode_timer >= 1.0: + self.transitioning = False + if (pid.i > 0.0 and a_target < -.05 and error < -.25 and + not (CS.vEgo <= .35 and a_target > -.40)): + pid.i *= float(np.interp(abs(error), [.25, .75, 1.5], [.55, .25, 0.])) + # Source CP KF defaults to zero, which original LoC interpreted as gain1. + return a_target, self.transitioning and self.prev_mode == 'blended' and self.current_mode == 'acc' + + def shape_output(self, output, a_target, error, CS, last_output): + if (output > 0.0 and a_target < -.10 and error < -.35 and + not (CS.vEgo <= .35 and a_target > -.40)): + output = min(output, float(np.interp(a_target, [-1.5, -.6, -.1], [0., 0., .05]))) + if self.transitioning and self.prev_mode == 'blended' and self.current_mode == 'acc': + if output > last_output: + output = last_output + (output - last_output) * min(1., self.mode_timer) + elif self.transitioning and self.prev_mode == 'acc' and self.current_mode == 'blended': + if output < 0.0 and output < last_output: + urgency = min(1., abs(output / -4.) ** .4) + blend = 1. - (1. - min(1., self.mode_timer)) * (1. - urgency) + output = last_output + (output - last_output) * blend + return output diff --git a/opendbc_repo/opendbc/car/hyundai/model_startup.py b/opendbc_repo/opendbc/car/hyundai/model_startup.py new file mode 100644 index 0000000000..531daa8144 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/model_startup.py @@ -0,0 +1,17 @@ +"""Vehicle startup evidence for external model hardware.""" + +from opendbc.car.hyundai.hyundaicanfd import CanBus +from opendbc.car.hyundai.values import CAR + + +def model_power_ready_bus(CP) -> int | None: + # This car briefly interrupts accessory power when transitioning into READY. + return CanBus(CP).ECAN if CP.carFingerprint == CAR.HYUNDAI_IONIQ_6 else None + + +def model_power_ready(messages, bus: int) -> bool | None: + ready = None + for message in messages: + if message.address == 0x35 and message.src == bus and len(message.dat) == 32: + ready = bool(message.dat[3] & 0x40) + return ready diff --git a/opendbc_repo/opendbc/car/hyundai/non_scc_aol.py b/opendbc_repo/opendbc/car/hyundai/non_scc_aol.py new file mode 100644 index 0000000000..a181e92b2c --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/non_scc_aol.py @@ -0,0 +1,102 @@ +"""Exact source-owned classic NON_SCC lateral profiles. + +Gas and steering limits remain those of each declared platform. Ray's pedal +and refresh-message topology is deliberately handled by its separate owner. +""" +import time + +from opendbc.car import structs +from opendbc.car.hyundai.values import CAR, HyundaiFlags, HyundaiSafetyFlags + +NON_SCC_IDS = frozenset((CAR.HYUNDAI_BAYON_1ST_GEN_NON_SCC, CAR.HYUNDAI_ELANTRA_2022_NON_SCC, + CAR.HYUNDAI_ELANTRA_HEV_2022_NON_SCC, CAR.HYUNDAI_KONA_NON_SCC, CAR.HYUNDAI_KONA_EV_NON_SCC, + CAR.KIA_CEED_PHEV_2022_NON_SCC, CAR.KIA_FORTE_2019_NON_SCC, CAR.KIA_FORTE_2021_NON_SCC, + CAR.KIA_SELTOS_2023_NON_SCC, CAR.GENESIS_G70_2021_NON_SCC)) +AOL_MARKER = 0x0400 +AOL_EXPERIENCE = 32 +AOL_WORDS = frozenset((0x1400, 0x1c00, 0x1440, 0x1c40, 0x1441, 0x1c41, 0x1402, 0x1c02)) + + +def stock_word(cp): + word = int(HyundaiSafetyFlags.NON_SCC) + for flag, safety in ((HyundaiFlags.EV, HyundaiSafetyFlags.EV_GAS), + (HyundaiFlags.HYBRID, HyundaiSafetyFlags.HYBRID_GAS), + (HyundaiFlags.ALT_LIMITS, HyundaiSafetyFlags.ALT_LIMITS)): + if cp.flags & flag: + word |= int(safety) + if cp.flags & HyundaiFlags.HAS_LDA_BUTTON: + word |= int(HyundaiSafetyFlags.HAS_LDA_BUTTON) + return word + + +def qualified(cp): + if cp.carFingerprint not in NON_SCC_IDS or cp.brand != 'hyundai': + return False + declared = int(CAR[cp.carFingerprint].config.flags) + identity_mask = int(HyundaiFlags.NON_SCC | HyundaiFlags.EV | HyundaiFlags.HYBRID | HyundaiFlags.ALT_LIMITS | + HyundaiFlags.ALT_LIMITS_2 | HyundaiFlags.CANFD) + allowed = declared | int(HyundaiFlags.HAS_LDA_BUTTON | HyundaiFlags.USE_FCA | HyundaiFlags.SEND_LFA | + HyundaiFlags.NON_SCC_RADAR_FCA | HyundaiFlags.NON_SCC_NO_FCA) + if (int(cp.flags) & identity_mask != declared & identity_mask or int(cp.flags) & ~allowed or + cp.passive or cp.dashcamOnly or cp.notCar or cp.openpilotLongitudinalControl or not cp.pcmCruise or + cp.alternativeExperience not in (0, AOL_EXPERIENCE) or len(cp.safetyConfigs) != 1): + return False + safety = cp.safetyConfigs[0] + word = stock_word(cp) + # The observed alternative CLU source is marked only at the AOL boundary; + # ordinary safety may still use its original MAIN-only profile. + ordinary = word & ~int(HyundaiSafetyFlags.HAS_LDA_BUTTON) + return (safety.safetyModel == structs.CarParams.SafetyModel.hyundai and + safety.safetyParam in (word, ordinary, word | AOL_MARKER)) + + +def aol_word(cp): + return stock_word(cp) | AOL_MARKER + + +SOURCE_MAX_AGE_NS = 300_000_000 + + +class NonSccLkasSources: + """Fuse observed alternative sources without replaying held startup input.""" + def __init__(self): + self.sources = {} + self.neutral_seen = False + self.held = False + self.edges = [] + + def update(self, can_packets): + self.edges = [] + try: + now = time.clock_gettime_ns(time.CLOCK_BOOTTIME) + except (AttributeError, OSError): + self.sources.clear() + self.neutral_seen = False + self.held = False + return + current = {address: value for address, value in self.sources.items() if 0 <= now - value[0] <= SOURCE_MAX_AGE_NS} + if len(current) != len(self.sources): + # Expiry is loss of evidence, never a manufactured release or press. + self.neutral_seen = False + self.held = any(value[1] for value in current.values()) + self.sources = current + for stamp, packets in can_packets: + if type(stamp) is not int or not 0 < stamp <= now or now - stamp > SOURCE_MAX_AGE_NS: + continue + for address, data, bus in packets: + if bus != 0 or address not in (0x391, 0x50c) or len(data) != 8: + continue + previous = self.sources.get(address) + if previous is not None and stamp <= previous[0]: + continue + pressed = bool(data[0] & 0x10) if address == 0x391 else bool(data[7] & 1) + source_neutral = (previous[2] if previous is not None else False) or not pressed + self.sources[address] = (stamp, pressed, source_neutral) + held = any(value[1] for value in self.sources.values()) + if not held and not self.neutral_seen: + self.neutral_seen = True + if not self.held: + self.edges.append(False) + if held != self.held and self.neutral_seen and (not held or source_neutral): + self.edges.append(held) + self.held = held diff --git a/opendbc_repo/opendbc/car/hyundai/radar_interface.py b/opendbc_repo/opendbc/car/hyundai/radar_interface.py index a32f2f7d11..f49b5d3b48 100644 --- a/opendbc_repo/opendbc/car/hyundai/radar_interface.py +++ b/opendbc_repo/opendbc/car/hyundai/radar_interface.py @@ -3,27 +3,84 @@ import math from opendbc.can import CANParser from opendbc.car import Bus, structs from opendbc.car.interfaces import RadarInterfaceBase -from opendbc.car.hyundai.values import DBC +from opendbc.car.hyundai.values import (CAR, DBC, HyundaiFlags, HYUNDAI_MRR35_RADAR_DBC, HYUNDAI_MRR30_RADAR_DBC, + HYUNDAI_GV70_RADAR_DBC, HYUNDAI_G90_RADAR_DBC, is_blended) RADAR_START_ADDR = 0x500 RADAR_MSG_COUNT = 32 +G90_RADAR_MSG_COUNT = 64 +G90_RADAR_CYCLE_NS = 20_000_000 +MRREVO14F_RADAR_START_ADDR = 0x602 +MRREVO14F_RADAR_MSG_COUNT = 16 +MRREVO14F_RADAR_CYCLE_NS = 20_000_000 # track messages are published at 50 Hz +MRR30_RADAR_START_ADDR = 0x210 +MRR30_RADAR_MSG_COUNT = 16 +MRR30_RADAR_CYCLE_NS = 20_000_000 +GV70_RADAR_CYCLE_NS = 50_000_000 +MRR35_RADAR_START_ADDR = 0x3A5 +MRR35_RADAR_MSG_COUNT = 32 +MRR35_RADAR_CYCLE_NS = 50_000_000 # 20 Hz + + +def blended_track_radar(CP) -> bool: + # Exact mixed classic PT / HDA-II steering identity, independently observed + # by the existing factory. Never relabel another classic or CANFD car. + return bool(CP.brand == 'hyundai' and is_blended(CP) and CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG) + + +def radar_dbc(CP): + return HYUNDAI_GV70_RADAR_DBC if blended_track_radar(CP) else DBC[CP.carFingerprint].get(Bus.radar) + + +def radar_bus(CP) -> int: + # Frozen Ioniq 6 camera-SCC wiring exposes the MRR35 object bank on bus 1; + # its LKA/HDA-II wiring and the other MRR35 cars expose it on bus 0. + if CP.carFingerprint == CAR.HYUNDAI_IONIQ_6 and CP.flags & HyundaiFlags.CANFD_CAMERA_SCC: + return 1 + return 0 # POC for parsing corner radars: https://github.com/commaai/openpilot/pull/24221/ def get_radar_can_parser(CP): - if Bus.radar not in DBC[CP.carFingerprint]: + if radar_dbc(CP) is None: return None - messages = [(f"RADAR_TRACK_{addr:x}", 50) for addr in range(RADAR_START_ADDR, RADAR_START_ADDR + RADAR_MSG_COUNT)] - return CANParser(DBC[CP.carFingerprint][Bus.radar], messages, 1) + start, count = radar_message_range(CP) + mrr35 = radar_dbc(CP) == HYUNDAI_MRR35_RADAR_DBC + g90 = radar_dbc(CP) == HYUNDAI_G90_RADAR_DBC + gv70 = radar_dbc(CP) == HYUNDAI_GV70_RADAR_DBC + messages = [(f"RADAR_TRACK_{addr:x}", + 20 if mrr35 and addr == start + count - 1 else + float("nan") if mrr35 or g90 and addr >= start + RADAR_MSG_COUNT else 20 if gv70 else 50) for addr in range(start, start + count)] + mrr30 = radar_dbc(CP) == HYUNDAI_MRR30_RADAR_DBC + return CANParser(radar_dbc(CP), messages, radar_bus(CP) if mrr35 or mrr30 or gv70 else 1) + + +def radar_message_range(CP) -> tuple[int, int]: + if radar_dbc(CP) == HYUNDAI_G90_RADAR_DBC: + return RADAR_START_ADDR, G90_RADAR_MSG_COUNT + if radar_dbc(CP) in (HYUNDAI_MRR30_RADAR_DBC, HYUNDAI_GV70_RADAR_DBC): + return MRR30_RADAR_START_ADDR, MRR30_RADAR_MSG_COUNT + if radar_dbc(CP) == HYUNDAI_MRR35_RADAR_DBC: + return MRR35_RADAR_START_ADDR, MRR35_RADAR_MSG_COUNT + if radar_dbc(CP) == "hyundai_mrrevo14f_radar_generated": + return MRREVO14F_RADAR_START_ADDR, MRREVO14F_RADAR_MSG_COUNT + return RADAR_START_ADDR, RADAR_MSG_COUNT class RadarInterface(RadarInterfaceBase): def __init__(self, CP): super().__init__(CP) self.updated_messages = set() - self.trigger_msg = RADAR_START_ADDR + RADAR_MSG_COUNT - 1 + self.start_addr, self.msg_count = radar_message_range(CP) + self.g90 = radar_dbc(CP) == HYUNDAI_G90_RADAR_DBC + self.mrrevo14f = self.start_addr == MRREVO14F_RADAR_START_ADDR + self.mrr35 = self.start_addr == MRR35_RADAR_START_ADDR + self.gv70 = radar_dbc(CP) == HYUNDAI_GV70_RADAR_DBC + self.mrr30 = self.start_addr == MRR30_RADAR_START_ADDR and not self.gv70 + self.last_mrr35_trigger_ns = 0 + self.trigger_msg = self.start_addr + (RADAR_MSG_COUNT if self.g90 else self.msg_count) - 1 self.radar_off_can = CP.radarUnavailable self.rcp = get_radar_can_parser(CP) @@ -32,12 +89,26 @@ class RadarInterface(RadarInterfaceBase): if self.radar_off_can or (self.rcp is None): return super().update(None) + if (self.g90 or self.mrr30 or self.gv70 or self.mrr35 and self.CP.carFingerprint == CAR.HYUNDAI_IONIQ_6) and can_strings: + expected_length = 8 if self.g90 else 32 if self.mrr30 or self.gv70 else 24 + entries = [can_strings] if not isinstance(can_strings[0], list | tuple) else can_strings + can_strings = [(timestamp, [(addr, data, bus) for addr, data, bus in frames + if not (bus == self.rcp.bus and self.start_addr <= addr < self.start_addr + self.msg_count and len(data) != expected_length)]) + for timestamp, frames in entries] + vls = self.rcp.update(can_strings) self.updated_messages.update(vls) if self.trigger_msg not in self.updated_messages: return None + if self.mrr35: + trigger_ns = self.rcp.ts_nanos[self.trigger_msg]["LONG_DIST"] + if trigger_ns <= self.last_mrr35_trigger_ns: + self.updated_messages.clear() + return None + self.last_mrr35_trigger_ns = trigger_ns + rr = self._update(self.updated_messages) self.updated_messages.clear() @@ -51,9 +122,76 @@ class RadarInterface(RadarInterfaceBase): if not self.rcp.can_valid: ret.errors.canError = True - for addr in range(RADAR_START_ADDR, RADAR_START_ADDR + RADAR_MSG_COUNT): + for addr in range(self.start_addr, self.start_addr + self.msg_count): msg = self.rcp.vl[f"RADAR_TRACK_{addr:x}"] + if self.mrr30 or self.gv70: + trigger_time = self.rcp.ts_nanos[self.trigger_msg]["1_LONG_DIST"] + source_time = self.rcp.ts_nanos[addr]["1_LONG_DIST"] + cycle_ns = GV70_RADAR_CYCLE_NS if self.gv70 else MRR30_RADAR_CYCLE_NS + fresh = addr in updated_messages and 0 <= trigger_time - source_time <= cycle_ns + for index in (1, 2): + track_key = addr * 2 + index - 1 + if fresh and msg[f"{index}_STATE"] in (3, 4): + point = self.pts.get(track_key) + if point is None: + point = structs.RadarData.RadarPoint() + point.trackId = self.track_id + self.track_id += 1 + self.pts[track_key] = point + point.dRel = msg[f"{index}_LONG_DIST"] + point.yRel = msg[f"{index}_LAT_DIST"] + point.vRel = msg[f"{index}_REL_SPEED"] + elif track_key in self.pts: + del self.pts[track_key] + continue + + if self.mrr35: + trigger_ns = self.last_mrr35_trigger_ns + source_ns = self.rcp.ts_nanos[addr]["LONG_DIST"] + fresh = addr in updated_messages and 0 <= trigger_ns - source_ns <= MRR35_RADAR_CYCLE_NS + if fresh and msg["STATE"] in (3, 4): + point = self.pts.get(addr) + if point is None: + point = structs.RadarData.RadarPoint() + point.trackId = self.track_id + self.track_id += 1 + self.pts[addr] = point + point.dRel = msg["LONG_DIST"] + point.yRel = msg["LAT_DIST"] + point.vRel = msg["REL_SPEED"] + elif addr in self.pts: + del self.pts[addr] + continue + + if self.mrrevo14f: + trigger_time = self.rcp.ts_nanos[self.trigger_msg]["1_DISTANCE"] + source_time = self.rcp.ts_nanos[addr]["1_DISTANCE"] + fresh = addr in updated_messages and 0 <= trigger_time - source_time <= MRREVO14F_RADAR_CYCLE_NS + for index in (1, 2): + track_key = addr * 2 + index - 1 + valid = fresh and msg[f"{index}_DISTANCE"] != 255.75 + if valid: + point = self.pts.get(track_key) + if point is None: + point = structs.RadarData.RadarPoint() + point.trackId = self.track_id + self.track_id += 1 + self.pts[track_key] = point + point.dRel = msg[f"{index}_DISTANCE"] + point.yRel = msg[f"{index}_LATERAL"] + point.vRel = msg[f"{index}_SPEED"] + elif track_key in self.pts: + del self.pts[track_key] + continue + + if self.g90 and addr >= self.start_addr + RADAR_MSG_COUNT: + trigger_time = self.rcp.ts_nanos[self.trigger_msg]["LONG_DIST"] + source_time = self.rcp.ts_nanos[addr]["LONG_DIST"] + if addr not in updated_messages or not 0 <= trigger_time - source_time <= G90_RADAR_CYCLE_NS: + self.pts.pop(addr, None) + continue + if addr not in self.pts: self.pts[addr] = structs.RadarData.RadarPoint() self.pts[addr].trackId = self.track_id diff --git a/opendbc_repo/opendbc/car/hyundai/ray_pedal.py b/opendbc_repo/opendbc/car/hyundai/ray_pedal.py new file mode 100644 index 0000000000..c3eceaf7bf --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/ray_pedal.py @@ -0,0 +1,91 @@ +import numpy as np +from opendbc.car import rate_limit + +RAY_PEDAL_COMMAND_CAP = 0.70 +RAY_PEDAL_CAP_SPEED_BP = [0.0, 12.0, 20.0, 25.0] +RAY_PEDAL_CAP_V = [0.55, 0.55, 0.65, RAY_PEDAL_COMMAND_CAP] +RAY_PEDAL_OFFSET_SPEED_BP = [0.0, 2.0, 4.0, 8.0, 12.0, 16.0, 20.0, 25.0] +RAY_PEDAL_OFFSET_V = [0.08, 0.13, 0.20, 0.32, 0.42, 0.51, 0.565, 0.60] +RAY_PEDAL_ACCEL_GAIN = 0.22 +RAY_PEDAL_DECEL_GAIN = 0.55 +RAY_PEDAL_RATE_UP = 0.02 +RAY_PEDAL_RATE_DOWN = 0.06 +RAY_PEDAL_OVERSPEED_CUTOFF = 0.5 +RAY_PEDAL_TAPER_BELOW_TARGET = 0.75 + + +def ray_pedal_enabled(CP): + from opendbc.car.hyundai.values import CAR, RAY_PEDAL_SAFETY_PARAM + from opendbc.car import structs + return (CP.brand == "hyundai" and CP.carFingerprint == CAR.KIA_RAY_EV and CP.openpilotLongitudinalControl and + not CP.passive and not CP.dashcamOnly and not CP.notCar and len(CP.safetyConfigs) == 1 and + CP.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.hyundai and + CP.safetyConfigs[0].safetyParam == RAY_PEDAL_SAFETY_PARAM) + + +def pedal_checksum(address, sig, data): + crc = 0xFF + for value in reversed(data[:-1]): + crc ^= value + for _ in range(8): + crc = ((crc << 1) ^ 0xD5) & 0xFF if crc & 0x80 else (crc << 1) & 0xFF + return crc + + +def create_ray_pedal_command(packer, gas_amount, idx): + enable = gas_amount > 0.001 + values = {"ENABLE": enable, "COUNTER_PEDAL": idx & 0xF} + if enable: + values.update(GAS_COMMAND=gas_amount * 255., GAS_COMMAND2=gas_amount * 255.) + return packer.make_can_msg("GAS_COMMAND", 0, values) + + +def get_ray_pedal_target(accel, speed, set_speed): + if not all(np.isfinite(x) for x in (accel, speed, set_speed)) or not 1.0 <= set_speed <= 40.0: + return 0.0 + speed_error = set_speed - speed + if speed_error <= -RAY_PEDAL_OVERSPEED_CUTOFF: + return 0.0 + pedal_offset = float(np.interp(speed, RAY_PEDAL_OFFSET_SPEED_BP, RAY_PEDAL_OFFSET_V)) + pedal_cap = float(np.interp(speed, RAY_PEDAL_CAP_SPEED_BP, RAY_PEDAL_CAP_V)) + gain = RAY_PEDAL_DECEL_GAIN if accel < 0.0 else RAY_PEDAL_ACCEL_GAIN + target = float(np.clip(pedal_offset + accel * gain, 0.0, pedal_cap)) + if speed_error < 0.0: + target *= float(np.clip(0.65 * (1.0 + speed_error / RAY_PEDAL_OVERSPEED_CUTOFF), 0.0, 1.0)) + elif speed_error < RAY_PEDAL_TAPER_BELOW_TARGET: + target *= 0.65 + 0.35 * speed_error / RAY_PEDAL_TAPER_BELOW_TARGET + return target + + +def ray_pedal_gas(last, speed, accel, set_speed): + target = get_ray_pedal_target(accel, speed, set_speed) + if target <= 0.001: + return 0.0 + return min(rate_limit(target, last, -RAY_PEDAL_RATE_DOWN, RAY_PEDAL_RATE_UP), target) + + +class RayPedalLongitudinalPolicy: + """Own the selected Ray proportional gains and consume its supported factory integral gains.""" + + friction_variant = False + stopping_decel_rate = 1.0 + + def __init__(self, cp): + self.kp = ((0.0, 5.0, 15.0, 35.0), (0.095, 0.085, 0.065, 0.050)) + self.ki = (tuple(cp.longitudinalTuning.kiBP), tuple(cp.longitudinalTuning.kiV)) + + def reset(self): + pass + + def feedforward(self, target, _speed, _last_output): + return target + + def prepare_pid(self, _pid, _target, _error, _speed, _last_output, _limits, *, should_stop, has_lead): + return False + + def shape_output(self, output, _target, _error, _speed): + return output + + +def longitudinal_policy_for(cp): + return RayPedalLongitudinalPolicy(cp) if ray_pedal_enabled(cp) else None diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_blended_carstate.py b/opendbc_repo/opendbc/car/hyundai/tests/test_blended_carstate.py new file mode 100644 index 0000000000..37c6d5d8f6 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_blended_carstate.py @@ -0,0 +1,69 @@ +import unittest + +from opendbc.can import CANPacker +from opendbc.car import Bus, structs +from opendbc.car.hyundai.carstate import CarState +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.tests.test_palisade_2023 import params +from opendbc.car.hyundai.values import Buttons, DBC + + +def alpha_state(topology): + cp = params(topology) + cp.openpilotLongitudinalControl = True + cp.pcmCruise = False + cp.safetyConfigs[0].safetyParam |= 4 + state = CarState(cp) + return state, state.get_can_parsers(cp), CANPacker(DBC[cp.carFingerprint][Bus.pt]) + + +def feed(state, parsers, packer, tick, *, request=0, available=True, button=Buttons.NONE, omit=None): + frames = [] + for parser in parsers.values(): + for address in parser.addresses: + message = parser.dbc.addr_to_msg[address] + if message.name in ('SCC11', 'SCC12') or message.name == omit: + continue + values = {'COUNTER': tick % 16} if 'COUNTER' in message.sigs else {} + if message.name == 'TCS13': + values.update(ACCEnable=0 if available else 1, ACC_REQ=request) + elif message.name == 'CLU11': + values['CF_Clu_CruiseSwState'] = button + frames.append(packer.make_can_msg(message.name, parser.bus, values)) + for parser in parsers.values(): + parser.update([(1_000_000_000 + tick * 10_000_000, frames)]) + result = state.update(parsers) + state.out = result + return result + + +class TestBlendedCarState(unittest.TestCase): + def test_alpha_disabled_scc_optional_and_independent_sources_required(self): + for topology in ('hdai', 'hdaii'): + state, parsers, packer = alpha_state(topology) + pt = parsers[Bus.pt] + self.assertNotIn(0x420, pt.message_states) + if topology == 'hdaii': + self.assertTrue(pt.message_states[0x421].ignore_alive) + else: + self.assertNotIn(0x421, pt.message_states) + for tick in range(12): + result = feed(state, parsers, packer, tick, request=1) + self.assertTrue(pt.can_valid) + self.assertTrue(result.cruiseState.available and result.cruiseState.enabled) + self.assertFalse(result.cruiseState.standstill or result.cruiseState.nonAdaptive) + for tick in range(12, 75): + feed(state, parsers, packer, tick, omit='TCS13') + _ = pt.can_valid # The interface samples health on every source update. + self.assertFalse(pt.can_valid) + + + def test_stock_parser_and_cancel_behavior_unchanged(self): + for topology in ('hdai', 'hdaii'): + cp = params(topology) + ci = CarInterface(cp) + for address in (0x420, 0x421): + self.assertEqual(ci.can_parsers[Bus.pt].message_states[address].frequency, 50) + ci.CS.out.cruiseState.enabled = False + events = ci.CS.create_cruise_button_events(Buttons.CANCEL, Buttons.NONE) + self.assertEqual(events[0].type, structs.CarState.ButtonEvent.Type.cancel) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_blended_sender.py b/opendbc_repo/opendbc/car/hyundai/tests/test_blended_sender.py new file mode 100644 index 0000000000..380391ca00 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_blended_sender.py @@ -0,0 +1,170 @@ +"""Whole candidate sender contract under scripted diagnostic ACKs, never physical takeover.""" + +import unittest +from unittest.mock import patch + +from opendbc.car import Bus, gen_empty_fingerprint, structs +from opendbc.car.hyundai.blended_disable_ecu import confirm_disable_ecu +from opendbc.car.hyundai.blended_longitudinal import BlendedLongitudinalOwner, BlendedLongitudinalController, candidate_from_stock +from opendbc.car.hyundai.hyundaican import hyundai_checksum +from opendbc.car.hyundai.hyundaicanfd import CanBus +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.tests.test_palisade_2023 import feed +from opendbc.car.hyundai.values import CAR, HyundaiFlags + + +class PositiveQuery: + def __init__(self, *args, **kwargs): + self.address = args[3][0] + assert (args[4], args[5]) in (([b'\x11\x01'], [b'']), ([b'\x10\x03'], [b'\x50\x03']), ([b'\x28\x03\x01'], [b'\x68\x03'])) + + def get_data(self, timeout): + return {self.address: b''} + + +def selected_interface(hdaii, fca=False): + fingerprint = gen_empty_fingerprint() + if hdaii: + fingerprint[2][0x50] = 16 + if fca: + fingerprint[2][0x38D] = 8 + stock = CarInterface.get_params(CAR.HYUNDAI_PALISADE_2023, fingerprint, [], False, False, False) + cp = candidate_from_stock(stock, alpha_requested=True, native_qualified=True) + assert bool(cp.flags & HyundaiFlags.USE_FCA) == fca + ci = CarInterface(cp) + for tick in range(16): + feed(ci, tick) + owner = BlendedLongitudinalOwner(cp, CanBus(cp), confirm_disable_ecu) + with patch('opendbc.car.hyundai.blended_disable_ecu.IsoTpParallelQuery', PositiveQuery), patch('opendbc.car.hyundai.blended_disable_ecu.time.sleep'): + result = owner.begin(list, lambda frames: None, admission=True, unpublished=True) + assert result.admissible and owner.active() + ci.CC.blended_longitudinal = BlendedLongitudinalController(owner, ci.CC.packer, ci.CC.CAN) + # Original steady-loop cadence; startup handoff is independently qualified. + ci.CC.blended_longitudinal.normal_sent = True + return ci + + +def decode(ci, address, data, name): + signal = ci.CC.packer.dbc.addr_to_msg[address].sigs[name] + assert signal.is_little_endian + value = (int.from_bytes(data, 'little') >> signal.start_bit) & ((1 << signal.size) - 1) + if signal.is_signed and value & (1 << (signal.size - 1)): + value -= 1 << signal.size + return value * signal.factor + signal.offset + + +class TestBlendedSender(unittest.TestCase): + def test_whole_controller_order_cadence_crc_and_hda2_mirror(self): + for hdaii, fca in ((False, False), (True, False), (True, True)): + with self.subTest(hdaii=hdaii, fca=fca): + ci = selected_interface(hdaii, fca) + cp = ci.CP + self.assertEqual(ci.can_parsers[Bus.pt].bus, int(hdaii)) + command = structs.CarControl() + command.enabled = command.latActive = command.longActive = True + command.actuators.torque = 0.5 + command.actuators.accel = 3.5 + command.actuators.longControlState = structs.CarControl.Actuators.LongControlState.pid + command.hudControl.setSpeed = 25.0 + for frame in range(32): + actuators, messages = ci.apply(command.as_reader(), 2_000_000_000 + frame * 10_000_000) + wanted = [] + if frame % 100 == 0: + wanted.append((0x730 if hdaii else 0x7D0, int(hdaii))) + wanted.extend([(0x50, 0), (0x340, 1)] if hdaii else [(0x340, 0), (0x364, 0)]) + if hdaii and frame % 5 == 0: + wanted.append((0x2A4, 0)) + if hdaii: + wanted.append((0x51, 0)) + if frame % 2 == 0: + wanted.append((0x363, int(hdaii))) + if frame % 5 == 0: + wanted.extend((address, int(hdaii)) for address in ((0x398, 0x399, 0x39A, 0x39B, 0x39C) if hdaii else (0x398,))) + if hdaii and frame % 20 == 0: + wanted.append((0x43A, 1)) + if frame % 2 == 0: + wanted.extend((address, int(hdaii)) for address in (0x420, 0x421, 0x389)) + if fca: + wanted.append((0x38D, 1)) + if hdaii and frame % 5 == 0: + wanted.append((0x485, 1)) + self.assertEqual([(address, bus) for address, _, bus in messages], wanted) + packets = {address: data for address, data, _ in messages} + for address, data, _ in messages: + if address not in (0x730, 0x7D0, 0x50, 0x51, 0x2A4): + self.assertEqual(data[0], hyundai_checksum(data[1:8]), hex(address)) + self.assertAlmostEqual(actuators.accel, 3.5) + if hdaii: + self.assertNotIn(0x364, packets) + self.assertEqual(decode(ci, 0x50, packets[0x50], 'TORQUE_REQUEST'), actuators.torqueOutputCan) + self.assertEqual(decode(ci, 0x340, packets[0x340], 'CR_Lkas_StrToqReq'), actuators.torqueOutputCan) + self.assertEqual(decode(ci, 0x50, packets[0x50], 'STEER_REQ'), decode(ci, 0x340, packets[0x340], 'CF_Lkas_ActToi')) + self.assertEqual(decode(ci, 0x340, packets[0x340], 'CF_Lkas_MsgCount'), frame % 15) + if frame % 2 == 0: + self.assertAlmostEqual(decode(ci, 0x420, packets[0x420], 'aReqRaw'), 3.5) + self.assertFalse(cp.passive) + + def test_hda2_mirror_fault_suppression_and_disengage_icon_boundary(self): + ci = selected_interface(True) + command = structs.CarControl() + command.enabled = command.latActive = True + command.actuators.torque = 0.5 + ci.CS.out = ci.CS.out.as_reader().as_builder() + ci.CS.out.steeringAngleDeg = 85.0 + ci.CS.out = ci.CS.out.as_reader() + requests = [] + for frame in range(94): + _, messages = ci.apply(command.as_reader(), 2_000_000_000 + frame * 10_000_000) + packets = {address: data for address, data, _ in messages} + primary = decode(ci, 0x50, packets[0x50], 'STEER_REQ') + mirror = decode(ci, 0x340, packets[0x340], 'CF_Lkas_ActToi') + self.assertEqual(primary, mirror) + requests.append(bool(primary)) + self.assertEqual(requests[88:92], [True, False, False, True]) + command.enabled = command.latActive = False + for frame in range(94, 195): + _, messages = ci.apply(command.as_reader(), 2_000_000_000 + frame * 10_000_000) + packets = {address: data for address, data, _ in messages} + expected = 3 if frame - 93 < 100 else 1 + self.assertEqual(decode(ci, 0x50, packets[0x50], 'LKA_ICON'), expected) + self.assertEqual(decode(ci, 0x340, packets[0x340], 'CF_Lkas_FcwOpt_USM'), 2 if expected == 3 else 1) + + def test_actual_mixed_topology_metadata_correct_bus_and_wrong_bus(self): + for hdaii in (False, True): + for observation_bus in (0, 1, 2): + with self.subTest(hdaii=hdaii, observation_bus=observation_bus): + fingerprint = gen_empty_fingerprint() + if hdaii: + fingerprint[2][0x50] = 16 + fingerprint[observation_bus][0x38D] = 8 + fingerprint[observation_bus][0x58B] = 8 + stock = CarInterface.get_params(CAR.HYUNDAI_PALISADE_2023, fingerprint, [], False, False, False) + expected_fca = observation_bus in (int(hdaii), 2) + self.assertEqual(bool(stock.flags & HyundaiFlags.USE_FCA), expected_fca) + self.assertEqual(stock.deprecated.enableBsm, observation_bus == int(hdaii)) + candidate = candidate_from_stock(stock, alpha_requested=True, native_qualified=True) + if not hdaii and expected_fca: + self.assertIsNone(candidate) + else: + self.assertIsNotNone(candidate) + self.assertEqual(bool(candidate.flags & HyundaiFlags.USE_FCA), expected_fca) + self.assertEqual(candidate.deprecated.enableBsm, stock.deprecated.enableBsm) + + def test_mixed_standstill_metadata_retains_original_bus_zero(self): + for hdaii in (False, True): + for observation_bus in (0, 1): + with self.subTest(hdaii=hdaii, observation_bus=observation_bus): + fingerprint = gen_empty_fingerprint() + if hdaii: + fingerprint[2][0x50] = 16 + fingerprint[observation_bus][0x2AA] = 8 + stock = CarInterface.get_params(CAR.HYUNDAI_PALISADE_2023, fingerprint, [], False, False, False) + self.assertEqual(stock.steerAtStandstill, observation_bus == 0) + candidate = candidate_from_stock(stock, alpha_requested=True, native_qualified=True) + self.assertEqual(candidate.steerAtStandstill, stock.steerAtStandstill) + # Exercise the reached flag clear with a real mutable CP, not a fake field bag. + stock.flags |= HyundaiFlags.MIN_STEER_32_MPH.value + stock.minSteerSpeed = 10.0 + CarInterface._get_params(stock, CAR.HYUNDAI_PALISADE_2023, fingerprint, [], False, False, False) + self.assertEqual(bool(stock.flags & HyundaiFlags.MIN_STEER_32_MPH), observation_bus != 0) + self.assertEqual(stock.minSteerSpeed, 0.0 if observation_bus == 0 else 10.0) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_canfd_blinkers.py b/opendbc_repo/opendbc/car/hyundai/tests/test_canfd_blinkers.py new file mode 100644 index 0000000000..59bf103e96 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_canfd_blinkers.py @@ -0,0 +1,67 @@ +import unittest + +from opendbc.can import CANPacker +from opendbc.car import Bus, gen_empty_fingerprint +from opendbc.car.hyundai.carstate import CarState +from opendbc.car.hyundai.hyundaicanfd import CanBus +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.values import CAR, DBC, HyundaiFlags + + +class TestCanfdBlinkers(unittest.TestCase): + def read_lamps(self, model, values, invalid_selector=None): + cp = CarInterface.get_params(model, gen_empty_fingerprint(), [], False, False, False) + self.assertTrue(cp.flags & HyundaiFlags.CANFD) + state = CarState(cp) + parsers = state.get_can_parsers(cp) + state.update(parsers) + bus = CanBus(cp).ECAN + packer = CANPacker(DBC[model][Bus.pt]) + parsers[Bus.pt].update((1_000_000_000, [packer.make_can_msg("BLINKERS", bus, values)])) + if invalid_selector is not None: + parsers[Bus.pt].vl["BLINKERS"]["USE_ALT_LAMP"] = invalid_selector + out = state.update(parsers) + return out.leftBlinker, out.rightBlinker + + def test_selector_selects_packed_lamps_on_existing_canfd_cars(self): + for model in (CAR.KIA_EV6, CAR.HYUNDAI_IONIQ_6): + for selector in (0, 1): + for left, right in ((0, 0), (1, 0), (0, 1), (1, 1)): + with self.subTest(model=model, selector=selector, left=left, right=right): + values = {"USE_ALT_LAMP": selector, "LEFT_LAMP": left, "RIGHT_LAMP": right, + "LEFT_LAMP_ALT": 1 - left, "RIGHT_LAMP_ALT": 1 - right} + expected = (bool(left), bool(right)) if selector == 0 else (not left, not right) + self.assertEqual(self.read_lamps(model, values), expected) + + def test_ccnc_and_kona_keep_alternate_lamps_without_selector(self): + for model in (CAR.HYUNDAI_KONA_EV_2ND_GEN, CAR.KIA_SPORTAGE_2026): + for selector in (0, 1): + with self.subTest(model=model, selector=selector): + self.assertEqual(self.read_lamps(model, {"USE_ALT_LAMP": selector, + "LEFT_LAMP": 0, "RIGHT_LAMP": 1, + "LEFT_LAMP_ALT": 1, "RIGHT_LAMP_ALT": 0}), (True, False)) + + def test_selector_change_retains_existing_lamp_hold_and_release(self): + cp = CarInterface.get_params(CAR.KIA_EV6, gen_empty_fingerprint(), [], False, False, False) + state = CarState(cp) + parsers = state.get_can_parsers(cp) + state.update(parsers) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + bus = CanBus(cp).ECAN + for frame in range(52): + values = {"USE_ALT_LAMP": 1 if frame == 0 else 0, "LEFT_LAMP_ALT": int(frame == 0)} + parsers[Bus.pt].update(((frame + 1) * 10_000_000, [packer.make_can_msg("BLINKERS", bus, values)])) + out = state.update(parsers) + self.assertEqual(out.leftBlinker, frame < 50, frame) + self.assertFalse(out.rightBlinker) + + def test_default_lamps_and_invalid_selector_keep_standard_defaults(self): + for model in (CAR.KIA_EV6, CAR.HYUNDAI_IONIQ_6): + with self.subTest(model=model): + self.assertEqual(self.read_lamps(model, {}), (False, False)) + self.assertEqual(self.read_lamps(model, {"LEFT_LAMP": 1, "RIGHT_LAMP_ALT": 1}, + invalid_selector=2), (True, False)) + + +if __name__ == "__main__": + unittest.main() diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_canfd_lead.py b/opendbc_repo/opendbc/car/hyundai/tests/test_canfd_lead.py new file mode 100644 index 0000000000..219180a3ff --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_canfd_lead.py @@ -0,0 +1,50 @@ +"""Pure source-policy vectors; accepted receipts are synthetic, not hardware evidence.""" +from dataclasses import FrozenInstanceError +import math +import unittest + +from opendbc.car.hyundai.canfd_lead import CANFDLeadObservation as Observation, select_lead + +NOW = 1_000_000_000 +FLOOR = 100_000_000 + + +def selected(radar=None, camera=None, hud=False, now=NOW, floor=FLOOR): + return select_lead(radar, camera, hud_visible=hud, observed_ns=now, epoch_floor_ns=floor) + + +class TestCANFDLead(unittest.TestCase): + def test_precedence_minimum_distance_camera_and_hud_fallback(self): + radar = Observation(True, 15., -2., NOW) + camera = Observation(True, 10., 1., NOW) + self.assertEqual(selected(radar, camera).distance, 15.) + self.assertEqual(selected(Observation(True, .1, 2., NOW), camera).distance, 10.) + self.assertEqual(selected(Observation(True, .1, 2., NOW)).distance, 20.) + self.assertEqual(selected(hud=True).distance, 20.) + self.assertFalse(selected().visible) + self.assertFalse(selected(camera=Observation(True, .1, 0., NOW)).visible) + self.assertGreater(selected(Observation(True, math.nextafter(.1, math.inf), 0., NOW)).distance, .1) + + def test_camera_age_future_epoch_and_nonfinite_boundaries(self): + for age, visible in ((300_000_000, True), (300_000_001, False), (-1, False)): + self.assertEqual(selected(camera=Observation(True, 10., 0., NOW-age)).visible, visible) + for stamp in (0, FLOOR, FLOOR-1, NOW+1): + self.assertFalse(selected(Observation(True, 10., 0., stamp)).visible) + self.assertFalse(selected(camera=Observation(True, 10., 0., stamp)).visible) + for value in (math.nan, math.inf, -math.inf): + for observation in (Observation(True, value, 0., NOW), Observation(True, 10., value, NOW)): + self.assertFalse(selected(observation).visible) + self.assertFalse(selected(camera=observation).visible) + self.assertFalse(selected(hud=True, floor=NOW).visible) + self.assertFalse(selected(hud=True, floor=-1).visible) + + def test_clipping_stateless_recovery_and_immutable_values(self): + observation = Observation(True, 300., -100., NOW) + result = selected(observation) + self.assertEqual((result.distance, result.relative_speed), (204.7, -16.4)) + self.assertEqual(selected(Observation(True, 10., 100., NOW)).relative_speed, 34.7) + self.assertFalse(selected(camera=Observation(True, 10., 0., NOW-300_000_001)).visible) + self.assertTrue(selected(camera=Observation(True, 10., 0., NOW)).visible) + for instance in (observation, result): + with self.assertRaises(FrozenInstanceError): + instance.extra = 1 diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_captured_adrv_routing.py b/opendbc_repo/opendbc/car/hyundai/tests/test_captured_adrv_routing.py new file mode 100644 index 0000000000..5c45808cdd --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_captured_adrv_routing.py @@ -0,0 +1,73 @@ +"""Captured ADRV encoding keeps local validation and Panda routing separate.""" +import unittest + +from opendbc.can import CANPacker +from opendbc.car import Bus, CanData, structs +from opendbc.car.hyundai.ev6_template import EV6Template +from opendbc.car.hyundai.gv70_template import GV70Template +from opendbc.car.hyundai.hyundaicanfd import CanBus, create_adrv_messages, hkg_can_fd_checksum +from opendbc.car.hyundai.values import CAR, DBC, HyundaiFlags + +CAPTURE = bytes.fromhex('88ed2e091700ffff5e0d0000012006ff021c2200000000000800000010000000') + + +def buses(pandas: int, *, lka: bool = True) -> CanBus: + configs = [structs.CarParams.SafetyConfig(safetyModel=structs.CarParams.SafetyModel.noOutput) for _ in range(pandas - 1)] + configs.append(structs.CarParams.SafetyConfig(safetyModel=structs.CarParams.SafetyModel.hyundaiCanfd)) + cp = structs.CarParams(safetyConfigs=configs, flags=int(HyundaiFlags.CANFD_LKA_STEER_MSG) if lka else 0) + return CanBus(cp) + + +class TestCapturedADRVRouting(unittest.TestCase): + def setUp(self): + self.packer = CANPacker(DBC[CAR.KIA_EV6][Bus.pt]) + + def test_ev6_and_gv70_use_local_encoding_and_logical_owner_bus(self): + for pandas in (1, 2): + can = buses(pandas) + self.assertEqual((can.offset, can.ACAN, can.ECAN), (4 * (pandas - 1), 4 * (pandas - 1), 4 * (pandas - 1) + 1)) + for template in (EV6Template.capture(CAPTURE), GV70Template.capture(CAPTURE)): + for frame in (0, 7, 255, 256): + for drive in (False, True): + for speed in (None, 12.345): + with self.subTest(pandas=pandas, template=type(template).__name__, frame=frame, drive=drive, speed=speed): + local = (template.frame(frame, drive, 0, speed=speed) if isinstance(template, GV70Template) else + template.frame(frame, drive, 0)) + sent = create_adrv_messages(self.packer, can, frame, template=template, drive_gear=drive, speed=speed) + self.assertEqual(sent[0], CanData(local.address, local.dat, can.ACAN)) + self.assertEqual(int.from_bytes(sent[0].dat[:2], 'little'), hkg_can_fd_checksum(0x51, None, bytearray(sent[0].dat))) + self.assertEqual(template.data, CAPTURE) + self.assertTrue(all(packet[2] == can.ECAN for packet in sent[1:])) + if pandas == 2: + self.assertNotEqual(sent[0].src, 0) + + def test_default_packer_sender_preserves_logical_bus_for_both_harnesses(self): + expected_packer = CANPacker(DBC[CAR.KIA_EV6][Bus.pt]) + for pandas in (1, 2): + for lka in (False, True): + can = buses(pandas, lka=lka) + expected = expected_packer.make_can_msg('ADRV_0x51', can.ACAN, {}) + self.assertEqual(create_adrv_messages(self.packer, can, 7)[0], expected) + + def test_wrong_local_bus_and_noninteger_routing_remain_rejected(self): + for pandas in (1, 2): + for template in (EV6Template.capture(CAPTURE), GV70Template.capture(CAPTURE)): + with self.subTest(pandas=pandas, template=type(template).__name__, local_bus=1): + with self.assertRaises(ValueError): + create_adrv_messages(self.packer, buses(pandas, lka=False), 7, template=template, drive_gear=True) + for field, value in (('_a', False), ('_a', 0.0), ('offset', False), ('offset', 0.0)): + can = buses(pandas) + setattr(can, field, value) + with self.subTest(pandas=pandas, template=type(template).__name__, field=field, value=value): + with self.assertRaises(ValueError): + create_adrv_messages(self.packer, can, 7, template=template, drive_gear=True) + + def test_template_physical_bus_and_drive_guards_remain_closed(self): + for pandas in (1, 2): + for template in (EV6Template.capture(CAPTURE), GV70Template.capture(CAPTURE)): + for drive in (0, 1): + with self.assertRaises(ValueError): + create_adrv_messages(self.packer, buses(pandas), 7, template=template, drive_gear=drive) + for bus in (False, 1, 4): + with self.assertRaises(ValueError): + template.frame(7, True, bus) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_ccnc_angle_lv2_fault.py b/opendbc_repo/opendbc/car/hyundai/tests/test_ccnc_angle_lv2_fault.py new file mode 100644 index 0000000000..b299e95166 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_ccnc_angle_lv2_fault.py @@ -0,0 +1,84 @@ +import unittest +from itertools import product + +from opendbc.can import CANPacker +from opendbc.car import Bus, gen_empty_fingerprint, structs +from opendbc.car.hyundai.carcontroller import CarController +from opendbc.car.hyundai.carstate import CarState +from opendbc.car.hyundai.hyundaicanfd import CanBus +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.values import CAR, DBC, HyundaiFlags + + +class TestCcncAngleLv2Fault(unittest.TestCase): + def test_packed_mdps_fault_inhibits_angle_request_and_recovers(self): + for car_model, lka_alt in ((CAR.HYUNDAI_SANTA_FE_HEV_5TH_GEN, True), + (CAR.KIA_SPORTAGE_2026, False)): + fingerprint = gen_empty_fingerprint() + if lka_alt: + fingerprint[2][0x110] = 32 + cp = CarInterface.get_params(car_model, fingerprint, [], False, False, False) + self.assertTrue(cp.flags & HyundaiFlags.CANFD_ANGLE_STEERING) + self.assertEqual(bool(cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG_ALT), lka_alt) + state = CarState(cp) + parsers = state.get_can_parsers(cp) + packer = CANPacker(DBC[car_model][Bus.pt]) + can = CanBus(cp) + controller = CarController(DBC[car_model], cp) + control = structs.CarControl() + control.enabled = control.latActive = True + control.actuators.steeringAngleDeg = 10.0 + fuel = "ACCELERATOR_ALT" if cp.flags & HyundaiFlags.HYBRID else "ACCELERATOR_BRAKE_ALT" + setup = [(fuel, {}), (state.gear_msg_canfd, {"GEAR": 5}), + ("TCS", {"ACCEnable": 0, "ACC_REQ": 1}), ("WHEEL_SPEEDS", {}), + ("STEERING_SENSORS", {}), ("DOORS_SEATBELTS", {"DRIVER_SEATBELT": 1}), + ("BLINKERS", {})] + cases = [*product(range(8), (0, 1)), (0, 0)] + for index, (lv2_fault, base_fault) in enumerate(cases): + with self.subTest(car_model=car_model, lv2_fault=lv2_fault, base_fault=base_fault): + now_ns = (index + 1) * 10_000_000 + messages = [packer.make_can_msg(name, can.ECAN, values) for name, values in + [*setup, ("MDPS", {"MDPS_ADAS_AciFltSig_Lv2": lv2_fault, + "MDPS_LkaFailSta": base_fault})]] + mdps_bytes = messages[-1][1] + self.assertEqual((mdps_bytes[18] >> 4) & 7, lv2_fault) + self.assertEqual(bool(mdps_bytes[18] & 0x20), bool(lv2_fault & 2)) + parsers[Bus.pt].update((now_ns, messages)) + if cp.flags & HyundaiFlags.CANFD_CAMERA_SCC: + parsers[Bus.cam].update((now_ns, [packer.make_can_msg("SCC_CONTROL", can.CAM, {"ACCMode": 1})])) + else: + parsers[Bus.pt].update((now_ns, [packer.make_can_msg("SCC_CONTROL", can.ECAN, {"ACCMode": 1})])) + state.out = state.update(parsers) + expected_fault = bool((lv2_fault & 2) or base_fault) + self.assertEqual(state.out.steerFaultTemporary, expected_fault) + self.assertFalse(state.out.steerFaultPermanent) + _, sent = controller.update(control.as_reader(), state, now_ns) + angle_addresses = {0x110} if lka_alt else {0x12a} + angle_frames = [(address, data) for address, data, _ in sent if address in angle_addresses] + self.assertTrue(angle_frames) + self.assertEqual((angle_frames[0][1][9] >> 4) & 3, 1 if expected_fault else 2) + + def test_torque_car_ignores_lv2_fault(self): + car_model = CAR.HYUNDAI_IONIQ_6 + cp = CarInterface.get_params(car_model, gen_empty_fingerprint(), [], False, False, False) + self.assertFalse(cp.flags & HyundaiFlags.CANFD_ANGLE_STEERING) + state = CarState(cp) + parsers = state.get_can_parsers(cp) + packer = CANPacker(DBC[car_model][Bus.pt]) + can = CanBus(cp) + setup = [("ACCELERATOR", {"GEAR": 5}), ("TCS", {"ACCEnable": 0, "ACC_REQ": 1}), + ("WHEEL_SPEEDS", {}), ("STEERING_SENSORS", {}), + ("DOORS_SEATBELTS", {"DRIVER_SEATBELT": 1}), ("BLINKERS", {})] + for index, (lv2_fault, base_fault) in enumerate(product(range(8), (0, 1))): + with self.subTest(lv2_fault=lv2_fault, base_fault=base_fault): + messages = [packer.make_can_msg(name, can.ECAN, values) for name, values in + [*setup, ("MDPS", {"MDPS_ADAS_AciFltSig_Lv2": lv2_fault, + "MDPS_LkaFailSta": base_fault})]] + parsers[Bus.pt].update(((index + 1) * 10_000_000, messages)) + state.out = state.update(parsers) + self.assertEqual(state.out.steerFaultTemporary, bool(base_fault)) + self.assertFalse(state.out.steerFaultPermanent) + + +if __name__ == "__main__": + unittest.main() diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_ccnc_six.py b/opendbc_repo/opendbc/car/hyundai/tests/test_ccnc_six.py new file mode 100644 index 0000000000..1de81e6d64 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_ccnc_six.py @@ -0,0 +1,228 @@ +import unittest + +from opendbc.can import CANPacker +from opendbc.car import Bus, gen_empty_fingerprint, structs +from opendbc.car.fw_versions import match_fw_to_car +from opendbc.car.hyundai.fingerprints import FW_VERSIONS +from opendbc.car.hyundai.hyundaicanfd import CanBus, create_carnival_alt_resume, hkg_can_fd_checksum +from opendbc.car.hyundai.carcontroller import CarController +from opendbc.car.hyundai.carstate import CarState +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.values import CAR, DBC, HyundaiFlags, HyundaiSafetyFlags +from opendbc.car.structs import CarParams +from opendbc.safety.tests.libsafety import libsafety_py + + +SIX = (CAR.HYUNDAI_IONIQ_5_N, CAR.HYUNDAI_TUCSON_PHEV_2025, CAR.KIA_K4_2025, + CAR.KIA_SORENTO_2024, CAR.KIA_CARNIVAL_2025, CAR.KIA_CARNIVAL_HEV_4TH_GEN) +CARNIVALS = SIX[-2:] + + +def params(car, hda2=False, alt_buttons=False, alpha=False, adas=False, alt_lka=False): + fp = gen_empty_fingerprint() + if hda2: + fp[2][0x110 if alt_lka else 0x50] = 32 if alt_lka else 16 + if alt_buttons: + fp[1 if hda2 else 0][0x1aa] = 16 + else: + fp[1 if hda2 else 0][0x1cf] = 8 + fw = [CarParams.CarFw(ecu=CarParams.Ecu.adas)] if adas else [] + return CarInterface.get_params(car, fp, fw, alpha, False, False) + + +class TestCcncSix(unittest.TestCase): + def test_exact_firmware_requires_both_distinguishing_ecus(self): + for car in SIX: + with self.subTest(car=car): + fw = [CarParams.CarFw(ecu=ecu, address=addr, subAddress=sub or 0, fwVersion=versions[0], brand="hyundai") + for (ecu, addr, sub), versions in FW_VERSIONS[car].items()] + exact, matches = match_fw_to_car(fw, "", log=False) + self.assertTrue(exact) + self.assertEqual(matches, {car}) + for item in fw: + _, partial = match_fw_to_car([item], "", log=False) + self.assertNotIn(car, partial) + + for sibling in (CAR.HYUNDAI_TUCSON_HEV_2025, CAR.HYUNDAI_TUCSON_2025, CAR.KIA_CARNIVAL_4TH_GEN, + CAR.KIA_SORENTO_4TH_GEN, CAR.HYUNDAI_IONIQ_5, CAR.KIA_EV6): + if sibling not in FW_VERSIONS: + continue + fw = [CarParams.CarFw(ecu=ecu, address=addr, subAddress=sub or 0, fwVersion=versions[0], brand="hyundai") + for (ecu, addr, sub), versions in FW_VERSIONS[sibling].items()] + _, matches = match_fw_to_car(fw, "", log=False) + self.assertFalse(set(SIX) & matches, sibling) + + radar_key = (CarParams.Ecu.fwdRadar, 0x7d0, None) + camera_key = (CarParams.Ecu.fwdCamera, 0x7c4, None) + for car, sibling in ((CAR.HYUNDAI_IONIQ_5_N, CAR.HYUNDAI_IONIQ_5), + (CAR.HYUNDAI_TUCSON_PHEV_2025, CAR.HYUNDAI_TUCSON_HEV_2025), + (CAR.KIA_SORENTO_2024, CAR.KIA_SORENTO_4TH_GEN), + (CAR.KIA_CARNIVAL_2025, CAR.KIA_CARNIVAL_4TH_GEN)): + mixed = [CarParams.CarFw(ecu=key[0], address=key[1], fwVersion=FW_VERSIONS[owner][key][0], brand="hyundai") + for key, owner in ((camera_key, car), (radar_key, sibling))] + _, matches = match_fw_to_car(mixed, "", log=False) + self.assertNotIn(car, matches) + + def test_six_topology_and_stock_ownership(self): + for car in SIX: + for hda2 in (False, True): + with self.subTest(car=car, hda2=hda2): + cp = params(car, hda2=hda2) + self.assertEqual(bool(cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG), hda2) + self.assertTrue(cp.pcmCruise) + self.assertFalse(cp.openpilotLongitudinalControl) + self.assertEqual(bool(cp.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CCNC), not hda2) + unavailable = params(car, hda2=hda2, alpha=True) + self.assertEqual(unavailable.alphaLongitudinalAvailable, not hda2) + self.assertEqual(unavailable.openpilotLongitudinalControl, not hda2) + eligible = params(car, hda2=hda2, alpha=True, adas=True) + self.assertTrue(eligible.alphaLongitudinalAvailable) + self.assertTrue(eligible.openpilotLongitudinalControl) + self.assertTrue(eligible.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.LONG) + + def test_carnival_alt_resume_only_with_observed_source_and_stock_long(self): + for car in CARNIVALS: + for hda2 in (False, True): + cp = params(car, hda2=hda2, alt_buttons=True) + self.assertTrue(cp.flags & HyundaiFlags.CANFD_ALT_BUTTONS) + self.assertTrue(cp.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CARNIVAL_ALT_RESUME) + cp2 = params(car, hda2=hda2, alt_buttons=False) + self.assertFalse(cp2.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CARNIVAL_ALT_RESUME) + if not hda2: + cp_long = params(car, alt_buttons=True, alpha=True) + self.assertTrue(cp_long.openpilotLongitudinalControl) + self.assertFalse(cp_long.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CARNIVAL_ALT_RESUME) + + def test_parser_and_controller_require_fresh_idle_carnival_button_source(self): + for hda2 in (False, True): + cp = params(CAR.KIA_CARNIVAL_2025, hda2=hda2, alt_buttons=True) + state = CarState(cp) + parsers = state.get_can_parsers(cp) + bus = CanBus(cp) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + source = packer.make_can_msg("CRUISE_BUTTONS_ALT", bus.ECAN, + {"COUNTER": 23, "SET_ME_1": 1, "CRUISE_BUTTONS": 0}) + parsers[Bus.pt].update((1_000_000_000, [source])) + state.out = state.update(parsers) + self.assertEqual(state.cruise_buttons_alt_msg["COUNTER"], 23) + self.assertEqual(state.cruise_buttons_alt_ts_ns, 1_000_000_000) + control = structs.CarControl() + control.cruiseControl.resume = True + controller = CarController(DBC[cp.carFingerprint], cp) + controller.frame = 26 + _, sent = controller.update(control.as_reader(), state, 1_050_000_000) + self.assertEqual(len([msg for msg in sent if msg[0] == 0x1aa]), 1) + controller.frame = 52 + _, sent = controller.update(control.as_reader(), state, 1_101_000_000) + self.assertFalse(any(msg[0] == 0x1aa for msg in sent)) + state.cruise_buttons_alt_msg["ADAPTIVE_CRUISE_MAIN_BTN"] = 1 + controller.frame = 78 + _, sent = controller.update(control.as_reader(), state, 1_050_000_000) + self.assertFalse(any(msg[0] == 0x1aa for msg in sent)) + + def test_packed_carnival_resume_native_counter_crc_and_replay(self): + self._check_packed_carnival_resume(libsafety_py.libsafety) + + def _check_packed_carnival_resume(self, safety): + packer = CANPacker(DBC[CAR.KIA_CARNIVAL_2025][Bus.pt]) + for hda2 in (False, True): + cp = params(CAR.KIA_CARNIVAL_2025, hda2=hda2, alt_buttons=True) + can = CanBus(cp) + safety.set_safety_hooks(CarParams.SafetyModel.hyundaiCanfd, cp.safetyConfigs[-1].safetyParam) + safety.init_tests() + bus = can.ECAN + source_values = {"COUNTER": 17, "SET_ME_1": 1, "CRUISE_BUTTONS": 0, "BYTE6": 0x55} + addr, raw, _ = packer.make_can_msg("CRUISE_BUTTONS_ALT", bus, source_values) + data = bytearray(raw) + data[0:2] = hkg_can_fd_checksum(addr, None, data).to_bytes(2, "little") + source = dict(source_values) + resume = create_carnival_alt_resume(packer, cp, can, source) + self.assertEqual(resume[2], bus if hda2 else can.CAM) + tx = libsafety_py.make_CANPacket(resume[0], resume[2], resume[1]) + self.assertFalse(safety.safety_tx_hook(tx)) + self.assertTrue(safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, bus, bytes(data)))) + safety.set_controls_allowed(True) + self.assertTrue(safety.safety_tx_hook(tx)) + self.assertFalse(safety.safety_tx_hook(tx)) + self.assertTrue(safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, bus, bytes(data)))) + self.assertFalse(safety.safety_tx_hook(tx)) # replayed stock counter cannot reopen the gate + + source["COUNTER"] = 18 + source_values["COUNTER"] = 18 + _, raw, _ = packer.make_can_msg("CRUISE_BUTTONS_ALT", bus, source_values) + data = bytearray(raw) + data[0:2] = hkg_can_fd_checksum(addr, None, data).to_bytes(2, "little") + resume = create_carnival_alt_resume(packer, cp, can, source) + tx = libsafety_py.make_CANPacket(resume[0], resume[2], resume[1]) + bad_source = bytearray(data) + bad_source[0] ^= 1 + self.assertTrue(safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, bus, bad_source))) + self.assertFalse(safety.safety_tx_hook(tx)) + active_source = bytearray(data) + active_source[4] |= 0x20 # driver SET button + active_source[0:2] = hkg_can_fd_checksum(addr, None, active_source).to_bytes(2, "little") + self.assertTrue(safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, bus, active_source))) + self.assertFalse(safety.safety_tx_hook(tx)) + self.assertTrue(safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, bus, bytes(data)))) + wrong_counter = bytearray(resume[1]) + wrong_counter[2] += 1 + wrong_counter[0:2] = hkg_can_fd_checksum(addr, None, wrong_counter).to_bytes(2, "little") + self.assertFalse(safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, resume[2], wrong_counter))) + wrong_field = bytearray(resume[1]) + wrong_field[6] ^= 1 + wrong_field[0:2] = hkg_can_fd_checksum(addr, None, wrong_field).to_bytes(2, "little") + self.assertFalse(safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, resume[2], wrong_field))) + safety.set_timer(100_001) + self.assertFalse(safety.safety_tx_hook(tx)) + safety.set_timer(0) + self.assertTrue(safety.safety_tx_hook(tx)) + + source["COUNTER"] = 19 + source_values["COUNTER"] = 19 + source_values["ADAPTIVE_CRUISE_MAIN_BTN"] = 1 + _, raw, _ = packer.make_can_msg("CRUISE_BUTTONS_ALT", bus, source_values) + data = bytearray(raw) + data[0:2] = hkg_can_fd_checksum(addr, None, data).to_bytes(2, "little") + self.assertTrue(safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, bus, data))) + source["ADAPTIVE_CRUISE_MAIN_BTN"] = 1 + active_resume = create_carnival_alt_resume(packer, cp, can, source) + self.assertFalse(safety.safety_tx_hook(libsafety_py.make_CANPacket(active_resume[0], active_resume[2], active_resume[1]))) + + source_values["ADAPTIVE_CRUISE_MAIN_BTN"] = 0 + source_values["CRUISE_BUTTONS"] = 4 # driver cancel + _, raw, _ = packer.make_can_msg("CRUISE_BUTTONS_ALT", bus, source_values) + data = bytearray(raw) + data[0:2] = hkg_can_fd_checksum(addr, None, data).to_bytes(2, "little") + self.assertTrue(safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, bus, data))) + source["ADAPTIVE_CRUISE_MAIN_BTN"] = 0 + source["CRUISE_BUTTONS"] = 4 + active_resume = create_carnival_alt_resume(packer, cp, can, source) + self.assertFalse(safety.safety_tx_hook(libsafety_py.make_CANPacket(active_resume[0], active_resume[2], active_resume[1]))) + + source_values["CRUISE_BUTTONS"] = 0 + _, raw, _ = packer.make_can_msg("CRUISE_BUTTONS_ALT", bus, source_values) + data = bytearray(raw) + data[0:2] = hkg_can_fd_checksum(addr, None, data).to_bytes(2, "little") + self.assertTrue(safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, bus, data))) + idle_resume = create_carnival_alt_resume(packer, cp, can, {**source, "CRUISE_BUTTONS": 0}) + safety.set_timer(2_000_000) + safety.safety_tick() + self.assertFalse(safety.safety_tx_hook(libsafety_py.make_CANPacket(idle_resume[0], idle_resume[2], idle_resume[1]))) + + safety.set_safety_hooks(CarParams.SafetyModel.hyundaiCanfd, + cp.safetyConfigs[-1].safetyParam & ~HyundaiSafetyFlags.CARNIVAL_ALT_RESUME) + safety.init_tests() + safety.set_controls_allowed(True) + self.assertFalse(safety.safety_tx_hook(tx)) + + safety.set_safety_hooks(CarParams.SafetyModel.hyundaiCanfd, cp.safetyConfigs[-1].safetyParam) + safety.init_tests() + source_values["COUNTER"] = 255 + _, raw, _ = packer.make_can_msg("CRUISE_BUTTONS_ALT", bus, source_values) + data = bytearray(raw) + data[0:2] = hkg_can_fd_checksum(addr, None, data).to_bytes(2, "little") + self.assertTrue(safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, bus, data))) + safety.set_controls_allowed(True) + wrap_resume = create_carnival_alt_resume(packer, cp, can, {**source, "COUNTER": 255, "CRUISE_BUTTONS": 0}) + self.assertEqual(wrap_resume[1][2], 0) + self.assertTrue(safety.safety_tx_hook(libsafety_py.make_CANPacket(wrap_resume[0], wrap_resume[2], wrap_resume[1]))) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_ev6_2025.py b/opendbc_repo/opendbc/car/hyundai/tests/test_ev6_2025.py new file mode 100644 index 0000000000..0617f566c9 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_ev6_2025.py @@ -0,0 +1,117 @@ +import unittest +from unittest.mock import patch + +from opendbc.car import Bus, gen_empty_fingerprint, structs +from opendbc.car.fw_versions import match_fw_to_car +from opendbc.car.hyundai.fingerprints import FW_VERSIONS +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.values import CAR, DBC, HyundaiFlags +from opendbc.car.hyundai.hyundaicanfd import CanBus + + +def params(*, alternate=False, topology='lfa', alpha=False, release=False, adas=False, radar_length=None, hybrid=False): + fp = gen_empty_fingerprint() + if topology == 'lka': + fp[2][0x50] = 16 + elif topology == 'lka_alt': + fp[2][0x110] = 32 + if hybrid: + fp[0][0xFA] = 8 + fp[0][0x1AA if alternate else 0x1CF] = 16 if alternate else 8 + if radar_length is not None: + fp[0][0x210] = radar_length + if adas: + fp[2][0xCB] = 24 + return CarInterface.get_params(CAR.KIA_EV6_2025, fp, [], alpha, release, False) + + +class TestEv62025(unittest.TestCase): + def test_stock_profiles_preserve_identity_without_long_or_optional_hardware(self): + for alternate in (False, True): + for alpha in (False, True): + for release in (False, True): + for adas in (False, True): + cp = params(alternate=alternate, alpha=alpha, release=release, adas=adas) + self.assertEqual(cp.carFingerprint, CAR.KIA_EV6_2025) + self.assertEqual(cp.steerControlType, structs.CarParams.SteerControlType.angle) + self.assertEqual(cp.safetyConfigs[-1].safetyParam, 0x7829 if alternate else 0x7809) + self.assertFalse(cp.flags & HyundaiFlags.SEND_LFA) + self.assertFalse(cp.alphaLongitudinalAvailable) + self.assertFalse(cp.openpilotLongitudinalControl) + self.assertTrue(cp.pcmCruise) + self.assertFalse(cp.dashcamOnly) + self.assertEqual((CanBus(cp).ECAN, CanBus(cp).CAM), (0, 2)) + self.assertAlmostEqual(cp.wheelbase, 2.9, places=6) + self.assertAlmostEqual(cp.steerRatio, 14.26, places=5) + ci = CarInterface(cp) + self.assertEqual(ci.CS.accelerator_msg_canfd, 'ACCELERATOR') + self.assertEqual(ci.can_parsers[Bus.cam].message_states[0x1A0].frequency, 50) + self.assertTrue(ci.can_parsers[Bus.pt].message_states[0x2E0].ignore_alive) + with patch('opendbc.car.hyundai.interface.disable_ecu') as disable: + ci.init(cp, list, lambda _: None) + disable.assert_not_called() + + def test_contradictory_lka_topology_has_no_output_or_diagnostic_transaction(self): + for topology, hybrid in (('lka', False), ('lka_alt', False), ('lfa', True)): + cp = params(topology=topology, hybrid=hybrid, alpha=True) + self.assertTrue(cp.dashcamOnly) + self.assertEqual(cp.safetyConfigs[-1].safetyModel, structs.CarParams.SafetyModel.noOutput) + self.assertFalse(cp.openpilotLongitudinalControl) + ci = CarInterface(cp) + control = structs.CarControl() + control.enabled = control.latActive = control.longActive = True + control.cruiseControl.cancel = control.cruiseControl.resume = True + _, frames = ci.CC.update(control.as_reader(), ci.CS, 1_000_000_000) + self.assertEqual(frames, []) + with patch('opendbc.car.hyundai.interface.disable_ecu') as disable: + ci.init(cp, list, lambda _: None) + disable.assert_not_called() + + def test_mrr30_real_dbc_layout_units_and_cycle_freshness(self): + from opendbc.can import CANPacker + from opendbc.car.hyundai.radar_interface import RadarInterface + + for length, available in ((None, False), (8, False), (24, False), (32, True)): + self.assertEqual(not params(radar_length=length).radarUnavailable, available) + cp = params(radar_length=32) + radar = RadarInterface(cp) + self.assertEqual(radar.rcp.bus, 0) + self.assertEqual((radar.start_addr, radar.msg_count), (0x210, 16)) + packer = CANPacker(DBC[CAR.KIA_EV6_2025][Bus.radar]) + frames = [packer.make_can_msg(f'RADAR_TRACK_{addr:x}', 0, {}) for addr in range(0x210, 0x220)] + frames[0] = packer.make_can_msg('RADAR_TRACK_210', 0, { + '1_STATE': 3, '1_LONG_DIST': 42.5, '1_LAT_DIST': -1.25, '1_REL_SPEED': -2.5, + '2_STATE': 4, '2_LONG_DIST': 18.0, '2_LAT_DIST': 1.5, '2_REL_SPEED': 3.25, + }) + self.assertEqual(len(frames[0][1]), 32) + self.assertIsNone(radar.update((1_000_000_000, [(addr, data, 1) for addr, data, _ in frames]))) + self.assertIsNone(radar.update((1_010_000_000, [(addr, data[:24], bus) for addr, data, bus in frames]))) + result = radar.update((1_020_000_000, frames)) + self.assertFalse(result.errors.canError) + self.assertEqual(len(result.points), 2) + self.assertAlmostEqual(result.points[0].dRel, 42.5) + self.assertAlmostEqual(result.points[0].yRel, -1.25) + self.assertAlmostEqual(result.points[0].vRel, -2.5) + self.assertAlmostEqual(result.points[1].dRel, 18.0) + self.assertAlmostEqual(result.points[1].yRel, 1.5) + self.assertAlmostEqual(result.points[1].vRel, 3.25) + self.assertEqual(len(radar.update((1_040_000_000, [frames[-1]])).points), 0) + self.assertIsNone(radar.update((1_050_000_000, [frames[0]]))) + self.assertEqual(len(radar.update((1_100_000_000, [frames[-1]])).points), 0) + + def test_original_camera_radar_versions_identify_distinct_vehicle(self): + entries = FW_VERSIONS[CAR.KIA_EV6_2025] + self.assertEqual(sum(map(len, entries.values())), 4) + for radar in entries[(structs.CarParams.Ecu.fwdRadar, 0x7d0, None)]: + for camera in entries[(structs.CarParams.Ecu.fwdCamera, 0x7c4, None)]: + fw = [] + for ecu, address, version in ((structs.CarParams.Ecu.fwdRadar, 0x7d0, radar), + (structs.CarParams.Ecu.fwdCamera, 0x7c4, camera)): + fw.append(structs.CarParams.CarFw(ecu=ecu, address=address, fwVersion=version, brand='hyundai')) + _, matched = match_fw_to_car(fw, '', log=False) + self.assertEqual(matched, {CAR.KIA_EV6_2025}) + self.assertEqual(DBC[CAR.KIA_EV6_2025][Bus.radar], 'hyundai_mrr30_radar_generated') + + +if __name__ == '__main__': + unittest.main() diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_ev6_template.py b/opendbc_repo/opendbc/car/hyundai/tests/test_ev6_template.py new file mode 100644 index 0000000000..72bd037d46 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_ev6_template.py @@ -0,0 +1,77 @@ +"""Captured EV6 status integrity and pinned-original counter/Drive recipe.""" +from dataclasses import FrozenInstanceError +import unittest + +from opendbc.car.hyundai.ev6_template import EV6Template + +# Retained original EV6 factory fixture; no new physical capture claimed. +CAPTURE = bytes.fromhex('88ed2e091700ffff5e0d0000012006ff021c2200000000000800000010000000') + + +def independent_crc(data): + crc = 0 + for byte in bytes(data)[2:] + bytes((0x51, 0)): + crc ^= byte << 8 + for _ in range(8): + crc = ((crc << 1) ^ (0x1021 if crc & 0x8000 else 0)) & 0xFFFF + return crc ^ 0x9F5B + + +def valid_capture(counter, body=CAPTURE[3:]): + data = bytearray(bytes(2) + bytes((counter,)) + body) + data[:2] = independent_crc(data).to_bytes(2, 'little') + return bytes(data) + + +class TestEV6Template(unittest.TestCase): + def test_retained_capture_is_crc_valid_and_immutable(self): + self.assertEqual(independent_crc(CAPTURE), int.from_bytes(CAPTURE[:2], 'little')) + template = EV6Template.capture(CAPTURE) + self.assertEqual(template.counter, 46) + self.assertEqual(template.data, CAPTURE) + with self.assertRaises(FrozenInstanceError): + template.data = bytes(32) + + def test_constructor_and_capture_reject_invalid_or_empty_body(self): + corrupt = bytearray(CAPTURE) + corrupt[12] ^= 1 + for data in (b'', CAPTURE[:-1], CAPTURE + b'\x00', bytearray(CAPTURE), bytes(corrupt), + valid_capture(46, bytes(29))): + for constructor in (EV6Template, EV6Template.capture): + with self.subTest(size=len(data), constructor=constructor): + with self.assertRaises(ValueError): + constructor(data) + + def test_counter_wrap_physical_drive_and_opaque_bytes_match_original(self): + for counter in (0, 1, 46, 254, 255): + source = valid_capture(counter) + template = EV6Template.capture(source) + for frame in (0, 1, 7, 255, 256, 511, 65535): + for drive in (False, True): + with self.subTest(counter=counter, frame=frame, drive=drive): + packet = template.frame(frame, drive) + expected = bytearray(source) + expected[2] = (counter + frame + 1) & 255 + expected[3] = (source[3] & ~1) | int(drive) + expected[:2] = independent_crc(expected).to_bytes(2, 'little') + self.assertEqual(packet, (0x51, bytes(expected), 0)) + self.assertEqual(packet.dat[4:], source[4:]) + self.assertEqual(template.data, source) + + def test_instances_and_repeated_calls_do_not_share_mutable_state(self): + first = EV6Template.capture(CAPTURE) + second = EV6Template.capture(valid_capture(255)) + expected = first.frame(7, True) + second.frame(255, False) + first.frame(300, False) + self.assertEqual(first.frame(7, True), expected) + self.assertEqual(second.counter, 255) + self.assertEqual(first.counter, 46) + + def test_invalid_frame_drive_and_bus_are_not_coerced(self): + template = EV6Template.capture(CAPTURE) + for frame, drive, bus in ((-1, False, 0), (True, False, 0), (.5, False, 0), + (0, 1, 0), (0, False, 1), (0, False, False)): + with self.subTest(frame=frame, drive=drive, bus=bus): + with self.assertRaises(ValueError): + template.frame(frame, drive, bus) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_ev9_keeper.py b/opendbc_repo/opendbc/car/hyundai/tests/test_ev9_keeper.py new file mode 100644 index 0000000000..685fa0d248 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_ev9_keeper.py @@ -0,0 +1,50 @@ +"""Startup clock/sender checks without threads, receive callbacks or sleeps.""" +import unittest +from opendbc.car.hyundai.ev9_keeper import EV9Keeper + + +class TestEV9Keeper(unittest.TestCase): + def test_original_one_hz_cadence_and_software_deadline(self): + now, sent = [0.0], [] + keeper = EV9Keeper(lambda: sent.append(now[0]), clock=lambda: now[0]) + for value in (0.0, 0.2, 0.999, 1.0, 1.1, 2.0): + now[0] = value + self.assertTrue(keeper.tick()) + self.assertEqual(sent, [0.0, 1.0, 2.0]) + now[0] = 60.0 + self.assertFalse(keeper.tick()) + self.assertIsNotNone(keeper.abort_reason) + self.assertEqual(sent, [0.0, 1.0, 2.0]) + + def test_send_failure_only_marks_abort_no_receive_or_restore(self): + calls = [] + def send(): + calls.append('send') + raise OSError('transport') + keeper = EV9Keeper(send, clock=lambda: 0.0) + self.assertFalse(keeper.tick()) + self.assertEqual(calls, ['send']) + self.assertEqual(keeper.abort_reason, 'startup tester send failed') + self.assertFalse(keeper.tick()) + self.assertEqual(calls, ['send']) + + def test_stop_sets_event_before_join_without_sender_lock(self): + operations = [] + class Event: + stopped = False + def is_set(self): + return self.stopped + def set(self): + self.stopped = True + operations.append('stop') + event = Event() + keeper = EV9Keeper(lambda: None, clock=lambda: 0.0, event=event) + class Thread: + def join(self): + if not event.stopped: + raise AssertionError('join before event') + operations.append('join') + keeper.thread = Thread() + keeper.stop() + self.assertEqual(operations, ['stop', 'join']) + self.assertFalse(keeper.tick()) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_ev9_long_controller.py b/opendbc_repo/opendbc/car/hyundai/tests/test_ev9_long_controller.py new file mode 100644 index 0000000000..02f0492660 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_ev9_long_controller.py @@ -0,0 +1,194 @@ +"""EV9 optional LONG actual parser/controller and reached-cadence boundaries.""" +import unittest +from opendbc.can import CANPacker +from opendbc.car import Bus, structs +from opendbc.car.hyundai.ev9_longitudinal import candidate, qualified, EV9LongitudinalPolicy, LongCtrlState +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.tests.test_ioniq5pe_stock import params +from opendbc.car.hyundai.values import CAR, DBC + + +def packets(packer, tick, *, fault=0, moving=True, gear=5, cruise=True, advance_counter=False, cruise_button=0): + values = { + 'ACCELERATOR': (100, {'GEAR': gear, 'ACCELERATOR_PEDAL': 0}), + 'TCS': (50, {'ACCEnable': 0, 'ACC_REQ': int(cruise), 'DriverBraking': 0}), + 'WHEEL_SPEEDS': (100, dict.fromkeys(('WHL_SpdFLVal','WHL_SpdFRVal','WHL_SpdRLVal','WHL_SpdRRVal'), 36. if moving else 0.)), + 'MDPS': (100, {'MDPS_ADAS_AciFltSig_Lv2': fault}), + 'STEERING_SENSORS': (100, {}), + 'DOORS_SEATBELTS': (10, {'DRIVER_SEATBELT': 1}), + 'BLINKERS': (10, {}), + 'CRUISE_BUTTONS': (50, {'CRUISE_BUTTONS': cruise_button}), + } + frames = [packer.make_can_msg(name, 1, {**fields, **( + {"COUNTER": tick // (100 // hz)} if advance_counter and name not in ("DOORS_SEATBELTS", "BLINKERS") else {})}) + for name, (hz, fields) in values.items() + if tick % (100 // hz) == 0] + # Optional stock camera receive frames remain available on CAM2; no active + # steering source is inserted on ACAN0 where relay detection checks ownership. + frames.append(packer.make_can_msg('LKAS_ALT', 2, {})) + if tick % 5 == 0: + frames.append(packer.make_can_msg('CAM_0x362', 2, {})) + assert all(address != 0x1a0 and not 0x3a5 <= address <= 0x3c4 for address, _, _ in frames) + return frames + + +def update(ci, packer, tick, **kwargs): + return ci.update([(tick * 10_000_000, packets(packer, tick, **kwargs))]) + +class TestEV9LongController(unittest.TestCase): + def instance(self): + cp = candidate(params(candidate=CAR.KIA_EV9), enabled=True, is_release=False) + self.assertTrue(qualified(cp)) + ci = CarInterface(cp) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + ci.update([]) + for tick in range(1, 13): + update(ci, packer, tick) + self.assertTrue(ci.CS.out.canValid) + return ci, packer + + def test_original_calibration_20hz_and_stop_latch_50hz(self): + policy = EV9LongitudinalPolicy() + first = policy.update(0, 1.0, 0.0, 0.0, LongCtrlState.starting, True, False) + self.assertGreater(first, 0) + for frame in range(1, 5): + self.assertEqual(policy.update(frame, 1.0, 0.0, 0.0, LongCtrlState.starting, True, False), first) + self.assertGreater(policy.update(5, 1.0, 0.0, 0.0, LongCtrlState.starting, True, False), first) + policy.update_stop(True, False, True, 0.47) + for sample in range(1, 179): + policy.update_stop(True, False, True, 0.47) + self.assertEqual(policy.stop.cruise_standstill, sample >= 178) + for sample in range(1, 8): + policy.update_stop(True, False, False, 0.2) + self.assertEqual(policy.stop.stop_request, sample <= 6) + self.assertFalse(policy.stop.cruise_standstill) + + def test_actual_mdps_byte16_and_original_mask2(self): + ci, packer = self.instance() + for tick, fault in enumerate((0, 1, 2, 4), 13): + update(ci, packer, tick, fault=fault) + ci.update([((tick * 10_000_000) + 1, [packer.make_can_msg("MDPS", 1, + {"MDPS_EstStrAnglVal": 12.3, "MDPS_PaStrAnglVal": -45.6, "MDPS_ADAS_AciFltSig_Lv2": fault})])]) + self.assertAlmostEqual(ci.CS.angle_steering_angle, 12.3) + self.assertEqual(ci.CS.angle_steering_fault, bool(fault & 2)) + self.assertEqual(ci.CS.out.steerFaultTemporary, bool(fault & 2)) + + def test_actual_controller_cb_only_and_adrv_cadence(self): + ci, _ = self.instance() + cc = structs.CarControl(enabled=True, latActive=True, longActive=True) + cc.actuators.steeringAngleDeg = 10.0 + cc.actuators.accel = 1.0 + cc.actuators.longControlState = LongCtrlState.pid + cc.hudControl.setSpeed = 20.0 + for frame in range(101): + _, packets = ci.apply(cc.as_reader(), 1_000_000_000 + frame * 10_000_000) + ids = [p[0] for p in packets] + self.assertNotIn(0x110, ids) + self.assertNotIn(0x51, ids) + self.assertEqual(ids.count(0xcb), 1) + self.assertEqual(ids.count(0x100), 1) + for address, period in ((0x160, 2), (0x1da, 100), (0x1ea, 5), (0x200, 5), (0x345, 20), + (0x1e0, 5), (0x38c, 20), (0x161, 5), (0x162, 5), (0x1ba, 5), (0x1e5, 5), + (0x1a0, 2), (0x362, 5)): + self.assertEqual(ids.count(address), int(frame % period == 0), (frame, hex(address))) + cb = next(p for p in packets if p[0] == 0xcb) + self.assertEqual(cb[2], 1) + self.assertEqual((cb[1][3] >> 4) & 0xf, 2) + + def test_nondrive_neutral_lfa_cb_and_emitted_acceleration_bound(self): + ci, _ = self.instance() + cc = structs.CarControl(enabled=True, latActive=True, longActive=True) + cc.actuators.longControlState = LongCtrlState.off + cc.actuators.accel = 3.5 + ci.CS.out.gearShifter = structs.CarState.GearShifter.park + ci.CS.angle_steering_angle = 10.0 + _, packets = ci.apply(cc.as_reader(), 1_000_000_000) + ids = [p[0] for p in packets] + self.assertEqual(ids[:2], [0x730, 0x12a]) + self.assertNotIn(0x110, ids) + self.assertNotIn(0x362, ids) + cb = next(p for p in packets if p[0] == 0xcb) + self.assertEqual((cb[1][3] >> 4) & 0xf, 1) + self.assertEqual(cb[1][6], 0) + self.assertEqual(ci.CC.accel_last, 2.2) + + def test_exact_ev9_pid_ceiling_and_sibling_limits_unchanged(self): + stock = params(candidate=CAR.KIA_EV9) + long_cp = candidate(stock, enabled=True, is_release=False) + for speed in (0., 10., 30.): + self.assertEqual(CarInterface.get_pid_accel_limits(long_cp, speed, 20.), (-3.5, 2.2)) + self.assertEqual(CarInterface.get_pid_accel_limits(stock, speed, 20.), (-3.5, 2.0)) + for sibling in (CAR.HYUNDAI_IONIQ_5_PE, CAR.HYUNDAI_IONIQ_6): + self.assertEqual(CarInterface.get_pid_accel_limits(params(candidate=sibling), speed, 20.), (-3.5, 2.0)) + changed = long_cp.as_reader().as_builder() + changed.safetyConfigs[0].safetyParam = 0x5c91 + self.assertEqual(CarInterface.get_pid_accel_limits(changed, 0., 20.), (-3.5, 2.0)) + + def test_stale_lateral_command_cannot_bypass_native_state_gates(self): + ci, _ = self.instance() + cc = structs.CarControl(enabled=True, latActive=True, longActive=True) + cc.actuators.steeringAngleDeg = 10. + cc.actuators.longControlState = LongCtrlState.pid + cc.actuators.accel = 0. + for index, (field, value) in enumerate((('standstill', True), ('gasPressed', True), ('brakePressed', True), + ('steerFaultTemporary', True), ('steerFaultPermanent', True), + ('canTimeout', True), ('canValid', False), + ('gearShifter', structs.CarState.GearShifter.park))): + with self.subTest(field=field): + previous = getattr(ci.CS.out, field) + setattr(ci.CS.out, field, value) + ci.CC.frame = 5 * index + _, packets = ci.apply(cc.as_reader(), 1_000_000_000 + index * 50_000_000) + ids = [packet[0] for packet in packets] + if field in ('canTimeout', 'canValid'): + self.assertNotIn(0xcb, ids) + self.assertNotIn(0x12a, ids) + else: + cb = next(packet for packet in packets if packet[0] == 0xcb) + self.assertEqual((cb[1][3] >> 4) & 0xf, 1) + self.assertEqual(cb[1][6], 0) + self.assertNotIn(0x362, ids) + setattr(ci.CS.out, field, previous) + cc.enabled = False + ci.CC.frame = 50 + _, packets = ci.apply(cc.as_reader(), 2_000_000_000) + cb = next(packet for packet in packets if packet[0] == 0xcb) + self.assertEqual((cb[1][3] >> 4) & 0xf, 1) + self.assertEqual(cb[1][6], 0) + self.assertNotIn(0x362, [packet[0] for packet in packets]) + + def test_advertised_stock_availability_still_requires_startup_choice(self): + for alpha in (False, True): + stock = params(candidate=CAR.KIA_EV9, alpha=alpha) + self.assertTrue(stock.alphaLongitudinalAvailable) + self.assertFalse(stock.openpilotLongitudinalControl) + self.assertEqual(stock.safetyConfigs[0].safetyParam, 0x5c91) + not_requested = candidate(stock, enabled=False, is_release=False) + self.assertIs(not_requested, stock) + selected = candidate(stock, enabled=True, is_release=False) + self.assertTrue(qualified(selected)) + self.assertEqual(selected.safetyConfigs[0].safetyParam, 0x5c95) + self.assertEqual(stock.safetyConfigs[0].safetyParam, 0x5c91) + self.assertFalse(stock.openpilotLongitudinalControl) + release_stock = params(candidate=CAR.KIA_EV9, alpha=alpha, release=True) + self.assertFalse(release_stock.alphaLongitudinalAvailable) + self.assertIs(candidate(release_stock, enabled=True, is_release=True), release_stock) + + def test_long_healthy_without_stock_scc_but_stock_still_requires_scc(self): + for selected in (False, True): + stock = params(candidate=CAR.KIA_EV9) + cp = candidate(stock, enabled=selected, is_release=False) + ci = CarInterface(cp) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + ci.update([]) + subscribed = ci.can_parsers[Bus.pt].addresses + self.assertEqual(0x1a0 in subscribed, not selected) + for tick in range(1, 101): + frames = packets(packer, tick) + self.assertNotIn(0x1a0, [address for address, _, _ in frames]) + ci.update([(tick * 10_000_000, frames)]) + self.assertEqual(ci.CS.out.canValid, selected) + if selected: + self.assertFalse(ci.CS.out.canTimeout) + self.assertTrue(ci.CS.out.cruiseState.available) + self.assertTrue(ci.CS.out.cruiseState.enabled) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_ev9_sender.py b/opendbc_repo/opendbc/car/hyundai/tests/test_ev9_sender.py new file mode 100644 index 0000000000..9282a9cb2b --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_ev9_sender.py @@ -0,0 +1,36 @@ +"""Original EV9 reached-wire cases; ordinary ownership is tested separately.""" +import unittest +from types import SimpleNamespace + +from opendbc.can import CANPacker +from opendbc.car.hyundai.hyundaicanfd import create_angle_steering_messages +from opendbc.car.hyundai.values import CAR, HyundaiFlags + + +class TestEV9Sender(unittest.TestCase): + @staticmethod + def frame(candidate, angle, status): + flags = HyundaiFlags.CANFD_ANGLE_STEERING | HyundaiFlags.CANFD_LKA_STEER_MSG | HyundaiFlags.CANFD_LKA_STEER_MSG_ALT + cp = SimpleNamespace(carFingerprint=candidate, flags=flags, openpilotLongitudinalControl=False) + packer = CANPacker('hyundai_canfd_generated') + packer.counters[0x110] = 37 + return create_angle_steering_messages(packer, cp, SimpleNamespace(ACAN=0, ECAN=1), + True, True, angle, 0.5, status)[0] + + def test_original_ev9_signed_half_step_quantization(self): + # Original DBC packer uses floor(angle / 0.1 + 0.5), not ties-to-even. + for angle, units in ((0.05, 1), (-0.15, -1), (5.0, 50), (-12.3, -123)): + with self.subTest(angle=angle): + address, data, bus = self.frame(CAR.KIA_EV9, angle, {}) + raw = (data[11] << 6) | (data[10] >> 2) + signed = raw if raw < 8192 else raw - 16384 + self.assertEqual((address, bus, data[2], signed), (0x110, 0, 37, units)) + + def test_ev9_active_status_reset_preserves_sibling_status_behavior(self): + stock = {'LKA_LHLnWrnSta': 2, 'LKA_RHLnWrnSta': 3, 'ToiFltSta': 2, 'LKA_UsmMod': 3} + _, ev9, _ = self.frame(CAR.KIA_EV9, 5.0, stock) + _, sibling, _ = self.frame(CAR.HYUNDAI_IONIQ_5_N, 5.0, stock) + # Source-derived original active body resets these fields to zero. + for data, expected in ((ev9, (0, 0, 0, 0)), (sibling, (2, 3, 2, 3))): + actual = ((data[3] >> 6) & 3, data[4] & 3, (data[6] >> 6) & 3, data[10] & 3) + self.assertEqual(actual, expected) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_ev9_stock.py b/opendbc_repo/opendbc/car/hyundai/tests/test_ev9_stock.py new file mode 100644 index 0000000000..2719e3df5c --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_ev9_stock.py @@ -0,0 +1,134 @@ +"""Exact EV9 stock-SCC metadata, decoder and ordinary request boundary.""" +import math +import unittest + +from opendbc.can import CANPacker +from opendbc.car import Bus, structs +from opendbc.car.hyundai.ccnc_ev_stock import qualified, replacement_requested +from opendbc.car.hyundai.fingerprints import FW_VERSIONS +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.tests.test_ioniq5pe_stock import params, update +from opendbc.car.hyundai.values import CAR, DBC, HyundaiFlags +from opendbc.car.interfaces import get_torque_params + + +class TestEV9Stock(unittest.TestCase): + def test_original_metadata_and_constructor(self): + cfg = CAR.KIA_EV9.config + self.assertEqual((cfg.specs.mass, cfg.specs.wheelbase, cfg.specs.steerRatio), (2664, 3.1, 16)) + self.assertEqual(cfg.flags, HyundaiFlags.CANFD | HyundaiFlags.EV | HyundaiFlags.CANFD_ANGLE_STEERING | HyundaiFlags.CCNC) + self.assertEqual(cfg.dbc_dict[Bus.radar], "hyundai_mrr35_radar_generated") + tune = get_torque_params()[CAR.KIA_EV9] + self.assertEqual(tune["MAX_LAT_ACCEL_MEASURED"], 2.5) + self.assertTrue(math.isnan(tune["LAT_ACCEL_FACTOR"])) + self.assertEqual(CarInterface.get_std_params(CAR.KIA_EV9).maxLateralAccel, 2.5) + self.assertEqual(set(FW_VERSIONS[CAR.KIA_EV9]), {(structs.CarParams.Ecu.fwdCamera, 0x7c4, None), + (structs.CarParams.Ecu.fwdRadar, 0x7d0, None)}) + + def test_exact_stock_topology_and_no_automatic_long(self): + for alpha in (False, True): + for release in (False, True): + with self.subTest(alpha=alpha, release=release): + cp = params(candidate=CAR.KIA_EV9, alpha=alpha, release=release) + self.assertTrue(qualified(cp)) + self.assertEqual(cp.safetyConfigs[0].safetyParam, 0x5c91) + self.assertFalse(cp.openpilotLongitudinalControl) + self.assertTrue(cp.pcmCruise) + self.assertEqual(cp.alphaLongitudinalAvailable, not release) + self.assertFalse(cp.steerAtStandstill) + self.assertEqual(cp.longitudinalActuatorDelay, 0.5) + for alpha in (False, True): + for missing in (0x35, 0x110, 0x362, 0x1cf, 0x1a0): + cp = params(candidate=CAR.KIA_EV9, missing=missing, alpha=alpha) + self.assertFalse(qualified(cp)) + self.assertFalse(cp.alphaLongitudinalAvailable) + self.assertFalse(cp.openpilotLongitudinalControl) + for topology in ("lka", "lfa"): + cp = params(candidate=CAR.KIA_EV9, topology=topology, alpha=alpha) + self.assertFalse(qualified(cp)) + self.assertFalse(cp.alphaLongitudinalAvailable) + self.assertFalse(cp.openpilotLongitudinalControl) + for release in (False, True): + pe = params(alpha=alpha, release=release) + self.assertFalse(pe.alphaLongitudinalAvailable) + self.assertFalse(pe.openpilotLongitudinalControl) + self.assertEqual(pe.safetyConfigs[0].safetyParam, 0x5491) + + def test_actual_ev_decoder_and_all_lv2_faults(self): + cp = params(candidate=CAR.KIA_EV9) + ci = CarInterface(cp) + self.assertEqual(ci.CS.gear_msg_canfd, "ACCELERATOR") + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + ci.update([]) + for tick in range(1, 13): + state = update(ci, packer, tick) + self.assertTrue(state.canValid) + self.assertEqual(state.gearShifter, structs.CarState.GearShifter.drive) + for fault in range(1, 8): + state = update(ci, packer, 12 + fault, fault=fault) + self.assertTrue(state.steerFaultTemporary) + + def test_normal_lateral_request_releases_at_standstill_and_fault(self): + cp = params(candidate=CAR.KIA_EV9) + state = structs.CarState(canValid=True, gearShifter=structs.CarState.GearShifter.drive) + state.cruiseState.enabled = True + control = structs.CarControl(enabled=True, latActive=True) + self.assertTrue(replacement_requested(cp, control, state)) + for field in ("standstill", "brakePressed", "gasPressed", "steerFaultTemporary", "canTimeout"): + setattr(state, field, True) + self.assertFalse(replacement_requested(cp, control, state)) + setattr(state, field, False) + control.latActive = False + self.assertFalse(replacement_requested(cp, control, state)) + + def test_actual_state_fault_and_ordered_replacement_recipe(self): + cp = params(candidate=CAR.KIA_EV9) + ci = CarInterface(cp) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + ci.update([]) + command = structs.CarControl() + command.enabled = command.latActive = True + command.actuators.steeringAngleDeg = 5.0 + for warmup in range(1, 13): + state = update(ci, packer, warmup) + self.assertTrue(state.canValid) + self.assertFalse(state.canTimeout) + tick = 0 + for fault, moving, gear, cruise, active in [(0, True, 5, True, True), + *[(value, True, 5, True, True) for value in range(1, 8)], + (0, False, 5, True, True), (0, True, 7, True, True), + (0, True, 5, False, True), (0, True, 5, True, False), + (0, True, 5, True, True)]: + with self.subTest(fault=fault, moving=moving, gear=gear, cruise=cruise, active=active): + command.latActive = active + state = update(ci, packer, tick + 13, fault=fault, moving=moving, gear=gear, cruise=cruise) + self.assertEqual(state.steerFaultTemporary, fault != 0) + self.assertEqual(state.standstill, not moving) + expected = fault == 0 and moving and gear == 5 and cruise and active + self.assertEqual(bool(replacement_requested(cp, command, state)), expected) + _, frames = ci.apply(command.as_reader(), (tick + 13) * 10_000_000) + addresses = [frame[0] for frame in frames] + self.assertEqual(0x110 in addresses, expected) + self.assertEqual(0x362 in addresses, expected and tick % 5 == 0) + if 0x362 in addresses: + self.assertLess(addresses.index(0x110), addresses.index(0x362)) + tick += 1 + + def test_original_ev9_optional_corner_and_rear_blindspots(self): + cp = params(candidate=CAR.KIA_EV9) + self.assertTrue(cp.deprecated.enableBsm) + ci = CarInterface(cp) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + ci.update([]) + for tick in range(1, 13): + state = update(ci, packer, tick) + self.assertTrue(state.canValid) # No optional corner sources in startup. + for tick, (rear_left, rear_right, corner, expected) in enumerate( + ((0, 0, 0x10, (True, False)), (0, 0, 0x08, (False, True)), + (1, 0, 0x08, (True, True)), (0, 1, 0, (False, True)), (0, 0, 0, (False, False))), 14): + update(ci, packer, tick) + messages = [packer.make_can_msg("ADAS_CMD_50_50ms", 1, {"BCW_LtIndSta": rear_left, "BCW_RtIndSta": rear_right}), + packer.make_can_msg("BLINDSPOTS_FRONT_CORNER_2", 1, {"SIDE_DETECT_STATE": corner})] + state = ci.update([(tick * 10_000_000 + 1, messages)]) + self.assertTrue(state.canValid) + self.assertEqual((state.leftBlindspot, state.rightBlindspot), expected) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_g90_lead.py b/opendbc_repo/opendbc/car/hyundai/tests/test_g90_lead.py new file mode 100644 index 0000000000..6d4f2dc849 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_g90_lead.py @@ -0,0 +1,62 @@ +"""Source-derived G90 lead hysteresis and existing CAN encoder fields.""" +import unittest + +from opendbc.can import CANPacker, CANParser +from opendbc.car import Bus, structs +from opendbc.car.hyundai.g90_lead import G90LeadState, LeadObservation +from opendbc.car.hyundai.hyundaican import create_acc_commands +from opendbc.car.hyundai.tests.test_g90_longitudinal import params +from opendbc.car.hyundai.values import CAR, DBC + + +def observation(distance=26., relative=-.5, present=True, drive=1): + return LeadObservation(drive, 100, 101, 102, present, distance, relative) + + +class TestG90Lead(unittest.TestCase): + def test_visibility_gap_and_relative_state_counts_controller_updates(self): + state = G90LeadState() + for _ in range(49): + lead = state.update(observation(), False) + self.assertFalse(lead.lead_visible) + self.assertEqual(lead.object_gap, 0) + lead = state.update(observation(), False) + self.assertTrue(lead.lead_visible) + self.assertEqual((lead.object_gap, lead.object_rel_gap), (4, 2)) + for _ in range(49): + lead = state.update(observation(0, 0, False), False) + self.assertTrue(lead.lead_visible) + self.assertFalse(state.update(observation(0, 0, False), False).lead_visible) + state = G90LeadState() + for frame in range(50): + lead = state.update(observation(15 if frame % 2 else 26), False) + self.assertEqual(lead.object_gap, 2) # Original non-current gaps share one counter. + self.assertEqual(state.update(observation(.05, -1), False).lead_distance, 20.) + self.assertEqual(state.update(observation(999, -999), False).lead_rel_speed, -170.) + self.assertEqual(state.update(observation(26, -.2), False).object_rel_gap, 1) + + def test_optional_payload_preserves_sibling_and_accel_aeb_bytes(self): + for car, alpha in ((CAR.GENESIS_G90, True), (CAR.GENESIS_G90, False), (CAR.GENESIS_G80, True)): + cp = params(car, alpha) + hud = structs.CarControl.HUDControl(leadVisible=True, leadDistanceBars=2) + state = G90LeadState() + for _ in range(50): + lead = state.update(observation(), False) + def packed(payload, cp=cp, hud=hud): + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + return create_acc_commands(packer, True, .2, 1., 7, hud, 70., False, False, True, cp, lead_data=payload) + baseline, changed = packed(None), packed(lead) + if car != CAR.GENESIS_G90 or not alpha: + self.assertEqual(baseline, changed) + else: + for old, new in zip(baseline, changed, strict=True): + if old[0] in (0x421, 0x38d): + self.assertEqual(old, new) + parser = CANParser(DBC[cp.carFingerprint][Bus.pt], [('FCA11', 0), ('SCC12', 0)], 0) + parser.update([(1_000_000_000, changed)]) + self.assertEqual(parser.vl['FCA11']['FCA_Status'], 1) + non_fca = create_acc_commands(CANPacker(DBC[cp.carFingerprint][Bus.pt]), True, .2, 1., 7, + hud, 70., False, False, False, cp, lead_data=lead) + parser.update([(1_010_000_000, non_fca)]) + self.assertEqual(parser.vl['SCC12']['AEB_Status'], 1) + diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_g90_longitudinal.py b/opendbc_repo/opendbc/car/hyundai/tests/test_g90_longitudinal.py new file mode 100644 index 0000000000..60ff16bb93 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_g90_longitudinal.py @@ -0,0 +1,71 @@ +import unittest + +from opendbc.can import CANParser +from opendbc.car import Bus, gen_empty_fingerprint, structs +from opendbc.car.hyundai.g90_longitudinal import G90LongitudinalPolicy, LongCtrlState +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.values import CAR, DBC + + +def params(car=CAR.GENESIS_G90, alpha=True, release=False): + return CarInterface.get_params(car, gen_empty_fingerprint(), [], alpha, release, False) + + +class TestG90Longitudinal(unittest.TestCase): + def test_stop_hold_relaxes_braking_then_release_ramps(self): + policy = G90LongitudinalPolicy() + self.assertAlmostEqual(policy.update(-2, 2, LongCtrlState.stopping, True), -1.4) + self.assertAlmostEqual(policy.update(-2, 0, LongCtrlState.stopping, True), -1.3) + for _ in range(20): + policy.update(-2, 0, LongCtrlState.stopping, True) + self.assertAlmostEqual(policy.actual_accel, -0.1) + self.assertAlmostEqual(policy.update(1, 0, LongCtrlState.pid, True), -0.05) + self.assertTrue(policy.release_active) + self.assertAlmostEqual(policy.update(1, 0, LongCtrlState.pid, True), 0) + self.assertEqual(policy.update(1, 0, LongCtrlState.off, False), 0) + self.assertFalse(policy.release_active) + self.assertEqual(policy.update(1, 0, LongCtrlState.pid, True), 1) + + def test_only_g90_with_longitudinal_ownership_uses_policy(self): + for car in (CAR.GENESIS_G90, CAR.GENESIS_G80): + for alpha in (False, True): + for release in (False, True): + with self.subTest(car=car, alpha=alpha, release=release): + cp = params(car, alpha, release) + ci = CarInterface(cp) + self.assertEqual(ci.CC.g90_longitudinal is not None, car == CAR.GENESIS_G90 and cp.openpilotLongitudinalControl) + self.assertEqual(cp.pcmCruise, not cp.openpilotLongitudinalControl) + + def test_classic_can_uses_shaped_accel_and_inactive_messages_are_neutral(self): + from opendbc.safety.tests.libsafety import libsafety_py + from opendbc.safety.tests.libsafety.libsafety_py import make_CANPacket + + cp = params() + native = libsafety_py.libsafety + self.assertEqual(native.set_safety_hooks(cp.safetyConfigs[-1].safetyModel.raw, cp.safetyConfigs[-1].safetyParam), 0) + native.init_tests() + native.set_controls_allowed(True) + ci = CarInterface(cp) + ci.update([]) + parser = CANParser(DBC[CAR.GENESIS_G90][Bus.pt], [("SCC12", 0)], 0) + command = structs.CarControl(enabled=True, longActive=True) + command.hudControl.setSpeed = 20 + for frame in range(160): + stopped = frame < 80 + command.actuators.longControlState = LongCtrlState.stopping if stopped else LongCtrlState.pid + command.actuators.accel = -2 if stopped else 1 + ci.CS.out.vEgo = max(0, 2 - frame * 0.05) + native.set_timer(frame * 10_000) + actuators, messages = ci.CC.update(command.as_reader(), ci.CS, frame * 10_000_000) + for address, data, bus in messages: + self.assertTrue(native.safety_tx_hook(make_CANPacket(address, bus, data)), (frame, address, data)) + if frame % 2 == 0: + parser.update((1_000_000_000 + frame * 10_000_000, messages)) + self.assertAlmostEqual(parser.vl["SCC12"]["aReqRaw"], actuators.accel, delta=0.011) + self.assertAlmostEqual(parser.vl["SCC12"]["aReqValue"], actuators.accel, delta=0.011) + command.enabled = command.longActive = False + command.actuators.accel = 0 + _, messages = ci.CC.update(command.as_reader(), ci.CS, 1_600_000_000) + parser.update((2_600_000_000, messages)) + self.assertEqual(parser.vl["SCC12"]["aReqRaw"], 0) + self.assertEqual(parser.vl["SCC12"]["aReqValue"], 0) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_g90_radar.py b/opendbc_repo/opendbc/car/hyundai/tests/test_g90_radar.py new file mode 100644 index 0000000000..647e837339 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_g90_radar.py @@ -0,0 +1,117 @@ +import math +import unittest + +from opendbc.can import CANPacker +from opendbc.car import Bus, gen_empty_fingerprint +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.radar_interface import RadarInterface +from opendbc.car.hyundai.values import CAR, DBC, HyundaiFlags, HYUNDAI_G90_RADAR_DBC +from opendbc.dbc.generator.hyundai.hyundai_kia_mando_front_radar import generate + + +def params(bank=None): + fp = gen_empty_fingerprint() + if bank is not None: + fp[1].update(bank) + return CarInterface.get_params(CAR.GENESIS_G90, fp, [], False, False, False) + + +def raw_motorola(data, start, size, signed=False): + raw = 0 + bit = start + for _ in range(size): + raw = (raw << 1) | ((data[bit // 8] >> (bit % 8)) & 1) + bit = bit - 1 if bit % 8 else bit + 15 + return raw - (1 << size) if signed and raw & (1 << (size - 1)) else raw + + +class TestG90Radar(unittest.TestCase): + def setUp(self): + self.packer = CANPacker(HYUNDAI_G90_RADAR_DBC) + self.bank = dict.fromkeys(range(0x500, 0x540), 8) + + def test_readiness_lower_bank_exact_length_and_stable_authority(self): + absent = params() + present = params(self.bank) + self.assertTrue(absent.radarUnavailable) + self.assertFalse(present.radarUnavailable) + self.assertTrue(present.flags & HyundaiFlags.MANDO_RADAR) + self.assertFalse(present.openpilotLongitudinalControl) + self.assertEqual([(cfg.safetyModel, cfg.safetyParam) for cfg in absent.safetyConfigs], + [(cfg.safetyModel, cfg.safetyParam) for cfg in present.safetyConfigs]) + lower_bank = dict.fromkeys(range(0x500, 0x520), 8) + self.assertFalse(params(lower_bank).radarUnavailable) + radar = RadarInterface(params(lower_bank)) + lower_frames = [self.packer.make_can_msg(f'RADAR_TRACK_{addr:x}', 1, {'STATE': 3, 'LONG_DIST': 10}) + for addr in lower_bank] + result = radar.update((1_000_000_000, lower_frames)) + self.assertEqual(len(result.points), 32) + self.assertTrue(all(addr < 0x520 for addr in radar.pts)) + for address in lower_bank: + bank = lower_bank.copy() + del bank[address] + self.assertTrue(params(bank).radarUnavailable) + bank[address] = 16 + self.assertTrue(params(bank).radarUnavailable) + + def test_both_halves_all_current_typed_outputs_match_original_bits(self): + radar = RadarInterface(params(self.bank)) + self.assertEqual((radar.rcp.bus, radar.start_addr, radar.msg_count, radar.trigger_msg), (1, 0x500, 64, 0x51F)) + frames = [] + for index, address in enumerate(self.bank): + frames.append(self.packer.make_can_msg(f'RADAR_TRACK_{address:x}', 1, { + 'STATE': 3 if index % 2 == 0 else 4, 'LONG_DIST': index + 1, + 'AZIMUTH': 10.0 if index % 2 == 0 else -10.0, 'REL_SPEED': -.01 * index, + })) + result = radar.update((1_000_000_000, frames)) + self.assertFalse(result.errors.canError) + self.assertEqual(len(result.points), 64) + self.assertEqual(len({point.trackId for point in result.points}), 64) + for point, (_, data, _) in zip(result.points, frames, strict=True): + azimuth = math.radians(raw_motorola(data, 12, 10, True) * .2) + distance = raw_motorola(data, 18, 11) * .1 + speed = raw_motorola(data, 53, 14, True) * .01 + self.assertAlmostEqual(point.dRel, math.cos(azimuth) * distance, places=5) + self.assertAlmostEqual(point.yRel, -.5 * math.sin(azimuth) * distance, places=5) + self.assertAlmostEqual(point.vRel, speed, places=6) + + def test_original_lower_trigger_and_upper_cycle_freshness(self): + radar = RadarInterface(params(self.bank)) + lower = [self.packer.make_can_msg(f'RADAR_TRACK_{addr:x}', 1, {}) for addr in range(0x500, 0x520)] + upper = self.packer.make_can_msg('RADAR_TRACK_520', 1, {'STATE': 3, 'LONG_DIST': 20, 'REL_SPEED': -1}) + self.assertIsNone(radar.update((1_000_000_000, [(upper[0], upper[1], 0)]))) + self.assertIsNone(radar.update((1_010_000_000, [(upper[0], upper[1] * 2, 1)]))) + self.assertIsNone(radar.update((1_020_000_000, [(upper[0], upper[1][:4], 1)]))) + self.assertEqual(len(radar.update((1_030_000_000, lower)).points), 0) + self.assertIsNone(radar.update((1_040_000_000, [upper]))) + result = radar.update((1_050_000_000, lower)) + self.assertEqual(len(result.points), 1) + first_id = result.points[0].trackId + # Fresh upper-bank packet never drives cadence by itself. + self.assertIsNone(radar.update((1_060_000_000, [upper]))) + self.assertEqual(len(radar.update((1_090_000_000, lower)).points), 0) + self.assertIsNone(radar.update((1_100_000_000, [upper]))) + self.assertEqual(len(radar.update((1_110_000_000, lower)).points), 1) + self.assertGreater(radar.pts[0x520].trackId, first_id) + invalid = self.packer.make_can_msg('RADAR_TRACK_520', 1, {'STATE': 2}) + radar.update((1_120_000_000, [invalid])) + self.assertEqual(len(radar.update((1_130_000_000, lower)).points), 0) + + def test_ordinary_mando_generator_and_parser_unchanged(self): + outputs = generate() + ordinary = outputs['hyundai_kia_mando_front_radar.dbc'] + extended = outputs['hyundai_genesis_g90_radar.dbc'] + self.assertTrue(extended.startswith(ordinary)) + self.assertEqual(ordinary.count('\nBO_ '), 32) + self.assertEqual(extended.count('\nBO_ '), 64) + cars = [car for car in CAR if car != CAR.GENESIS_G90 and + DBC[car].get(Bus.radar) == 'hyundai_kia_mando_front_radar_generated'] + self.assertTrue(cars) + for car in cars: + cp = CarInterface.get_params(car, gen_empty_fingerprint(), [], False, False, False) + radar = RadarInterface(cp) + self.assertEqual((radar.rcp.bus, radar.start_addr, radar.msg_count, radar.trigger_msg), (1, 0x500, 32, 0x51F)) + + +if __name__ == '__main__': + unittest.main() diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_gv70_camera_lead.py b/opendbc_repo/opendbc/car/hyundai/tests/test_gv70_camera_lead.py new file mode 100644 index 0000000000..95b8c49f10 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_gv70_camera_lead.py @@ -0,0 +1,132 @@ +"""Actual isolated parser tests using synthetic packed camera CAN evidence.""" +import unittest +from unittest.mock import patch + +from opendbc.can.packer import CANPacker +from opendbc.car import Bus, CanData, gen_empty_fingerprint, structs +from opendbc.car.hyundai.gv70_camera_lead import GV70CameraLead +from opendbc.car.hyundai.hyundaicanfd import CanBus, hkg_can_fd_checksum +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.values import CAR, DBC, HyundaiFlags + + +def params(lka=True): + fingerprint = gen_empty_fingerprint() + if lka: + fingerprint[2][0x50] = 16 + fingerprint[1 if lka else 0][0x1cf] = 8 + firmware = [structs.CarParams.CarFw(ecu=structs.CarParams.Ecu.adas)] if lka else [] + cp = CarInterface.get_params(CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, fingerprint, firmware, True, False, False) + assert bool(cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG) == lka + return cp + + +def packet(packer, bus, counter, distance=12., relative=-2.): + address, raw, src = packer.make_can_msg('FR_CMR_03_50ms', bus, { + 'FR_CMR_AlvCnt3Val': counter, 'Longitudinal_Distance': distance, 'Relative_Velocity': relative}) + data = bytearray(raw) + data[:2] = hkg_can_fd_checksum(address, None, data).to_bytes(2, 'little') + return CanData(address, bytes(data), src) + + + +class TestGV70CameraLead(unittest.TestCase): + def setUp(self): + boot_clock = patch("opendbc.car.hyundai.canfd_camera_lead.time.CLOCK_BOOTTIME", 7, create=True) + boot_clock.start() + self.addCleanup(boot_clock.stop) + self.clock = patch("opendbc.car.hyundai.canfd_camera_lead.time.clock_gettime_ns", return_value=10_000_000_000) + self.clock_mock = self.clock.start() + self.addCleanup(self.clock.stop) + + def test_scoped_raw_integrity_units_and_topology(self): + for lka in (True, False): + cp = params(lka) + owner = GV70CameraLead(cp) + buses = CanBus(cp) + self.assertEqual(owner.bus, buses.ECAN if lka else buses.CAM) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + for counter in (1, 2): + owner.update([(counter*50_000_000, [packet(packer, owner.bus, counter)])]) + observation = owner.current(100_000_000) + self.assertIsNotNone(observation) + self.assertTrue(observation.visible) + self.assertEqual(observation.distance_m, 12.) + self.assertEqual(observation.relative_speed_mps, -2.) + decoded = owner.parser.vl['FR_CMR_03_50ms'] + self.assertNotIn('CHECKSUM', decoded) + self.assertNotIn('COUNTER', decoded) + self.assertEqual(decoded['FR_CMR_AlvCnt3Val'], 2) + self.assertEqual(owner.parser.message_states[0x1b5].counter_fail, 0) + + def test_bad_crc_counter_wrong_bus_length_do_not_renew_and_expire(self): + cp = params() + owner = GV70CameraLead(cp) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + owner.update([(50_000_000, [packet(packer, owner.bus, 1)])]) + good = owner.current(50_000_000) + self.assertIsNotNone(good) + wrong_crc = bytearray(packet(packer, owner.bus, 2).dat) + wrong_crc[27] ^= 1 + owner.update([(100_000_000, [(0x1b5, bytes(wrong_crc), owner.bus)])]) + self.assertEqual(owner.current(100_000_000), good) + # Repeated raw counters cannot renew the optional observation. + for tick in range(3, 9): + owner.update([(tick*50_000_000, [packet(packer, owner.bus, 1)])]) + self.assertEqual(owner.observation, good) + self.assertIsNone(owner.current(400_000_000)) + for bus, data in ((owner.bus+1, packet(packer, owner.bus, 3).dat), + (owner.bus, bytes(31)), (owner.bus, bytes(33))): + owner.update([(450_000_000, [(0x1b5, data, bus)])]) + self.assertEqual(owner.observation, good) + owner.update([(500_000_000, [packet(packer, owner.bus, 2)])]) + self.assertEqual(owner.current(500_000_000).producer_boot_ns, 500_000_000) + owner.update([(550_000_000, [packet(packer, owner.bus, 4)])]) + self.assertEqual(owner.observation.producer_boot_ns, 500_000_000) + owner.update([(600_000_000, [packet(packer, owner.bus, 5)])]) + self.assertEqual(owner.current(600_000_000).producer_boot_ns, 600_000_000) + + def test_clock_floor_visibility_and_descriptor_scope(self): + cp = params() + owner = GV70CameraLead(cp) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + owner.update([(100_000_000, [packet(packer, owner.bus, 1, .1)])]) + self.assertFalse(owner.current(100_000_000).visible) + self.assertIsNone(owner.current(99_999_999)) + self.assertIsNone(owner.current(100_000_000, 100_000_000)) + self.assertIsNotNone(owner.current(400_000_000)) + self.assertIsNone(owner.current(400_000_001)) + previous_counter = owner.last_counter + owner.update([(99_999_999, [packet(packer, owner.bus, 2, .15)])]) + self.assertEqual(owner.last_counter, previous_counter) + owner.update([(10_000_000_001, [packet(packer, owner.bus, 2, .15)])]) + self.assertEqual(owner.last_counter, previous_counter) + self.assertIsNone(owner.current(10_000_000_001)) + self.assertEqual(owner.observation.producer_boot_ns, 100_000_000) + with patch("opendbc.car.hyundai.canfd_camera_lead.time.CLOCK_BOOTTIME", None): + owner.update([(200_000_000, [packet(packer, owner.bus, 2, .15)])]) + with patch("opendbc.car.hyundai.canfd_camera_lead.time.clock_gettime_ns", side_effect=OSError): + owner.update([(200_000_000, [packet(packer, owner.bus, 2, .15)])]) + self.assertEqual(owner.observation.producer_boot_ns, 100_000_000) + self.assertEqual(owner.last_packet_ns, 100_000_000) + cp.openpilotLongitudinalControl = False + with self.assertRaises(ValueError): + GV70CameraLead(cp) + cp.openpilotLongitudinalControl = True + cp.carFingerprint = CAR.KIA_EV6 + with self.assertRaises(ValueError): + GV70CameraLead(cp) + + def test_isolated_counter_failure_does_not_join_controls_health(self): + cp = params() + ci = CarInterface(cp) + owner = GV70CameraLead(cp) + self.assertNotIn(owner.parser, ci.can_parsers.values()) + self.assertTrue(all(0x1b5 not in parser.addresses for parser in ci.can_parsers.values())) + before = [(parser.can_valid, parser.bus_timeout, parser.can_invalid_cnt) for parser in ci.can_parsers.values()] + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + for tick in range(8): + owner.update([((tick+1)*50_000_000, [packet(packer, owner.bus, 1)])]) + self.assertEqual(owner.observation.producer_boot_ns, 50_000_000) + after = [(parser.can_valid, parser.bus_timeout, parser.can_invalid_cnt) for parser in ci.can_parsers.values()] + self.assertEqual([x[:2] for x in before], [x[:2] for x in after]) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_gv70_longitudinal.py b/opendbc_repo/opendbc/car/hyundai/tests/test_gv70_longitudinal.py new file mode 100644 index 0000000000..b2f3137b71 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_gv70_longitudinal.py @@ -0,0 +1,123 @@ +"""First-generation GV70 demand/cancel policy through actual CAN callers.""" +import unittest + +from opendbc.can.packer import CANPacker +from opendbc.can.parser import CANParser +from opendbc.car import Bus, CanData, gen_empty_fingerprint, structs +from opendbc.car.hyundai.gv70_longitudinal import scc_request, suppress_stock_cancel +from opendbc.car.hyundai.hyundaicanfd import CanBus, create_acc_control +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.values import CAR, DBC, HyundaiFlags + + +def params(car=CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, alpha=True, *, lka=True, alt=False): + fp = gen_empty_fingerprint() + if lka: + fp[2][0x50] = 16 + fp[1 if lka else 0][0x1aa if alt else 0x1cf] = 16 if alt else 8 + fw = [structs.CarParams.CarFw(ecu=structs.CarParams.Ecu.adas)] if lka else [] + return CarInterface.get_params(car, fp, fw, alpha, False, False) + + +def update(ci, packer, tick, brake=False): + can = CanBus(ci.CP) + frames = [CanData(*packer.make_can_msg('WHEEL_SPEEDS', can.ECAN, + dict.fromkeys(('WHL_SpdFLVal', 'WHL_SpdFRVal', 'WHL_SpdRLVal', 'WHL_SpdRRVal'), 36.))), + CanData(*packer.make_can_msg('ACCELERATOR', can.ECAN, {'GEAR': 4})), + CanData(*packer.make_can_msg('TCS', can.ECAN, {'DriverBraking': int(brake)})), + CanData(*packer.make_can_msg('CAM_0x2a4', can.CAM, {})), + CanData(*packer.make_can_msg('CRUISE_BUTTONS_ALT', can.ECAN, {})), + CanData(*packer.make_can_msg('SCC_CONTROL', can.CAM if ci.CP.flags & HyundaiFlags.CANFD_CAMERA_SCC else can.ECAN, {}))] + return ci.update([((tick+1)*10_000_000, frames)]) + + +class TestGV70Longitudinal(unittest.TestCase): + def test_demand_thresholds_disable_override_and_shaped_history(self): + for raw, stop, lower in ((-.999, False, 2.), (-1., False, 5.), (.7, True, 5.)): + result = scc_request(True, False, stop, raw, 0.) + self.assertEqual(result.raw_accel, raw) + self.assertEqual(result.jerk_lower, lower) + self.assertEqual(result.jerk_upper, 1.5) + self.assertAlmostEqual(result.accel, min(max(raw, -lower/50.), .03)) + for enabled, override in ((False, False), (True, True)): + self.assertEqual(scc_request(enabled, override, False, 1., .5).accel, 0.) + self.assertAlmostEqual(scc_request(True, False, False, 1., .03).accel, .06) + + def test_actual_50hz_sender_raw_shaped_jerk_and_disabled_modes(self): + for lka, alt in ((True, False), (False, False), (False, True)): + with self.subTest(lka=lka, alt=alt): + cp = params(lka=lka, alt=alt) + self.assertTrue(cp.openpilotLongitudinalControl) + self.assertEqual(bool(cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG), lka) + self.assertEqual(bool(cp.flags & HyundaiFlags.CANFD_ALT_BUTTONS), alt) + ci = CarInterface(cp) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + decoder = CANParser(DBC[cp.carFingerprint][Bus.pt], [('SCC_CONTROL', 50)], CanBus(cp).ECAN) + command = structs.CarControl() + history = 0. + for tick in range(14): + raw, stop, enabled, override = [(1., False, True, False), (-.5, False, True, False), + (-1., False, True, False), (.8, True, True, False), + (.4, False, True, True), (.8, False, False, False), + (1., False, True, False)][tick//2] + command.enabled = enabled + command.cruiseControl.override = override + command.actuators.accel = raw + command.actuators.longControlState = structs.CarControl.Actuators.LongControlState.stopping if stop else \ + structs.CarControl.Actuators.LongControlState.pid + update(ci, packer, tick) + _, outputs = ci.apply(command.as_reader(), (tick+1)*10_000_000) + packets = [CanData(*frame) for frame in outputs if frame[0] == 0x1a0] + self.assertEqual(len(packets), int(tick % 2 == 0)) + if not packets: + self.assertEqual(ci.CC.accel_last, history) + continue + lower = 5. if stop or raw <= -1. else 2. + history = min(max(raw, history-lower/50.), history+.03) if enabled and not override else 0. + decoder.update([((tick+1)*10_000_000, packets)]) + values = decoder.vl['SCC_CONTROL'] + self.assertAlmostEqual(values['aReqRaw'], raw if enabled and not override else 0., places=2) + self.assertAlmostEqual(values['aReqValue'], history, places=2) + self.assertEqual(values['JerkUpperLimit'], 1.5) + self.assertEqual(values['JerkLowerLimit'], lower) + self.assertEqual(values['ACCMode'], 0 if not enabled else 2 if override else 1) + self.assertEqual(values['StopReq'], int(stop)) + self.assertEqual(values['MainMode_ACC'], 1) + self.assertAlmostEqual(ci.CC.accel_last, history) + + def test_stock_brake_cancel_scope_through_actual_controller(self): + for car, brake, lateral, suppress, lka, alt in ( + (CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, True, True, True, True, False), + (CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, False, True, False, True, False), + (CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, True, False, False, True, False), + (CAR.KIA_EV6, True, True, False, True, False), + (CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, True, True, True, False, False), + (CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, False, True, False, False, False), + (CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, True, True, True, False, True), + (CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, False, True, False, False, True)): + cp = params(car, False, lka=lka, alt=alt) + self.assertEqual(bool(cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG), lka) + self.assertEqual(bool(cp.flags & HyundaiFlags.CANFD_ALT_BUTTONS), alt) + self.assertEqual(suppress_stock_cancel(cp, brake, lateral), suppress) + ci = CarInterface(cp) + # Non-LKA parsers lazily subscribe TCS on the first actual state update. + # Prime registration before the first observed brake frame. + ci.update([]) + packer = CANPacker(DBC[car][Bus.pt]) + command = structs.CarControl() + command.cruiseControl.cancel = True + command.latActive = lateral + buttons = [] + for tick in range(60): + state = update(ci, packer, tick, brake) + self.assertEqual(state.brakePressed, brake, (car, lka, alt, tick)) + _, outputs = ci.apply(command.as_reader(), (tick+1)*10_000_000) + buttons.extend(frame for frame in outputs if frame[0] == (0x1a0 if alt else 0x1cf)) + self.assertEqual(bool(buttons), not suppress) + + def test_raw_optional_keyword_preserves_generic_and_ioniq_defaults(self): + for car in (CAR.KIA_EV6, CAR.HYUNDAI_IONIQ_6): + cp = params(car) + first, second = CANPacker(DBC[car][Bus.pt]), CANPacker(DBC[car][Bus.pt]) + args = (CanBus(cp), True, 0., .5, False, False, 80., structs.CarControl.HUDControl()) + self.assertEqual(create_acc_control(first, *args), create_acc_control(second, *args, raw_accel=None)) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_gv70_radar.py b/opendbc_repo/opendbc/car/hyundai/tests/test_gv70_radar.py new file mode 100644 index 0000000000..dbc95ca374 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_gv70_radar.py @@ -0,0 +1,126 @@ +import unittest + +from opendbc.can import CANPacker +from opendbc.car import Bus, gen_empty_fingerprint +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.radar_interface import RadarInterface, get_radar_can_parser +from opendbc.car.hyundai.values import CAR, DBC, HYUNDAI_GV70_RADAR_DBC, HYUNDAI_MRR35_RADAR_DBC + +CAR_ID = CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN + + +def params(bank=None): + fp = gen_empty_fingerprint() + fp[0][0x1CF] = 8 + if bank is not None: + fp[0].update(bank) + return CarInterface.get_params(CAR_ID, fp, [], False, False, False) + + +class TestGv70Radar(unittest.TestCase): + def test_complete_exact_bank_required_without_changing_controls(self): + complete = dict.fromkeys(range(0x210, 0x220), 32) + absent = params() + active = params(complete) + self.assertTrue(absent.radarUnavailable) + self.assertFalse(active.radarUnavailable) + self.assertEqual(absent.flags, active.flags) + self.assertEqual([(cfg.safetyModel, cfg.safetyParam) for cfg in absent.safetyConfigs], + [(cfg.safetyModel, cfg.safetyParam) for cfg in active.safetyConfigs]) + self.assertEqual(absent.openpilotLongitudinalControl, active.openpilotLongitudinalControl) + for address in complete: + bank = complete.copy() + del bank[address] + self.assertTrue(params(bank).radarUnavailable) + bank[address] = 24 + self.assertTrue(params(bank).radarUnavailable) + self.assertEqual(DBC[CAR_ID][Bus.radar], HYUNDAI_GV70_RADAR_DBC) + self.assertEqual(DBC[CAR.GENESIS_GV70_ELECTRIFIED_2ND_GEN][Bus.radar], HYUNDAI_MRR35_RADAR_DBC) + + def test_existing_non_gv70_radar_parser_bus_and_bank_unchanged(self): + expected = { + 'hyundai_kia_mando_front_radar_generated': (1, 0x500, 32), + 'hyundai_mrrevo14f_radar_generated': (1, 0x602, 16), + 'hyundai_mrr30_radar_generated': (0, 0x210, 16), + 'hyundai_mrr35_radar_generated': (0, 0x3A5, 32), + } + checked = set() + for car in CAR: + dbc = DBC[car].get(Bus.radar) + if dbc not in expected or dbc in checked: + continue + cp = CarInterface.get_params(car, gen_empty_fingerprint(), [], False, False, False) + radar = RadarInterface(cp) + parser = get_radar_can_parser(cp) + bus, start, count = expected[dbc] + if car == CAR.HYUNDAI_IONIQ_6: + continue + self.assertEqual((parser.bus, radar.start_addr, radar.msg_count), (bus, start, count)) + checked.add(dbc) + self.assertEqual(checked, set(expected)) + + def test_all_32_original_target_slots_remain_distinct(self): + radar = RadarInterface(params(dict.fromkeys(range(0x210, 0x220), 32))) + packer = CANPacker(HYUNDAI_GV70_RADAR_DBC) + frames = [] + for ordinal, addr in enumerate(range(0x210, 0x220)): + values = {} + for slot in (1, 2): + target = ordinal * 2 + slot + values.update({f'{slot}_STATE': 3 if slot == 1 else 4, + f'{slot}_LONG_DIST': target, f'{slot}_LAT_DIST': -.05 * target, + f'{slot}_REL_SPEED': -.01 * target}) + frames.append(packer.make_can_msg(f'RADAR_TRACK_{addr:x}', 0, values)) + result = radar.update((1_000_000_000, frames)) + self.assertFalse(result.errors.canError) + self.assertEqual(len(result.points), 32) + self.assertEqual(len({point.trackId for point in result.points}), 32) + for ordinal, point in enumerate(result.points, 1): + self.assertAlmostEqual(point.dRel, ordinal) + self.assertAlmostEqual(point.yRel, -.05 * ordinal, places=6) + self.assertAlmostEqual(point.vRel, -.01 * ordinal, places=6) + + def test_original_layout_to_current_typed_points_and_freshness(self): + radar = RadarInterface(params(dict.fromkeys(range(0x210, 0x220), 32))) + self.assertEqual((radar.rcp.bus, radar.start_addr, radar.msg_count), (0, 0x210, 16)) + packer = CANPacker(HYUNDAI_GV70_RADAR_DBC) + frames = [packer.make_can_msg(f'RADAR_TRACK_{addr:x}', 0, {}) for addr in range(0x210, 0x220)] + frames[0] = packer.make_can_msg('RADAR_TRACK_210', 0, { + '1_STATE': 3, '1_LONG_DIST': 42.5, '1_LAT_DIST': -1.25, '1_REL_SPEED': -2.5, + '2_STATE': 4, '2_LONG_DIST': 18.0, '2_LAT_DIST': 1.5, '2_REL_SPEED': 3.25, + }) + data = frames[0][1] + self.assertEqual(len(data), 32) + # Independent raw extraction of original bit54/182 Motorola state and + # little-endian bit64/192 distance, bit76/204 signed lateral, bit88/216 speed. + expected = [] + for offset in (0, 16): + self.assertEqual((data[offset + 6] >> 4) & 7, 3 if offset == 0 else 4) + raw = int.from_bytes(data[offset + 8:offset + 13], 'little') + distance = (raw & 0xFFF) * .05 + lateral = (raw >> 12) & 0xFFF + speed = (raw >> 24) & 0x3FFF + expected.append((distance, (lateral - 4096 if lateral & 2048 else lateral) * .05, + (speed - 16384 if speed & 8192 else speed) * .01)) + self.assertIsNone(radar.update((1_000_000_000, [(addr, dat, 1) for addr, dat, _ in frames]))) + self.assertIsNone(radar.update((1_010_000_000, [(addr, dat[:24], bus) for addr, dat, bus in frames]))) + result = radar.update((1_020_000_000, frames)) + self.assertEqual(len(result.points), 2) + ids = [point.trackId for point in result.points] + for point, values in zip(result.points, expected, strict=True): + self.assertAlmostEqual(point.dRel, values[0]) + self.assertAlmostEqual(point.yRel, values[1]) + self.assertAlmostEqual(point.vRel, values[2]) + result = radar.update((1_040_000_000, frames)) + self.assertEqual([point.trackId for point in result.points], ids) + self.assertEqual(len(radar.update((1_090_000_000, [frames[-1]])).points), 0) + self.assertIsNone(radar.update((1_100_000_000, [frames[0]]))) + self.assertEqual(len(radar.update((1_160_000_000, [frames[-1]])).points), 0) + # Valid→invalid must remove tracks, never retain an earlier target. + radar.update((1_200_000_000, frames)) + frames[0] = packer.make_can_msg('RADAR_TRACK_210', 0, {'1_STATE': 2, '2_STATE': 7}) + self.assertEqual(len(radar.update((1_220_000_000, frames)).points), 0) + + +if __name__ == '__main__': + unittest.main() diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_gv70_template.py b/opendbc_repo/opendbc/car/hyundai/tests/test_gv70_template.py new file mode 100644 index 0000000000..d8219602a7 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_gv70_template.py @@ -0,0 +1,70 @@ +"""Synthetic CRC-valid fixtures qualify arithmetic, not GV70 hardware provenance.""" +from dataclasses import FrozenInstanceError +import math +import unittest + +from opendbc.car.hyundai.ev6_template import EV6Template +from opendbc.car.hyundai.gv70_template import GV70Template + + +def crc(data): + value = 0 + for byte in data[2:] + bytes((0x51, 0)): + value ^= byte << 8 + for _ in range(8): + value = ((value << 1) ^ (0x1021 if value & 0x8000 else 0)) & 65535 + return value ^ 0x9f5b + + +def capture(counter=254): + data = bytearray(range(32)) + data[2] = counter + data[:2] = crc(data).to_bytes(2, 'little') + return bytes(data) + + +class TestGV70Template(unittest.TestCase): + def test_finite_speed_round_clip_counter_drive_and_crc(self): + for counter in (0, 254, 255): + source = capture(counter) + template = GV70Template.capture(source) + for frame in (0, 1, 255, 256): + for drive in (False, True): + for speed in (-1., 0., .005, .015, 1.005, 12.345, 655.335, 655.34, 655.345, 1000.): + with self.subTest(counter=counter, frame=frame, drive=drive, speed=speed): + expected = bytearray(source) + expected[2] = (counter + frame + 1) & 255 + expected[3] = (source[3] & ~1) | int(drive) + expected[8:10] = min(max(round(speed * 100.), 0), 65534).to_bytes(2, 'little') + expected[:2] = crc(expected).to_bytes(2, 'little') + self.assertEqual(template.frame(frame, drive, speed=speed), (0x51, bytes(expected), 0)) + self.assertEqual(template.data, source) + + def test_absent_and_nonfinite_speed_preserve_capture(self): + source = capture() + descriptor = GV70Template.capture(source) + ev6 = EV6Template.capture(source) + for speed in (None, math.nan, math.inf, -math.inf): + self.assertEqual(descriptor.frame(7, True, speed=speed), ev6.frame(7, True)) + self.assertEqual(descriptor.frame(7, True, speed=speed).dat[8:10], source[8:10]) + + def test_capture_integrity_instances_and_ev6_signature_remain_closed(self): + source = capture() + descriptor = GV70Template.capture(source) + for instance in (descriptor, EV6Template.capture(source)): + for name, value in (('data', bytes(32)), ('frame', None), ('_finish', None), ('extra', 1)): + with self.subTest(descriptor=type(instance).__name__, field=name): + with self.assertRaises(FrozenInstanceError): + setattr(instance, name, value) + bad = bytearray(source) + bad[12] ^= 1 + for value in (b'', source[:-1], source+b'0', bytearray(source), bytes(bad)): + with self.assertRaises(ValueError): + GV70Template.capture(value) + with self.assertRaises(TypeError): + EV6Template.capture(source).frame(0, False, speed=123.) + other = GV70Template.capture(capture(0)) + self.assertNotEqual(descriptor.frame(0, False).dat[2], other.frame(0, False).dat[2]) + for args in ((-1, False, 0), (True, False, 0), (0, 1, 0), (0, False, 1)): + with self.assertRaises(ValueError): + descriptor.frame(*args, speed=1.) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py index 9a899e6dac..3afd36438e 100644 --- a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py @@ -1,12 +1,17 @@ import unittest +from opendbc.can import CANPacker, CANParser +from opendbc.car import Bus, structs from opendbc.car import gen_empty_fingerprint from opendbc.car.structs import CarParams -from opendbc.car.fw_versions import build_fw_dict +from opendbc.car.fw_versions import build_fw_dict, match_fw_to_car +from opendbc.car.hyundai.carstate import CarState +from opendbc.car.hyundai.carcontroller import CarController from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai import hyundaican from opendbc.car.hyundai.hyundaicanfd import CanBus from opendbc.car.hyundai.radar_interface import RADAR_START_ADDR -from opendbc.car.hyundai.values import CAR, DATE_FW_ECUS, FW_QUERY_CONFIG, CANFD_FUZZY_WHITELIST, \ +from opendbc.car.hyundai.values import CAR, DBC, DATE_FW_ECUS, FW_QUERY_CONFIG, CANFD_FUZZY_WHITELIST, \ PLATFORM_CODE_ECUS, HYUNDAI_VERSION_REQUEST_LONG, \ HyundaiFlags, get_platform_codes, HyundaiSafetyFlags from opendbc.car.hyundai.fingerprints import FW_VERSIONS @@ -19,12 +24,24 @@ Ecu = CarParams.Ecu NO_DATES_PLATFORMS = { # CAN FD CAR.KIA_SPORTAGE_5TH_GEN, + CAR.KIA_SPORTAGE_2026, + CAR.KIA_SPORTAGE_HEV_2026, CAR.HYUNDAI_SANTA_CRUZ_1ST_GEN, + CAR.HYUNDAI_SANTA_CRUZ_2025, CAR.HYUNDAI_TUCSON_4TH_GEN, + CAR.HYUNDAI_TUCSON_2025, + CAR.HYUNDAI_TUCSON_HEV_2025, + CAR.HYUNDAI_TUCSON_PHEV_2025, # CAN CAR.HYUNDAI_ELANTRA, CAR.HYUNDAI_ELANTRA_GT_I30, CAR.KIA_CEED, + CAR.KIA_XCEED_PHEV, + CAR.HYUNDAI_BAYON_1ST_GEN_NON_SCC, + CAR.HYUNDAI_KONA_NON_SCC, + CAR.KIA_CEED_PHEV_2022_NON_SCC, + CAR.KIA_FORTE_2019_NON_SCC, + CAR.KIA_FORTE_2021_NON_SCC, CAR.KIA_CEED_PHEV, CAR.KIA_FORTE, CAR.KIA_OPTIMA_G4, @@ -52,6 +69,293 @@ def cars_with(flags): class TestHyundaiFingerprint(unittest.TestCase): + + def test_k4_shared_platform_physical_button_events(self): + from opendbc.car.hyundai.values import Buttons + button_type = structs.CarState.ButtonEvent.Type + signals = (("LDA_BTN", 1, button_type.lkas), ("ADAPTIVE_CRUISE_MAIN_BTN", 1, button_type.mainCruise), + ("CRUISE_BUTTONS", Buttons.RES_ACCEL, button_type.accelCruise), + ("CRUISE_BUTTONS", Buttons.SET_DECEL, button_type.decelCruise), + ("CRUISE_BUTTONS", Buttons.CANCEL, button_type.cancel)) + for alpha_long in (False, True): + for alternate in (False, True): + for signal, value, expected in signals: + with self.subTest(alpha_long=alpha_long, alternate=alternate, signal=signal, value=value): + fingerprint = gen_empty_fingerprint() + if not alternate: + fingerprint[0][0x1CF] = 8 + cp = CarInterface.get_params(CAR.KIA_K4_2025, fingerprint, [], alpha_long, False, False) + self.assertEqual(cp.carFingerprint, CAR.KIA_K4_2025) + self.assertEqual(bool(cp.flags & HyundaiFlags.CANFD_ALT_BUTTONS), alternate) + state = CarState(cp) + parsers = state.get_can_parsers(cp) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + state.update(parsers) + for frame, sample in enumerate((0, value, 0), 1): + message = packer.make_can_msg(state.cruise_btns_msg_canfd, CanBus(cp).ECAN, {signal: sample}) + parsers[Bus.pt].update([(frame * 10_000_000, [message])]) + events = state.update(parsers).buttonEvents + actual = [(event.type, event.pressed) for event in events] + self.assertEqual(actual, [] if frame == 1 else [(expected, frame == 2)]) + + def test_eight_ordinary_ccnc_interfaces_and_display_frames(self): + family = ( + CAR.HYUNDAI_KONA_2ND_GEN, CAR.HYUNDAI_KONA_HEV_2ND_GEN, CAR.HYUNDAI_SANTA_CRUZ_2025, + CAR.HYUNDAI_SONATA_2024, CAR.HYUNDAI_SONATA_HEV_2024, CAR.HYUNDAI_TUCSON_2025, + CAR.HYUNDAI_TUCSON_HEV_2025, CAR.KIA_K5_2025, + ) + for car_model in family: + for alpha_long in (False, True): + with self.subTest(car_model=car_model, alpha_long=alpha_long): + cp = CarInterface.get_params(car_model, gen_empty_fingerprint(), [], alpha_long, False, False) + self.assertTrue(cp.flags & HyundaiFlags.CCNC) + self.assertFalse(cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG) + self.assertEqual(bool(cp.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CCNC), True) + self.assertEqual(bool(cp.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.LONG), alpha_long) + self.assertFalse(cp.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CAN_REFRESH_MSGS) + state = CarState(cp) + parsers = state.get_can_parsers(cp) + cam = parsers[Bus.cam] + self.assertTrue(cam.can_valid) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + can = CanBus(cp) + output_parser = CANParser(DBC[cp.carFingerprint][Bus.pt], [("CCNC_0x161", 0), ("CCNC_0x162", 0)], can.ECAN) + controller = CarController(DBC[cp.carFingerprint], cp) + control = structs.CarControl() + control.enabled = control.latActive = True + control.longActive = alpha_long + control.hudControl.setSpeed = 20.0 + # Establish the controller's startup baseline before camera display arrives. + state.out = state.update(parsers) + controller.update(control.as_reader(), state, 990_000_000) + controller.frame = 5 + stock_161 = {"COUNTER": 7, "BACKGROUND": 3, "SETSPEED_SPEED": 83, + "ALERTS_2": 4, "ALERTS_3": 7, "ALERTS_5": 4, "SOUNDS_3": 5} + stock_162 = {"COUNTER": 9, "SPEEDLIMIT": 60, "FAULT_DAS": 1, "LEAD_DISTANCE": 25} + incoming = [packer.make_can_msg("CCNC_0x161", can.CAM, stock_161), + packer.make_can_msg("CCNC_0x162", can.CAM, stock_162), + packer.make_can_msg("FR_CMR_03_50ms", can.CAM, {"Longitudinal_Distance": 47})] + self.assertEqual(cam.update((1_000_000_000, incoming)), {0x161, 0x162, 0x1b5}) + self.assertEqual([cam.ts_nanos[addr]["COUNTER"] for addr in (0x161, 0x162)], [1_000_000_000] * 2) + state.out = state.update(parsers) + state.out.vCruiseCluster = 72.0 # car/card's current driver-selected display speed + _, sent = controller.update(control.as_reader(), state, 1_000_000_000) + displays = [msg for msg in sent if msg[0] in (0x161, 0x162)] + self.assertEqual([(addr, bus, len(data)) for addr, data, bus in displays], + [(0x161, can.ECAN, 32), (0x162, can.ECAN, 32)]) + self.assertFalse(any(addr in (0x160, 0x1E0, 0x7C4, 0xEA) and bus == can.ECAN for addr, _, bus in sent)) + output_parser.update((1_010_000_000, displays)) + self.assertEqual(output_parser.vl["CCNC_0x161"]["COUNTER"], 7) + self.assertEqual(output_parser.vl["CCNC_0x162"]["COUNTER"], 9) + self.assertEqual(output_parser.vl["CCNC_0x161"]["BACKGROUND"], 3) + self.assertEqual(output_parser.vl["CCNC_0x162"]["SPEEDLIMIT"], 60) + self.assertEqual(output_parser.vl["CCNC_0x162"]["FAULT_DAS"], 1) + self.assertEqual(output_parser.vl["CCNC_0x162"]["LEAD_DISTANCE"], 47 if alpha_long else 25) + for signal, expected in (("ALERTS_3", 7), ("ALERTS_5", 4), ("SOUNDS_3", 5)): + self.assertEqual(output_parser.vl["CCNC_0x161"][signal], expected) + self.assertEqual(output_parser.vl["CCNC_0x161"]["SETSPEED_SPEED"], 72 if alpha_long else 83) + # No new camera pair means no second CCNC emission at the next 20 Hz slot. + for tick in range(1, 6): + _, held = controller.update(control.as_reader(), state, 1_000_000_000 + tick * 10_000_000) + self.assertFalse(any(addr in (0x161, 0x162) for addr, _, _ in held)) + + # A new 161 alone cannot be paired with held 162, and old 1b5 + # camera geometry/lead distance cannot overwrite a later pair. + next_161 = packer.make_can_msg("CCNC_0x161", can.CAM, {**stock_161, "COUNTER": 8}) + cam.update((1_200_000_000, [next_161])) + state.out = state.update(parsers) + controller.frame = 15 + _, unpaired = controller.update(control.as_reader(), state, 1_200_000_000) + self.assertFalse(any(addr in (0x161, 0x162) for addr, _, _ in unpaired)) + next_162 = packer.make_can_msg("CCNC_0x162", can.CAM, {**stock_162, "COUNTER": 10, "LEAD_DISTANCE": 29}) + cam.update((1_210_000_000, [next_162])) + state.out = state.update(parsers) + controller.frame = 20 + _, paired = controller.update(control.as_reader(), state, 1_210_000_000) + display_pair = [msg for msg in paired if msg[0] in (0x161, 0x162)] + self.assertEqual([msg[0] for msg in display_pair], [0x161, 0x162]) + output_parser.update((1_220_000_000, display_pair)) + self.assertEqual(output_parser.vl["CCNC_0x162"]["LEAD_DISTANCE"], 29) + + # Reversing the controller clock baselines the held pair again. + controller.frame = 25 + _, reversed_clock = controller.update(control.as_reader(), state, 1_100_000_000) + self.assertFalse(any(addr in (0x161, 0x162) for addr, _, _ in reversed_clock)) + fresh_pair = [packer.make_can_msg("CCNC_0x161", can.CAM, {**stock_161, "COUNTER": 9}), + packer.make_can_msg("CCNC_0x162", can.CAM, {**stock_162, "COUNTER": 11})] + cam.update((1_300_000_000, fresh_pair)) + state.out = state.update(parsers) + controller.frame = 30 + _, resumed = controller.update(control.as_reader(), state, 1_300_000_000) + self.assertEqual([msg[0] for msg in resumed if msg[0] in (0x161, 0x162)], [0x161, 0x162]) + + # Paired timestamps alone are insufficient if the control loop has + # not emitted them until well after that display cycle. + late_pair = [packer.make_can_msg("CCNC_0x161", can.CAM, {**stock_161, "COUNTER": 10}), + packer.make_can_msg("CCNC_0x162", can.CAM, {**stock_162, "COUNTER": 12})] + cam.update((1_400_000_000, late_pair)) + state.out = state.update(parsers) + controller.frame = 35 + _, late = controller.update(control.as_reader(), state, 2_000_000_000) + self.assertFalse(any(msg[0] in (0x161, 0x162) for msg in late)) + + control.latActive = False + inactive_pair = [packer.make_can_msg("CCNC_0x161", can.CAM, + {**stock_161, "COUNTER": 11, "LFA_ICON": 2, "CENTERLINE": 1}), + packer.make_can_msg("CCNC_0x162", can.CAM, {**stock_162, "COUNTER": 13})] + cam.update((2_050_000_000, inactive_pair)) + state.out = state.update(parsers) + controller.frame = 40 + _, inactive = controller.update(control.as_reader(), state, 2_050_000_000) + display_pair = [msg for msg in inactive if msg[0] in (0x161, 0x162)] + self.assertEqual(len(display_pair), 2) + output_parser.update((2_060_000_000, display_pair)) + for signal in ("LFA_ICON", "CENTERLINE", "LANELINE_LEFT", "LANELINE_RIGHT"): + self.assertEqual(output_parser.vl["CCNC_0x161"][signal], 0) + self.assertEqual(output_parser.vl["CCNC_0x162"]["FAULT_DAS"], 1) + + def test_ccnc_permission_requires_observed_non_lka_camera_topology(self): + fingerprint = gen_empty_fingerprint() + cam_bus = CanBus(None, fingerprint).CAM + fingerprint[cam_bus][0x50] = 16 + cp = CarInterface.get_params(CAR.HYUNDAI_KONA_2ND_GEN, fingerprint, [], False, False, False) + self.assertTrue(cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG) + self.assertFalse(cp.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CCNC) + cam = CarState(cp).get_can_parsers(cp)[Bus.cam] + self.assertNotIn("CCNC_0x161", cam.vl) + self.assertNotIn("CCNC_0x162", cam.vl) + + def test_elantra_hev_2024_optional_buttons_do_not_require_reception(self): + cp = CarInterface.get_params(CAR.HYUNDAI_ELANTRA_HEV_2024, gen_empty_fingerprint(), [], False, False, False) + pt = CarState(cp).get_can_parsers(cp)[Bus.pt] + # Only the two optional button subscriptions exist before ordinary signals + # are accessed. Missing either button message must not invalidate CAN health. + for tick in range(100): + pt.update((1_000_000_000 + tick * 10_000_000, [])) + self.assertTrue(pt.can_valid) + + def test_ioniq6_left_paddle_is_factual_button_event(self): + for alpha_long in (False, True): + with self.subTest(alpha_long=alpha_long): + cp = CarInterface.get_params(CAR.HYUNDAI_IONIQ_6, gen_empty_fingerprint(), [], alpha_long, False, False) + state = CarState(cp) + parsers = state.get_can_parsers(cp) + pt = parsers[Bus.pt] + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + bus = CanBus(cp).ECAN + + def update(value: int, frame: int, *, packer=packer, bus=bus, pt=pt, state=state, parsers=parsers): + msg = packer.make_can_msg("CRUISE_BUTTONS", bus, {"SET_ME_1": 1, "LEFT_PADDLE": value}) + pt.update((frame * 20_000_000, [msg])) + return state.update(parsers) + + update(0, 1) + pressed = update(1, 2) + held = update(1, 3) + released = update(0, 4) + button = structs.CarState.ButtonEvent.Type.altButton2 + self.assertEqual([(event.type, event.pressed) for event in pressed.buttonEvents if event.type == button], [(button, True)]) + self.assertFalse(any(event.type == button for event in held.buttonEvents)) + self.assertEqual([(event.type, event.pressed) for event in released.buttonEvents if event.type == button], [(button, False)]) + + def test_elantra_hev_2024_refresh_contract(self): + for alpha_long in (False, True): + with self.subTest(alpha_long=alpha_long): + cp = CarInterface.get_params(CAR.HYUNDAI_ELANTRA_HEV_2024, gen_empty_fingerprint(), [], alpha_long, False, False) + self.assertEqual(cp.carFingerprint, CAR.HYUNDAI_ELANTRA_HEV_2024) + self.assertEqual(CAR.HYUNDAI_ELANTRA_HEV_2024.config.dbc_dict[Bus.pt], "hyundai_can_refresh_generated") + self.assertEqual(cp.openpilotLongitudinalControl, alpha_long) + self.assertAlmostEqual(cp.longitudinalActuatorDelay, 0.22) + self.assertEqual(int(cp.safetyConfigs[0].safetyParam), int(HyundaiSafetyFlags.CAMERA_SCC | HyundaiSafetyFlags.CAN_REFRESH_MSGS | + HyundaiSafetyFlags.HYBRID_GAS | (HyundaiSafetyFlags.LONG if alpha_long else 0))) + prior = CarInterface.get_params(CAR.HYUNDAI_ELANTRA_HEV_2021, gen_empty_fingerprint(), [], alpha_long, False, False) + self.assertEqual(cp.mass, prior.mass) + + ecu_versions = FW_VERSIONS[CAR.HYUNDAI_ELANTRA_HEV_2024] + self.assertEqual({ecu for ecu, _, _ in ecu_versions}, {Ecu.fwdRadar, Ecu.eps, Ecu.fwdCamera}) + self.assertEqual(sum(map(len, ecu_versions.values())), 8) + + def test_elantra_hev_2024_exact_firmware_tuple(self): + fw = FW_VERSIONS[CAR.HYUNDAI_ELANTRA_HEV_2024] + car_fw = [CarParams.CarFw(ecu=ecu, address=addr, subAddress=0, fwVersion=versions[-1], brand="hyundai") + for (ecu, addr, _), versions in fw.items()] + exact, matches = match_fw_to_car(car_fw, "", allow_exact=True, allow_fuzzy=False, log=False) + self.assertTrue(exact) + self.assertEqual(matches, {CAR.HYUNDAI_ELANTRA_HEV_2024}) + + def test_elantra_hev_2024_parser_pulse_and_hybrid_signals(self): + cp = CarInterface.get_params(CAR.HYUNDAI_ELANTRA_HEV_2024, gen_empty_fingerprint(), [], False, False, False) + state = CarState(cp) + parsers = state.get_can_parsers(cp) + pt = parsers[Bus.pt] + packer = CANPacker("hyundai_can_refresh_generated") + state.update(parsers) # subscribe to ordinary dynamically accessed signals + + pulse = [packer.make_can_msg("BCM_PO_11", 0, {"LDA_BTN": value}) for value in (1, 0)] + signals = [packer.make_can_msg("E_EMS11", 0, {"CR_Vcu_AccPedDep_Pos": 3}), + packer.make_can_msg("ELECT_GEAR", 0, {"Elect_Gear_Shifter": 5}), + packer.make_can_msg("TCS13", 0, {"DriverOverride": 2})] + pt.update((1_000_000_000, [*pulse, *signals])) + self.assertEqual(pt.vl_all["BCM_PO_11"]["LDA_BTN"], [1, 0]) + ret = state.update(parsers) + self.assertTrue(ret.gasPressed) + self.assertTrue(ret.brakePressed) + self.assertEqual(ret.gearShifter, structs.CarState.GearShifter.drive) + self.assertTrue(any(event.type == structs.CarState.ButtonEvent.Type.lkas and event.pressed for event in ret.buttonEvents)) + + cleared_signals = [packer.make_can_msg("CLU13", 0, {"CF_Clu_LdwsLkasSW": 0}), + packer.make_can_msg("E_EMS11", 0, {"CR_Vcu_AccPedDep_Pos": 0}), + packer.make_can_msg("ELECT_GEAR", 0, {"Elect_Gear_Shifter": 0}), + packer.make_can_msg("TCS13", 0, {"DriverOverride": 0})] + pt.update((1_010_000_000, cleared_signals)) + released = state.update(parsers) + self.assertTrue(any(event.type == structs.CarState.ButtonEvent.Type.lkas and not event.pressed for event in released.buttonEvents)) + self.assertFalse(released.gasPressed) + self.assertFalse(released.brakePressed) + self.assertEqual(released.gearShifter, structs.CarState.GearShifter.park) + + def test_elantra_hev_2024_cluster_button_short_pulse(self): + cp = CarInterface.get_params(CAR.HYUNDAI_ELANTRA_HEV_2024, gen_empty_fingerprint(), [], False, False, False) + state = CarState(cp) + parsers = state.get_can_parsers(cp) + pt = parsers[Bus.pt] + packer = CANPacker("hyundai_can_refresh_generated") + state.update(parsers) + pulse = [packer.make_can_msg("CLU13", 0, {"CF_Clu_LdwsLkasSW": value}) for value in (1, 0)] + pt.update((1_000_000_000, pulse)) + self.assertEqual(pt.vl_all["CLU13"]["CF_Clu_LdwsLkasSW"], [1, 0]) + pressed = state.update(parsers) + self.assertTrue(any(event.type == structs.CarState.ButtonEvent.Type.lkas and event.pressed for event in pressed.buttonEvents)) + pt.update((1_010_000_000, [packer.make_can_msg("CLU13", 0, {"CF_Clu_LdwsLkasSW": 0})])) + released = state.update(parsers) + self.assertTrue(any(event.type == structs.CarState.ButtonEvent.Type.lkas and not event.pressed for event in released.buttonEvents)) + + def test_elantra_2024_refresh_contract(self): + for alpha_long in (False, True): + with self.subTest(alpha_long=alpha_long): + cp = CarInterface.get_params(CAR.HYUNDAI_ELANTRA_2024, gen_empty_fingerprint(), [], alpha_long, False, False) + self.assertEqual(cp.carFingerprint, CAR.HYUNDAI_ELANTRA_2024) + self.assertEqual(CAR.HYUNDAI_ELANTRA_2024.config.dbc_dict[Bus.pt], "hyundai_can_refresh_generated") + self.assertEqual(cp.openpilotLongitudinalControl, alpha_long) + self.assertTrue(cp.safetyConfigs[0].safetyParam & HyundaiSafetyFlags.CAMERA_SCC) + self.assertTrue(cp.safetyConfigs[0].safetyParam & HyundaiSafetyFlags.CAN_REFRESH_MSGS) + self.assertEqual(bool(cp.safetyConfigs[0].safetyParam & HyundaiSafetyFlags.LONG), alpha_long) + + ecu_versions = FW_VERSIONS[CAR.HYUNDAI_ELANTRA_2024] + self.assertEqual({ecu for ecu, _, _ in ecu_versions}, {Ecu.fwdRadar, Ecu.eps, Ecu.fwdCamera, Ecu.abs}) + self.assertEqual(sum(map(len, ecu_versions.values())), 5) + + def test_elantra_2024_refresh_dbc_round_trip(self): + packer = CANPacker("hyundai_can_refresh_generated") + parser = CANParser("hyundai_can_refresh_generated", [("LFAHDA_MFC", 0)], 0) + msg = packer.make_can_msg("LFAHDA_MFC", 0, {"LFA_Icon_State": 2}) + self.assertEqual((msg[0], msg[2], len(msg[1])), (0x485, 0, 8)) + self.assertEqual(parser.update((1_000_000_000, [msg])), {0x485}) + self.assertEqual(parser.vl["LFAHDA_MFC"]["LFA_Icon_State"], 2) + old_msg = CANPacker("hyundai_can_generated").make_can_msg("LFAHDA_MFC", 0, {"LFA_Icon_State": 2}) + self.assertEqual(len(old_msg[1]), 4) + self.assertEqual(len(hyundaican.create_lfahda_mfc(packer, True)[1]), 8) + def test_feature_detection(self): # LKA steering for lka_steering in (True, False): @@ -97,10 +401,10 @@ class TestHyundaiFingerprint(unittest.TestCase): def test_canfd_ecu_whitelist(self): # Asserts only expected Ecus can exist in database for CAN-FD cars - for car_model in CAR: + for car_model, fw_versions in FW_VERSIONS.items(): if not (car_model.config.flags & HyundaiFlags.CANFD): continue - ecus = {fw[0] for fw in FW_VERSIONS[car_model].keys()} + ecus = {fw[0] for fw in fw_versions.keys()} ecus_not_in_whitelist = ecus - CANFD_EXPECTED_ECUS ecu_strings = ", ".join([f"Ecu.{ecu}" for ecu in ecus_not_in_whitelist]) assert len(ecus_not_in_whitelist) == 0, \ @@ -147,7 +451,7 @@ class TestHyundaiFingerprint(unittest.TestCase): # Third and fourth character are usually EV/hybrid identifiers codes = {code.split(b"-")[0][:2] for code, _ in get_platform_codes(fws)} - if car_model == CAR.HYUNDAI_PALISADE: + if car_model in (CAR.HYUNDAI_PALISADE, CAR.HYUNDAI_PALISADE_2023): assert codes == {b"LX", b"ON"}, f"Car has unexpected platform codes: {car_model} {codes}" elif car_model == CAR.HYUNDAI_KONA_EV and ecu[0] == Ecu.fwdCamera: assert codes == {b"OE", b"OS"}, f"Car has unexpected platform codes: {car_model} {codes}" @@ -158,8 +462,12 @@ class TestHyundaiFingerprint(unittest.TestCase): # fingerprint in the absence of full FW matches: def test_platform_code_ecus_available(self): # TODO: add queries for these non-CAN FD cars to get EPS - no_eps_platforms = cars_with(HyundaiFlags.CANFD) | {CAR.KIA_SORENTO, CAR.KIA_OPTIMA_G4, CAR.KIA_OPTIMA_G4_FL, CAR.KIA_OPTIMA_H, CAR.KIA_K7_2017, - CAR.KIA_OPTIMA_H_G4_FL, CAR.HYUNDAI_SONATA_LF, CAR.HYUNDAI_TUCSON, CAR.GENESIS_G90, CAR.GENESIS_G80, CAR.HYUNDAI_ELANTRA} + no_eps_platforms = cars_with(HyundaiFlags.CANFD) | { + CAR.KIA_RAY_EV, + CAR.HYUNDAI_BAYON_1ST_GEN_NON_SCC, CAR.KIA_SORENTO, CAR.KIA_OPTIMA_G4, CAR.KIA_OPTIMA_G4_FL, + CAR.KIA_OPTIMA_H, CAR.KIA_K7_2017, CAR.KIA_OPTIMA_H_G4_FL, CAR.HYUNDAI_SONATA_LF, + CAR.HYUNDAI_TUCSON, CAR.GENESIS_G90, CAR.GENESIS_G80, CAR.HYUNDAI_ELANTRA, + } # Asserts ECU keys essential for fuzzy fingerprinting are available on all platforms for car_model, ecus in FW_VERSIONS.items(): @@ -167,6 +475,8 @@ class TestHyundaiFingerprint(unittest.TestCase): for platform_code_ecu in PLATFORM_CODE_ECUS: if platform_code_ecu in (Ecu.fwdRadar, Ecu.eps) and car_model == CAR.HYUNDAI_GENESIS: continue + if platform_code_ecu == Ecu.fwdRadar and car_model.config.flags & HyundaiFlags.NON_SCC: + continue if platform_code_ecu == Ecu.eps and car_model in no_eps_platforms: continue assert platform_code_ecu in [e[0] for e in ecus] @@ -188,6 +498,15 @@ class TestHyundaiFingerprint(unittest.TestCase): result = get_platform_codes([fw]) assert 1 == len(result), f"Unable to parse FW: {fw}" codes |= result + # Exact historical camera record has no embedded ECU part number. + # Keep every other record, including these platforms' radars, strict. + historical_no_part = ( + car_model in (CAR.KIA_SPORTAGE_2026, CAR.KIA_SPORTAGE_HEV_2026) and + ecu == (Ecu.fwdCamera, 0x7c4, None) and + fw == b'\xf1\x00NQ51.011.021.012551000HKP_NQ524_50509099211P1110' + ) + if car_model != CAR.HYUNDAI_GENESIS and not historical_no_part: + assert all(b"-" in code for code, _ in result), f"FW does not have part number: {fw}" if ecu[0] not in DATE_FW_ECUS or car_model in NO_DATES_PLATFORMS: assert all(date is None for _, date in codes) @@ -199,8 +518,6 @@ class TestHyundaiFingerprint(unittest.TestCase): # Hyundai places the ECU part number in their FW versions, assert all parsable # Some examples of valid formats: b"56310-L0010", b"56310L0010", b"56310/M6300" - assert all(b"-" in code for code, _ in codes), \ - f"FW does not have part number: {fw}" def test_platform_codes_spot_check(self): # Asserts basic platform code parsing behavior for a few cases @@ -236,9 +553,13 @@ class TestHyundaiFingerprint(unittest.TestCase): excluded_platforms = { CAR.GENESIS_G70, # shared platform code, part number, and date CAR.GENESIS_G70_2020, + CAR.GENESIS_G70_2021_NON_SCC, } excluded_platforms |= cars_with(HyundaiFlags.CANFD) - cars_with(HyundaiFlags.EV) - CANFD_FUZZY_WHITELIST # shared platform codes excluded_platforms |= NO_DATES_PLATFORMS # date codes are required to match + # Manual-only identities are absent from the FW database and cannot appear + # in the matcher loop below. + excluded_platforms &= FW_VERSIONS.keys() platforms_with_shared_codes = set() for platform, fw_by_addr in FW_VERSIONS.items(): diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq5pe_compatibility.py b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq5pe_compatibility.py new file mode 100644 index 0000000000..2fa10a40a7 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq5pe_compatibility.py @@ -0,0 +1,23 @@ +"""The legacy PE helper keeps its identity boundary after shared extraction.""" +import unittest + +from opendbc.car import structs +from opendbc.car.hyundai import ccnc_ev_stock, ioniq5pe +from opendbc.car.hyundai.tests.test_ioniq5pe_stock import params +from opendbc.car.hyundai.values import CAR + + +class TestIoniq5PECompatibility(unittest.TestCase): + def test_pe_api_rejects_ev9_while_shared_owner_accepts_both(self): + state = structs.CarState(canValid=True, gearShifter=structs.CarState.GearShifter.drive) + state.cruiseState.enabled = True + control = structs.CarControl(enabled=True, latActive=True) + for identity in (CAR.HYUNDAI_IONIQ_5_PE, CAR.KIA_EV9): + cp = params(candidate=identity) + expected = identity == CAR.HYUNDAI_IONIQ_5_PE + self.assertTrue(ccnc_ev_stock.qualified(cp)) + self.assertTrue(ccnc_ev_stock.request_allowed(cp, state)) + self.assertTrue(ccnc_ev_stock.replacement_requested(cp, control, state)) + self.assertEqual(ioniq5pe.qualified(cp), expected) + self.assertEqual(ioniq5pe.request_allowed(cp, state), expected) + self.assertEqual(ioniq5pe.replacement_requested(cp, control, state), expected) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq5pe_metadata.py b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq5pe_metadata.py new file mode 100644 index 0000000000..e4964e5b9a --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq5pe_metadata.py @@ -0,0 +1,44 @@ +import math +import unittest + +from opendbc.car import Bus, structs +from opendbc.car.hyundai.fingerprints import FW_VERSIONS +from opendbc.car.hyundai.values import CAR, HyundaiFlags +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.interfaces import get_torque_params + + +class TestIoniq5PEMetadata(unittest.TestCase): + def test_exact_platform_specs_and_angle_flags(self): + config = CAR.HYUNDAI_IONIQ_5_PE.config + self.assertEqual(config.specs, CAR.HYUNDAI_IONIQ_5.config.specs) + self.assertEqual((config.specs.mass, config.specs.wheelbase, config.specs.steerRatio, config.specs.tireStiffnessFactor), + (1948, 2.97, 14.26, 0.65)) + self.assertEqual(config.flags, HyundaiFlags.CANFD | HyundaiFlags.EV | HyundaiFlags.CANFD_ANGLE_STEERING | HyundaiFlags.CCNC) + self.assertEqual(config.dbc_dict[Bus.pt], "hyundai_canfd_generated") + self.assertEqual(config.dbc_dict[Bus.radar], "hyundai_mrr35_radar_generated") + self.assertNotEqual(config.specs, CAR.HYUNDAI_IONIQ_5_N.config.specs) + + def test_original_angle_metadata_allows_interface_constructor(self): + tune = get_torque_params()[CAR.HYUNDAI_IONIQ_5_PE] + self.assertEqual(tune["MAX_LAT_ACCEL_MEASURED"], 2.5) + self.assertTrue(math.isnan(tune["LAT_ACCEL_FACTOR"])) + self.assertTrue(math.isnan(tune["FRICTION"])) + cp = CarInterface.get_std_params(CAR.HYUNDAI_IONIQ_5_PE) + self.assertEqual(cp.maxLateralAccel, 2.5) + + def test_exact_firmware_entries_remain_distinct(self): + versions = FW_VERSIONS[CAR.HYUNDAI_IONIQ_5_PE] + radar = versions[(structs.CarParams.Ecu.fwdRadar, 0x7d0, None)] + camera = versions[(structs.CarParams.Ecu.fwdCamera, 0x7c4, None)] + self.assertEqual(len(versions), 2) + self.assertEqual(len(radar), 2) + self.assertEqual(len(camera), 4) + self.assertTrue(all(version.startswith(b"\xf1\x00NE__ RDR") for version in radar)) + self.assertTrue(all(version.startswith(b"\xf1\x00NE MFC") for version in camera)) + old_camera = FW_VERSIONS[CAR.HYUNDAI_IONIQ_5][(structs.CarParams.Ecu.fwdCamera, 0x7c4, None)] + self.assertFalse(set(camera) & set(old_camera)) + + +if __name__ == "__main__": + unittest.main() diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq5pe_sender.py b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq5pe_sender.py new file mode 100644 index 0000000000..2aa5084478 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq5pe_sender.py @@ -0,0 +1,36 @@ +"""Original PE reached-wire cases; ordinary ownership is tested separately.""" +import unittest +from types import SimpleNamespace + +from opendbc.can import CANPacker +from opendbc.car.hyundai.hyundaicanfd import create_angle_steering_messages +from opendbc.car.hyundai.values import CAR, HyundaiFlags + + +class TestIoniq5PESender(unittest.TestCase): + @staticmethod + def frame(candidate, angle, status): + flags = HyundaiFlags.CANFD_ANGLE_STEERING | HyundaiFlags.CANFD_LKA_STEER_MSG | HyundaiFlags.CANFD_LKA_STEER_MSG_ALT + cp = SimpleNamespace(carFingerprint=candidate, flags=flags, openpilotLongitudinalControl=False) + packer = CANPacker('hyundai_canfd_generated') + packer.counters[0x110] = 37 + return create_angle_steering_messages(packer, cp, SimpleNamespace(ACAN=0, ECAN=1), + True, True, angle, 0.5, status)[0] + + def test_original_pe_signed_half_step_quantization(self): + # Original DBC packer uses floor(angle / 0.1 + 0.5), not ties-to-even. + for angle, units in ((0.05, 1), (-0.15, -1), (5.0, 50), (-12.3, -123)): + with self.subTest(angle=angle): + address, data, bus = self.frame(CAR.HYUNDAI_IONIQ_5_PE, angle, {}) + raw = (data[11] << 6) | (data[10] >> 2) + signed = raw if raw < 8192 else raw - 16384 + self.assertEqual((address, bus, data[2], signed), (0x110, 0, 37, units)) + + def test_pe_active_status_reset_preserves_sibling_status_behavior(self): + stock = {'LKA_LHLnWrnSta': 2, 'LKA_RHLnWrnSta': 3, 'ToiFltSta': 2, 'LKA_UsmMod': 3} + _, pe, _ = self.frame(CAR.HYUNDAI_IONIQ_5_PE, 5.0, stock) + _, sibling, _ = self.frame(CAR.HYUNDAI_IONIQ_5_N, 5.0, stock) + # Source-derived original active body resets these fields to zero. + for data, expected in ((pe, (0, 0, 0, 0)), (sibling, (2, 3, 2, 3))): + actual = ((data[3] >> 6) & 3, data[4] & 3, (data[6] >> 6) & 3, data[10] & 3) + self.assertEqual(actual, expected) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq5pe_stock.py b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq5pe_stock.py new file mode 100644 index 0000000000..2b142642d6 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq5pe_stock.py @@ -0,0 +1,128 @@ +import unittest + +from opendbc.can import CANPacker +from opendbc.car import Bus, gen_empty_fingerprint, structs +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.ioniq5pe import qualified, replacement_requested +from opendbc.car.hyundai.values import CAR, DBC + + +def params(*, candidate=CAR.HYUNDAI_IONIQ_5_PE, alpha=False, release=False, topology="lka_alt", missing=None, gear_metadata=0x130): + fingerprint = gen_empty_fingerprint() + fingerprint[1].update({0x35: 32, 0x175: 24, 0xA0: 24, 0xEA: 24, 0x1CF: 8, 0x1A0: 32}) + if gear_metadata is not None: + fingerprint[1][gear_metadata] = 16 if gear_metadata == 0x130 else 32 + if topology == "lka_alt": + fingerprint[2].update({0x110: 32, 0x362: 32}) + elif topology == "lka": + fingerprint[2][0x50] = 16 + if missing is not None: + for bus in fingerprint.values(): + bus.pop(missing, None) + return CarInterface.get_params(candidate, fingerprint, [], alpha, release, False) + + +def update(ci, packer, tick, *, fault=0, moving=True, gear=5, cruise=True): + wheels = dict.fromkeys(("WHL_SpdFLVal", "WHL_SpdFRVal", "WHL_SpdRLVal", "WHL_SpdRRVal"), 36.0 if moving else 0.0) + values = {"ACCELERATOR": {"GEAR": gear}, "GEAR_SHIFTER": {"GEAR": gear}, "TCS": {"ACCEnable": 0, "ACC_REQ": int(cruise)}, + "WHEEL_SPEEDS": wheels, "MDPS": {"MDPS_ADAS_AciFltSig_Lv2": fault}, "STEERING_SENSORS": {}, + "DOORS_SEATBELTS": {"DRIVER_SEATBELT": 1}, "BLINKERS": {}, "CRUISE_BUTTONS": {}, + "SCC_CONTROL": {"ACCMode": int(cruise), "VSetDis": 80}} + frames = [packer.make_can_msg(name, 1, fields) for name, fields in values.items()] + frames += [packer.make_can_msg("LKAS_ALT", 2, {}), packer.make_can_msg("CAM_0x362", 2, {})] + return ci.update([(tick * 10_000_000, frames)]) + + +class TestIoniq5PEStock(unittest.TestCase): + def test_final_stock_cp_and_topology_boundary(self): + for alpha in (False, True): + for release in (False, True): + cp = params(alpha=alpha, release=release) + self.assertTrue(qualified(cp)) + self.assertEqual(cp.safetyConfigs[0].safetyParam, 0x5491) + self.assertFalse(cp.openpilotLongitudinalControl) + self.assertFalse(cp.alphaLongitudinalAvailable) + self.assertTrue(cp.pcmCruise) + self.assertEqual(cp.alternativeExperience, 0) + self.assertAlmostEqual(cp.wheelbase, 2.97) + self.assertAlmostEqual(cp.longitudinalActuatorDelay, 0.35) + self.assertTrue(cp.steerAtStandstill) + for gear_metadata in (None, 0x40): + cp = params(gear_metadata=gear_metadata) + self.assertTrue(qualified(cp)) + ci = CarInterface(cp) + self.assertEqual(ci.CS.gear_msg_canfd, "ACCELERATOR") + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + ci.update([]) + for tick in range(1, 13): + state = update(ci, packer, tick) + self.assertTrue(state.canValid) + self.assertEqual(state.gearShifter, structs.CarState.GearShifter.drive) + self.assertEqual(cp.safetyConfigs[0].safetyParam, 0x5491) + for topology in ("lka", "lfa"): + self.assertTrue(params(topology=topology).dashcamOnly) + for missing in (0x35, 0x110, 0x362, 0x1CF, 0x1A0): + self.assertFalse(qualified(params(missing=missing))) + + def test_exact_profile_rejects_nonstock_and_other_identities(self): + for field, value in (("passive", True), ("dashcamOnly", True), ("notCar", True), + ("openpilotLongitudinalControl", True), ("pcmCruise", False), + ("alternativeExperience", 32)): + with self.subTest(field=field): + cp = params() + setattr(cp, field, value) + self.assertFalse(qualified(cp)) + for candidate in (CAR.HYUNDAI_IONIQ_5, CAR.HYUNDAI_IONIQ_5_N, CAR.HYUNDAI_IONIQ_6): + with self.subTest(candidate=candidate): + cp = params(candidate=candidate) + self.assertFalse(qualified(cp)) + self.assertNotEqual(cp.safetyConfigs[-1].safetyParam, 0x5491) + cp = params() + for fault in (False, True): + state = structs.CarState() + state.canValid = True + state.cruiseState.enabled = True + state.gearShifter = structs.CarState.GearShifter.drive + state.steerFaultPermanent = fault + command = structs.CarControl() + command.latActive = True + self.assertEqual(bool(replacement_requested(cp, command, state)), not fault) + state.canTimeout = True + self.assertFalse(replacement_requested(cp, command, state)) + + def test_actual_state_fault_and_ordered_replacement_recipe(self): + cp = params() + ci = CarInterface(cp) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + ci.update([]) + command = structs.CarControl() + command.enabled = command.latActive = True + command.actuators.steeringAngleDeg = 5.0 + for warmup in range(1, 13): + state = update(ci, packer, warmup) + self.assertTrue(state.canValid) + self.assertFalse(state.canTimeout) + tick = 0 + for fault, moving, gear, cruise, active in [(0, True, 5, True, True), + *[(value, True, 5, True, True) for value in range(1, 8)], + (0, False, 5, True, True), (0, True, 7, True, True), + (0, True, 5, False, True), (0, True, 5, True, False), + (0, True, 5, True, True)]: + with self.subTest(fault=fault, moving=moving, gear=gear, cruise=cruise, active=active): + command.latActive = active + state = update(ci, packer, tick + 13, fault=fault, moving=moving, gear=gear, cruise=cruise) + self.assertEqual(state.steerFaultTemporary, fault != 0) + self.assertEqual(state.standstill, not moving) + expected = fault == 0 and moving and gear == 5 and cruise and active + self.assertEqual(bool(replacement_requested(cp, command, state)), expected) + _, frames = ci.apply(command.as_reader(), (tick + 13) * 10_000_000) + addresses = [frame[0] for frame in frames] + self.assertEqual(0x110 in addresses, expected) + self.assertEqual(0x362 in addresses, expected and tick % 5 == 0) + if 0x362 in addresses: + self.assertLess(addresses.index(0x110), addresses.index(0x362)) + tick += 1 + + +if __name__ == "__main__": + unittest.main() diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_bsm.py b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_bsm.py new file mode 100644 index 0000000000..769ebecef9 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_bsm.py @@ -0,0 +1,127 @@ +import json +import unittest +from pathlib import Path + +from opendbc.can import CANPacker +from opendbc.car import Bus, CanData, gen_empty_fingerprint, structs +from opendbc.car.hyundai.carcontroller import CarController +from opendbc.car.hyundai.hyundaicanfd import create_ioniq6_blindspot_status, hkg_can_fd_checksum +from opendbc.car.hyundai.ioniq6_bsm import BlindspotStatus, Ioniq6BlindspotSources +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.ioniq6_handoff import build_ioniq6_hda2_long_candidate +from opendbc.car.hyundai.values import CAR, DBC + + +CASES = json.loads((Path(__file__).parent / "testdata" / "ioniq6_bsm_frozen.json").read_text())["cases"] + + +def long_params(): + fingerprint = gen_empty_fingerprint() + fingerprint[2].update({0x50: 16, 0x2A4: 24}) + fingerprint[1].update({0x1CF: 8, 0x35: 32, 0x175: 24, 0xA0: 24, 0xEA: 24, + 0x1BA: 24, 0x1E5: 16, 0x36A: 16}) + fingerprint[0][0x3A5] = 24 + stock = CarInterface.get_params(CAR.HYUNDAI_IONIQ_6, fingerprint, [], False, False, False) + candidate = build_ioniq6_hda2_long_candidate(stock, fingerprint) + assert candidate is not None + return candidate + + +class TestIoniq6BlindspotStatus(unittest.TestCase): + def test_all_sixteen_frozen_executable_outputs_and_integrity(self): + for case in CASES: + with self.subTest(case=case): + left = 1 + int(case["leftBlinker"]) if case["left"] else 0 + right = 1 + int(case["rightBlinker"]) if case["right"] else 0 + frames = create_ioniq6_blindspot_status(case["counter"], BlindspotStatus(left, right)) + self.assertEqual([(addr, bus) for addr, _, bus in frames], [(0x1BA, 1), (0x1E5, 1)]) + self.assertEqual([payload.hex() for _, payload, _ in frames], [case["rear"], case["front"]]) + for addr, payload, _ in frames: + self.assertEqual(int.from_bytes(payload[:2], "little"), hkg_can_fd_checksum(addr, None, bytearray(payload))) + + def test_fresh_corner_and_lamp_holding_boundary(self): + sources = Ioniq6BlindspotSources() + t = 1_000_000_000 + self.assertIsNone(sources.status(t)) + sources.observe(corner_source_ns=t, corner_state=0x18, lamp_source_ns=t, left_lamp=True, right_lamp=False) + self.assertEqual(sources.status(t), BlindspotStatus(2, 1)) + sources.observe(corner_source_ns=t + 100_000_000, corner_state=0x18, + lamp_source_ns=t + 100_000_000, left_lamp=False, right_lamp=False) + self.assertEqual(sources.status(t + 100_000_000), BlindspotStatus(2, 1)) + # Repeated false lamp frames must not restart the hold. + sources.observe(corner_source_ns=t + 590_000_000, corner_state=0x18, + lamp_source_ns=t + 590_000_000, left_lamp=False, right_lamp=False) + self.assertEqual(sources.status(t + 600_000_000), BlindspotStatus(2, 1)) + self.assertEqual(sources.status(t + 600_000_001), BlindspotStatus(1, 1)) + self.assertIsNone(sources.status(t + 690_000_001)) # corner expiry + sources.observe(corner_source_ns=t + 700_000_000, corner_state=0x18, + lamp_source_ns=t + 700_000_000, left_lamp=False, right_lamp=False) + sources.observe(corner_source_ns=t + 2_200_000_001, corner_state=0x18, + lamp_source_ns=t + 700_000_000, left_lamp=False, right_lamp=False) + self.assertIsNone(sources.status(t + 2_200_000_001)) # lamp expiry + + def test_live_lamp_does_not_expire_before_its_off_transition(self): + sources = Ioniq6BlindspotSources() + t = 1_000_000_000 + sources.observe(corner_source_ns=t, corner_state=0x10, lamp_source_ns=t, left_lamp=True, right_lamp=False) + sources.observe(corner_source_ns=t + 600_000_000, corner_state=0x10, + lamp_source_ns=t + 600_000_000, left_lamp=True, right_lamp=False) + self.assertEqual(sources.status(t + 600_000_000), BlindspotStatus(2, 0)) + sources.observe(corner_source_ns=t + 700_000_000, corner_state=0x10, + lamp_source_ns=t + 700_000_000, left_lamp=False, right_lamp=False) + self.assertEqual(sources.status(t + 700_000_000), BlindspotStatus(2, 0)) + sources.observe(corner_source_ns=t + 1_200_000_001, corner_state=0x10, + lamp_source_ns=t + 1_200_000_001, left_lamp=False, right_lamp=False) + self.assertEqual(sources.status(t + 1_200_000_001), BlindspotStatus(1, 0)) + + def test_stale_lamp_gap_cannot_create_a_new_hold(self): + sources = Ioniq6BlindspotSources() + t = 1_000_000_000 + sources.observe(corner_source_ns=t, corner_state=0x10, lamp_source_ns=t, left_lamp=True, right_lamp=False) + self.assertEqual(sources.status(t), BlindspotStatus(2, 0)) + self.assertIsNone(sources.status(t + 1_500_000_001)) + sources.observe(corner_source_ns=t + 2_000_000_000, corner_state=0x10, + lamp_source_ns=t + 2_000_000_000, left_lamp=False, right_lamp=False) + self.assertEqual(sources.status(t + 2_000_000_000), BlindspotStatus(1, 0)) + + def test_actual_parser_dynamic_alt_lamp_and_checksum_freshness(self): + cp = long_params() + ci = CarInterface(cp) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + t = 1_000_000_000 + corner = packer.make_can_msg("BLINDSPOTS_FRONT_CORNER_2", 1, {"SIDE_DETECT_STATE": 0x10}) + alt_lamp = packer.make_can_msg("BLINKERS", 1, {"USE_ALT_LAMP": 1, "LEFT_LAMP_ALT": 1}) + ci.update([(t, [corner, alt_lamp])]) + self.assertTrue(ci.CS.out.leftBlinker) + self.assertEqual(ci.CS.ioniq6_bsm_sources.status(t), BlindspotStatus(2, 0)) + corrupted = bytearray(corner[1]) + corrupted[0] ^= 1 + ci.update([(t + 101_000_000, [CanData(corner[0], bytes(corrupted), corner[2])])]) + self.assertIsNone(ci.CS.ioniq6_bsm_sources.status(t + 101_000_000)) + + def test_controller_emits_only_fresh_paired_status_in_exact_long_profile(self): + cp = long_params() + ci = CarInterface(cp) + controller = CarController(DBC[cp.carFingerprint], cp) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + t = 10_000_000_000 # CAN BOOTTIME; controller below sees MONOTONIC. + ci.update([(t, [packer.make_can_msg("BLINDSPOTS_FRONT_CORNER_2", 1, {"SIDE_DETECT_STATE": 0x10}), + packer.make_can_msg("BLINKERS", 1, {})])]) + control = structs.CarControl() + _, frames = controller.update(control.as_reader(), ci.CS, t - 9_000_000_000) + rear_front = [(addr, payload, bus) for addr, payload, bus in frames if addr in (0x1BA, 0x1E5)] + self.assertEqual(rear_front, create_ioniq6_blindspot_status(0, BlindspotStatus(1, 0))) + for i in range(1, 5): + controller.update(control.as_reader(), ci.CS, t - 9_000_000_000 + i * 10_000_000) + ci.update([(t + 50_000_000, [packer.make_can_msg("BLINDSPOTS_FRONT_CORNER_2", 1, {"SIDE_DETECT_STATE": 0x10}), + packer.make_can_msg("BLINKERS", 1, {})])]) + _, next_frames = controller.update(control.as_reader(), ci.CS, t - 9_000_000_000 + 50_000_000) + self.assertEqual([(addr, payload, bus) for addr, payload, bus in next_frames if addr in (0x1BA, 0x1E5)], + create_ioniq6_blindspot_status(1, BlindspotStatus(1, 0))) + for i in range(6, 10): + controller.update(control.as_reader(), ci.CS, t - 9_000_000_000 + i * 10_000_000) + # An unrelated physical CAN update advances BOOTTIME without refreshing the + # corner source. Host must withhold both frames; no zero-status substitute. + ci.update([(t + 160_000_000, [packer.make_can_msg("WHEEL_SPEEDS", 1, {})])]) + _, stale = controller.update(control.as_reader(), ci.CS, t - 9_000_000_000 + 160_000_000) + self.assertFalse(any(addr in (0x1BA, 0x1E5) for addr, _, _ in stale)) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_dash_icons.py b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_dash_icons.py new file mode 100644 index 0000000000..bb60308119 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_dash_icons.py @@ -0,0 +1,68 @@ +import unittest + +from opendbc.can import CANPacker, CANParser +from opendbc.car import Bus, gen_empty_fingerprint, structs +from opendbc.car.hyundai.canfd_dash_icons import CanFDDashIcons +from opendbc.car.hyundai.tests.test_ioniq6_longitudinal import controller_fixture +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.values import CAR, DBC + + +class TestCanFDDashIcons(unittest.TestCase): + def test_lateral_and_cruise_activity_disengage_blink_and_reengage(self): + icons = CanFDDashIcons() + for frame, enabled, lateral, expected in ((0, False, False, 0), (1, False, True, 2), + (2, False, False, 3), (100, False, False, 3), + (101, False, False, 0), (102, True, False, 2), + (103, False, True, 2), (104, False, False, 3)): + self.assertEqual(icons.update(frame, enabled, lateral), expected) + + def test_actual_controller_preserves_cluster_cadence_and_axis_lifecycle(self): + for alternate in (False, True): + cp, cs, controller = controller_fixture(alternate, aol=True) + parser = CANParser(DBC[cp.carFingerprint][Bus.pt], [('LFAHDA_CLUSTER', 0)], controller.CAN.ECAN) + command = structs.CarControl() + for frame in range(120): + command.latActive = 5 <= frame < 10 or frame >= 115 + command.enabled = 105 <= frame < 110 + _, messages = controller.update(command.as_reader(), cs, 1_000_000_000 + frame * 10_000_000) + cluster = [message for message in messages if message[0] == 0x1E0] + self.assertEqual(len(cluster), int(frame % 5 == 0)) + if cluster: + parser.update([(1_000_000_000 + frame * 10_000_000, cluster)]) + value = parser.vl['LFAHDA_CLUSTER'] + expected = 2 if command.enabled or command.latActive else 3 if 10 <= frame < 109 else 0 + self.assertEqual(value['LFA_ICON'], expected) + self.assertEqual(value['HDA_ICON'], int(command.enabled)) + + def test_actual_alpha_off_interface_does_not_install_icons_or_emit_cluster(self): + for alternate in (False, True): + with self.subTest(alternate=alternate): + fingerprint = gen_empty_fingerprint() + fingerprint[2].update({0x110: 32, 0x362: 32} if alternate else {0x50: 16, 0x2A4: 24}) + fingerprint[1].update({0x1CF: 8, 0x35: 32, 0x175: 24, 0xA0: 24, 0xEA: 24, + 0x1BA: 24, 0x1E5: 16, 0x36A: 16}) + fingerprint[0].update({0x3A5: 24, 0x100: 24}) + cp = CarInterface.get_params(CAR.HYUNDAI_IONIQ_6, fingerprint, [], False, False, False) + self.assertFalse(cp.openpilotLongitudinalControl) + self.assertTrue(cp.pcmCruise) + ci = CarInterface(cp) + self.assertIsNone(ci.CC.canfd_dash_icons) + ci.update([]) # Register the actual lazy parser reads. + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + control = structs.CarControl() + for tick in range(120): + stamp = 1_000_000_000 + tick * 10_000_000 + inputs = [packer.make_can_msg(name, ci.CC.CAN.ECAN, values) for name, values in ( + ('ACCELERATOR', {'GEAR': 5}), ('TCS', {'ACCEnable': 0, 'ACC_REQ': 1}), + ('WHEEL_SPEEDS', {}), ('MDPS', {'MDPS_StrTqSnsrVal': 0, 'MDPS_OutTqVal': 0}), + ('CRUISE_BUTTONS', {'COUNTER': tick % 16}), + ('DOORS_SEATBELTS', {'DRIVER_SEATBELT': 1}), ('STEERING_SENSORS', {}), ('BLINKERS', {}), + ('SCC_CONTROL', {'MainMode_ACC': 1, 'ACCMode': 1}))] + inputs.append(packer.make_can_msg('CAM_0x362' if alternate else 'CAM_0x2a4', ci.CC.CAN.CAM, {})) + state = ci.update([(stamp, inputs)]) + self.assertTrue(state.canValid) + control.enabled = 10 <= tick < 20 + control.latActive = 5 <= tick < 15 + _, messages = ci.apply(control.as_reader(), stamp) + self.assertFalse(any(message[0] == 0x1E0 for message in messages), tick) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_handoff.py b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_handoff.py new file mode 100644 index 0000000000..1f88739a95 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_handoff.py @@ -0,0 +1,996 @@ +import threading +import tempfile +import unittest +from types import SimpleNamespace +from unittest.mock import Mock, patch + +from opendbc.car import gen_empty_fingerprint +from opendbc.car.can_definitions import CanData +from opendbc.car.car_helpers import get_car +from opendbc.car.hyundai.ioniq6_handoff import (HandoffOutcome, HandoffResult, ObservingCanRecv, build_ioniq6_hda2_long_candidate, + Ioniq6StartupKeepalive, finalize_ioniq6_prepublication, + prepare_ioniq6_long_candidate, inspect_ioniq6_long_sources, + _measured_period, _read_window, query_ioniq6_adas, run_ioniq6_handoff) +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.values import CAR +from opendbc.car.hyundai.hyundaicanfd import hkg_can_fd_checksum + + +class FakeIoniq6Bus: + def __init__(self, *, source=True, heartbeat_source=True, heartbeat_stays_after_disable=False, + bsm_source=True, bsm_stays_after_disable=False, missing_status=None, + disable_works=True, disable_ack=True, restore_works=True, + restore_ack=True, missing_required=None, post_missing_required=None): + self.now = 0.0 + self.source = source + self.heartbeat_source = heartbeat_source + self.heartbeat_stays_after_disable = heartbeat_stays_after_disable + self.bsm_source = bsm_source + self.bsm_stays_after_disable = bsm_stays_after_disable + self.missing_status = missing_status + self.disable_works = disable_works + self.disable_ack = disable_ack + self.restore_works = restore_works + self.restore_ack = restore_ack + self.missing_required = missing_required + self.post_missing_required = post_missing_required + self.counter = 0 + self.queries = [] + + @staticmethod + def frame(address, length, counter=0): + data = bytearray(length) + data[2] = counter + data[:2] = hkg_can_fd_checksum(address, None, data).to_bytes(2, "little") + return CanData(address, bytes(data), 1) + + def recv(self, wait_for_one=False): + self.now = round(self.now + 0.01, 4) + if round(self.now * 100) % 2: + return [] + messages = [self.frame(address, length) for address, length in + ((0x35, 32), (0x175, 24), (0xA0, 24), (0xEA, 24), (0x1CF, 8), (0x36A, 16)) + if address != self.missing_required and (self.source or address != self.post_missing_required)] + if self.source: + messages.append(self.frame(0x1A0, 32, self.counter)) + if self.bsm_source: + messages.extend(self.frame(address, length, self.counter) for address, length in ((0x1BA, 24), (0x1E5, 16)) + if address != self.missing_status) + if self.source: + self.counter = (self.counter + 1) % 256 + if self.heartbeat_source: + messages.append(CanData(0x100, self.frame(0x100, 24, self.counter).dat, 0)) + return [messages] + + def exchange(self, request, expected): + self.queries.append((request, expected)) + if request == b"\x10\x03": + return True + if request == b"\x28\x01\x01": + if self.disable_works: + self.source = False + self.heartbeat_source = self.heartbeat_stays_after_disable + self.bsm_source = self.bsm_stays_after_disable + return self.disable_ack + if request == b"\x28\x00\x01": + if self.restore_works: + self.source = True + self.heartbeat_source = True + self.bsm_source = True + return self.restore_ack + if request == b"\x10\x01": + return self.restore_ack + raise AssertionError(request) + + def run(self): + return run_ioniq6_handoff(self.recv, self.exchange, lambda: self.now) + + +class TestIoniq6PrepublicationHandoff(unittest.TestCase): + def test_exact_positive_ack_and_measured_silence(self): + bus = FakeIoniq6Bus() + result = bus.run() + self.assertEqual(result.outcome, HandoffOutcome.CONFIRMED) + self.assertAlmostEqual(result.source_period, 0.02, delta=0.001) + self.assertTrue(result.disable_acknowledged) + self.assertEqual(bus.queries, [(b"\x10\x03", b"\x50\x03"), (b"\x28\x01\x01", b"\x68\x01")]) + + def test_positive_ack_without_silence_restores_before_stock(self): + bus = FakeIoniq6Bus(disable_works=False) + result = bus.run() + self.assertEqual(result.outcome, HandoffOutcome.STOCK) + self.assertTrue(result.restore_acknowledged) + self.assertEqual(bus.queries[-2:], [(b"\x28\x00\x01", b"\x68\x00"), (b"\x10\x01", b"\x50\x01")]) + + def test_unanswered_disable_can_have_taken_effect_and_must_restore(self): + bus = FakeIoniq6Bus(disable_ack=False) + self.assertEqual(bus.run().outcome, HandoffOutcome.STOCK) + self.assertTrue(bus.source) + + def test_missing_pre_source_never_attempts_disable(self): + bus = FakeIoniq6Bus(source=False) + self.assertEqual(bus.run().outcome, HandoffOutcome.NOT_READY) + self.assertEqual(bus.queries, []) + bus.source = True + self.assertEqual(bus.run().outcome, HandoffOutcome.CONFIRMED) + + def test_missing_or_persistent_stock_heartbeat_never_confirms_long(self): + missing = FakeIoniq6Bus(heartbeat_source=False) + self.assertEqual(missing.run().outcome, HandoffOutcome.NOT_READY) + self.assertEqual(missing.queries, []) + persistent = FakeIoniq6Bus(heartbeat_stays_after_disable=True) + self.assertEqual(persistent.run().outcome, HandoffOutcome.STOCK) + self.assertTrue(persistent.heartbeat_source) + + def test_both_stock_bsm_status_streams_must_stop_and_restore(self): + for address in (0x1BA, 0x1E5): + with self.subTest(missing=hex(address)): + missing = FakeIoniq6Bus(missing_status=address) + self.assertEqual(missing.run().outcome, HandoffOutcome.NOT_READY) + self.assertEqual(missing.queries, []) + persistent = FakeIoniq6Bus(bsm_stays_after_disable=True) + self.assertEqual(persistent.run().outcome, HandoffOutcome.STOCK) + self.assertTrue(persistent.bsm_source) + unverified = FakeIoniq6Bus(disable_ack=False, restore_works=False) + self.assertEqual(unverified.run().outcome, HandoffOutcome.UNAVAILABLE) + + def test_real_card_callback_batched_producer_time_needs_fresh_arrivals(self): + from openpilot.selfdrive.car.card import can_comm_callbacks + + def make_event(counter, producer_ns): + source = FakeIoniq6Bus.frame(0x1A0, 32, counter) + return SimpleNamespace(logMonoTime=producer_ns, + can=[SimpleNamespace(address=source.address, dat=source.dat, src=source.src)]) + + for old_burst_only in (False, True): + with self.subTest(old_burst_only=old_burst_only): + now = [0.0] + groups = [[make_event(i, 5_000_000_000 + i * 20_000_000) for i in range(10)]] if old_burst_only else [ + [make_event(2 * j + i, 5_000_000_000 + (2 * j + i) * 20_000_000) for i in range(2)] + for j in range(5)] + + def drain(_socket, wait_for_one=False, *, observed=now, batches=groups): + observed[0] += 0.04 + return batches.pop(0) if batches else [] + + with patch("openpilot.selfdrive.car.card.messaging.drain_sock", side_effect=drain): + receive, _ = can_comm_callbacks(SimpleNamespace(), SimpleNamespace(send=lambda packet: None)) + samples, _, _, _, _, _ = _read_window(receive, lambda observed=now: observed[0], 0.20) + if old_burst_only: + self.assertIsNone(_measured_period(samples)) + else: + self.assertAlmostEqual(_measured_period(samples), 0.02, places=6) + + def test_single_sub_eight_ms_stock_arrival_requires_new_preflight_window(self): + # The parked Ioniq trace had continuous +1 SCC counters, but one 7.1 ms + # producer interval in an otherwise 20 ms window. Do not lower the bound. + deltas = (0.0169, 0.0229, 0.0170, 0.0203, 0.0294, 0.0071, 0.0216, + 0.0181, 0.0203, 0.0220, 0.0195, 0.0227, 0.0175) + now = 0.0 + samples = [(now, 90, now)] + for index, delta in enumerate(deltas, start=1): + now += delta + samples.append((now, 90 + index, now)) + self.assertIsNone(_measured_period(samples)) + clean = [(index * 0.02, 110 + index, index * 0.02) for index in range(14)] + self.assertAlmostEqual(_measured_period(clean), 0.02) + + def test_missing_health_or_unverified_restore_is_unavailable(self): + for options, expected in (({"missing_required": 0xEA}, HandoffOutcome.NOT_READY), + ({"disable_works": False, "restore_ack": False}, HandoffOutcome.UNAVAILABLE), + ({"post_missing_required": 0xEA, "restore_works": False}, HandoffOutcome.UNAVAILABLE)): + with self.subTest(options=options): + bus = FakeIoniq6Bus(**options) + self.assertEqual(bus.run().outcome, expected) + + def test_get_car_hook_runs_before_interface_parser_and_controller_construction(self): + fingerprint = gen_empty_fingerprint() + fingerprint[2][0x50] = 16 + fingerprint[1][0x1CF] = 8 + + def fake_fingerprint(*args, **kwargs): + return CAR.HYUNDAI_IONIQ_6, fingerprint, "", [], 0, True + + def pre_create(cp, candidate, observed, car_fw): + self.assertEqual(candidate, CAR.HYUNDAI_IONIQ_6) + self.assertIs(observed, fingerprint) + self.assertFalse(cp.openpilotLongitudinalControl) + cp.steerAtStandstill = True + return cp + + with patch("opendbc.car.car_helpers.fingerprint", fake_fingerprint): + ci = get_car(lambda wait_for_one=False: [], lambda msgs: None, lambda enabled: None, + False, False, pre_create_hook=pre_create) + self.assertTrue(ci.CP.steerAtStandstill) + self.assertTrue(ci.CS.CP.steerAtStandstill) + self.assertTrue(ci.CC.CP.steerAtStandstill) + + def test_stock_ev_optional_msla_does_not_invalidate_lateral_inputs(self): + from opendbc.can import CANPacker + from opendbc.car import Bus + from opendbc.car.hyundai.values import DBC + + for model in (CAR.HYUNDAI_IONIQ_6, CAR.HYUNDAI_IONIQ_5, CAR.KIA_EV6): + with self.subTest(model=model): + fp = gen_empty_fingerprint() + fp[2].update({0x50: 16, 0x2A4: 24}) + fp[1].update({0x1CF: 8, 0x35: 32, 0x130: 32}) + cp = CarInterface.get_params(model, fp, [], False, False, False) + ci = CarInterface(cp) + packer = CANPacker(DBC[model][Bus.pt]) + ci.update((1_000_000_000, [])) + sources = [(parser, state) for parser in ci.can_parsers.values() + for state in parser.message_states.values() if not state.ignore_alive and state.address != 0x2E0] + for frame in range(120): + packets = [packer.make_can_msg(state.name, parser.bus, + {"COUNTER": frame % 256} if any(sig.name == "COUNTER" for sig in state.signals) else {}) + for parser, state in sources] + state = ci.update((1_000_000_000 + frame * 100_000_000, packets)) + self.assertTrue(state.canValid) + self.assertFalse(cp.openpilotLongitudinalControl) + self.assertTrue(cp.pcmCruise) + self.assertFalse(cp.dashcamOnly) + # Independent native steering inputs remain required. + for frame in range(120, 240): + packets = [packer.make_can_msg(msg.name, parser.bus, + {"COUNTER": frame % 256} if any(sig.name == "COUNTER" for sig in msg.signals) else {}) + for parser, msg in sources if msg.name != "MDPS"] + state = ci.update((1_000_000_000 + frame * 100_000_000, packets)) + self.assertFalse(state.canValid) + + def test_card_tags_aol_candidate_before_cp_publication_without_disabling_ecu(self): + self.assert_card_prepublication_fallback({}) + + def test_card_missing_long_prerequisites_preserves_stock_lateral(self): + for options in ({"source": False}, {"heartbeat_source": False}, {"bsm_source": False}, + {"missing_required": 0xEA}, {"missing_status": 0x1E5}): + with self.subTest(options=options): + self.assert_card_prepublication_fallback(options) + + def assert_card_prepublication_fallback(self, options): + from openpilot.common.params import Params + from openpilot.selfdrive.car import card as card_module + + class CandidateCaptured(Exception): + pass + + fp = gen_empty_fingerprint() + fp[2].update({0x50: 16, 0x2A4: 24}) + fp[0][0x3A5] = 24 + fp[1].update({0x1CF: 8, 0x35: 32, 0x175: 24, 0xA0: 24, 0xEA: 24, 0x1BA: 24, 0x1E5: 16, 0x36A: 16}) + bus = FakeIoniq6Bus(**options) + stock = CarInterface.get_params(CAR.HYUNDAI_IONIQ_6, fp, [], False, False, False) + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + params = Params(directory.name) + for key in ("OpenpilotEnabledToggle", "AlphaLongitudinalEnabled"): + params.put_bool(key, True, block=True) + + class FakeSubMaster: + valid = {"pandaStates": True} + alive = {"pandaStates": True} + + @staticmethod + def update(timeout): + pass + + def __getitem__(self, key): + return [SimpleNamespace(ignitionLine=True, ignitionCan=False)] + + sm = FakeSubMaster() + pm = SimpleNamespace(sock={"sendcan": object()}) + + def capture_get_car(*args, pre_create_hook=None, **kwargs): + self.assertIsNotNone(pre_create_hook) + selected = pre_create_hook(stock, CAR.HYUNDAI_IONIQ_6, fp, []) + self.assertEqual(selected.carFingerprint, CAR.HYUNDAI_IONIQ_6) + self.assertFalse(selected.dashcamOnly) + self.assertEqual(selected.safetyConfigs[-1].safetyParam, 0x11 if options else 0x8815) + self.assertEqual(selected.openpilotLongitudinalControl, not bool(options)) + self.assertEqual(selected.pcmCruise, bool(options)) + interface = CarInterface(selected) + parser = interface.CS.get_can_parsers(selected) + from opendbc.car import Bus + if options: + self.assertNotIn(0x2A4, parser[Bus.cam].message_states) + self.assertEqual(parser[Bus.pt].message_states[0x1CF].frequency, 1) + else: + self.assertEqual(parser[Bus.cam].message_states[0x2A4].frequency, 20) + raise CandidateCaptured + + def capture_preparation(stock_cp, long_cp, **kwargs): + if options: + self.assertIsNone(long_cp) + else: + self.assertEqual(long_cp.safetyConfigs[-1].safetyParam, 0x8815) + self.assertTrue(kwargs["enabled"]) + return prepare_ioniq6_long_candidate(stock_cp, long_cp, **kwargs) + + with patch.dict("os.environ", {"AOL_REPLAY_RUNTIME": ""}), \ + patch.object(card_module, "feature_requested", side_effect=lambda _params, feature: feature == 'aol'), \ + patch.object(card_module, "prewarm_cache_contracts"), \ + patch.object(card_module.messaging, "sub_sock", return_value=object()), \ + patch.object(card_module.messaging, "SubMaster", return_value=sm), \ + patch.object(card_module.messaging, "PubMaster", return_value=pm), \ + patch.object(card_module.messaging, "recv_one_retry", return_value=SimpleNamespace(can=[1])), \ + patch.object(card_module, "Params", return_value=params), \ + patch.object(card_module, "startup_candidate", return_value=None), \ + patch.object(card_module, "get_cache", return_value=None), \ + patch.object(card_module, "can_comm_callbacks", return_value=(lambda wait_for_one=False: [], lambda msgs: None)), \ + patch.object(card_module, "read_settings", return_value=object()), \ + patch.object(card_module, "independent_axis_requested", return_value=True), \ + patch.object(card_module, "inspect_ioniq6_long_sources", + side_effect=lambda recv: inspect_ioniq6_long_sources(bus.recv, lambda: bus.now)), \ + patch.object(card_module, "prepare_ioniq6_long_candidate", side_effect=capture_preparation), \ + patch.object(card_module, "confirm_ioniq6_prepared_takeover") as confirm, \ + patch.object(card_module, "get_car", side_effect=capture_get_car): + with self.assertRaises(CandidateCaptured): + card_module.Car() + confirm.assert_not_called() + self.assertEqual(bus.queries, []) + + def test_prepublication_candidate_is_selected_only_after_complete_takeover(self): + for bus_options, expected in (({}, HandoffOutcome.CONFIRMED), + ({"disable_works": False}, HandoffOutcome.STOCK), + ({"post_missing_required": 0xEA, "restore_works": False}, HandoffOutcome.UNAVAILABLE)): + with self.subTest(bus_options=bus_options): + bus = FakeIoniq6Bus(**bus_options) + stock = SimpleNamespace(dashcamOnly=False) + long = SimpleNamespace(openpilotLongitudinalControl=True, + safetyConfigs=[SimpleNamespace(safetyParam=0x8015)]) + with patch("opendbc.car.hyundai.ioniq6_handoff.query_ioniq6_adas", + side_effect=lambda recv, send, request, response, active_bus=bus: active_bus.exchange(request, response)): + selected, prearmed, result = finalize_ioniq6_prepublication( + stock, long, bus.recv, lambda msgs: None, enabled=True, is_release=False, clock=lambda active_bus=bus: active_bus.now) + self.assertEqual(result.outcome, expected) + self.assertIs(selected, long if expected is HandoffOutcome.CONFIRMED else stock) + self.assertEqual(prearmed, expected is HandoffOutcome.CONFIRMED) + self.assertEqual(stock.dashcamOnly, expected is HandoffOutcome.UNAVAILABLE) + + def test_preparation_selects_immutable_candidate_without_source_transaction(self): + stock = SimpleNamespace() + prepared = SimpleNamespace(openpilotLongitudinalControl=True, + safetyConfigs=[SimpleNamespace(safetyParam=0x8895)]) + selected, pending = prepare_ioniq6_long_candidate(stock, prepared, enabled=True, is_release=False) + self.assertIs(selected, prepared) + self.assertTrue(pending) + self.assertEqual(prepare_ioniq6_long_candidate(stock, prepared, enabled=False, is_release=False), (stock, False)) + self.assertEqual(prepare_ioniq6_long_candidate(stock, prepared, enabled=True, is_release=True), (stock, False)) + prepared.safetyConfigs[-1].safetyParam = 0x0015 + with self.assertRaisesRegex(ValueError, 'unreviewed'): + prepare_ioniq6_long_candidate(stock, prepared, enabled=True, is_release=False) + + def test_release_and_unreviewed_raw_do_not_request_takeover(self): + stock = SimpleNamespace(dashcamOnly=False) + long = SimpleNamespace(openpilotLongitudinalControl=True, safetyConfigs=[SimpleNamespace(safetyParam=0x8015)]) + bus = FakeIoniq6Bus() + selected, armed, result = finalize_ioniq6_prepublication( + stock, long, bus.recv, lambda msgs: None, enabled=True, is_release=True, clock=lambda: bus.now) + self.assertIs(selected, stock) + self.assertFalse(armed) + self.assertIsNone(result) + self.assertEqual(bus.queries, []) + long.safetyConfigs[-1].safetyParam = 0x0015 + with self.assertRaises(ValueError): + finalize_ioniq6_prepublication(stock, long, bus.recv, lambda msgs: None, + enabled=True, is_release=False, clock=lambda: bus.now) + + def test_long_candidate_exact_topology_is_prepared_before_diagnostics(self): + for steering_addr, steering_len, support_addr, support_len, raw in ((0x50, 16, 0x2A4, 24, 0x8015), + (0x110, 32, 0x362, 32, 0x8095)): + with self.subTest(steering_addr=steering_addr): + fp = gen_empty_fingerprint() + fp[2][steering_addr] = steering_len + fp[2][support_addr] = support_len + fp[0][0x3A5] = 24 + # The public Ioniq 6 LKA fixture contains both 0x1CF and 0x1AA; the + # selected standard-button profile is determined by the actual 0x1CF + # source and stock safety mode, not mere 0x1AA presence. + fp[1].update({0x1CF: 8, 0x1AA: 16, 0x35: 32, 0x175: 24, 0xA0: 24, 0xEA: 24, + 0x1BA: 24, 0x1E5: 16, 0x36A: 16}) + stock = CarInterface.get_params(CAR.HYUNDAI_IONIQ_6, fp, [], False, False, False) + candidate = build_ioniq6_hda2_long_candidate(stock, fp) + self.assertIsNotNone(candidate) + self.assertFalse(stock.openpilotLongitudinalControl) + self.assertTrue(stock.pcmCruise) + self.assertTrue(candidate.openpilotLongitudinalControl) + self.assertFalse(candidate.pcmCruise) + self.assertEqual(candidate.safetyConfigs[-1].safetyParam, raw) + fp[2][support_addr] = 8 + self.assertIsNone(build_ioniq6_hda2_long_candidate(stock, fp)) + + def test_real_isotp_adapter_keeps_interleaved_scc_evidence(self): + pending = [] + + def can_recv(wait_for_one=False): + ret = [pending[:]] if pending else [] + pending.clear() + return ret + + def can_send(msgs): + for msg in msgs: + request_len = msg.dat[0] + request = msg.dat[1:request_len + 1] + response = {b"\x10\x03": b"\x50\x03\x00\x32\x01\xf4", b"\x28\x01\x01": b"\x68\x01", + b"\x10\x01": b"\x50\x01\x00\x32\x01\xf4", b"\x28\x00\x01": b"\x68\x00"}[request] + heartbeat = FakeIoniq6Bus.frame(0x100, 24, 9) + pending.extend((FakeIoniq6Bus.frame(0x1A0, 32, 9), CanData(0x100, heartbeat.dat, 0), + CanData(0x738, bytes([len(response)]) + response + bytes(7 - len(response)), 1))) + + observed = ObservingCanRecv(can_recv) + self.assertTrue(query_ioniq6_adas(observed, can_send, b"\x10\x03", b"\x50\x03")) + self.assertTrue(query_ioniq6_adas(observed, can_send, b"\x28\x01\x01", b"\x68\x01")) + self.assertTrue(query_ioniq6_adas(observed, can_send, b"\x28\x00\x01", b"\x68\x00")) + self.assertTrue(query_ioniq6_adas(observed, can_send, b"\x10\x01", b"\x50\x01")) + self.assertEqual(observed.source_seen_count, 4) + self.assertEqual(observed.heartbeat_seen_count, 4) + self.assertIsNotNone(observed.latest_source_time) + + def test_uds_response_shape_rejects_extra_communication_control_payload(self): + pending = [] + + def recv(wait_for_one=False): + ret = [pending[:]] if pending else [] + pending.clear() + return ret + + def send(msgs): + pending.append(CanData(0x738, b"\x03\x68\x01\x99\x00\x00\x00\x00", 1)) + + self.assertFalse(query_ioniq6_adas(recv, send, b"\x28\x01\x01", b"\x68\x01")) + + def test_keepalive_aborts_at_bounded_deadline_without_claiming_stock(self): + now = [0.0] + sent = [] + restored = [] + aborted = [] + + def restore(): + restored.append(True) + return False + + owner = Ioniq6StartupKeepalive( + lambda messages: sent.extend(messages), restore, lambda: aborted.append(True), + lambda verified: aborted.append(verified), clock=lambda: now[0], max_startup_sec=3.0) + owner.tick() + self.assertEqual((sent[-1].address, sent[-1].src, sent[-1].dat), (0x730, 1, b"\x02\x3e\x80\x00\x00\x00\x00\x00")) + now[0] = 0.5 + owner.tick() + self.assertEqual(len(sent), 2) + now[0] = 3.0 + owner.tick() + self.assertEqual(restored, [True]) + self.assertEqual(aborted, [True, False]) + owner.tick() + self.assertEqual(len(sent), 2) + + def test_keepalive_stop_waits_for_inflight_send_before_controller_owner(self): + entered = threading.Event() + release = threading.Event() + owner_done = threading.Event() + sends = [] + + def blocking_send(messages): + entered.set() + self.assertTrue(release.wait(2)) + sends.extend(messages) + + owner = Ioniq6StartupKeepalive(blocking_send, lambda: True, lambda: None, lambda verified: None) + tick = threading.Thread(target=owner.tick) + tick.start() + self.assertTrue(entered.wait(2)) + stopper = threading.Thread(target=lambda: (owner.stop(), owner_done.set())) + stopper.start() + self.assertFalse(owner_done.wait(0.05)) + release.set() + self.assertTrue(owner_done.wait(2)) + tick.join(2) + stopper.join(2) + self.assertEqual(len(sends), 1) + owner.tick() + self.assertEqual(len(sends), 1) + + def test_timeout_latches_loss_before_restore_and_stop_waits_for_restore(self): + now = [0.0] + restoring = threading.Event() + release = threading.Event() + stopped = threading.Event() + lost = [] + + def restore(): + self.assertEqual(lost, [True]) + restoring.set() + self.assertTrue(release.wait(2)) + return False + + owner = Ioniq6StartupKeepalive(lambda messages: None, restore, lambda: lost.append(True), + lambda verified: None, clock=lambda: now[0], max_startup_sec=1.0) + now[0] = 1.0 + tick = threading.Thread(target=owner.tick) + tick.start() + self.assertTrue(restoring.wait(2)) + stopper = threading.Thread(target=lambda: (owner.stop(), stopped.set())) + stopper.start() + self.assertFalse(stopped.wait(0.05)) + release.set() + self.assertTrue(stopped.wait(2)) + tick.join(2) + stopper.join(2) + self.assertEqual(lost, [True]) + + def test_card_warmup_and_late_source_are_separate_states(self): + from openpilot.selfdrive.car.card import Car + + card = Car.__new__(Car) + card.ioniq6_long_prearmed = True + card.ioniq6_host_warmed = False + card.ioniq6_long_lost = False + card.ioniq6_keepalive = None + invalid = SimpleNamespace(canValid=False) + card.observe_ioniq6_long_authority([], invalid) + self.assertFalse(card.ioniq6_long_lost) + self.assertFalse(card.ioniq6_host_warmed) + valid = SimpleNamespace(canValid=True) + card.observe_ioniq6_long_authority([], valid) + self.assertTrue(card.ioniq6_host_warmed) + card.observe_ioniq6_long_authority([], invalid) + self.assertTrue(card.ioniq6_long_lost) + self.assertFalse(invalid.canValid) + + card.ioniq6_long_lost = False + source = CanData(0x1A0, b"\x00" * 8, 1) + card.observe_ioniq6_long_authority([(1_000_000_000, [source])], valid) + self.assertTrue(card.ioniq6_long_lost) + self.assertFalse(valid.canValid) + + @staticmethod + def ready_card(*, ioniq_selected: bool): + from openpilot.selfdrive.car.card import Car + + class FakeSM: + def __init__(self): + self.valid = {'carControl': False, 'pandaStates': True} + self.alive = {'pandaStates': True} + self.panda = [SimpleNamespace(safetyModel=3, safetyParam=1, controlsAllowed=False)] + + def all_alive(self, services): + return all(self.valid[service] for service in services) + + def __getitem__(self, key): + return self.panda if key == 'pandaStates' else None + + card = Car.__new__(Car) + card.ci_initialized = False + card.initialized_prev = False + card.ioniq6_long_selected = ioniq_selected + card.ioniq6_long_pending = ioniq_selected + card.ioniq6_last_preflight_at = 0.0 + card.ioniq6_long_prearmed = False + card.ioniq6_long_lost = False + card.ioniq6_host_warmed = False + card.ioniq6_panda_armed = False + card.ioniq6_keepalive = None + card.ioniq6_restore_callbacks = None + card.ioniq6_handoff_result = None + card.can_callbacks = (lambda wait_for_one=False: [], lambda messages: None) + card.params = SimpleNamespace(put_bool=Mock()) + card.CP = SimpleNamespace(safetyConfigs=[object()]) + card.CI = SimpleNamespace(init=Mock(), apply=Mock(return_value=(object(), []))) + card.sm = FakeSM() + card.publish_sendcan = Mock() + card.ioniq6_panda_matches = Mock(return_value=False) + return card + + def test_delayed_ioniq_takeover_waits_for_control_and_can_then_panda(self): + from openpilot.selfdrive.car import card as card_module + + card = self.ready_card(ioniq_selected=True) + cs = SimpleNamespace(canValid=True, canTimeout=False) + cc = object() + confirmed = HandoffResult(HandoffOutcome.CONFIRMED, 'confirmed') + with (patch.object(card_module, 'confirm_ioniq6_prepared_takeover', return_value=confirmed) as takeover, + patch.object(card_module.cloudlog, 'event') as logged): + for _ in range(200): # Delayed model/control startup cannot disable the ECU. + card.controls_update(cs, cc) + takeover.assert_not_called() + card.params.put_bool.assert_not_called() + card.CI.apply.assert_not_called() + + card.sm.valid['carControl'] = True + cs.canValid = False + card.controls_update(cs, cc) + takeover.assert_not_called() + cs.canValid = True + card.controls_update(cs, cc) + takeover.assert_called_once_with(*card.can_callbacks) + self.assertEqual(logged.call_args.args, ('ioniq6_long_handoff',)) + self.assertEqual(logged.call_args.kwargs['outcome'], 'CONFIRMED') + card.params.put_bool.assert_called_once_with('ControlsReady', True) + card.CI.init.assert_not_called() + card.CI.apply.assert_not_called() # Panda is still in its prior mode. + + card.ioniq6_panda_matches.return_value = True + card.controls_update(cs, cc) + card.CI.apply.assert_not_called() + self.assertFalse(card.ioniq6_host_warmed) + cs.canValid = False # Transaction consumed the old CAN cursor. + card.observe_ioniq6_long_authority([], cs) + self.assertFalse(card.ioniq6_long_lost) + card.controls_update(cs, cc) + card.CI.apply.assert_not_called() + cs.canValid = True + card.observe_ioniq6_long_authority([], cs) + card.controls_update(cs, cc) + card.CI.apply.assert_called_once() + cs.canValid = False + card.observe_ioniq6_long_authority([], cs) + self.assertTrue(card.ioniq6_long_lost) + card.controls_update(cs, cc) + card.CI.apply.assert_called_once() + self.assertTrue(card.ioniq6_long_prearmed) + self.assertTrue(card.ioniq6_panda_armed) + + def test_failed_delayed_takeover_never_acts_or_publishes_valid_carstate(self): + from openpilot.selfdrive.car import card as card_module + + card = self.ready_card(ioniq_selected=True) + card.sm.valid['carControl'] = True + cs = SimpleNamespace(canValid=True, canTimeout=False) + cc = object() + failed = HandoffResult(HandoffOutcome.UNAVAILABLE, 'restore unverified') + with (patch.object(card_module, 'confirm_ioniq6_prepared_takeover', return_value=failed) as takeover, + patch.object(card_module.cloudlog, 'event') as logged): + card.controls_update(cs, cc) + self.assertFalse(cs.canValid) + card.params.put_bool.assert_not_called() + card.CI.apply.assert_not_called() + for _ in range(10): + cs.canValid = True # The next raw CAN frame is valid but CP is still LONG. + card.observe_ioniq6_long_authority([], cs) + self.assertFalse(cs.canValid) + card.controls_update(cs, cc) + takeover.assert_called_once() + self.assertEqual(logged.call_args.kwargs['reason'], 'restore unverified') + card.CI.apply.assert_not_called() + + def test_pending_takeover_requires_live_diagnostic_panda_and_neutral_controls(self): + from openpilot.selfdrive.car import card as card_module + + card = self.ready_card(ioniq_selected=True) + card.sm.valid['carControl'] = True + cs = SimpleNamespace(canValid=True, canTimeout=False) + cc = object() + confirmed = HandoffResult(HandoffOutcome.CONFIRMED, 'confirmed') + with patch.object(card_module, 'confirm_ioniq6_prepared_takeover', return_value=confirmed) as takeover: + for mutation in ( + lambda: card.sm.valid.__setitem__('pandaStates', False), + lambda: card.sm.alive.__setitem__('pandaStates', False), + lambda: setattr(card.sm.panda[0], 'safetyModel', 28), + lambda: setattr(card.sm.panda[0], 'safetyParam', 2), + lambda: setattr(card.sm.panda[0], 'controlsAllowed', True), + lambda: card.sm.panda.append(SimpleNamespace(safetyModel=3, safetyParam=1, controlsAllowed=False)), + ): + card.sm.valid['pandaStates'] = True + card.sm.alive['pandaStates'] = True + card.sm.panda[:] = [SimpleNamespace(safetyModel=3, safetyParam=1, controlsAllowed=False)] + mutation() + card.controls_update(cs, cc) + takeover.assert_not_called() + self.assertTrue(card.ioniq6_long_pending) + card.params.put_bool.assert_not_called() + card.CI.apply.assert_not_called() + + for diagnostic_param in (0, 1): + next_card = self.ready_card(ioniq_selected=True) + next_card.sm.valid['carControl'] = True + next_card.sm.panda[0].safetyParam = diagnostic_param + next_card.controls_update(SimpleNamespace(canValid=True, canTimeout=False), cc) + self.assertTrue(next_card.ioniq6_long_prearmed) + next_card.params.put_bool.assert_called_once_with('ControlsReady', True) + self.assertEqual(takeover.call_count, 2) + + def test_preflight_miss_retries_without_uds_or_permanent_latch(self): + from openpilot.selfdrive.car import card as card_module + + card = self.ready_card(ioniq_selected=True) + card.sm.valid['carControl'] = True + cs = SimpleNamespace(canValid=True, canTimeout=False) + cc = object() + clock = [100.0] + waiting = HandoffResult(HandoffOutcome.NOT_READY, 'preflight_scc_period') + confirmed = HandoffResult(HandoffOutcome.CONFIRMED, 'confirmed') + with (patch.object(card_module, 'confirm_ioniq6_prepared_takeover', side_effect=(waiting, confirmed)) as takeover, + patch.object(card_module.time, 'monotonic', side_effect=lambda: clock[0])): + card.controls_update(cs, cc) + self.assertTrue(card.ioniq6_long_pending) + self.assertFalse(card.ioniq6_long_lost) + self.assertTrue(cs.canValid) + card.params.put_bool.assert_not_called() + card.CI.apply.assert_not_called() + self.assertEqual(takeover.call_count, 1) + + clock[0] += 0.2 + card.controls_update(cs, cc) + self.assertEqual(takeover.call_count, 1) + clock[0] += 0.4 + card.controls_update(cs, cc) + self.assertEqual(takeover.call_count, 2) + self.assertFalse(card.ioniq6_long_pending) + self.assertTrue(card.ioniq6_long_prearmed) + card.params.put_bool.assert_called_once_with('ControlsReady', True) + card.CI.apply.assert_not_called() # Hyundai safety has not switched yet. + + def test_generic_ecu_init_waits_for_valid_can_and_live_control_once(self): + card = self.ready_card(ioniq_selected=False) + cs = SimpleNamespace(canValid=True, canTimeout=False) + cc = object() + for _ in range(20): + card.controls_update(cs, cc) + card.CI.init.assert_not_called() + card.sm.valid['carControl'] = True + cs.canValid = False + card.controls_update(cs, cc) + card.CI.init.assert_not_called() + cs.canValid = True + card.controls_update(cs, cc) + card.controls_update(cs, cc) + card.CI.init.assert_called_once_with(card.CP, *card.can_callbacks) + card.params.put_bool.assert_called_once_with('ControlsReady', True) + self.assertEqual(card.CI.apply.call_count, 2) + + def test_card_late_source_reads_actual_can_converter_tuples(self): + from openpilot.selfdrive.car.card import Car + from openpilot.selfdrive.pandad import can_capnp_to_list, can_list_to_can_capnp + + card = Car.__new__(Car) + card.ioniq6_long_prearmed = True + card.ioniq6_host_warmed = True + card.ioniq6_long_lost = False + card.ioniq6_keepalive = None + valid = SimpleNamespace(canValid=True) + + def observe(frames): + packets = can_capnp_to_list([can_list_to_can_capnp(frames)]) + self.assertIs(type(packets[0][1][0]), tuple) + card.observe_ioniq6_long_authority(packets, valid) + + observe([CanData(0x35, b'\0' * 32, 1), CanData(0x1A0, b'\0' * 32, 0)]) + self.assertFalse(card.ioniq6_long_lost) # The SCC address on the wrong bus is not a source. + observe([CanData(0x1A0, b'\0' * 32, 1)]) + self.assertTrue(card.ioniq6_long_lost) + self.assertFalse(valid.canValid) + + card.ioniq6_long_lost = False + valid.canValid = True + heartbeat = CanData(0x100, b"\x00" * 8, 0) + card.observe_ioniq6_long_authority([(1_000_000_000, [heartbeat])], valid) + self.assertTrue(card.ioniq6_long_lost) + self.assertFalse(valid.canValid) + + def test_card_serialized_receive_publish_send_and_source_revocation(self): + from opendbc.car import structs + from opendbc.car.structs import car + from openpilot.cereal import messaging + from openpilot.selfdrive.car import card as card_module + from openpilot.selfdrive.pandad import can_capnp_to_list, can_list_to_can_capnp + + card_owner = card_module.Car.__new__(card_module.Car) + cp = car.CarParams(openpilotLongitudinalControl=True, pcmCruise=False, passive=False) + cp.init('safetyConfigs', 1) + card_owner.CP = cp + card_owner.ci_initialized = False + card_owner.initialized_prev = False + card_owner.ioniq6_long_selected = True + card_owner.ioniq6_long_pending = True + card_owner.ioniq6_last_preflight_at = 0.0 + card_owner.ioniq6_long_prearmed = False + card_owner.ioniq6_long_lost = False + card_owner.ioniq6_host_warmed = False + card_owner.ioniq6_panda_armed = False + card_owner.ioniq6_keepalive = None + card_owner.ioniq6_restore_callbacks = None + card_owner.ioniq6_restore_attempted = False + card_owner.can_rcv_cum_timeout_counter = 0 + card_owner.can_sock = object() + card_owner.can_callbacks = (lambda wait_for_one=False: [], lambda frames: None) + card_owner.params = SimpleNamespace(put_bool=Mock()) + card_owner.slc_replay = card_owner.curve_replay = card_owner.conditional_replay = card_owner.aol_replay = False + card_owner.aol_card_intent = None + card_owner.ioniq6_media = None + card_owner.CC_prev = car.CarControl() + card_owner.CS_prev = car.CarState() + card_owner.last_actuators_output = structs.CarControl.Actuators() + card_owner.rk = SimpleNamespace(remaining=0.0) + card_owner.is_metric = True + card_owner.experimental_mode = False + card_owner.v_cruise_helper = SimpleNamespace(update_v_cruise=lambda *_: None, initialize_v_cruise=lambda *_: None, + slc_consumed_button=None, + slc_cruise_change=None, v_cruise_kph=60.0, v_cruise_cluster_kph=60.0) + card_owner.RI = SimpleNamespace(update=lambda _: None) + control = car.CarControl(enabled=True, longActive=True) + + class SM: + frame = 1 + seen = {'onroadEvents': True} + valid = {'carControl': True} + alive = {'carControl': True} + logMonoTime = {'carControl': 1} + + def update(self, _timeout): + pass + + def __getitem__(self, name): + return [] if name == 'onroadEvents' else control + + def all_alive(self, names): + return all(self.alive[name] for name in names) + + def all_checks(self, names): + return all(self.valid[name] for name in names) + + card_owner.sm = SM() + sent = [] + card_owner.pm = SimpleNamespace(send=lambda service, packet: sent.append((service, packet))) + card_owner.CI = SimpleNamespace(CS=SimpleNamespace(), + update=Mock(side_effect=lambda packets: car.CarState(canValid=True, canTimeout=False)), + apply=Mock(return_value=(structs.CarControl.Actuators(), [CanData(0x1A0, b'\0' * 8, 1)])), + init=Mock()) + card_owner.ioniq6_panda_diagnostic_ready = Mock(return_value=True) + card_owner.ioniq6_panda_matches = Mock(side_effect=(False, True, True)) + packets = iter(( + can_list_to_can_capnp([CanData(0x35, b'\0' * 32, 1)]), + can_list_to_can_capnp([CanData(0x35, b'\0' * 32, 1)]), + can_list_to_can_capnp([CanData(0x1A0, b'\0' * 32, 1)]), + )) + with (patch.object(card_module.messaging, 'drain_sock_raw', side_effect=lambda *_args, **_kwargs: [next(packets)]), + patch.object(card_module, 'confirm_ioniq6_prepared_takeover', return_value=HandoffResult(HandoffOutcome.CONFIRMED, 'confirmed')), + patch.object(card_module.cloudlog, 'event')): + card_owner.step() + card_owner.step() + card_owner.step() + card_owner.CI.init.assert_not_called() + self.assertEqual(card_owner.CI.apply.call_count, 1) + self.assertEqual(card_owner.CI.update.call_count, 3) + self.assertTrue(all(type(group[0][1][0]) is tuple for (group,) in (call.args for call in card_owner.CI.update.call_args_list))) + outputs = [(messaging.log_from_bytes(packet).valid, can_capnp_to_list([packet], msgtype='sendcan')[0][1]) + for service, packet in sent if service == 'sendcan'] + self.assertEqual([(valid, [frame[0] for frame in frames]) for valid, frames in outputs], + [(False, []), (True, [0x1A0]), (False, [])]) + published = [packet for service, packet in sent if service == 'carState'] + self.assertEqual([message.valid for message in published], [True, True, False]) + self.assertTrue(card_owner.ioniq6_long_lost) + with (patch.object(card_module, 'config_realtime_process'), + patch.object(card_module.Car, '__new__', return_value=card_owner), + patch.object(card_module.Car, '__init__', return_value=None), + patch.object(card_module.Car, 'card_thread', return_value=None), + patch.object(card_module, 'restore_ioniq6_adas', return_value=True) as restore): + card_module.main() + restore.assert_called_once_with(*card_owner.can_callbacks) + + def test_card_requires_fresh_matching_panda_mode(self): + from openpilot.selfdrive.car.card import Car + + class PandaSM: + def __init__(self): + self.valid = {"pandaStates": True} + self.alive = {"pandaStates": True} + self.states = [SimpleNamespace(safetyModel=28, safetyParam=0x8015, + alternativeExperience=0, safetyRxChecksInvalid=False)] + + def __getitem__(self, key): + return self.states + + card = Car.__new__(Car) + card.sm = PandaSM() + card.CP = SimpleNamespace(safetyConfigs=[SimpleNamespace(safetyModel=28, safetyParam=0x8015)], + alternativeExperience=0) + self.assertTrue(card.ioniq6_panda_matches()) + card.sm.alive["pandaStates"] = False + self.assertFalse(card.ioniq6_panda_matches()) + card.sm.alive["pandaStates"] = True + card.sm.states[0].safetyParam = 0x11 + self.assertFalse(card.ioniq6_panda_matches()) + + def test_main_stops_diagnostic_worker_after_partial_constructor_failure(self): + from openpilot.selfdrive.car import card as card_module + + stopped = [] + + def fail_after_prearm(card): + card.ioniq6_keepalive = SimpleNamespace(stop=lambda: stopped.append(True)) + card.ioniq6_long_lost = False + card.ioniq6_long_prearmed = True + card.ioniq6_init_complete = False + card.ioniq6_restore_callbacks = (lambda wait_for_one=False: [], lambda messages: None) + raise RuntimeError("failed after ECU handoff") + + with (patch.object(card_module, "config_realtime_process"), patch.object(card_module.Car, "__init__", fail_after_prearm), + patch.object(card_module, "restore_ioniq6_adas", return_value=True) as restore): + with self.assertRaisesRegex(RuntimeError, "failed after ECU handoff"): + card_module.main() + self.assertEqual(stopped, [True]) + restore.assert_called_once() + + def test_main_restores_after_post_init_exception_and_normal_shutdown(self): + from openpilot.selfdrive.car import card as card_module + + def check_case(outcome, restored): + cards = [] + stopped = [] + callbacks = (lambda wait_for_one=False: [], lambda messages: None) + + def prepared(card): + cards.append(card) + card.ioniq6_keepalive = SimpleNamespace(stop=lambda: stopped.append(True)) + card.ioniq6_long_lost = False + card.ioniq6_long_prearmed = True + card.ioniq6_init_complete = True + card.ioniq6_restore_callbacks = callbacks + card.ioniq6_restore_attempted = False + + def run(_card): + if outcome == 'crash': + raise RuntimeError('failed after CP publication') + + with (patch.object(card_module, 'config_realtime_process'), patch.object(card_module.Car, '__init__', prepared), + patch.object(card_module.Car, 'card_thread', run), + patch.object(card_module, 'restore_ioniq6_adas', return_value=restored) as restore, + patch.object(card_module.cloudlog, 'error') as logged): + if outcome == 'crash': + with self.assertRaisesRegex(RuntimeError, 'failed after CP publication'): + card_module.main() + else: + card_module.main() + self.assertTrue(cards[0].ioniq6_long_lost) + self.assertTrue(cards[0].ioniq6_restore_attempted) + self.assertEqual(stopped, [True]) + restore.assert_called_once_with(*callbacks) + if restored: + logged.assert_not_called() + else: + logged.assert_called_once_with('Ioniq 6 Card exited; stock SCC restoration unverified') + + for outcome, restored in (('crash', True), ('shutdown', True), ('crash', False)): + with self.subTest(outcome=outcome, restored=restored): + check_case(outcome, restored) + + def test_main_does_not_repeat_keepalive_restore(self): + from openpilot.selfdrive.car import card as card_module + + def prepared(card): + card.ioniq6_keepalive = SimpleNamespace(stop=lambda: None) + card.ioniq6_long_lost = False + card.ioniq6_long_prearmed = True + card.ioniq6_restore_callbacks = (lambda wait_for_one=False: [], lambda messages: None) + card.ioniq6_restore_attempted = True # Timeout worker already restored or tried to restore. + + with (patch.object(card_module, 'config_realtime_process'), patch.object(card_module.Car, '__init__', prepared), + patch.object(card_module.Car, 'card_thread', lambda _card: None), + patch.object(card_module, 'restore_ioniq6_adas') as restore): + card_module.main() + restore.assert_not_called() + + def test_same_card_can_cursor_consumes_pre_disable_scc_before_state_update(self): + from openpilot.selfdrive.car.card import can_comm_callbacks + + bus = FakeIoniq6Bus() + bus.counter = 1 + pending = [[FakeIoniq6Bus.frame(0x1A0, 32, 0)]] + + def drain(_socket, wait_for_one=False): + if pending: + packet = pending.pop(0) + bus.now += 0.02 + frames = [packet] + else: + frames = bus.recv(wait_for_one) + return [SimpleNamespace(logMonoTime=int(bus.now * 1e9), + can=[SimpleNamespace(address=m.address, dat=m.dat, src=m.src) for m in packet]) + for packet in frames] + + with patch("openpilot.selfdrive.car.card.messaging.drain_sock", side_effect=drain): + receive, _ = can_comm_callbacks(SimpleNamespace(), SimpleNamespace(send=lambda packet: None)) + result = run_ioniq6_handoff(receive, bus.exchange, lambda: bus.now) + self.assertEqual(result.outcome, HandoffOutcome.CONFIRMED) + self.assertEqual(pending, []) + self.assertFalse(any(m.address == 0x1A0 for packet in receive() for m in packet)) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_longitudinal.py b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_longitudinal.py new file mode 100644 index 0000000000..90fc9ff28d --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_longitudinal.py @@ -0,0 +1,327 @@ +import hashlib +import json +from pathlib import Path +from types import SimpleNamespace +import unittest + +from opendbc.can import CANPacker, CANParser +from opendbc.car import Bus, gen_empty_fingerprint, structs +from opendbc.car.hyundai.carcontroller import CarController +from opendbc.car.hyundai.hyundaicanfd import CanBus, create_acc_control, create_ioniq6_radar_heartbeat +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.ioniq6_handoff import build_ioniq6_hda2_long_candidate +from opendbc.car.hyundai.ioniq6_longitudinal import Ioniq6LongitudinalPolicy, Ioniq6LongitudinalState, LongState, update_calibration +from opendbc.car.hyundai.values import CAR, DBC +from openpilot.selfdrive.controls.lib.longcontrol import LongControl +from openpilot.starpilot.longitudinal.ioniq6_start import StartEvidence + +DATA = Path(__file__).parent / 'testdata' + + +def controller_fixture(alternate=False, *, aol=False): + fingerprint = gen_empty_fingerprint() + fingerprint[2].update({0x110: 32, 0x362: 32} if alternate else {0x50: 16, 0x2A4: 24}) + fingerprint[1].update({0x1CF: 8, 0x35: 32, 0x175: 24, 0xA0: 24, 0xEA: 24, + 0x1BA: 24, 0x1E5: 16, 0x36A: 16}) + fingerprint[0][0x3A5] = 24 + fingerprint[0][0x100] = 24 + stock = CarInterface.get_params(CAR.HYUNDAI_IONIQ_6, fingerprint, [], False, False, False) + cp = build_ioniq6_hda2_long_candidate(stock, fingerprint) + if aol: + cp.safetyConfigs[-1].safetyParam |= 0x0800 + ci = CarInterface(cp) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + bus = CanBus(cp) + inputs = [packer.make_can_msg(name, bus.ECAN, values) for name, values in ( + ('ACCELERATOR', {'GEAR': 5}), ('TCS', {'ACCEnable': 0, 'ACC_REQ': 1}), + ('WHEEL_SPEEDS', {}), ('MDPS', {}), ('CRUISE_BUTTONS', {'COUNTER': 1}), + ('DOORS_SEATBELTS', {'DRIVER_SEATBELT': 1}), ('STEERING_SENSORS', {}))] + inputs.append(packer.make_can_msg('CAM_0x362' if alternate else 'CAM_0x2a4', bus.CAM, {})) + ci.update((1_000_000_000, inputs)) + return cp, ci.CS, CarController(DBC[cp.carFingerprint], cp) + + +class TestIoniq6Longitudinal(unittest.TestCase): + def test_active_calibration_matches_continuous_corrected_frozen_reference(self): + reference = json.loads((DATA / 'ioniq6_long_calibration.json').read_text()) + corrected = json.loads((DATA / 'ioniq6_long_calibration_soft_floor.json').read_text()) + self.assertEqual(corrected['originalTraceSha256'], hashlib.sha256((DATA / 'ioniq6_long_calibration.json').read_bytes()).hexdigest()) + # Independently derived from committed prechange calibration by adding + # only the approved below-floor revocation. Keep every state transition + # continuous across both historical soft-start windows. + changed_rows = [index for index, (old, new) in enumerate(zip(reference['outputs'], corrected['outputs'], strict=True)) if old != new] + self.assertEqual(changed_rows, list(range(12)) + list(range(76, 90))) + state = Ioniq6LongitudinalState() + for row, expected in zip(reference['inputs'], corrected['outputs'], strict=True): + accel, speed, measured_accel, mode = row + update_calibration(state, accel, speed, measured_accel, getattr(LongState, mode)) + actual = (state.desired_accel, state.actual_accel, state.accel_last, state.jerk_upper, + state.jerk_lower, state.launch_active, state.stopping, int(state.long_control_state_last)) + for value, previous in zip(actual, expected, strict=True): + self.assertAlmostEqual(value, previous, places=12) + + def test_soft_target_revokes_launch_on_offcycle_and_scheduled_ticks(self): + for drop_frame in (31, 35): + with self.subTest(drop_frame=drop_frame): + policy = Ioniq6LongitudinalPolicy() + last_sent = 0.0 + for frame in range(drop_frame): + request = policy.update(frame, active=True, override=False, accel=1.2, speed=0.0, + measured_accel=0.0, control_state=LongState.starting, last_sent_accel=last_sent) + if frame % 2 == 0: + last_sent = request.accel + self.assertTrue(policy.state.launch_active) + soft = policy.update(drop_frame, active=True, override=False, accel=.2, speed=0.0, + measured_accel=0.0, control_state=LongState.starting, last_sent_accel=last_sent) + self.assertFalse(policy.state.launch_active) + self.assertLess(soft.accel, last_sent) + self.assertGreater(soft.accel, .2) # Residual demand is rate-limited, not abruptly clipped. + self.assertLessEqual(soft.accel, last_sent + 1e-12) + negative = policy.update(drop_frame + 1, active=True, override=False, accel=-1.0, speed=0.0, + measured_accel=0.0, control_state=LongState.starting, last_sent_accel=soft.accel) + self.assertFalse(policy.state.launch_active) + self.assertLess(negative.accel, soft.accel) + self.assertGreater(negative.accel, -1.0) + policy.update(drop_frame + 2, active=True, override=False, accel=1.2, speed=0.0, + measured_accel=0.0, control_state=LongState.starting, last_sent_accel=negative.accel) + if (drop_frame + 2) % 5 != 0: + self.assertFalse(policy.state.launch_active) + for frame in range(drop_frame + 3, drop_frame + 8): + policy.update(frame, active=True, override=False, accel=1.2, speed=0.0, + measured_accel=0.0, control_state=LongState.starting, last_sent_accel=negative.accel) + self.assertTrue(policy.state.launch_active) + policy.update(drop_frame + 8, active=True, override=True, accel=1.2, speed=0.0, + measured_accel=0.0, control_state=LongState.starting, last_sent_accel=0.0) + self.assertEqual(policy.state, Ioniq6LongitudinalState()) + + def test_disengage_override_and_off_state_discard_old_demand(self): + for inactive in ({'active': False}, {'override': True}, {'control_state': LongState.off}): + with self.subTest(inactive=inactive): + policy = Ioniq6LongitudinalPolicy() + inputs = dict(active=True, override=False, accel=1.8, speed=0.0, measured_accel=0.0, + control_state=LongState.starting, last_sent_accel=0.0) + for frame in range(50): + inputs['last_sent_accel'] = policy.update(frame, **inputs).accel + result = policy.update(51, **(inputs | inactive)) + self.assertEqual(result.accel, 0.0) + self.assertFalse(result.stopping) + self.assertEqual(policy.state, Ioniq6LongitudinalState()) + resumed = policy.update(52, **(inputs | {'accel': .1, 'last_sent_accel': 0.0})) + fresh = Ioniq6LongitudinalPolicy().update(52, **(inputs | {'accel': .1, 'last_sent_accel': 0.0})) + self.assertEqual(resumed, fresh) + + def test_braking_rate_uses_last_transmitted_frame_and_stays_bounded(self): + policy = Ioniq6LongitudinalPolicy() + inputs = dict(active=True, override=False, speed=20., measured_accel=.2, control_state=LongState.pid) + last_sent = 0.0 + for frame in range(300): + result = policy.update(frame, **inputs, accel=2.0 if frame < 50 else -3.5, last_sent_accel=last_sent) + self.assertGreaterEqual(result.accel, -3.5) + self.assertLessEqual(result.accel, 2.0) + if frame % 2 == 0: + if frame >= 50: + self.assertGreaterEqual(result.accel + 1e-12, last_sent - .36) + last_sent = result.accel + self.assertAlmostEqual(last_sent, -3.5) + + def test_real_controller_scc_respects_all_four_axis_states(self): + for alternate in (False, True): + for lateral, longitudinal in ((False, False), (True, False), (False, True), (True, True)): + for override in (False, True): + with self.subTest(alternate=alternate, lat=lateral, long=longitudinal, override=override): + cp, cs, controller = controller_fixture(alternate) + control = structs.CarControl() + control.enabled = lateral or longitudinal + control.latActive, control.longActive = lateral, longitudinal + control.cruiseControl.override = override + cs.out.gasPressed = lateral and not longitudinal and override + control.actuators.accel = 1.0 + control.actuators.torque = .02 + control.actuators.longControlState = LongState.starting + output, frames = controller.update(control.as_reader(), cs, 1_000_000_000) + scc = next(frame for frame in frames if frame[0] == 0x1A0) + parser = CANParser(DBC[cp.carFingerprint][Bus.pt], [('SCC_CONTROL', 0)], 1) + parser.update((1_000_000_000, [scc])) + values = parser.vl['SCC_CONTROL'] + self.assertEqual(values['ACCMode'], 2 if control.enabled and override else 1 if longitudinal else 0) + if not longitudinal or override: + self.assertEqual((values['aReqRaw'], values['aReqValue'], output.accel), (0., 0., 0.)) + self.assertEqual(values['StopReq'], 0) + else: + self.assertGreater(values['aReqValue'], 0.) + self.assertAlmostEqual(values['aReqValue'], output.accel, delta=.0051) + self.assertAlmostEqual(values['aReqRaw'], output.accel, delta=.0051) + if not lateral: + self.assertEqual(output.torque, 0.) + + def test_pedal_override_keeps_scc_engaged_without_acceleration(self): + cp, cs, controller = controller_fixture() + control = structs.CarControl() + control.enabled = True + control.latActive = True + control.actuators.accel = 1.0 + control.actuators.longControlState = LongState.pid + parser = CANParser(DBC[cp.carFingerprint][Bus.pt], [('SCC_CONTROL', 0)], 1) + + for enabled, long_active, override, gas_pressed, brake_pressed, expected_mode in ( + (True, True, False, False, False, 1), + (True, False, True, True, False, 2), + (True, False, True, False, False, 0), + (True, False, True, True, True, 0), + (True, True, False, False, False, 1), + (False, False, False, False, False, 0), + ): + control.enabled = enabled + control.longActive = long_active + control.cruiseControl.override = override + cs.out.gasPressed = gas_pressed + cs.out.brakePressed = brake_pressed + _, frames = controller.update(control.as_reader(), cs, 1_000_000_000) + scc = next(frame for frame in frames if frame[0] == 0x1A0) + parser.update((1_000_000_000, [scc])) + values = parser.vl['SCC_CONTROL'] + self.assertEqual(values['ACCMode'], expected_mode) + if not long_active: + self.assertEqual((values['aReqRaw'], values['aReqValue'], values['StopReq']), (0., 0., 0)) + controller.frame += 1 + + def test_real_controller_scc_withdraws_launch_floor_after_soft_target(self): + for alternate in (False, True): + for drop_frame in (31, 34): # Between SCC sends and on an SCC send. + with self.subTest(alternate=alternate, drop_frame=drop_frame): + cp, cs, controller = controller_fixture(alternate) + parser = CANParser(DBC[cp.carFingerprint][Bus.pt], [('SCC_CONTROL', 0)], 1) + control = structs.CarControl() + control.enabled = True + control.longActive = True + control.actuators.longControlState = LongState.starting + last_scc_accel = 0.0 + + def tick(frame: int, target: float, *, override: bool = False, + control=control, controller=controller, cs=cs, parser=parser): + control.actuators.accel = target + control.cruiseControl.override = override + output, frames = controller.update(control.as_reader(), cs, 1_000_000_000 + frame * 10_000_000) + scc = [message for message in frames if message[0] == 0x1A0] + self.assertEqual(len(scc), int(frame % 2 == 0)) + if scc: + parser.update((1_000_000_000 + frame * 10_000_000, scc)) + self.assertAlmostEqual(parser.vl['SCC_CONTROL']['aReqValue'], output.accel, delta=.0051) + return output.accel, parser.vl['SCC_CONTROL']['aReqValue'] if scc else None + + for frame in range(drop_frame): + _, sent = tick(frame, 1.2) + if sent is not None: + last_scc_accel = sent + self.assertTrue(controller.ioniq6_longitudinal.state.launch_active) + soft_output, soft_scc = tick(drop_frame, .2) + self.assertFalse(controller.ioniq6_longitudinal.state.launch_active) + if soft_scc is not None: + self.assertLess(soft_scc, last_scc_accel) + self.assertGreater(soft_scc, .2) + else: + self.assertLess(soft_output, last_scc_accel) + _, next_scc = tick(drop_frame + 1, .2) + self.assertIsNotNone(next_scc) + self.assertLess(next_scc, last_scc_accel) + self.assertGreater(next_scc, .2) + + next_frame = drop_frame + (2 if drop_frame % 2 == 0 else 3) + for frame in range(drop_frame + (2 if drop_frame % 2 else 1), next_frame): + tick(frame, .2) + negative, sent = tick(next_frame, -1.0) + self.assertFalse(controller.ioniq6_longitudinal.state.launch_active) + self.assertIsNotNone(sent) + self.assertLess(negative, soft_output) + self.assertGreater(negative, -1.0) + for frame in range(next_frame + 1, next_frame + 7): + tick(frame, 1.2) + self.assertTrue(controller.ioniq6_longitudinal.state.launch_active) + reset, sent = tick(next_frame + 7, 1.2, override=True) + self.assertEqual(reset, 0.0) + self.assertEqual(controller.ioniq6_longitudinal.state, Ioniq6LongitudinalState()) + + def test_real_host_state_then_controller_scc_on_same_tick(self): + for alternate in (False, True): + with self.subTest(alternate=alternate): + cp, cs, controller = controller_fixture(alternate) + host = LongControl(cp) + self.assertIsNotNone(host.ioniq6_start) + parser = CANParser(DBC[cp.carFingerprint][Bus.pt], [('SCC_CONTROL', 0)], 1) + cs.out.canValid = True + cs.out.canTimeout = False + cs.out.vEgo = 0.0 + cs.out.aEgo = 0.0 + control = structs.CarControl() + control.enabled = True + control.longActive = True + + def advance(frame: int, target: float, *, override: bool = False, + host=host, cs=cs, control=control, controller=controller, parser=parser): + control.cruiseControl.override = override + evidence = StartEvidence(True, True, True, 1, 1_000_000_000 + frame * 10_000_000) + output = host.update(not override, cs.out, target, False, (-3.5, 2.0), start_evidence=evidence) + # Match Controls' tagged-Ioniq order: publish state after LoC.update. + control.actuators.longControlState = host.long_control_state + control.actuators.accel = float(output) + sent, frames = controller.update(control.as_reader(), cs, 1_000_000_000 + frame * 10_000_000) + scc = [message for message in frames if message[0] == 0x1A0] + if scc: + parser.update((1_000_000_000 + frame * 10_000_000, scc)) + return sent.accel, parser.vl['SCC_CONTROL']['aReqValue'] if scc else None + + for frame in range(34): + advance(frame, .8) + self.assertEqual(host.long_control_state, LongState.pid) + output, scc = advance(34, .8) + self.assertEqual(host.long_control_state, LongState.starting) + self.assertIsNotNone(scc) + self.assertAlmostEqual(output, scc, delta=.0051) + advance(35, .8) # Controller calibration runs every fifth tick. + self.assertTrue(controller.ioniq6_longitudinal.state.launch_active) + + soft_output, soft_scc = advance(36, .2) + self.assertEqual(host.long_control_state, LongState.pid) + self.assertFalse(controller.ioniq6_longitudinal.state.launch_active) + self.assertIsNotNone(soft_scc) + self.assertLess(soft_scc, scc) + self.assertAlmostEqual(soft_output, soft_scc, delta=.0051) + negative, _ = advance(37, -.3) + self.assertFalse(controller.ioniq6_longitudinal.state.launch_active) + self.assertLess(negative, soft_output) + + def test_default_scc_packing_matches_committed_parent_bytes(self): + reference = json.loads((DATA / 'scc_parent_frames.json').read_text()) + for row in reference['cases']: + packer = CANPacker(DBC[CAR.HYUNDAI_IONIQ_6][Bus.pt]) + frame = create_acc_control(packer, SimpleNamespace(ECAN=1), *row['args'], SimpleNamespace(leadDistanceBars=2)) + self.assertEqual(frame[1].hex(), row['data']) + + def test_radar_heartbeat_matches_frozen_wire_and_runs_while_axes_are_off(self): + reference = json.loads((DATA / 'ioniq6_heartbeat.json').read_text()) + for row in reference['cases']: + frame = create_ioniq6_radar_heartbeat(row['counter'], row['brake'], row['gas']) + self.assertEqual((frame.address, frame.src, len(frame.dat)), (0x100, 0, 24)) + self.assertEqual(frame.dat.hex(), row['payload']) + for alternate in (False, True): + _, cs, controller = controller_fixture(alternate) + control = structs.CarControl() + counters = [] + for tick in range(1040): + cs.out.brakePressed = tick % 8 < 4 + cs.out.gasPressed = tick % 12 < 4 + _, frames = controller.update(control.as_reader(), cs, 1_000_000_000 + tick * 10_000_000) + heartbeats = [frame for frame in frames if frame[0] == 0x100] + self.assertEqual(len(heartbeats), int(tick % 4 == 0)) + if heartbeats: + frame = heartbeats[0] + self.assertEqual(frame[2], 0) + self.assertEqual(frame[1][4], int(cs.out.brakePressed)) + self.assertEqual(frame[1][22], int(cs.out.gasPressed)) + counters.append(frame[1][2]) + self.assertEqual(counters, [counter & 0xff for counter in range(260)]) + + def test_stock_ioniq_and_other_cars_do_not_install_this_policy(self): + for car in (CAR.HYUNDAI_IONIQ_6, CAR.HYUNDAI_IONIQ_5, CAR.KIA_EV6): + cp = CarInterface.get_non_essential_params(car) + self.assertIsNone(CarController(DBC[car], cp).ioniq6_longitudinal) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_media.py b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_media.py new file mode 100644 index 0000000000..79739465ef --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_media.py @@ -0,0 +1,94 @@ +import math +import unittest + +from opendbc.can import CANPacker, CANParser +from opendbc.car.hyundai.ioniq6_media import Ioniq6MediaButtons, MEDIA_ADDRESS, MEDIA_MAX_GAP_NS + + +START = 10_000_000_000 + + +def packet(stamp, *, mode=False, custom=False, bus=1): + payload = bytearray(8) + payload[2] = 0x40 if mode else 0 + payload[5] = 0x10 if custom else 0 + return (stamp, [(MEDIA_ADDRESS, bytes(payload), bus)]) + + +class TestIoniq6Media(unittest.TestCase): + def test_dbc_matches_independent_physical_bits(self): + packer = CANPacker('hyundai_canfd_generated') + parser = CANParser('hyundai_canfd_generated', [('STEERING_WHEEL_MEDIA_BUTTONS', math.nan)], 1) + source = Ioniq6MediaButtons(1) + for index, (mode, custom) in enumerate(((False, False), (True, False), (False, True), (True, True))): + expected = packet(START + index * 200_000_000, mode=mode, custom=custom) + frame = packer.make_can_msg('STEERING_WHEEL_MEDIA_BUTTONS', 1, {'MODE_BUTTON': mode, 'CUSTOM_BUTTON': custom}) + self.assertEqual(tuple(frame), expected[1][0]) + parser.update([expected]) + self.assertEqual(parser.vl['STEERING_WHEEL_MEDIA_BUTTONS']['MODE_BUTTON'], int(mode)) + self.assertEqual(parser.vl['STEERING_WHEEL_MEDIA_BUTTONS']['CUSTOM_BUTTON'], int(custom)) + sample = source.update([expected]).samples[0] + self.assertEqual((sample.mode_held, sample.custom_held), (mode, custom)) + + def test_keeps_packet_order_and_never_repeats_cached_samples(self): + source = Ioniq6MediaButtons(1) + observed = source.update([packet(START), packet(START + 100_000_000, mode=True), packet(START + 200_000_000)]) + self.assertTrue(observed.valid) + self.assertEqual([s.mode_held for s in observed.samples], [False, True, False]) + self.assertEqual([s.source_boot_ns for s in observed.samples], [START, START + 100_000_000, START + 200_000_000]) + cached = source.update([(START + 250_000_000, [(0x123, b'other', 1)])]) + self.assertTrue(cached.valid) + self.assertEqual(cached.samples, ()) + self.assertEqual(cached.source_epoch, observed.source_epoch) + + def test_wrong_bus_and_echo_cannot_create_or_refresh_source(self): + for bus in (0, 2, 3, 129): + with self.subTest(bus=bus): + source = Ioniq6MediaButtons(1) + self.assertFalse(source.update([packet(START, mode=True, bus=bus)]).valid) + self.assertTrue(source.update([packet(START + 1)]).valid) + result = source.update([packet(START + MEDIA_MAX_GAP_NS + 2, mode=True, bus=bus)]) + self.assertFalse(result.valid) + self.assertEqual(result.samples, ()) + for bus in (True, -1, 4, 129): + with self.assertRaises(ValueError): + Ioniq6MediaButtons(bus) + + def test_short_long_and_duplicate_packets_revoke_whole_batch(self): + for payload in (b'', b'\0' * 5, b'\0' * 7, b'\0' * 9, b'\0' * 64): + with self.subTest(length=len(payload)): + source = Ioniq6MediaButtons(1) + initial = source.update([packet(START)]) + observed = source.update([packet(START + 10, mode=True), (START + 20, [(MEDIA_ADDRESS, payload, 1)])]) + self.assertFalse(observed.valid) + self.assertEqual(observed.samples, ()) + self.assertGreater(observed.source_epoch, initial.source_epoch) + source = Ioniq6MediaButtons(1) + source.update([packet(START)]) + self.assertFalse(source.update([packet(START, mode=True)]).valid) + same_stamp = source.update([packet(START + 10, mode=True), packet(START + 10)]) + self.assertFalse(same_stamp.valid) + self.assertEqual(same_stamp.samples, ()) + + def test_gap_missing_can_and_clock_rewind_start_new_epoch(self): + cases = ([packet(START + MEDIA_MAX_GAP_NS + 1, mode=True)], + [(START + MEDIA_MAX_GAP_NS + 1, [])], [packet(START - 1)], []) + for next_packets in cases: + with self.subTest(packets=next_packets): + source = Ioniq6MediaButtons(1) + first = source.update([packet(START, mode=True)]) + invalid = source.update(next_packets) + self.assertFalse(invalid.valid) + self.assertGreater(invalid.source_epoch, first.source_epoch) + # Recovery still supplies only observations; the gesture owner must + # observe neutral in the new epoch before accepting a held button. + recovered = source.update([packet(START + 1_000_000_000, mode=True)]) + self.assertTrue(recovered.valid) + self.assertEqual(recovered.source_epoch, invalid.source_epoch) + source = Ioniq6MediaButtons(1) + source.update([packet(START)]) + self.assertTrue(source.update([packet(START + MEDIA_MAX_GAP_NS)]).valid) + + +if __name__ == '__main__': + unittest.main() diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_preflight_retry.py b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_preflight_retry.py new file mode 100644 index 0000000000..6ee9a90d70 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_preflight_retry.py @@ -0,0 +1,124 @@ +"""Source-only preflight regression: never imports the original/runtime tree.""" +import ast +import binascii +from collections import namedtuple +from pathlib import Path +import statistics +import time +import unittest +from dataclasses import dataclass +from enum import Enum, auto +from unittest.mock import patch + + +def checksum(address, _sig, data): + # Equivalent XMODEM polynomial and existing Hyundai length XORs. + return binascii.crc_hqx(bytes(data[2:]) + address.to_bytes(2, 'little'), 0) ^ {8: 0x5F29, 16: 0x041D, 24: 0x819D, 32: 0x9F5B}[len(data)] + + +SOURCE = Path(__file__).resolve().parents[1] / 'ioniq6_handoff.py' +NAMES = {'HandoffOutcome', 'HandoffResult', '_valid_canfd_crc', '_read_window', '_measured_period', 'inspect_ioniq6_long_sources', 'run_ioniq6_handoff'} +tree = ast.parse(SOURCE.read_text()) +body = [n for n in tree.body if (isinstance(n, (ast.FunctionDef, ast.ClassDef)) and n.name in NAMES) or + (isinstance(n, ast.Assign) and any(isinstance(t, ast.Name) and t.id.startswith('IONIQ6_') for t in n.targets))] +ns = dict(statistics=statistics, time=time, dataclass=dataclass, Enum=Enum, auto=auto, hkg_can_fd_checksum=checksum, __name__=__name__) +body.insert(0, ast.ImportFrom(module='__future__', names=[ast.alias(name='annotations')], level=0)) +exec(compile(ast.fix_missing_locations(ast.Module(body=body, type_ignores=[])), str(SOURCE), 'exec'), ns) +CanData = namedtuple('CanData', 'address dat src') + + +class Bus: + def __init__(self, modes): + self.modes, self.now, self.window, self.index, self.drains, self.flags = modes, 0., -1, -1, 0, [] + + @staticmethod + def frame(address, length, counter=0, src=1, bad_crc=False): + data = bytearray(length) + data[2] = counter % 256 + data[:2] = checksum(address, None, data).to_bytes(2, 'little') + if bad_crc: + data[0] ^= 1 + return CanData(address, bytes(data), src) + + def recv(self, wait_for_one=False): + self.flags.append(wait_for_one) + if self.index in (-1, 13): + self.window += 1 + self.drains += 1 + self.index = 0 + self.now += .001 + return [] + self.index += 1 + mode = self.modes[min(self.window, len(self.modes) - 1)] + self.now += .020 + if mode == 'empty': + return [] + frames = [self.frame(a, length) for a, length in ns['IONIQ6_REQUIRED_RX'].items() + if not (mode == 'required' and a == 0xEA)] + frames.append(self.frame(0x1A0, 32, self.index * (2 if mode == 'counter' else 1), bad_crc=mode == 'crc')) + if mode != 'bsm': + frames.extend(self.frame(a, length) for a, length in ns['IONIQ6_STOCK_BSM'].items()) + if mode != 'heartbeat': + frames.append(self.frame(0x100, 24, src=0)) + packet = type('Packet', (list,), {})(frames) + packet.log_mono_time_ns = int((self.now - (.013 if mode == 'period' and self.index == 5 else 0)) * 1e9) + return [packet] + + +class TestRetry(unittest.TestCase): + def inspect(self, modes): + bus = Bus(modes) + with patch.object(time, 'sleep'): + result = ns['inspect_ioniq6_long_sources'](bus.recv, lambda: bus.now) + return bus, result + + def test_invalid_then_valid(self): + bus, result = self.inspect(['period', 'valid']) + self.assertEqual(result.reason, 'preflight_ready') + self.assertAlmostEqual(result.source_period, .020) + self.assertEqual(bus.drains, 2) + self.assertFalse(any(bus.flags)) + + def test_all_invalid_denies_without_uds(self): + bus, exchanges = Bus(['period']), [] + with patch.object(time, 'sleep'): + result = ns['run_ioniq6_handoff'](bus.recv, lambda *args: exchanges.append(args), lambda: bus.now) + self.assertEqual(result.reason, 'preflight_scc_period') + self.assertIsNone(result.source_period) + self.assertEqual(bus.drains, 3) + self.assertEqual(exchanges, []) + self.assertLess(bus.now, .81) + + def test_crc_counter_health_unchanged(self): + for mode, reason in [('crc', 'scc_period'), ('counter', 'scc_period'), ('required', 'required_rx'), ('bsm', 'bsm_status'), ('heartbeat', 'radar_heartbeat')]: + with self.subTest(mode=mode): + bus, result = self.inspect([mode]) + self.assertIsNone(result.source_period) + self.assertIn(reason, result.reason) + self.assertEqual(bus.drains, 3) + + def test_no_cross_window_health_and_latest_failure(self): + bus, result = self.inspect(['heartbeat', 'bsm', 'required']) + self.assertEqual(result.reason, 'preflight_required_rx') + self.assertIsNone(result.source_period) + self.assertEqual(bus.drains, 3) + + def test_empty_bus_real_clock_is_bounded(self): + flags = [] + started = time.monotonic() + result = ns['inspect_ioniq6_long_sources'](lambda wait_for_one=False: flags.append(wait_for_one) or []) + self.assertIsNone(result.source_period) + self.assertFalse(any(flags)) + self.assertLess(time.monotonic() - started, 1.25) + + def test_cadence_thresholds_unchanged(self): + measured = ns['_measured_period'] + good = [(i * .020, i, i * .020) for i in range(8)] + self.assertAlmostEqual(measured(good), .020) + for bad in ([ (t,c,0.) for t,c,_ in good], [(t,c*2,r) for t,c,r in good], + [(i*.031,i,i*.031) for i in range(8)], [(i*.011,i,i*.020) for i in range(8)], good[:3]): + self.assertIsNone(measured(bad)) + + +if __name__ == '__main__': + unittest.main() diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_rx.py b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_rx.py new file mode 100644 index 0000000000..4f7a1cb761 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_rx.py @@ -0,0 +1,127 @@ +import unittest + +from opendbc.can import CANPacker +from opendbc.car import Bus, gen_empty_fingerprint, structs +from opendbc.car.hyundai.carcontroller import CarController +from opendbc.car.hyundai.carstate import CarState, decode_ioniq_6_corner_bsm +from opendbc.car.hyundai.hyundaicanfd import CanBus +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.radar_interface import MRR35_RADAR_START_ADDR, RadarInterface, get_radar_can_parser, radar_bus +from opendbc.car.hyundai.values import CAR, DBC, HyundaiFlags + + +def params(topology: str, *, radar_bus_number: int | None = None, radar_length: int = 24, bsm: bool = False): + fingerprint = gen_empty_fingerprint() + if topology == "lka": + fingerprint[2][0x50] = 16 + elif topology == "lka_alt": + fingerprint[2][0x110] = 32 + else: + assert topology == "camera" + ecan = 1 if topology != "camera" else 0 + fingerprint[ecan][0x1cf] = 8 + if radar_bus_number is not None: + fingerprint[radar_bus_number][MRR35_RADAR_START_ADDR] = radar_length + if bsm: + fingerprint[ecan][0x1ba] = 24 + return CarInterface.get_params(CAR.HYUNDAI_IONIQ_6, fingerprint, [], False, False, False) + + +class TestIoniq6ReceiveOnly(unittest.TestCase): + def test_radar_bus_length_and_stock_cruise_are_topology_bound(self): + for topology, expected_bus in (("camera", 1), ("lka", 0), ("lka_alt", 0)): + for observed_bus, length in ((None, 24), (expected_bus, 24), (1 - expected_bus, 24), (expected_bus, 8)): + with self.subTest(topology=topology, bus=observed_bus, length=length): + cp = params(topology, radar_bus_number=observed_bus, radar_length=length) + self.assertEqual(radar_bus(cp), expected_bus) + self.assertEqual(get_radar_can_parser(cp).bus, expected_bus) + self.assertEqual(cp.radarUnavailable, observed_bus != expected_bus or length != 24) + self.assertFalse(cp.openpilotLongitudinalControl) + self.assertTrue(cp.pcmCruise) + self.assertFalse(cp.alphaLongitudinalAvailable) + self.assertEqual(bool(cp.flags & HyundaiFlags.CANFD_CAMERA_SCC), topology == "camera") + + def test_mrr35_real_packed_cycle_with_missing_and_delayed_tracks(self): + for topology, expected_bus in (("camera", 1), ("lka", 0), ("lka_alt", 0)): + with self.subTest(topology=topology): + cp = params(topology, radar_bus_number=expected_bus) + radar = RadarInterface(cp) + packer = CANPacker(DBC[cp.carFingerprint][Bus.radar]) + track = packer.make_can_msg("RADAR_TRACK_3a5", expected_bus, { + "STATE": 3, "LONG_DIST": 28.5, "LAT_DIST": -1.25, "REL_SPEED": -2.5, + }) + trigger = packer.make_can_msg("RADAR_TRACK_3c4", expected_bus, {}) + wrong_bus_track = (track[0], track[1], 1 - expected_bus) + self.assertIsNone(radar.update((1_000_000_000, [wrong_bus_track]))) + self.assertEqual(len(radar.update((1_010_000_000, [(track[0], track[1][:8], expected_bus), trigger])).points), 0) + out = radar.update((1_050_000_000, [track, trigger])) + self.assertEqual(len(out.points), 1) + self.assertAlmostEqual(out.points[0].dRel, 28.5, delta=0.05) + self.assertAlmostEqual(out.points[0].yRel, -1.25, delta=0.05) + self.assertAlmostEqual(out.points[0].vRel, -2.5, delta=0.02) + out = radar.update((1_100_000_000, [trigger])) + self.assertEqual(len(out.points), 0) + self.assertIsNone(radar.update((1_125_000_000, [track, (trigger[0], trigger[1][:8], expected_bus)]))) + self.assertIsNone(radar.update((1_150_000_000, [track]))) + out = radar.update((1_250_000_000, [trigger])) + self.assertEqual(len(out.points), 0) + + def test_corner_bits_are_received_without_status_transmission(self): + self.assertEqual(decode_ioniq_6_corner_bsm(0x02), (False, False)) + self.assertEqual(decode_ioniq_6_corner_bsm(0x0a), (False, True)) + self.assertEqual(decode_ioniq_6_corner_bsm(0x12), (True, False)) + self.assertEqual(decode_ioniq_6_corner_bsm(0x1a), (True, True)) + for topology in ("camera", "lka", "lka_alt"): + with self.subTest(topology=topology): + cp = params(topology, bsm=True) + self.assertTrue(cp.deprecated.enableBsm) + can_bus = CanBus(cp) + state = CarState(cp) + parsers = state.get_can_parsers(cp) + parser = parsers[Bus.pt] + self.assertEqual(parser.message_states[0x1ba].frequency, 0) + self.assertEqual(parser.message_states[0x36a].frequency, 0) + state.out = state.update(parsers) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + rear = packer.make_can_msg("ADAS_CMD_50_50ms", can_bus.ECAN, {"BCW_LtIndSta": 1}) + front = packer.make_can_msg("BLINDSPOTS_FRONT_CORNER_2", can_bus.ECAN, {"SIDE_DETECT_STATE": 0x08}) + parser.update((1_000_000_000, [rear, front])) + state.out = state.update(parsers) + self.assertTrue(state.out.leftBlindspot) + self.assertTrue(state.out.rightBlindspot) + control = structs.CarControl() + control.enabled = control.latActive = True + _, sent = CarController(DBC[cp.carFingerprint], cp).update(control.as_reader(), state, 1_000_000_000) + self.assertFalse({0x1ba, 0x1e5, 0x36a} & {address for address, _, _ in sent}) + wrong_bus_front = (front[0], front[1], 1 - can_bus.ECAN) + parser.update((1_100_000_000, [wrong_bus_front])) + state.out = state.update(parsers) + self.assertTrue(state.out.rightBlindspot) + # Rear and front sources expire independently as normal fuel traffic + # advances the parser clock; neither can advertise indefinitely. + fuel = packer.make_can_msg("ACCELERATOR", can_bus.ECAN, {}) + fresh_front = packer.make_can_msg("BLINDSPOTS_FRONT_CORNER_2", can_bus.ECAN, {"SIDE_DETECT_STATE": 0x10}) + parser.update((1_250_000_000, [fuel, fresh_front])) + state.out = state.update(parsers) + self.assertTrue(state.out.leftBlindspot) + self.assertFalse(state.out.rightBlindspot) + parser.update((1_500_000_000, [fuel])) + state.out = state.update(parsers) + self.assertFalse(state.out.leftBlindspot) + self.assertFalse(state.out.rightBlindspot) + + def test_front_source_without_rear_detection_does_not_enable_bsm(self): + cp = params("lka") + self.assertFalse(cp.deprecated.enableBsm) + state = CarState(cp) + parser = state.get_can_parsers(cp)[Bus.pt] + self.assertNotIn(0x36a, parser.message_states) + fingerprint = gen_empty_fingerprint() + fingerprint[2][0x50] = 16 + fingerprint[1][0x1ba] = 16 + cp = CarInterface.get_params(CAR.HYUNDAI_IONIQ_6, fingerprint, [], False, False, False) + self.assertFalse(cp.deprecated.enableBsm) + + +if __name__ == "__main__": + unittest.main() diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_stock_parser.py b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_stock_parser.py new file mode 100644 index 0000000000..67ccf6abfd --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_stock_parser.py @@ -0,0 +1,119 @@ +"""Synthetic stock CAN graph through actual CarState, CANParser and CI aggregation.""" +import unittest +from types import SimpleNamespace +from unittest.mock import patch + +from opendbc.can import CANPacker +from opendbc.car import Bus, gen_empty_fingerprint +from opendbc.car.hyundai.carstate import CarState +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.interfaces import CarInterfaceBase +from opendbc.car.hyundai.ioniq6_handoff import (build_ioniq6_hda2_long_candidate, finalize_ioniq6_prepublication, + HandoffOutcome, HandoffResult) +from opendbc.car.hyundai.values import CAR, DBC + + +def params(alternate): + fingerprint = gen_empty_fingerprint() + fingerprint[2].update({0x110 if alternate else 0x50: 32 if alternate else 16, + 0x362 if alternate else 0x2A4: 32 if alternate else 24}) + fingerprint[1].update({0x1CF: 8, 0x35: 32, 0x175: 24, 0xA0: 24, 0xEA: 24, + 0x1BA: 24, 0x1E5: 16, 0x36A: 16}) + fingerprint[0][0x3A5] = 24 + cp = CarInterface.get_params(CAR.HYUNDAI_IONIQ_6, fingerprint, [], False, False, False) + return cp, fingerprint + + +class StockParserTests(unittest.TestCase): + def host(self, cp): + cs = CarState(cp) + ci = SimpleNamespace(CS=cs, CP=cp, can_parsers=cs.get_can_parsers(cp), v_ego_cluster_seen=False) + ci.update = lambda frames: CarInterfaceBase.update(ci, frames) + ci.update([]) # register every lazy field the real CarState consumes + return ci + + def feed(self, ci, *, omit=(), alt_units=None, count=200): + packer = CANPacker(DBC[ci.CP.carFingerprint][Bus.pt]) + out = None + for index in range(1, count + 1): + frames = [] + ci.test_time_ns = getattr(ci, 'test_time_ns', 1_000_000_000) + 10_000_000 + for parser in ci.can_parsers.values(): + for state in list(parser.message_states.values()): + if state.name in omit or (state.ignore_alive and not (state.name == 'CRUISE_BUTTONS_ALT' and alt_units is not None)): + continue + values = {'COUNTER': index % (16 if state.name == 'CRUISE_BUTTONS' else 256)} if any(signal.name == 'COUNTER' for signal in state.signals) else {} + if state.name == 'ACCELERATOR': values.update(GEAR=5) + elif state.name == 'DOORS_SEATBELTS': values.update(DRIVER_SEATBELT=1) + elif state.name == 'SCC_CONTROL': values.update(ACCMode=1, VSetDis=60) + elif state.name == 'CRUISE_BUTTONS_ALT': values.update(DISTANCE_UNIT=alt_units) + frame = packer.make_can_msg(state.name, parser.bus, values) + frames.append((frame[0], frame[1], frame[2])) + out = ci.update([(ci.test_time_ns, frames)]) + return out + + def test_alpha_off_and_alpha_preflight_stock_fallback_without_optional_alt(self): + for alternate in (False, True): + for enabled in (False, True): + with self.subTest(alternate=alternate, alpha_requested=enabled): + stock, fingerprint = params(alternate) + candidate = build_ioniq6_hda2_long_candidate(stock, fingerprint) + exchanges = [] + with patch('opendbc.car.hyundai.ioniq6_handoff.run_ioniq6_handoff', + return_value=HandoffResult(HandoffOutcome.STOCK, 'preflight_scc_period')): + cp, selected, _ = finalize_ioniq6_prepublication(stock, candidate, lambda **kw: [], exchanges.append, + enabled=enabled, is_release=False) + self.assertIs(cp, stock) + self.assertFalse(selected) + self.assertEqual(exchanges, []) + self.assertTrue(cp.pcmCruise) + self.assertFalse(cp.openpilotLongitudinalControl) + ci = self.host(cp) + parser = ci.can_parsers[Bus.pt] + self.assertTrue(parser.message_states[parser.dbc.name_to_msg['CRUISE_BUTTONS_ALT'].address].ignore_alive) + out = self.feed(ci) + self.assertTrue(out.canValid) + self.assertFalse(out.canTimeout) + self.assertTrue(out.cruiseState.enabled) + self.assertTrue(ci.CS.is_metric) # existing absent-units default, not current units proof + + def test_present_optional_alt_units_still_decode(self): + for alternate in (False, True): + cp, _ = params(alternate) + ci = self.host(cp) + out = self.feed(ci, alt_units=1) + self.assertTrue(out.canValid) + self.assertFalse(ci.CS.is_metric) + + def test_stock_required_inputs_remain_required(self): + for alternate in (False, True): + for missing in ('ACCELERATOR', 'TCS', 'WHEEL_SPEEDS', 'MDPS', 'CRUISE_BUTTONS', 'SCC_CONTROL'): + with self.subTest(alternate=alternate, missing=missing): + cp, _ = params(alternate) + ci = self.host(cp) + self.assertTrue(self.feed(ci).canValid) + parser = ci.can_parsers[Bus.pt] + state = parser.message_states[parser.dbc.name_to_msg[missing].address] + self.assertFalse(state.ignore_alive) + # Advance past the actual retained parser timeout, keep all siblings healthy. + out = self.feed(ci, omit=(missing,), count=int(state.timeout_threshold / 10_000_000) + 20) + self.assertFalse(out.canValid) + + def test_existing_long_parser_subscription_contract_is_unchanged(self): + for alternate in (False, True): + stock, fingerprint = params(alternate) + cp = build_ioniq6_hda2_long_candidate(stock, fingerprint) + self.assertIsNotNone(cp) + ci = self.host(cp) + pt = ci.can_parsers[Bus.pt] + for name in ('ACCELERATOR', 'TCS', 'WHEEL_SPEEDS', 'MDPS', 'CRUISE_BUTTONS'): + self.assertFalse(pt.message_states[pt.dbc.name_to_msg[name].address].ignore_alive) + self.assertTrue(pt.message_states[pt.dbc.name_to_msg['CRUISE_BUTTONS_ALT'].address].ignore_alive) + self.assertNotIn(pt.dbc.name_to_msg['SCC_CONTROL'].address, pt.message_states) + camera_name = 'CAM_0x362' if alternate else 'CAM_0x2a4' + cam = ci.can_parsers[Bus.cam] + self.assertEqual(cam.message_states[cam.dbc.name_to_msg[camera_name].address].frequency, 20) + + +if __name__ == '__main__': + unittest.main() diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_non_scc.py b/opendbc_repo/opendbc/car/hyundai/tests/test_non_scc.py new file mode 100644 index 0000000000..693c161038 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_non_scc.py @@ -0,0 +1,182 @@ +import unittest + +from opendbc.can import CANPacker, CANParser +from opendbc.car import Bus, gen_empty_fingerprint, structs +from opendbc.car.fw_versions import match_fw_to_car +from opendbc.car.hyundai.carcontroller import CarController +from opendbc.car.hyundai.carstate import CarState, get_non_scc_cruise_signals +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.radar_interface import RadarInterface, get_radar_can_parser +from opendbc.car.hyundai.values import CAR, DBC, HyundaiFlags, HyundaiSafetyFlags +from opendbc.car.hyundai.fingerprints import FW_VERSIONS +from opendbc.car.structs import CarParams + + +NON_SCC_CARS = ( + CAR.GENESIS_G70_2021_NON_SCC, CAR.HYUNDAI_BAYON_1ST_GEN_NON_SCC, + CAR.HYUNDAI_ELANTRA_2022_NON_SCC, CAR.HYUNDAI_ELANTRA_HEV_2022_NON_SCC, + CAR.HYUNDAI_KONA_EV_NON_SCC, CAR.HYUNDAI_KONA_NON_SCC, + CAR.KIA_CEED_PHEV_2022_NON_SCC, CAR.KIA_FORTE_2019_NON_SCC, + CAR.KIA_FORTE_2021_NON_SCC, CAR.KIA_SELTOS_2023_NON_SCC, +) + + +def params(candidate, fingerprint=None, car_fw=None, alpha_long=True): + return CarInterface.get_params(candidate, fingerprint or gen_empty_fingerprint(), car_fw or [], alpha_long, False, False) + + +class TestNonSccFamily(unittest.TestCase): + def test_forte_sender_preserves_original_torque_scale_and_sibling_limits(self): + cases = ((CAR.KIA_FORTE_2019_NON_SCC, 255), (CAR.KIA_FORTE_2021_NON_SCC, 255), + (CAR.KIA_FORTE, 255), (CAR.HYUNDAI_KONA_NON_SCC, 270), (CAR.KIA_SELTOS_2023_NON_SCC, 384)) + for candidate, maximum in cases: + for request in (-1., -.5, .5, 1.): + with self.subTest(candidate=candidate, request=request): + cp = params(candidate, alpha_long=False) + state = CarState(cp) + state.out = state.update(state.get_can_parsers(cp)) + controller = CarController(DBC[candidate], cp) + parser = CANParser(DBC[candidate][Bus.pt], [('LKAS11', 100)], 0) + control = structs.CarControl() + control.latActive = True + control.actuators.torque = request + previous = 0 + for frame in range(180): + stamp = 1_000_000_000 + frame * 10_000_000 + _, sent = controller.update(control.as_reader(), state, stamp) + parser.update((stamp, sent)) + torque = parser.vl['LKAS11']['CR_Lkas_StrToqReq'] + self.assertLessEqual(abs(torque), maximum) + self.assertGreaterEqual(torque * request, 0) + self.assertLessEqual(abs(torque - previous), 3) + previous = torque + self.assertEqual(torque, round(request * maximum)) + self.assertTrue(parser.vl['LKAS11']['CF_Lkas_ActToi']) + control.latActive = False + _, sent = controller.update(control.as_reader(), state, stamp + 10_000_000) + parser.update((stamp + 10_000_000, sent)) + self.assertEqual(parser.vl['LKAS11']['CR_Lkas_StrToqReq'], 0) + self.assertFalse(parser.vl['LKAS11']['CF_Lkas_ActToi']) + + def test_all_ten_have_stock_longitudinal_ownership_and_lateral_only_controller(self): + for candidate in NON_SCC_CARS: + for alpha_long in (False, True): + with self.subTest(candidate=candidate, alpha_long=alpha_long): + cp = params(candidate, alpha_long=alpha_long) + self.assertTrue(cp.flags & HyundaiFlags.NON_SCC) + self.assertFalse(cp.alphaLongitudinalAvailable) + self.assertFalse(cp.openpilotLongitudinalControl) + self.assertTrue(cp.pcmCruise) + self.assertTrue(cp.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.NON_SCC) + self.assertFalse(cp.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.LONG) + state = CarState(cp) + parsers = state.get_can_parsers(cp) + state.out = state.update(parsers) + controller = CarController(DBC[cp.carFingerprint], cp) + control = structs.CarControl() + control.enabled = control.latActive = control.longActive = True + _, sent = controller.update(control.as_reader(), state, 1_000_000_000) + self.assertFalse({0x420, 0x421, 0x50A, 0x389, 0x38D, 0x483} & {addr for addr, _, _ in sent}) + + def test_fuel_architectures_decode_actual_cruise_frames(self): + families: tuple[tuple[str, dict[str, dict[str, float]]], ...] = ( + (CAR.KIA_FORTE_2021_NON_SCC, {"EMS16": {"CRUISE_LAMP_M": 1, "CRUISE_LAMP_S": 1}, + "LVR12": {"CF_Lvr_CruiseSet": 72}}), + (CAR.HYUNDAI_ELANTRA_HEV_2022_NON_SCC, + {"E_CRUISE_CONTROL": {"CRUISE_LAMP_M": 1, "CRUISE_LAMP_S": 1}, "ELECT_GEAR": {"SLC_SET_SPEED": 72}}), + (CAR.HYUNDAI_KONA_EV_NON_SCC, + {"LABEL11": {"CC_React": 1}, "EMS12": {"ACC_ACT": 1}, "E_EMS11": {"Cruise_Limit_Target": 72}}), + ) + for candidate, messages in families: + with self.subTest(candidate=candidate): + cp = params(candidate) + state = CarState(cp) + parsers = state.get_can_parsers(cp) + pt = parsers[Bus.pt] + names = set(get_non_scc_cruise_signals(cp.flags)[::2]) + self.assertTrue(names <= set(pt.vl)) + self.assertNotIn("SCC12", names) + packer = CANPacker(DBC[candidate][Bus.pt]) + frames = [packer.make_can_msg(name, 0, values) for name, values in messages.items()] + self.assertEqual(pt.update((1_000_000_000, frames)), {frame[0] for frame in frames}) + out = state.update(parsers) + self.assertTrue(out.cruiseState.available) + self.assertTrue(out.cruiseState.enabled) + self.assertAlmostEqual(out.cruiseState.speed, 20.0, places=4) + self.assertFalse(out.accFaulted) + self.assertFalse(out.cruiseState.standstill) + self.assertFalse(out.cruiseState.nonAdaptive) + + def test_fca_source_and_lda_require_observed_configuration(self): + fingerprint = gen_empty_fingerprint() + fingerprint[1][0x602] = 8 + fingerprint[0][0x391] = 8 + kona = params(CAR.HYUNDAI_KONA_NON_SCC, fingerprint) + self.assertTrue(kona.flags & HyundaiFlags.NON_SCC_RADAR_FCA) + self.assertFalse(kona.radarUnavailable) + self.assertTrue(kona.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.HAS_LDA_BUTTON) + parsers = CarState(kona).get_can_parsers(kona) + self.assertIn("FCA11", parsers[Bus.pt].vl) + self.assertNotIn("FCA11", parsers[Bus.cam].vl) + state = CarState(kona) + state.update(parsers) + self.assertIsNotNone(state.forte_lkas_sources) + self.assertNotIn("BCM_PO_11", parsers[Bus.pt].vl) + plain = params(CAR.HYUNDAI_KONA_NON_SCC) + self.assertTrue(plain.radarUnavailable) + self.assertFalse(plain.flags & HyundaiFlags.NON_SCC_RADAR_FCA) + self.assertFalse(plain.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.HAS_LDA_BUTTON) + camera = params(CAR.KIA_FORTE_2021_NON_SCC) + self.assertIn("FCA11", CarState(camera).get_can_parsers(camera)[Bus.cam].vl) + no_fca = params(CAR.KIA_FORTE_2019_NON_SCC) + self.assertNotIn("FCA11", CarState(no_fca).get_can_parsers(no_fca)[Bus.cam].vl) + + def test_exact_firmware_and_unidentified_manual_kona_ev(self): + self.assertNotIn(CAR.HYUNDAI_KONA_EV_NON_SCC, FW_VERSIONS) + for candidate in NON_SCC_CARS: + if candidate == CAR.HYUNDAI_KONA_EV_NON_SCC: + continue + with self.subTest(candidate=candidate): + fw = [CarParams.CarFw(ecu=ecu, address=addr, subAddress=sub or 0, fwVersion=versions[0], brand="hyundai") + for (ecu, addr, sub), versions in FW_VERSIONS[candidate].items()] + exact, matches = match_fw_to_car(fw, "", allow_exact=True, allow_fuzzy=False, log=False) + self.assertTrue(exact) + self.assertEqual(matches, {candidate}) + + # A shared partial ECU response cannot be promoted to either Kona sibling. + key = (CarParams.Ecu.eps, 0x7d4, None) + shared = set(FW_VERSIONS[CAR.HYUNDAI_KONA_NON_SCC][key]) & set(FW_VERSIONS[CAR.HYUNDAI_KONA][key]) + self.assertTrue(shared) + partial = [CarParams.CarFw(ecu=key[0], address=key[1], subAddress=0, fwVersion=next(iter(shared)), brand="hyundai")] + exact, matches = match_fw_to_car(partial, "", allow_exact=True, allow_fuzzy=False, log=False) + self.assertTrue(exact) + self.assertFalse(matches) + # Unknown and contradictory firmware must not falsely identify the new variant. + partial[0].fwVersion = b"unknown firmware" + self.assertNotIn(CAR.HYUNDAI_KONA_NON_SCC, + match_fw_to_car(partial, "", allow_exact=True, allow_fuzzy=False, log=False)[1]) + + def test_kona_receive_only_radar_tracks_and_freshness(self): + fingerprint = gen_empty_fingerprint() + fingerprint[1][0x602] = 8 + cp = params(CAR.HYUNDAI_KONA_NON_SCC, fingerprint) + radar = RadarInterface(cp) + parser = get_radar_can_parser(cp) + self.assertIn("RADAR_TRACK_602", parser.vl) + self.assertIn("RADAR_TRACK_611", parser.vl) + self.assertNotIn("RADAR_TRACK_500", parser.vl) + packer = CANPacker(DBC[cp.carFingerprint][Bus.radar]) + first = packer.make_can_msg("RADAR_TRACK_602", 1, {"1_DISTANCE": 25, "1_LATERAL": 1.5, "1_SPEED": -2, + "2_DISTANCE": 255.75}) + trigger = packer.make_can_msg("RADAR_TRACK_611", 1, {"1_DISTANCE": 255.75, "2_DISTANCE": 255.75}) + out = radar.update((1_000_000_000, [first, trigger])) + self.assertEqual(len(out.points), 1) + self.assertAlmostEqual(out.points[0].dRel, 25) + self.assertAlmostEqual(out.points[0].yRel, 1.5, delta=0.02) # 0.03 m DBC resolution + self.assertAlmostEqual(out.points[0].vRel, -2) + # A fresh trigger without a fresh 0x602 must withdraw the old point. + out = radar.update((1_050_000_000, [trigger])) + self.assertEqual(len(out.points), 0) + self.assertIsNone(radar.update((1_100_000_000, [first]))) + out = radar.update((1_200_000_000, [trigger])) + self.assertEqual(len(out.points), 0) # old source cannot revive at a delayed trigger diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_non_scc_startup.py b/opendbc_repo/opendbc/car/hyundai/tests/test_non_scc_startup.py new file mode 100644 index 0000000000..2946603c54 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_non_scc_startup.py @@ -0,0 +1,161 @@ +"""Forte receive topology and startup state.""" +import itertools +import unittest +from unittest.mock import patch + +from opendbc.can import CANPacker +from opendbc.car import Bus, gen_empty_fingerprint, structs +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.non_scc_aol import aol_word, qualified, AOL_EXPERIENCE +from opendbc.car.hyundai.values import CAR, DBC, HyundaiFlags +from opendbc.safety.tests.test_hyundai import checksum + + +FORTE_IDS = (CAR.KIA_FORTE_2019_NON_SCC, CAR.KIA_FORTE_2021_NON_SCC) +# Fixed classic source set, independent of the production parser's subscriptions. +PT_RATES = {'MDPS12': 50, 'TCS11': 100, 'TCS13': 50, 'TCS15': 10, 'CLU11': 50, + 'CLU15': 5, 'ESP12': 100, 'CGW1': 10, 'CGW2': 5, 'WHL_SPD11': 50, + 'SAS11': 100, 'EMS12': 100, 'EMS16': 100, 'LVR12': 100} + + +def params(identity, *, source=0x391, alpha=False, aol=False, fca=False, bsm=False): + fp = gen_empty_fingerprint() + if source: + fp[0][source] = 8 + if fca: + fp[2][0x38d] = 8 + if bsm: + fp[0][0x58b] = 8 + cp = CarInterface.get_params(identity, fp, [], alpha, False, False) + if aol: + cp.safetyConfigs[0].safetyParam = aol_word(cp) + cp.alternativeExperience = AOL_EXPERIENCE + return cp + + +class ForteReceiveStream: + def __init__(self, cp, *, source=0x391): + self.ci = CarInterface(cp) + self.source = source + self.packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + self.tick = 0 + self.now = 1_000_000_000 + self.gear = next(code for code, name in self.ci.CS.shifter_values.items() if name == 'D') + + def step(self, *, missing=None, wrong_bus=None, held=False, fca=True): + n = self.tick + self.tick += 1 + self.now += 10_000_000 + values = {'EMS16': {'CRUISE_LAMP_M': 1, 'CRUISE_LAMP_S': 1, 'AliveCounter': n % 4}, + 'WHL_SPD11': {'WHL_SPD_FL': 72, 'WHL_SPD_FR': 72, 'WHL_SPD_RL': 72, 'WHL_SPD_RR': 72, + 'WHL_SPD_AliveCounter_LSB': n % 4, 'WHL_SPD_AliveCounter_MSB': n // 4 % 4}, + 'TCS13': {'AliveCounterTCS': n % 8}, 'CLU11': {'CF_Clu_AliveCnt1': n % 16}, + 'CGW1': {'CF_Gway_DrvSeatBeltSw': 1}, 'LVR12': {'CF_Lvr_Gear': self.gear}} + frames = [] + sources = [(name, 0, rate) for name, rate in PT_RATES.items()] + sources += [('LKAS11', 2, 100)] + if self.source: + sources += [('BCM_PO_11' if self.source == 0x391 else 'CLU13', 0, 10)] + if fca and not self.ci.CP.flags & HyundaiFlags.NON_SCC_NO_FCA: + sources += [('FCA11', 2, 50)] + if self.ci.CP.deprecated.enableBsm: + sources += [('LCA11', 0, 20)] + for name, bus, rate in sources: + if name == missing or n and n * rate // 100 == (n - 1) * rate // 100: + continue + signal = {'BCM_PO_11': {'LDA_BTN': int(held)}, 'CLU13': {'CF_Clu_LdwsLkasSW': int(held)}, + 'LKAS11': {'CF_Lkas_MsgCount': n % 16}}.get(name, values.get(name, {})) + frame = self.packer.make_can_msg(name, bus, signal) + if name in ('EMS16', 'WHL_SPD11', 'TCS13'): + frame = checksum(frame) + if name == wrong_bus: + frame = (frame[0], frame[1], 2 if bus == 0 else 0) + frames.append(frame) + with patch('opendbc.car.hyundai.non_scc_aol.time.clock_gettime_ns', return_value=self.now): + return self.ci.update([(self.now, frames)]) + + def warm(self, **kwargs): + for _ in range(220): + state = self.step(**kwargs) + return state + + +class TestNonSccStartup(unittest.TestCase): + def test_complete_forte_final_profiles_and_lazy_requirements(self): + for identity, source, alpha, aol, bsm, fca in itertools.product(FORTE_IDS, (0x391, 0x50c), + (False, True), (False, True), (False, True), (False, True)): + with self.subTest(identity=identity, source=source, alpha=alpha, aol=aol, bsm=bsm, fca=fca): + cp = params(identity, source=source, alpha=alpha, aol=aol, bsm=bsm, fca=fca) + self.assertTrue(qualified(cp)) + self.assertTrue(cp.pcmCruise) + self.assertFalse(cp.openpilotLongitudinalControl or cp.alphaLongitudinalAvailable) + stream = ForteReceiveStream(cp, source=source) + state = stream.warm() + self.assertTrue(state.canValid) + self.assertFalse(state.canTimeout) + self.assertEqual(state.gearShifter, structs.CarState.GearShifter.drive) + self.assertFalse(state.doorOpen or state.seatbeltUnlatched) + self.assertTrue(state.cruiseState.available and state.cruiseState.enabled) + self.assertNotIn('SCC11', stream.ci.can_parsers[Bus.pt].vl) + self.assertNotIn('SCC12', stream.ci.can_parsers[Bus.pt].vl) + stream.warm(fca=False) + self.assertTrue(stream.step(fca=False).canValid) + + def test_all_ten_selected_cruise_sources_require_arrival_and_expire(self): + from opendbc.car.hyundai.tests.test_non_scc import NON_SCC_CARS + from opendbc.car.hyundai.carstate import get_non_scc_cruise_signals + for identity in NON_SCC_CARS: + cp = params(identity, source=0) + names = get_non_scc_cruise_signals(cp.flags, cp.carFingerprint)[::2] + for name in set(names): + with self.subTest(identity=identity, source=name): + ci = CarInterface(cp) + state = ci.update([]) + self.assertFalse(state.canValid, (identity, name, 'empty')) + parser = ci.can_parsers[Bus.pt] + message = parser.message_states[parser.dbc.name_to_msg[name].address] + self.assertFalse(message.ignore_alive, (identity, name)) + self.assertFalse(message.timestamps, (identity, name)) + self.assertFalse(message.valid(1_000_000_000, False), (identity, name, 'never received')) + packer = CANPacker(DBC[identity][Bus.pt]) + packet = packer.make_can_msg(name, 0, {}) + if name == 'EMS16': + packet = checksum(packet) + ci.update([(1_000_000_000, [packet])]) + self.assertTrue(message.timestamps, (identity, name, 'packed arrival')) + self.assertTrue(message.valid(1_000_000_000, False), (identity, name, 'arrived')) + expired = 1_000_000_000 + int(message.timeout_threshold) + 1 + self.assertFalse(message.valid(expired, False), (identity, name, 'expired')) + self.assertFalse(ci.update([(expired, [])]).canValid, (identity, name, 'incomplete stream')) + + def test_required_sources_loss_wrong_bus_and_recovery(self): + for identity, source, missing, mode in itertools.product(FORTE_IDS, (0x391, 0x50c), + ('MDPS12', 'WHL_SPD11', 'EMS16', 'TCS13', 'CLU11', 'LKAS11'), ('missing', 'wrong_bus')): + with self.subTest(identity=identity, source=source, missing=missing, mode=mode): + stream = ForteReceiveStream(params(identity, source=source, aol=True), source=source) + case = (identity, source, missing, mode) + self.assertTrue(stream.warm().canValid, case) + state = stream.warm(**{mode: missing}) + self.assertFalse(state.canValid, case) + self.assertTrue(stream.warm().canValid, case) + + def test_alternative_lda_source_expiry_needs_new_neutral_gesture(self): + for identity, source in itertools.product(FORTE_IDS, (0x391, 0x50c)): + with self.subTest(identity=identity, source=source): + stream = ForteReceiveStream(params(identity, source=source, aol=True), source=source) + self.assertTrue(stream.warm().canValid) + stream.warm(held=True) + self.assertTrue(stream.ci.CS.forte_lkas_sources.held) + name = 'BCM_PO_11' if source == 0x391 else 'CLU13' + self.assertTrue(stream.warm(missing=name).canValid) + self.assertFalse(stream.ci.CS.forte_lkas_sources.held) + for _ in range(220): + stream.step(held=True) + self.assertNotIn(True, stream.ci.CS.forte_lkas_sources.edges) + stream.warm(held=False) + presses = [] + for _ in range(220): + stream.step(held=True) + presses.extend(stream.ci.CS.forte_lkas_sources.edges) + self.assertIn(True, presses) + self.assertTrue(stream.ci.CS.forte_lkas_sources.held) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_palisade_2023.py b/opendbc_repo/opendbc/car/hyundai/tests/test_palisade_2023.py new file mode 100644 index 0000000000..a2eca95f62 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_palisade_2023.py @@ -0,0 +1,208 @@ +import unittest +from unittest.mock import patch + +from opendbc.can import CANPacker, CANParser +from opendbc.car import Bus, gen_empty_fingerprint, structs +from opendbc.car.hyundai.hyundaican import hyundai_checksum +from opendbc.car.hyundai.hyundaicanfd import CanBus +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.values import CAR, DBC, CarControllerParams, HyundaiFlags + + +def params(topology='hdai', *, adas=False, alpha=False, release=False): + fp = gen_empty_fingerprint() + if topology == 'hdaii': + fp[2][0x50] = 16 + elif topology == '110': + fp[2][0x110] = 32 + fw = [] + if adas: + item = structs.CarParams.CarFw() + item.ecu = structs.CarParams.Ecu.adas + fw.append(item) + return CarInterface.get_params(CAR.HYUNDAI_PALISADE_2023, fp, fw, alpha, release, False) + + +def feed(ci, tick, *, main=True, cruise=1, display=0, gas=False, brake=False, torque=0): + packer = CANPacker(DBC[ci.CP.carFingerprint][Bus.pt]) + messages = [] + for parser in ci.can_parsers.values(): + for address in parser.addresses: + message = parser.dbc.addr_to_msg[address] + values = {'COUNTER': tick % 16} if 'COUNTER' in message.sigs else {} + if message.name == 'SCC12': + values.update(MainMode_ACC=int(main), ACCMode=cruise, SCCInfoDisplay=display, VSetDis=90) + elif message.name == 'EMS16': + values['CF_Ems_AclAct'] = int(gas) + elif message.name == 'TCS13': + values['DriverOverride'] = 2 if brake else 0 + elif message.name == 'MDPS12': + values['CR_Mdps_StrColTq'] = torque + elif message.name == 'CGW1': + values['CF_Gway_DrvSeatBeltSw'] = 1 + elif message.name == 'ALERTS_364': + values.update(BYTE2=0x52, BYTE3=0x31, BYTE5=0x75, BYTE6=0xA6, BYTE7=0x17, DAW_Warning=1) + messages.append(packer.make_can_msg(message.name, parser.bus, values)) + return ci.update([(1_000_000_000 + tick * 10_000_000, messages)]) + + +class TestPalisade2023(unittest.TestCase): + def test_exact_topology_stock_cp_and_original_driver_limits(self): + for topology, adas, hdaii in [('hdai', False, False), ('110', False, False), ('hdaii', False, True), ('hdai', True, True)]: + for alpha in (False, True): + for release in (False, True): + cp = params(topology, adas=adas, alpha=alpha, release=release) + self.assertEqual(cp.safetyConfigs[-1].safetyParam, 0x2010 if hdaii else 0x2000) + self.assertEqual(cp.safetyConfigs[-1].safetyModel, structs.CarParams.SafetyModel.hyundai) + self.assertEqual(cp.steerControlType, structs.CarParams.SteerControlType.torque) + self.assertEqual(cp.alphaLongitudinalAvailable, not release) + self.assertFalse(cp.openpilotLongitudinalControl or cp.dashcamOnly) + self.assertTrue(cp.pcmCruise) + self.assertAlmostEqual(cp.stopAccel, -.85, places=6) + self.assertEqual((CanBus(cp).ECAN, CanBus(cp).ACAN, CanBus(cp).CAM), (1, 0, 2) if hdaii else (0, 1, 2)) + limits = CarControllerParams(cp) + self.assertEqual((limits.STEER_MAX, limits.STEER_DELTA_UP, limits.STEER_DELTA_DOWN, limits.STEER_DRIVER_ALLOWANCE), + (384, 3, 7, 50) if hdaii else (404, 2, 3, 50)) + ci = CarInterface(cp) + with patch('opendbc.car.hyundai.interface.disable_ecu') as disable: + ci.init(cp, list, lambda _: None) + disable.assert_not_called() + parser = ci.can_parsers[Bus.pt] + self.assertEqual(parser.bus, int(hdaii)) + self.assertEqual(parser.message_states[0x421].frequency, 50) + if hdaii: + self.assertEqual(ci.can_parsers[Bus.cam].message_states[0x2A4].frequency, 20) + + def test_actual_parser_stock_cruise_gas_brake_and_mixed_camera_sources(self): + for topology in ('hdai', 'hdaii'): + ci = CarInterface(params(topology)) + for tick in range(12): + result = feed(ci, tick, main=True, cruise=1, display=4, gas=True, brake=True, torque=175) + self.assertTrue(result.canValid) + self.assertTrue(result.cruiseState.available and result.cruiseState.enabled and result.cruiseState.standstill) + self.assertAlmostEqual(result.cruiseState.speed, 25., places=6) + self.assertTrue(result.gasPressed and result.brakePressed and result.steeringPressed) + self.assertFalse(result.stockAeb or result.stockFcw) + result = feed(ci, 12, main=False, cruise=0, display=2) + self.assertFalse(result.cruiseState.available or result.cruiseState.enabled or result.cruiseState.standstill) + self.assertTrue(result.cruiseState.nonAdaptive) + self.assertFalse(result.gasPressed or result.brakePressed) + self.assertEqual(bool(ci.CS.lkas11), topology == 'hdai') + + def test_actual_controller_stock_frames_crc_opaque_alerts_and_no_longitudinal_tx(self): + for topology in ('hdai', 'hdaii'): + ci = CarInterface(params(topology)) + for tick in range(12): + feed(ci, tick) + command = structs.CarControl() + command.enabled = command.latActive = True + command.actuators.torque = 1. + command.hudControl.setSpeed = 25. + previous = 0 + for tick in range(20): + actuators, messages = ci.apply(command.as_reader(), 1_200_000_000 + tick * 10_000_000) + self.assertEqual(actuators.torqueOutputCan - previous, 3 if topology == 'hdaii' else 2) + previous = actuators.torqueOutputCan + addresses = {a for a, _, _ in messages} + self.assertFalse(addresses & {0x420, 0x421, 0x389, 0x7D0, 0x730}) + if topology == 'hdaii': + self.assertEqual(addresses, {0x50, 0x2A4} if tick % 5 == 0 else {0x50}) + self.assertTrue(all(bus == 0 for _, _, bus in messages)) + else: + self.assertEqual(addresses, {0x340, 0x364}) + for _, data, bus in messages: + self.assertEqual(bus, 0) + self.assertEqual(data[0], hyundai_checksum(data[1:8])) + alert = next(m for m in messages if m[0] == 0x364) + decoder = CANParser(DBC[ci.CP.carFingerprint][Bus.pt], [('ALERTS_364', 0)], 0) + decoder.update([(1_200_000_000 + tick * 10_000_000, [alert])]) + self.assertEqual([decoder.vl['ALERTS_364'][k] for k in ('BYTE2', 'BYTE3', 'BYTE5', 'BYTE6', 'BYTE7')], + [0x52, 0x31, 0x75, 0xA6, 0x17]) + self.assertEqual(decoder.vl['ALERTS_364']['DAW_Warning'], 0) + command.latActive = False + actuators, _ = ci.apply(command.as_reader(), 1_500_000_000) + self.assertEqual(actuators.torqueOutputCan, 0) + + def test_cancel_resume_route_to_exact_pt_bus_and_sibling_params_unchanged(self): + for topology in ('hdai', 'hdaii'): + ci = CarInterface(params(topology)) + for tick in range(12): + feed(ci, tick) + command = structs.CarControl() + command.cruiseControl.cancel = True + for tick in range(11): + _, messages = ci.apply(command.as_reader(), 1_200_000_000 + tick * 10_000_000) + buttons = [m for m in messages if m[0] == 0x4F1] + self.assertEqual(len(buttons), 1) + self.assertEqual(buttons[0][2], int(topology == 'hdaii')) + command.cruiseControl.cancel = False + command.cruiseControl.resume = True + ci.CC.frame = 20 + _, messages = ci.apply(command.as_reader(), 1_500_000_000) + buttons = [m for m in messages if m[0] == 0x4F1] + self.assertEqual(len(buttons), 25) + self.assertTrue(all(m[2] == int(topology == 'hdaii') for m in buttons)) + cp = CarInterface.get_params(CAR.HYUNDAI_PALISADE, gen_empty_fingerprint(), [], False, False, False) + self.assertEqual(cp.safetyConfigs[-1].safetyParam, 0) + self.assertFalse(cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG) + self.assertEqual(DBC[cp.carFingerprint][Bus.pt], 'hyundai_can_generated') + + def test_required_source_loss_wrong_bus_and_optional_blended_lfa_payload(self): + for topology in ('hdai', 'hdaii'): + cp = params(topology) + cp.flags |= HyundaiFlags.SEND_LFA.value + ci = CarInterface(cp) + for tick in range(12): + result = feed(ci, tick) + self.assertTrue(result.canValid) + command = structs.CarControl() + command.latActive = True + command.hudControl.setSpeed = 25. + _, messages = ci.apply(command.as_reader(), 1_200_000_000) + lfa = next(m for m in messages if m[0] == 0x485) + self.assertEqual(lfa[2], int(topology == 'hdaii')) + self.assertEqual(lfa[1][0], hyundai_checksum(lfa[1][1:8])) + decoder = CANParser(DBC[cp.carFingerprint][Bus.pt], [('LFAHDA_MFC', 0)], lfa[2]) + decoder.update([(1_200_000_000, [lfa])]) + self.assertEqual(decoder.vl['LFAHDA_MFC']['LFA_Icon_State'], 2) + pt = ci.can_parsers[Bus.pt] + old = dict(pt.vl['SCC12']) + bad_bus = 0 if topology == 'hdaii' else 1 + frame = CANPacker(DBC[cp.carFingerprint][Bus.pt]).make_can_msg('SCC12', bad_bus, {'MainMode_ACC': 0}) + # Required sources have timed out; parser validity debounces five invalid updates. + for invalid_tick in range(5): + result = ci.update([(2_000_000_000 + invalid_tick * 10_000_000, [frame])]) + self.assertEqual(result.canValid, invalid_tick < 4) + self.assertFalse(result.canValid) + self.assertEqual(dict(pt.vl['SCC12']), old) + + def test_actual_mixed_topology_metadata_correct_bus_and_wrong_bus(self): + for hdaii in (False, True): + for observation_bus in (0, 1, 2): + with self.subTest(hdaii=hdaii, observation_bus=observation_bus): + fingerprint = gen_empty_fingerprint() + if hdaii: + fingerprint[2][0x50] = 16 + fingerprint[observation_bus][0x38D] = 8 + fingerprint[observation_bus][0x58B] = 8 + stock = CarInterface.get_params(CAR.HYUNDAI_PALISADE_2023, fingerprint, [], False, False, False) + expected_fca = observation_bus in (int(hdaii), 2) + self.assertEqual(bool(stock.flags & HyundaiFlags.USE_FCA), expected_fca) + self.assertEqual(stock.deprecated.enableBsm, observation_bus == int(hdaii)) + + def test_mixed_standstill_metadata_retains_original_bus_zero(self): + for hdaii in (False, True): + for observation_bus in (0, 1): + with self.subTest(hdaii=hdaii, observation_bus=observation_bus): + fingerprint = gen_empty_fingerprint() + if hdaii: + fingerprint[2][0x50] = 16 + fingerprint[observation_bus][0x2AA] = 8 + stock = CarInterface.get_params(CAR.HYUNDAI_PALISADE_2023, fingerprint, [], False, False, False) + self.assertEqual(stock.steerAtStandstill, observation_bus == 0) + # Exercise the reached flag clear with a real mutable CP, not a fake field bag. + stock.flags |= HyundaiFlags.MIN_STEER_32_MPH.value + stock.minSteerSpeed = 10.0 + CarInterface._get_params(stock, CAR.HYUNDAI_PALISADE_2023, fingerprint, [], False, False, False) + self.assertEqual(bool(stock.flags & HyundaiFlags.MIN_STEER_32_MPH), observation_bus != 0) + self.assertEqual(stock.minSteerSpeed, 0.0 if observation_bus == 0 else 10.0) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_palisade_blended_radar.py b/opendbc_repo/opendbc/car/hyundai/tests/test_palisade_blended_radar.py new file mode 100644 index 0000000000..cf81323f8e --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_palisade_blended_radar.py @@ -0,0 +1,110 @@ +"""Mixed Palisade radar qualifies through the actual factory/parser/ownership transaction.""" + +import pytest +from opendbc.can import CANPacker +from opendbc.car import Bus, gen_empty_fingerprint +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.radar_interface import RadarInterface, get_radar_can_parser, radar_dbc +from opendbc.car.hyundai.hyundaicanfd import CanBus +from opendbc.car.hyundai.values import CAR, DBC, HYUNDAI_GV70_RADAR_DBC +from opendbc.car.hyundai.blended_longitudinal import candidate_from_stock, BlendedLongitudinalOwner, Outcome, DISABLE, ENABLE + + +def params(bank=None, lka=True, bus=0): + fp = gen_empty_fingerprint() + if lka: + fp[2][0x50] = 32 + if bank is not None: + fp[bus].update(bank) + return CarInterface.get_params(CAR.HYUNDAI_PALISADE_2023, fp, [], False, False, False) + + +def test_exact_full_bank_factory_availability_does_not_grant_long_control(): + full = dict.fromkeys(range(0x210, 0x220), 32) + absent = params() + stock = params(full) + assert absent.radarUnavailable and not stock.radarUnavailable + assert stock.flags == absent.flags + assert [(r.safetyModel, r.safetyParam) for r in stock.safetyConfigs] == [(r.safetyModel, r.safetyParam) for r in absent.safetyConfigs] + assert not stock.openpilotLongitudinalControl and not absent.openpilotLongitudinalControl + assert stock.pcmCruise and absent.pcmCruise + assert radar_dbc(stock) == HYUNDAI_GV70_RADAR_DBC + assert Bus.radar not in DBC[CAR.HYUNDAI_PALISADE_2023] # Static siblings/config untouched. + for addr in full: + missing = full.copy() + missing.pop(addr) + assert params(missing).radarUnavailable + short = full.copy() + short[addr] = 24 + assert params(short).radarUnavailable + assert params(full, bus=1).radarUnavailable + assert params(full, lka=False).radarUnavailable + + +def frames(): + packer = CANPacker(HYUNDAI_GV70_RADAR_DBC) + result = [] + for ordinal, addr in enumerate(range(0x210, 0x220)): + values = {} + for slot in (1, 2): + count = ordinal * 2 + slot + values.update({f"{slot}_STATE": 3, f"{slot}_LONG_DIST": count, f"{slot}_LAT_DIST": -0.05 * count, f"{slot}_REL_SPEED": -0.01 * count}) + result.append(packer.make_can_msg(f"RADAR_TRACK_{addr:x}", 0, values)) + return result + + +def test_actual_parser_full_32_slots_exact_wire_and_fresh_source(): + stock = params(dict.fromkeys(range(0x210, 0x220), 32)) + radar = RadarInterface(stock) + assert (radar.start_addr, radar.msg_count, radar.rcp.bus) == (0x210, 16, 0) + packed = frames() + assert all(len(data) == 32 and bus == 0 for _, data, bus in packed) + result = radar.update((1_000_000_000, packed)) + assert result is not None and not result.errors.canError + assert len(result.points) == len({p.trackId for p in result.points}) == 32 + for index, point in enumerate(result.points, 1): + assert point.dRel == pytest.approx(index, abs=0.051) + assert point.yRel == pytest.approx(-0.05 * index, abs=0.026) + assert point.vRel == pytest.approx(-0.01 * index, abs=0.051) + # One trigger-only frame cannot resurrect any missing older slot after50ms. + result = radar.update((1_100_000_000, [packed[-1]])) + assert all(point.dRel >= 31.0 for point in result.points) + wrong_bus = [(addr, data, 1) for addr, data, _ in packed] + fresh = RadarInterface(stock) + assert fresh.update((1_000_000_000, wrong_bus)) is None + short = [(addr, data[:-1], bus) for addr, data, bus in packed] + fresh = RadarInterface(stock) + assert fresh.update((1_000_000_000, short)) is None + + +def test_current_owner_requires_native_admission_and_ack_before_candidate_publication(): + stock = params(dict.fromkeys(range(0x210, 0x220), 32)) + assert candidate_from_stock(stock, alpha_requested=True, native_qualified=False) is None + assert candidate_from_stock(stock, alpha_requested=False, native_qualified=True) is None + candidate = candidate_from_stock(stock, alpha_requested=True, native_qualified=True) + assert candidate is not None and not candidate.radarUnavailable + assert candidate.openpilotLongitudinalControl and not candidate.pcmCruise + calls = [] + + def exchange(*args, **kwargs): + calls.append(kwargs["com_cont_req"]) + return True + + def restore(*args, **kwargs): + calls.append(ENABLE) + return True + + owner = BlendedLongitudinalOwner(candidate, CanBus(candidate), exchange, restore_exchange=restore) + with pytest.raises(RuntimeError): + owner.begin(None, None, unpublished=False, admission=True) + assert calls == [] + result = owner.begin(None, None, unpublished=True, admission=True) + assert result.outcome == Outcome.OWNED and owner.active() and calls == [DISABLE] + assert owner.restore(None, None) + assert not owner.active() and calls == [DISABLE, ENABLE] + + +def test_missing_radar_remains_unavailable_in_admitted_candidate(): + candidate = candidate_from_stock(params(), alpha_requested=True, native_qualified=True) + assert candidate is not None and candidate.radarUnavailable + assert get_radar_can_parser(candidate) is not None diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_ray_pedal.py b/opendbc_repo/opendbc/car/hyundai/tests/test_ray_pedal.py new file mode 100644 index 0000000000..7a4435cc8e --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_ray_pedal.py @@ -0,0 +1,171 @@ +import unittest + +from opendbc.can import CANPacker, CANParser +from opendbc.can.dbc import DBC as CANDBC, SignalType +from opendbc.car import Bus, gen_empty_fingerprint, structs +from opendbc.car.hyundai.carcontroller import CarController +from opendbc.car.hyundai.carstate import CarState +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.ray_pedal import create_ray_pedal_command, ray_pedal_gas, ray_pedal_enabled +from opendbc.car.hyundai.values import CAR, DBC, HyundaiSafetyFlags + + +def ray_fingerprint(sensor=6, lfa=8): + fp = gen_empty_fingerprint() + fp[0].update({0x201: sensor, 0x391: 8}) + fp[2][0x485] = lfa + return fp + + +def ray_controller(): + cp = CarInterface.get_params(CAR.KIA_RAY_EV, ray_fingerprint(), [], False, False, False) + controller = CarController(DBC[cp.carFingerprint], cp) + state = CarState(cp) + state.out = structs.CarState() + state.out.vEgo = state.out.vEgoRaw = 12 + state.out.gearShifter = structs.CarState.GearShifter.drive + state.ray_pedal_valid = True + state.ray_pedal_state = 0 + parser = CANParser(DBC[cp.carFingerprint][Bus.pt], [('LKAS11', 0), ('CLU11', 0)], 0) + state.lkas11, state.clu11 = parser.vl['LKAS11'], parser.vl['CLU11'] + command = structs.CarControl() + command.enabled = command.latActive = command.longActive = True + command.hudControl.setSpeed = 20 + command.actuators.accel = 1.5 + return cp, controller, command, state + + +class TestRayPedal(unittest.TestCase): + def test_actual_cp_signature_is_unique_and_requires_exact_sensor_layout(self): + signature = int(HyundaiSafetyFlags.EV_GAS | HyundaiSafetyFlags.LONG | HyundaiSafetyFlags.NON_SCC | + HyundaiSafetyFlags.HAS_LDA_BUTTON | HyundaiSafetyFlags.CAN_REFRESH_MSGS) + self.assertEqual(signature, 0x9805) + for car in CAR: + for alpha in (False, True): + cp = CarInterface.get_params(car, ray_fingerprint(), [], alpha, False, False) + self.assertEqual(ray_pedal_enabled(cp), car == CAR.KIA_RAY_EV) + self.assertEqual(cp.safetyConfigs[-1].safetyParam == signature, car == CAR.KIA_RAY_EV) + for fp in (ray_fingerprint(sensor=8), ray_fingerprint(lfa=4)): + cp = CarInterface.get_params(CAR.KIA_RAY_EV, fp, [], False, False, False) + self.assertFalse(ray_pedal_enabled(cp)) + self.assertFalse(cp.openpilotLongitudinalControl) + self.assertTrue(cp.pcmCruise) + cp, _, _, _ = ray_controller() + self.assertTrue(cp.openpilotLongitudinalControl) + self.assertFalse(cp.pcmCruise) + self.assertFalse(cp.autoResumeSng) + self.assertEqual(cp.minEnableSpeed, -1) + + def test_sensor_real_route_crc_fault_and_physical_override(self): + cp, _, _, state = ray_controller() + parsers = state.get_can_parsers(cp) + state.update(parsers) + samples = ['01f403d55de8', '01f603d55ef1', '01f403d55f51', '01f603d3503f', '01f903d551ab', '01f903d552a4', '01f703d55370'] + for index, sample in enumerate(samples): + frame = (0x201, bytes.fromhex(sample), 0) + parsers[Bus.party].update([(1_000_000_000 + index * 20_000_000, [frame])]) + result = state.update(parsers) + self.assertTrue(state.ray_pedal_valid) + self.assertEqual(state.ray_pedal_state, 5) + self.assertTrue(result.accFaulted) + prior = dict(parsers[Bus.party].vl['GAS_SENSOR']) + bad = bytearray(bytes.fromhex(samples[-1])) + bad[-1] ^= 1 + parsers[Bus.party].update([(1_160_000_000, [(0x201, bytes(bad), 0)])]) + self.assertEqual(dict(parsers[Bus.party].vl['GAS_SENSOR']), prior) + packer = CANPacker('hyundai_kia_ray_pedal') + for counter, tracks, pressed in ((4, (0, 0), False), (5, ((310-264)*.672, (593-497)*.332), True)): + sensor = packer.make_can_msg('GAS_SENSOR', 0, {'INTERCEPTOR_GAS': tracks[0], 'INTERCEPTOR_GAS2': tracks[1], + 'STATE': 0, 'COUNTER_PEDAL': counter}) + parsers[Bus.party].update([(1_200_000_000 + counter * 20_000_000, [sensor])]) + result = state.update(parsers) + self.assertFalse(result.accFaulted) + self.assertEqual(result.gasPressed, pressed) + camera = CANPacker(DBC[cp.carFingerprint][Bus.pt]).make_can_msg('LABEL11', 0, {'CC_React': 1, 'CC_Engaged': 1}) + parsers[Bus.pt].update([(1_500_000_000, [camera])]) + result = state.update(parsers) + self.assertTrue(result.cruiseState.enabled) + self.assertTrue(result.cruiseState.available) + + def test_checksum_extension_is_scoped_to_ray_dbc(self): + for name in ('honda_accord_2017_can_ext_generated', 'toyota_nodsu_pt_generated'): + db = CANDBC(name) + for msg in db.name_to_msg.values(): + for sig in msg.sigs.values(): + if sig.name in ('CHECKSUM_PEDAL', 'COUNTER_PEDAL'): + self.assertEqual(sig.type, SignalType.DEFAULT) + self.assertIsNone(sig.calc_checksum) + db = CANDBC('hyundai_kia_ray_pedal') + self.assertEqual(db.name_to_msg['GAS_SENSOR'].sigs['CHECKSUM_PEDAL'].type, SignalType.RAY_PEDAL_CHECKSUM) + packet = create_ray_pedal_command(CANPacker('hyundai_kia_ray_pedal'), 0, 15) + self.assertEqual(packet[1][:4], bytes(4)) + self.assertEqual(packet[1][4] & 15, 15) + + def test_controller_cadence_override_cancellation_and_no_scc_commands(self): + cp, controller, command, state = ray_controller() + + def messages(frame): + controller.frame = frame + return controller.update(command.as_reader(), state, frame * 10_000_000)[1] + first = messages(0) + self.assertFalse(any(frame[0] == 0x340 for frame in first)) + self.assertTrue(any(frame[0] == 0x485 and len(frame[1]) == 8 for frame in first)) + self.assertFalse(any(frame[0] in (0x420, 0x421, 0x50A, 0x389, 0x7D0) for frame in first)) + self.assertTrue(any(frame[0] == 0x340 for frame in messages(1))) + self.assertFalse(any(frame[0] == 0x200 for frame in messages(2))) + for frame in range(4, 160, 4): + pedal = next(m for m in messages(frame) if m[0] == 0x200) + self.assertEqual(pedal[1][4] & 15, frame // 4 & 15) + self.assertAlmostEqual(controller._ray_pedal_gas_last, .55) + for cause in ('brake', 'gas', 'override', 'inactive', 'invalid', 'fault'): + with self.subTest(cause=cause): + cp, controller, command, state = ray_controller() + if cause == 'brake': + state.out.brakePressed = True + elif cause == 'gas': + state.out.gasPressed = True + elif cause == 'override': + command.cruiseControl.override = True + elif cause == 'inactive': + command.longActive = False + elif cause == 'invalid': + state.ray_pedal_valid = False + else: + state.ray_pedal_state = 5 + pedal = next(m for m in messages(20) if m[0] == 0x200) + self.assertEqual(pedal[1][:4], bytes(4)) + self.assertFalse(pedal[1][4] & 0x80) + state.out.cruiseState.enabled = True + state.out.gasPressed = True + msgs = messages(24) + self.assertTrue(any(m[0] == 0x4F1 and m[1][0] & 7 == 4 for m in msgs)) + self.assertFalse(any(m[0] == 0x4F1 for m in messages(25))) + + def test_source_ray_dashboard_icons_active_and_disengaging(self): + cp, controller, command, state = ray_controller() + parser = CANParser('hyundai_kia_ray_lfa', [('LFAHDA_MFC', 0)], 0) + lkas = CANParser(DBC[cp.carFingerprint][Bus.pt], [('LKAS11', 0)], 0) + for frame in range(106): + if frame == 5: + command.enabled = command.latActive = command.longActive = False + sends = controller.update(command.as_reader(), state, frame*10_000_000)[1] + parser.update([(frame+1, sends)]) + lkas.update([(frame+1, sends)]) + if frame in (0, 5, 100, 105): + expected = 2 if frame == 0 else 3 if frame < 104 else 0 + self.assertEqual(parser.vl['LFAHDA_MFC']['LFA_Icon_State'], expected) + if frame: + self.assertEqual(lkas.vl['LKAS11']['CF_Lkas_FcwOpt_USM'], expected or 1) + + def test_exact_latest_taper_rate_and_regen_behavior(self): + gas = 0 + for _ in range(40): + gas = ray_pedal_gas(gas, 12, 1.5, 20) + self.assertEqual(gas, .55) + gas = ray_pedal_gas(gas, 12, 1.5, 12) + self.assertAlmostEqual(gas, .55 * .65) + gas = ray_pedal_gas(gas, 12, 1.5, 11.8) + self.assertAlmostEqual(gas, .55 * .65 * .6) + for speed in (11.4, float('nan'), .9, 40.1): + self.assertEqual(ray_pedal_gas(gas, 12, 1.5, speed), 0) + self.assertEqual(ray_pedal_gas(.55, 12, -1.5, 20), 0) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_ray_pedal_commands.py b/opendbc_repo/opendbc/car/hyundai/tests/test_ray_pedal_commands.py new file mode 100644 index 0000000000..3ac2bc63de --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_ray_pedal_commands.py @@ -0,0 +1,52 @@ +"""Private qualification: execute only after root assembles source and native candidate.""" + +import math +import pytest +from opendbc.car.hyundai.ray_pedal import get_ray_pedal_target, ray_pedal_gas, pedal_checksum +from opendbc.car.hyundai.tests.test_ray_pedal import ray_controller +from opendbc.safety.tests.test_hyundai_ray_pedal import TestHyundaiRayPedal + + +@pytest.mark.parametrize("speed,cap", [(0, 0.55), (12, 0.55), (20, 0.65), (25, 0.70), (35, 0.70)]) +def test_speed_cap_and_slew(speed, cap): + assert get_ray_pedal_target(10, speed, min(speed + 2, 40)) == pytest.approx(cap) + assert ray_pedal_gas(0, speed, 10, min(speed + 2, 40)) == pytest.approx(0.02) + + +@pytest.mark.parametrize("field", ["accel", "speed", "set_speed"]) +@pytest.mark.parametrize("bad", [math.nan, math.inf, -math.inf]) +def test_nonfinite_target_is_zero(field, bad): + values = dict(accel=0.5, speed=20, set_speed=25) + values[field] = bad + assert get_ray_pedal_target(**values) == 0 + + +@pytest.mark.parametrize("gate", ["gasPressed", "brakePressed", "override", "longActive", "fault", "invalid"]) +def test_actual_controller_disabled_frames_on_gates(gate): + cp, controller, cc, cs = ray_controller() + controller._ray_pedal_gas_last = 0.7 + if gate in ("gasPressed", "brakePressed"): + setattr(cs.out, gate, True) + elif gate == "override": + cc.cruiseControl.override = True + elif gate == "longActive": + cc.longActive = False + elif gate == "fault": + cs.ray_pedal_state = 1 + else: + cs.ray_pedal_valid = False + controller.frame = 4 + _, frames = controller.update(cc.as_reader(), cs, 1_000_000_000) + data = next(f[1] for f in frames if f[0] == 0x200) + assert data[:4] == bytes(4) + assert data[4] & 0x80 == 0 + assert controller._ray_pedal_gas_last == 0 + + +class TestNativeWafflesBoundaries(TestHyundaiRayPedal): + def test_each_raw_track_boundary(self): + self.arm() + for a, b, allowed in [(530, 1035, True), (531, 1035, False), (530, 1036, False), (263, 497, False), (264, 496, False), (530, 980, False)]: + data = bytearray(a.to_bytes(2, "big") + b.to_bytes(2, "big") + bytes([0x81, 0])) + data[-1] = pedal_checksum(0, None, data) + self.assertEqual(self.safety.safety_tx_hook(self.packet((0x200, bytes(data), 0))), allowed) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_stinger_hud.py b/opendbc_repo/opendbc/car/hyundai/tests/test_stinger_hud.py new file mode 100644 index 0000000000..1372c424e0 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_stinger_hud.py @@ -0,0 +1,68 @@ +import itertools +import unittest + +from opendbc.can import CANParser +from opendbc.car import Bus, gen_empty_fingerprint, structs +from opendbc.car.hyundai.carcontroller import CarController +from opendbc.car.hyundai.carstate import CarState +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.values import CAR, DBC + + +def controller(car, alpha): + cp = CarInterface.get_params(car, gen_empty_fingerprint(), [], alpha, False, False) + cc = CarController(DBC[cp.carFingerprint], cp) + cs = CarState(cp) + cs.out = structs.CarState.new_message(vEgo=15., vEgoRaw=15.) + cs.out.gearShifter = structs.CarState.GearShifter.drive + parser = CANParser(DBC[cp.carFingerprint][Bus.pt], [('LKAS11', 0), ('CLU11', 0)], 0) + cs.lkas11, cs.clu11 = parser.vl['LKAS11'], parser.vl['CLU11'] + return cc, cs, parser + + +class TestStingerHud(unittest.TestCase): + def test_lkas_cluster_status_matches_active_lateral(self): + for car, alpha, enabled, lateral in itertools.product( + (CAR.KIA_STINGER_2022, CAR.KIA_STINGER, CAR.HYUNDAI_SONATA), (False, True), (False, True), (False, True)): + with self.subTest(car=car, alpha=alpha, enabled=enabled, lateral=lateral): + cc, cs, parser = controller(car, alpha) + command = structs.CarControl.new_message(enabled=enabled, latActive=lateral) + command.hudControl.leftLaneVisible = command.hudControl.rightLaneVisible = True + command.hudControl.setSpeed = 25. + command.actuators.torque = .3 + for tick in range(2): + _, frames = cc.update(command.as_reader(), cs, (tick + 1) * 10_000_000) + parser.update([((tick + 1) * 10_000_000, frames)]) + hud_active = enabled or (car == CAR.KIA_STINGER_2022 and lateral) + values = parser.vl['LKAS11'] + self.assertEqual(values['CF_Lkas_LdwsSysState'], 3 if hud_active else 4) + if car != CAR.KIA_STINGER: + self.assertEqual(values['CF_Lkas_FcwOpt_USM'], 2 if hud_active else 1) + self.assertEqual(values['CF_Lkas_ActToi'], int(lateral)) + self.assertEqual(values['CF_Lkas_ToiFlt'], 0) + self.assertEqual(values['CF_Lkas_MsgCount'], 1) + if not lateral: + self.assertEqual(values['CR_Lkas_StrToqReq'], 0) + + def test_lateral_hud_keeps_lane_warnings_and_driver_alerts(self): + for left, right, alert in itertools.product((False, True), (False, True), + (structs.CarControl.HUDControl.VisualAlert.none, + structs.CarControl.HUDControl.VisualAlert.steerRequired)): + cc, cs, parser = controller(CAR.KIA_STINGER_2022, False) + command = structs.CarControl.new_message(enabled=False, latActive=True) + command.hudControl.leftLaneVisible, command.hudControl.rightLaneVisible = left, right + command.hudControl.leftLaneDepart, command.hudControl.rightLaneDepart = True, True + command.hudControl.visualAlert = alert + _, frames = cc.update(command.as_reader(), cs, 10_000_000) + parser.update([(10_000_000, frames)]) + values = parser.vl['LKAS11'] + warning = alert == structs.CarControl.HUDControl.VisualAlert.steerRequired + self.assertEqual(values['CF_Lkas_LdwsSysState'], 3 if (left and right) or warning else 5 if left else 6 if right else 1) + self.assertEqual(values['CF_Lkas_SysWarning'], 4 if warning else 0) + self.assertEqual(values['CF_Lkas_LdwsLHWarning'], 2) + self.assertEqual(values['CF_Lkas_LdwsRHWarning'], 2) + self.assertEqual(values['CF_Lkas_LdwsActivemode'], int(left) + 2 * int(right)) + + +if __name__ == '__main__': + unittest.main() diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_xceed_phev.py b/opendbc_repo/opendbc/car/hyundai/tests/test_xceed_phev.py new file mode 100644 index 0000000000..64cd64d480 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_xceed_phev.py @@ -0,0 +1,74 @@ +import unittest + +from opendbc.can import CANParser +from opendbc.car import Bus, gen_empty_fingerprint, structs +from opendbc.car.fw_versions import match_fw_to_car +from opendbc.car.hyundai.carcontroller import CarController +from opendbc.car.hyundai.carstate import CarState +from opendbc.car.hyundai.fingerprints import FW_VERSIONS +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.radar_interface import RADAR_START_ADDR, get_radar_can_parser +from opendbc.car.hyundai.values import CAR, DBC, HyundaiFlags, HyundaiSafetyFlags +from opendbc.safety.tests import common +from opendbc.safety.tests.libsafety import libsafety_py + + +class TestXceedPhevPort(unittest.TestCase): + @staticmethod + def params(alpha_long=False, *, release=False): + fingerprint = gen_empty_fingerprint() + fingerprint[1][RADAR_START_ADDR] = 8 + return CarInterface.get_params(CAR.KIA_XCEED_PHEV, fingerprint, [], alpha_long, release, False) + + def test_saved_firmware_and_stock_legacy_hybrid_configuration(self): + versions = FW_VERSIONS[CAR.KIA_XCEED_PHEV] + self.assertEqual(len(versions), 3) + observed = [structs.CarParams.CarFw(ecu=ecu, address=addr, subAddress=0, + fwVersion=values[0], brand='hyundai') + for (ecu, addr, _), values in versions.items()] + exact, matches = match_fw_to_car(observed, '', allow_exact=True, allow_fuzzy=False, log=False) + self.assertTrue(exact) + self.assertEqual(matches, {CAR.KIA_XCEED_PHEV, CAR.KIA_CEED_PHEV}) + for alpha_long, release in ((False, False), (True, False), (False, True), (True, True)): + cp = self.params(alpha_long, release=release) + self.assertTrue(cp.flags & HyundaiFlags.LEGACY) + self.assertTrue(cp.flags & HyundaiFlags.HYBRID) + self.assertTrue(cp.flags & HyundaiFlags.MANDO_RADAR) + self.assertEqual(cp.alphaLongitudinalAvailable, not release) + self.assertFalse(cp.openpilotLongitudinalControl) + self.assertTrue(cp.pcmCruise) + self.assertFalse(cp.radarUnavailable) + self.assertEqual(cp.safetyConfigs[-1].safetyModel, structs.CarParams.SafetyModel.hyundaiLegacy) + self.assertTrue(cp.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.HYBRID_GAS) + self.assertFalse(cp.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.LONG) + self.assertEqual(DBC[CAR.KIA_XCEED_PHEV][Bus.radar], 'hyundai_kia_mando_front_radar_generated') + self.assertEqual(get_radar_can_parser(cp).bus, 1) + + def test_actual_controller_and_native_safety_keep_scc_with_stock(self): + cp = self.params() + state = CarState(cp) + parsers = state.get_can_parsers(cp) + state.update(parsers) + control = structs.CarControl() + control.enabled = True + control.latActive = True + control.longActive = False + controller = CarController(DBC[CAR.KIA_XCEED_PHEV], cp) + _, sends = controller.update(control.as_reader(), state, 1_000_000_000) + self.assertIn(0x340, [addr for addr, _, _ in sends]) + self.assertNotIn(0x420, [addr for addr, _, _ in sends]) + self.assertNotIn(0x421, [addr for addr, _, _ in sends]) + + safety = libsafety_py.libsafety + safety.set_safety_hooks(int(cp.safetyConfigs[-1].safetyModel.raw), cp.safetyConfigs[-1].safetyParam) + safety.init_tests() + self.assertFalse(safety.safety_tx_hook(common.make_msg(0, 0x420, 8))) + lkas = next(frame for frame in sends if frame[0] == 0x340) + self.assertTrue(safety.safety_tx_hook(libsafety_py.make_CANPacket(lkas[0], lkas[2], lkas[1]))) + parser = CANParser(DBC[CAR.KIA_XCEED_PHEV][Bus.pt], [('LKAS11', 0)], 0) + parser.update([(1_000_000_000, sends)]) + self.assertEqual(parser.vl['LKAS11']['CR_Lkas_StrToqReq'], 0) + + +if __name__ == '__main__': + unittest.main() diff --git a/opendbc_repo/opendbc/car/hyundai/tests/testdata/README.md b/opendbc_repo/opendbc/car/hyundai/tests/testdata/README.md new file mode 100644 index 0000000000..6229362dcb --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/testdata/README.md @@ -0,0 +1,22 @@ +# Ioniq 6 reference fixtures + +These synthetic fixtures check source behavior and CAN payloads. They contain +no recorded routes, VINs, or location data. + +- `ioniq6_long_calibration.json` records 100 successive calls to the reference + acceleration calibration. The test also checks intentional + disengagement and override resets separately. +- `ioniq6_heartbeat.json` records the reference 24-byte radar heartbeat at + counters 0, 1, 255, and 256 for all four physical pedal combinations. + It checks payload integrity and counter rollover. +- `ioniq6_bsm_frozen.json` records all 16 left/right presence and blinker + combinations from the reference blind-spot encoder, using a + synthetic neutral base and counters 0–15. The frozen indicator mirror bits + remain in the raw payload even though the current DBC omits their names; one + alias is decoded as a sound-warning field, so this fixture proves bytes, + not audible behavior. +- `scc_parent_frames.json` records the default SCC packer's output before the + Ioniq-specific options were added. Other vehicles retain these defaults. + +These are source and wire-format regression checks. They do not establish +device startup timing, ECU takeover behavior, or on-road qualification. diff --git a/opendbc_repo/opendbc/car/hyundai/tests/testdata/ioniq6_bsm_frozen.json b/opendbc_repo/opendbc/car/hyundai/tests/testdata/ioniq6_bsm_frozen.json new file mode 100644 index 0000000000..6705d30c8e --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/testdata/ioniq6_bsm_frozen.json @@ -0,0 +1,149 @@ +{ + "frozenSourceCommit": "678af78347d9656bc2f5dacd4204b012db861fa7", + "cases": [ + { + "left": false, + "right": false, + "leftBlinker": false, + "rightBlinker": false, + "counter": 0, + "rear": "b3f200000000008000000000000000000000000000000000", + "front": "1a430000000000000000000100000000" + }, + { + "left": false, + "right": false, + "leftBlinker": false, + "rightBlinker": true, + "counter": 1, + "rear": "b0c501000000008000000000000000000000000000000000", + "front": "3eeb0100000000000000000100000000" + }, + { + "left": false, + "right": false, + "leftBlinker": true, + "rightBlinker": false, + "counter": 2, + "rear": "b59c02000000008000000000000000000000000000000000", + "front": "73030200000000000000000100000000" + }, + { + "left": false, + "right": false, + "leftBlinker": true, + "rightBlinker": true, + "counter": 3, + "rear": "b6ab03000000008000000000000000000000000000000000", + "front": "57ab0300000000000000000100000000" + }, + { + "left": false, + "right": true, + "leftBlinker": false, + "rightBlinker": false, + "counter": 4, + "rear": "c1cd04010100208800000000000000000400000000000000", + "front": "c8c30400000000000000000100000000" + }, + { + "left": false, + "right": true, + "leftBlinker": false, + "rightBlinker": true, + "counter": 5, + "rear": "7c6005010200409000000000000000000800000000000000", + "front": "ec6b0500000000000000000100000000" + }, + { + "left": false, + "right": true, + "leftBlinker": true, + "rightBlinker": false, + "counter": 6, + "rear": "c7a306010100208800000000000000000400000000000000", + "front": "a1830600000000000000000100000000" + }, + { + "left": false, + "right": true, + "leftBlinker": true, + "rightBlinker": true, + "counter": 7, + "rear": "7a0e07010200409000000000000000000800000000000000", + "front": "852b0700000000000000000100000000" + }, + { + "left": true, + "right": false, + "leftBlinker": false, + "rightBlinker": false, + "counter": 8, + "rear": "efba08410002008800000000000000000100000000000000", + "front": "9f520800000000000000000100000000" + }, + { + "left": true, + "right": false, + "leftBlinker": false, + "rightBlinker": true, + "counter": 9, + "rear": "ec8d09410002008800000000000000000100000000000000", + "front": "bbfa0900000000000000000100000000" + }, + { + "left": true, + "right": false, + "leftBlinker": true, + "rightBlinker": false, + "counter": 10, + "rear": "7a4b0a810004009000000000000000000200000000000000", + "front": "f6120a00000000000000000100000000" + }, + { + "left": true, + "right": false, + "leftBlinker": true, + "rightBlinker": true, + "counter": 11, + "rear": "797c0b810004009000000000000000000200000000000000", + "front": "d2ba0b00000000000000000100000000" + }, + { + "left": true, + "right": true, + "leftBlinker": false, + "rightBlinker": false, + "counter": 12, + "rear": "a8700c410102208800000000000000000500000000000000", + "front": "4dd20c00000000000000000100000000" + }, + { + "left": true, + "right": true, + "leftBlinker": false, + "rightBlinker": true, + "counter": 13, + "rear": "15dd0d410202409000000000000000000900000000000000", + "front": "697a0d00000000000000000100000000" + }, + { + "left": true, + "right": true, + "leftBlinker": true, + "rightBlinker": false, + "counter": 14, + "rear": "3d810e810104209000000000000000000600000000000000", + "front": "24920e00000000000000000100000000" + }, + { + "left": true, + "right": true, + "leftBlinker": true, + "rightBlinker": true, + "counter": 15, + "rear": "e38c0f810204409000000000000000000a00000000000000", + "front": "003a0f00000000000000000100000000" + } + ] +} diff --git a/opendbc_repo/opendbc/car/hyundai/tests/testdata/ioniq6_heartbeat.json b/opendbc_repo/opendbc/car/hyundai/tests/testdata/ioniq6_heartbeat.json new file mode 100644 index 0000000000..6619a65562 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/testdata/ioniq6_heartbeat.json @@ -0,0 +1,101 @@ +{ + "source": "678af78347d9656bc2f5dacd4204b012db861fa7", + "cases": [ + { + "counter": 0, + "brake": false, + "gas": false, + "payload": "342400020000fcff000000000020000055ff000068000000" + }, + { + "counter": 0, + "brake": true, + "gas": false, + "payload": "ec4900020100fcff000000000020000055ff000068000000" + }, + { + "counter": 0, + "brake": false, + "gas": true, + "payload": "805200020000fcff000000000020000055ff000068000100" + }, + { + "counter": 0, + "brake": true, + "gas": true, + "payload": "583f00020100fcff000000000020000055ff000068000100" + }, + { + "counter": 1, + "brake": false, + "gas": false, + "payload": "371301020000fcff000000000020000055ff000068000000" + }, + { + "counter": 1, + "brake": true, + "gas": false, + "payload": "ef7e01020100fcff000000000020000055ff000068000000" + }, + { + "counter": 1, + "brake": false, + "gas": true, + "payload": "836501020000fcff000000000020000055ff000068000100" + }, + { + "counter": 1, + "brake": true, + "gas": true, + "payload": "5b0801020100fcff000000000020000055ff000068000100" + }, + { + "counter": 255, + "brake": false, + "gas": false, + "payload": "46faff020000fcff000000000020000055ff000068000000" + }, + { + "counter": 255, + "brake": true, + "gas": false, + "payload": "9e97ff020100fcff000000000020000055ff000068000000" + }, + { + "counter": 255, + "brake": false, + "gas": true, + "payload": "f28cff020000fcff000000000020000055ff000068000100" + }, + { + "counter": 255, + "brake": true, + "gas": true, + "payload": "2ae1ff020100fcff000000000020000055ff000068000100" + }, + { + "counter": 256, + "brake": false, + "gas": false, + "payload": "342400020000fcff000000000020000055ff000068000000" + }, + { + "counter": 256, + "brake": true, + "gas": false, + "payload": "ec4900020100fcff000000000020000055ff000068000000" + }, + { + "counter": 256, + "brake": false, + "gas": true, + "payload": "805200020000fcff000000000020000055ff000068000100" + }, + { + "counter": 256, + "brake": true, + "gas": true, + "payload": "583f00020100fcff000000000020000055ff000068000100" + } + ] +} diff --git a/opendbc_repo/opendbc/car/hyundai/tests/testdata/ioniq6_long_calibration.json b/opendbc_repo/opendbc/car/hyundai/tests/testdata/ioniq6_long_calibration.json new file mode 100644 index 0000000000..27ac64cb9f --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/testdata/ioniq6_long_calibration.json @@ -0,0 +1 @@ +{"source":"678af78347d9656bc2f5dacd4204b012db861fa7","scope":"Executed frozen Ioniq calibration helper at 20 Hz; active-control cases only","inputs":[[0.5,0.0,0.0,"starting"],[0.5,0.0,0.0,"starting"],[0.5,0.0,0.0,"starting"],[0.5,0.0,0.0,"starting"],[0.5,0.0,0.0,"starting"],[0.5,0.0,0.0,"starting"],[0.5,0.0,0.0,"starting"],[0.5,0.0,0.0,"starting"],[1.2,1.0,0.2,"pid"],[1.2,1.0,0.2,"pid"],[1.2,1.0,0.2,"pid"],[1.2,1.0,0.2,"pid"],[1.2,1.0,0.2,"pid"],[1.2,1.0,0.2,"pid"],[1.2,1.0,0.2,"pid"],[1.2,1.0,0.2,"pid"],[1.2,1.0,0.2,"pid"],[1.2,1.0,0.2,"pid"],[1.2,1.0,0.2,"pid"],[1.2,1.0,0.2,"pid"],[1.5,8.0,0.5,"pid"],[1.5,8.0,0.5,"pid"],[1.5,8.0,0.5,"pid"],[1.5,8.0,0.5,"pid"],[1.5,8.0,0.5,"pid"],[1.5,8.0,0.5,"pid"],[1.5,8.0,0.5,"pid"],[1.5,8.0,0.5,"pid"],[1.5,8.0,0.5,"pid"],[1.5,8.0,0.5,"pid"],[1.5,8.0,0.5,"pid"],[1.5,8.0,0.5,"pid"],[-1.5,20.0,-0.2,"pid"],[-1.5,20.0,-0.2,"pid"],[-1.5,20.0,-0.2,"pid"],[-1.5,20.0,-0.2,"pid"],[-1.5,20.0,-0.2,"pid"],[-1.5,20.0,-0.2,"pid"],[-1.5,20.0,-0.2,"pid"],[-1.5,20.0,-0.2,"pid"],[-1.5,20.0,-0.2,"pid"],[-1.5,20.0,-0.2,"pid"],[-1.5,20.0,-0.2,"pid"],[-1.5,20.0,-0.2,"pid"],[-3.0,3.0,-1.0,"stopping"],[-3.0,3.0,-1.0,"stopping"],[-3.0,3.0,-1.0,"stopping"],[-3.0,3.0,-1.0,"stopping"],[-3.0,3.0,-1.0,"stopping"],[-3.0,3.0,-1.0,"stopping"],[-3.0,3.0,-1.0,"stopping"],[-3.0,3.0,-1.0,"stopping"],[-3.0,3.0,-1.0,"stopping"],[-3.0,3.0,-1.0,"stopping"],[-3.0,3.0,-1.0,"stopping"],[-3.0,3.0,-1.0,"stopping"],[-2.0,1.0,-0.4,"stopping"],[-2.0,1.0,-0.4,"stopping"],[-2.0,1.0,-0.4,"stopping"],[-2.0,1.0,-0.4,"stopping"],[-2.0,1.0,-0.4,"stopping"],[-2.0,1.0,-0.4,"stopping"],[-2.0,1.0,-0.4,"stopping"],[-2.0,1.0,-0.4,"stopping"],[-2.0,1.0,-0.4,"stopping"],[-2.0,1.0,-0.4,"stopping"],[-2.0,1.0,-0.4,"stopping"],[-2.0,1.0,-0.4,"stopping"],[-0.5,0.0,0.0,"stopping"],[-0.5,0.0,0.0,"stopping"],[-0.5,0.0,0.0,"stopping"],[-0.5,0.0,0.0,"stopping"],[-0.5,0.0,0.0,"stopping"],[-0.5,0.0,0.0,"stopping"],[-0.5,0.0,0.0,"stopping"],[-0.5,0.0,0.0,"stopping"],[0.5,0.0,0.0,"starting"],[0.5,0.0,0.0,"starting"],[0.5,0.0,0.0,"starting"],[0.5,0.0,0.0,"starting"],[0.5,0.0,0.0,"starting"],[0.5,0.0,0.0,"starting"],[0.5,0.0,0.0,"starting"],[0.5,0.0,0.0,"starting"],[1.0,2.0,0.4,"pid"],[1.0,2.0,0.4,"pid"],[1.0,2.0,0.4,"pid"],[1.0,2.0,0.4,"pid"],[1.0,2.0,0.4,"pid"],[1.0,2.0,0.4,"pid"],[1.0,2.0,0.4,"pid"],[1.0,2.0,0.4,"pid"],[0.0,15.0,0.1,"pid"],[0.0,15.0,0.1,"pid"],[0.0,15.0,0.1,"pid"],[0.0,15.0,0.1,"pid"],[0.0,15.0,0.1,"pid"],[0.0,15.0,0.1,"pid"],[0.0,15.0,0.1,"pid"],[0.0,15.0,0.1,"pid"]],"outputs":[[0.75,0.288,0.288,5.76,1.0,true,false,3],[0.75,0.504,0.504,4.32,1.4140509090909092,true,false,3],[0.75,0.72,0.72,4.32,1.514589090909091,true,false,3],[0.75,0.75,0.75,4.32,1.6151272727272727,true,false,3],[0.75,0.75,0.75,4.32,1.6290909090909091,true,false,3],[0.75,0.75,0.75,4.32,1.6290909090909091,true,false,3],[0.75,0.75,0.75,4.32,1.6290909090909091,true,false,3],[0.75,0.75,0.75,4.32,1.6290909090909091,true,false,3],[1.2,0.9996,0.9996,4.992,1.536,true,false,1],[1.2,1.2,1.2,4.992,1.6521774545454546,true,false,1],[1.2,1.2,1.2,4.992,1.7454545454545456,true,false,1],[1.2,1.2,1.2,4.992,1.7454545454545456,true,false,1],[1.2,1.2,1.2,4.992,1.7454545454545456,true,false,1],[1.2,1.2,1.2,4.992,1.7454545454545456,true,false,1],[1.2,1.2,1.2,4.992,1.7454545454545456,true,false,1],[1.2,1.2,1.2,4.992,1.7454545454545456,true,false,1],[1.2,1.2,1.2,4.992,1.7454545454545456,true,false,1],[1.2,1.2,1.2,4.992,1.7454545454545456,true,false,1],[1.2,1.2,1.2,4.992,1.7454545454545456,true,false,1],[1.2,1.2,1.2,4.992,1.7454545454545456,true,false,1],[1.5,1.2375,1.2375,0.7500000000000001,1.6058181818181818,false,false,1],[1.5,1.2703125,1.2703125,0.6562499999999999,1.6232727272727274,false,false,1],[1.5,1.3003125,1.3003125,0.6,1.6385454545454547,false,false,1],[1.5,1.3303125,1.3303125,0.6,1.652509090909091,false,false,1],[1.5,1.3603125,1.3603125,0.6,1.6664727272727273,false,false,1],[1.5,1.3903125,1.3903125,0.6,1.6804363636363637,false,false,1],[1.5,1.4203125,1.4203125,0.6,1.6944000000000001,false,false,1],[1.5,1.4503125000000001,1.4503125000000001,0.6,1.7083636363636365,false,false,1],[1.5,1.4803125000000001,1.4803125000000001,0.6,1.722327272727273,false,false,1],[1.5,1.5,1.5,0.6,1.7362909090909093,false,false,1],[1.5,1.5,1.5,0.6,1.7454545454545456,false,false,1],[1.5,1.5,1.5,0.6,1.7454545454545456,false,false,1],[-1.5,1.32,1.32,0.6,3.5999999999999996,false,false,1],[-1.5,1.1828072727272727,1.1828072727272727,0.6,2.7438545454545453,false,false,1],[-1.5,1.0621259107438017,1.0621259107438017,0.6,2.4136272396694216,false,false,1],[-1.5,0.9519767403516154,0.9519767403516154,0.6,2.2029834078437265,false,false,1],[-1.5,0.8514405884663835,0.8514405884663835,0.6,2.010723037704638,false,false,1],[-1.5,0.7596785007456809,0.7596785007456809,0.6,1.8352417544140514,false,false,1],[-1.5,0.6733441647283268,0.6733441647283268,0.6,1.7266867203470808,false,false,1],[-1.5,0.5890190641673767,0.5890190641673767,0.6,1.6865020112190032,false,false,1],[-1.5,0.5066564386740269,0.5066564386740269,0.6,1.6472525098669972,false,false,1],[-1.5,0.42621061610124955,0.42621061610124955,0.6,1.6089164514555472,false,false,1],[-1.5,0.34763698721743863,0.34763698721743863,0.6,1.5714725776762182,false,false,1],[-1.5,0.27089198096946915,0.27089198096946915,0.6,1.5349001249593897,false,false,1],[-3.0,0.15997777172122457,0.15997777172122457,0.6,2.2182841849648915,false,true,2],[-3.0,0.058743348007372256,0.058743348007372256,0.6,2.0246884742770463,false,true,2],[-3.0,-0.03365607145508934,-0.03365607145508934,0.6,1.8479883892492317,false,true,2],[-3.0,-0.1201455301557709,-0.1201455301557709,0.6,1.7297891740136313,false,true,2],[-3.0,-0.20462214327214567,-0.20462214327214567,0.6,1.6895322623274958,false,true,2],[-3.0,-0.2871327552105394,-0.2871327552105394,0.6,1.6502122387678742,false,true,2],[-3.0,-0.36772312018018505,-0.36772312018018505,0.6,1.6118072993929127,false,true,2],[-3.0,-0.4464379275650826,-0.4464379275650826,0.6,1.5742961476979505,false,true,2],[-3.0,-0.5233208267053862,-0.5233208267053862,0.6,1.5376579828060708,false,true,2],[-3.0,-0.5984144511020608,-0.5984144511020608,0.6,1.501872487933493,false,true,2],[-3.0,-0.671760442058231,-0.671760442058231,0.6,1.4669198191234045,false,true,2],[-3.0,-0.7433994717703304,-0.7433994717703304,0.6,1.4327805942419871,false,true,2],[-0.66,-0.7133994717703304,-0.7133994717703304,0.6,0.6,false,true,2],[-0.66,-0.6833994717703303,-0.6833994717703303,0.6,0.6,false,true,2],[-0.66,-0.66,-0.66,0.6,0.6,false,true,2],[-0.66,-0.66,-0.66,0.6,0.6,false,true,2],[-0.66,-0.66,-0.66,0.6,0.6,false,true,2],[-0.66,-0.66,-0.66,0.6,0.6,false,true,2],[-0.66,-0.66,-0.66,0.6,0.6,false,true,2],[-0.66,-0.66,-0.66,0.6,0.6,false,true,2],[-0.66,-0.66,-0.66,0.6,0.6,false,true,2],[-0.66,-0.66,-0.66,0.6,0.6,false,true,2],[-0.66,-0.66,-0.66,0.6,0.6,false,true,2],[-0.66,-0.66,-0.66,0.6,0.6,false,true,2],[-0.15,-0.639,-0.639,0.42,0.6,false,true,2],[-0.15,-0.618,-0.618,0.42,0.6,false,true,2],[-0.15,-0.597,-0.597,0.42,0.6,false,true,2],[-0.15,-0.576,-0.576,0.42,0.6,false,true,2],[-0.15,-0.5549999999999999,-0.5549999999999999,0.42,0.6,false,true,2],[-0.15,-0.5339999999999999,-0.5339999999999999,0.42,0.6,false,true,2],[-0.15,-0.5129999999999999,-0.5129999999999999,0.42,0.6,false,true,2],[-0.15,-0.4919999999999999,-0.4919999999999999,0.42,0.6,false,true,2],[0.75,-0.27599999999999986,-0.27599999999999986,4.32,1.0,true,false,3],[0.75,-0.05999999999999983,-0.05999999999999983,4.32,1.0,true,false,3],[0.75,0.1560000000000002,0.1560000000000002,4.32,1.0,true,false,3],[0.75,0.3720000000000002,0.3720000000000002,4.32,1.2869818181818184,true,false,3],[0.75,0.5880000000000003,0.5880000000000003,4.32,1.4531490909090912,true,false,3],[0.75,0.75,0.75,4.32,1.553687272727273,true,false,3],[0.75,0.75,0.75,4.32,1.6290909090909091,true,false,3],[0.75,0.75,0.75,4.32,1.6290909090909091,true,false,3],[1.0,0.9612,0.9612,4.224,1.442909090909091,true,false,1],[1.0,1.0,1.0,4.224,1.541213090909091,true,false,1],[1.0,1.0,1.0,4.224,1.5592727272727274,true,false,1],[1.0,1.0,1.0,4.224,1.5592727272727274,true,false,1],[1.0,1.0,1.0,4.224,1.5592727272727274,true,false,1],[1.0,1.0,1.0,4.224,1.5592727272727274,true,false,1],[1.0,1.0,1.0,4.224,1.5592727272727274,true,false,1],[1.0,1.0,1.0,4.224,1.5592727272727274,true,false,1],[0.0,0.9150545454545455,0.9150545454545455,0.6,1.6989090909090911,false,false,1],[0.0,0.8320860033057851,0.8320860033057851,0.6,1.6593708429752068,false,false,1],[0.0,0.7510483654106687,0.7510483654106687,0.6,1.620752757902329,false,false,1],[0.0,0.6718966943611113,0.6718966943611113,0.6,1.5830334209911476,false,false,1],[0.0,0.5945870985650709,0.5945870985650709,0.6,1.5461919159208084,false,false,1],[0.0,0.5190767079075564,0.5190767079075564,0.6,1.5102078131502876,false,false,1],[0.0,0.4453236499780715,0.4453236499780715,0.6,1.475061158589699,false,false,1],[0.0,0.3732870268513091,0.3732870268513091,0.6,1.440732462535248,false,false,1]]} diff --git a/opendbc_repo/opendbc/car/hyundai/tests/testdata/ioniq6_long_calibration_soft_floor.json b/opendbc_repo/opendbc/car/hyundai/tests/testdata/ioniq6_long_calibration_soft_floor.json new file mode 100644 index 0000000000..41495ed866 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/testdata/ioniq6_long_calibration_soft_floor.json @@ -0,0 +1,102 @@ +{"baseSourceSha256":"31a7ba0d96c4b8bcc75ee2e28ce173067bfb8569cd70db37ad78b2473e6dbe7b","originalTraceSha256":"3823b567e6df8f78c2e396e22d02f703b14b42eaab05a6981d96e71ef42c42ee","method":"Committed prechange calibration, one approved accel-below-speed-dependent-floor launch revocation guard","outputs":[ +[0.5,0.03,0.03,0.6,0.6,false,false,3], +[0.5,0.06,0.06,0.6,1.1403636363636365,false,false,3], +[0.5,0.09,0.09,0.6,1.1752727272727275,false,false,3], +[0.5,0.12,0.12,0.6,1.2101818181818182,false,false,3], +[0.5,0.15,0.15,0.6,1.2450909090909092,false,false,3], +[0.5,0.18,0.18,0.6,1.28,false,false,3], +[0.5,0.21,0.21,0.6,1.314909090909091,false,false,3], +[0.5,0.24,0.24,0.6,1.3498181818181818,false,false,3], +[1.2,0.4896,0.4896,4.992,1.1520000000000001,true,false,1], +[1.2,0.7392,0.7392,4.992,1.4147956363636365,true,false,1], +[1.2,0.9887999999999999,0.9887999999999999,4.992,1.530973090909091,true,false,1], +[1.2,1.2,1.2,4.992,1.6471505454545456,true,false,1], +[1.2,1.2,1.2,4.992,1.7454545454545456,true,false,1], +[1.2,1.2,1.2,4.992,1.7454545454545456,true,false,1], +[1.2,1.2,1.2,4.992,1.7454545454545456,true,false,1], +[1.2,1.2,1.2,4.992,1.7454545454545456,true,false,1], +[1.2,1.2,1.2,4.992,1.7454545454545456,true,false,1], +[1.2,1.2,1.2,4.992,1.7454545454545456,true,false,1], +[1.2,1.2,1.2,4.992,1.7454545454545456,true,false,1], +[1.2,1.2,1.2,4.992,1.7454545454545456,true,false,1], +[1.5,1.2375,1.2375,0.7500000000000001,1.6058181818181818,false,false,1], +[1.5,1.2703125,1.2703125,0.6562499999999999,1.6232727272727274,false,false,1], +[1.5,1.3003125,1.3003125,0.6,1.6385454545454547,false,false,1], +[1.5,1.3303125,1.3303125,0.6,1.652509090909091,false,false,1], +[1.5,1.3603125,1.3603125,0.6,1.6664727272727273,false,false,1], +[1.5,1.3903125,1.3903125,0.6,1.6804363636363637,false,false,1], +[1.5,1.4203125,1.4203125,0.6,1.6944000000000001,false,false,1], +[1.5,1.4503125000000001,1.4503125000000001,0.6,1.7083636363636365,false,false,1], +[1.5,1.4803125000000001,1.4803125000000001,0.6,1.722327272727273,false,false,1], +[1.5,1.5,1.5,0.6,1.7362909090909093,false,false,1], +[1.5,1.5,1.5,0.6,1.7454545454545456,false,false,1], +[1.5,1.5,1.5,0.6,1.7454545454545456,false,false,1], +[-1.5,1.32,1.32,0.6,3.5999999999999996,false,false,1], +[-1.5,1.1828072727272727,1.1828072727272727,0.6,2.7438545454545453,false,false,1], +[-1.5,1.0621259107438017,1.0621259107438017,0.6,2.4136272396694216,false,false,1], +[-1.5,0.9519767403516154,0.9519767403516154,0.6,2.2029834078437265,false,false,1], +[-1.5,0.8514405884663835,0.8514405884663835,0.6,2.010723037704638,false,false,1], +[-1.5,0.7596785007456809,0.7596785007456809,0.6,1.8352417544140514,false,false,1], +[-1.5,0.6733441647283268,0.6733441647283268,0.6,1.7266867203470808,false,false,1], +[-1.5,0.5890190641673767,0.5890190641673767,0.6,1.6865020112190032,false,false,1], +[-1.5,0.5066564386740269,0.5066564386740269,0.6,1.6472525098669972,false,false,1], +[-1.5,0.42621061610124955,0.42621061610124955,0.6,1.6089164514555472,false,false,1], +[-1.5,0.34763698721743863,0.34763698721743863,0.6,1.5714725776762182,false,false,1], +[-1.5,0.27089198096946915,0.27089198096946915,0.6,1.5349001249593897,false,false,1], +[-3.0,0.15997777172122457,0.15997777172122457,0.6,2.2182841849648915,false,true,2], +[-3.0,0.058743348007372256,0.058743348007372256,0.6,2.0246884742770463,false,true,2], +[-3.0,-0.03365607145508934,-0.03365607145508934,0.6,1.8479883892492317,false,true,2], +[-3.0,-0.1201455301557709,-0.1201455301557709,0.6,1.7297891740136313,false,true,2], +[-3.0,-0.20462214327214567,-0.20462214327214567,0.6,1.6895322623274958,false,true,2], +[-3.0,-0.2871327552105394,-0.2871327552105394,0.6,1.6502122387678742,false,true,2], +[-3.0,-0.36772312018018505,-0.36772312018018505,0.6,1.6118072993929127,false,true,2], +[-3.0,-0.4464379275650826,-0.4464379275650826,0.6,1.5742961476979505,false,true,2], +[-3.0,-0.5233208267053862,-0.5233208267053862,0.6,1.5376579828060708,false,true,2], +[-3.0,-0.5984144511020608,-0.5984144511020608,0.6,1.501872487933493,false,true,2], +[-3.0,-0.671760442058231,-0.671760442058231,0.6,1.4669198191234045,false,true,2], +[-3.0,-0.7433994717703304,-0.7433994717703304,0.6,1.4327805942419871,false,true,2], +[-0.66,-0.7133994717703304,-0.7133994717703304,0.6,0.6,false,true,2], +[-0.66,-0.6833994717703303,-0.6833994717703303,0.6,0.6,false,true,2], +[-0.66,-0.66,-0.66,0.6,0.6,false,true,2], +[-0.66,-0.66,-0.66,0.6,0.6,false,true,2], +[-0.66,-0.66,-0.66,0.6,0.6,false,true,2], +[-0.66,-0.66,-0.66,0.6,0.6,false,true,2], +[-0.66,-0.66,-0.66,0.6,0.6,false,true,2], +[-0.66,-0.66,-0.66,0.6,0.6,false,true,2], +[-0.66,-0.66,-0.66,0.6,0.6,false,true,2], +[-0.66,-0.66,-0.66,0.6,0.6,false,true,2], +[-0.66,-0.66,-0.66,0.6,0.6,false,true,2], +[-0.66,-0.66,-0.66,0.6,0.6,false,true,2], +[-0.15,-0.639,-0.639,0.42,0.6,false,true,2], +[-0.15,-0.618,-0.618,0.42,0.6,false,true,2], +[-0.15,-0.597,-0.597,0.42,0.6,false,true,2], +[-0.15,-0.576,-0.576,0.42,0.6,false,true,2], +[-0.15,-0.5549999999999999,-0.5549999999999999,0.42,0.6,false,true,2], +[-0.15,-0.5339999999999999,-0.5339999999999999,0.42,0.6,false,true,2], +[-0.15,-0.5129999999999999,-0.5129999999999999,0.42,0.6,false,true,2], +[-0.15,-0.4919999999999999,-0.4919999999999999,0.42,0.6,false,true,2], +[0.5,-0.46199999999999986,-0.46199999999999986,0.6,0.6,false,false,3], +[0.5,-0.43199999999999983,-0.43199999999999983,0.6,0.6,false,false,3], +[0.5,-0.4019999999999998,-0.4019999999999998,0.6,0.6,false,false,3], +[0.5,-0.3719999999999998,-0.3719999999999998,0.6,0.6,false,false,3], +[0.5,-0.34199999999999975,-0.34199999999999975,0.6,0.6,false,false,3], +[0.5,-0.3119999999999997,-0.3119999999999997,0.6,0.6,false,false,3], +[0.5,-0.2819999999999997,-0.2819999999999997,0.6,0.6,false,false,3], +[0.5,-0.25199999999999967,-0.25199999999999967,0.6,0.6,false,false,3], +[1.0,-0.04079999999999967,-0.04079999999999967,4.224,1.0,true,false,1], +[1.0,0.17040000000000033,0.17040000000000033,4.224,1.0,true,false,1], +[1.0,0.38160000000000033,0.38160000000000033,4.224,1.0,true,false,1], +[1.0,0.5928000000000003,0.5928000000000003,4.224,1.0,true,false,1], +[1.0,0.8040000000000003,0.8040000000000003,4.224,1.3298036363636367,true,false,1], +[1.0,1.0,1.0,4.224,1.4680436363636367,true,false,1], +[1.0,1.0,1.0,4.224,1.5592727272727274,true,false,1], +[1.0,1.0,1.0,4.224,1.5592727272727274,true,false,1], +[0.0,0.9150545454545455,0.9150545454545455,0.6,1.6989090909090911,false,false,1], +[0.0,0.8320860033057851,0.8320860033057851,0.6,1.6593708429752068,false,false,1], +[0.0,0.7510483654106687,0.7510483654106687,0.6,1.620752757902329,false,false,1], +[0.0,0.6718966943611113,0.6718966943611113,0.6,1.5830334209911476,false,false,1], +[0.0,0.5945870985650709,0.5945870985650709,0.6,1.5461919159208084,false,false,1], +[0.0,0.5190767079075564,0.5190767079075564,0.6,1.5102078131502876,false,false,1], +[0.0,0.4453236499780715,0.4453236499780715,0.6,1.475061158589699,false,false,1], +[0.0,0.3732870268513091,0.3732870268513091,0.6,1.440732462535248,false,false,1] +]} diff --git a/opendbc_repo/opendbc/car/hyundai/tests/testdata/scc_parent_frames.json b/opendbc_repo/opendbc/car/hyundai/tests/testdata/scc_parent_frames.json new file mode 100644 index 0000000000..23dd5ee92e --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/testdata/scc_parent_frames.json @@ -0,0 +1,170 @@ +{ + "source": "0ca4d92e7550140d98f2a47f5ff2743e6631ca64", + "cases": [ + { + "args": [ + false, + 0.2, + -3.5, + true, + false, + 80.0 + ], + "data": "5556000a003064000400000250080000fff33f1e0a0002010000000000000000" + }, + { + "args": [ + false, + 0.2, + -0.5, + true, + false, + 80.0 + ], + "data": "5556000a003064000400000250080000fff33f1e0a0002010000000000000000" + }, + { + "args": [ + false, + 0.2, + 0.0, + false, + false, + 80.0 + ], + "data": "1c8e000a003064000400000250080000fff33f1e0a0002000000000000000000" + }, + { + "args": [ + false, + 0.2, + 0.5, + false, + false, + 80.0 + ], + "data": "1c8e000a003064000400000250080000fff33f1e0a0002000000000000000000" + }, + { + "args": [ + false, + 0.2, + 2.0, + false, + false, + 80.0 + ], + "data": "1c8e000a003064000400000250080000fff33f1e0a0002000000000000000000" + }, + { + "args": [ + true, + 0.2, + -3.5, + true, + false, + 80.0 + ], + "data": "4457000a00306400140000025008000009142a1e320002010000000000000000" + }, + { + "args": [ + true, + 0.2, + -0.5, + true, + false, + 80.0 + ], + "data": "f93c000a00306400140000025008000009d43c1e320002010000000000000000" + }, + { + "args": [ + true, + 0.2, + 0.0, + false, + false, + 80.0 + ], + "data": "ff30000a00306400140000025008000009f43f1e320002000000000000000000" + }, + { + "args": [ + true, + 0.2, + 0.5, + false, + false, + 80.0 + ], + "data": "c8fe000a0030640014000002500800001d14431e320002000000000000000000" + }, + { + "args": [ + true, + 0.2, + 2.0, + false, + false, + 80.0 + ], + "data": "d4c3000a0030640014000002500800001d744c1e320002000000000000000000" + }, + { + "args": [ + true, + 0.2, + -3.5, + true, + true, + 80.0 + ], + "data": "b228000a003064002400000250080000fff33f1e320002010000000000000000" + }, + { + "args": [ + true, + 0.2, + -0.5, + true, + true, + 80.0 + ], + "data": "b228000a003064002400000250080000fff33f1e320002010000000000000000" + }, + { + "args": [ + true, + 0.2, + 0.0, + false, + true, + 80.0 + ], + "data": "fbf0000a003064002400000250080000fff33f1e320002000000000000000000" + }, + { + "args": [ + true, + 0.2, + 0.5, + false, + true, + 80.0 + ], + "data": "fbf0000a003064002400000250080000fff33f1e320002000000000000000000" + }, + { + "args": [ + true, + 0.2, + 2.0, + false, + true, + 80.0 + ], + "data": "fbf0000a003064002400000250080000fff33f1e320002000000000000000000" + } + ] +} diff --git a/opendbc_repo/opendbc/car/hyundai/torque_ev_scc.py b/opendbc_repo/opendbc/car/hyundai/torque_ev_scc.py new file mode 100644 index 0000000000..de21cf84a0 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/torque_ev_scc.py @@ -0,0 +1,39 @@ +import time + +from opendbc.car.hyundai.canfd_lead import CANFDLeadObservation, select_lead +from opendbc.car import structs +from opendbc.car.hyundai.canfd_stock_aol import qualified_long + + +def eligible(cp): + return qualified_long(cp) + + +def options(available, control_state, lead): + return { + "direct_accel": True, + "main_mode_acc": int(available), + "jerk_lower": 5.0, + "jerk_upper": 3.0 if control_state == structs.CarControl.Actuators.LongControlState.pid else 1.0, + "lead_visible": lead.visible, + "lead_distance": lead.distance, + "lead_rel_speed": lead.relative_speed, + } + + +def clock_boot_ns(): + try: + return time.clock_gettime_ns(time.CLOCK_BOOTTIME) + except (AttributeError, OSError): + return 0 + + +def fallback_lead(camera, hud_visible, floor_ns): + now_ns = clock_boot_ns() + observation = camera.current(now_ns, floor_ns) if camera is not None and now_ns > floor_ns > 0 else None + camera_lead = ( + None + if observation is None + else CANFDLeadObservation(observation.visible, observation.distance_m, observation.relative_speed_mps, observation.producer_boot_ns) + ) + return select_lead(None, camera_lead, hud_visible=hud_visible, observed_ns=max(now_ns, floor_ns + 1, 1), epoch_floor_ns=max(floor_ns, 0)) diff --git a/opendbc_repo/opendbc/car/hyundai/torque_ev_startup.py b/opendbc_repo/opendbc/car/hyundai/torque_ev_startup.py new file mode 100644 index 0000000000..87a3a90c75 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/torque_ev_startup.py @@ -0,0 +1,219 @@ +"""Torque EV HDAII startup transaction. + +Ioniq 5 and second-generation Kona EV share the original session and suppressed +communication-control request. No SecurityAccess key exchange is implied. +""" + +from copy import deepcopy +import time + +from opendbc.car import Bus, make_tester_present_msg +from opendbc.car.hyundai.canfd_stock_aol import qualified as stock_qualified +from opendbc.car.hyundai.ecu_startup import HyundaiECUStartup, Outcome +from opendbc.car.hyundai.ev9_keeper import EV9Keeper as StartupKeeper +from opendbc.car.hyundai.values import CAR, HyundaiFlags, HyundaiSafetyFlags +from opendbc.car.hyundai.hyundaicanfd import CanBus +from opendbc.car.structs import CarParams +from opendbc.can.parser import MAX_BAD_COUNTER + +CARS = frozenset((CAR.HYUNDAI_IONIQ_5, CAR.HYUNDAI_KONA_EV_2ND_GEN)) + + +def topology_owned(cp): + required_flags = HyundaiFlags.CANFD | HyundaiFlags.EV | HyundaiFlags.CANFD_LKA_STEER_MSG + allowed = ( + required_flags + | HyundaiFlags.CANFD_LKA_STEER_MSG_ALT + | HyundaiFlags.CANFD_ALT_GEARS + | HyundaiFlags.CANFD_ALT_GEARS_2 + | HyundaiFlags.CANFD_NO_RADAR_DISABLE + | HyundaiFlags.CCNC + ) + if ( + cp.carFingerprint not in CARS + or cp.brand != "hyundai" + or cp.passive + or cp.dashcamOnly + or cp.notCar + or cp.steerControlType != CarParams.SteerControlType.torque + or int(cp.flags) & int(required_flags) != int(required_flags) + or int(cp.flags) & ~int(allowed) + or len(cp.safetyConfigs) != 1 + or cp.safetyConfigs[0].safetyModel != CarParams.SafetyModel.hyundaiCanfd + ): + return False + bus = CanBus(cp) + return (bus.ACAN, bus.ECAN, bus.CAM) == (0, 1, 2) + + +def required(cp): + return cp.carFingerprint in CARS and cp.openpilotLongitudinalControl and bool(cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG) + + +def candidate(stock, *, requested, is_release): + # Restrict takeover to the supported stock HDAII torque topology. + if not requested or is_release or not stock.alphaLongitudinalAvailable or not topology_owned(stock) or not stock_qualified(stock): + return None + cp = stock.as_reader().as_builder() + cp.alphaLongitudinalAvailable = True + cp.openpilotLongitudinalControl = True + cp.pcmCruise = False + cp.safetyConfigs[0].safetyParam = (int(cp.safetyConfigs[0].safetyParam) & ~0x0800) | HyundaiSafetyFlags.LONG.value + return cp + + +class TorqueEVStartup(HyundaiECUStartup): + def __init__(self, cp, callbacks, *, stock_cp, clock=time.monotonic, keeper_factory=StartupKeeper): + if stock_cp.carFingerprint not in CARS or not stock_qualified(stock_cp): + raise ValueError("Exact torque EV stock CP required") + expected = candidate(stock_cp, requested=True, is_release=False) + if expected is None or cp.to_dict() != expected.to_dict(): + raise ValueError("Torque EV LONG candidate does not match stock contract") + super().__init__(cp, callbacks, address=0x730, bus=1, label="Torque EV") + self.stock_cp = stock_cp.as_reader().as_builder() + self.clock = clock + self.keeper_factory = keeper_factory + self.keeper = None + self.handed_off = False + + def _disable(self, can_recv, can_send, **kwargs): + # Existing generic helper returns after send without inspecting negatives. + # Preserve original explicit rejection vs expected suppressed silence. + self.disable_response = None + for _ in range(10): + try: + session = self._query(can_send, can_recv, self.bus, [(self.address, None)], [b"\x10\x03"], [b"\x50\x03"]) + if not session.get_data(0.1): + time.sleep(0.1) + continue + time.sleep(0.05) + command = self._query(can_send, can_recv, self.bus, [(self.address, None)], [b"\x28\x83\x01"], [b""]) + replies = command.get_data(0.1) + if not replies: + self.disable_response = "suppressed_reply_sent_unconfirmed" + return True + for payload in replies.values(): + if payload == b"\x68\x03": + self.disable_response = "positive_communication_control" + elif len(payload) == 3 and payload[:2] == b"\x7f\x28": + self.disable_response = "negative_communication_control" + return False + else: + self.disable_response = "unexpected_or_malformed_reply" + return False + return True + except Exception: + if self.disable_attempted: + return False + return False + + def prepare(self, *, admission): + super().prepare(admission=admission) + if self.outcome in (Outcome.STOCK_UNTOUCHED, Outcome.STOCK_RESTORED): + # Retain every stock field/marker, not a partly reconstructed fallback. + self.cp = self.stock_cp.as_reader().as_builder() + self.prepared_cp = deepcopy(self.cp.to_dict()) + elif self.outcome is Outcome.SENT_UNCONFIRMED: + self.keeper = self.keeper_factory(lambda: self.callbacks[1]([make_tester_present_msg(self.address, self.bus, suppress_response=True)]), clock=self.clock) + self.keeper.start() + return self.cp + + def finalize_aol_configuration(self, ci): + if self.published or self.closed or ci is not self.ci or not self.ready: + raise RuntimeError("Torque EV AOL finalization requires configured startup") + if self.prepared_for(ci.CP): + return + if self.outcome in (Outcome.STOCK_UNTOUCHED, Outcome.STOCK_RESTORED): + return super().finalize_aol_configuration(ci) + from opendbc.car.hyundai.canfd_stock_aol import qualified_long + + if self.outcome is not Outcome.SENT_UNCONFIRMED or not qualified_long(ci.CP, marked_only=True): + raise RuntimeError("Torque EV startup does not admit this AOL configuration") + expected = deepcopy(self.prepared_cp) + configs = expected.get("safetyConfigs", []) + if ( + len(configs) != 1 + or configs[0]["safetyModel"] != "hyundaiCanfd" + or configs[0]["safetyParam"] not in (0x15, 0x95) + or expected["alternativeExperience"] != 0 + ): + raise RuntimeError("Torque EV prepared safety profile cannot be finalized") + configs[0]["safetyParam"] |= 0x0800 + expected["alternativeExperience"] = 32 + if ci.CP.to_dict() != expected: + raise RuntimeError("Torque EV AOL finalization changed unrelated CarParams") + self.prepared_cp = expected + + def check(self): + super().check() + if self.keeper is not None and self.keeper.abort_reason is not None: + self.keeper.stop() + # Main thread alone performs receive/restore; worker only marks abort. + if self.disable_attempted and not self.restored: + try: + self._restore() + except Exception: + pass + self.outcome = Outcome.ABORT_UNCERTAIN + raise RuntimeError("Torque EV startup handoff aborted") + + def sources_current(self, ci, now_ns): + if not ci.CS.out.canValid or ci.CS.out.canTimeout: + return False + parser = ci.can_parsers[Bus.pt] + for name in ("MDPS", "ACCELERATOR", "WHEEL_SPEEDS", "TCS"): + source = parser.message_states.get(parser.dbc.name_to_msg[name].address) + if ( + source is None + or not source.timestamps + or source.counter_fail >= MAX_BAD_COUNTER + or not self.floor_ns < int(source.timestamps[-1]) <= now_ns + or now_ns - int(source.timestamps[-1]) > source.timeout_threshold + ): + return False + return True + + def before_control(self, *, configured, sources_current, control_current): + self.check() + if self.outcome is not Outcome.SENT_UNCONFIRMED: + return True + if not (configured and sources_current and control_current): + return False + if not self.handed_off: + self.keeper.stop() # Never join while holding sender callback lock. + self.check() + self.handed_off = True + return True + + def _warm_stock(self, ci): + deadline = self.clock() + 3.0 + while self.clock() < deadline: + packets = self.callbacks[0]() + stamped = [(int(getattr(p, "log_mono_time_ns", 0)), list(p)) for p in packets] + if any(stamp <= 0 for stamp, _ in stamped): + raise RuntimeError("Stock warmup requires actual timestamped CAN") + state = ci.update(stamped) + now = time.clock_gettime_ns(time.CLOCK_BOOTTIME) + parser = ci.can_parsers[Bus.pt] + scc = parser.message_states.get(parser.dbc.name_to_msg["SCC_CONTROL"].address) + if ( + state.canValid + and not state.canTimeout + and parser.bus == self.bus + and self.sources_current(ci, now) + and scc is not None + and scc.timestamps + and scc.counter_fail < MAX_BAD_COUNTER + and self.floor_ns < int(scc.timestamps[-1]) <= now + and now - int(scc.timestamps[-1]) <= scc.timeout_threshold + ): + self.ready = True + return + time.sleep(0.005) + self.outcome = Outcome.ABORT_UNCERTAIN + raise RuntimeError("Stock sources did not resume before publication") + + def close(self): + if self.keeper is not None: + self.keeper.stop() + super().close() diff --git a/opendbc_repo/opendbc/car/hyundai/values.py b/opendbc_repo/opendbc/car/hyundai/values.py index c3df8a51b3..2173d0eb00 100644 --- a/opendbc_repo/opendbc/car/hyundai/values.py +++ b/opendbc_repo/opendbc/car/hyundai/values.py @@ -5,8 +5,9 @@ from enum import IntFlag from opendbc.car import Bus, CarSpecs, DbcDict, PlatformConfig, Platforms, uds from opendbc.car.common.conversions import Conversions as CV from opendbc.car.structs import CarParams -from opendbc.car.docs_definitions import CarHarness, CarDocs, CarParts +from opendbc.car.docs_definitions import CarHarness, CarDocs, CarParts, SupportType from opendbc.car.fw_query_definitions import FwQueryConfig, Request, p16 +from opendbc.car.lateral import AngleSteeringLimitsVM Ecu = CarParams.Ecu @@ -14,8 +15,10 @@ Ecu = CarParams.Ecu class CarControllerParams: ACCEL_MIN = -3.5 # m/s^2 ACCEL_MAX = 2.0 # m/s^2 + ANGLE_LIMITS = AngleSteeringLimitsVM(360, MAX_ANGLE_RATE=5) - def __init__(self, CP): + def __init__(self, CP, vEgoRaw=100.): + self.ACCEL_MAX = 3.5 if is_blended_alpha(CP) else type(self).ACCEL_MAX self.STEER_DELTA_UP = 3 self.STEER_DELTA_DOWN = 7 self.STEER_DRIVER_ALLOWANCE = 50 @@ -25,17 +28,22 @@ class CarControllerParams: self.STEER_STEP = 1 # 100 Hz if CP.flags & HyundaiFlags.CANFD: - self.STEER_MAX = 270 + self.STEER_MAX = 270 if CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING else 409 self.STEER_DRIVER_ALLOWANCE = 250 self.STEER_DRIVER_MULTIPLIER = 2 self.STEER_THRESHOLD = 250 self.STEER_DELTA_UP = 2 self.STEER_DELTA_DOWN = 3 + if (not CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING and 0 <= vEgoRaw < 15 and + CP.carFingerprint != CAR.KIA_CARNIVAL_HEV_4TH_GEN): + self.STEER_DELTA_UP = 10 + self.STEER_DELTA_DOWN = 8 # To determine the limit for your car, find the maximum value that the stock LKAS will request. # If the max stock LKAS request is <384, add your car to this list. elif CP.carFingerprint in (CAR.GENESIS_G80, CAR.HYUNDAI_ELANTRA, CAR.HYUNDAI_ELANTRA_GT_I30, CAR.HYUNDAI_IONIQ, CAR.HYUNDAI_IONIQ_EV_LTD, CAR.HYUNDAI_SANTA_FE_PHEV_2022, CAR.HYUNDAI_SONATA_LF, CAR.KIA_FORTE, CAR.KIA_NIRO_PHEV, + CAR.KIA_FORTE_2019_NON_SCC, CAR.KIA_FORTE_2021_NON_SCC, CAR.KIA_OPTIMA_H, CAR.KIA_OPTIMA_H_G4_FL, CAR.KIA_SORENTO): self.STEER_MAX = 255 @@ -50,10 +58,19 @@ class CarControllerParams: self.STEER_DELTA_UP = 2 self.STEER_DELTA_DOWN = 3 + elif is_blended(CP): + self.STEER_MAX = 384 if CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG else 404 + if not CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG: + self.STEER_DELTA_UP = 2 + self.STEER_DELTA_DOWN = 3 + # Default for most HKG else: self.STEER_MAX = 384 + if CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING: + self.STEER_THRESHOLD = 175 + class HyundaiSafetyFlags(IntFlag): EV_GAS = 1 @@ -66,6 +83,37 @@ class HyundaiSafetyFlags(IntFlag): CANFD_LKA_STEER_MSG_ALT = 128 FCEV_GAS = 256 ALT_LIMITS_2 = 512 + CCNC = 1024 + HAS_LDA_BUTTON = 2048 + NON_SCC = 4096 + CAN_CANFD_BLENDED = 8192 # classic Hyundai mode only + CARNIVAL_ALT_RESUME = 8192 + CANFD_ANGLE_STEERING = 16384 + CAN_REFRESH_MSGS = 32768 + + +RAY_PEDAL_SAFETY_PARAM = int(HyundaiSafetyFlags.EV_GAS | HyundaiSafetyFlags.LONG | HyundaiSafetyFlags.HAS_LDA_BUTTON | + HyundaiSafetyFlags.NON_SCC | HyundaiSafetyFlags.CAN_REFRESH_MSGS) + +# Bits 11-13 are a geometry selector only when the angle discriminator is set. +# The existing classic-CAN meanings are unchanged in all angle-disabled modes. +CANFD_ANGLE_OBSERVED_ADAS_BIT = 512 # angle-only 0xCB profile; classic ALT_LIMITS_2 is unaffected +CANFD_ANGLE_MODEL_BANK_BIT = 8192 # angle-only model bank; ordinary Carnival resume is unaffected +CANFD_ANGLE_CCNC_MODEL_BANK_BIT = 1024 # angle-only CCNC geometry bank; ordinary CCNC is unaffected +CANFD_ANGLE_MODEL_BITS = { + "GENESIS_GV70_2026": 0, + "GENESIS_GV70_ELECTRIFIED_2ND_GEN": 2048, + "GENESIS_GV80_2025": 4096, + "HYUNDAI_IONIQ_9": 6144, + "HYUNDAI_AZERA_HEV_7TH_GEN": 8192, + "KIA_SORENTO_HEV_4TH_GEN_LFA2": 10240, + "KIA_SPORTAGE_HEV_2026": 12288, + "HYUNDAI_SANTA_FE_HEV_5TH_GEN": CANFD_ANGLE_CCNC_MODEL_BANK_BIT, + "KIA_SPORTAGE_2026": CANFD_ANGLE_CCNC_MODEL_BANK_BIT | 2048, + "KIA_EV6_2025": 14336, + "HYUNDAI_IONIQ_5_PE": CANFD_ANGLE_CCNC_MODEL_BANK_BIT | 4096, + "KIA_EV9": CANFD_ANGLE_CCNC_MODEL_BANK_BIT | 6144, +} # Hyundai/Kia/Genesis SCC (Smart Cruise Control) and steering architecture: @@ -146,8 +194,13 @@ class HyundaiFlags(IntFlag): FCEV = 2 ** 25 ALT_LIMITS_2 = 2 ** 26 + CCNC = 2 ** 27 + NON_SCC = 2 ** 28 + NON_SCC_NO_FCA = 2 ** 29 + NON_SCC_RADAR_FCA = 2 ** 30 + CANFD_ANGLE_STEERING = 2 ** 31 - HAS_BSM = 2 ** 27 # blind spot monitoring + # BSM retains its existing CarParams.deprecated.enableBsm wire field; all 32 flag bits are assigned. @dataclass @@ -155,24 +208,57 @@ class HyundaiCarDocs(CarDocs): package: str = "Smart Cruise Control (SCC)" +@dataclass +class HyundaiNonSccCarDocs(CarDocs): + package: str = "No Smart Cruise Control (Non-SCC)" + support_type: SupportType = SupportType.COMMUNITY + support_link: str = "community" + + @dataclass class HyundaiPlatformConfig(PlatformConfig): dbc_dict: DbcDict = field(default_factory=lambda: {Bus.pt: "hyundai_can_generated"}) def init(self): if self.flags & HyundaiFlags.MANDO_RADAR: - self.dbc_dict = {Bus.pt: "hyundai_can_generated", Bus.radar: 'hyundai_kia_mando_front_radar_generated'} + self.dbc_dict = {Bus.pt: "hyundai_can_generated", Bus.radar: self.dbc_dict.get(Bus.radar, 'hyundai_kia_mando_front_radar_generated')} if self.flags & HyundaiFlags.MIN_STEER_32_MPH: self.specs = self.specs.override(minSteerSpeed=32 * CV.MPH_TO_MS) +@dataclass +class HyundaiRefreshPlatformConfig(HyundaiPlatformConfig): + dbc_dict: DbcDict = field(default_factory=lambda: {Bus.pt: "hyundai_can_refresh_generated"}) + + +@dataclass +class HyundaiNonSccPlatformConfig(HyundaiPlatformConfig): + radar_dbc: str | None = None + + def init(self): + super().init() + if self.radar_dbc is not None: + self.dbc_dict = {Bus.pt: "hyundai_can_generated", Bus.radar: self.radar_dbc} + self.flags |= HyundaiFlags.NON_SCC + + +HYUNDAI_MRREVO14F_RADAR_DBC = "hyundai_mrrevo14f_radar_generated" +HYUNDAI_G90_RADAR_DBC = "hyundai_genesis_g90_radar_generated" +HYUNDAI_GV70_RADAR_DBC = "hyundai_radar_210_21f_generated" +HYUNDAI_MRR30_RADAR_DBC = "hyundai_mrr30_radar_generated" +HYUNDAI_MRR35_RADAR_DBC = "hyundai_mrr35_radar_generated" + + @dataclass class HyundaiCanFDPlatformConfig(PlatformConfig): dbc_dict: DbcDict = field(default_factory=lambda: {Bus.pt: "hyundai_canfd_generated"}) + radar_dbc: str | None = None def init(self): self.flags |= HyundaiFlags.CANFD + if self.radar_dbc is not None: + self.dbc_dict = {Bus.pt: "hyundai_canfd_generated", Bus.radar: self.radar_dbc} class CAR(Platforms): @@ -189,6 +275,12 @@ class CAR(Platforms): CarSpecs(mass=1675, wheelbase=2.885, steerRatio=14.5), flags=HyundaiFlags.HYBRID, ) + HYUNDAI_AZERA_HEV_7TH_GEN = HyundaiCanFDPlatformConfig( + [HyundaiCarDocs("Hyundai Azera Hybrid (with HDA II & LFA2) 2025", "Highway Driving Assist II & Lane Follow Assist 2", + car_parts=CarParts.common([CarHarness.hyundai_s]))], + CarSpecs(mass=1720, wheelbase=2.895, steerRatio=13.5), + flags=HyundaiFlags.CANFD_ANGLE_STEERING, + ) HYUNDAI_ELANTRA = HyundaiPlatformConfig( [ # TODO: 2017-18 could be Hyundai G @@ -212,12 +304,25 @@ class CAR(Platforms): CarSpecs(mass=2800 * CV.LB_TO_KG, wheelbase=2.72, steerRatio=12.9, tireStiffnessFactor=0.65), flags=HyundaiFlags.CHECKSUM_CRC8, ) + HYUNDAI_ELANTRA_2024 = HyundaiRefreshPlatformConfig( + [HyundaiCarDocs("Hyundai Elantra 2024-25", car_parts=CarParts.common([CarHarness.hyundai_k]))], + CarSpecs(mass=2797 * CV.LB_TO_KG, wheelbase=2.72, steerRatio=12.9, tireStiffnessFactor=0.65), + flags=HyundaiFlags.CHECKSUM_CRC8 | HyundaiFlags.CAMERA_SCC, + ) HYUNDAI_ELANTRA_HEV_2021 = HyundaiPlatformConfig( [HyundaiCarDocs("Hyundai Elantra Hybrid 2021-23", video="https://youtu.be/_EdYQtV52-c", car_parts=CarParts.common([CarHarness.hyundai_k]))], CarSpecs(mass=3017 * CV.LB_TO_KG, wheelbase=2.72, steerRatio=12.9, tireStiffnessFactor=0.65), flags=HyundaiFlags.CHECKSUM_CRC8 | HyundaiFlags.HYBRID, ) + HYUNDAI_ELANTRA_HEV_2024 = HyundaiRefreshPlatformConfig( + [ + HyundaiCarDocs("Hyundai Elantra Hybrid 2024-26", car_parts=CarParts.common([CarHarness.hyundai_k])), + HyundaiCarDocs("Hyundai i30 Hybrid 2024", car_parts=CarParts.common([CarHarness.hyundai_k])), + ], + HYUNDAI_ELANTRA_HEV_2021.specs, + flags=HyundaiFlags.CHECKSUM_CRC8 | HyundaiFlags.CAMERA_SCC | HyundaiFlags.HYBRID, + ) HYUNDAI_GENESIS = HyundaiPlatformConfig( [ # TODO: check 2015 packages @@ -346,6 +451,21 @@ class CAR(Platforms): CarSpecs(mass=1999, wheelbase=2.9, steerRatio=15.6 * 1.15, tireStiffnessFactor=0.63), flags=HyundaiFlags.MANDO_RADAR | HyundaiFlags.CHECKSUM_CRC8, ) + HYUNDAI_PALISADE_2023 = HyundaiPlatformConfig( + [ + HyundaiCarDocs("Hyundai Palisade (without HDA II) 2023-25", "Highway Driving Assist", + car_parts=CarParts.common([CarHarness.hyundai_a])), + HyundaiCarDocs("Hyundai Palisade (with HDA II) 2023-25", "Highway Driving Assist II", + car_parts=CarParts.common([CarHarness.hyundai_r])), + HyundaiCarDocs("Kia Telluride (without HDA II) 2023-25", "Highway Driving Assist", + car_parts=CarParts.common([CarHarness.hyundai_l])), + HyundaiCarDocs("Kia Telluride (with HDA II) 2023-24", "Highway Driving Assist II", + car_parts=CarParts.common([CarHarness.hyundai_p])), + ], + HYUNDAI_PALISADE.specs, + flags=HyundaiFlags.CHECKSUM_CRC8, + dbc_dict={Bus.pt: "hyundai_palisade_2023_generated"}, + ) HYUNDAI_VELOSTER = HyundaiPlatformConfig( [HyundaiCarDocs("Hyundai Veloster 2019-20", min_enable_speed=5. * CV.MPH_TO_MS, car_parts=CarParts.common([CarHarness.hyundai_e]))], CarSpecs(mass=2917 * CV.LB_TO_KG, wheelbase=2.8, steerRatio=13.75 * 1.15, tireStiffnessFactor=0.5), @@ -356,6 +476,16 @@ class CAR(Platforms): HYUNDAI_SONATA.specs, flags=HyundaiFlags.MANDO_RADAR | HyundaiFlags.CHECKSUM_CRC8 | HyundaiFlags.HYBRID, ) + HYUNDAI_SANTA_FE_HEV_5TH_GEN = HyundaiCanFDPlatformConfig( + [ + HyundaiCarDocs("Hyundai Santa Fe Hybrid (with HDA II & LFA2) 2024-25", "Highway Driving Assist II & Lane Follow Assist 2", + car_parts=CarParts.common([CarHarness.hyundai_p])), + HyundaiCarDocs("Hyundai Santa Fe Hybrid (without HDA II, LFA2) 2025-26", "Lane Follow Assist 2", + car_parts=CarParts.common([CarHarness.hyundai_l])), + ], + CarSpecs(mass=2035, wheelbase=2.81, steerRatio=13.72), + flags=HyundaiFlags.CANFD_ANGLE_STEERING | HyundaiFlags.CCNC, + ) HYUNDAI_IONIQ_5 = HyundaiCanFDPlatformConfig( [ HyundaiCarDocs("Hyundai Ioniq 5 (Southeast Asia and Europe only) 2022-24", "All", car_parts=CarParts.common([CarHarness.hyundai_q])), @@ -365,6 +495,27 @@ class CAR(Platforms): CarSpecs(mass=1948, wheelbase=2.97, steerRatio=14.26, tireStiffnessFactor=0.65), flags=HyundaiFlags.EV, ) + HYUNDAI_IONIQ_5_PE = HyundaiCanFDPlatformConfig( + [ + HyundaiCarDocs("Hyundai Ioniq 5 PE (with HDA II & LFA2) 2025-26", "Highway Driving Assist II & Lane Follow Assist 2", + car_parts=CarParts.common([CarHarness.hyundai_q])) + ], + HYUNDAI_IONIQ_5.specs, + flags=HyundaiFlags.EV | HyundaiFlags.CANFD_ANGLE_STEERING | HyundaiFlags.CCNC, + radar_dbc=HYUNDAI_MRR35_RADAR_DBC, + ) + HYUNDAI_IONIQ_5_N = HyundaiCanFDPlatformConfig( + [HyundaiCarDocs("Hyundai Ioniq 5 N (with HDA II) 2024", car_parts=CarParts.common([CarHarness.hyundai_s]))], + CarSpecs(mass=2205, wheelbase=3.00, steerRatio=14.26, tireStiffnessFactor=1.3), + flags=HyundaiFlags.EV | HyundaiFlags.CCNC, + ) + HYUNDAI_IONIQ_9 = HyundaiCanFDPlatformConfig( + [HyundaiCarDocs("Hyundai Ioniq 9 (with HDA II & LFA2) 2025-26", "Highway Driving Assist II & Lane Follow Assist 2", + car_parts=CarParts.common([CarHarness.hyundai_m]))], + CarSpecs(mass=2700, wheelbase=3.13, steerRatio=16.02), + flags=HyundaiFlags.EV | HyundaiFlags.CANFD_ANGLE_STEERING, + radar_dbc=HYUNDAI_MRR35_RADAR_DBC, + ) HYUNDAI_IONIQ_6 = HyundaiCanFDPlatformConfig( [ HyundaiCarDocs("Hyundai Ioniq 6 (without HDA II) 2023-24", "Highway Driving Assist", car_parts=CarParts.common([CarHarness.hyundai_l])), @@ -372,6 +523,7 @@ class CAR(Platforms): ], HYUNDAI_IONIQ_5.specs, flags=HyundaiFlags.EV | HyundaiFlags.CANFD_NO_RADAR_DISABLE, + radar_dbc=HYUNDAI_MRR35_RADAR_DBC, ) HYUNDAI_TUCSON_4TH_GEN = HyundaiCanFDPlatformConfig( [ @@ -393,6 +545,47 @@ class CAR(Platforms): flags=HyundaiFlags.CHECKSUM_CRC8, ) + HYUNDAI_KONA_2ND_GEN = HyundaiCanFDPlatformConfig( + [HyundaiCarDocs("Hyundai Kona (without HDA II) 2024-25", car_parts=CarParts.common([CarHarness.hyundai_l]))], + CarSpecs(mass=1590, wheelbase=2.66, steerRatio=13.6, tireStiffnessFactor=0.385), + flags=HyundaiFlags.CCNC, + ) + HYUNDAI_KONA_HEV_2ND_GEN = HyundaiCanFDPlatformConfig( + [HyundaiCarDocs("Hyundai Kona Hybrid (without HDA II) 2024", car_parts=CarParts.common([CarHarness.hyundai_l]))], + CarSpecs(mass=1590, wheelbase=2.66, steerRatio=13.6, tireStiffnessFactor=0.385), + flags=HyundaiFlags.CCNC, + ) + HYUNDAI_SANTA_CRUZ_2025 = HyundaiCanFDPlatformConfig( + [HyundaiCarDocs("Hyundai Santa Cruz (without HDA II) 2025", car_parts=CarParts.common([CarHarness.hyundai_n]))], + CarSpecs(mass=1920, wheelbase=3, steerRatio=14.2), + flags=HyundaiFlags.CCNC, + ) + HYUNDAI_SONATA_2024 = HyundaiCanFDPlatformConfig( + [HyundaiCarDocs("Hyundai Sonata (without HDA II) 2024-25", car_parts=CarParts.common([CarHarness.hyundai_a]))], + CarSpecs(mass=1556, wheelbase=2.84, steerRatio=12.81), + flags=HyundaiFlags.CCNC, + ) + HYUNDAI_SONATA_HEV_2024 = HyundaiCanFDPlatformConfig( + [HyundaiCarDocs("Hyundai Sonata Hybrid (without HDA II) 2024-25", car_parts=CarParts.common([CarHarness.hyundai_a]))], + CarSpecs(mass=1616, wheelbase=2.84, steerRatio=13.27), + flags=HyundaiFlags.CCNC, + ) + HYUNDAI_TUCSON_2025 = HyundaiCanFDPlatformConfig( + [HyundaiCarDocs("Hyundai Tucson (without HDA II) 2025-26", car_parts=CarParts.common([CarHarness.hyundai_n]))], + CarSpecs(mass=1630, wheelbase=2.756, steerRatio=13.7, tireStiffnessFactor=0.385), + flags=HyundaiFlags.CCNC, + ) + HYUNDAI_TUCSON_HEV_2025 = HyundaiCanFDPlatformConfig( + [HyundaiCarDocs("Hyundai Tucson Hybrid (without HDA II) 2025-26", car_parts=CarParts.common([CarHarness.hyundai_n]))], + CarSpecs(mass=1630, wheelbase=2.756, steerRatio=13.7, tireStiffnessFactor=0.385), + flags=HyundaiFlags.CCNC, + ) + HYUNDAI_TUCSON_PHEV_2025 = HyundaiCanFDPlatformConfig( + [HyundaiCarDocs("Hyundai Tucson Plug-in Hybrid (without HDA II) 2025", car_parts=CarParts.common([CarHarness.hyundai_n]))], + HYUNDAI_TUCSON_HEV_2025.specs, + flags=HyundaiFlags.CCNC, + ) + # Kia KIA_FORTE = HyundaiPlatformConfig( [ @@ -401,11 +594,25 @@ class CAR(Platforms): ], CarSpecs(mass=2878 * CV.LB_TO_KG, wheelbase=2.8, steerRatio=13.75, tireStiffnessFactor=0.5) ) + KIA_K4_2025 = HyundaiCanFDPlatformConfig( + [ + HyundaiCarDocs("Kia K4 (without HDA II) 2025", car_parts=CarParts.common([CarHarness.hyundai_a])), + HyundaiCarDocs("Kia K4 (without HDA II) 2026", car_parts=CarParts.common([CarHarness.hyundai_a])), + HyundaiCarDocs("Kia K4 (with HDA II) 2025", car_parts=CarParts.common([CarHarness.hyundai_r])), + ], + CarSpecs(mass=2987 * CV.LB_TO_KG, wheelbase=2.72, steerRatio=13.4), + flags=HyundaiFlags.CCNC, + ) KIA_K5_2021 = HyundaiPlatformConfig( [HyundaiCarDocs("Kia K5 2021-24", car_parts=CarParts.common([CarHarness.hyundai_a]))], CarSpecs(mass=3381 * CV.LB_TO_KG, wheelbase=2.85, steerRatio=13.27, tireStiffnessFactor=0.5), # 2021 Kia K5 Steering Ratio (all trims) flags=HyundaiFlags.CHECKSUM_CRC8, ) + KIA_K5_2025 = HyundaiCanFDPlatformConfig( + [HyundaiCarDocs("Kia K5 (without HDA II) 2025", car_parts=CarParts.common([CarHarness.hyundai_m]))], + CarSpecs(mass=3230 * CV.LB_TO_KG, wheelbase=2.85, steerRatio=13.27), + flags=HyundaiFlags.CCNC, + ) KIA_K5_HEV_2020 = HyundaiPlatformConfig( [HyundaiCarDocs("Kia K5 Hybrid 2020-22", car_parts=CarParts.common([CarHarness.hyundai_a]))], KIA_K5_2021.specs, @@ -503,6 +710,16 @@ class CAR(Platforms): # weight from SX and above trims, average of FWD and AWD version, steering ratio according to Kia News https://www.kiamedia.com/us/en/models/sportage/2023/specifications CarSpecs(mass=1725, wheelbase=2.756, steerRatio=13.6), ) + KIA_SPORTAGE_2026 = HyundaiCanFDPlatformConfig( + [HyundaiCarDocs("Kia Sportage (without HDA II) 2026", car_parts=CarParts.common([CarHarness.hyundai_n]))], + CarSpecs(mass=1735, wheelbase=2.756, steerRatio=13.7), + flags=HyundaiFlags.CANFD_ANGLE_STEERING | HyundaiFlags.CCNC, + ) + KIA_SPORTAGE_HEV_2026 = HyundaiCanFDPlatformConfig( + [HyundaiCarDocs("Kia Sportage Hybrid 2026", car_parts=CarParts.common([CarHarness.hyundai_n]))], + CarSpecs(mass=1812, wheelbase=2.756, steerRatio=13.7), + flags=HyundaiFlags.CANFD_ANGLE_STEERING, + ) KIA_SORENTO = HyundaiPlatformConfig( [ HyundaiCarDocs("Kia Sorento 2018", "Advanced Smart Cruise Control & LKAS", video="https://www.youtube.com/watch?v=Fkh3s6WHJz8", @@ -517,6 +734,11 @@ class CAR(Platforms): CarSpecs(mass=3957 * CV.LB_TO_KG, wheelbase=2.81, steerRatio=13.5), # average of the platforms flags=HyundaiFlags.CANFD_RADAR_SCC, ) + KIA_SORENTO_2024 = HyundaiCanFDPlatformConfig( + [HyundaiCarDocs("Kia Sorento (without HDA II) 2024-25", car_parts=CarParts.common([CarHarness.hyundai_a]))], + KIA_SORENTO_4TH_GEN.specs, + flags=HyundaiFlags.CCNC, + ) KIA_SORENTO_HEV_4TH_GEN = HyundaiCanFDPlatformConfig( [ HyundaiCarDocs("Kia Sorento Hybrid 2021-23", "All", car_parts=CarParts.common([CarHarness.hyundai_a])), @@ -525,6 +747,11 @@ class CAR(Platforms): CarSpecs(mass=4395 * CV.LB_TO_KG, wheelbase=2.81, steerRatio=13.5), # average of the platforms flags=HyundaiFlags.CANFD_RADAR_SCC, ) + KIA_SORENTO_HEV_4TH_GEN_LFA2 = HyundaiCanFDPlatformConfig( + [HyundaiCarDocs("Kia Sorento Hybrid 2026", "All", car_parts=CarParts.common([CarHarness.hyundai_q]))], + CarSpecs(mass=1970, wheelbase=2.814, steerRatio=13.27, tireStiffnessFactor=0.65), + flags=HyundaiFlags.CANFD_ANGLE_STEERING, + ) KIA_STINGER = HyundaiPlatformConfig( [HyundaiCarDocs("Kia Stinger 2018-20", video="https://www.youtube.com/watch?v=MJ94qoofYw0", car_parts=CarParts.common([CarHarness.hyundai_c]))], @@ -539,6 +766,11 @@ class CAR(Platforms): CarSpecs(mass=1450, wheelbase=2.65, steerRatio=13.75, tireStiffnessFactor=0.5), flags=HyundaiFlags.LEGACY, ) + KIA_XCEED_PHEV = HyundaiPlatformConfig( + [HyundaiCarDocs("Kia XCeed Plug-in Hybrid 2021", car_parts=CarParts.common([CarHarness.hyundai_b]))], + CarSpecs(mass=1650, wheelbase=2.65, steerRatio=13.75, tireStiffnessFactor=0.5), + flags=HyundaiFlags.LEGACY | HyundaiFlags.HYBRID | HyundaiFlags.MANDO_RADAR, + ) KIA_CEED_PHEV = HyundaiPlatformConfig( [HyundaiCarDocs("Kia Ceed Plug-in Hybrid 2021", car_parts=CarParts.common([CarHarness.hyundai_b]))], CarSpecs(mass=1450, wheelbase=2.65, steerRatio=13.75, tireStiffnessFactor=0.5), @@ -553,6 +785,22 @@ class CAR(Platforms): CarSpecs(mass=2055, wheelbase=2.9, steerRatio=16, tireStiffnessFactor=0.65), flags=HyundaiFlags.EV, ) + KIA_EV6_2025 = HyundaiCanFDPlatformConfig( + [ + HyundaiCarDocs("Kia EV6 (with HDA I) 2025", "Highway Driving Assist I", car_parts=CarParts.common([CarHarness.hyundai_p])) + ], + CarSpecs(mass=2055, wheelbase=2.9, steerRatio=14.26, tireStiffnessFactor=0.65), + flags=HyundaiFlags.EV | HyundaiFlags.CANFD_ANGLE_STEERING, + radar_dbc=HYUNDAI_MRR30_RADAR_DBC, + ) + KIA_EV9 = HyundaiCanFDPlatformConfig( + [ + HyundaiCarDocs("Kia EV9 2025-26", car_parts=CarParts.common([CarHarness.hyundai_r])) + ], + CarSpecs(mass=2664, wheelbase=3.1, steerRatio=16), + flags=HyundaiFlags.EV | HyundaiFlags.CANFD_ANGLE_STEERING | HyundaiFlags.CCNC, + radar_dbc=HYUNDAI_MRR35_RADAR_DBC, + ) KIA_CARNIVAL_4TH_GEN = HyundaiCanFDPlatformConfig( [ HyundaiCarDocs("Kia Carnival 2022-24", car_parts=CarParts.common([CarHarness.hyundai_a])), @@ -561,6 +809,24 @@ class CAR(Platforms): CarSpecs(mass=2087, wheelbase=3.09, steerRatio=14.23), flags=HyundaiFlags.CANFD_RADAR_SCC, ) + KIA_CARNIVAL_2025 = HyundaiCanFDPlatformConfig( + [ + HyundaiCarDocs("Kia Carnival 2025", car_parts=CarParts.common([CarHarness.hyundai_k])), + HyundaiCarDocs("Kia Carnival (with HDA II) 2025", "Highway Driving Assist II", car_parts=CarParts.common([CarHarness.hyundai_q])), + ], + KIA_CARNIVAL_4TH_GEN.specs, + flags=HyundaiFlags.CCNC, + ) + KIA_CARNIVAL_HEV_4TH_GEN = HyundaiCanFDPlatformConfig( + [ + HyundaiCarDocs("Kia Carnival Hybrid 2025", car_parts=CarParts.common([CarHarness.hyundai_k])), + HyundaiCarDocs("Kia Carnival Hybrid 2026", car_parts=CarParts.common([CarHarness.hyundai_a])), + HyundaiCarDocs("Kia Carnival Hybrid (with HDA II) 2025-26", "Highway Driving Assist II", + car_parts=CarParts.common([CarHarness.hyundai_q])), + ], + CarSpecs(mass=2253, wheelbase=3.09, steerRatio=14.23), + flags=HyundaiFlags.CCNC, + ) # Genesis GENESIS_GV60_EV_1ST_GEN = HyundaiCanFDPlatformConfig( @@ -602,6 +868,20 @@ class CAR(Platforms): ], CarSpecs(mass=2260, wheelbase=2.87, steerRatio=17.1), flags=HyundaiFlags.EV, + radar_dbc=HYUNDAI_GV70_RADAR_DBC, + ) + GENESIS_GV70_ELECTRIFIED_2ND_GEN = HyundaiCanFDPlatformConfig( + [HyundaiCarDocs("Genesis GV70 Electrified 2026", "All", car_parts=CarParts.common([CarHarness.hyundai_m]))], + GENESIS_GV70_ELECTRIFIED_1ST_GEN.specs, + flags=HyundaiFlags.EV | HyundaiFlags.CANFD_ANGLE_STEERING, + radar_dbc=HYUNDAI_MRR35_RADAR_DBC, + ) + GENESIS_GV70_2026 = HyundaiCanFDPlatformConfig( + [HyundaiCarDocs("Genesis GV70 (3.5T Sport Prestige Trim, with HDA II & LFA2) 2026", + "Highway Driving Assist II & Lane Follow Assist 2", car_parts=CarParts.common([CarHarness.hyundai_m]))], + GENESIS_GV70_1ST_GEN.specs, + flags=HyundaiFlags.CANFD_ANGLE_STEERING, + radar_dbc=HYUNDAI_MRR35_RADAR_DBC, ) GENESIS_G80 = HyundaiPlatformConfig( [HyundaiCarDocs("Genesis G80 2018-19", "All", car_parts=CarParts.common([CarHarness.hyundai_h]))], @@ -615,12 +895,80 @@ class CAR(Platforms): GENESIS_G90 = HyundaiPlatformConfig( [HyundaiCarDocs("Genesis G90 2017-20", "All", car_parts=CarParts.common([CarHarness.hyundai_c]))], CarSpecs(mass=2200, wheelbase=3.15, steerRatio=12.069), + flags=HyundaiFlags.MANDO_RADAR, + dbc_dict={Bus.pt: "hyundai_can_generated", Bus.radar: HYUNDAI_G90_RADAR_DBC}, ) GENESIS_GV80 = HyundaiCanFDPlatformConfig( [HyundaiCarDocs("Genesis GV80 2023", "All", car_parts=CarParts.common([CarHarness.hyundai_m]))], CarSpecs(mass=2258, wheelbase=2.95, steerRatio=14.14), flags=HyundaiFlags.CANFD_RADAR_SCC, ) + GENESIS_GV80_2025 = HyundaiCanFDPlatformConfig( + [HyundaiCarDocs("Genesis GV80 (3.5T Prestige Trim, with HDA II & LFA2) 2025", + "Highway Driving Assist II & Lane Follow Assist 2", car_parts=CarParts.common([CarHarness.hyundai_q])), + HyundaiCarDocs("Genesis GV80 Coupe (with HDA II & LFA2) 2025", + "Highway Driving Assist II & Lane Follow Assist 2", car_parts=CarParts.common([CarHarness.hyundai_q]))], + GENESIS_GV80.specs, + flags=HyundaiFlags.CANFD_ANGLE_STEERING, + radar_dbc=HYUNDAI_MRR35_RADAR_DBC, + ) + + HYUNDAI_BAYON_1ST_GEN_NON_SCC = HyundaiNonSccPlatformConfig( + [HyundaiNonSccCarDocs("Hyundai Bayon Non-SCC 2021", car_parts=CarParts.common([CarHarness.hyundai_n]))], + CarSpecs(mass=1150, wheelbase=2.58, steerRatio=13.27 * 1.15), + flags=HyundaiFlags.CHECKSUM_CRC8, + ) + HYUNDAI_ELANTRA_2022_NON_SCC = HyundaiNonSccPlatformConfig( + [HyundaiNonSccCarDocs("Hyundai Elantra Non-SCC 2022", car_parts=CarParts.common([CarHarness.hyundai_k]))], + HYUNDAI_ELANTRA_2021.specs, + flags=HyundaiFlags.CHECKSUM_CRC8, + ) + HYUNDAI_ELANTRA_HEV_2022_NON_SCC = HyundaiNonSccPlatformConfig( + [HyundaiNonSccCarDocs("Hyundai Elantra Hybrid Non-SCC 2022", car_parts=CarParts.common([CarHarness.hyundai_k]))], + HYUNDAI_ELANTRA_HEV_2021.specs, + flags=HyundaiFlags.CHECKSUM_CRC8 | HyundaiFlags.HYBRID, + ) + HYUNDAI_KONA_NON_SCC = HyundaiNonSccPlatformConfig( + [HyundaiNonSccCarDocs("Hyundai Kona Non-SCC 2019", car_parts=CarParts.common([CarHarness.hyundai_b]))], + HYUNDAI_KONA.specs, + flags=HyundaiFlags.ALT_LIMITS, + radar_dbc=HYUNDAI_MRREVO14F_RADAR_DBC, + ) + HYUNDAI_KONA_EV_NON_SCC = HyundaiNonSccPlatformConfig( + [HyundaiNonSccCarDocs("Hyundai Kona Electric Non-SCC 2019", car_parts=CarParts.common([CarHarness.hyundai_g]))], + HYUNDAI_KONA_EV.specs, + flags=HyundaiFlags.EV | HyundaiFlags.ALT_LIMITS, + ) + KIA_RAY_EV = HyundaiNonSccPlatformConfig( + [HyundaiNonSccCarDocs("Kia Ray EV 2025", package="Non-SCC with comma pedal (6-byte sensor) and 8-byte LFA; other variants dashcam only", + car_parts=CarParts.common([CarHarness.hyundai_h]))], + CarSpecs(mass=1295, wheelbase=2.52, steerRatio=14.5), + flags=HyundaiFlags.EV | HyundaiFlags.CHECKSUM_CRC8, + ) + KIA_CEED_PHEV_2022_NON_SCC = HyundaiNonSccPlatformConfig( + [HyundaiNonSccCarDocs("Kia Ceed Plug-in Hybrid Non-SCC 2022", car_parts=CarParts.common([CarHarness.hyundai_i]))], + CarSpecs(mass=1650, wheelbase=2.65, steerRatio=13.75, tireStiffnessFactor=0.5), + flags=HyundaiFlags.HYBRID, + ) + KIA_FORTE_2019_NON_SCC = HyundaiNonSccPlatformConfig( + [HyundaiNonSccCarDocs("Kia Forte Non-SCC 2019", car_parts=CarParts.common([CarHarness.hyundai_g]))], + KIA_FORTE.specs, + flags=HyundaiFlags.NON_SCC_NO_FCA, + ) + KIA_FORTE_2021_NON_SCC = HyundaiNonSccPlatformConfig( + [HyundaiNonSccCarDocs("Kia Forte Non-SCC 2021", car_parts=CarParts.common([CarHarness.hyundai_g]))], + KIA_FORTE.specs, + ) + KIA_SELTOS_2023_NON_SCC = HyundaiNonSccPlatformConfig( + [HyundaiNonSccCarDocs("Kia Seltos Non-SCC 2023-24", car_parts=CarParts.common([CarHarness.hyundai_l]))], + KIA_SELTOS.specs, + flags=HyundaiFlags.CHECKSUM_CRC8, + ) + GENESIS_G70_2021_NON_SCC = HyundaiNonSccPlatformConfig( + [HyundaiNonSccCarDocs("Genesis G70 Non-SCC 2021", car_parts=CarParts.common([CarHarness.hyundai_f]))], + GENESIS_G70_2020.specs, + flags=HyundaiFlags.CHECKSUM_CRC8 | HyundaiFlags.NON_SCC_RADAR_FCA, + ) class Buttons: @@ -799,3 +1147,21 @@ FW_QUERY_CONFIG = FwQueryConfig( ) DBC = CAR.create_dbc_map() + + +def is_blended(CP) -> bool: + """Exact mixed classic-PT/CANFD steering identity; no spare UInt32 flag.""" + return CP.carFingerprint == CAR.HYUNDAI_PALISADE_2023 + + +def is_blended_alpha(CP) -> bool: + """Exact CP-selected alpha namespace; startup admission remains separately disabled.""" + if (not is_blended(CP) or not CP.openpilotLongitudinalControl or CP.pcmCruise or CP.passive or + CP.dashcamOnly or CP.notCar or len(CP.safetyConfigs) != 1 or + CP.flags & (HyundaiFlags.CANFD | HyundaiFlags.LEGACY | HyundaiFlags.UNSUPPORTED_LONGITUDINAL | HyundaiFlags.NON_SCC)): + return False + hdaii = bool(CP.flags & HyundaiFlags.CANFD_LKA_STEER_MSG) + if not hdaii and CP.flags & HyundaiFlags.USE_FCA: + return False + safety = CP.safetyConfigs[0] + return safety.safetyModel == CarParams.SafetyModel.hyundai and safety.safetyParam == (0x2014 if hdaii else 0x2004) diff --git a/opendbc_repo/opendbc/dbc/generator/hyundai/_hyundai_palisade_2023_canfd_common.dbc b/opendbc_repo/opendbc/dbc/generator/hyundai/_hyundai_palisade_2023_canfd_common.dbc new file mode 100644 index 0000000000..19751557b8 --- /dev/null +++ b/opendbc_repo/opendbc/dbc/generator/hyundai/_hyundai_palisade_2023_canfd_common.dbc @@ -0,0 +1,232 @@ +VERSION "" + + +NS_ : + NS_DESC_ + CM_ + BA_DEF_ + BA_ + VAL_ + CAT_DEF_ + CAT_ + FILTER + BA_DEF_DEF_ + EV_DATA_ + ENVVAR_DATA_ + SGTYPE_ + SGTYPE_VAL_ + BA_DEF_SGTYPE_ + BA_SGTYPE_ + SIG_TYPE_REF_ + VAL_TABLE_ + SIG_GROUP_ + SIG_VALTYPE_ + SIGTYPE_VALTYPE_ + BO_TX_BU_ + BA_DEF_REL_ + BA_REL_ + BA_DEF_DEF_REL_ + BU_SG_REL_ + BU_EV_REL_ + BU_BO_REL_ + SG_MUL_VAL_ + +BS_: + +BU_: XXX CAMERA FRONT_RADAR ADRV APRK + + +BO_ 80 LKAS: 16 XXX + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ LKA_MODE : 24|3@1+ (1,0) [0|7] "" XXX + SG_ LKA_AVAILABLE : 27|2@1+ (1,0) [0|255] "" XXX + SG_ LKA_WARNING : 32|1@1+ (1,0) [0|1] "" XXX + SG_ LKA_ICON : 38|2@1+ (1,0) [0|255] "" XXX + SG_ FCA_SYSWARN : 40|1@0+ (1,0) [0|1] "" XXX + SG_ TORQUE_REQUEST : 41|11@1+ (1,-1024) [0|4095] "" XXX + SG_ STEER_REQ : 52|1@1+ (1,0) [0|1] "" XXX + SG_ LFA_BUTTON : 56|1@1+ (1,0) [0|255] "" XXX + SG_ LKA_ASSIST : 62|1@1+ (1,0) [0|1] "" XXX + SG_ STEER_MODE : 65|3@1+ (1,0) [0|1] "" XXX + SG_ NEW_SIGNAL_2 : 70|2@0+ (1,0) [0|3] "" XXX + SG_ HAS_LANE_SAFETY : 80|1@0+ (1,0) [0|1] "" XXX + SG_ DAMP_FACTOR : 64|8@1+ (1,0) [0|255] "" XXX + +BO_ 81 ADRV_0x51: 32 ADRV + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 384 CAM_0x180: 32 CAMERA + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 385 CAM_0x181: 32 CAMERA + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 386 CAM_0x182: 32 CAMERA + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 387 CAM_0x183: 32 CAMERA + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 388 CAM_0x184: 32 CAMERA + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 389 CAM_0x185: 8 CAMERA + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 438 CAM_0x1b6: 32 CAMERA + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 439 CAM_0x1b7: 32 CAMERA + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 440 CAM_0x1b8: 32 CAMERA + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 441 CAM_0x1b9: 32 CAMERA + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 513 RADAR_0x201: 32 FRONT_RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 528 RADAR_0x210: 32 FRONT_RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 529 RADAR_0x211: 32 FRONT_RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 530 RADAR_0x212: 32 FRONT_RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 531 RADAR_0x213: 32 FRONT_RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 532 RADAR_0x214: 32 FRONT_RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 533 RADAR_0x215: 32 FRONT_RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 534 RADAR_0x216: 32 FRONT_RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 535 RADAR_0x217: 32 FRONT_RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 536 RADAR_0x218: 32 FRONT_RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 537 RADAR_0x219: 32 FRONT_RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 538 RADAR_0x21a: 32 FRONT_RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 539 RADAR_0x21b: 32 FRONT_RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 540 RADAR_0x21c: 32 FRONT_RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 541 RADAR_0x21d: 32 FRONT_RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 542 RADAR_0x21e: 32 FRONT_RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 543 RADAR_0x21f: 32 FRONT_RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 576 RADAR_0x240: 16 FRONT_RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 674 CAM_0x2a2: 32 CAMERA + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 675 CAM_0x2a3: 32 CAMERA + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 676 CAM_0x2a4: 24 XXX + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE3 : 24|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE4 : 32|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE5 : 40|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE6 : 48|8@1+ (1,0) [0|255] "" XXX + SG_ LEFT_LANE_LINE : 56|2@1+ (1,0) [0|3] "" XXX + SG_ SET_ME_0 : 58|2@1+ (1,0) [0|3] "" XXX + SG_ RIGHT_LANE_LINE : 60|2@1+ (1,0) [0|3] "" XXX + SG_ SET_ME_0_2 : 62|2@1+ (1,0) [0|3] "" XXX + SG_ BYTE8 : 64|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE9 : 72|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE10 : 80|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE11 : 88|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE12 : 96|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE13 : 104|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE14 : 112|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE15 : 120|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE16 : 128|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE17 : 136|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE18 : 144|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE19 : 152|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE20 : 160|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE21 : 168|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE22 : 176|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE23 : 184|8@1+ (1,0) [0|255] "" XXX + +BO_ 699 CAM_0x2bb: 32 CAMERA + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 700 CAM_0x2bc: 32 CAMERA + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 701 CAM_0x2bd: 32 CAMERA + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +BO_ 702 CAM_0x2be: 32 CAMERA + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + +CM_ BO_ 676 "Contains signals with detailed lane line information. Used by ADAS ECU on HDA 2 vehicles to operate LFA."; + +CM_ SG_ 80 HAS_LANE_SAFETY "If 0, hides LKAS 'Lane Safety' menu from vehicle settings"; +CM_ SG_ 676 LEFT_LANE_LINE "Left lane line confidence"; +CM_ SG_ 676 RIGHT_LANE_LINE "Right lane line confidence"; +VAL_ 80 LKA_ICON 0 "hidden" 1 "grey" 2 "green" 3 "flashing green" ; +VAL_ 80 LKA_MODE 1 "warning only" 2 "assist" 6 "off" ; +VAL_ 676 LEFT_LANE_LINE 0 "Not Detected" 1 "Low Confidence" 2 "Medium Confidence" 3 "High Confidence"; +VAL_ 676 RIGHT_LANE_LINE 0 "Not Detected" 1 "Low Confidence" 2 "Medium Confidence" 3 "High Confidence"; diff --git a/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_can.dbc b/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_can.dbc index f94511ad42..0ac84f10f0 100644 --- a/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_can.dbc +++ b/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_can.dbc @@ -1480,6 +1480,7 @@ BO_ 905 SCC14: 8 SCC SG_ JerkLowerLimit : 19|7@1+ (0.1,0) [0|12.7] "m/s^3" ESC SG_ ACCMode : 32|3@1+ (1,0) [0|7] "" CLU,HUD,LDWS_LKAS,ESC SG_ ObjGap : 56|8@1+ (1,0) [0|255] "" CLU,HUD,ESC + SG_ ObjDistStat : 42|2@1+ (1,0) [0|3] "" XXX BO_ 1157 LFAHDA_MFC: 4 XXX SG_ HDA_USM : 0|2@1+ (1,0) [0|3] "" XXX @@ -1500,6 +1501,7 @@ BO_ 913 BCM_PO_11: 8 Vector__XXX BO_ 1426 LABEL11: 8 XXX SG_ CC_React : 34|1@1+ (1,0) [0|1] "" XXX + SG_ CC_Engaged : 35|1@1+ (1,0) [0|1] "" XXX BO_ 910 WHL_SPD12_FS: 5 iBAU SG_ CRC : 0|8@1+ (1,0) [0|0] "" Vector__XXX @@ -1536,6 +1538,10 @@ BO_ 881 E_EMS11: 8 XXX BO_ 1355 EV_PC6: 8 CGW SG_ CF_Vcu_SbwWarnMsg : 16|3@1+ (1,0) [0|7] "" Vector__XXX +BO_ 1429 E_CRUISE_CONTROL: 8 XXX + SG_ CRUISE_LAMP_M : 50|1@0+ (1,0) [0|1] "" XXX + SG_ CRUISE_LAMP_S : 51|1@0+ (1,0) [0|1] "" XXX + BO_ 1430 EV_PC2: 8 CGW SG_ CR_Ldc_ActVol_LS_V : 32|8@1+ (0.1,0) [0|0] "V" Vector__XXX diff --git a/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_can_refresh.py b/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_can_refresh.py new file mode 100644 index 0000000000..16d301189e --- /dev/null +++ b/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_can_refresh.py @@ -0,0 +1,13 @@ +# ruff: noqa: INP001 +from pathlib import Path + + +GENERIC_LFAHDA = "BO_ 1157 LFAHDA_MFC: 4 XXX" +REFRESH_LFAHDA = "BO_ 1157 LFAHDA_MFC: 8 XXX" + + +def generate() -> dict[str, str]: + source = Path(__file__).with_name("hyundai_can.dbc").read_text(encoding="utf-8") + if source.count(GENERIC_LFAHDA) != 1: + raise RuntimeError("expected exactly one 4-byte LFAHDA_MFC definition") + return {"hyundai_can_refresh.dbc": source.replace(GENERIC_LFAHDA, REFRESH_LFAHDA, 1)} diff --git a/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_canfd.dbc b/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_canfd.dbc index 3687b76291..6b0e1ddb5d 100644 --- a/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_canfd.dbc +++ b/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_canfd.dbc @@ -1,5 +1,9 @@ CM_ "IMPORT _hyundai_canfd_common.dbc"; +BO_ 1096 STEERING_WHEEL_MEDIA_BUTTONS: 8 XXX + SG_ MODE_BUTTON : 22|1@0+ (1,0) [0|1] "" XXX + SG_ CUSTOM_BUTTON : 44|1@0+ (1,0) [0|1] "" XXX + BO_ 53 ACCELERATOR: 32 XXX SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX @@ -689,6 +693,7 @@ BO_ 866 CAM_0x362: 32 CAMERA BO_ 874 BLINDSPOTS_FRONT_CORNER_2: 16 XXX SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ SIDE_DETECT_STATE : 24|8@1+ (1,0) [0|255] "" XXX BO_ 961 BLINKER_STALKS: 8 XXX SG_ CHECKSUM_MAYBE : 7|8@0+ (1,0) [0|255] "" XXX diff --git a/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_kia_mando_front_radar.py b/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_kia_mando_front_radar.py index 78c5410caf..5e4c6de3f8 100755 --- a/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_kia_mando_front_radar.py +++ b/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_kia_mando_front_radar.py @@ -58,4 +58,10 @@ BO_ {a} RADAR_TRACK_{a:x}: 8 RADAR SG_ STATE_2 : 55|2@0+ (1,0) [0|3] "" XXX """) - return {"hyundai_kia_mando_front_radar.dbc": "".join(parts)} + ordinary = "".join(parts) + extended_parts = list(parts) + template = parts[1] + for address in range(0x520, 0x540): + extended_parts.append(template.replace("BO_ 1280 RADAR_TRACK_500:", f"BO_ {address} RADAR_TRACK_{address:x}:")) + return {"hyundai_kia_mando_front_radar.dbc": ordinary, + "hyundai_genesis_g90_radar.dbc": "".join(extended_parts)} diff --git a/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_mrr30_radar.py b/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_mrr30_radar.py new file mode 100644 index 0000000000..da64001044 --- /dev/null +++ b/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_mrr30_radar.py @@ -0,0 +1,71 @@ +from io import StringIO + + +def generate() -> dict[str, str]: + f = StringIO() + f.write(""" +VERSION "" + + +NS_ : + NS_DESC_ + CM_ + BA_DEF_ + BA_ + VAL_ + CAT_DEF_ + CAT_ + FILTER + BA_DEF_DEF_ + EV_DATA_ + ENVVAR_DATA_ + SGTYPE_ + SGTYPE_VAL_ + BA_DEF_SGTYPE_ + BA_SGTYPE_ + SIG_TYPE_REF_ + VAL_TABLE_ + SIG_GROUP_ + SIG_VALTYPE_ + SIGTYPE_VALTYPE_ + BO_TX_BU_ + BA_DEF_REL_ + BA_REL_ + BA_DEF_DEF_REL_ + BU_SG_REL_ + BU_EV_REL_ + BU_BO_REL_ + SG_MUL_VAL_ + +BS_: + +BU_: XXX +""") + + for a in range(0x210, 0x210 + 16): + f.write(f""" +BO_ {a} RADAR_TRACK_{a:x}: 32 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ 1_COUNTER_255 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ 1_STATE_ALT : 51|4@0+ (1,0) [0|15] "" XXX + SG_ 1_STATE : 55|4@0+ (1,0) [0|15] "" XXX + SG_ 1_NEW_SIGNAL_3 : 63|8@0- (1,0) [0|255] "" XXX + SG_ 1_LONG_DIST : 64|12@1+ (0.05,0) [0|4095] "" XXX + SG_ 1_LAT_DIST : 76|12@1- (0.05,0) [0|4095] "" XXX + SG_ 1_REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ 1_NEW_SIGNAL_1 : 102|2@1+ (1,0) [0|3] "" XXX + SG_ 1_LAT_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ 1_REL_ACCEL : 118|10@1- (1,0) [0|1023] "" XXX + SG_ 2_COUNTER_255 : 175|8@0+ (1,0) [0|255] "" XXX + SG_ 2_STATE_ALT : 179|4@0+ (1,0) [0|15] "" XXX + SG_ 2_STATE : 183|4@0+ (1,0) [0|15] "" XXX + SG_ 2_NEW_SIGNAL_3 : 191|8@0- (1,0) [0|255] "" XXX + SG_ 2_LONG_DIST : 192|12@1+ (0.05,0) [0|4095] "" XXX + SG_ 2_LAT_DIST : 204|12@1- (0.05,0) [0|4095] "" XXX + SG_ 2_REL_SPEED : 216|14@1- (0.01,0) [0|65535] "" XXX + SG_ 2_NEW_SIGNAL_1 : 230|2@1+ (1,0) [0|3] "" XXX + SG_ 2_LAT_ACCEL : 232|13@1- (1,0) [0|8191] "" XXX + SG_ 2_REL_ACCEL : 246|10@1- (1,0) [0|1023] "" XXX +""") + return {"hyundai_mrr30_radar.dbc": f.getvalue()} diff --git a/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_mrr35_radar.dbc b/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_mrr35_radar.dbc new file mode 100644 index 0000000000..fb5b8b29d9 --- /dev/null +++ b/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_mrr35_radar.dbc @@ -0,0 +1,997 @@ + +VERSION "" + + +NS_ : + NS_DESC_ + CM_ + BA_DEF_ + BA_ + VAL_ + CAT_DEF_ + CAT_ + FILTER + BA_DEF_DEF_ + EV_DATA_ + ENVVAR_DATA_ + SGTYPE_ + SGTYPE_VAL_ + BA_DEF_SGTYPE_ + BA_SGTYPE_ + SIG_TYPE_REF_ + VAL_TABLE_ + SIG_GROUP_ + SIG_VALTYPE_ + SIGTYPE_VALTYPE_ + BO_TX_BU_ + BA_DEF_REL_ + BA_REL_ + BA_DEF_DEF_REL_ + BU_SG_REL_ + BU_EV_REL_ + BU_BO_REL_ + SG_MUL_VAL_ + +BS_: + +BU_: XXX + +BO_ 933 RADAR_TRACK_3a5: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 934 RADAR_TRACK_3a6: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 935 RADAR_TRACK_3a7: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 936 RADAR_TRACK_3a8: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 937 RADAR_TRACK_3a9: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 938 RADAR_TRACK_3aa: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 939 RADAR_TRACK_3ab: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 940 RADAR_TRACK_3ac: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 941 RADAR_TRACK_3ad: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 942 RADAR_TRACK_3ae: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 943 RADAR_TRACK_3af: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 944 RADAR_TRACK_3b0: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 945 RADAR_TRACK_3b1: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 946 RADAR_TRACK_3b2: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 947 RADAR_TRACK_3b3: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 948 RADAR_TRACK_3b4: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 949 RADAR_TRACK_3b5: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 950 RADAR_TRACK_3b6: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 951 RADAR_TRACK_3b7: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 952 RADAR_TRACK_3b8: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 953 RADAR_TRACK_3b9: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 954 RADAR_TRACK_3ba: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 955 RADAR_TRACK_3bb: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 956 RADAR_TRACK_3bc: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 957 RADAR_TRACK_3bd: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 958 RADAR_TRACK_3be: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 959 RADAR_TRACK_3bf: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 960 RADAR_TRACK_3c0: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 961 RADAR_TRACK_3c1: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 962 RADAR_TRACK_3c2: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 963 RADAR_TRACK_3c3: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX + +BO_ 964 RADAR_TRACK_3c4: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX diff --git a/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_mrr35_radar.py b/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_mrr35_radar.py new file mode 100644 index 0000000000..8fe80bd8e8 --- /dev/null +++ b/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_mrr35_radar.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +import os + +if __name__ == "__main__": + dbc_name = os.path.basename(__file__).replace(".py", ".dbc") + hyundai_path = os.path.dirname(os.path.realpath(__file__)) + with open(os.path.join(hyundai_path, dbc_name), "w", encoding="utf-8") as f: + f.write(""" +VERSION "" + + +NS_ : + NS_DESC_ + CM_ + BA_DEF_ + BA_ + VAL_ + CAT_DEF_ + CAT_ + FILTER + BA_DEF_DEF_ + EV_DATA_ + ENVVAR_DATA_ + SGTYPE_ + SGTYPE_VAL_ + BA_DEF_SGTYPE_ + BA_SGTYPE_ + SIG_TYPE_REF_ + VAL_TABLE_ + SIG_GROUP_ + SIG_VALTYPE_ + SIGTYPE_VALTYPE_ + BO_TX_BU_ + BA_DEF_REL_ + BA_REL_ + BA_DEF_DEF_REL_ + BU_SG_REL_ + BU_EV_REL_ + BU_BO_REL_ + SG_MUL_VAL_ + +BS_: + +BU_: XXX +""") + + for a in range(0x3A5, 0x3A5 + 32): + f.write(f""" +BO_ {a} RADAR_TRACK_{a:x}: 24 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX + SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX + SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX + SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX + SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX + SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX + SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX + SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_10 : 138|5@1+ (1,0) [0|31] "" XXX + SG_ NEW_SIGNAL_11 : 149|6@0+ (1,0) [0|63] "" XXX + SG_ NEW_SIGNAL_7 : 152|10@1+ (1,0) [0|1023] "" XXX + SG_ NEW_SIGNAL_9 : 162|9@1- (1,0) [0|511] "" XXX + SG_ NEW_SIGNAL_13 : 175|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_12 : 179|4@0+ (1,0) [0|15] "" XXX + SG_ NEW_SIGNAL_14 : 181|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_15 : 183|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_16 : 185|2@0+ (1,0) [0|3] "" XXX + SG_ NEW_SIGNAL_17 : 187|2@0+ (1,0) [0|3] "" XXX +""") diff --git a/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_mrrevo14f_radar.dbc b/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_mrrevo14f_radar.dbc new file mode 100644 index 0000000000..834c22bd91 --- /dev/null +++ b/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_mrrevo14f_radar.dbc @@ -0,0 +1,251 @@ + +VERSION "" + + +NS_ : + NS_DESC_ + CM_ + BA_DEF_ + BA_ + VAL_ + CAT_DEF_ + CAT_ + FILTER + BA_DEF_DEF_ + EV_DATA_ + ENVVAR_DATA_ + SGTYPE_ + SGTYPE_VAL_ + BA_DEF_SGTYPE_ + BA_SGTYPE_ + SIG_TYPE_REF_ + VAL_TABLE_ + SIG_GROUP_ + SIG_VALTYPE_ + SIGTYPE_VALTYPE_ + BO_TX_BU_ + BA_DEF_REL_ + BA_REL_ + BA_DEF_DEF_REL_ + BU_SG_REL_ + BU_EV_REL_ + BU_BO_REL_ + SG_MUL_VAL_ + +BS_: + +BU_: XXX + +BO_ 1537 RADAR_LEAD: 8 RADAR + SG_ DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ ID : 31|9@1+ (1,0) [0|511] "" XXX + SG_ SOMETHING_1 : 42|8@1+ (1,-128) [-128|127] "" XXX + SG_ SOMETHING_2 : 50|6@1+ (1,-32) [-32|31] "" XXX + SG_ COUNTER : 56|4@1+ (1,0) [0|15] "" XXX + SG_ CHECKSUM : 63|4@0+ (1,0) [0|15] "" XXX + +BO_ 1538 RADAR_TRACK_602: 8 RADAR + SG_ 1_DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 1_LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 1_SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ 2_DISTANCE : 31|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 2_LATERAL : 41|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 2_SPEED : 52|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ COUNTER : 62|2@1+ (1,0) [0|3] "" XXX + +BO_ 1539 RADAR_TRACK_603: 8 RADAR + SG_ 1_DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 1_LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 1_SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ 2_DISTANCE : 31|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 2_LATERAL : 41|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 2_SPEED : 52|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ COUNTER : 62|2@1+ (1,0) [0|3] "" XXX + +BO_ 1540 RADAR_TRACK_604: 8 RADAR + SG_ 1_DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 1_LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 1_SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ 2_DISTANCE : 31|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 2_LATERAL : 41|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 2_SPEED : 52|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ COUNTER : 62|2@1+ (1,0) [0|3] "" XXX + +BO_ 1541 RADAR_TRACK_605: 8 RADAR + SG_ 1_DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 1_LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 1_SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ 2_DISTANCE : 31|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 2_LATERAL : 41|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 2_SPEED : 52|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ COUNTER : 62|2@1+ (1,0) [0|3] "" XXX + +BO_ 1542 RADAR_TRACK_606: 8 RADAR + SG_ 1_DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 1_LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 1_SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ 2_DISTANCE : 31|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 2_LATERAL : 41|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 2_SPEED : 52|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ COUNTER : 62|2@1+ (1,0) [0|3] "" XXX + +BO_ 1543 RADAR_TRACK_607: 8 RADAR + SG_ 1_DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 1_LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 1_SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ 2_DISTANCE : 31|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 2_LATERAL : 41|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 2_SPEED : 52|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ COUNTER : 62|2@1+ (1,0) [0|3] "" XXX + +BO_ 1544 RADAR_TRACK_608: 8 RADAR + SG_ 1_DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 1_LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 1_SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ 2_DISTANCE : 31|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 2_LATERAL : 41|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 2_SPEED : 52|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ COUNTER : 62|2@1+ (1,0) [0|3] "" XXX + +BO_ 1545 RADAR_TRACK_609: 8 RADAR + SG_ 1_DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 1_LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 1_SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ 2_DISTANCE : 31|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 2_LATERAL : 41|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 2_SPEED : 52|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ COUNTER : 62|2@1+ (1,0) [0|3] "" XXX + +BO_ 1546 RADAR_TRACK_60a: 8 RADAR + SG_ 1_DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 1_LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 1_SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ 2_DISTANCE : 31|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 2_LATERAL : 41|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 2_SPEED : 52|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ COUNTER : 62|2@1+ (1,0) [0|3] "" XXX + +BO_ 1547 RADAR_TRACK_60b: 8 RADAR + SG_ 1_DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 1_LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 1_SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ 2_DISTANCE : 31|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 2_LATERAL : 41|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 2_SPEED : 52|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ COUNTER : 62|2@1+ (1,0) [0|3] "" XXX + +BO_ 1548 RADAR_TRACK_60c: 8 RADAR + SG_ 1_DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 1_LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 1_SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ 2_DISTANCE : 31|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 2_LATERAL : 41|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 2_SPEED : 52|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ COUNTER : 62|2@1+ (1,0) [0|3] "" XXX + +BO_ 1549 RADAR_TRACK_60d: 8 RADAR + SG_ 1_DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 1_LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 1_SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ 2_DISTANCE : 31|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 2_LATERAL : 41|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 2_SPEED : 52|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ COUNTER : 62|2@1+ (1,0) [0|3] "" XXX + +BO_ 1550 RADAR_TRACK_60e: 8 RADAR + SG_ 1_DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 1_LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 1_SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ 2_DISTANCE : 31|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 2_LATERAL : 41|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 2_SPEED : 52|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ COUNTER : 62|2@1+ (1,0) [0|3] "" XXX + +BO_ 1551 RADAR_TRACK_60f: 8 RADAR + SG_ 1_DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 1_LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 1_SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ 2_DISTANCE : 31|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 2_LATERAL : 41|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 2_SPEED : 52|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ COUNTER : 62|2@1+ (1,0) [0|3] "" XXX + +BO_ 1552 RADAR_TRACK_610: 8 RADAR + SG_ 1_DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 1_LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 1_SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ 2_DISTANCE : 31|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 2_LATERAL : 41|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 2_SPEED : 52|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ COUNTER : 62|2@1+ (1,0) [0|3] "" XXX + +BO_ 1553 RADAR_TRACK_611: 8 RADAR + SG_ 1_DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 1_LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 1_SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ 2_DISTANCE : 31|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 2_LATERAL : 41|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 2_SPEED : 52|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ COUNTER : 62|2@1+ (1,0) [0|3] "" XXX + +BO_ 1554 RADAR_ALT_612: 8 RADAR + SG_ DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ ID : 31|9@1+ (1,0) [0|511] "" XXX + SG_ SOMETHING_1 : 42|8@1+ (1,-128) [-128|127] "" XXX + SG_ SOMETHING_2 : 50|6@1+ (1,-32) [-32|31] "" XXX + SG_ COUNTER : 56|4@1+ (1,0) [0|15] "" XXX + SG_ CHECKSUM : 63|4@0+ (1,0) [0|15] "" XXX + +BO_ 1555 RADAR_ALT_613: 8 RADAR + SG_ DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ ID : 31|9@1+ (1,0) [0|511] "" XXX + SG_ SOMETHING_1 : 42|8@1+ (1,-128) [-128|127] "" XXX + SG_ SOMETHING_2 : 50|6@1+ (1,-32) [-32|31] "" XXX + SG_ COUNTER : 56|4@1+ (1,0) [0|15] "" XXX + SG_ CHECKSUM : 63|4@0+ (1,0) [0|15] "" XXX + +BO_ 1556 RADAR_ALT_614: 8 RADAR + SG_ DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ ID : 31|9@1+ (1,0) [0|511] "" XXX + SG_ SOMETHING_1 : 42|8@1+ (1,-128) [-128|127] "" XXX + SG_ SOMETHING_2 : 50|6@1+ (1,-32) [-32|31] "" XXX + SG_ COUNTER : 56|4@1+ (1,0) [0|15] "" XXX + SG_ CHECKSUM : 63|4@0+ (1,0) [0|15] "" XXX + +BO_ 1557 RADAR_ALT_615: 8 RADAR + SG_ DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ ID : 31|9@1+ (1,0) [0|511] "" XXX + SG_ SOMETHING_1 : 42|8@1+ (1,-128) [-128|127] "" XXX + SG_ SOMETHING_2 : 50|6@1+ (1,-32) [-32|31] "" XXX + SG_ COUNTER : 56|4@1+ (1,0) [0|15] "" XXX + SG_ CHECKSUM : 63|4@0+ (1,0) [0|15] "" XXX + +BO_ 1558 RADAR_ALT_616: 8 RADAR + SG_ DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ ID : 31|9@1+ (1,0) [0|511] "" XXX + SG_ SOMETHING_1 : 42|8@1+ (1,-128) [-128|127] "" XXX + SG_ SOMETHING_2 : 50|6@1+ (1,-32) [-32|31] "" XXX + SG_ COUNTER : 56|4@1+ (1,0) [0|15] "" XXX + SG_ CHECKSUM : 63|4@0+ (1,0) [0|15] "" XXX + +BO_ 1559 RADAR_ALT_617: 8 RADAR + SG_ DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ ID : 31|9@1+ (1,0) [0|511] "" XXX + SG_ SOMETHING_1 : 42|8@1+ (1,-128) [-128|127] "" XXX + SG_ SOMETHING_2 : 50|6@1+ (1,-32) [-32|31] "" XXX + SG_ COUNTER : 56|4@1+ (1,0) [0|15] "" XXX + SG_ CHECKSUM : 63|4@0+ (1,0) [0|15] "" XXX diff --git a/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_mrrevo14f_radar.py b/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_mrrevo14f_radar.py new file mode 100644 index 0000000000..f53622e651 --- /dev/null +++ b/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_mrrevo14f_radar.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +import os + +if __name__ == "__main__": + dbc_name = os.path.basename(__file__).replace(".py", ".dbc") + hyundai_path = os.path.dirname(os.path.realpath(__file__)) + with open(os.path.join(hyundai_path, dbc_name), "w", encoding="utf-8") as f: + f.write(""" +VERSION "" + + +NS_ : + NS_DESC_ + CM_ + BA_DEF_ + BA_ + VAL_ + CAT_DEF_ + CAT_ + FILTER + BA_DEF_DEF_ + EV_DATA_ + ENVVAR_DATA_ + SGTYPE_ + SGTYPE_VAL_ + BA_DEF_SGTYPE_ + BA_SGTYPE_ + SIG_TYPE_REF_ + VAL_TABLE_ + SIG_GROUP_ + SIG_VALTYPE_ + SIGTYPE_VALTYPE_ + BO_TX_BU_ + BA_DEF_REL_ + BA_REL_ + BA_DEF_DEF_REL_ + BU_SG_REL_ + BU_EV_REL_ + BU_BO_REL_ + SG_MUL_VAL_ + +BS_: + +BU_: XXX + +BO_ 1537 RADAR_LEAD: 8 RADAR + SG_ DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ ID : 31|9@1+ (1,0) [0|511] "" XXX + SG_ SOMETHING_1 : 42|8@1+ (1,-128) [-128|127] "" XXX + SG_ SOMETHING_2 : 50|6@1+ (1,-32) [-32|31] "" XXX + SG_ COUNTER : 56|4@1+ (1,0) [0|15] "" XXX + SG_ CHECKSUM : 63|4@0+ (1,0) [0|15] "" XXX +""") + + for a in range(0x602, 0x602 + 16): + f.write(f""" +BO_ {a} RADAR_TRACK_{a:x}: 8 RADAR + SG_ 1_DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 1_LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 1_SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ 2_DISTANCE : 31|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ 2_LATERAL : 41|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ 2_SPEED : 52|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ COUNTER : 62|2@1+ (1,0) [0|3] "" XXX +""") + + for a in range(0x612, 0x612 + 6): + f.write(f""" +BO_ {a} RADAR_ALT_{a:x}: 8 RADAR + SG_ DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX + SG_ LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX + SG_ SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX + SG_ ID : 31|9@1+ (1,0) [0|511] "" XXX + SG_ SOMETHING_1 : 42|8@1+ (1,-128) [-128|127] "" XXX + SG_ SOMETHING_2 : 50|6@1+ (1,-32) [-32|31] "" XXX + SG_ COUNTER : 56|4@1+ (1,0) [0|15] "" XXX + SG_ CHECKSUM : 63|4@0+ (1,0) [0|15] "" XXX +""") diff --git a/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_palisade_2023.dbc b/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_palisade_2023.dbc index 68f5d60af4..a67c7dbca7 100644 --- a/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_palisade_2023.dbc +++ b/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_palisade_2023.dbc @@ -1,4 +1,4 @@ -CM_ "IMPORT _hyundai_canfd_common.dbc"; +CM_ "IMPORT _hyundai_palisade_2023_canfd_common.dbc"; BO_ 67 DATC13: 8 XXX SG_ CF_Datc_AcDisp : 22|2@1+ (1,0) [0|0] "" XXX @@ -188,6 +188,50 @@ BO_ 640 EMS13: 8 XXX SG_ TCO : 24|8@1+ (0.75,-48) [0|0] "" XXX SG_ TIA : 48|8@1+ (0.75,-48) [0|0] "" XXX +BO_ 683 ESCC: 8 XXX + SG_ FCA_CmdAct : 0|1@1+ (1,0) [0|1] "" ESC + SG_ CF_VSM_Warn_FCA11 : 1|2@1+ (1,0) [0|3] "" ACU,CLU,ESC + SG_ AEB_CmdAct : 3|1@1+ (1,0) [0|1] "" ESC + SG_ CF_VSM_Warn_SCC12 : 4|2@1+ (1,0) [0|3] "" CLU,ESC,IAP + SG_ CF_VSM_DecCmdAct_SCC12 : 6|1@1+ (1,0) [0|1] "" ESC + SG_ CF_VSM_DecCmdAct_FCA11 : 7|1@1+ (1,0) [0|1] "" ESC + SG_ CR_VSM_DecCmd_SCC12 : 8|8@1+ (0.01,0) [0|2.55] "g" ESC + SG_ ObjValid : 16|1@1+ (1,0) [0|1] "" CLU,ESC,TCU + SG_ ACC_ObjStatus : 17|2@1+ (1,0) [0|3] "" ABS,ESC + SG_ ACC_ObjRelSpd : 40|9@1+ (0.1,-16.4) [0|0] "m/s" ABS,ESC + SG_ ACC_ObjDist : 24|11@1+ (0.1,0) [0|204.7] "m" ABS,ESC + SG_ CR_VSM_DecCmd_FCA11 : 56|8@1+ (0.01,0) [0|2.55] "g" ESC + +BO_ 2016 ESCC_DEBUG: 8 XXX + SG_ safety_mode : 0|8@1+ (1,0) [0|255] "" XXX + SG_ watchdog_fails : 8|8@1+ (1,0) [0|255] "" XXX + SG_ fwd_hook_count : 16|16@1+ (1,0) [0|255] "" XXX + SG_ fifo_overrun : 32|8@1+ (1,0) [0|255] "" XXX + SG_ fifo_overrun_count : 40|8@1+ (1,0) [0|255] "" XXX + SG_ can3_queue : 48|16@1+ (1,0) [0|255] "" XXX + +BO_ 2017 ESCC_DEBUG2: 8 XXX + SG_ CAN1_TME0 : 26|1@1+ (1,0) [0|1] "" XXX + SG_ CAN1_TME1 : 27|1@1+ (1,0) [0|1] "" XXX + SG_ CAN1_TME2 : 28|1@1+ (1,0) [0|1] "" XXX + SG_ CAN1_ALST0 : 2|1@1+ (1,0) [0|1] "" XXX + SG_ CAN1_ALST1 : 10|1@1+ (1,0) [0|1] "" XXX + SG_ CAN1_ALST2 : 18|1@1+ (1,0) [0|1] "" XXX + SG_ CAN1_TERR0 : 1|1@1+ (1,0) [0|1] "" XXX + SG_ CAN1_TERR1 : 9|1@1+ (1,0) [0|1] "" XXX + SG_ CAN1_TERR2 : 17|1@1+ (1,0) [0|1] "" XXX + SG_ CAN1_RQCP0 : 0|1@1+ (1,0) [0|1] "" XXX + SG_ CAN1_RQCP1 : 8|1@1+ (1,0) [0|1] "" XXX + SG_ CAN1_RQCP2 : 16|1@1+ (1,0) [0|1] "" XXX + SG_ CAN1_TX_ERR_CNT : 32|8@1+ (1,0) [0|255] "" XXX + SG_ CAN1_RX_ERR_CNT : 40|8@1+ (1,0) [0|255] "" XXX + SG_ CAN1_EWGF : 48|1@1+ (1,0) [0|1] "" XXX + SG_ CAN1_EPVF : 49|1@1+ (1,0) [0|1] "" XXX + SG_ CAN1_BOFF : 50|1@1+ (1,0) [0|1] "" XXX + SG_ CAN1_LEC : 56|3@1+ (1,0) [0|7] "" XXX + +VAL_ 2017 CAN1_LEC 0 "No Error" 1 "Stuff Error" 2 "Form Error" 3 "Ack Error" 4 "Bit Recessive Error" 5 "Bit Dominant Error" 6 "CRC Error" 7 "Set by Software" ; + BO_ 688 SAS11: 5 XXX SG_ CheckSum : 36|4@1+ (1,0) [0|0] "" XXX SG_ MsgCount : 32|4@1+ (1,0) [0|0] "" XXX @@ -256,7 +300,7 @@ BO_ 854 M_356: 8 XXX SG_ PAINT4 : 38|1@0+ (1,0) [0|0] "" XXX BO_ 867 RADAR_0x363: 8 XXX - SG_ _CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX + SG_ CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX SG_ COUNTER : 12|4@1+ (1,0) [0|0] "" XXX SG_ FCA_ESA : 8|2@1+ (1,0) [0|3] "" XXX SG_ BYTE2 : 16|8@1+ (1,0) [0|0] "" XXX @@ -266,6 +310,17 @@ BO_ 867 RADAR_0x363: 8 XXX SG_ BYTE6 : 48|8@1+ (1,0) [0|0] "" XXX SG_ BYTE7 : 56|8@1+ (1,0) [0|0] "" XXX +BO_ 868 ALERTS_364: 8 XXX + SG_ CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX + SG_ COUNTER : 12|4@1+ (1,0) [0|0] "" XXX + SG_ BYTE2 : 16|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE3 : 24|8@1+ (1,0) [0|255] "" XXX + SG_ DAW_Status : 35|3@1+ (1,0) [0|0] "" XXX + SG_ DAW_Warning : 39|1@1+ (1,0) [0|7] "" XXX + SG_ BYTE5 : 40|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE6 : 48|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE7 : 56|8@1+ (1,0) [0|255] "" XXX + BO_ 870 EMS_366: 8 XXX SG_ N : 8|16@1+ (0.25,0) [0|0] "" XXX SG_ SWI_IGK : 48|1@0+ (1,0) [0|0] "" XXX @@ -275,6 +330,7 @@ BO_ 870 EMS_366: 8 XXX BO_ 871 LVR12: 8 XXX SG_ CF_Lvr_CruiseSet : 0|8@1+ (1,0) [0|0] "" XXX + SG_ AUTO_START_STOP_STATUS : 8|2@1+ (1,0) [0|3] "" XXX SG_ CF_Lvr_Gear : 32|4@1+ (1,0) [0|0] "" XXX BO_ 872 LVR11: 8 XXX @@ -329,26 +385,29 @@ BO_ 903 WHL_PUL11: 6 XXX SG_ WHL_PUL_RR : 24|8@1+ (0.5,0) [0|0] "" XXX BO_ 905 SCC14: 8 XXX - SG_ ACC_ObjLatPos : 16|9@1+ (0.1,-20) [-20|31.1] "m" ABS,ESC - SG_ _CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX + SG_ ACC_ObjRelSpd : 16|9@1+ (0.1,-20) [-20|31.1] "m/s" ABS,ESC + SG_ CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX SG_ COUNTER : 12|4@1+ (1,0) [0|0] "" XXX - SG_ ObjGap : 51|3@1+ (1,0) [0|255] "" CLU,HUD,ESC + SG_ ObjValid : 27|1@1+ (1,0) [0|1] "" CLU,ESC,TCU + SG_ ObjStatus : 51|3@1+ (1,0) [0|7] "" CLU,HUD,ESC BO_ 909 FCA11: 8 XXX - SG_ CR_FCA_ChkSum : 0|8@1+ (1,0) [0|0] "" XXX - SG_ NEW_SIGNAL_1 : 8|4@1+ (1,0) [0|0] "" XXX - SG_ CR_FCA_Alive : 12|4@1+ (1,0) [0|0] "" XXX - SG_ BYTE2 : 16|8@1+ (1,0) [0|0] "" XXX - SG_ BYTE3 : 24|8@1+ (1,0) [0|0] "" XXX - SG_ BYTE4 : 32|8@1+ (1,0) [0|0] "" XXX - SG_ BYTE5 : 40|8@1+ (1,0) [0|0] "" XXX - SG_ BYTE6 : 48|8@1+ (1,0) [0|0] "" XXX - SG_ BYTE7 : 56|8@1+ (1,0) [0|0] "" XXX + SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX + SG_ COUNTER : 12|4@1+ (1,0) [0|0] "" XXX + SG_ aeb_cmd_act : 16|1@1+ (1,0) [0|1] "" XXX + SG_ fca_cmd_act : 19|1@1+ (1,0) [0|1] "" XXX + SG_ fca_fault : 32|2@1+ (1,0) [0|3] "" XXX + SG_ cr_vsm_deccmd : 38|8@1+ (1,0) [0|255] "" XXX + SG_ NEW_SIGNAL_2 : 47|1@1+ (1,0) [0|1] "" XXX + SG_ cf_vsm_deccmdact : 48|7@1+ (1,-127) [0|15] "" XXX + SG_ cf_vsm_warn_1 : 55|1@1+ (1,0) [0|0] "" XXX + SG_ cf_vsm_warn_2 : 57|1@1+ (1,0) [0|1] "" XXX + SG_ cf_vsm_warn_3 : 62|1@1+ (1,0) [0|0] "" XXX BO_ 913 BCM_PO_11: 8 XXX SG_ BCM_Door_Dri_Status : 5|1@0+ (1,0) [0|0] "" XXX SG_ BCM_Shift_R_MT_SW_Status : 39|2@0+ (1,0) [0|0] "" XXX - SG_ LFA_Pressed : 4|1@0+ (1,0) [0|0] "" XXX + SG_ LDA_BTN : 4|1@0+ (1,0) [0|0] "" XXX BO_ 916 TCS13: 8 XXX SG_ ACCEL_REF_ACC : 32|11@1+ (0.01,-10.23) [0|0] "" XXX @@ -359,7 +418,7 @@ BO_ 916 TCS13: 8 XXX SG_ AliveCounterTCS : 13|3@1+ (1,0) [0|0] "" XXX SG_ BrakeLight : 11|1@1+ (1,0) [0|0] "" XXX SG_ CF_DriBkeStat : 60|1@1+ (1,0) [0|0] "" XXX - SG_ CF_VSM_Avail : 57|2@1+ (1,0) [0|0] "" XXX + SG_ CF_FCW_AEB_Status : 57|2@1+ (1,0) [0|0] "" XXX SG_ CF_VSM_Coded : 56|1@1+ (1,0) [0|0] "" XXX SG_ CF_VSM_ConfSwi : 61|2@1+ (1,0) [0|0] "" XXX SG_ CF_VSM_Handshake : 59|1@1+ (1,0) [0|0] "" XXX @@ -378,7 +437,7 @@ BO_ 916 TCS13: 8 XXX SG_ aBasis : 0|11@1+ (0.01,-10.23) [0|0] "" XXX BO_ 920 RADAR_0x398: 8 XXX - SG_ _CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX + SG_ CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX SG_ NEW_SIGNAL_1 : 8|4@1+ (1,0) [0|0] "" XXX SG_ COUNTER : 12|4@1+ (1,0) [0|0] "" XXX SG_ BYTE2 : 16|8@1+ (1,0) [0|0] "" XXX @@ -389,7 +448,7 @@ BO_ 920 RADAR_0x398: 8 XXX SG_ BYTE7 : 56|8@1+ (1,0) [0|0] "" XXX BO_ 921 RADAR_0x399: 8 XXX - SG_ _CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX + SG_ CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX SG_ NEW_SIGNAL_1 : 8|4@1+ (1,0) [0|0] "" XXX SG_ COUNTER : 12|4@1+ (1,0) [0|0] "" XXX SG_ BYTE2 : 16|8@1+ (1,0) [0|0] "" XXX @@ -400,7 +459,7 @@ BO_ 921 RADAR_0x399: 8 XXX SG_ BYTE7 : 56|8@1+ (1,0) [0|0] "" XXX BO_ 922 RADAR_0x39a: 8 XXX - SG_ _CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX + SG_ CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX SG_ NEW_SIGNAL_1 : 8|4@1+ (1,0) [0|0] "" XXX SG_ COUNTER : 12|4@1+ (1,0) [0|0] "" XXX SG_ BYTE2 : 16|8@1+ (1,0) [0|0] "" XXX @@ -411,7 +470,7 @@ BO_ 922 RADAR_0x39a: 8 XXX SG_ BYTE7 : 56|8@1+ (1,0) [0|0] "" XXX BO_ 923 RADAR_0x39b: 8 XXX - SG_ _CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX + SG_ CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX SG_ NEW_SIGNAL_1 : 8|4@1+ (1,0) [0|0] "" XXX SG_ COUNTER : 12|4@1+ (1,0) [0|0] "" XXX SG_ BYTE2 : 16|8@1+ (1,0) [0|0] "" XXX @@ -422,7 +481,7 @@ BO_ 923 RADAR_0x39b: 8 XXX SG_ BYTE7 : 56|8@1+ (1,0) [0|0] "" XXX BO_ 924 RADAR_0x39c: 8 XXX - SG_ _CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX + SG_ CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX SG_ NEW_SIGNAL_1 : 8|4@1+ (1,0) [0|0] "" XXX SG_ COUNTER : 12|4@1+ (1,0) [0|0] "" XXX SG_ BYTE2 : 16|8@1+ (1,0) [0|0] "" XXX @@ -470,25 +529,27 @@ BO_ 1042 ICM_412h: 8 XXX SG_ WarningSoundOutput_3Group : 7|1@0+ (1,0) [0|0] "" XXX BO_ 1056 SCC11: 8 XXX - SG_ _CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX - SG_ AliveCounterACC : 12|4@1+ (1,0) [0|15] "" CLU,EMS,ESC,TCU - SG_ JerkLowerLimit : 50|7@1+ (0.1,0) [0|12.7] "m/s^3" ESC - SG_ JerkUpperLimit : 43|7@1+ (0.1,0) [0|12.7] "m/s^3" ESC - SG_ ObjValid : 59|1@1+ (1,0) [0|1] "" CLU,ESC,TCU - SG_ aReqRaw : 27|11@1+ (0.01,-10.23) [-10.23|10.24] "m/s^2" Vector__XXX + SG_ CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX + SG_ COUNTER : 12|4@1+ (1,0) [0|15] "" CLU,EMS,ESC,TCU SG_ aReqValue : 16|11@1+ (0.01,-10.23) [-10.23|10.24] "m/s^2" Vector__XXX + SG_ aReqRaw : 27|11@1+ (0.01,-10.23) [-10.23|10.24] "m/s^2" Vector__XXX + SG_ JerkUpperLimit : 38|7@1+ (0.1,0) [0|12.7] "m/s^3" ESC + SG_ JerkLowerLimit : 45|7@1+ (0.1,0) [0|12.7] "m/s^3" ESC + SG_ ComfortBandUpper : 52|6@1+ (0.02,0) [0|1.26] "m/s^2" ESC + SG_ ComfortBandLower : 58|6@1+ (0.02,0) [0|1.26] "m/s^2" ESC BO_ 1057 SCC12: 8 XXX SG_ ACCMode : 28|2@1+ (1,0) [0|3] "" CLU,HUD,LDWS_LKAS,ESC SG_ ACCMode_Inactive : 30|1@1+ (1,0) [0|1] "" CLU,HUD,LDWS_LKAS,ESC SG_ ACC_ObjDist : 16|11@1+ (0.1,0) [0|204.7] "m" ABS,ESC - SG_ CR_VSM_ChkSum : 0|8@1+ (1,0) [0|0] "" XXX - SG_ CR_VSM_Alive : 12|4@1+ (1,0) [0|15] "" ESC,PSB + SG_ CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX + SG_ COUNTER : 12|4@1+ (1,0) [0|15] "" ESC,PSB SG_ MainMode_ACC : 27|1@1+ (1,0) [0|1] "" CLU,EMS,ESC SG_ SCCInfoDisplay : 32|3@1+ (1,0) [0|7] "" CLU,ESC SG_ TauGapSet : 37|3@1+ (1,0) [0|7] "" CLU,ESC,TCU SG_ VSetDis : 41|8@1+ (1,0) [0|255] "km/h or MPH" CLU,ESC,TCU SG_ StopReq : 49|1@1+ (1,0) [0|1] "" EPB,ESC + SG_ ACC_ObjDist_Ref : 50|11@1+ (0.1,0) [0|204.7] "" CLU,HUD,LDWS_LKAS,ESC BO_ 1064 _4WD11: 8 XXX SG_ AUTO_ACT : 43|1@1+ (1,0) [0|0] "" XXX @@ -519,7 +580,7 @@ BO_ 1078 PAS11: 4 XXX SG_ CF_Gway_PASSystemOn : 24|2@1+ (1,0) [0|0] "" XXX BO_ 1082 RADAR_0x43a: 8 XXX - SG_ _CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX + SG_ CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX SG_ NEW_SIGNAL_1 : 8|4@1+ (1,0) [0|0] "" XXX SG_ COUNTER : 12|4@1+ (1,0) [0|0] "" XXX SG_ BYTE2 : 16|8@1+ (1,0) [0|0] "" XXX @@ -529,6 +590,11 @@ BO_ 1082 RADAR_0x43a: 8 XXX SG_ BYTE6 : 48|8@1+ (1,0) [0|0] "" XXX SG_ BYTE7 : 56|8@1+ (1,0) [0|0] "" XXX +BO_ 1123 MSG_463: 8 XXX + SG_ DRV_MODE_PRIMARY_STATUS : 0|8@1+ (1,0) [0|255] "" XXX + SG_ DRV_MODE_SECONDARY_STATUS : 8|8@1+ (1,0) [0|255] "" XXX + SG_ DRV_MODE_EXT_IND : 16|8@1+ (1,0) [0|255] "" XXX + BO_ 1136 P_STS: 8 XXX SG_ Checksum : 62|2@1+ (1,0) [0|0] "" XXX SG_ Counter : 58|4@1+ (1,0) [0|0] "" XXX @@ -549,7 +615,7 @@ BO_ 1151 ESP11: 6 XXX SG_ _4WD_TQC_LIM : 16|16@1+ (1,0) [0|0] "" XXX BO_ 1157 LFAHDA_MFC: 8 XXX - SG_ _CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX + SG_ CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX SG_ NEW_SIGNAL_1 : 8|4@1+ (1,0) [0|0] "" XXX SG_ COUNTER : 12|4@1+ (1,0) [0|0] "" XXX SG_ NEW_SIGNAL_2 : 16|1@0+ (1,0) [0|0] "" XXX @@ -602,15 +668,15 @@ BO_ 1170 EMS19: 8 XXX BO_ 1173 YRS13: 8 XXX SG_ YRS_SeralNo : 16|48@1+ (1,0) [0|0] "" XXX -BO_ 1186 FRT_RADAR11: 2 XXX - SG_ BYTE0 : 0|8@1+ (1,0) [0|0] "" XXX - SG_ BYTE1 : 8|8@1+ (1,0) [0|0] "" XXX - SG_ BYTE2 : 16|8@1+ (1,0) [0|0] "" XXX - SG_ BYTE3 : 24|8@1+ (1,0) [0|0] "" XXX - SG_ BYTE4 : 32|8@1+ (1,0) [0|0] "" XXX - SG_ BYTE5 : 40|8@1+ (1,0) [0|0] "" XXX - SG_ BYTE6 : 48|8@1+ (1,0) [0|0] "" XXX - SG_ BYTE7 : 56|8@1+ (1,0) [0|0] "" XXX +BO_ 1186 FRT_RADAR11: 8 FCA + SG_ BYTE0 : 0|8@1+ (1,0) [0|7] "" LDWS_LKAS,LDW_LKA,ESC + SG_ BYTE1 : 8|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE2 : 16|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE3 : 24|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE4 : 32|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE5 : 40|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE6 : 48|8@1+ (1,0) [0|255] "" XXX + SG_ BYTE7 : 56|8@1+ (1,0) [0|255] "" XXX BO_ 1265 CLU11: 4 XXX SG_ CF_Clu_AliveCnt1 : 28|4@1+ (1,0) [0|0] "" XXX @@ -686,13 +752,15 @@ BO_ 1322 CLU15: 8 XXX BO_ 1342 LKAS12: 8 XXX SG_ CF_LkasDawStatus : 40|3@1+ (1,0) [0|0] "" XXX - SG_ CF_LkasTsrSpeed_Display_Navi : 24|8@1+ (1,0) [0|0] "" XXX - SG_ CF_Lkas_Daw_USM : 37|3@1+ (1,0) [0|0] "" XXX - SG_ CF_Lkas_TsrAddinfo_Display : 32|2@1+ (1,0) [0|0] "" XXX + SG_ CF_LkasTsrSpeed_Display_Navi : 32|8@1+ (1,0) [0|0] "" XXX + SG_ CF_Lkas_TsrSpeed_Display_Clu : 24|8@1+ (1,0) [0|0] "" XXX SG_ CF_Lkas_TsrSlifOpt : 10|2@1+ (1,0) [0|0] "" XXX - SG_ CF_Lkas_TsrSpeed_Display_Clu : 16|8@1+ (1,0) [0|0] "" XXX - SG_ _CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX + SG_ CHECKSUM : 0|8@1+ (1,0) [0|0] "" XXX SG_ COUNTER : 12|4@1+ (1,0) [0|0] "" XXX + SG_ CF_SLA_Avail : 48|2@1+ (1,0) [0|3] "" XXX + SG_ CF_SLA_AdjDisp : 51|2@1+ (1,0) [0|3] "" XXX + SG_ Sign_detected : 56|5@1+ (1,0) [0|255] "" XXX + SG_ CF_SLA_ACT : 61|1@0+ (1,0) [0|1] "" XXX BO_ 1345 CGW1: 8 XXX SG_ CF_Gway_ALightStat : 37|1@1+ (1,0) [0|0] "" XXX @@ -852,14 +920,26 @@ BO_ 1456 CLU12: 4 XXX SG_ CF_Clu_Odometer : 0|24@1+ (0.1,0) [0|0] "" XXX +CM_ SG_ 1123 DRV_MODE_PRIMARY_STATUS "Indicates the current drive mode selected by the driver (Smart, Sport, Comfort, ECO, Snow). This is a status-only signal, not writable."; +CM_ SG_ 1123 DRV_MODE_SECONDARY_STATUS "Indicates a secondary status modifier, often used in combination with PRIMARY to disambiguate ECO vs Snow. Status-only."; +CM_ SG_ 1123 DRV_MODE_EXT_IND "Drive mode extension indicator (e.g. Snow Mode flag). Status-only signal."; CM_ SG_ 1348 SpeedLim_Nav_Clu "Speed limit displayed on Nav, Cluster and HUD"; +CM_ SG_ 1342 CF_SLA_ACT "Speed limit assist active flag"; +CM_ SG_ 1342 CF_SLA_AdjDisp "Display of the automatically adjusting speed message"; +CM_ SG_ 1342 CF_SLA_Avail "Up or Down arrow showing speed limit assist available"; VAL_ 871 CF_Lvr_Gear 12 "T" 5 "D" 8 "S" 6 "N" 7 "R" 0 "P"; +VAL_ 871 AUTO_START_STOP_STATUS 3 "Off" 1 "On"; VAL_ 909 CF_VSM_Warn 2 "FCW" 3 "AEB"; VAL_ 916 ACCEnable 0 "SCC ready" 1 "SCC temp fault" 2 "SCC permanent fault" 3 "SCC permanent fault, communication issue"; +VAL_ 916 CF_FCW_AEB_Status 0 "Normal" 3 "Fault"; VAL_ 1057 ACCMode 0 "off" 1 "enabled" 2 "driver_override" 3 "off_maybe_fault" 4 "cancelled"; +VAL_ 1123 DRV_MODE_PRIMARY_STATUS 49 "Smart" 33 "Sport" 1 "Comfort" 17 "ECO_or_Snow"; +VAL_ 1123 DRV_MODE_SECONDARY_STATUS 1 "Normal" 17 "Snow"; +VAL_ 1123 DRV_MODE_EXT_IND 0 "Standard" 16 "SnowFlag"; VAL_ 1157 HDA_Icon_State 0 "no_hda" 1 "white_hda" 2 "green_hda"; VAL_ 1157 LFA_SysWarning 0 "no_message" 1 "switching_to_hda" 2 "switching_to_scc" 3 "lfa_error" 4 "check_hda" 5 "keep_hands_on_wheel_orange" 6 "keep_hands_on_wheel_red"; VAL_ 1157 LFA_Icon_State 0 "no_wheel" 1 "white_wheel" 2 "green_wheel" 3 "green_wheel_blink"; VAL_ 1157 HDA_SysWarning 0 "no_message" 1 "driving_convenience_systems_cancelled" 2 "highway_drive_assist_system_cancelled"; VAL_ 1322 CF_Clu_Gear 1 "P" 2 "R" 4 "N" 8 "D"; +VAL_ 1342 Sign_detected 1 "Slow/Caution" 8 "Stop" 9 "Yield" \ No newline at end of file diff --git a/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_radar_210_21f.dbc b/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_radar_210_21f.dbc new file mode 100644 index 0000000000..13c688eb05 --- /dev/null +++ b/opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_radar_210_21f.dbc @@ -0,0 +1,1221 @@ + +VERSION "" + + +NS_ : + NS_DESC_ + CM_ + BA_DEF_ + BA_ + VAL_ + CAT_DEF_ + CAT_ + FILTER + BA_DEF_DEF_ + EV_DATA_ + ENVVAR_DATA_ + SGTYPE_ + SGTYPE_VAL_ + BA_DEF_SGTYPE_ + BA_SGTYPE_ + SIG_TYPE_REF_ + VAL_TABLE_ + SIG_GROUP_ + SIG_VALTYPE_ + SIGTYPE_VALTYPE_ + BO_TX_BU_ + BA_DEF_REL_ + BA_REL_ + BA_DEF_DEF_REL_ + BU_SG_REL_ + BU_EV_REL_ + BU_BO_REL_ + SG_MUL_VAL_ + +BS_: + +BU_: XXX + +BO_ 528 RADAR_TRACK_210: 32 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ 1_STATE_ALT : 25|2@0+ (1,0) [0|3] "" XXX + SG_ 1_MOTION_STATE : 29|3@0+ (1,0) [0|4] "" XXX + SG_ 1_TRACK_QUALITY : 38|7@0+ (1,0) [0|127] "" XXX + SG_ 1_AGE : 47|8@0+ (1,0) [0|255] "" XXX + SG_ 1_COAST_AGE : 51|4@0+ (1,0) [0|15] "" XXX + SG_ 1_STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ 1_RCS : 63|8@0- (1,0) [-128|127] "" XXX + SG_ 1_LONG_DIST : 64|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 1_LAT_DIST : 76|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 1_REL_SPEED : 88|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 1_NEW_SIGNAL_4 : 102|2@1+ (1,0) [0|2] "" XXX + SG_ 1_REL_LAT_SPEED : 104|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 1_REL_ACCEL : 118|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + SG_ 1_NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|1] "" XXX + SG_ 1_OBJECT_ID : 135|6@0+ (1,0) [0|63] "" XXX + SG_ 2_NEW_SIGNAL_18 : 137|2@0+ (1,0) [0|1] "" XXX + SG_ 2_OBJECT_ID : 143|6@0+ (1,0) [0|63] "" XXX + SG_ 2_STATE_ALT : 153|2@0+ (1,0) [0|3] "" XXX + SG_ 2_MOTION_STATE : 157|3@0+ (1,0) [0|4] "" XXX + SG_ 2_TRACK_QUALITY : 166|7@0+ (1,0) [0|127] "" XXX + SG_ 2_AGE : 175|8@0+ (1,0) [0|255] "" XXX + SG_ 2_COAST_AGE : 179|4@0+ (1,0) [0|15] "" XXX + SG_ 2_STATE : 182|3@0+ (1,0) [0|7] "" XXX + SG_ 2_RCS : 191|8@0- (1,0) [-128|127] "" XXX + SG_ 2_LONG_DIST : 192|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 2_LAT_DIST : 204|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 2_REL_SPEED : 216|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 2_NEW_SIGNAL_4 : 230|2@1+ (1,0) [0|2] "" XXX + SG_ 2_REL_LAT_SPEED : 232|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 2_REL_ACCEL : 246|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + +VAL_ 528 1_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 528 2_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 528 1_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 528 2_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 528 1_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; +VAL_ 528 2_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; + CM_ BO_ 528 "Hyundai/Kia 210-21F radar-track layout: 16 messages with two target slots per 32-byte message (32 tracks total). Two related dialects share the same position, velocity, age, coast-age, and compressed-state core. The detailed dialect also populates STATE, RCS, and NEW_SIGNAL_4; the compact Tucson/Sportage/Santa Cruz dialect leaves those fields zero and uses STATE_ALT. The dialect is independent of the logged raw bus and is not an HDA-level discriminator: HDA2-like routes carried the sustained stream on A-CAN mapped to bus 0, while non-HDA2 routes mapped A-CAN to bus 1. Compact targets populate per-slot NEW_SIGNAL_18/OBJECT_ID extension bytes at bytes 16 and 17; detailed platforms usually leave them zero. Byte 18 and bits 26, 30-31, 39, 55, and 117 remained zero. REL_LAT_SPEED has opposite sign conventions between the detailed and compact dialects. The HKG CAN-FD checksum matched every one of 13,210,543 checked frames."; +CM_ SG_ 528 1_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 528 1_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 528 1_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 528 1_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 528 1_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 528 1_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 528 1_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 528 1_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 528 1_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 528 1_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 528 1_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 528 1_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 528 1_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 528 1_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 528 1_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 528 2_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 528 2_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 528 2_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 528 2_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 528 2_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 528 2_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 528 2_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 528 2_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 528 2_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 528 2_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 528 2_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 528 2_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 528 2_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 528 2_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 528 2_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 528 CHECKSUM "16-bit HKG CAN-FD checksum using the CAN address as the data ID."; +CM_ SG_ 528 COUNTER "8-bit transmit-cycle counter. It covers 0-255 and wraps."; + +BO_ 529 RADAR_TRACK_211: 32 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ 1_STATE_ALT : 25|2@0+ (1,0) [0|3] "" XXX + SG_ 1_MOTION_STATE : 29|3@0+ (1,0) [0|4] "" XXX + SG_ 1_TRACK_QUALITY : 38|7@0+ (1,0) [0|127] "" XXX + SG_ 1_AGE : 47|8@0+ (1,0) [0|255] "" XXX + SG_ 1_COAST_AGE : 51|4@0+ (1,0) [0|15] "" XXX + SG_ 1_STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ 1_RCS : 63|8@0- (1,0) [-128|127] "" XXX + SG_ 1_LONG_DIST : 64|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 1_LAT_DIST : 76|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 1_REL_SPEED : 88|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 1_NEW_SIGNAL_4 : 102|2@1+ (1,0) [0|2] "" XXX + SG_ 1_REL_LAT_SPEED : 104|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 1_REL_ACCEL : 118|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + SG_ 1_NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|1] "" XXX + SG_ 1_OBJECT_ID : 135|6@0+ (1,0) [0|63] "" XXX + SG_ 2_NEW_SIGNAL_18 : 137|2@0+ (1,0) [0|1] "" XXX + SG_ 2_OBJECT_ID : 143|6@0+ (1,0) [0|63] "" XXX + SG_ 2_STATE_ALT : 153|2@0+ (1,0) [0|3] "" XXX + SG_ 2_MOTION_STATE : 157|3@0+ (1,0) [0|4] "" XXX + SG_ 2_TRACK_QUALITY : 166|7@0+ (1,0) [0|127] "" XXX + SG_ 2_AGE : 175|8@0+ (1,0) [0|255] "" XXX + SG_ 2_COAST_AGE : 179|4@0+ (1,0) [0|15] "" XXX + SG_ 2_STATE : 182|3@0+ (1,0) [0|7] "" XXX + SG_ 2_RCS : 191|8@0- (1,0) [-128|127] "" XXX + SG_ 2_LONG_DIST : 192|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 2_LAT_DIST : 204|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 2_REL_SPEED : 216|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 2_NEW_SIGNAL_4 : 230|2@1+ (1,0) [0|2] "" XXX + SG_ 2_REL_LAT_SPEED : 232|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 2_REL_ACCEL : 246|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + +VAL_ 529 1_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 529 2_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 529 1_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 529 2_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 529 1_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; +VAL_ 529 2_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; + CM_ BO_ 529 "Hyundai/Kia 210-21F radar-track layout: 16 messages with two target slots per 32-byte message (32 tracks total). Two related dialects share the same position, velocity, age, coast-age, and compressed-state core. The detailed dialect also populates STATE, RCS, and NEW_SIGNAL_4; the compact Tucson/Sportage/Santa Cruz dialect leaves those fields zero and uses STATE_ALT. The dialect is independent of the logged raw bus and is not an HDA-level discriminator: HDA2-like routes carried the sustained stream on A-CAN mapped to bus 0, while non-HDA2 routes mapped A-CAN to bus 1. Compact targets populate per-slot NEW_SIGNAL_18/OBJECT_ID extension bytes at bytes 16 and 17; detailed platforms usually leave them zero. Byte 18 and bits 26, 30-31, 39, 55, and 117 remained zero. REL_LAT_SPEED has opposite sign conventions between the detailed and compact dialects. The HKG CAN-FD checksum matched every one of 13,210,543 checked frames."; +CM_ SG_ 529 1_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 529 1_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 529 1_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 529 1_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 529 1_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 529 1_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 529 1_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 529 1_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 529 1_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 529 1_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 529 1_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 529 1_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 529 1_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 529 1_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 529 1_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 529 2_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 529 2_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 529 2_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 529 2_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 529 2_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 529 2_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 529 2_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 529 2_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 529 2_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 529 2_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 529 2_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 529 2_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 529 2_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 529 2_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 529 2_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 529 CHECKSUM "16-bit HKG CAN-FD checksum using the CAN address as the data ID."; +CM_ SG_ 529 COUNTER "8-bit transmit-cycle counter. It covers 0-255 and wraps."; + +BO_ 530 RADAR_TRACK_212: 32 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ 1_STATE_ALT : 25|2@0+ (1,0) [0|3] "" XXX + SG_ 1_MOTION_STATE : 29|3@0+ (1,0) [0|4] "" XXX + SG_ 1_TRACK_QUALITY : 38|7@0+ (1,0) [0|127] "" XXX + SG_ 1_AGE : 47|8@0+ (1,0) [0|255] "" XXX + SG_ 1_COAST_AGE : 51|4@0+ (1,0) [0|15] "" XXX + SG_ 1_STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ 1_RCS : 63|8@0- (1,0) [-128|127] "" XXX + SG_ 1_LONG_DIST : 64|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 1_LAT_DIST : 76|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 1_REL_SPEED : 88|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 1_NEW_SIGNAL_4 : 102|2@1+ (1,0) [0|2] "" XXX + SG_ 1_REL_LAT_SPEED : 104|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 1_REL_ACCEL : 118|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + SG_ 1_NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|1] "" XXX + SG_ 1_OBJECT_ID : 135|6@0+ (1,0) [0|63] "" XXX + SG_ 2_NEW_SIGNAL_18 : 137|2@0+ (1,0) [0|1] "" XXX + SG_ 2_OBJECT_ID : 143|6@0+ (1,0) [0|63] "" XXX + SG_ 2_STATE_ALT : 153|2@0+ (1,0) [0|3] "" XXX + SG_ 2_MOTION_STATE : 157|3@0+ (1,0) [0|4] "" XXX + SG_ 2_TRACK_QUALITY : 166|7@0+ (1,0) [0|127] "" XXX + SG_ 2_AGE : 175|8@0+ (1,0) [0|255] "" XXX + SG_ 2_COAST_AGE : 179|4@0+ (1,0) [0|15] "" XXX + SG_ 2_STATE : 182|3@0+ (1,0) [0|7] "" XXX + SG_ 2_RCS : 191|8@0- (1,0) [-128|127] "" XXX + SG_ 2_LONG_DIST : 192|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 2_LAT_DIST : 204|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 2_REL_SPEED : 216|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 2_NEW_SIGNAL_4 : 230|2@1+ (1,0) [0|2] "" XXX + SG_ 2_REL_LAT_SPEED : 232|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 2_REL_ACCEL : 246|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + +VAL_ 530 1_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 530 2_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 530 1_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 530 2_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 530 1_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; +VAL_ 530 2_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; + CM_ BO_ 530 "Hyundai/Kia 210-21F radar-track layout: 16 messages with two target slots per 32-byte message (32 tracks total). Two related dialects share the same position, velocity, age, coast-age, and compressed-state core. The detailed dialect also populates STATE, RCS, and NEW_SIGNAL_4; the compact Tucson/Sportage/Santa Cruz dialect leaves those fields zero and uses STATE_ALT. The dialect is independent of the logged raw bus and is not an HDA-level discriminator: HDA2-like routes carried the sustained stream on A-CAN mapped to bus 0, while non-HDA2 routes mapped A-CAN to bus 1. Compact targets populate per-slot NEW_SIGNAL_18/OBJECT_ID extension bytes at bytes 16 and 17; detailed platforms usually leave them zero. Byte 18 and bits 26, 30-31, 39, 55, and 117 remained zero. REL_LAT_SPEED has opposite sign conventions between the detailed and compact dialects. The HKG CAN-FD checksum matched every one of 13,210,543 checked frames."; +CM_ SG_ 530 1_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 530 1_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 530 1_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 530 1_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 530 1_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 530 1_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 530 1_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 530 1_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 530 1_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 530 1_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 530 1_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 530 1_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 530 1_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 530 1_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 530 1_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 530 2_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 530 2_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 530 2_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 530 2_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 530 2_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 530 2_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 530 2_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 530 2_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 530 2_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 530 2_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 530 2_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 530 2_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 530 2_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 530 2_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 530 2_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 530 CHECKSUM "16-bit HKG CAN-FD checksum using the CAN address as the data ID."; +CM_ SG_ 530 COUNTER "8-bit transmit-cycle counter. It covers 0-255 and wraps."; + +BO_ 531 RADAR_TRACK_213: 32 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ 1_STATE_ALT : 25|2@0+ (1,0) [0|3] "" XXX + SG_ 1_MOTION_STATE : 29|3@0+ (1,0) [0|4] "" XXX + SG_ 1_TRACK_QUALITY : 38|7@0+ (1,0) [0|127] "" XXX + SG_ 1_AGE : 47|8@0+ (1,0) [0|255] "" XXX + SG_ 1_COAST_AGE : 51|4@0+ (1,0) [0|15] "" XXX + SG_ 1_STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ 1_RCS : 63|8@0- (1,0) [-128|127] "" XXX + SG_ 1_LONG_DIST : 64|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 1_LAT_DIST : 76|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 1_REL_SPEED : 88|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 1_NEW_SIGNAL_4 : 102|2@1+ (1,0) [0|2] "" XXX + SG_ 1_REL_LAT_SPEED : 104|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 1_REL_ACCEL : 118|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + SG_ 1_NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|1] "" XXX + SG_ 1_OBJECT_ID : 135|6@0+ (1,0) [0|63] "" XXX + SG_ 2_NEW_SIGNAL_18 : 137|2@0+ (1,0) [0|1] "" XXX + SG_ 2_OBJECT_ID : 143|6@0+ (1,0) [0|63] "" XXX + SG_ 2_STATE_ALT : 153|2@0+ (1,0) [0|3] "" XXX + SG_ 2_MOTION_STATE : 157|3@0+ (1,0) [0|4] "" XXX + SG_ 2_TRACK_QUALITY : 166|7@0+ (1,0) [0|127] "" XXX + SG_ 2_AGE : 175|8@0+ (1,0) [0|255] "" XXX + SG_ 2_COAST_AGE : 179|4@0+ (1,0) [0|15] "" XXX + SG_ 2_STATE : 182|3@0+ (1,0) [0|7] "" XXX + SG_ 2_RCS : 191|8@0- (1,0) [-128|127] "" XXX + SG_ 2_LONG_DIST : 192|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 2_LAT_DIST : 204|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 2_REL_SPEED : 216|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 2_NEW_SIGNAL_4 : 230|2@1+ (1,0) [0|2] "" XXX + SG_ 2_REL_LAT_SPEED : 232|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 2_REL_ACCEL : 246|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + +VAL_ 531 1_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 531 2_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 531 1_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 531 2_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 531 1_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; +VAL_ 531 2_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; + CM_ BO_ 531 "Hyundai/Kia 210-21F radar-track layout: 16 messages with two target slots per 32-byte message (32 tracks total). Two related dialects share the same position, velocity, age, coast-age, and compressed-state core. The detailed dialect also populates STATE, RCS, and NEW_SIGNAL_4; the compact Tucson/Sportage/Santa Cruz dialect leaves those fields zero and uses STATE_ALT. The dialect is independent of the logged raw bus and is not an HDA-level discriminator: HDA2-like routes carried the sustained stream on A-CAN mapped to bus 0, while non-HDA2 routes mapped A-CAN to bus 1. Compact targets populate per-slot NEW_SIGNAL_18/OBJECT_ID extension bytes at bytes 16 and 17; detailed platforms usually leave them zero. Byte 18 and bits 26, 30-31, 39, 55, and 117 remained zero. REL_LAT_SPEED has opposite sign conventions between the detailed and compact dialects. The HKG CAN-FD checksum matched every one of 13,210,543 checked frames."; +CM_ SG_ 531 1_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 531 1_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 531 1_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 531 1_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 531 1_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 531 1_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 531 1_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 531 1_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 531 1_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 531 1_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 531 1_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 531 1_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 531 1_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 531 1_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 531 1_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 531 2_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 531 2_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 531 2_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 531 2_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 531 2_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 531 2_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 531 2_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 531 2_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 531 2_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 531 2_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 531 2_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 531 2_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 531 2_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 531 2_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 531 2_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 531 CHECKSUM "16-bit HKG CAN-FD checksum using the CAN address as the data ID."; +CM_ SG_ 531 COUNTER "8-bit transmit-cycle counter. It covers 0-255 and wraps."; + +BO_ 532 RADAR_TRACK_214: 32 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ 1_STATE_ALT : 25|2@0+ (1,0) [0|3] "" XXX + SG_ 1_MOTION_STATE : 29|3@0+ (1,0) [0|4] "" XXX + SG_ 1_TRACK_QUALITY : 38|7@0+ (1,0) [0|127] "" XXX + SG_ 1_AGE : 47|8@0+ (1,0) [0|255] "" XXX + SG_ 1_COAST_AGE : 51|4@0+ (1,0) [0|15] "" XXX + SG_ 1_STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ 1_RCS : 63|8@0- (1,0) [-128|127] "" XXX + SG_ 1_LONG_DIST : 64|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 1_LAT_DIST : 76|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 1_REL_SPEED : 88|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 1_NEW_SIGNAL_4 : 102|2@1+ (1,0) [0|2] "" XXX + SG_ 1_REL_LAT_SPEED : 104|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 1_REL_ACCEL : 118|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + SG_ 1_NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|1] "" XXX + SG_ 1_OBJECT_ID : 135|6@0+ (1,0) [0|63] "" XXX + SG_ 2_NEW_SIGNAL_18 : 137|2@0+ (1,0) [0|1] "" XXX + SG_ 2_OBJECT_ID : 143|6@0+ (1,0) [0|63] "" XXX + SG_ 2_STATE_ALT : 153|2@0+ (1,0) [0|3] "" XXX + SG_ 2_MOTION_STATE : 157|3@0+ (1,0) [0|4] "" XXX + SG_ 2_TRACK_QUALITY : 166|7@0+ (1,0) [0|127] "" XXX + SG_ 2_AGE : 175|8@0+ (1,0) [0|255] "" XXX + SG_ 2_COAST_AGE : 179|4@0+ (1,0) [0|15] "" XXX + SG_ 2_STATE : 182|3@0+ (1,0) [0|7] "" XXX + SG_ 2_RCS : 191|8@0- (1,0) [-128|127] "" XXX + SG_ 2_LONG_DIST : 192|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 2_LAT_DIST : 204|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 2_REL_SPEED : 216|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 2_NEW_SIGNAL_4 : 230|2@1+ (1,0) [0|2] "" XXX + SG_ 2_REL_LAT_SPEED : 232|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 2_REL_ACCEL : 246|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + +VAL_ 532 1_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 532 2_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 532 1_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 532 2_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 532 1_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; +VAL_ 532 2_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; + CM_ BO_ 532 "Hyundai/Kia 210-21F radar-track layout: 16 messages with two target slots per 32-byte message (32 tracks total). Two related dialects share the same position, velocity, age, coast-age, and compressed-state core. The detailed dialect also populates STATE, RCS, and NEW_SIGNAL_4; the compact Tucson/Sportage/Santa Cruz dialect leaves those fields zero and uses STATE_ALT. The dialect is independent of the logged raw bus and is not an HDA-level discriminator: HDA2-like routes carried the sustained stream on A-CAN mapped to bus 0, while non-HDA2 routes mapped A-CAN to bus 1. Compact targets populate per-slot NEW_SIGNAL_18/OBJECT_ID extension bytes at bytes 16 and 17; detailed platforms usually leave them zero. Byte 18 and bits 26, 30-31, 39, 55, and 117 remained zero. REL_LAT_SPEED has opposite sign conventions between the detailed and compact dialects. The HKG CAN-FD checksum matched every one of 13,210,543 checked frames."; +CM_ SG_ 532 1_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 532 1_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 532 1_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 532 1_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 532 1_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 532 1_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 532 1_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 532 1_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 532 1_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 532 1_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 532 1_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 532 1_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 532 1_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 532 1_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 532 1_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 532 2_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 532 2_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 532 2_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 532 2_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 532 2_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 532 2_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 532 2_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 532 2_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 532 2_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 532 2_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 532 2_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 532 2_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 532 2_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 532 2_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 532 2_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 532 CHECKSUM "16-bit HKG CAN-FD checksum using the CAN address as the data ID."; +CM_ SG_ 532 COUNTER "8-bit transmit-cycle counter. It covers 0-255 and wraps."; + +BO_ 533 RADAR_TRACK_215: 32 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ 1_STATE_ALT : 25|2@0+ (1,0) [0|3] "" XXX + SG_ 1_MOTION_STATE : 29|3@0+ (1,0) [0|4] "" XXX + SG_ 1_TRACK_QUALITY : 38|7@0+ (1,0) [0|127] "" XXX + SG_ 1_AGE : 47|8@0+ (1,0) [0|255] "" XXX + SG_ 1_COAST_AGE : 51|4@0+ (1,0) [0|15] "" XXX + SG_ 1_STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ 1_RCS : 63|8@0- (1,0) [-128|127] "" XXX + SG_ 1_LONG_DIST : 64|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 1_LAT_DIST : 76|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 1_REL_SPEED : 88|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 1_NEW_SIGNAL_4 : 102|2@1+ (1,0) [0|2] "" XXX + SG_ 1_REL_LAT_SPEED : 104|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 1_REL_ACCEL : 118|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + SG_ 1_NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|1] "" XXX + SG_ 1_OBJECT_ID : 135|6@0+ (1,0) [0|63] "" XXX + SG_ 2_NEW_SIGNAL_18 : 137|2@0+ (1,0) [0|1] "" XXX + SG_ 2_OBJECT_ID : 143|6@0+ (1,0) [0|63] "" XXX + SG_ 2_STATE_ALT : 153|2@0+ (1,0) [0|3] "" XXX + SG_ 2_MOTION_STATE : 157|3@0+ (1,0) [0|4] "" XXX + SG_ 2_TRACK_QUALITY : 166|7@0+ (1,0) [0|127] "" XXX + SG_ 2_AGE : 175|8@0+ (1,0) [0|255] "" XXX + SG_ 2_COAST_AGE : 179|4@0+ (1,0) [0|15] "" XXX + SG_ 2_STATE : 182|3@0+ (1,0) [0|7] "" XXX + SG_ 2_RCS : 191|8@0- (1,0) [-128|127] "" XXX + SG_ 2_LONG_DIST : 192|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 2_LAT_DIST : 204|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 2_REL_SPEED : 216|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 2_NEW_SIGNAL_4 : 230|2@1+ (1,0) [0|2] "" XXX + SG_ 2_REL_LAT_SPEED : 232|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 2_REL_ACCEL : 246|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + +VAL_ 533 1_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 533 2_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 533 1_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 533 2_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 533 1_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; +VAL_ 533 2_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; + CM_ BO_ 533 "Hyundai/Kia 210-21F radar-track layout: 16 messages with two target slots per 32-byte message (32 tracks total). Two related dialects share the same position, velocity, age, coast-age, and compressed-state core. The detailed dialect also populates STATE, RCS, and NEW_SIGNAL_4; the compact Tucson/Sportage/Santa Cruz dialect leaves those fields zero and uses STATE_ALT. The dialect is independent of the logged raw bus and is not an HDA-level discriminator: HDA2-like routes carried the sustained stream on A-CAN mapped to bus 0, while non-HDA2 routes mapped A-CAN to bus 1. Compact targets populate per-slot NEW_SIGNAL_18/OBJECT_ID extension bytes at bytes 16 and 17; detailed platforms usually leave them zero. Byte 18 and bits 26, 30-31, 39, 55, and 117 remained zero. REL_LAT_SPEED has opposite sign conventions between the detailed and compact dialects. The HKG CAN-FD checksum matched every one of 13,210,543 checked frames."; +CM_ SG_ 533 1_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 533 1_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 533 1_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 533 1_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 533 1_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 533 1_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 533 1_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 533 1_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 533 1_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 533 1_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 533 1_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 533 1_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 533 1_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 533 1_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 533 1_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 533 2_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 533 2_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 533 2_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 533 2_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 533 2_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 533 2_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 533 2_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 533 2_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 533 2_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 533 2_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 533 2_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 533 2_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 533 2_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 533 2_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 533 2_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 533 CHECKSUM "16-bit HKG CAN-FD checksum using the CAN address as the data ID."; +CM_ SG_ 533 COUNTER "8-bit transmit-cycle counter. It covers 0-255 and wraps."; + +BO_ 534 RADAR_TRACK_216: 32 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ 1_STATE_ALT : 25|2@0+ (1,0) [0|3] "" XXX + SG_ 1_MOTION_STATE : 29|3@0+ (1,0) [0|4] "" XXX + SG_ 1_TRACK_QUALITY : 38|7@0+ (1,0) [0|127] "" XXX + SG_ 1_AGE : 47|8@0+ (1,0) [0|255] "" XXX + SG_ 1_COAST_AGE : 51|4@0+ (1,0) [0|15] "" XXX + SG_ 1_STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ 1_RCS : 63|8@0- (1,0) [-128|127] "" XXX + SG_ 1_LONG_DIST : 64|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 1_LAT_DIST : 76|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 1_REL_SPEED : 88|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 1_NEW_SIGNAL_4 : 102|2@1+ (1,0) [0|2] "" XXX + SG_ 1_REL_LAT_SPEED : 104|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 1_REL_ACCEL : 118|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + SG_ 1_NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|1] "" XXX + SG_ 1_OBJECT_ID : 135|6@0+ (1,0) [0|63] "" XXX + SG_ 2_NEW_SIGNAL_18 : 137|2@0+ (1,0) [0|1] "" XXX + SG_ 2_OBJECT_ID : 143|6@0+ (1,0) [0|63] "" XXX + SG_ 2_STATE_ALT : 153|2@0+ (1,0) [0|3] "" XXX + SG_ 2_MOTION_STATE : 157|3@0+ (1,0) [0|4] "" XXX + SG_ 2_TRACK_QUALITY : 166|7@0+ (1,0) [0|127] "" XXX + SG_ 2_AGE : 175|8@0+ (1,0) [0|255] "" XXX + SG_ 2_COAST_AGE : 179|4@0+ (1,0) [0|15] "" XXX + SG_ 2_STATE : 182|3@0+ (1,0) [0|7] "" XXX + SG_ 2_RCS : 191|8@0- (1,0) [-128|127] "" XXX + SG_ 2_LONG_DIST : 192|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 2_LAT_DIST : 204|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 2_REL_SPEED : 216|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 2_NEW_SIGNAL_4 : 230|2@1+ (1,0) [0|2] "" XXX + SG_ 2_REL_LAT_SPEED : 232|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 2_REL_ACCEL : 246|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + +VAL_ 534 1_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 534 2_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 534 1_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 534 2_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 534 1_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; +VAL_ 534 2_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; + CM_ BO_ 534 "Hyundai/Kia 210-21F radar-track layout: 16 messages with two target slots per 32-byte message (32 tracks total). Two related dialects share the same position, velocity, age, coast-age, and compressed-state core. The detailed dialect also populates STATE, RCS, and NEW_SIGNAL_4; the compact Tucson/Sportage/Santa Cruz dialect leaves those fields zero and uses STATE_ALT. The dialect is independent of the logged raw bus and is not an HDA-level discriminator: HDA2-like routes carried the sustained stream on A-CAN mapped to bus 0, while non-HDA2 routes mapped A-CAN to bus 1. Compact targets populate per-slot NEW_SIGNAL_18/OBJECT_ID extension bytes at bytes 16 and 17; detailed platforms usually leave them zero. Byte 18 and bits 26, 30-31, 39, 55, and 117 remained zero. REL_LAT_SPEED has opposite sign conventions between the detailed and compact dialects. The HKG CAN-FD checksum matched every one of 13,210,543 checked frames."; +CM_ SG_ 534 1_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 534 1_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 534 1_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 534 1_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 534 1_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 534 1_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 534 1_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 534 1_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 534 1_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 534 1_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 534 1_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 534 1_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 534 1_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 534 1_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 534 1_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 534 2_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 534 2_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 534 2_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 534 2_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 534 2_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 534 2_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 534 2_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 534 2_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 534 2_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 534 2_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 534 2_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 534 2_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 534 2_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 534 2_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 534 2_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 534 CHECKSUM "16-bit HKG CAN-FD checksum using the CAN address as the data ID."; +CM_ SG_ 534 COUNTER "8-bit transmit-cycle counter. It covers 0-255 and wraps."; + +BO_ 535 RADAR_TRACK_217: 32 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ 1_STATE_ALT : 25|2@0+ (1,0) [0|3] "" XXX + SG_ 1_MOTION_STATE : 29|3@0+ (1,0) [0|4] "" XXX + SG_ 1_TRACK_QUALITY : 38|7@0+ (1,0) [0|127] "" XXX + SG_ 1_AGE : 47|8@0+ (1,0) [0|255] "" XXX + SG_ 1_COAST_AGE : 51|4@0+ (1,0) [0|15] "" XXX + SG_ 1_STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ 1_RCS : 63|8@0- (1,0) [-128|127] "" XXX + SG_ 1_LONG_DIST : 64|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 1_LAT_DIST : 76|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 1_REL_SPEED : 88|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 1_NEW_SIGNAL_4 : 102|2@1+ (1,0) [0|2] "" XXX + SG_ 1_REL_LAT_SPEED : 104|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 1_REL_ACCEL : 118|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + SG_ 1_NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|1] "" XXX + SG_ 1_OBJECT_ID : 135|6@0+ (1,0) [0|63] "" XXX + SG_ 2_NEW_SIGNAL_18 : 137|2@0+ (1,0) [0|1] "" XXX + SG_ 2_OBJECT_ID : 143|6@0+ (1,0) [0|63] "" XXX + SG_ 2_STATE_ALT : 153|2@0+ (1,0) [0|3] "" XXX + SG_ 2_MOTION_STATE : 157|3@0+ (1,0) [0|4] "" XXX + SG_ 2_TRACK_QUALITY : 166|7@0+ (1,0) [0|127] "" XXX + SG_ 2_AGE : 175|8@0+ (1,0) [0|255] "" XXX + SG_ 2_COAST_AGE : 179|4@0+ (1,0) [0|15] "" XXX + SG_ 2_STATE : 182|3@0+ (1,0) [0|7] "" XXX + SG_ 2_RCS : 191|8@0- (1,0) [-128|127] "" XXX + SG_ 2_LONG_DIST : 192|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 2_LAT_DIST : 204|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 2_REL_SPEED : 216|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 2_NEW_SIGNAL_4 : 230|2@1+ (1,0) [0|2] "" XXX + SG_ 2_REL_LAT_SPEED : 232|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 2_REL_ACCEL : 246|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + +VAL_ 535 1_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 535 2_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 535 1_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 535 2_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 535 1_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; +VAL_ 535 2_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; + CM_ BO_ 535 "Hyundai/Kia 210-21F radar-track layout: 16 messages with two target slots per 32-byte message (32 tracks total). Two related dialects share the same position, velocity, age, coast-age, and compressed-state core. The detailed dialect also populates STATE, RCS, and NEW_SIGNAL_4; the compact Tucson/Sportage/Santa Cruz dialect leaves those fields zero and uses STATE_ALT. The dialect is independent of the logged raw bus and is not an HDA-level discriminator: HDA2-like routes carried the sustained stream on A-CAN mapped to bus 0, while non-HDA2 routes mapped A-CAN to bus 1. Compact targets populate per-slot NEW_SIGNAL_18/OBJECT_ID extension bytes at bytes 16 and 17; detailed platforms usually leave them zero. Byte 18 and bits 26, 30-31, 39, 55, and 117 remained zero. REL_LAT_SPEED has opposite sign conventions between the detailed and compact dialects. The HKG CAN-FD checksum matched every one of 13,210,543 checked frames."; +CM_ SG_ 535 1_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 535 1_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 535 1_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 535 1_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 535 1_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 535 1_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 535 1_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 535 1_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 535 1_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 535 1_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 535 1_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 535 1_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 535 1_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 535 1_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 535 1_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 535 2_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 535 2_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 535 2_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 535 2_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 535 2_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 535 2_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 535 2_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 535 2_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 535 2_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 535 2_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 535 2_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 535 2_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 535 2_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 535 2_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 535 2_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 535 CHECKSUM "16-bit HKG CAN-FD checksum using the CAN address as the data ID."; +CM_ SG_ 535 COUNTER "8-bit transmit-cycle counter. It covers 0-255 and wraps."; + +BO_ 536 RADAR_TRACK_218: 32 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ 1_STATE_ALT : 25|2@0+ (1,0) [0|3] "" XXX + SG_ 1_MOTION_STATE : 29|3@0+ (1,0) [0|4] "" XXX + SG_ 1_TRACK_QUALITY : 38|7@0+ (1,0) [0|127] "" XXX + SG_ 1_AGE : 47|8@0+ (1,0) [0|255] "" XXX + SG_ 1_COAST_AGE : 51|4@0+ (1,0) [0|15] "" XXX + SG_ 1_STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ 1_RCS : 63|8@0- (1,0) [-128|127] "" XXX + SG_ 1_LONG_DIST : 64|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 1_LAT_DIST : 76|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 1_REL_SPEED : 88|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 1_NEW_SIGNAL_4 : 102|2@1+ (1,0) [0|2] "" XXX + SG_ 1_REL_LAT_SPEED : 104|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 1_REL_ACCEL : 118|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + SG_ 1_NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|1] "" XXX + SG_ 1_OBJECT_ID : 135|6@0+ (1,0) [0|63] "" XXX + SG_ 2_NEW_SIGNAL_18 : 137|2@0+ (1,0) [0|1] "" XXX + SG_ 2_OBJECT_ID : 143|6@0+ (1,0) [0|63] "" XXX + SG_ 2_STATE_ALT : 153|2@0+ (1,0) [0|3] "" XXX + SG_ 2_MOTION_STATE : 157|3@0+ (1,0) [0|4] "" XXX + SG_ 2_TRACK_QUALITY : 166|7@0+ (1,0) [0|127] "" XXX + SG_ 2_AGE : 175|8@0+ (1,0) [0|255] "" XXX + SG_ 2_COAST_AGE : 179|4@0+ (1,0) [0|15] "" XXX + SG_ 2_STATE : 182|3@0+ (1,0) [0|7] "" XXX + SG_ 2_RCS : 191|8@0- (1,0) [-128|127] "" XXX + SG_ 2_LONG_DIST : 192|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 2_LAT_DIST : 204|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 2_REL_SPEED : 216|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 2_NEW_SIGNAL_4 : 230|2@1+ (1,0) [0|2] "" XXX + SG_ 2_REL_LAT_SPEED : 232|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 2_REL_ACCEL : 246|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + +VAL_ 536 1_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 536 2_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 536 1_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 536 2_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 536 1_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; +VAL_ 536 2_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; + CM_ BO_ 536 "Hyundai/Kia 210-21F radar-track layout: 16 messages with two target slots per 32-byte message (32 tracks total). Two related dialects share the same position, velocity, age, coast-age, and compressed-state core. The detailed dialect also populates STATE, RCS, and NEW_SIGNAL_4; the compact Tucson/Sportage/Santa Cruz dialect leaves those fields zero and uses STATE_ALT. The dialect is independent of the logged raw bus and is not an HDA-level discriminator: HDA2-like routes carried the sustained stream on A-CAN mapped to bus 0, while non-HDA2 routes mapped A-CAN to bus 1. Compact targets populate per-slot NEW_SIGNAL_18/OBJECT_ID extension bytes at bytes 16 and 17; detailed platforms usually leave them zero. Byte 18 and bits 26, 30-31, 39, 55, and 117 remained zero. REL_LAT_SPEED has opposite sign conventions between the detailed and compact dialects. The HKG CAN-FD checksum matched every one of 13,210,543 checked frames."; +CM_ SG_ 536 1_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 536 1_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 536 1_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 536 1_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 536 1_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 536 1_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 536 1_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 536 1_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 536 1_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 536 1_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 536 1_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 536 1_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 536 1_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 536 1_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 536 1_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 536 2_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 536 2_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 536 2_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 536 2_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 536 2_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 536 2_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 536 2_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 536 2_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 536 2_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 536 2_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 536 2_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 536 2_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 536 2_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 536 2_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 536 2_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 536 CHECKSUM "16-bit HKG CAN-FD checksum using the CAN address as the data ID."; +CM_ SG_ 536 COUNTER "8-bit transmit-cycle counter. It covers 0-255 and wraps."; + +BO_ 537 RADAR_TRACK_219: 32 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ 1_STATE_ALT : 25|2@0+ (1,0) [0|3] "" XXX + SG_ 1_MOTION_STATE : 29|3@0+ (1,0) [0|4] "" XXX + SG_ 1_TRACK_QUALITY : 38|7@0+ (1,0) [0|127] "" XXX + SG_ 1_AGE : 47|8@0+ (1,0) [0|255] "" XXX + SG_ 1_COAST_AGE : 51|4@0+ (1,0) [0|15] "" XXX + SG_ 1_STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ 1_RCS : 63|8@0- (1,0) [-128|127] "" XXX + SG_ 1_LONG_DIST : 64|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 1_LAT_DIST : 76|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 1_REL_SPEED : 88|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 1_NEW_SIGNAL_4 : 102|2@1+ (1,0) [0|2] "" XXX + SG_ 1_REL_LAT_SPEED : 104|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 1_REL_ACCEL : 118|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + SG_ 1_NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|1] "" XXX + SG_ 1_OBJECT_ID : 135|6@0+ (1,0) [0|63] "" XXX + SG_ 2_NEW_SIGNAL_18 : 137|2@0+ (1,0) [0|1] "" XXX + SG_ 2_OBJECT_ID : 143|6@0+ (1,0) [0|63] "" XXX + SG_ 2_STATE_ALT : 153|2@0+ (1,0) [0|3] "" XXX + SG_ 2_MOTION_STATE : 157|3@0+ (1,0) [0|4] "" XXX + SG_ 2_TRACK_QUALITY : 166|7@0+ (1,0) [0|127] "" XXX + SG_ 2_AGE : 175|8@0+ (1,0) [0|255] "" XXX + SG_ 2_COAST_AGE : 179|4@0+ (1,0) [0|15] "" XXX + SG_ 2_STATE : 182|3@0+ (1,0) [0|7] "" XXX + SG_ 2_RCS : 191|8@0- (1,0) [-128|127] "" XXX + SG_ 2_LONG_DIST : 192|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 2_LAT_DIST : 204|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 2_REL_SPEED : 216|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 2_NEW_SIGNAL_4 : 230|2@1+ (1,0) [0|2] "" XXX + SG_ 2_REL_LAT_SPEED : 232|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 2_REL_ACCEL : 246|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + +VAL_ 537 1_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 537 2_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 537 1_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 537 2_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 537 1_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; +VAL_ 537 2_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; + CM_ BO_ 537 "Hyundai/Kia 210-21F radar-track layout: 16 messages with two target slots per 32-byte message (32 tracks total). Two related dialects share the same position, velocity, age, coast-age, and compressed-state core. The detailed dialect also populates STATE, RCS, and NEW_SIGNAL_4; the compact Tucson/Sportage/Santa Cruz dialect leaves those fields zero and uses STATE_ALT. The dialect is independent of the logged raw bus and is not an HDA-level discriminator: HDA2-like routes carried the sustained stream on A-CAN mapped to bus 0, while non-HDA2 routes mapped A-CAN to bus 1. Compact targets populate per-slot NEW_SIGNAL_18/OBJECT_ID extension bytes at bytes 16 and 17; detailed platforms usually leave them zero. Byte 18 and bits 26, 30-31, 39, 55, and 117 remained zero. REL_LAT_SPEED has opposite sign conventions between the detailed and compact dialects. The HKG CAN-FD checksum matched every one of 13,210,543 checked frames."; +CM_ SG_ 537 1_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 537 1_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 537 1_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 537 1_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 537 1_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 537 1_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 537 1_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 537 1_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 537 1_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 537 1_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 537 1_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 537 1_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 537 1_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 537 1_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 537 1_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 537 2_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 537 2_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 537 2_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 537 2_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 537 2_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 537 2_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 537 2_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 537 2_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 537 2_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 537 2_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 537 2_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 537 2_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 537 2_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 537 2_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 537 2_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 537 CHECKSUM "16-bit HKG CAN-FD checksum using the CAN address as the data ID."; +CM_ SG_ 537 COUNTER "8-bit transmit-cycle counter. It covers 0-255 and wraps."; + +BO_ 538 RADAR_TRACK_21a: 32 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ 1_STATE_ALT : 25|2@0+ (1,0) [0|3] "" XXX + SG_ 1_MOTION_STATE : 29|3@0+ (1,0) [0|4] "" XXX + SG_ 1_TRACK_QUALITY : 38|7@0+ (1,0) [0|127] "" XXX + SG_ 1_AGE : 47|8@0+ (1,0) [0|255] "" XXX + SG_ 1_COAST_AGE : 51|4@0+ (1,0) [0|15] "" XXX + SG_ 1_STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ 1_RCS : 63|8@0- (1,0) [-128|127] "" XXX + SG_ 1_LONG_DIST : 64|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 1_LAT_DIST : 76|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 1_REL_SPEED : 88|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 1_NEW_SIGNAL_4 : 102|2@1+ (1,0) [0|2] "" XXX + SG_ 1_REL_LAT_SPEED : 104|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 1_REL_ACCEL : 118|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + SG_ 1_NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|1] "" XXX + SG_ 1_OBJECT_ID : 135|6@0+ (1,0) [0|63] "" XXX + SG_ 2_NEW_SIGNAL_18 : 137|2@0+ (1,0) [0|1] "" XXX + SG_ 2_OBJECT_ID : 143|6@0+ (1,0) [0|63] "" XXX + SG_ 2_STATE_ALT : 153|2@0+ (1,0) [0|3] "" XXX + SG_ 2_MOTION_STATE : 157|3@0+ (1,0) [0|4] "" XXX + SG_ 2_TRACK_QUALITY : 166|7@0+ (1,0) [0|127] "" XXX + SG_ 2_AGE : 175|8@0+ (1,0) [0|255] "" XXX + SG_ 2_COAST_AGE : 179|4@0+ (1,0) [0|15] "" XXX + SG_ 2_STATE : 182|3@0+ (1,0) [0|7] "" XXX + SG_ 2_RCS : 191|8@0- (1,0) [-128|127] "" XXX + SG_ 2_LONG_DIST : 192|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 2_LAT_DIST : 204|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 2_REL_SPEED : 216|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 2_NEW_SIGNAL_4 : 230|2@1+ (1,0) [0|2] "" XXX + SG_ 2_REL_LAT_SPEED : 232|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 2_REL_ACCEL : 246|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + +VAL_ 538 1_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 538 2_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 538 1_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 538 2_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 538 1_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; +VAL_ 538 2_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; + CM_ BO_ 538 "Hyundai/Kia 210-21F radar-track layout: 16 messages with two target slots per 32-byte message (32 tracks total). Two related dialects share the same position, velocity, age, coast-age, and compressed-state core. The detailed dialect also populates STATE, RCS, and NEW_SIGNAL_4; the compact Tucson/Sportage/Santa Cruz dialect leaves those fields zero and uses STATE_ALT. The dialect is independent of the logged raw bus and is not an HDA-level discriminator: HDA2-like routes carried the sustained stream on A-CAN mapped to bus 0, while non-HDA2 routes mapped A-CAN to bus 1. Compact targets populate per-slot NEW_SIGNAL_18/OBJECT_ID extension bytes at bytes 16 and 17; detailed platforms usually leave them zero. Byte 18 and bits 26, 30-31, 39, 55, and 117 remained zero. REL_LAT_SPEED has opposite sign conventions between the detailed and compact dialects. The HKG CAN-FD checksum matched every one of 13,210,543 checked frames."; +CM_ SG_ 538 1_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 538 1_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 538 1_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 538 1_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 538 1_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 538 1_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 538 1_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 538 1_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 538 1_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 538 1_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 538 1_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 538 1_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 538 1_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 538 1_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 538 1_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 538 2_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 538 2_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 538 2_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 538 2_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 538 2_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 538 2_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 538 2_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 538 2_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 538 2_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 538 2_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 538 2_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 538 2_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 538 2_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 538 2_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 538 2_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 538 CHECKSUM "16-bit HKG CAN-FD checksum using the CAN address as the data ID."; +CM_ SG_ 538 COUNTER "8-bit transmit-cycle counter. It covers 0-255 and wraps."; + +BO_ 539 RADAR_TRACK_21b: 32 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ 1_STATE_ALT : 25|2@0+ (1,0) [0|3] "" XXX + SG_ 1_MOTION_STATE : 29|3@0+ (1,0) [0|4] "" XXX + SG_ 1_TRACK_QUALITY : 38|7@0+ (1,0) [0|127] "" XXX + SG_ 1_AGE : 47|8@0+ (1,0) [0|255] "" XXX + SG_ 1_COAST_AGE : 51|4@0+ (1,0) [0|15] "" XXX + SG_ 1_STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ 1_RCS : 63|8@0- (1,0) [-128|127] "" XXX + SG_ 1_LONG_DIST : 64|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 1_LAT_DIST : 76|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 1_REL_SPEED : 88|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 1_NEW_SIGNAL_4 : 102|2@1+ (1,0) [0|2] "" XXX + SG_ 1_REL_LAT_SPEED : 104|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 1_REL_ACCEL : 118|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + SG_ 1_NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|1] "" XXX + SG_ 1_OBJECT_ID : 135|6@0+ (1,0) [0|63] "" XXX + SG_ 2_NEW_SIGNAL_18 : 137|2@0+ (1,0) [0|1] "" XXX + SG_ 2_OBJECT_ID : 143|6@0+ (1,0) [0|63] "" XXX + SG_ 2_STATE_ALT : 153|2@0+ (1,0) [0|3] "" XXX + SG_ 2_MOTION_STATE : 157|3@0+ (1,0) [0|4] "" XXX + SG_ 2_TRACK_QUALITY : 166|7@0+ (1,0) [0|127] "" XXX + SG_ 2_AGE : 175|8@0+ (1,0) [0|255] "" XXX + SG_ 2_COAST_AGE : 179|4@0+ (1,0) [0|15] "" XXX + SG_ 2_STATE : 182|3@0+ (1,0) [0|7] "" XXX + SG_ 2_RCS : 191|8@0- (1,0) [-128|127] "" XXX + SG_ 2_LONG_DIST : 192|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 2_LAT_DIST : 204|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 2_REL_SPEED : 216|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 2_NEW_SIGNAL_4 : 230|2@1+ (1,0) [0|2] "" XXX + SG_ 2_REL_LAT_SPEED : 232|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 2_REL_ACCEL : 246|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + +VAL_ 539 1_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 539 2_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 539 1_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 539 2_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 539 1_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; +VAL_ 539 2_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; + CM_ BO_ 539 "Hyundai/Kia 210-21F radar-track layout: 16 messages with two target slots per 32-byte message (32 tracks total). Two related dialects share the same position, velocity, age, coast-age, and compressed-state core. The detailed dialect also populates STATE, RCS, and NEW_SIGNAL_4; the compact Tucson/Sportage/Santa Cruz dialect leaves those fields zero and uses STATE_ALT. The dialect is independent of the logged raw bus and is not an HDA-level discriminator: HDA2-like routes carried the sustained stream on A-CAN mapped to bus 0, while non-HDA2 routes mapped A-CAN to bus 1. Compact targets populate per-slot NEW_SIGNAL_18/OBJECT_ID extension bytes at bytes 16 and 17; detailed platforms usually leave them zero. Byte 18 and bits 26, 30-31, 39, 55, and 117 remained zero. REL_LAT_SPEED has opposite sign conventions between the detailed and compact dialects. The HKG CAN-FD checksum matched every one of 13,210,543 checked frames."; +CM_ SG_ 539 1_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 539 1_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 539 1_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 539 1_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 539 1_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 539 1_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 539 1_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 539 1_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 539 1_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 539 1_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 539 1_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 539 1_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 539 1_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 539 1_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 539 1_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 539 2_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 539 2_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 539 2_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 539 2_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 539 2_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 539 2_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 539 2_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 539 2_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 539 2_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 539 2_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 539 2_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 539 2_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 539 2_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 539 2_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 539 2_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 539 CHECKSUM "16-bit HKG CAN-FD checksum using the CAN address as the data ID."; +CM_ SG_ 539 COUNTER "8-bit transmit-cycle counter. It covers 0-255 and wraps."; + +BO_ 540 RADAR_TRACK_21c: 32 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ 1_STATE_ALT : 25|2@0+ (1,0) [0|3] "" XXX + SG_ 1_MOTION_STATE : 29|3@0+ (1,0) [0|4] "" XXX + SG_ 1_TRACK_QUALITY : 38|7@0+ (1,0) [0|127] "" XXX + SG_ 1_AGE : 47|8@0+ (1,0) [0|255] "" XXX + SG_ 1_COAST_AGE : 51|4@0+ (1,0) [0|15] "" XXX + SG_ 1_STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ 1_RCS : 63|8@0- (1,0) [-128|127] "" XXX + SG_ 1_LONG_DIST : 64|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 1_LAT_DIST : 76|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 1_REL_SPEED : 88|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 1_NEW_SIGNAL_4 : 102|2@1+ (1,0) [0|2] "" XXX + SG_ 1_REL_LAT_SPEED : 104|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 1_REL_ACCEL : 118|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + SG_ 1_NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|1] "" XXX + SG_ 1_OBJECT_ID : 135|6@0+ (1,0) [0|63] "" XXX + SG_ 2_NEW_SIGNAL_18 : 137|2@0+ (1,0) [0|1] "" XXX + SG_ 2_OBJECT_ID : 143|6@0+ (1,0) [0|63] "" XXX + SG_ 2_STATE_ALT : 153|2@0+ (1,0) [0|3] "" XXX + SG_ 2_MOTION_STATE : 157|3@0+ (1,0) [0|4] "" XXX + SG_ 2_TRACK_QUALITY : 166|7@0+ (1,0) [0|127] "" XXX + SG_ 2_AGE : 175|8@0+ (1,0) [0|255] "" XXX + SG_ 2_COAST_AGE : 179|4@0+ (1,0) [0|15] "" XXX + SG_ 2_STATE : 182|3@0+ (1,0) [0|7] "" XXX + SG_ 2_RCS : 191|8@0- (1,0) [-128|127] "" XXX + SG_ 2_LONG_DIST : 192|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 2_LAT_DIST : 204|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 2_REL_SPEED : 216|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 2_NEW_SIGNAL_4 : 230|2@1+ (1,0) [0|2] "" XXX + SG_ 2_REL_LAT_SPEED : 232|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 2_REL_ACCEL : 246|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + +VAL_ 540 1_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 540 2_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 540 1_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 540 2_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 540 1_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; +VAL_ 540 2_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; + CM_ BO_ 540 "Hyundai/Kia 210-21F radar-track layout: 16 messages with two target slots per 32-byte message (32 tracks total). Two related dialects share the same position, velocity, age, coast-age, and compressed-state core. The detailed dialect also populates STATE, RCS, and NEW_SIGNAL_4; the compact Tucson/Sportage/Santa Cruz dialect leaves those fields zero and uses STATE_ALT. The dialect is independent of the logged raw bus and is not an HDA-level discriminator: HDA2-like routes carried the sustained stream on A-CAN mapped to bus 0, while non-HDA2 routes mapped A-CAN to bus 1. Compact targets populate per-slot NEW_SIGNAL_18/OBJECT_ID extension bytes at bytes 16 and 17; detailed platforms usually leave them zero. Byte 18 and bits 26, 30-31, 39, 55, and 117 remained zero. REL_LAT_SPEED has opposite sign conventions between the detailed and compact dialects. The HKG CAN-FD checksum matched every one of 13,210,543 checked frames."; +CM_ SG_ 540 1_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 540 1_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 540 1_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 540 1_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 540 1_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 540 1_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 540 1_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 540 1_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 540 1_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 540 1_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 540 1_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 540 1_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 540 1_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 540 1_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 540 1_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 540 2_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 540 2_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 540 2_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 540 2_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 540 2_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 540 2_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 540 2_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 540 2_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 540 2_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 540 2_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 540 2_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 540 2_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 540 2_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 540 2_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 540 2_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 540 CHECKSUM "16-bit HKG CAN-FD checksum using the CAN address as the data ID."; +CM_ SG_ 540 COUNTER "8-bit transmit-cycle counter. It covers 0-255 and wraps."; + +BO_ 541 RADAR_TRACK_21d: 32 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ 1_STATE_ALT : 25|2@0+ (1,0) [0|3] "" XXX + SG_ 1_MOTION_STATE : 29|3@0+ (1,0) [0|4] "" XXX + SG_ 1_TRACK_QUALITY : 38|7@0+ (1,0) [0|127] "" XXX + SG_ 1_AGE : 47|8@0+ (1,0) [0|255] "" XXX + SG_ 1_COAST_AGE : 51|4@0+ (1,0) [0|15] "" XXX + SG_ 1_STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ 1_RCS : 63|8@0- (1,0) [-128|127] "" XXX + SG_ 1_LONG_DIST : 64|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 1_LAT_DIST : 76|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 1_REL_SPEED : 88|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 1_NEW_SIGNAL_4 : 102|2@1+ (1,0) [0|2] "" XXX + SG_ 1_REL_LAT_SPEED : 104|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 1_REL_ACCEL : 118|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + SG_ 1_NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|1] "" XXX + SG_ 1_OBJECT_ID : 135|6@0+ (1,0) [0|63] "" XXX + SG_ 2_NEW_SIGNAL_18 : 137|2@0+ (1,0) [0|1] "" XXX + SG_ 2_OBJECT_ID : 143|6@0+ (1,0) [0|63] "" XXX + SG_ 2_STATE_ALT : 153|2@0+ (1,0) [0|3] "" XXX + SG_ 2_MOTION_STATE : 157|3@0+ (1,0) [0|4] "" XXX + SG_ 2_TRACK_QUALITY : 166|7@0+ (1,0) [0|127] "" XXX + SG_ 2_AGE : 175|8@0+ (1,0) [0|255] "" XXX + SG_ 2_COAST_AGE : 179|4@0+ (1,0) [0|15] "" XXX + SG_ 2_STATE : 182|3@0+ (1,0) [0|7] "" XXX + SG_ 2_RCS : 191|8@0- (1,0) [-128|127] "" XXX + SG_ 2_LONG_DIST : 192|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 2_LAT_DIST : 204|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 2_REL_SPEED : 216|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 2_NEW_SIGNAL_4 : 230|2@1+ (1,0) [0|2] "" XXX + SG_ 2_REL_LAT_SPEED : 232|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 2_REL_ACCEL : 246|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + +VAL_ 541 1_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 541 2_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 541 1_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 541 2_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 541 1_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; +VAL_ 541 2_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; + CM_ BO_ 541 "Hyundai/Kia 210-21F radar-track layout: 16 messages with two target slots per 32-byte message (32 tracks total). Two related dialects share the same position, velocity, age, coast-age, and compressed-state core. The detailed dialect also populates STATE, RCS, and NEW_SIGNAL_4; the compact Tucson/Sportage/Santa Cruz dialect leaves those fields zero and uses STATE_ALT. The dialect is independent of the logged raw bus and is not an HDA-level discriminator: HDA2-like routes carried the sustained stream on A-CAN mapped to bus 0, while non-HDA2 routes mapped A-CAN to bus 1. Compact targets populate per-slot NEW_SIGNAL_18/OBJECT_ID extension bytes at bytes 16 and 17; detailed platforms usually leave them zero. Byte 18 and bits 26, 30-31, 39, 55, and 117 remained zero. REL_LAT_SPEED has opposite sign conventions between the detailed and compact dialects. The HKG CAN-FD checksum matched every one of 13,210,543 checked frames."; +CM_ SG_ 541 1_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 541 1_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 541 1_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 541 1_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 541 1_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 541 1_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 541 1_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 541 1_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 541 1_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 541 1_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 541 1_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 541 1_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 541 1_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 541 1_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 541 1_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 541 2_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 541 2_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 541 2_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 541 2_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 541 2_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 541 2_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 541 2_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 541 2_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 541 2_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 541 2_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 541 2_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 541 2_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 541 2_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 541 2_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 541 2_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 541 CHECKSUM "16-bit HKG CAN-FD checksum using the CAN address as the data ID."; +CM_ SG_ 541 COUNTER "8-bit transmit-cycle counter. It covers 0-255 and wraps."; + +BO_ 542 RADAR_TRACK_21e: 32 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ 1_STATE_ALT : 25|2@0+ (1,0) [0|3] "" XXX + SG_ 1_MOTION_STATE : 29|3@0+ (1,0) [0|4] "" XXX + SG_ 1_TRACK_QUALITY : 38|7@0+ (1,0) [0|127] "" XXX + SG_ 1_AGE : 47|8@0+ (1,0) [0|255] "" XXX + SG_ 1_COAST_AGE : 51|4@0+ (1,0) [0|15] "" XXX + SG_ 1_STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ 1_RCS : 63|8@0- (1,0) [-128|127] "" XXX + SG_ 1_LONG_DIST : 64|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 1_LAT_DIST : 76|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 1_REL_SPEED : 88|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 1_NEW_SIGNAL_4 : 102|2@1+ (1,0) [0|2] "" XXX + SG_ 1_REL_LAT_SPEED : 104|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 1_REL_ACCEL : 118|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + SG_ 1_NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|1] "" XXX + SG_ 1_OBJECT_ID : 135|6@0+ (1,0) [0|63] "" XXX + SG_ 2_NEW_SIGNAL_18 : 137|2@0+ (1,0) [0|1] "" XXX + SG_ 2_OBJECT_ID : 143|6@0+ (1,0) [0|63] "" XXX + SG_ 2_STATE_ALT : 153|2@0+ (1,0) [0|3] "" XXX + SG_ 2_MOTION_STATE : 157|3@0+ (1,0) [0|4] "" XXX + SG_ 2_TRACK_QUALITY : 166|7@0+ (1,0) [0|127] "" XXX + SG_ 2_AGE : 175|8@0+ (1,0) [0|255] "" XXX + SG_ 2_COAST_AGE : 179|4@0+ (1,0) [0|15] "" XXX + SG_ 2_STATE : 182|3@0+ (1,0) [0|7] "" XXX + SG_ 2_RCS : 191|8@0- (1,0) [-128|127] "" XXX + SG_ 2_LONG_DIST : 192|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 2_LAT_DIST : 204|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 2_REL_SPEED : 216|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 2_NEW_SIGNAL_4 : 230|2@1+ (1,0) [0|2] "" XXX + SG_ 2_REL_LAT_SPEED : 232|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 2_REL_ACCEL : 246|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + +VAL_ 542 1_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 542 2_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 542 1_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 542 2_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 542 1_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; +VAL_ 542 2_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; + CM_ BO_ 542 "Hyundai/Kia 210-21F radar-track layout: 16 messages with two target slots per 32-byte message (32 tracks total). Two related dialects share the same position, velocity, age, coast-age, and compressed-state core. The detailed dialect also populates STATE, RCS, and NEW_SIGNAL_4; the compact Tucson/Sportage/Santa Cruz dialect leaves those fields zero and uses STATE_ALT. The dialect is independent of the logged raw bus and is not an HDA-level discriminator: HDA2-like routes carried the sustained stream on A-CAN mapped to bus 0, while non-HDA2 routes mapped A-CAN to bus 1. Compact targets populate per-slot NEW_SIGNAL_18/OBJECT_ID extension bytes at bytes 16 and 17; detailed platforms usually leave them zero. Byte 18 and bits 26, 30-31, 39, 55, and 117 remained zero. REL_LAT_SPEED has opposite sign conventions between the detailed and compact dialects. The HKG CAN-FD checksum matched every one of 13,210,543 checked frames."; +CM_ SG_ 542 1_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 542 1_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 542 1_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 542 1_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 542 1_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 542 1_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 542 1_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 542 1_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 542 1_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 542 1_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 542 1_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 542 1_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 542 1_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 542 1_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 542 1_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 542 2_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 542 2_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 542 2_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 542 2_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 542 2_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 542 2_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 542 2_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 542 2_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 542 2_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 542 2_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 542 2_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 542 2_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 542 2_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 542 2_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 542 2_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 542 CHECKSUM "16-bit HKG CAN-FD checksum using the CAN address as the data ID."; +CM_ SG_ 542 COUNTER "8-bit transmit-cycle counter. It covers 0-255 and wraps."; + +BO_ 543 RADAR_TRACK_21f: 32 RADAR + SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX + SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX + SG_ 1_STATE_ALT : 25|2@0+ (1,0) [0|3] "" XXX + SG_ 1_MOTION_STATE : 29|3@0+ (1,0) [0|4] "" XXX + SG_ 1_TRACK_QUALITY : 38|7@0+ (1,0) [0|127] "" XXX + SG_ 1_AGE : 47|8@0+ (1,0) [0|255] "" XXX + SG_ 1_COAST_AGE : 51|4@0+ (1,0) [0|15] "" XXX + SG_ 1_STATE : 54|3@0+ (1,0) [0|7] "" XXX + SG_ 1_RCS : 63|8@0- (1,0) [-128|127] "" XXX + SG_ 1_LONG_DIST : 64|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 1_LAT_DIST : 76|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 1_REL_SPEED : 88|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 1_NEW_SIGNAL_4 : 102|2@1+ (1,0) [0|2] "" XXX + SG_ 1_REL_LAT_SPEED : 104|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 1_REL_ACCEL : 118|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + SG_ 1_NEW_SIGNAL_18 : 129|2@0+ (1,0) [0|1] "" XXX + SG_ 1_OBJECT_ID : 135|6@0+ (1,0) [0|63] "" XXX + SG_ 2_NEW_SIGNAL_18 : 137|2@0+ (1,0) [0|1] "" XXX + SG_ 2_OBJECT_ID : 143|6@0+ (1,0) [0|63] "" XXX + SG_ 2_STATE_ALT : 153|2@0+ (1,0) [0|3] "" XXX + SG_ 2_MOTION_STATE : 157|3@0+ (1,0) [0|4] "" XXX + SG_ 2_TRACK_QUALITY : 166|7@0+ (1,0) [0|127] "" XXX + SG_ 2_AGE : 175|8@0+ (1,0) [0|255] "" XXX + SG_ 2_COAST_AGE : 179|4@0+ (1,0) [0|15] "" XXX + SG_ 2_STATE : 182|3@0+ (1,0) [0|7] "" XXX + SG_ 2_RCS : 191|8@0- (1,0) [-128|127] "" XXX + SG_ 2_LONG_DIST : 192|12@1+ (0.05,0) [0|204.75] "m" XXX + SG_ 2_LAT_DIST : 204|12@1- (0.05,0) [-102.4|102.35] "m" XXX + SG_ 2_REL_SPEED : 216|14@1- (0.01,0) [-81.92|81.91] "m/s" XXX + SG_ 2_NEW_SIGNAL_4 : 230|2@1+ (1,0) [0|2] "" XXX + SG_ 2_REL_LAT_SPEED : 232|13@1- (0.01,0) [-40.96|40.95] "m/s" XXX + SG_ 2_REL_ACCEL : 246|10@1- (0.05,0) [-25.6|25.55] "m/s^2" XXX + +VAL_ 543 1_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 543 2_STATE_ALT 0 "EMPTY" 1 "TENTATIVE" 2 "MEASURED" 3 "COASTED" ; +VAL_ 543 1_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 543 2_MOTION_STATE 0 "UNKNOWN" 1 "STATIONARY" 2 "MOVING" 3 "STOPPED" 4 "ONCOMING" ; +VAL_ 543 1_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; +VAL_ 543 2_STATE 0 "EMPTY" 1 "TENTATIVE_1" 2 "TENTATIVE_2" 3 "MEASURED" 4 "COASTED" 7 "UNRESOLVED_TENTATIVE" ; + CM_ BO_ 543 "Hyundai/Kia 210-21F radar-track layout: 16 messages with two target slots per 32-byte message (32 tracks total). Two related dialects share the same position, velocity, age, coast-age, and compressed-state core. The detailed dialect also populates STATE, RCS, and NEW_SIGNAL_4; the compact Tucson/Sportage/Santa Cruz dialect leaves those fields zero and uses STATE_ALT. The dialect is independent of the logged raw bus and is not an HDA-level discriminator: HDA2-like routes carried the sustained stream on A-CAN mapped to bus 0, while non-HDA2 routes mapped A-CAN to bus 1. Compact targets populate per-slot NEW_SIGNAL_18/OBJECT_ID extension bytes at bytes 16 and 17; detailed platforms usually leave them zero. Byte 18 and bits 26, 30-31, 39, 55, and 117 remained zero. REL_LAT_SPEED has opposite sign conventions between the detailed and compact dialects. The HKG CAN-FD checksum matched every one of 13,210,543 checked frames."; +CM_ SG_ 543 1_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 543 1_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 543 1_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 543 1_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 543 1_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 543 1_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 543 1_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 543 1_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 543 1_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 543 1_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 543 1_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 543 1_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 543 1_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 543 1_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 543 1_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 543 2_STATE_ALT "Compressed lifecycle state: 0=empty, 1=tentative, 2=measured, 3=coasted."; +CM_ SG_ 543 2_MOTION_STATE "Ground-frame target-motion class: 0=unknown, 1=stationary, 2=moving, 3=stopped, and 4=oncoming. The stopped label is independently supported by related Hyundai BN7 radar and HMVS4 object DBCs. Values 3/4 were observed only on the compact dialect; the detailed dialect has no dedicated oncoming value."; +CM_ SG_ 543 2_TRACK_QUALITY "Unsigned 7-bit track quality/existence score. Detailed platforms share the same lifecycle-dependent distribution as the 3A5-3C4 layout; compact platforms use a higher calibration that falls as COAST_AGE increases. A related Hyundai object layout defines its 7-bit quality level as reliability, validity, or probability evidence; the raw radar scale is not confirmed to be a percentage."; +CM_ SG_ 543 2_AGE "Per-track alive age/lifetime count, observed 0-255. It normally increments on an update and may hold."; +CM_ SG_ 543 2_COAST_AGE "Prediction age: normally 0 while measured, then increments from 1 to 15 while coasted."; +CM_ SG_ 543 2_STATE "Detailed lifecycle state: 0=empty, 1/2=tentative acquisition, 3=measured, 4=coasted/predicted, 7=unresolved tentative. Values 5 and 6 were not observed; compact platforms leave this field zero."; +CM_ SG_ 543 2_RCS "Signed radar cross-section/return-strength value. Its distribution matches the 3A5-3C4 RCS field, and distance-controlled samples consistently increase from small to passenger to large targets. The raw unit is not calibrated; compact platforms leave it zero."; +CM_ SG_ 543 2_LONG_DIST "Longitudinal distance from the ego radar. Empty compact slots may contain the 204.75 m endpoint sentinel."; +CM_ SG_ 543 2_LAT_DIST "Lateral position relative to the ego axis."; +CM_ SG_ 543 2_REL_SPEED "Longitudinal speed relative to ego; negative means the target is moving longitudinally slower than ego."; +CM_ SG_ 543 2_NEW_SIGNAL_4 "Unknown category, observed 0-2 on detailed platforms. Compact platforms leave it zero."; +CM_ SG_ 543 2_REL_LAT_SPEED "Target lateral speed relative to the ego axis at 0.01 m/s per bit. The detailed dialect uses the decoded sign; compact platforms use the opposite sign."; +CM_ SG_ 543 2_REL_ACCEL "Target longitudinal acceleration relative to ego acceleration at 0.05 m/s^2 per bit."; +CM_ SG_ 543 2_NEW_SIGNAL_18 "Unknown compact-dialect per-target status area, observed only as 0/1; its second bit remained zero. Detailed platforms leave it zero."; +CM_ SG_ 543 2_OBJECT_ID "Strongly inferred compact-dialect persistent object identifier, observed 0-63. It held through 99.9% of consecutive same-track updates and changed when a slot acquired a replacement target. A related Hyundai BN7 radar DBC independently defines ObjectId as a global ID that remains constant while an object is tracked. Detailed platforms usually leave it zero."; +CM_ SG_ 543 CHECKSUM "16-bit HKG CAN-FD checksum using the CAN address as the data ID."; +CM_ SG_ 543 COUNTER "8-bit transmit-cycle counter. It covers 0-255 and wraps."; diff --git a/opendbc_repo/opendbc/dbc/hyundai_kia_ray_lfa.dbc b/opendbc_repo/opendbc/dbc/hyundai_kia_ray_lfa.dbc new file mode 100644 index 0000000000..9f35bafbb0 --- /dev/null +++ b/opendbc_repo/opendbc/dbc/hyundai_kia_ray_lfa.dbc @@ -0,0 +1,25 @@ +VERSION "" + +NS_ : + NS_DESC_ + CM_ + BA_DEF_ + BA_ + VAL_ + +BS_: + +BU_: XXX + +BO_ 1157 LFAHDA_MFC: 8 XXX + SG_ HDA_USM : 0|2@1+ (1,0) [0|3] "" XXX + SG_ HDA_Active : 2|1@1+ (1,0) [0|1] "" XXX + SG_ HDA_Icon_State : 3|2@1+ (1,0) [0|3] "" XXX + SG_ HDA_Chime : 7|1@1+ (1,0) [0|1] "" XXX + SG_ HDA_VSetReq : 8|8@1+ (1,0) [0|255] "km/h" XXX + SG_ LFA_SysWarning : 16|3@1+ (1,0) [0|7] "" XXX + SG_ HDA_Icon_Wheel : 20|1@1+ (1,0) [0|1] "" XXX + SG_ HDA_LdwSysState : 21|2@1+ (1,0) [0|3] "" XXX + SG_ LFA_Icon_State : 24|2@1+ (1,0) [0|3] "" XXX + SG_ LFA_USM : 27|2@1+ (1,0) [0|3] "" XXX + SG_ HDA_SysWarning : 29|2@1+ (1,0) [0|3] "" XXX diff --git a/opendbc_repo/opendbc/dbc/hyundai_kia_ray_pedal.dbc b/opendbc_repo/opendbc/dbc/hyundai_kia_ray_pedal.dbc new file mode 100644 index 0000000000..2f7dae4de1 --- /dev/null +++ b/opendbc_repo/opendbc/dbc/hyundai_kia_ray_pedal.dbc @@ -0,0 +1,23 @@ +VERSION "" + +NS_ : +BS_: +BU_: INTERCEPTOR NEO + +BO_ 512 GAS_COMMAND: 6 NEO + SG_ GAS_COMMAND : 7|16@0+ (0.672,-177.408) [0|255] "" INTERCEPTOR + SG_ GAS_COMMAND2 : 23|16@0+ (0.332,-165.004) [0|255] "" INTERCEPTOR + SG_ ENABLE : 39|1@0+ (1,0) [0|1] "" INTERCEPTOR + SG_ COUNTER_PEDAL : 35|4@0+ (1,0) [0|15] "" INTERCEPTOR + SG_ CHECKSUM_PEDAL : 47|8@0+ (1,0) [0|255] "" INTERCEPTOR + +BO_ 513 GAS_SENSOR: 6 INTERCEPTOR + SG_ INTERCEPTOR_GAS : 7|16@0+ (0.672,-177.408) [0|255] "" NEO + SG_ INTERCEPTOR_GAS2 : 23|16@0+ (0.332,-165.004) [0|255] "" NEO + SG_ STATE : 39|4@0+ (1,0) [0|15] "" NEO + SG_ COUNTER_PEDAL : 35|4@0+ (1,0) [0|15] "" NEO + SG_ CHECKSUM_PEDAL : 47|8@0+ (1,0) [0|255] "" NEO + +VAL_ 513 STATE 5 "FAULT_TIMEOUT" 4 "FAULT_STARTUP" 3 "FAULT_SCE" 2 "FAULT_SEND" 1 "FAULT_BAD_CHECKSUM" 0 "NO_FAULT" ; + +CM_ "Kia Ray EV comma pedal uses the standard 0x200/0x201 rolling counter and CRC8 protocol. Command scaling is Ray-only, estimated from the September 15 route: zero command raw 264/497, slopes about 3.79/7.68 raw counts per native E_EMS11 pedal unit, and 100 native units mapped to 255 comma pedal command units. October ray_1800_M100 firmware transforms physical ADC inputs by 1.8 minus 100, but receives command tracks directly as DAC counts: do not apply that input transform to GAS_COMMAND. Sensor offsets depend on firmware; driver override uses raw tracks, not these physical signal values. Confirm against installed Ray firmware before increasing the command cap."; diff --git a/opendbc_repo/opendbc/safety/modes/hyundai.h b/opendbc_repo/opendbc/safety/modes/hyundai.h index bf18071f69..5fc2626ae5 100644 --- a/opendbc_repo/opendbc/safety/modes/hyundai.h +++ b/opendbc_repo/opendbc/safety/modes/hyundai.h @@ -2,6 +2,8 @@ #include "opendbc/safety/declarations.h" #include "opendbc/safety/modes/hyundai_common.h" +#include "opendbc/safety/modes/hyundai_classic_non_scc_aol.h" +#include "opendbc/safety/modes/hyundai_blended_stock_aol.h" #define HYUNDAI_LIMITS(steer, rate_up, rate_down) { \ .max_torque = (steer), \ @@ -25,13 +27,13 @@ const LongitudinalLimits HYUNDAI_LONG_LIMITS = { .min_accel = -350, // 1/100 m/s2 }; -#define HYUNDAI_COMMON_TX_MSGS(scc_bus) \ +#define HYUNDAI_COMMON_TX_MSGS(scc_bus, refresh) \ {0x340, 0, 8, .check_relay = true}, /* LKAS11 Bus 0 */ \ {0x4F1, scc_bus, 4, .check_relay = false}, /* CLU11 Bus 0 (radar-SCC) or 2 (camera-SCC) */ \ - {0x485, 0, 4, .check_relay = true}, /* LFAHDA_MFC Bus 0 */ \ + {0x485, 0, (refresh) ? 8 : 4, .check_relay = true}, /* LFAHDA_MFC Bus 0 */ \ -#define HYUNDAI_LONG_COMMON_TX_MSGS(scc_bus) \ - HYUNDAI_COMMON_TX_MSGS(scc_bus) \ +#define HYUNDAI_LONG_COMMON_TX_MSGS(scc_bus, refresh) \ + HYUNDAI_COMMON_TX_MSGS(scc_bus, refresh) \ {0x420, 0, 8, .check_relay = true}, /* SCC11 Bus 0 */ \ {0x421, 0, 8, .check_relay = true}, /* SCC12 Bus 0 */ \ {0x50A, 0, 8, .check_relay = true}, /* SCC13 Bus 0 */ \ @@ -52,137 +54,328 @@ const LongitudinalLimits HYUNDAI_LONG_LIMITS = { #define HYUNDAI_FCEV_GAS_ADDR_CHECK \ {.msg = {{0x91, 0, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ +#define HYUNDAI_NON_SCC_HEV_ADDR_CHECK \ + {.msg = {{0x595, 0, 8, 10U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ + +#define HYUNDAI_NON_SCC_EV_ADDR_CHECK \ + {.msg = {{0x329, 0, 8, 10U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ + +#define HYUNDAI_LDA_BUTTON_ADDR_CHECK \ + {.msg = {{0x391, 0, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ + +// Ordinary-cruise combustion cars require EMS16 itself; E_EMS11 is not a +// substitute for the cruise lamp even though it is a gas-source alternative +// on other Hyundai configurations. +#define HYUNDAI_NON_SCC_ICE_RX_CHECKS \ + {.msg = {{0x260, 0, 8, 100U, .max_counter = 3U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ + {.msg = {{0x386, 0, 8, 100U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ + {.msg = {{0x394, 0, 8, 100U, .max_counter = 7U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ + {.msg = {{0x251, 0, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ + {.msg = {{0x4F1, 0, 4, 50U, .ignore_checksum = true, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ + static const CanMsg HYUNDAI_TX_MSGS[] = { - HYUNDAI_COMMON_TX_MSGS(0) + HYUNDAI_COMMON_TX_MSGS(0, false) }; +// Classic-mode-only meanings. CANFD Carnival/angle bit8192 is untouched. +// Exact stock words deliberately do not admit blended alpha-long or other flags. +static bool hyundai_blended_stock = false; +// Exact prepared mixed longitudinal profiles. +static bool hyundai_blended_alpha = false; +static bool hyundai_blended_cancel_armed = false; +static bool hyundai_blended_tcs_seen = false; +static uint32_t hyundai_blended_tcs_ts = 0U; +static bool hyundai_blended_mirror_pending = false; +static bool hyundai_blended_hda2 = false; + +#define HYUNDAI_BLENDED_COMMON_RX_CHECKS(pt_bus, speed_frequency) \ + {.msg = {{0x260, (pt_bus), 8, 100U, .max_counter = 3U, .ignore_quality_flag = true}, \ + {0x371, (pt_bus), 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}}}, \ + {.msg = {{0x386, (pt_bus), 8, (speed_frequency), .max_counter = 15U, .ignore_quality_flag = true}, {0}, {0}}}, \ + {.msg = {{0x394, (pt_bus), 8, (speed_frequency), .max_counter = 7U, .ignore_quality_flag = true}, {0}, {0}}}, \ + {.msg = {{0x251, (pt_bus), 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}, {0}}}, \ + {.msg = {{0x4F1, (pt_bus), 4, 50U, .ignore_checksum = true, .max_counter = 15U, .ignore_quality_flag = true}, {0}, {0}}}, \ + {.msg = {{0x420, (pt_bus), 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}, {0}}}, \ + /* Stock SCC12 CRC is unverified: preserve original CRC omission explicitly. */ \ + /* Original max_counter15 enforced even though its ignore_counter was true. */ \ + {.msg = {{0x421, (pt_bus), 8, 50U, .ignore_checksum = true, .max_counter = 15U, .ignore_quality_flag = true}, {0}, {0}}}, + +#define HYUNDAI_BLENDED_ALPHA_RX_CHECKS(pt_bus, speed_frequency) \ + {.msg = {{0x260, (pt_bus), 8, 100U, .max_counter = 3U, .ignore_quality_flag = true}, \ + {0x371, (pt_bus), 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}}}, \ + {.msg = {{0x386, (pt_bus), 8, (speed_frequency), .max_counter = 15U, .ignore_quality_flag = true}, {0}, {0}}}, \ + {.msg = {{0x394, (pt_bus), 8, (speed_frequency), .max_counter = 7U, .ignore_quality_flag = true}, {0}, {0}}}, \ + {.msg = {{0x251, (pt_bus), 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}, {0}}}, \ + {.msg = {{0x4F1, (pt_bus), 4, 50U, .ignore_checksum = true, .max_counter = 15U, .ignore_quality_flag = true}, {0}, {0}}}, + static bool hyundai_legacy = false; +static bool hyundai_ray_pedal = false; +static bool hyundai_ray_pedal_healthy = false; +static uint32_t hyundai_ray_pedal_ts = 0U; + +static uint8_t hyundai_ray_pedal_checksum(const CANPacket_t *msg) { + uint8_t crc = 0xFFU; + for (int i = 4; i >= 0; i--) { + crc ^= msg->data[i]; + for (int j = 0; j < 8; j++) { + crc = (crc & 0x80U) ? (uint8_t)((crc << 1U) ^ 0xD5U) : (uint8_t)(crc << 1U); + } + } + return crc; +} static uint8_t hyundai_get_counter(const CANPacket_t *msg) { uint8_t cnt = 0; - if (msg->addr == 0x260U) { - cnt = (msg->data[7] >> 4) & 0x3U; - } - if (msg->addr == 0x386U) { - cnt = ((msg->data[3] >> 6) << 2) | (msg->data[1] >> 6); - } - if (msg->addr == 0x394U) { - cnt = (msg->data[1] >> 5) & 0x7U; - } - if (msg->addr == 0x421U) { - cnt = msg->data[7] & 0xFU; - } - if (msg->addr == 0x4F1U) { - cnt = (msg->data[3] >> 4) & 0xFU; + if (hyundai_blended_hda2 && (msg->addr == 0x2A4U)) { + cnt = msg->data[2]; + } else { + if (msg->addr == 0x260U) { + cnt = (msg->data[7] >> 4) & 0x3U; + } + if (msg->addr == 0x386U) { + cnt = ((msg->data[3] >> 6) << 2) | (msg->data[1] >> 6); + } + if (msg->addr == 0x394U) { + cnt = (msg->data[1] >> 5) & 0x7U; + } else if (msg->addr == 0x421U) { + cnt = hyundai_blended_stock ? ((msg->data[1] >> 4U) & 0xFU) : (msg->data[7] & 0xFU); + } else if (hyundai_ray_pedal && (msg->addr == 0x201U)) { + cnt = msg->data[4] & 0xFU; + } else { + // Retain the counter already selected above for other addresses. + } + if (msg->addr == 0x4F1U) { + cnt = (msg->data[3] >> 4) & 0xFU; + } } return cnt; } static uint32_t hyundai_get_checksum(const CANPacket_t *msg) { - uint8_t chksum = 0; - if (msg->addr == 0x260U) { - chksum = msg->data[7] & 0xFU; + uint32_t result = 0U; + if (hyundai_blended_hda2 && (msg->addr == 0x2A4U)) { + result = GET_BYTES_LE(msg, 0, 2); + } else { + uint8_t chksum = 0; + if (msg->addr == 0x260U) { + chksum = msg->data[7] & 0xFU; + } + if (msg->addr == 0x386U) { + chksum = ((msg->data[7] >> 6) << 2) | (msg->data[5] >> 6); + } + if (msg->addr == 0x394U) { + chksum = msg->data[6] & 0xFU; + } + if (msg->addr == 0x421U) { + chksum = msg->data[7] >> 4; + } + if (hyundai_ray_pedal && (msg->addr == 0x201U)) { + chksum = msg->data[5]; + } + result = chksum; } - if (msg->addr == 0x386U) { - chksum = ((msg->data[7] >> 6) << 2) | (msg->data[5] >> 6); - } - if (msg->addr == 0x394U) { - chksum = msg->data[6] & 0xFU; - } - if (msg->addr == 0x421U) { - chksum = msg->data[7] >> 4; - } - return chksum; + return result; } static uint32_t hyundai_compute_checksum(const CANPacket_t *msg) { - uint8_t chksum = 0; - if (msg->addr == 0x386U) { - // count the bits - for (int i = 0; i < 8; i++) { - uint8_t b = msg->data[i]; - for (int j = 0; j < 8; j++) { - uint8_t bit = 0; - // exclude checksum and counter - if (((i != 1) || (j < 6)) && ((i != 3) || (j < 6)) && ((i != 5) || (j < 6)) && ((i != 7) || (j < 6))) { - bit = (b >> (uint8_t)j) & 1U; - } - chksum += bit; - } - } - chksum = (chksum ^ 9U) & 15U; + uint32_t result = 0U; + if (hyundai_blended_hda2 && (msg->addr == 0x2A4U)) { + result = hyundai_common_canfd_compute_checksum(msg); } else { - // sum of nibbles - for (int i = 0; i < 8; i++) { - if ((msg->addr == 0x394U) && (i == 7)) { - continue; // exclude + uint8_t chksum = 0; + if (hyundai_ray_pedal && (msg->addr == 0x201U)) { + chksum = hyundai_ray_pedal_checksum(msg); + } else if (msg->addr == 0x386U) { + // count the bits + for (int i = 0; i < 8; i++) { + uint8_t b = msg->data[i]; + for (int j = 0; j < 8; j++) { + uint8_t bit = 0; + // exclude checksum and counter + if (((i != 1) || (j < 6)) && ((i != 3) || (j < 6)) && ((i != 5) || (j < 6)) && ((i != 7) || (j < 6))) { + bit = (b >> (uint8_t)j) & 1U; + } + chksum += bit; + } } - uint8_t b = msg->data[i]; - if (((msg->addr == 0x260U) && (i == 7)) || ((msg->addr == 0x394U) && (i == 6)) || ((msg->addr == 0x421U) && (i == 7))) { - b &= (msg->addr == 0x421U) ? 0x0FU : 0xF0U; // remove checksum + chksum = (chksum ^ 9U) & 15U; + } else { + // sum of nibbles + for (int i = 0; i < 8; i++) { + if ((msg->addr == 0x394U) && (i == 7)) { + continue; // exclude + } + uint8_t b = msg->data[i]; + if (((msg->addr == 0x260U) && (i == 7)) || ((msg->addr == 0x394U) && (i == 6)) || ((msg->addr == 0x421U) && (i == 7))) { + b &= (msg->addr == 0x421U) ? 0x0FU : 0xF0U; // remove checksum + } + chksum += (b % 16U) + (b / 16U); } - chksum += (b % 16U) + (b / 16U); + chksum = (16U - (chksum % 16U)) % 16U; } - chksum = (16U - (chksum % 16U)) % 16U; - } - return chksum; + result = chksum; + } + return result; +} + +// A held cancel pair cannot survive an intervening inhibit or stale source. +static void hyundai_blended_cancel_check(void) { + if (hyundai_blended_alpha && (cruise_engaged_prev || brake_pressed || gas_pressed || + safety_rx_checks_invalid || relay_malfunction || !hyundai_blended_tcs_seen || + (safety_get_ts_elapsed(microsecond_timer_get(), hyundai_blended_tcs_ts) > 100000U))) { + hyundai_blended_cancel_armed = false; + } } static void hyundai_rx_hook(const CANPacket_t *msg) { + classic_non_scc_aol_rx(msg); + blended_aol_rx(msg); + hyundai_blended_cancel_check(); + + const uint8_t pt_bus = hyundai_blended_hda2 ? 1U : 0U; + + if (hyundai_blended_alpha && (msg->bus == pt_bus) && (msg->addr == 0x394U)) { + hyundai_blended_tcs_seen = true; + hyundai_blended_tcs_ts = microsecond_timer_get(); + cruise_engaged_prev = GET_BIT(msg, 54U); + } + // SCC12 is on bus 2 for camera-based SCC cars, bus 0 on all others - if (msg_matches(msg, 0x421U, hyundai_camera_scc ? 2U : 0U)) { - // 2 bits: 13-14 - int cruise_engaged = (GET_BYTES_LE(msg, 0, 4) >> 13) & 0x3U; - hyundai_common_cruise_state_check(cruise_engaged); + if ((msg->addr == 0x421U) && !hyundai_non_scc && !hyundai_blended_alpha) { + if (((msg->bus == pt_bus) && !hyundai_camera_scc) || ((msg->bus == 2U) && hyundai_camera_scc)) { + // 2 bits: 13-14 + int cruise_engaged = hyundai_blended_stock ? ((msg->data[3] >> 4U) & 0x3U) : ((GET_BYTES_LE(msg, 0, 4) >> 13) & 0x3U); + hyundai_common_cruise_state_check(cruise_engaged); + } } - if (msg_matches(msg, 0x251U, 0U)) { - int torque_driver_new = (GET_BYTES_LE(msg, 0, 2) & 0x7ffU) - 1024U; - // update array of samples - update_sample(&torque_driver, torque_driver_new); + if (hyundai_blended_stock && (msg->addr == 0x421U) && (msg->bus == pt_bus)) { + acc_main_on = GET_BIT(msg, 27U); } - // ACC steering wheel buttons - if (msg_matches(msg, 0x4F1U, 0U)) { - int cruise_button = msg->data[0] & 0x7U; - bool main_button = GET_BIT(msg, 3U); - hyundai_common_cruise_buttons_check(cruise_button, main_button); - } + if (msg->bus == pt_bus) { + if (hyundai_ray_pedal && (msg->addr == 0x592U)) { + acc_main_on = GET_BIT(msg, 34U); + hyundai_common_cruise_state_check(GET_BIT(msg, 35U)); + } + if (hyundai_non_scc) { + if ((msg->addr == 0x595U) && hyundai_hybrid_gas_signal) { + hyundai_common_cruise_state_check(GET_BIT(msg, 51U)); + } else if ((msg->addr == 0x329U) && hyundai_ev_gas_signal) { + hyundai_common_cruise_state_check(GET_BIT(msg, 30U)); + } else if ((msg->addr == 0x260U) && !hyundai_ev_gas_signal && !hyundai_hybrid_gas_signal) { + hyundai_common_cruise_state_check(GET_BIT(msg, 26U)); + } else { + // No alternate cruise source is selected for this message. + } + } + if (msg->addr == 0x251U) { + int torque_driver_new = (GET_BYTES_LE(msg, 0, 2) & 0x7ffU) - 1024U; + // update array of samples + update_sample(&torque_driver, torque_driver_new); + } - // gas press, different for EV, hybrid, and ICE models - if (msg_matches(msg, 0x371U, 0U) && hyundai_ev_gas_signal) { - gas_pressed = (((msg->data[4] & 0x7FU) << 1) | (msg->data[3] >> 7)) != 0U; - } else if (msg_matches(msg, 0x371U, 0U) && hyundai_hybrid_gas_signal) { - gas_pressed = msg->data[7] != 0U; - } else if (msg_matches(msg, 0x91U, 0U) && hyundai_fcev_gas_signal) { - gas_pressed = msg->data[6] != 0U; - } else if (msg_matches(msg, 0x260U, 0U) && !hyundai_ev_gas_signal && !hyundai_hybrid_gas_signal) { - gas_pressed = (msg->data[7] >> 6) != 0U; - } else { - } + // ACC steering wheel buttons + if (msg->addr == 0x4F1U) { + int cruise_button = msg->data[0] & 0x7U; + bool main_button = GET_BIT(msg, 3U); + if (hyundai_blended_alpha) { + bool fresh_tcs = hyundai_blended_tcs_seen && !safety_rx_checks_invalid && + (safety_get_ts_elapsed(microsecond_timer_get(), hyundai_blended_tcs_ts) <= 100000U); + if ((cruise_button == HYUNDAI_BTN_CANCEL) && (cruise_button_prev != HYUNDAI_BTN_CANCEL)) { + hyundai_blended_cancel_armed = fresh_tcs && !cruise_engaged_prev && !controls_allowed && + !brake_pressed && !gas_pressed && !relay_malfunction; + controls_allowed = false; + } else if ((cruise_button != HYUNDAI_BTN_CANCEL) && (cruise_button_prev == HYUNDAI_BTN_CANCEL)) { + controls_allowed = hyundai_blended_cancel_armed && fresh_tcs && !cruise_engaged_prev && + !brake_pressed && !gas_pressed; + hyundai_blended_cancel_armed = false; + } else if ((cruise_button != HYUNDAI_BTN_SET) && (cruise_button_prev == HYUNDAI_BTN_SET)) { + controls_allowed = fresh_tcs && !brake_pressed && !gas_pressed; + } else if ((cruise_button != HYUNDAI_BTN_RESUME) && (cruise_button_prev == HYUNDAI_BTN_RESUME)) { + controls_allowed = fresh_tcs && !brake_pressed && !gas_pressed; + } else { + } + cruise_button_prev = cruise_button; + } else { + hyundai_common_cruise_buttons_check(cruise_button, main_button); + } + } - // sample wheel speed, averaging opposite corners - if (msg_matches(msg, 0x386U, 0U)) { - uint32_t front_left_speed = GET_BYTES_LE(msg, 0, 2) & 0x3FFFU; - uint32_t rear_right_speed = GET_BYTES_LE(msg, 6, 2) & 0x3FFFU; - vehicle_moving = (front_left_speed > HYUNDAI_STANDSTILL_THRSLD) || (rear_right_speed > HYUNDAI_STANDSTILL_THRSLD); - } + if (hyundai_ray_pedal && (msg->addr == 0x201U)) { + gas_pressed = ((((uint32_t)msg->data[0] << 8U) | msg->data[1]) > 272U) || + ((((uint32_t)msg->data[2] << 8U) | msg->data[3]) > 513U); + hyundai_ray_pedal_healthy = (msg->data[4] >> 4U) == 0U; + hyundai_ray_pedal_ts = microsecond_timer_get(); + } - if (msg_matches(msg, 0x394U, 0U)) { - brake_pressed = ((msg->data[5] >> 5U) & 0x3U) == 0x2U; + // gas press, different for EV, hybrid, and ICE models + if ((msg->addr == 0x371U) && hyundai_ev_gas_signal && !hyundai_ray_pedal) { + gas_pressed = (((msg->data[4] & 0x7FU) << 1) | (msg->data[3] >> 7)) != 0U; + } else if ((msg->addr == 0x371U) && hyundai_hybrid_gas_signal) { + gas_pressed = msg->data[7] != 0U; + } else if ((msg->addr == 0x91U) && hyundai_fcev_gas_signal) { + gas_pressed = msg->data[6] != 0U; + } else if ((msg->addr == 0x260U) && !hyundai_ev_gas_signal && !hyundai_hybrid_gas_signal && + !(classic_scc_aol_enabled && hyundai_fcev_gas_signal)) { + gas_pressed = (msg->data[7] >> 6) != 0U; + } else { + } + + // sample wheel speed, averaging opposite corners + if (msg->addr == 0x386U) { + uint32_t front_left_speed = GET_BYTES_LE(msg, 0, 2) & 0x3FFFU; + uint32_t rear_right_speed = GET_BYTES_LE(msg, 6, 2) & 0x3FFFU; + vehicle_moving = (front_left_speed > HYUNDAI_STANDSTILL_THRSLD) || (rear_right_speed > HYUNDAI_STANDSTILL_THRSLD); + } + + if (msg->addr == 0x394U) { + brake_pressed = ((msg->data[5] >> 5U) & 0x3U) == 0x2U; + } + } + hyundai_blended_cancel_check(); + if (hyundai_ray_pedal && !acc_main_on) { + controls_allowed = false; } } static bool hyundai_tx_hook(const CANPacket_t *msg) { + static int hyundai_blended_mirror_torque = 0; + static bool hyundai_blended_mirror_request = false; + static uint32_t hyundai_blended_mirror_ts = 0U; + hyundai_blended_cancel_check(); const TorqueSteeringLimits HYUNDAI_STEERING_LIMITS = HYUNDAI_LIMITS(384, 3, 7); + const TorqueSteeringLimits HYUNDAI_STEERING_LIMITS_BLENDED = HYUNDAI_LIMITS(404, 2, 3); const TorqueSteeringLimits HYUNDAI_STEERING_LIMITS_ALT = HYUNDAI_LIMITS(270, 2, 3); const TorqueSteeringLimits HYUNDAI_STEERING_LIMITS_ALT_2 = HYUNDAI_LIMITS(170, 2, 3); bool tx = true; + if (hyundai_ray_pedal && (msg->addr == 0x200U)) { + const uint16_t track1 = ((uint16_t)msg->data[0] << 8U) | msg->data[1]; + const uint16_t track2 = ((uint16_t)msg->data[2] << 8U) | msg->data[3]; + const bool enabled = (msg->data[4] & 0x80U) != 0U; + const int track_error = (83 * ((int)track2 - 497)) - (168 * ((int)track1 - 264)); + bool rx_valid = hyundai_ray_pedal_healthy && !safety_rx_checks_invalid && + (safety_get_ts_elapsed(microsecond_timer_get(), hyundai_ray_pedal_ts) <= 1000000U); + for (int i = 0; i < current_safety_config.rx_checks_len; i++) { + const RxStatus *status = ¤t_safety_config.rx_checks[i].status; + rx_valid &= status->msg_seen && status->valid_checksum && status->valid_quality_flag && + (status->wrong_counters < MAX_WRONG_COUNTERS) && !status->lagging && + (safety_get_ts_elapsed(microsecond_timer_get(), status->last_timestamp) <= 1000000U); + } + if (((msg->data[4] & 0x70U) != 0U) || (msg->data[5] != hyundai_ray_pedal_checksum(msg)) || + (enabled && ((track1 < 264U) || (track1 > 530U) || (track2 < 497U) || (track2 > 1035U) || + (track_error < -125) || (track_error > 125))) || + (!enabled && ((track1 != 0U) || (track2 != 0U))) || + (enabled && (!get_longitudinal_allowed() || brake_pressed_prev || !acc_main_on || !rx_valid))) { + tx = false; + } + } + // FCA11: Block any potential actuation - if (msg->addr == 0x38DU) { + if ((msg->addr == 0x38DU) && !hyundai_blended_alpha) { int CR_VSM_DecCmd = msg->data[1]; bool FCA_CmdAct = GET_BIT(msg, 20U); bool CF_VSM_DecCmdAct = GET_BIT(msg, 31U); @@ -193,7 +386,7 @@ static bool hyundai_tx_hook(const CANPacket_t *msg) { } // ACCEL: safety check - if (msg->addr == 0x421U) { + if ((msg->addr == 0x421U) && !hyundai_blended_alpha) { int desired_accel_raw = (((msg->data[4] & 0x7U) << 8) | msg->data[3]) - 1023U; int desired_accel_val = ((msg->data[5] << 3) | (msg->data[4] >> 5)) - 1023U; @@ -214,12 +407,87 @@ static bool hyundai_tx_hook(const CANPacket_t *msg) { } } + if (hyundai_blended_alpha && (msg->addr == 0x420U)) { + const LongitudinalLimits blended_long_limits = {.max_accel = 350, .min_accel = -350}; + uint32_t raw = (((uint32_t)msg->data[4] & 0x3FU) << 5U) | ((uint32_t)msg->data[3] >> 3U); + uint32_t value = (((uint32_t)msg->data[3] & 0x7U) << 8U) | msg->data[2]; + if (longitudinal_accel_checks((int)raw - 1023, blended_long_limits) || + longitudinal_accel_checks((int)value - 1023, blended_long_limits)) { + tx = false; + } + } + // Exact reached ADRV source supplies only the packer's checksum and counter. + if (hyundai_blended_alpha && (msg->addr == 0x51U)) { + for (unsigned int i = 3U; i < 32U; i++) { + if (msg->data[i] != 0U) { + tx = false; + } + } + } + // Radar auxiliary payloads are fixed source bytes; no arbitrary new fields. + if (hyundai_blended_alpha && ((msg->addr == 0x363U) || (msg->addr == 0x398U) || + (msg->addr == 0x399U) || (msg->addr == 0x39AU) || (msg->addr == 0x39BU) || + (msg->addr == 0x39CU) || (msg->addr == 0x43AU))) { + uint8_t expected[8] = {0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U}; + if (msg->addr == 0x363U) { + expected[1] = 1U; + } else if (msg->addr == 0x398U) { + expected[4] = 0x80U; + expected[5] = hyundai_blended_hda2 ? 0x5DU : 0x10U; + } else if (msg->addr == 0x399U) { + expected[2] = 2U; + } else if (msg->addr == 0x39AU) { + expected[7] = 0xFFU; + } else if (msg->addr == 0x39CU) { + expected[5] = 0xE0U; + expected[6] = 0x79U; + } else if (msg->addr == 0x43AU) { + expected[2] = 7U; + } else { + } + if ((msg->data[1] & 0xFU) != expected[1]) { + tx = false; + } + for (unsigned int i = 2U; i < 8U; i++) { + if (msg->data[i] != expected[i]) { + tx = false; + } + } + } + // HDAI FCA remains closed: reached old host and native layouts conflict. + // HDAII preserves the old explicit request guard and exact source sentinels. + if (hyundai_blended_alpha && (msg->addr == 0x38DU)) { + if (!hyundai_blended_hda2 || GET_BIT(msg, 16U) || GET_BIT(msg, 19U) || + ((msg->data[1] & 0xFU) != 0U) || ((msg->data[1] >> 4U) == 15U) || + (msg->data[2] != 0U) || (msg->data[3] != 0U) || + (msg->data[4] != 0xC0U) || (msg->data[5] != 0x3FU) || + (msg->data[6] != 0x7FU) || (msg->data[7] != 0U)) { + tx = false; + } + } + if (hyundai_blended_alpha && (msg->addr == 0x4F1U)) { + tx = false; + } + if (hyundai_blended_alpha && hyundai_blended_hda2 && (msg->addr == 0x340U)) { + const uint32_t torque_raw = (GET_BYTES_LE(msg, 0, 4) >> 16U) & 0x7FFU; + int torque = (int)torque_raw - 1024; + bool request = GET_BIT(msg, 27U); + if (!hyundai_blended_mirror_pending || (torque != hyundai_blended_mirror_torque) || + (request != hyundai_blended_mirror_request) || + (safety_get_ts_elapsed(microsecond_timer_get(), hyundai_blended_mirror_ts) > 10000U) || + (!lateral_controls_allowed() && ((torque != 0) || request))) { + tx = false; + } + hyundai_blended_mirror_pending = false; + } + // LKA STEER: safety check - if (msg->addr == 0x340U) { + if ((msg->addr == 0x340U) && !hyundai_blended_hda2) { int desired_torque = ((GET_BYTES_LE(msg, 0, 4) >> 16) & 0x7ffU) - 1024U; bool steer_req = GET_BIT(msg, 27U); - const TorqueSteeringLimits limits = hyundai_alt_limits_2 ? HYUNDAI_STEERING_LIMITS_ALT_2 : + const TorqueSteeringLimits limits = hyundai_blended_stock ? HYUNDAI_STEERING_LIMITS_BLENDED : + hyundai_alt_limits_2 ? HYUNDAI_STEERING_LIMITS_ALT_2 : hyundai_alt_limits ? HYUNDAI_STEERING_LIMITS_ALT : HYUNDAI_STEERING_LIMITS; if (steer_torque_cmd_checks(desired_torque, steer_req, limits)) { @@ -227,8 +495,25 @@ static bool hyundai_tx_hook(const CANPacket_t *msg) { } } + if (hyundai_blended_hda2 && (msg->addr == 0x50U)) { + uint32_t raw_torque = ((((uint32_t)msg->data[6] & 0xFU) << 7U) | ((uint32_t)msg->data[5] >> 1U)); + int desired_torque = (int)raw_torque - 1024; + bool steer_req = GET_BIT(msg, 52U); + if (steer_torque_cmd_checks(desired_torque, steer_req, HYUNDAI_STEERING_LIMITS)) { + tx = false; + } + } + + // Source-backed HDAII dashboard status: checksum, counter and LFA icon only. + if ((hyundai_blended_hda2 || hyundai_blended_alpha) && (msg->addr == 0x485U)) { + if (((msg->data[1] & 0xFU) != 0U) || (msg->data[2] != 0U) || + ((msg->data[3] & 0xF9U) != 0U) || (GET_BYTES_LE(msg, 4, 4) != 0U)) { + tx = false; + } + } + // UDS: Only tester present ("\x02\x3E\x80\x00\x00\x00\x00\x00") allowed on diagnostics address - if (msg->addr == 0x7D0U) { + if ((msg->addr == 0x7D0U) || (hyundai_blended_alpha && (msg->addr == 0x730U))) { if (GET_BYTES_64_LE(msg, 0, 8) != 0x0000000000803E02ULL) { tx = false; } @@ -239,40 +524,238 @@ static bool hyundai_tx_hook(const CANPacket_t *msg) { int button = msg->data[0] & 0x7U; bool allowed_resume = (button == 1) && controls_allowed; + bool allowed_set = hyundai_blended_stock && (button == 2) && controls_allowed; bool allowed_cancel = (button == 4) && cruise_engaged_prev; - if (!(allowed_resume || allowed_cancel)) { + if (!(allowed_resume || allowed_set || allowed_cancel)) { tx = false; } } + tx &= blended_aol_integrity(msg); + if (hyundai_blended_alpha && hyundai_blended_hda2 && (msg->addr == 0x50U)) { + hyundai_blended_mirror_pending = tx && !relay_malfunction && !safety_rx_checks_invalid; + const uint32_t mirror_torque_raw = (((uint32_t)msg->data[6] & 0xFU) << 7U) | ((uint32_t)msg->data[5] >> 1U); + hyundai_blended_mirror_torque = (int)mirror_torque_raw - 1024; + hyundai_blended_mirror_request = GET_BIT(msg, 52U); + hyundai_blended_mirror_ts = microsecond_timer_get(); + } + blended_aol_tx(msg, tx); return tx; } +static void hyundai_classic_scc_aol_rx_config(safety_config *config, bool legacy) { + static RxCheck normal[] = { + HYUNDAI_COMMON_RX_CHECKS(false) + HYUNDAI_SCC12_ADDR_CHECK(0) + {.msg = {{0x420, 0, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}, {0}}}, + }; + static RxCheck camera[] = { + HYUNDAI_COMMON_RX_CHECKS(false) + HYUNDAI_SCC12_ADDR_CHECK(2) + {.msg = {{0x420, 2, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}, {0}}}, + }; + static RxCheck older[] = { + HYUNDAI_COMMON_RX_CHECKS(true) + HYUNDAI_SCC12_ADDR_CHECK(0) + {.msg = {{0x420, 0, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}, {0}}}, + }; + static RxCheck fcev[] = { + HYUNDAI_COMMON_RX_CHECKS(false) + HYUNDAI_SCC12_ADDR_CHECK(0) + {.msg = {{0x420, 0, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}, {0}}}, + HYUNDAI_FCEV_GAS_ADDR_CHECK + }; + if (classic_scc_aol_enabled) { + if (hyundai_fcev_gas_signal && !legacy) { + SET_RX_CHECKS(fcev, (*config)); + } else if (legacy) { + SET_RX_CHECKS(older, (*config)); + } else if (classic_scc_aol_camera) { + SET_RX_CHECKS(camera, (*config)); + } else { + SET_RX_CHECKS(normal, (*config)); + } + } +} + +static RxCheck hyundai_long_rx_checks[] = { + HYUNDAI_COMMON_RX_CHECKS(false) +}; + +static void hyundai_long_rx_variant(bool legacy) { + hyundai_long_rx_checks[1].msg[0].ignore_checksum = legacy; + hyundai_long_rx_checks[1].msg[0].ignore_counter = legacy; + hyundai_long_rx_checks[1].msg[0].max_counter = legacy ? 0U : 15U; + hyundai_long_rx_checks[2].msg[0].ignore_checksum = legacy; + hyundai_long_rx_checks[2].msg[0].ignore_counter = legacy; + hyundai_long_rx_checks[2].msg[0].max_counter = legacy ? 0U : 7U; +} + +#ifdef ALLOW_DEBUG +static const CanMsg HYUNDAI_LONG_TX_MSGS[] = { + HYUNDAI_LONG_COMMON_TX_MSGS(0, false) + {0x38D, 0, 8, .check_relay = false}, // FCA11 Bus 0 + {0x483, 0, 8, .check_relay = false}, // FCA12 Bus 0 + {0x7D0, 0, 8, .check_relay = false}, // radar UDS TX addr Bus 0 (for radar disable) + }; +#endif + static safety_config hyundai_init(uint16_t param) { +#ifndef ALLOW_DEBUG static const CanMsg HYUNDAI_LONG_TX_MSGS[] = { - HYUNDAI_LONG_COMMON_TX_MSGS(0) - {0x38D, 0, 8, .check_relay = false}, // FCA11 Bus 0 - {0x483, 0, 8, .check_relay = false}, // FCA12 Bus 0 - {0x7D0, 0, 8, .check_relay = false}, // radar UDS TX addr Bus 0 (for radar disable) + HYUNDAI_LONG_COMMON_TX_MSGS(0, false) + {0x38D, 0, 8, .check_relay = false}, // FCA11 Bus 0 + {0x483, 0, 8, .check_relay = false}, // FCA12 Bus 0 + {0x7D0, 0, 8, .check_relay = false}, // radar UDS TX addr Bus 0 (for radar disable) + }; +#endif + hyundai_long_rx_variant(false); + static const CanMsg HYUNDAI_REFRESH_TX_MSGS[] = { + HYUNDAI_COMMON_TX_MSGS(0, true) + }; + + static const CanMsg HYUNDAI_LONG_REFRESH_TX_MSGS[] = { + HYUNDAI_LONG_COMMON_TX_MSGS(0, true) + {0x38D, 0, 8, .check_relay = false}, + {0x483, 0, 8, .check_relay = false}, + {0x7D0, 0, 8, .check_relay = false}, }; static const CanMsg HYUNDAI_CAMERA_SCC_TX_MSGS[] = { - HYUNDAI_COMMON_TX_MSGS(2) + HYUNDAI_COMMON_TX_MSGS(2, false) + }; + + static const CanMsg HYUNDAI_CAMERA_SCC_REFRESH_TX_MSGS[] = { + HYUNDAI_COMMON_TX_MSGS(2, true) }; static const CanMsg HYUNDAI_CAMERA_SCC_LONG_TX_MSGS[] = { - HYUNDAI_LONG_COMMON_TX_MSGS(2) + HYUNDAI_LONG_COMMON_TX_MSGS(2, false) + }; + + static const CanMsg HYUNDAI_CAMERA_SCC_LONG_REFRESH_TX_MSGS[] = { + HYUNDAI_LONG_COMMON_TX_MSGS(2, true) }; hyundai_common_init(param); + classic_non_scc_aol_configure(param, false); + blended_aol_configure(param); + hyundai_blended_alpha = false; +#ifdef ALLOW_DEBUG + hyundai_blended_alpha = (param == 0x2004U) || (param == 0x2014U); +#endif + hyundai_blended_cancel_armed = false; + hyundai_blended_tcs_seen = false; + hyundai_blended_mirror_pending = false; + hyundai_blended_stock = (param == 0x2000U) || (param == 0x2010U); + hyundai_blended_hda2 = (param == 0x2010U); +#ifdef ALLOW_DEBUG + hyundai_blended_hda2 |= (param == 0x2014U); +#endif + hyundai_blended_stock |= hyundai_blended_alpha; + if (hyundai_blended_hda2) { + gen_crc_lookup_table_16(0x1021U, hyundai_canfd_crc_lut); + } + hyundai_ray_pedal = param == 0x9805U; + hyundai_ray_pedal_healthy = false; + hyundai_ray_pedal_ts = 0U; + if (hyundai_ray_pedal) { + hyundai_longitudinal = true; + } hyundai_legacy = false; safety_config ret; - if (hyundai_longitudinal) { - // Use CLU11 (buttons) to manage controls allowed instead of SCC cruise state - static RxCheck hyundai_long_rx_checks[] = { - HYUNDAI_COMMON_RX_CHECKS(false) +#ifdef ALLOW_DEBUG + if (hyundai_blended_alpha) { + static const CanMsg alpha_hda1_tx[] = { + {0x340, 0, 8, .check_relay = true, .disable_static_blocking = true}, + {0x485, 0, 8, .check_relay = true, .disable_static_blocking = true}, + {0x364, 0, 8, .check_relay = true, .disable_static_blocking = true}, {0x420, 0, 8, .check_relay = true}, + {0x421, 0, 8, .check_relay = true}, {0x389, 0, 8, .check_relay = true}, + {0x7D0, 0, 8, .check_relay = false}, {0x363, 0, 8, .check_relay = false}, + {0x398, 0, 8, .check_relay = false}, }; + static const CanMsg alpha_hda2_tx[] = { + {0x50, 0, 16, .check_relay = true, .disable_static_blocking = true}, + {0x2A4, 0, 24, .check_relay = true, .disable_static_blocking = true}, + {0x51, 0, 32, .check_relay = false}, {0x730, 1, 8, .check_relay = false}, + {0x340, 1, 8, .check_relay = true, .disable_static_blocking = true}, + {0x485, 1, 8, .check_relay = true, .disable_static_blocking = true}, + {0x420, 1, 8, .check_relay = true}, {0x421, 1, 8, .check_relay = true}, + {0x389, 1, 8, .check_relay = true}, {0x38D, 1, 8, .check_relay = false}, + {0x363, 1, 8, .check_relay = false}, + {0x398, 1, 8, .check_relay = false}, {0x399, 1, 8, .check_relay = false}, + {0x39A, 1, 8, .check_relay = false}, {0x39B, 1, 8, .check_relay = false}, + {0x39C, 1, 8, .check_relay = false}, {0x43A, 1, 8, .check_relay = false}, + }; + if (hyundai_blended_hda2) { + static RxCheck alpha_hda2_rx[] = { + HYUNDAI_BLENDED_ALPHA_RX_CHECKS(1, 50U) + {.msg = {{0x2A4, 2, 24, 20U, .max_counter = 255U, .ignore_quality_flag = true}, {0}, {0}}}, + }; + ret = BUILD_SAFETY_CFG(alpha_hda2_rx, alpha_hda2_tx); + } else { + static RxCheck alpha_hda1_rx[] = { + HYUNDAI_BLENDED_ALPHA_RX_CHECKS(0, 100U) + {.msg = {{0x391, 0, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, + {0x50C, 0, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, + {0x50C, 1, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}}}, + }; + ret = BUILD_SAFETY_CFG(alpha_hda1_rx, alpha_hda1_tx); + } + } else +#endif + if (hyundai_blended_stock) { + static const CanMsg blended_tx[] = { + {0x340, 0, 8, .check_relay = true}, + {0x4F1, 0, 4, .check_relay = false}, + {0x485, 0, 8, .check_relay = true}, + {0x364, 0, 8, .check_relay = true}, + }; + static const CanMsg blended_hda2_tx[] = { + {0x50, 0, 16, .check_relay = true}, + {0x4F1, 1, 4, .check_relay = false}, + {0x2A4, 0, 24, .check_relay = true}, + {0x485, 1, 8, .check_relay = true}, + }; + static const CanMsg blended_hda2_aol_tx[] = { + {0x50, 0, 16, .check_relay = true, .disable_static_blocking = true}, + {0x4F1, 1, 4, .check_relay = false}, + {0x2A4, 0, 24, .check_relay = true, .disable_static_blocking = true}, + {0x485, 1, 8, .check_relay = true, .disable_static_blocking = true}, + }; + static const CanMsg blended_aol_tx_msgs[] = { + {0x340, 0, 8, .check_relay = true, .disable_static_blocking = true}, + {0x4F1, 0, 4, .check_relay = false}, + {0x485, 0, 8, .check_relay = true, .disable_static_blocking = true}, + {0x364, 0, 8, .check_relay = true, .disable_static_blocking = true}, + }; + if (hyundai_blended_hda2) { + static RxCheck blended_hda2_rx[] = { + HYUNDAI_BLENDED_COMMON_RX_CHECKS(1, 50U) + {.msg = {{0x2A4, 2, 24, 20U, .max_counter = 255U, .ignore_quality_flag = true}, {0}, {0}}}, + }; + ret = blended_aol_enabled ? BUILD_SAFETY_CFG(blended_hda2_rx, blended_hda2_aol_tx) : + BUILD_SAFETY_CFG(blended_hda2_rx, blended_hda2_tx); + } else { + static RxCheck blended_rx[] = {HYUNDAI_BLENDED_COMMON_RX_CHECKS(0, 100U)}; + ret = blended_aol_enabled ? BUILD_SAFETY_CFG(blended_rx, blended_aol_tx_msgs) : BUILD_SAFETY_CFG(blended_rx, blended_tx); + } + } else if (hyundai_ray_pedal) { + static const CanMsg ray_tx_msgs[] = { + HYUNDAI_COMMON_TX_MSGS(0, true) + {0x200, 0, 6, .check_relay = false}, + }; + static RxCheck ray_rx_checks[] = { + HYUNDAI_COMMON_RX_CHECKS(false) + {.msg = {{0x592, 0, 8, 10U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}, {0}}}, + HYUNDAI_LDA_BUTTON_ADDR_CHECK + {.msg = {{0x201, 0, 6, 50U, .max_counter = 15U, .ignore_quality_flag = true}, {0}, {0}}}, + }; + SET_RX_CHECKS(ray_rx_checks, ret); + SET_TX_MSGS(ray_tx_msgs, ret); + } else if (hyundai_longitudinal) { + // Use CLU11 (buttons) to manage controls allowed instead of SCC cruise state static RxCheck hyundai_fcev_long_rx_checks[] = { HYUNDAI_COMMON_RX_CHECKS(false) @@ -285,9 +768,17 @@ static safety_config hyundai_init(uint16_t param) { SET_RX_CHECKS(hyundai_long_rx_checks, ret); } if (hyundai_camera_scc) { - SET_TX_MSGS(HYUNDAI_CAMERA_SCC_LONG_TX_MSGS, ret); + if (hyundai_can_refresh_msgs) { + SET_TX_MSGS(HYUNDAI_CAMERA_SCC_LONG_REFRESH_TX_MSGS, ret); + } else { + SET_TX_MSGS(HYUNDAI_CAMERA_SCC_LONG_TX_MSGS, ret); + } } else { - SET_TX_MSGS(HYUNDAI_LONG_TX_MSGS, ret); + if (hyundai_can_refresh_msgs) { + SET_TX_MSGS(HYUNDAI_LONG_REFRESH_TX_MSGS, ret); + } else { + SET_TX_MSGS(HYUNDAI_LONG_TX_MSGS, ret); + } } } else if (hyundai_camera_scc) { static RxCheck hyundai_cam_scc_rx_checks[] = { @@ -295,7 +786,12 @@ static safety_config hyundai_init(uint16_t param) { HYUNDAI_SCC12_ADDR_CHECK(2) }; - ret = BUILD_SAFETY_CFG(hyundai_cam_scc_rx_checks, HYUNDAI_CAMERA_SCC_TX_MSGS); + if (hyundai_can_refresh_msgs) { + SET_RX_CHECKS(hyundai_cam_scc_rx_checks, ret); + SET_TX_MSGS(HYUNDAI_CAMERA_SCC_REFRESH_TX_MSGS, ret); + } else { + ret = BUILD_SAFETY_CFG(hyundai_cam_scc_rx_checks, HYUNDAI_CAMERA_SCC_TX_MSGS); + } } else { static RxCheck hyundai_rx_checks[] = { HYUNDAI_COMMON_RX_CHECKS(false) @@ -308,13 +804,102 @@ static safety_config hyundai_init(uint16_t param) { HYUNDAI_FCEV_GAS_ADDR_CHECK }; - SET_TX_MSGS(HYUNDAI_TX_MSGS, ret); - if (hyundai_fcev_gas_signal) { + static RxCheck hyundai_non_scc_rx_checks[] = { + HYUNDAI_NON_SCC_ICE_RX_CHECKS + }; + static RxCheck hyundai_non_scc_lda_rx_checks[] = { + HYUNDAI_NON_SCC_ICE_RX_CHECKS + HYUNDAI_LDA_BUTTON_ADDR_CHECK + }; + static RxCheck hyundai_classic_non_scc_aol_lda_rx_checks[] = { + HYUNDAI_NON_SCC_ICE_RX_CHECKS + {.msg = {{0x391, 0, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, + {0x50C, 0, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}}}, + }; + #define HYUNDAI_CLASSIC_AOL_LDA_RX_CHECKS \ + {.msg = {{0x391, 0, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, \ + {0x50C, 0, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}}}, + #define HYUNDAI_CLASSIC_AOL_EV_MAIN_RX_CHECK \ + {.msg = {{0x592, 0, 8, 10U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}, {0}}}, + static RxCheck hyundai_classic_aol_hev_lda_rx_checks[] = { + HYUNDAI_COMMON_RX_CHECKS(false) + HYUNDAI_NON_SCC_HEV_ADDR_CHECK + HYUNDAI_CLASSIC_AOL_LDA_RX_CHECKS + }; + static RxCheck hyundai_classic_aol_ev_rx_checks[] = { + HYUNDAI_COMMON_RX_CHECKS(false) + HYUNDAI_NON_SCC_EV_ADDR_CHECK + HYUNDAI_CLASSIC_AOL_EV_MAIN_RX_CHECK + }; + static RxCheck hyundai_classic_aol_ev_lda_rx_checks[] = { + HYUNDAI_COMMON_RX_CHECKS(false) + HYUNDAI_NON_SCC_EV_ADDR_CHECK + HYUNDAI_CLASSIC_AOL_EV_MAIN_RX_CHECK + HYUNDAI_CLASSIC_AOL_LDA_RX_CHECKS + }; + static RxCheck hyundai_non_scc_hev_rx_checks[] = { + HYUNDAI_COMMON_RX_CHECKS(false) + HYUNDAI_NON_SCC_HEV_ADDR_CHECK + }; + static RxCheck hyundai_non_scc_hev_lda_rx_checks[] = { + HYUNDAI_COMMON_RX_CHECKS(false) + HYUNDAI_NON_SCC_HEV_ADDR_CHECK + HYUNDAI_LDA_BUTTON_ADDR_CHECK + }; + static RxCheck hyundai_non_scc_ev_rx_checks[] = { + HYUNDAI_COMMON_RX_CHECKS(false) + HYUNDAI_NON_SCC_EV_ADDR_CHECK + }; + static RxCheck hyundai_non_scc_ev_lda_rx_checks[] = { + HYUNDAI_COMMON_RX_CHECKS(false) + HYUNDAI_NON_SCC_EV_ADDR_CHECK + HYUNDAI_LDA_BUTTON_ADDR_CHECK + }; + + if (hyundai_can_refresh_msgs) { + SET_TX_MSGS(HYUNDAI_REFRESH_TX_MSGS, ret); + } else { + SET_TX_MSGS(HYUNDAI_TX_MSGS, ret); + } + if (hyundai_non_scc) { + if (classic_non_scc_aol_ev) { + if (classic_non_scc_aol_lda) { + SET_RX_CHECKS(hyundai_classic_aol_ev_lda_rx_checks, ret); + } else { + SET_RX_CHECKS(hyundai_classic_aol_ev_rx_checks, ret); + } + } else if (classic_non_scc_aol_hev && classic_non_scc_aol_lda) { + SET_RX_CHECKS(hyundai_classic_aol_hev_lda_rx_checks, ret); + } else if (classic_non_scc_aol_enabled && classic_non_scc_aol_lda) { + SET_RX_CHECKS(hyundai_classic_non_scc_aol_lda_rx_checks, ret); + } else if (hyundai_ev_gas_signal) { + if (hyundai_has_lda_button) { + SET_RX_CHECKS(hyundai_non_scc_ev_lda_rx_checks, ret); + } else { + SET_RX_CHECKS(hyundai_non_scc_ev_rx_checks, ret); + } + } else if (hyundai_hybrid_gas_signal) { + if (hyundai_has_lda_button) { + SET_RX_CHECKS(hyundai_non_scc_hev_lda_rx_checks, ret); + } else { + SET_RX_CHECKS(hyundai_non_scc_hev_rx_checks, ret); + } + } else if (hyundai_has_lda_button) { + SET_RX_CHECKS(hyundai_non_scc_lda_rx_checks, ret); + } else { + SET_RX_CHECKS(hyundai_non_scc_rx_checks, ret); + } + } else if (hyundai_fcev_gas_signal) { SET_RX_CHECKS(hyundai_fcev_rx_checks, ret); } else { SET_RX_CHECKS(hyundai_rx_checks, ret); } } + hyundai_classic_scc_aol_rx_config(&ret, false); + if (classic_long_aol_param(param) && !classic_long_aol_enabled) { + const safety_config denied = {0}; + ret = denied; + } return ret; } @@ -325,15 +910,65 @@ static safety_config hyundai_legacy_init(uint16_t param) { HYUNDAI_SCC12_ADDR_CHECK(0) }; - hyundai_common_init(param); + const bool legacy_long = legacy_long_param(param); + uint16_t normalized = param; + if (legacy_long) { + normalized = ((param & 1U) != 0U) ? 6U : 4U; + if (legacy_long_aol_param(param)) { + normalized |= 0x0400U; + } + if ((param & 0x10U) != 0U) { + normalized |= 0x0800U; + } + } + hyundai_common_init(normalized); + classic_non_scc_aol_configure(normalized, !legacy_long); hyundai_legacy = true; + hyundai_blended_stock = false; + hyundai_blended_hda2 = false; + hyundai_blended_alpha = false; + hyundai_blended_cancel_armed = false; + hyundai_blended_tcs_seen = false; + hyundai_blended_mirror_pending = false; + hyundai_ray_pedal = false; + hyundai_ray_pedal_healthy = false; hyundai_longitudinal = false; hyundai_camera_scc = false; - return BUILD_SAFETY_CFG(hyundai_legacy_rx_checks, HYUNDAI_TX_MSGS); + safety_config ret = BUILD_SAFETY_CFG(hyundai_legacy_rx_checks, HYUNDAI_TX_MSGS); + if ((param & 0xFF00U) == 0xE900U) { + const safety_config denied = {0}; + ret = denied; +#ifdef ALLOW_DEBUG + const bool marked = legacy_long_aol_param(param); + const bool alternative_matches = marked ? ((unsigned int)alternative_experience == 32U) : ((unsigned int)alternative_experience == 0U); + if (legacy_long && alternative_matches) { + hyundai_long_rx_variant(true); + hyundai_longitudinal = true; + SET_RX_CHECKS(hyundai_long_rx_checks, ret); + SET_TX_MSGS(HYUNDAI_LONG_TX_MSGS, ret); + } +#endif + } + hyundai_classic_scc_aol_rx_config(&ret, true); + return ret; +} + +static void hyundai_optional_rx_hook(const CANPacket_t *msg) { + if ((msg->addr == 0x391U) || (msg->addr == 0x50CU)) { + blended_aol_rx(msg); + } + // The unselected physical alternative does not renew mandatory RX health. + // Only these exact classic LDA profiles may use shape-checked button evidence. + if (classic_non_scc_aol_enabled && classic_non_scc_aol_lda && (msg->bus == 0U) && (GET_LEN(msg) == 8U) && + ((msg->addr == 0x391U) || (msg->addr == 0x50CU))) { + classic_non_scc_aol_rx(msg); + } } const safety_hooks hyundai_hooks = { + .fwd = blended_aol_fwd, .init = hyundai_init, + .optional_rx = hyundai_optional_rx_hook, .rx = hyundai_rx_hook, .tx = hyundai_tx_hook, .get_counter = hyundai_get_counter, @@ -343,6 +978,7 @@ const safety_hooks hyundai_hooks = { const safety_hooks hyundai_legacy_hooks = { .init = hyundai_legacy_init, + .optional_rx = hyundai_optional_rx_hook, .rx = hyundai_rx_hook, .tx = hyundai_tx_hook, .get_counter = hyundai_get_counter, diff --git a/opendbc_repo/opendbc/safety/modes/hyundai_blended_stock_aol.h b/opendbc_repo/opendbc/safety/modes/hyundai_blended_stock_aol.h new file mode 100644 index 0000000000..e603242c5a --- /dev/null +++ b/opendbc_repo/opendbc/safety/modes/hyundai_blended_stock_aol.h @@ -0,0 +1,306 @@ +#pragma once + +static bool blended_aol_enabled = false; +static bool blended_aol_long = false; +static bool blended_aol_hda2 = false; +static bool blended_adrv_counter_seen = false; +static uint8_t blended_adrv_counter = 0U; +static bool blended_camera_seen = false; +static uint8_t blended_camera_data[24] = {0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U}; +static uint32_t blended_camera_ts = 0U; +static bool blended_aol_main_seen = false; +static bool blended_aol_main = false; +static uint32_t blended_aol_main_ts = 0U; +static bool blended_aol_token = false; +static bool blended_aol_claimed = false; +static bool blended_aol_session = false; +static bool blended_aol_request_seen = false; +static bool blended_aol_neutral[3] = {false, false, false}; +static bool blended_aol_pressed[3] = {false, false, false}; +static uint32_t blended_aol_source_ts[3] = {0U, 0U, 0U}; +static uint32_t blended_aol_token_ts = 0U; +static unsigned int blended_aol_token_source = 3U; +static bool blended_aol_counter_seen = false; +static uint8_t blended_aol_counter = 0U; +static bool blended_aol_owned[4] = {false, false, false, false}; +static uint32_t blended_aol_tx_ts[4] = {0U, 0U, 0U, 0U}; + +static void blended_aol_clear(void) { + blended_aol_main_seen = false; + blended_aol_main = false; + blended_aol_main_ts = 0U; + blended_aol_counter_seen = false; + blended_aol_counter = 0U; + blended_adrv_counter_seen = false; + blended_adrv_counter = 0U; + blended_aol_owned[3] = false; + blended_aol_tx_ts[3] = 0U; + blended_aol_token = false; + blended_aol_claimed = false; + blended_aol_session = false; + blended_aol_request_seen = false; + blended_aol_token_ts = 0U; + blended_aol_token_source = 3U; + aol_host_axis_mask = 0U; + for (unsigned int i = 0U; i < 3U; i++) { + blended_aol_neutral[i] = false; + blended_aol_pressed[i] = false; + blended_aol_source_ts[i] = 0U; + blended_aol_owned[i] = false; + blended_aol_tx_ts[i] = 0U; + } +} + +static void blended_aol_reset(void) { + blended_aol_enabled = false; + blended_aol_long = false; + blended_aol_hda2 = false; + blended_camera_seen = false; + blended_camera_ts = 0U; + blended_aol_counter_seen = false; + blended_aol_counter = 0U; + blended_aol_clear(); +} + +static void blended_aol_host_request(uint8_t mask) { + blended_aol_request_seen |= mask != 0U; +} + +static uint8_t blended_aol_request_mask(void) { + uint8_t result = 0U; + const uint32_t now = microsecond_timer_get(); + const bool token_expired = blended_aol_token && !blended_aol_claimed && + (safety_get_ts_elapsed(now, blended_aol_token_ts) > AOL_HOST_REQUEST_TIMEOUT_US); + const bool request_expired = blended_aol_request_seen && + (safety_get_ts_elapsed(now, aol_host_request_ts) > AOL_HOST_REQUEST_TIMEOUT_US); + if (relay_malfunction || safety_rx_checks_invalid || token_expired || request_expired || + (blended_aol_session && !heartbeat_engaged)) { + blended_aol_clear(); + } else if (heartbeat_engaged) { + blended_aol_session = true; + if (blended_aol_long && controls_allowed && blended_aol_main_seen && blended_aol_main && + (safety_get_ts_elapsed(now, blended_aol_main_ts) <= 100000U) && ((aol_host_axis_mask & 1U) != 0U)) { + // Normal paired enable can seed lateral; a lateral token never enables long. + blended_aol_token = true; + blended_aol_token_source = 0U; + blended_aol_token_ts = now; + } + result = aol_host_axis_mask & (blended_aol_long ? 3U : 1U); + if ((result != 0U) && blended_aol_token) { + blended_aol_claimed = true; + } + } else { + } + return result; +} + +static uint8_t blended_aol_permission_mask(void) { + uint8_t result = 0U; + const bool source_current = (blended_aol_token_source < 3U) && + (safety_get_ts_elapsed(microsecond_timer_get(), blended_aol_source_ts[blended_aol_token_source]) <= 300000U); + if (!aol_rx_healthy() || (blended_aol_token && !source_current)) { + blended_aol_clear(); + } else if (((blended_aol_request_mask() & 1U) != 0U) && (controls_allowed || blended_aol_token)) { + result = 1U; + } else { + } + if (blended_aol_long) { + const bool main_current = blended_aol_main_seen && blended_aol_main && + (safety_get_ts_elapsed(microsecond_timer_get(), blended_aol_main_ts) <= 100000U); + if (!main_current) { + blended_aol_clear(); + result = 0U; + } else if (aol_rx_healthy() && controls_allowed && !brake_pressed && !gas_pressed && + ((blended_aol_request_mask() & 2U) != 0U)) { + result |= 2U; + } else { + } + } else { + } + return result; +} + +static void blended_aol_rx(const CANPacket_t *msg) { + if (blended_aol_hda2 && msg_matches(msg, 0x2A4U, 2U, 24U)) { + for (unsigned int i = 0U; i < 24U; i++) { + blended_camera_data[i] = msg->data[i]; + } + blended_camera_seen = true; + blended_camera_ts = microsecond_timer_get(); + } + if (blended_aol_enabled) { + const unsigned int pt = blended_aol_hda2 ? 1U : 0U; + const uint32_t now = microsecond_timer_get(); + if (blended_aol_long && msg_matches(msg, 0x394U, pt, 8U)) { + blended_aol_main_seen = true; + blended_aol_main = ((msg->data[5] >> 3U) & 3U) == 0U; + blended_aol_main_ts = now; + if (!blended_aol_main) { + blended_aol_token = false; + blended_aol_claimed = false; + } + } + unsigned int source = 3U; + bool pressed = false; + if ((msg->bus == pt) && (msg->addr == 0x4F1U) && (GET_LEN(msg) == 4U)) { + if ((msg->data[0] & 7U) == 4U) { + blended_aol_clear(); + } + source = 0U; + pressed = GET_BIT(msg, 3U); + } else if ((msg->bus == pt) && (msg->addr == 0x391U) && (GET_LEN(msg) == 8U)) { + source = 1U; + pressed = GET_BIT(msg, 4U); + } else if ((msg->bus == pt) && (msg->addr == 0x50CU) && (GET_LEN(msg) == 8U)) { + source = 2U; + pressed = GET_BIT(msg, 56U); + } else { + } + if (source < 3U) { + const bool expired = (blended_aol_source_ts[source] == 0U) || + (safety_get_ts_elapsed(now, blended_aol_source_ts[source]) > 300000U); + if (expired) { + blended_aol_neutral[source] = false; + blended_aol_pressed[source] = pressed; + } + if (!pressed) { + blended_aol_neutral[source] = true; + } else if (blended_aol_neutral[source] && !blended_aol_pressed[source] && (aol_host_axis_mask == 0U)) { + blended_aol_token = true; + blended_aol_claimed = false; + blended_aol_token_ts = now; + blended_aol_token_source = source; + } else { + } + blended_aol_pressed[source] = pressed; + blended_aol_source_ts[source] = now; + } + } +} + +static void blended_aol_configure(uint16_t param) { + blended_aol_reset(); +#ifdef ALLOW_DEBUG + blended_aol_long = (param == 0x2004U) || (param == 0x2014U); +#endif + blended_aol_hda2 = (param == 0x2010U) || (param == 0x2014U); + blended_aol_enabled = ((param == 0x2000U) || (param == 0x2010U) || blended_aol_long) && ((unsigned int)alternative_experience == 32U); + if (blended_aol_enabled) { + static const AolSafetyPolicy policy = { + .reset = blended_aol_reset, + .host_request = blended_aol_host_request, + .request_mask = blended_aol_request_mask, + .permission_mask = blended_aol_permission_mask, + .rx_invalid = blended_aol_clear, + }; + aol_policy = &policy; + } +} + +static bool blended_aol_integrity(const CANPacket_t *msg) { + bool valid = true; + const unsigned int steering = blended_aol_hda2 ? 0x50U : 0x340U; + if (blended_aol_hda2 && ((msg->addr == 0x50U) || (msg->addr == 0x51U) || (msg->addr == 0x2A4U))) { + unsigned int length = 24U; + if (msg->addr == 0x50U) { + length = 16U; + } else if (msg->addr == 0x51U) { + length = 32U; + } else { + // The remaining admitted camera message retains its 24-byte shape. + } + const uint32_t xor_out = (length == 16U) ? 0x041DU : 0U; + valid = msg_matches(msg, msg->addr, 0U, length) && + (GET_BYTES_LE(msg, 0, 2) == (hyundai_common_canfd_compute_checksum(msg) ^ xor_out)); + if (valid && (msg->addr == 0x50U) && blended_aol_counter_seen) { + valid = msg->data[2] == (uint8_t)(blended_aol_counter + 1U); + } + if (valid && (msg->addr == 0x51U) && blended_adrv_counter_seen) { + valid = msg->data[2] == (uint8_t)(blended_adrv_counter + 1U); + } + if (valid && (msg->addr == 0x50U)) { + blended_aol_counter = msg->data[2]; + blended_aol_counter_seen = true; + } + if (valid && (msg->addr == 0x51U)) { + blended_adrv_counter = msg->data[2]; + blended_adrv_counter_seen = true; + } + if (valid && (msg->addr == 0x2A4U)) { + valid = blended_camera_seen && + (safety_get_ts_elapsed(microsecond_timer_get(), blended_camera_ts) <= 500000U) && + (msg->data[2] == blended_camera_data[2]) && (msg->data[7] == 0U); + for (unsigned int i = 3U; i < 24U; i++) { + if (i != 7U) { + valid &= msg->data[i] == blended_camera_data[i]; + } + } + } + } else if (blended_aol_enabled && (msg->addr == steering)) { + valid = msg_matches(msg, steering, 0U, 8U); + uint8_t counter = 0U; + if (valid) { + uint8_t crc = 0x22U; + for (unsigned int i = 1U; i < 8U; i++) { + crc = crc8_update(crc, msg->data[i], 0x1DU); + } + valid = (crc ^ 0xDFU) == msg->data[0]; + counter = (msg->data[4] >> 4U) & 0xFU; + } else { + } + if (valid && blended_aol_counter_seen) { + const unsigned int mask = 15U; + valid = counter == ((blended_aol_counter + 1U) & mask); + } + if (valid) { + blended_aol_counter = counter; + blended_aol_counter_seen = true; + } + } else { + // Messages outside the owned integrity paths retain the existing verdict. + } + if (!valid) { + blended_aol_clear(); + } + return valid; +} + +static void blended_aol_tx(const CANPacket_t *msg, bool accepted) { + if (blended_aol_enabled) { + bool accepted_current = accepted; + const uint32_t addresses[4] = {blended_aol_hda2 ? 0x50U : 0x340U, blended_aol_hda2 ? 0x2A4U : 0x364U, 0x485U, 0x340U}; + const unsigned int count = (blended_aol_hda2 && blended_aol_long) ? 4U : 3U; + if ((msg->addr == addresses[0]) && (msg->bus == 0U)) { + const bool active = GET_BIT(msg, blended_aol_hda2 ? 52U : 27U); + if (!accepted || !active) { + for (unsigned int i = 0U; i < count; i++) { + blended_aol_owned[i] = false; + } + } + accepted_current &= active; + } + for (unsigned int i = 0U; i < count; i++) { + const unsigned int bus = blended_aol_hda2 && (i >= 2U) ? 1U : 0U; + if ((msg->addr == addresses[i]) && (msg->bus == bus)) { + blended_aol_owned[i] = accepted_current && ((i == 0U) || (blended_aol_owned[0] && + safety_get_ts_elapsed(microsecond_timer_get(), blended_aol_tx_ts[0]) <= AOL_HOST_REQUEST_TIMEOUT_US)) && aol_rx_healthy() && (controls_allowed || (blended_aol_permission_mask() != 0U)); + blended_aol_tx_ts[i] = microsecond_timer_get(); + } + } + } +} + +static bool blended_aol_fwd(int bus, int addr) { + bool blocked = blended_aol_long && !blended_aol_enabled && (bus == 2) && + ((addr == (blended_aol_hda2 ? 0x50 : 0x340)) || + (addr == (blended_aol_hda2 ? 0x2A4 : 0x364)) || (!blended_aol_hda2 && (addr == 0x485))); + if (blended_aol_enabled && (bus == 2) && aol_rx_healthy() && (controls_allowed || (blended_aol_permission_mask() != 0U))) { + const uint32_t addresses[4] = {blended_aol_hda2 ? 0x50U : 0x340U, blended_aol_hda2 ? 0x2A4U : 0x364U, 0x485U, 0x340U}; + const unsigned int count = blended_aol_hda2 ? 2U : 3U; + for (unsigned int i = 0U; i < count; i++) { + blocked |= (addr == (int)addresses[i]) && blended_aol_owned[i] && + (safety_get_ts_elapsed(microsecond_timer_get(), blended_aol_tx_ts[i]) <= AOL_HOST_REQUEST_TIMEOUT_US); + } + } + return blocked; +} diff --git a/opendbc_repo/opendbc/safety/modes/hyundai_canfd.h b/opendbc_repo/opendbc/safety/modes/hyundai_canfd.h index bd44adb268..ad1b480048 100644 --- a/opendbc_repo/opendbc/safety/modes/hyundai_canfd.h +++ b/opendbc_repo/opendbc/safety/modes/hyundai_canfd.h @@ -2,6 +2,8 @@ #include "opendbc/safety/declarations.h" #include "opendbc/safety/modes/hyundai_common.h" +#include "opendbc/safety/modes/hyundai_canfd_aol.h" +#include "opendbc/safety/modes/hyundai_canfd_angle_aol.h" #define HYUNDAI_CANFD_CRUISE_BUTTON_TX_MSGS(bus) \ {0x1CF, bus, 8, .check_relay = false}, /* CRUISE_BUTTON */ \ @@ -33,6 +35,22 @@ {.msg = {{0xa0, (pt_bus), 24, 100U, .max_counter = 0xffU, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ {.msg = {{0xea, (pt_bus), 24, 100U, .max_counter = 0xffU, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ +// Only the three banked angle hybrids bind their observed fuel source exactly. +// Older profiles retain their established alternative-fuel RX definition. +#define HYUNDAI_CANFD_BANKED_ANGLE_RX_CHECKS(pt_bus, fuel_addr) \ + {.msg = {{(fuel_addr), (pt_bus), 32, 100U, .max_counter = 0xffU, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ + {.msg = {{0x175, (pt_bus), 24, 50U, .max_counter = 0xffU, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ + {.msg = {{0xa0, (pt_bus), 24, 100U, .max_counter = 0xffU, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ + {.msg = {{0xea, (pt_bus), 24, 100U, .max_counter = 0xffU, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ + +#define HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(pt_bus, fuel_addr) \ + HYUNDAI_CANFD_BANKED_ANGLE_RX_CHECKS(pt_bus, fuel_addr) \ + {.msg = {{0x1cf, (pt_bus), 8, 50U, .ignore_checksum = true, .max_counter = 0xfU, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ + +#define HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(pt_bus, fuel_addr) \ + HYUNDAI_CANFD_BANKED_ANGLE_RX_CHECKS(pt_bus, fuel_addr) \ + {.msg = {{0x1aa, (pt_bus), 16, 50U, .ignore_checksum = true, .max_counter = 0xffU, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ + #define HYUNDAI_CANFD_STD_BUTTONS_RX_CHECKS(pt_bus) \ HYUNDAI_CANFD_COMMON_RX_CHECKS(pt_bus) \ {.msg = {{0x1cf, (pt_bus), 8, 50U, .ignore_checksum = true, .max_counter = 0xfU, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ @@ -45,8 +63,341 @@ #define HYUNDAI_CANFD_SCC_ADDR_CHECK(scc_bus) \ {.msg = {{0x1a0, (scc_bus), 32, 50U, .max_counter = 0xffU, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ +// Exact stock-SCC Ioniq 5 PE banked angle profile. +#define HYUNDAI_IONIQ5_PE_STOCK_PARAM 0x5491U +#define HYUNDAI_EV9_STOCK_PARAM 0x5C91U +#define HYUNDAI_EV9_LONG_PARAM 0x5C95U +static bool hyundai_canfd_ev9_long = false; +static uint8_t hyundai_ev9_inactive_accel_count = 0U; +static bool hyundai_angle_aol_enabled = false; +static bool hyundai_angle_aol_drive = false; +static bool hyundai_angle_aol_eps_fault = false; +static bool hyundai_angle_aol_gear_seen = false; +static uint32_t hyundai_angle_aol_gear_ts = 0U; +static uint8_t hyundai_angle_aol_fault_mask = 2U; +static uint8_t hyundai_angle_aol_permission_mask(void); +static bool hyundai_ordinary_angle_stock = false; +static bool hyundai_ordinary_angle_drive = false; +static bool hyundai_ordinary_angle_eps_fault = false; +static bool hyundai_ordinary_angle_owned = false; +static bool hyundai_ordinary_angle_counter_seen = false; +static uint8_t hyundai_ordinary_angle_counter = 0U; +static uint32_t hyundai_ordinary_angle_accepted_ts = 0U; + +static void hyundai_ordinary_angle_release(void) { + hyundai_ordinary_angle_owned = false; + hyundai_ordinary_angle_counter_seen = false; + if (hyundai_ordinary_angle_stock || hyundai_angle_aol_enabled) { + // Match the shared limiter's inactive/violation baseline. Its rate and + // acceleration envelopes and real-time message budget remain unchanged. + desired_angle_last = SAFETY_CLAMP(angle_meas.values[0], -3600, 3600); + } +} + +static void hyundai_ordinary_angle_reset(void) { + hyundai_angle_aol_enabled = false; + hyundai_angle_aol_drive = false; + hyundai_angle_aol_eps_fault = false; + hyundai_angle_aol_gear_seen = false; + hyundai_angle_aol_gear_ts = 0U; + hyundai_ordinary_angle_release(); + hyundai_ordinary_angle_stock = false; + hyundai_canfd_ev9_long = false; + hyundai_ev9_inactive_accel_count = 0U; + hyundai_ordinary_angle_drive = false; + hyundai_ordinary_angle_eps_fault = false; + hyundai_ordinary_angle_counter_seen = false; + hyundai_ordinary_angle_counter = 0U; + hyundai_ordinary_angle_accepted_ts = 0U; +} + +static void hyundai_ordinary_angle_host_request(uint8_t mask) { + if ((mask & 1U) == 0U) { + hyundai_ordinary_angle_release(); + } +} + +static uint8_t hyundai_ordinary_angle_request_mask(void) { + uint8_t result = 0U; + if (hyundai_ordinary_angle_stock && heartbeat_engaged && !relay_malfunction && !safety_rx_checks_invalid && + (safety_get_ts_elapsed(microsecond_timer_get(), aol_host_request_ts) <= AOL_HOST_REQUEST_TIMEOUT_US)) { + result = aol_host_axis_mask & 1U; + } else { + hyundai_ordinary_angle_release(); + } + return result; +} + +static uint8_t hyundai_ordinary_angle_permission_mask(void) { + uint8_t result = 0U; + if (hyundai_angle_aol_enabled) { + result = hyundai_angle_aol_permission_mask(); + if (result == 0U) { + hyundai_ordinary_angle_release(); + } + } else if (aol_rx_healthy() && controls_allowed && vehicle_moving && hyundai_ordinary_angle_drive && !hyundai_ordinary_angle_eps_fault && + !brake_pressed && !gas_pressed && ((hyundai_ordinary_angle_request_mask() & 1U) != 0U)) { + result = 1U; + } else { + hyundai_ordinary_angle_release(); + } + return result; +} + +static bool hyundai_ordinary_angle_owner_current(void) { + if ((hyundai_ordinary_angle_permission_mask() == 0U) || !hyundai_ordinary_angle_owned || + (safety_get_ts_elapsed(microsecond_timer_get(), hyundai_ordinary_angle_accepted_ts) > AOL_HOST_REQUEST_TIMEOUT_US)) { + hyundai_ordinary_angle_release(); + } + return hyundai_ordinary_angle_owned; +} + +static bool hyundai_canfd_fwd_hook(int bus, int addr) { + bool blocked = false; + if ((bus == 2) && ((hyundai_ordinary_angle_stock && ((addr == 0x110) || (addr == 0x362))) || + (hyundai_angle_aol_enabled && ((addr == 0x50) || (addr == 0x110) || (addr == 0x12A) || (addr == 0xCB) || (addr == 0x2A4) || (addr == 0x362))))) { + blocked = hyundai_ordinary_angle_owner_current(); + } + if (hyundai_canfd_ev9_long) { + if ((bus == 2) && ((addr == 0x110) || (addr == 0x362))) { + blocked = controls_allowed && aol_rx_healthy() && vehicle_moving && hyundai_ordinary_angle_drive && + !hyundai_ordinary_angle_eps_fault && !brake_pressed && !gas_pressed; + } + // Original EV9 LONG replaces ADAS radar-track forwarding onto ECAN. + blocked |= (bus == 0) && (addr >= 0x3A5) && (addr <= 0x3C4); + } + return blocked; +} + static bool hyundai_canfd_alt_buttons = false; static bool hyundai_canfd_lka_steer_msg_alt = false; +static bool hyundai_canfd_carnival_alt_resume = false; +static bool hyundai_canfd_carnival_source_valid = false; +static bool hyundai_canfd_carnival_counter_consumed = false; +static bool hyundai_canfd_angle_steering = false; +static bool hyundai_canfd_angle_observed_adas = false; +static bool hyundai_canfd_ioniq6_long = false; +static bool aol_ioniq6_long = false; +static bool aol_optional_release_policy = false; +static bool aol_optional_release_neutral = false; +static uint8_t aol_optional_release_previous = 0U; +static bool aol_optional_release_pending = false; +static bool aol_optional_release_latch = false; +static uint32_t aol_optional_release_ts = 0U; +static bool aol_optional_release_counter_seen = false; +static bool hyundai_torque_long_aol_cancel_preserves_lateral = false; +static bool hyundai_canfd_torque_ev_long = false; +static bool aol_ioniq6_lateral_latch = false; +static bool aol_ioniq6_buttons_seen = false; +static bool aol_ioniq6_button_prev = false; +static bool aol_ioniq6_session_started = false; +static bool aol_ioniq6_request_seen = false; +static bool aol_ioniq6_lateral_token_claimed = false; +static uint32_t aol_ioniq6_gesture_ts = 0U; +static bool aol_ioniq6_enable_counter_seen = false; +static bool aol_ioniq6_enable_pending = false; +static bool aol_ioniq6_enable_neutral = false; +static bool aol_ioniq6_enable_release = false; +static uint32_t aol_ioniq6_enable_release_ts = 0U; +static bool hyundai_canfd_ioniq6_lfa_unpaired = false; +static uint32_t hyundai_canfd_ioniq6_lfa_ts = 0U; +static bool hyundai_canfd_ioniq6_heartbeat_seen = false; +static uint8_t hyundai_canfd_ioniq6_heartbeat_counter = 0U; +static bool hyundai_canfd_ioniq6_corner_valid = false; +static bool hyundai_canfd_ioniq6_corner_counter_seen = false; +static uint8_t hyundai_canfd_ioniq6_corner_counter = 0U; +static uint8_t hyundai_canfd_ioniq6_corner_bits = 0U; +static uint32_t hyundai_canfd_ioniq6_corner_ts = 0U; +static bool hyundai_canfd_ioniq6_lamp_valid = false; +static bool hyundai_canfd_ioniq6_left_lamp = false; +static bool hyundai_canfd_ioniq6_right_lamp = false; +static uint32_t hyundai_canfd_ioniq6_lamp_ts = 0U; +static uint32_t hyundai_canfd_ioniq6_left_lamp_off_ts = 0U; +static uint32_t hyundai_canfd_ioniq6_right_lamp_off_ts = 0U; +static bool hyundai_canfd_ioniq6_bsm_counter_seen = false; +static uint8_t hyundai_canfd_ioniq6_bsm_counter = 0U; +static bool hyundai_canfd_ioniq6_bsm_pair_pending = false; +static uint8_t hyundai_canfd_ioniq6_bsm_pair_counter = 0U; +static uint32_t hyundai_canfd_ioniq6_bsm_pair_ts = 0U; +static uint8_t hyundai_canfd_angle_model = 0U; +static uint8_t hyundai_canfd_carnival_consumed_counter = 0U; +static uint8_t hyundai_canfd_carnival_source[16] = {0}; + +static void aol_ioniq6_enable_reset(void) { + aol_ioniq6_enable_counter_seen = false; + aol_ioniq6_enable_neutral = false; + aol_ioniq6_enable_pending = false; + aol_ioniq6_enable_release = false; +} + +static void aol_ioniq6_reset(void) { + aol_ioniq6_long = false; + aol_ioniq6_enable_reset(); + aol_optional_release_capable = false; + aol_optional_release_policy = false; + aol_optional_release_neutral = false; + aol_optional_release_counter_seen = false; + aol_optional_release_previous = 0U; + aol_optional_release_pending = false; + aol_optional_release_latch = false; + + hyundai_torque_long_aol_cancel_preserves_lateral = false; + hyundai_canfd_torque_ev_long = false; + aol_ioniq6_lateral_latch = false; + aol_ioniq6_buttons_seen = false; + aol_ioniq6_button_prev = false; + aol_ioniq6_session_started = false; + aol_ioniq6_request_seen = false; + aol_ioniq6_lateral_token_claimed = false; + aol_ioniq6_gesture_ts = 0U; +} + +static void aol_ioniq6_host_request(uint8_t axis_mask) { + const bool policy = aol_optional_release_capable && ((axis_mask & 0x4U) != 0U); + if (!policy || !aol_optional_release_policy) { + aol_optional_release_neutral = false; + aol_optional_release_counter_seen = false; + aol_optional_release_previous = 0U; + aol_optional_release_pending = false; + aol_optional_release_latch = false; + } + aol_optional_release_policy = policy; + if ((axis_mask & 0x1U) == 0U) { + aol_optional_release_latch = false; + aol_optional_release_pending = false; + } + if (policy && aol_optional_release_pending && ((axis_mask & 0x1U) != 0U) && + heartbeat_engaged && aol_rx_healthy() && !relay_malfunction && + (safety_get_ts_elapsed(microsecond_timer_get(), aol_optional_release_ts) <= AOL_HOST_REQUEST_TIMEOUT_US)) { + aol_optional_release_latch = true; + aol_optional_release_pending = false; + } + + if (axis_mask != 0U) { + aol_ioniq6_request_seen = true; + } +} + +static uint8_t aol_ioniq6_request_mask(void) { + uint8_t request = 0U; + const uint32_t now = microsecond_timer_get(); + // A fresh physical gesture may precede the first host request and heartbeat. + const bool pre_session_expired = aol_ioniq6_lateral_latch && !aol_ioniq6_lateral_token_claimed && + (safety_get_ts_elapsed(now, aol_ioniq6_gesture_ts) > AOL_HOST_REQUEST_TIMEOUT_US); + const bool request_expired = aol_ioniq6_request_seen && + (safety_get_ts_elapsed(now, aol_host_request_ts) > AOL_HOST_REQUEST_TIMEOUT_US); + if (relay_malfunction || safety_rx_checks_invalid || + (aol_ioniq6_session_started && !heartbeat_engaged) || pre_session_expired || request_expired) { + aol_ioniq6_lateral_latch = false; + aol_ioniq6_enable_reset(); + aol_optional_release_policy = false; + aol_optional_release_neutral = false; + aol_optional_release_counter_seen = false; + aol_optional_release_previous = 0U; + aol_optional_release_pending = false; + aol_optional_release_latch = false; + aol_ioniq6_buttons_seen = false; + aol_ioniq6_button_prev = false; + aol_ioniq6_session_started = false; + aol_ioniq6_request_seen = false; + aol_ioniq6_lateral_token_claimed = false; + aol_host_axis_mask = 0U; + } else if (heartbeat_engaged) { + aol_ioniq6_session_started = true; + if (hyundai_canfd_ioniq6_long && controls_allowed && aol_ioniq6_enable_release && + ((aol_host_axis_mask & 0x3U) == 0x3U) && + (safety_get_ts_elapsed(now, aol_ioniq6_enable_release_ts) <= AOL_HOST_REQUEST_TIMEOUT_US)) { + aol_ioniq6_lateral_latch = true; + aol_ioniq6_enable_release = false; + } + if (((aol_host_axis_mask & 0x1U) != 0U) && aol_ioniq6_lateral_latch) { + aol_ioniq6_lateral_token_claimed = true; + } + request = aol_host_axis_mask; + } else { + // A pending physical gesture does not grant a host request before heartbeat. + } + return request; +} + +static uint8_t aol_ioniq6_permission_mask(void) { + uint8_t permission = 0U; + if (!aol_rx_healthy()) { + aol_ioniq6_lateral_latch = false; + aol_ioniq6_enable_reset(); + aol_optional_release_policy = false; + aol_optional_release_neutral = false; + aol_optional_release_counter_seen = false; + aol_optional_release_previous = 0U; + aol_optional_release_pending = false; + aol_optional_release_latch = false; + aol_ioniq6_buttons_seen = false; + aol_ioniq6_button_prev = false; + aol_ioniq6_session_started = false; + aol_ioniq6_request_seen = false; + aol_ioniq6_lateral_token_claimed = false; + aol_host_axis_mask = 0U; + } else { + const uint8_t request = aol_get_request_mask(); + if (((request & 0x1U) != 0U) && (aol_ioniq6_lateral_latch || aol_optional_release_latch || (!hyundai_angle_aol_enabled && controls_allowed))) { + permission |= 0x1U; + } + if (hyundai_longitudinal && ((request & 0x2U) != 0U) && controls_allowed) { + permission |= 0x2U; + } + } + return permission; +} + +static uint8_t hyundai_angle_aol_permission_mask(void) { + const uint8_t request = aol_ioniq6_permission_mask(); + const bool gear_current = hyundai_angle_aol_gear_seen && + (safety_get_ts_elapsed(microsecond_timer_get(), hyundai_angle_aol_gear_ts) <= 100000U); + return (((request & 1U) != 0U) && gear_current && hyundai_angle_aol_drive && vehicle_moving && + !hyundai_angle_aol_eps_fault && !gas_pressed && !brake_pressed) ? 1U : 0U; +} + +static void aol_ioniq6_rx_invalid(void) { + aol_ioniq6_enable_reset(); + aol_optional_release_policy = false; + aol_optional_release_neutral = false; + aol_optional_release_counter_seen = false; + aol_optional_release_previous = 0U; + aol_optional_release_pending = false; + aol_optional_release_latch = false; + + aol_ioniq6_lateral_latch = false; + aol_ioniq6_lateral_token_claimed = false; + aol_ioniq6_buttons_seen = false; + aol_ioniq6_button_prev = false; +} + +static bool hyundai_canfd_ioniq6_rx_healthy(void) { + const uint32_t now = microsecond_timer_get(); + bool healthy = (current_safety_config.rx_checks_len > 0) && (current_safety_config.rx_checks != NULL); + if (healthy) { + for (int i = 0; i < current_safety_config.rx_checks_len; i++) { + const RxCheck *check = ¤t_safety_config.rx_checks[i]; + healthy = check->status.msg_seen && !check->status.lagging && check->status.valid_checksum && + check->status.valid_quality_flag && (check->status.wrong_counters < MAX_WRONG_COUNTERS); + if (healthy) { + const uint32_t frequency = check->msg[check->status.index].frequency; + if (frequency < 10U) { + healthy = false; + } else { + const uint32_t period_limit = 10000000U / frequency; + const uint32_t max_age = (period_limit > 100000U) ? period_limit : 100000U; + healthy = safety_get_ts_elapsed(now, check->status.last_timestamp) <= max_age; + } + } + if (!healthy) { + break; + } + } + } + return healthy; +} +static uint32_t hyundai_canfd_carnival_source_ts = 0U; static unsigned int hyundai_canfd_get_lka_addr(void) { return hyundai_canfd_lka_steer_msg_alt ? 0x110U : 0x50U; @@ -67,7 +418,97 @@ static uint32_t hyundai_canfd_get_checksum(const CANPacket_t *msg) { return chksum; } +static void hyundai_canfd_ioniq6_optional_rx_hook(const CANPacket_t *msg) { + const uint32_t now = microsecond_timer_get(); + if (msg->addr == 0x36aU) { + if ((GET_LEN(msg) == 16U) && + (hyundai_canfd_get_checksum(msg) == (hyundai_common_canfd_compute_checksum(msg) ^ 0x041DU)) && + ((!hyundai_canfd_ioniq6_corner_counter_seen) || (msg->data[2] != hyundai_canfd_ioniq6_corner_counter))) { + hyundai_canfd_ioniq6_corner_valid = true; + hyundai_canfd_ioniq6_corner_counter_seen = true; + hyundai_canfd_ioniq6_corner_counter = msg->data[2]; + hyundai_canfd_ioniq6_corner_bits = msg->data[3] & 0x18U; + hyundai_canfd_ioniq6_corner_ts = now; + } else { + hyundai_canfd_ioniq6_corner_valid = false; + } + } else if ((msg->addr == 0x413U) && (GET_LEN(msg) == 8U)) { + if (!hyundai_canfd_ioniq6_lamp_valid || safety_get_ts_elapsed(now, hyundai_canfd_ioniq6_lamp_ts) > 1500000U) { + // A new frame after source loss cannot create a hold from an old true. + hyundai_canfd_ioniq6_left_lamp = false; + hyundai_canfd_ioniq6_right_lamp = false; + hyundai_canfd_ioniq6_left_lamp_off_ts = 0U; + hyundai_canfd_ioniq6_right_lamp_off_ts = 0U; + } + const bool alternate = (msg->data[7] & 0x40U) != 0U; + const bool left = alternate ? ((msg->data[7] & 0x08U) != 0U) : ((msg->data[2] & 0x10U) != 0U); + const bool right = alternate ? ((msg->data[7] & 0x20U) != 0U) : ((msg->data[2] & 0x40U) != 0U); + if (hyundai_canfd_ioniq6_left_lamp && !left) { + hyundai_canfd_ioniq6_left_lamp_off_ts = now; + } + if (hyundai_canfd_ioniq6_right_lamp && !right) { + hyundai_canfd_ioniq6_right_lamp_off_ts = now; + } + if (left) { + hyundai_canfd_ioniq6_left_lamp_off_ts = 0U; + } + if (right) { + hyundai_canfd_ioniq6_right_lamp_off_ts = 0U; + } + hyundai_canfd_ioniq6_left_lamp = left; + hyundai_canfd_ioniq6_right_lamp = right; + hyundai_canfd_ioniq6_lamp_valid = true; + hyundai_canfd_ioniq6_lamp_ts = now; + } else if (msg->addr == 0x413U) { + hyundai_canfd_ioniq6_lamp_valid = false; + hyundai_canfd_ioniq6_left_lamp = false; + hyundai_canfd_ioniq6_right_lamp = false; + hyundai_canfd_ioniq6_left_lamp_off_ts = 0U; + hyundai_canfd_ioniq6_right_lamp_off_ts = 0U; + } else { + // No optional Ioniq 6 source update for unrelated addresses. + } +} + +static void hyundai_canfd_optional_rx_hook(const CANPacket_t *msg) { + if (hyundai_canfd_ioniq6_long && (msg->bus == 1U)) { + hyundai_canfd_ioniq6_optional_rx_hook(msg); + } +} + +static bool hyundai_canfd_ioniq6_bsm_sources_healthy(uint32_t now) { + return hyundai_canfd_ioniq6_rx_healthy() && hyundai_canfd_ioniq6_corner_valid && hyundai_canfd_ioniq6_lamp_valid && + safety_get_ts_elapsed(now, hyundai_canfd_ioniq6_corner_ts) <= 100000U && + safety_get_ts_elapsed(now, hyundai_canfd_ioniq6_lamp_ts) <= 1500000U; +} + +static uint8_t hyundai_canfd_ioniq6_bsm_level(bool detected, bool lamp, uint32_t lamp_off_ts, uint32_t now) { + const bool held_lamp = lamp || ((lamp_off_ts != 0U) && (safety_get_ts_elapsed(now, lamp_off_ts) <= 500000U)); + return detected ? (held_lamp ? 2U : 1U) : 0U; +} + static void hyundai_canfd_rx_hook(const CANPacket_t *msg) { + if (hyundai_angle_aol_enabled) { + const unsigned int pt = hyundai_canfd_lka_steer_msg ? 1U : 0U; + const unsigned int gear_addr = hyundai_ev_gas_signal ? 0x35U : 0x70U; + if (msg_matches(msg, gear_addr, pt)) { + hyundai_angle_aol_drive = (hyundai_ev_gas_signal ? (msg->data[24] & 7U) : ((msg->data[7] >> 4U) & 7U)) == 5U; + hyundai_angle_aol_gear_seen = true; + hyundai_angle_aol_gear_ts = microsecond_timer_get(); + } + if (msg_matches(msg, 0xEAU, pt)) { + hyundai_angle_aol_eps_fault = (((msg->data[6] >> 6U) & 3U) != 0U) || + ((((msg->data[18] >> 4U) & 7U) & hyundai_angle_aol_fault_mask) != 0U); + } + } + if ((hyundai_ordinary_angle_stock || hyundai_canfd_ev9_long) && (msg->bus == 1U) && (msg->addr == 0x35U)) { + hyundai_ordinary_angle_drive = (msg->data[24] & 0x7U) == 5U; + } + + if ((hyundai_ordinary_angle_stock || hyundai_canfd_ev9_long) && (msg->bus == 1U) && (msg->addr == 0xEAU)) { + const uint8_t fault = (msg->data[18] >> 4U) & 0x7U; + hyundai_ordinary_angle_eps_fault = hyundai_canfd_ev9_long ? ((fault & 2U) != 0U) : (fault != 0U); + } const unsigned pt_bus = hyundai_canfd_lka_steer_msg ? 1U : 0U; const unsigned int scc_bus = hyundai_camera_scc ? 2U : pt_bus; @@ -76,6 +517,11 @@ static void hyundai_canfd_rx_hook(const CANPacket_t *msg) { int torque_driver_new = ((msg->data[11] & 0x1fU) << 8U) | msg->data[10]; torque_driver_new -= 4095; update_sample(&torque_driver, torque_driver_new); + if (hyundai_canfd_angle_steering) { + const unsigned int offset = hyundai_canfd_ev9_long ? 16U : 12U; + int angle_meas_new = (msg->data[offset + 1U] << 8U) | msg->data[offset]; + update_sample(&angle_meas, to_signed(angle_meas_new, 16)); + } } // cruise buttons @@ -90,18 +536,131 @@ static void hyundai_canfd_rx_hook(const CANPacket_t *msg) { cruise_button = (msg->data[4] >> 4) & 0x7U; main_button = GET_BIT(msg, 34U); } + const bool previous_controls_allowed = controls_allowed; + const int previous_cruise_button = cruise_button_prev; hyundai_common_cruise_buttons_check(cruise_button, main_button); + if (hyundai_canfd_ioniq6_long && aol_ioniq6_long) { + static uint8_t aol_ioniq6_enable_counter = 0U; + static uint32_t aol_ioniq6_enable_ts = 0U; + const uint8_t counter = hyundai_canfd_get_counter(msg); + const uint32_t now = microsecond_timer_get(); + const uint32_t elapsed = safety_get_ts_elapsed(now, aol_ioniq6_enable_ts); + const bool fresh = aol_ioniq6_enable_counter_seen && + (counter == ((aol_ioniq6_enable_counter + 1U) & 0xFU)) && (elapsed > 0U) && (elapsed <= 100000U); + if (!fresh || main_button || GET_BIT(msg, 23U)) { + aol_ioniq6_enable_pending = false; + aol_ioniq6_enable_release = false; + } else if (aol_ioniq6_enable_neutral && (previous_cruise_button == HYUNDAI_BTN_NONE) && + ((cruise_button == HYUNDAI_BTN_SET) || (cruise_button == HYUNDAI_BTN_RESUME))) { + aol_ioniq6_enable_pending = true; + } else if (aol_ioniq6_enable_pending && (cruise_button == HYUNDAI_BTN_NONE) && + ((previous_cruise_button == HYUNDAI_BTN_SET) || (previous_cruise_button == HYUNDAI_BTN_RESUME))) { + if (controls_allowed && aol_rx_healthy()) { + heartbeat_engaged_mismatches = 0U; + aol_ioniq6_enable_release = true; + aol_ioniq6_enable_release_ts = now; + } + aol_ioniq6_enable_pending = false; + } else if ((cruise_button != HYUNDAI_BTN_SET) && (cruise_button != HYUNDAI_BTN_RESUME)) { + aol_ioniq6_enable_pending = false; + } else { + // A held enable button does not renew the heartbeat grace. + } + aol_ioniq6_enable_neutral = !main_button && !GET_BIT(msg, 23U) && (cruise_button == HYUNDAI_BTN_NONE); + aol_ioniq6_enable_counter_seen = true; + aol_ioniq6_enable_counter = counter; + aol_ioniq6_enable_ts = now; + } + if (hyundai_canfd_ev9_long && !previous_controls_allowed && controls_allowed) { + hyundai_ev9_inactive_accel_count = 0U; + } + if (aol_ioniq6_long && (msg_matches(msg, hyundai_canfd_alt_buttons ? 0x1aaU : 0x1cfU, pt_bus))) { + if (aol_optional_release_capable && aol_optional_release_policy) { + static uint8_t aol_optional_release_counter = 0U; + static uint32_t aol_optional_release_sample_ts = 0U; + const uint8_t counter = hyundai_canfd_get_counter(msg); + const uint8_t maximum = hyundai_canfd_alt_buttons ? 0xffU : 0xfU; + const uint32_t sample_ts = microsecond_timer_get(); + const uint32_t elapsed = safety_get_ts_elapsed(sample_ts, aol_optional_release_sample_ts); + const bool fresh = !aol_optional_release_counter_seen || + (((uint8_t)((aol_optional_release_counter + 1U) & maximum) == counter) && + (elapsed > 0U) && (elapsed <= 100000U)); + aol_optional_release_counter_seen = true; + aol_optional_release_counter = counter; + aol_optional_release_sample_ts = sample_ts; + if (!fresh) { + aol_optional_release_neutral = false; + aol_optional_release_previous = 0U; + aol_optional_release_pending = false; + aol_optional_release_latch = false; + } + const bool raw_neutral = fresh && !main_button && + !(hyundai_canfd_alt_buttons ? GET_BIT(msg, 39U) : GET_BIT(msg, 23U)) && (cruise_button == HYUNDAI_BTN_NONE); + if (!aol_optional_release_neutral) { + aol_optional_release_neutral = raw_neutral; + } else if (fresh && ((aol_optional_release_previous == HYUNDAI_BTN_SET) || + (aol_optional_release_previous == HYUNDAI_BTN_RESUME)) && (cruise_button == HYUNDAI_BTN_NONE)) { + aol_optional_release_pending = true; + aol_optional_release_ts = microsecond_timer_get(); + } else { + // No release edge; retain the current neutral/release state. + } + aol_optional_release_previous = (uint8_t)cruise_button; + } + const bool gesture = main_button || (hyundai_canfd_alt_buttons ? GET_BIT(msg, 39U) : GET_BIT(msg, 23U)); // LDA/LKAS button + if (cruise_button == HYUNDAI_BTN_CANCEL) { + aol_ioniq6_enable_reset(); + // Exact DEBUG torque LONG AOL keeps only previously authorized lateral. + // Common cruise handling above still withdraws ordinary LONG; CANCEL + // cannot create a gesture, refresh a lease, or claim a lateral token. + if (!hyundai_torque_long_aol_cancel_preserves_lateral) { + aol_ioniq6_lateral_latch = false; + aol_optional_release_policy = false; + aol_optional_release_neutral = false; + aol_optional_release_counter_seen = false; + aol_optional_release_previous = 0U; + aol_optional_release_pending = false; + aol_optional_release_latch = false; + aol_ioniq6_lateral_token_claimed = false; + aol_set_host_request(0U); + } + } else if (!aol_ioniq6_buttons_seen) { + // A held button at safety init is not a fresh deliberate gesture. + aol_ioniq6_buttons_seen = !gesture; + } else if (gesture && !aol_ioniq6_button_prev) { + // Idempotent native authorization: host request remains the on/off + // toggle, so an RX/heartbeat reset cannot invert the two phases. + aol_ioniq6_lateral_latch = true; + aol_ioniq6_lateral_token_claimed = false; + aol_ioniq6_gesture_ts = microsecond_timer_get(); + } else { + // No new deliberate gesture in this button sample. + } + aol_ioniq6_button_prev = gesture; + } + } + if (hyundai_canfd_carnival_alt_resume && (msg_matches(msg, 0x1aaU, pt_bus))) { + hyundai_canfd_carnival_source_valid = false; + const bool idle_buttons = ((msg->data[4] & 0xF4U) == 0U) && ((msg->data[5] & 0x02U) == 0U); + if (idle_buttons && ((!hyundai_canfd_carnival_counter_consumed) || + (msg->data[2] != hyundai_canfd_carnival_consumed_counter)) && + (hyundai_canfd_get_checksum(msg) == (hyundai_common_canfd_compute_checksum(msg) ^ 0x041DU))) { + for (int i = 0; i < 16; i++) { + hyundai_canfd_carnival_source[i] = msg->data[i]; + } + hyundai_canfd_carnival_source_ts = microsecond_timer_get(); + hyundai_canfd_carnival_source_valid = true; + } } // gas press, different for EV, hybrid, and ICE models - if (msg_matches(msg, 0x35U, pt_bus) && hyundai_ev_gas_signal) { + if ((msg_matches(msg, 0x35U, pt_bus)) && hyundai_ev_gas_signal) { gas_pressed = msg->data[5] != 0U; - } - if (msg_matches(msg, 0x105U, pt_bus) && hyundai_hybrid_gas_signal) { - gas_pressed = ((msg->data[12] >> 7) | msg->data[13] | (msg->data[14] & 1U)) != 0U; - } - if (msg_matches(msg, 0x100U, pt_bus) && !hyundai_ev_gas_signal && !hyundai_hybrid_gas_signal) { + } else if ((msg_matches(msg, 0x105U, pt_bus)) && hyundai_hybrid_gas_signal) { + gas_pressed = GET_BIT(msg, 103U) || (msg->data[13] != 0U) || GET_BIT(msg, 112U); + } else if ((msg_matches(msg, 0x100U, pt_bus)) && !hyundai_ev_gas_signal && !hyundai_hybrid_gas_signal) { gas_pressed = GET_BIT(msg, 176U); + } else { } // brake press @@ -131,11 +690,12 @@ static void hyundai_canfd_rx_hook(const CANPacket_t *msg) { } } -static bool hyundai_canfd_tx_hook(const CANPacket_t *msg) { +static bool hyundai_canfd_tx_hook_valid(const CANPacket_t *msg) { + static uint8_t hyundai_canfd_ioniq6_lfa_mirror[4] = {0}; const TorqueSteeringLimits HYUNDAI_CANFD_STEERING_LIMITS = { - .max_torque = 270, - .max_rt_delta = 112, - .max_rate_up = 2, + .max_torque = 409, + .max_rt_delta = 375, + .max_rate_up = 10, .max_rate_down = 3, .driver_torque_allowance = 250, .driver_torque_multiplier = 2, @@ -148,17 +708,250 @@ static bool hyundai_canfd_tx_hook(const CANPacket_t *msg) { .min_valid_request_rt_interval = 810000, // 810ms; a ~10% buffer on cutting every 90 frames .has_steer_req_tolerance = true, }; + const AngleSteeringLimits HYUNDAI_CANFD_ANGLE_LIMITS = { + .max_angle = 3600, + .angle_deg_to_can = 10, + .frequency = 100U, + }; + // These are the actual CP VehicleModel geometries, including tire-stiffness + // derived slip. Selector bits 11-13 are valid only under the angle bit 14. + const AngleSteeringParams HYUNDAI_CANFD_ANGLE_MODELS[] = { + {.slip_factor = -0.0005844189175F, .steer_ratio = 14.6F, .wheelbase = 2.87F}, + {.slip_factor = -0.0005844189054F, .steer_ratio = 17.1F, .wheelbase = 2.87F}, + {.slip_factor = -0.0005685702046F, .steer_ratio = 14.14F, .wheelbase = 2.95F}, + {.slip_factor = -0.0005358728714F, .steer_ratio = 16.02F, .wheelbase = 3.13F}, + {.slip_factor = -0.0005793721360F, .steer_ratio = 13.5F, .wheelbase = 2.895F}, + {.slip_factor = -0.0009169985667F, .steer_ratio = 13.27F, .wheelbase = 2.814F}, + {.slip_factor = -0.0006085930193F, .steer_ratio = 13.7F, .wheelbase = 2.756F}, + {.slip_factor = -0.0005968975988F, .steer_ratio = 13.72F, .wheelbase = 2.81F}, // CCNC Santa Fe HEV + {.slip_factor = -0.0006085929296F, .steer_ratio = 13.7F, .wheelbase = 2.756F}, // CCNC Sportage 2026 + {.slip_factor = -0.0008898049378F, .steer_ratio = 14.26F, .wheelbase = 2.9F}, // EV6 2025 + {.slip_factor = -0.0008688329820F, .steer_ratio = 14.26F, .wheelbase = 2.97F}, // Ioniq 5 PE: typed CP geometry + {.slip_factor = -0.0005410588126F, .steer_ratio = 16.0F, .wheelbase = 3.1F}, // EV9: typed CP geometry + }; bool tx = true; // steering + const bool torque_lka_long = hyundai_canfd_ioniq6_long || hyundai_canfd_torque_ev_long || + (hyundai_canfd_lka_steer_msg && hyundai_longitudinal && !hyundai_canfd_angle_steering); const unsigned int steer_addr = (hyundai_canfd_lka_steer_msg && !hyundai_longitudinal) ? hyundai_canfd_get_lka_addr() : 0x12aU; if (msg->addr == steer_addr) { - int desired_torque = (((msg->data[6] & 0xFU) << 7U) | (msg->data[5] >> 1U)) - 1024U; - bool steer_req = GET_BIT(msg, 52U); + if (torque_lka_long) { + // Even a rejected replacement invalidates an older unpaired command. + hyundai_canfd_ioniq6_lfa_unpaired = false; + } + if (hyundai_canfd_ev9_long && (msg->addr == 0x12AU)) { + // Reached original caller emits only neutral LFA outside Drive. + const unsigned int torque_unsigned = (((unsigned int)msg->data[6] & 0xFU) << 7U) | ((unsigned int)msg->data[5] >> 1U); + const int torque = (int)torque_unsigned - 1024; + tx &= (torque == 0) && ((msg->data[6] & 0x30U) == 0U); + for (int i = 7; i < 16; i++) { + tx &= msg->data[i] == ((i == 13) ? 0x64U : 0U); + } + } else if (hyundai_canfd_angle_steering) { + const int angle_active = (msg->data[9] >> 4U) & 0x3U; + const bool steer_req = angle_active == 2; + int desired_angle = (msg->data[11] << 6U) | (msg->data[10] >> 2U); + desired_angle = to_signed(desired_angle, 14); + const uint8_t gain_raw = msg->data[12]; + const unsigned int torque_high = ((unsigned int)msg->data[6] & 0xFU) << 7U; + const unsigned int torque_low = (unsigned int)msg->data[5] >> 1U; + const unsigned int torque_bits = torque_high | torque_low; + const int torque_req = (int)torque_bits - 1024; + const int torque_enabled = (msg->data[6] >> 4U) & 0x3U; + // The current LFA DBC aliases FCA_ESA_CtrlSta onto a gain bit, so only + // LKAS_ALT has an independently checkable FCA enable field. + const bool fca_esa_enabled = (msg->addr == 0x110U) && ((msg->data[13] & 0x08U) != 0U); + const bool neutral_status = hyundai_canfd_angle_observed_adas && (msg->addr == 0x12AU); + if (((angle_active != 1) && (angle_active != 2)) || (gain_raw > 250U) || + (torque_req != 0) || (torque_enabled != 0) || fca_esa_enabled || + ((!steer_req) && (gain_raw != 0U)) || + (neutral_status && ((angle_active != 1) || steer_angle_cmd_inactive_check(desired_angle, HYUNDAI_CANFD_ANGLE_LIMITS.max_angle))) || + (!neutral_status && steer_angle_cmd_checks_vm(desired_angle, steer_req, HYUNDAI_CANFD_ANGLE_LIMITS, + HYUNDAI_CANFD_ANGLE_MODELS[hyundai_canfd_angle_model]))) { + tx = false; + } + } else { + int desired_torque = (((msg->data[6] & 0xFU) << 7U) | (msg->data[5] >> 1U)) - 1024U; + bool steer_req = GET_BIT(msg, 52U); + if (steer_torque_cmd_checks(desired_torque, steer_req, HYUNDAI_CANFD_STEERING_LIMITS)) { + tx = false; + } + if (torque_lka_long) { + // This E-CAN command is the sole limited steering authority. Other + // angle/enable fields in the same payload remain the host's fixed + // neutral status, and CRC must cover the exact wire bytes. + for (int i = 7; i < 16; i++) { + tx &= msg->data[i] == ((i == 13) ? 0x64U : 0U); + } + tx &= hyundai_canfd_get_checksum(msg) == (hyundai_common_canfd_compute_checksum(msg) ^ 0x041DU); + } + if ((torque_lka_long) && tx) { + for (int i = 0; i < 4; i++) { + hyundai_canfd_ioniq6_lfa_mirror[i] = msg->data[i + 3]; + } + hyundai_canfd_ioniq6_lfa_ts = microsecond_timer_get(); + hyundai_canfd_ioniq6_lfa_unpaired = true; + } + } + } - if (steer_torque_cmd_checks(desired_torque, steer_req, HYUNDAI_CANFD_STEERING_LIMITS)) { + // The HDA-II controller sends its limited E-CAN LFA command first, followed + // by the identical steering/status request on A-CAN. The second channel may + // mirror only that single fresh, accepted command; it must not run the global + // torque limiter a second time or create independent steering authority. + if ((torque_lka_long) && (msg->addr == hyundai_canfd_get_lka_addr())) { + bool mirror = hyundai_canfd_ioniq6_lfa_unpaired && + safety_get_ts_elapsed(microsecond_timer_get(), hyundai_canfd_ioniq6_lfa_ts) <= 10000U; + const int mirror_torque = (((msg->data[6] & 0xFU) << 7U) | (msg->data[5] >> 1U)) - 1024U; + const bool mirror_req = GET_BIT(msg, 52U); + if (((mirror_torque != 0) || mirror_req) && !lateral_controls_allowed()) { + mirror = false; + } + for (int i = 0; i < 4; i++) { + mirror &= msg->data[i + 3] == hyundai_canfd_ioniq6_lfa_mirror[i]; + } + if (msg->addr == 0x110U) { + mirror &= (msg->data[13] & 0x08U) == 0U; // no FCA_ESA actuation + } + for (uint8_t i = 7U; i < GET_LEN(msg); i++) { + mirror &= msg->data[i] == (((i == 8U) && !hyundai_canfd_ioniq6_long) ? 0x64U : 0U); + } + mirror &= hyundai_canfd_get_checksum(msg) == (hyundai_common_canfd_compute_checksum(msg) ^ + ((GET_LEN(msg) == 16U) ? 0x041DU : 0U)); + hyundai_canfd_ioniq6_lfa_unpaired = false; + tx &= mirror; + } + + if (hyundai_canfd_ioniq6_long && ((msg->addr == 0x51U) || (msg->addr == 0x160U) || (msg->addr == 0x1EAU) || + (msg->addr == 0x200U) || (msg->addr == 0x345U) || (msg->addr == 0x1DAU))) { + // These ADR V frames are constant-status emulation in the current host + // controller. Reject any other actuation/enable payload, including one + // with a recomputed CRC. Bytes 0-1 are CRC, byte 2 is the rolling counter; + // 0x160 additionally has a 20-frame warning counter in byte 3. + const uint8_t adr_51[29] = {0}; + const uint8_t adr_160[12] = {0, 0, 0, 2, 0xff, 0xfc, 9, 0, 0, 0, 0, 0}; + const uint8_t adr_1ea[29] = {[0] = 0x1c, [12] = 0xff, [26] = 0x0f, [27] = 0x0f}; + const uint8_t adr_200[5] = {0xe1, 0x3a, 0, 0, 0}; + const uint8_t adr_345[5] = {0x15, 0, 0, 0, 0}; + const uint8_t adr_1da[29] = { + 0x22, 0, 0x41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + const uint8_t *expected = NULL; + if (msg->addr == 0x51U) expected = adr_51; + if (msg->addr == 0x1EAU) expected = adr_1ea; + if (msg->addr == 0x200U) expected = adr_200; + if (msg->addr == 0x345U) expected = adr_345; + if (msg->addr == 0x1DAU) expected = adr_1da; + if (expected != NULL) { + for (uint8_t i = 3U; i < GET_LEN(msg); i++) { + tx &= msg->data[i] == expected[i - 3U]; + } + } else { + // 0x160 byte 3 is a source warning counter; all other bytes are fixed. + for (uint8_t i = 4U; i < GET_LEN(msg); i++) { + tx &= msg->data[i] == adr_160[i - 4U]; + } + } + uint16_t crc_xor = (GET_LEN(msg) == 8U) ? 0x5F29U : ((GET_LEN(msg) == 16U) ? 0x041DU : 0U); + tx &= hyundai_canfd_get_checksum(msg) == (hyundai_common_canfd_compute_checksum(msg) ^ crc_xor); + } + + if (hyundai_canfd_ioniq6_long && (msg->addr == 0x100U)) { + // User-route sendcan evidence: this A-CAN radar heartbeat keeps one + // constant 24-byte body; only CRC, counter, physical brake, and physical + // gas may vary. A duplicate counter cannot replay the last heartbeat. + static const uint8_t body[24] = { + 0, 0, 0, 2, 0, 0, 0xfc, 0xff, 0, 0, 0, 0, 0, 0x20, 0, 0, + 0x55, 0xff, 0, 0, 0x68, 0, 0, 0, + }; + bool valid = hyundai_canfd_ioniq6_rx_healthy() && + (hyundai_canfd_get_checksum(msg) == hyundai_common_canfd_compute_checksum(msg)) && + ((!hyundai_canfd_ioniq6_heartbeat_seen) || (msg->data[2] != hyundai_canfd_ioniq6_heartbeat_counter)) && + (GET_BIT(msg, 32U) == brake_pressed) && (GET_BIT(msg, 176U) == gas_pressed); + for (int i = 3; i < 24; i++) { + const uint8_t mask = ((i == 4) || (i == 22)) ? 0xfeU : 0xffU; + valid &= (msg->data[i] & mask) == body[i]; + } + if (valid) { + hyundai_canfd_ioniq6_heartbeat_seen = true; + hyundai_canfd_ioniq6_heartbeat_counter = msg->data[2]; + } + tx &= valid; + } + + if (hyundai_canfd_ioniq6_long && (msg->addr == 0x1baU)) { + // 0x1BA begins a one-use, paired blind-spot dashboard status. Any new + // attempt invalidates the older pending pair, accepted or rejected. + hyundai_canfd_ioniq6_bsm_pair_pending = false; + const uint32_t now = microsecond_timer_get(); + bool valid = hyundai_canfd_ioniq6_bsm_sources_healthy(now) && + (hyundai_canfd_get_checksum(msg) == hyundai_common_canfd_compute_checksum(msg)) && + ((!hyundai_canfd_ioniq6_bsm_counter_seen) || (msg->data[2] != hyundai_canfd_ioniq6_bsm_counter)); + const uint8_t left = hyundai_canfd_ioniq6_bsm_level((hyundai_canfd_ioniq6_corner_bits & 0x10U) != 0U, + hyundai_canfd_ioniq6_left_lamp, hyundai_canfd_ioniq6_left_lamp_off_ts, now); + const uint8_t right = hyundai_canfd_ioniq6_bsm_level((hyundai_canfd_ioniq6_corner_bits & 0x08U) != 0U, + hyundai_canfd_ioniq6_right_lamp, hyundai_canfd_ioniq6_right_lamp_off_ts, now); + uint8_t body[24] = {0}; + body[3] = ((left || right) ? 1U : 0U) | (left << 6U); + body[4] = right; + body[5] = left << 1U; + body[6] = right << 5U; + body[7] = 0x80U | (SAFETY_MAX(left, right) << 3U); + body[16] = left | (right << 2U); + for (int i = 3; i < 24; i++) { + valid &= msg->data[i] == body[i]; + } + if (valid) { + hyundai_canfd_ioniq6_bsm_counter_seen = true; + hyundai_canfd_ioniq6_bsm_counter = msg->data[2]; + hyundai_canfd_ioniq6_bsm_pair_pending = true; + hyundai_canfd_ioniq6_bsm_pair_counter = msg->data[2]; + hyundai_canfd_ioniq6_bsm_pair_ts = now; + } + tx &= valid; + } + if (hyundai_canfd_ioniq6_long && (msg->addr == 0x1e5U)) { + const uint32_t now = microsecond_timer_get(); + static const uint8_t body[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0}; + bool valid = hyundai_canfd_ioniq6_bsm_pair_pending && + hyundai_canfd_ioniq6_bsm_sources_healthy(now) && + (safety_get_ts_elapsed(now, hyundai_canfd_ioniq6_bsm_pair_ts) <= 10000U) && + (msg->data[2] == hyundai_canfd_ioniq6_bsm_pair_counter) && + hyundai_canfd_get_checksum(msg) == (hyundai_common_canfd_compute_checksum(msg) ^ 0x041DU); + hyundai_canfd_ioniq6_bsm_pair_pending = false; + for (int i = 3; i < 16; i++) { + valid &= msg->data[i] == body[i]; + } + tx &= valid; + } + + if (msg->addr == 0xCBU) { + if (!hyundai_canfd_angle_steering || (!hyundai_canfd_ev9_long && (!hyundai_canfd_angle_observed_adas || hyundai_canfd_lka_steer_msg))) { tx = false; + } else { + const int angle_active = (msg->data[3] >> 4U) & 0xFU; + const bool steer_req = angle_active == 2; + int desired_angle = ((msg->data[5] & 0x3FU) << 8U) | msg->data[4]; + desired_angle = to_signed(desired_angle, 14); + const uint8_t gain_raw = msg->data[6]; + if (hyundai_canfd_ev9_long) { + tx &= (msg->bus == 1U) && aol_rx_healthy() && !relay_malfunction && + (hyundai_canfd_get_checksum(msg) == hyundai_common_canfd_compute_checksum(msg)); + if (steer_req) { + tx &= controls_allowed && vehicle_moving && hyundai_ordinary_angle_drive && + !hyundai_ordinary_angle_eps_fault && !brake_pressed && !gas_pressed; + } + } + if (((angle_active != 1) && (angle_active != 2)) || (gain_raw > 250U) || + ((!steer_req) && (gain_raw != 0U)) || ((msg->data[3] & 0xFU) != 0U) || + ((msg->data[7] & 0x3U) != 0U) || (msg->data[8] != 0U) || + steer_angle_cmd_checks_vm(desired_angle, steer_req, HYUNDAI_CANFD_ANGLE_LIMITS, + HYUNDAI_CANFD_ANGLE_MODELS[hyundai_canfd_angle_model])) { + tx = false; + } } } @@ -173,6 +966,26 @@ static bool hyundai_canfd_tx_hook(const CANPacket_t *msg) { tx = false; } } + if (msg->addr == 0x1aaU) { + bool allowed = hyundai_canfd_carnival_alt_resume && controls_allowed && !safety_rx_checks_invalid && + hyundai_canfd_carnival_source_valid && + safety_get_ts_elapsed(microsecond_timer_get(), hyundai_canfd_carnival_source_ts) <= 100000U && + (msg->data[2] == (uint8_t)(hyundai_canfd_carnival_source[2] + 1U)) && + ((msg->data[3] & 0x30U) == 0x10U) && ((msg->data[4] & 0x70U) == 0x10U) && + (hyundai_canfd_get_checksum(msg) == (hyundai_common_canfd_compute_checksum(msg) ^ 0x041DU)); + for (int i = 3; i < 16; i++) { + const uint8_t changed_bits = (i == 3) ? 0x30U : ((i == 4) ? 0x70U : 0U); + const uint8_t stable_bits = (uint8_t)~changed_bits; + allowed &= ((msg->data[i] & stable_bits) == (hyundai_canfd_carnival_source[i] & stable_bits)); + } + if (!allowed) { + tx = false; + } else { + hyundai_canfd_carnival_consumed_counter = hyundai_canfd_carnival_source[2]; + hyundai_canfd_carnival_counter_consumed = true; + hyundai_canfd_carnival_source_valid = false; + } + } // UDS: only tester present ("\x02\x3E\x80\x00\x00\x00\x00\x00") allowed on diagnostics address if (((msg->addr == 0x730U) && hyundai_canfd_lka_steer_msg) || ((msg->addr == 0x7D0U) && !hyundai_camera_scc)) { @@ -189,8 +1002,15 @@ static bool hyundai_canfd_tx_hook(const CANPacket_t *msg) { bool violation = false; if (hyundai_longitudinal) { - violation |= longitudinal_accel_checks(desired_accel_raw, HYUNDAI_LONG_LIMITS); - violation |= longitudinal_accel_checks(desired_accel_val, HYUNDAI_LONG_LIMITS); + // Exact EV9's reached PID ceiling is 2.2 m/s2. Other vehicles retain + // their existing 2.0 m/s2 maximum; this is narrower than original EV9 safety. + const LongitudinalLimits ev9_long_limits = {.max_accel = 220, .min_accel = -350}; + const LongitudinalLimits limits = hyundai_canfd_ev9_long ? ev9_long_limits : HYUNDAI_LONG_LIMITS; + if (hyundai_canfd_ev9_long && ((desired_accel_raw != 0) || (desired_accel_val != 0))) { + violation |= !aol_rx_healthy(); + } + violation |= longitudinal_accel_checks(desired_accel_raw, limits); + violation |= longitudinal_accel_checks(desired_accel_val, limits); } else { // only used to cancel on here const int acc_mode = (msg->data[8] >> 4) & 0x7U; @@ -208,20 +1028,275 @@ static bool hyundai_canfd_tx_hook(const CANPacket_t *msg) { } } + if (hyundai_canfd_ev9_long) { + // No reached EV9 LONG caller emits 110; its steering owner is direct CB. + tx &= msg->addr != 0x110U; + if (msg->addr == 0x362U) { + tx &= controls_allowed && aol_rx_healthy() && vehicle_moving && hyundai_ordinary_angle_drive && + !hyundai_ordinary_angle_eps_fault && !brake_pressed && !gas_pressed; + } + if ((msg->addr != 0x730U) && (msg->addr != 0x1CFU)) { + const unsigned int len = GET_LEN(msg); + const uint32_t xor_out = (len == 16U) ? (uint32_t)0x041DU : ((len == 8U) ? (uint32_t)0x5F29U : (uint32_t)0U); + tx &= hyundai_canfd_get_checksum(msg) == (hyundai_common_canfd_compute_checksum(msg) ^ xor_out); + } + if (msg->addr == 0x100U) { + static const uint8_t ev9_heartbeat[24] = { + 0U, 0U, 0U, 0U, 0xFFU, 0U, 0x6FU, 0U, 0xE8U, 4U, 0U, 0U, + 0x12U, 1U, 3U, 0U, 0x55U, 0xFFU, 0xFFU, 0U, 0U, 0U, 0U, 0U, + }; + for (int i = 3; i < 24; i++) { + uint8_t expected = ev9_heartbeat[i]; + if (i == 4) { + expected = (uint8_t)((expected & 0xFEU) | (brake_pressed ? 1U : 0U)); + } else if (i == 22) { + expected = (uint8_t)((expected & 0xFEU) | (gas_pressed ? 1U : 0U)); + } else { + // The original caller changes only counter and physical pedal bits. + } + tx &= msg->data[i] == expected; + } + } + if (tx && (msg->addr == 0x1A0U)) { + const unsigned int raw_unsigned = (((unsigned int)msg->data[17] & 0x7U) << 8U) | (unsigned int)msg->data[16]; + const int raw = (int)raw_unsigned - 1023; + const unsigned int val_unsigned = ((unsigned int)msg->data[18] << 4U) | ((unsigned int)msg->data[17] >> 4U); + const int val = (int)val_unsigned - 1023; + const bool inactive = (((msg->data[8] >> 4U) & 0x7U) == 0U) && (raw == 0) && (val == 0); + hyundai_ev9_inactive_accel_count = inactive ? SAFETY_MIN(hyundai_ev9_inactive_accel_count + 1U, 10U) : 0U; + if (hyundai_ev9_inactive_accel_count >= 10U) { + controls_allowed = false; + } + } + } return tx; } -static safety_config hyundai_canfd_init(uint16_t param) { +static bool hyundai_canfd_tx_hook(const CANPacket_t *msg) { + bool tx = false; + const bool ordinary_angle_frame = hyundai_ordinary_angle_stock && ((msg->addr == 0x110U) || (msg->addr == 0x362U)); + bool ordinary_angle_valid = true; + const bool independent_command = hyundai_angle_aol_enabled && + ((msg->addr == 0x50U) || (msg->addr == 0x110U) || + ((msg->addr == 0x12AU) && !hyundai_canfd_angle_observed_adas) || (msg->addr == 0xCBU)); + bool independent_active = false; + if (independent_command) { + independent_active = (msg->addr == 0xCBU) ? (((msg->data[3] >> 4U) & 0xFU) == 2U) : (((msg->data[9] >> 4U) & 3U) == 2U); + if (independent_active) { + const unsigned int len = GET_LEN(msg); + const uint32_t xor_out = (len == 16U) ? 0x041DU : 0U; + ordinary_angle_valid = (hyundai_angle_aol_permission_mask() != 0U) && + (hyundai_canfd_get_checksum(msg) == (hyundai_common_canfd_compute_checksum(msg) ^ xor_out)); + if (hyundai_ordinary_angle_counter_seen) { + const uint8_t delta = (uint8_t)(msg->data[2] - hyundai_ordinary_angle_counter); + ordinary_angle_valid &= (delta > 0U) && (delta <= 127U); + } + } else { + hyundai_ordinary_angle_release(); + } + } + if (hyundai_ordinary_angle_stock && !hyundai_angle_aol_enabled) { + // Core whitelisting precedes this hook; relay is its only final denial. + ordinary_angle_valid = !relay_malfunction; + if (ordinary_angle_frame) { + // Recheck accepted-TX freshness before continuity, including a new 110. + // An expired acquisition releases its counter and measured-angle baseline. + const bool ordinary_angle_owner = hyundai_ordinary_angle_owner_current(); + ordinary_angle_valid &= (hyundai_ordinary_angle_permission_mask() != 0U) && (msg->bus == 0U) && (GET_LEN(msg) == 32U); + if (msg->addr == 0x110U) { + ordinary_angle_valid &= ((msg->data[9] >> 4U) & 0x3U) == 2U; + } + if (ordinary_angle_valid) { + ordinary_angle_valid = hyundai_canfd_get_checksum(msg) == hyundai_common_canfd_compute_checksum(msg); + } + if ((msg->addr == 0x110U) && ordinary_angle_valid && hyundai_ordinary_angle_counter_seen) { + const uint8_t delta = (uint8_t)(msg->data[2] - hyundai_ordinary_angle_counter); + ordinary_angle_valid = (delta > 0U) && (delta <= 127U); + } + if (msg->addr == 0x362U) { + ordinary_angle_valid &= ordinary_angle_owner; + } + } + } + if (ordinary_angle_valid && !((hyundai_canfd_ioniq6_long || hyundai_canfd_ev9_long) && safety_rx_checks_invalid)) { + tx = hyundai_canfd_tx_hook_valid(msg); + } + if ((hyundai_ordinary_angle_stock && !hyundai_angle_aol_enabled && (msg->addr == 0x110U) && tx) || + (independent_command && independent_active && tx)) { + // Commit only after every shape, CRC, counter and actuation guard accepts. + hyundai_ordinary_angle_owned = true; + hyundai_ordinary_angle_accepted_ts = microsecond_timer_get(); + hyundai_ordinary_angle_counter_seen = true; + hyundai_ordinary_angle_counter = msg->data[2]; + } + if (hyundai_angle_aol_enabled && independent_command && !tx) { + hyundai_ordinary_angle_release(); + } + return tx; +} + +static void hyundai_canfd_reset_ioniq6_state(void) { + hyundai_canfd_ioniq6_long = false; + aol_ioniq6_long = false; + aol_ioniq6_enable_reset(); + aol_optional_release_capable = false; + aol_optional_release_policy = false; + aol_optional_release_neutral = false; + aol_optional_release_counter_seen = false; + aol_optional_release_previous = 0U; + aol_optional_release_pending = false; + aol_optional_release_latch = false; + + hyundai_canfd_torque_ev_long = false; + hyundai_canfd_ioniq6_lfa_unpaired = false; + hyundai_canfd_ioniq6_lfa_ts = 0U; + hyundai_canfd_ioniq6_heartbeat_seen = false; + hyundai_canfd_ioniq6_heartbeat_counter = 0U; + hyundai_canfd_ioniq6_corner_valid = false; + hyundai_canfd_ioniq6_corner_counter_seen = false; + hyundai_canfd_ioniq6_corner_counter = 0U; + hyundai_canfd_ioniq6_corner_bits = 0U; + hyundai_canfd_ioniq6_corner_ts = 0U; + hyundai_canfd_ioniq6_lamp_valid = false; + hyundai_canfd_ioniq6_left_lamp = false; + hyundai_canfd_ioniq6_right_lamp = false; + hyundai_canfd_ioniq6_lamp_ts = 0U; + hyundai_canfd_ioniq6_left_lamp_off_ts = 0U; + hyundai_canfd_ioniq6_right_lamp_off_ts = 0U; + hyundai_canfd_ioniq6_bsm_counter_seen = false; + hyundai_canfd_ioniq6_bsm_counter = 0U; + hyundai_canfd_ioniq6_bsm_pair_pending = false; + hyundai_canfd_ioniq6_bsm_pair_counter = 0U; + hyundai_canfd_ioniq6_bsm_pair_ts = 0U; +} + +static bool hyundai_canfd_angle_aol_capacity(safety_config *ret) { + const bool bounded = (ret->tx_msgs_len > 0) && (ret->tx_msgs_len <= 64); + if (!bounded) { + ret->rx_checks_len = 0; + ret->tx_msgs_len = 0; + } + return bounded; +} + +static safety_config hyundai_canfd_init_validated(uint16_t param) { + const uint16_t HYUNDAI_PARAM_LONGITUDINAL = 4U; const uint16_t HYUNDAI_PARAM_CANFD_LKA_STEER_MSG_ALT = 128; const uint16_t HYUNDAI_PARAM_CANFD_ALT_BUTTONS = 32; + const uint16_t HYUNDAI_PARAM_CARNIVAL_ALT_RESUME = 8192U; + const uint16_t HYUNDAI_PARAM_CANFD_ANGLE = 16384U; + const uint16_t HYUNDAI_PARAM_ANGLE_OBSERVED_ADAS = 512U; + const uint16_t HYUNDAI_PARAM_ANGLE_CCNC_MODEL_BANK = 1024U; + const uint16_t HYUNDAI_PARAM_ANGLE_MODEL_BANK = 8192U; + const uint16_t HYUNDAI_PARAM_ANGLE_MODEL_MASK = 2048U | 4096U | HYUNDAI_PARAM_ANGLE_MODEL_BANK; + const uint16_t HYUNDAI_PARAM_ANGLE_TOPOLOGY_MASK = 8U | 16U | 32U | 128U; + const bool angle_requested = GET_FLAG(param, HYUNDAI_PARAM_CANFD_ANGLE); + const bool ioniq6_stock_aol_requested = hyundai_canfd_stock_torque_aol_param(param); +#ifdef ALLOW_DEBUG + const bool torque_long_aol_requested = hyundai_canfd_torque_long_aol_param(param) && ((unsigned int)alternative_experience == 32U); +#else + const bool torque_long_aol_requested = false; +#endif + const bool angle_aol_requested = hyundai_canfd_angle_aol_param(param) && ((unsigned int)alternative_experience == 32U); + const bool pe_requested = (param == HYUNDAI_IONIQ5_PE_STOCK_PARAM) && (((unsigned int)alternative_experience == 0U) || angle_aol_requested); + const bool ev9_requested = (param == HYUNDAI_EV9_STOCK_PARAM) && (((unsigned int)alternative_experience == 0U) || angle_aol_requested); + const bool ordinary_angle_requested = pe_requested || ev9_requested; +#ifdef ALLOW_DEBUG + const bool ev9_long_requested = (param == HYUNDAI_EV9_LONG_PARAM) && ((unsigned int)alternative_experience == 0U); +#else + const bool ev9_long_requested = false; +#endif + const uint16_t angle_strip_mask = HYUNDAI_PARAM_CANFD_ANGLE | HYUNDAI_PARAM_ANGLE_CCNC_MODEL_BANK | HYUNDAI_PARAM_ANGLE_MODEL_MASK | + HYUNDAI_PARAM_ANGLE_OBSERVED_ADAS; +#ifdef ALLOW_DEBUG + const bool ioniq6_long_requested = GET_FLAG(param, 32768U); +#endif + // Model selector bits are shared with ordinary-mode metadata, but never passed + // to common init as those meanings when the angle discriminator is present. +#ifdef ALLOW_DEBUG + uint16_t common_param = (ioniq6_stock_aol_requested || torque_long_aol_requested) ? (param & (uint16_t)~2048U) : param; + if (angle_requested) { + common_param = param & (uint16_t)~angle_strip_mask; + } else if (ioniq6_long_requested) { + const uint16_t ioniq6_strip_mask = 32768U | 2048U; + common_param = param & (uint16_t)~ioniq6_strip_mask; + } else { + // Ordinary safety parameters have no profile-specific bits to strip. + } +#else + const uint16_t common_param = angle_requested ? (uint16_t)(param & (uint16_t)~angle_strip_mask) : + (uint16_t)(ioniq6_stock_aol_requested ? (param & (uint16_t)~2048U) : param); +#endif static const CanMsg HYUNDAI_CANFD_LKA_STEER_MSG_TX_MSGS[] = { HYUNDAI_CANFD_LKA_STEER_MSG_COMMON_TX_MSGS(0, 1) }; + static const CanMsg HYUNDAI_CANFD_LKA_STEER_MSG_CARNIVAL_TX_MSGS[] = { + HYUNDAI_CANFD_LKA_STEER_MSG_COMMON_TX_MSGS(0, 1) + {0x1AA, 1, 16, .check_relay = false}, + }; static const CanMsg HYUNDAI_CANFD_LKA_STEER_MSG_ALT_TX_MSGS[] = { HYUNDAI_CANFD_LKA_STEER_MSG_ALT_COMMON_TX_MSGS(0, 1) }; + static const CanMsg HYUNDAI_CANFD_ANGLE_LFA_TX_MSGS[] = { + HYUNDAI_CANFD_CRUISE_BUTTON_TX_MSGS(2) + HYUNDAI_CANFD_LFA_STEERING_COMMON_TX_MSGS(0) + HYUNDAI_CANFD_SCC_CONTROL_COMMON_TX_MSGS(0, false) + {0x160, 0, 16, .check_relay = false}, // stock camera warning + }; + static const CanMsg HYUNDAI_CANFD_ANGLE_LFA_ADAS_TX_MSGS[] = { + HYUNDAI_CANFD_CRUISE_BUTTON_TX_MSGS(2) + HYUNDAI_CANFD_LFA_STEERING_COMMON_TX_MSGS(0) + HYUNDAI_CANFD_SCC_CONTROL_COMMON_TX_MSGS(0, false) + {0xCB, 0, 24, .check_relay = true}, // ADAS_CMD angle control (current DBC: LFA_ALT) + {0x160, 0, 16, .check_relay = false}, // stock camera warning + }; + static const CanMsg HYUNDAI_CANFD_ANGLE_LFA_ALT_BUTTONS_TX_MSGS[] = { + HYUNDAI_CANFD_LFA_STEERING_COMMON_TX_MSGS(0) + HYUNDAI_CANFD_SCC_CONTROL_COMMON_TX_MSGS(0, false) + {0x160, 0, 16, .check_relay = false}, + }; + static const CanMsg HYUNDAI_CANFD_ANGLE_LFA_ALT_BUTTONS_ADAS_TX_MSGS[] = { + HYUNDAI_CANFD_LFA_STEERING_COMMON_TX_MSGS(0) + HYUNDAI_CANFD_SCC_CONTROL_COMMON_TX_MSGS(0, false) + {0xCB, 0, 24, .check_relay = true}, + {0x160, 0, 16, .check_relay = false}, + }; + // Stock-SCC CCNC angle SUVs re-emit only the paired camera display sample. + // These lists deliberately omit legacy 0x160, diagnostic and MDPS traffic. + static const CanMsg HYUNDAI_CANFD_ANGLE_CCNC_LFA_TX_MSGS[] = { + HYUNDAI_CANFD_CRUISE_BUTTON_TX_MSGS(2) + HYUNDAI_CANFD_LFA_STEERING_COMMON_TX_MSGS(0) + HYUNDAI_CANFD_SCC_CONTROL_COMMON_TX_MSGS(0, false) + {0x161, 0, 32, .check_relay = true}, + {0x162, 0, 32, .check_relay = true}, + }; + static const CanMsg HYUNDAI_CANFD_ANGLE_CCNC_LFA_ADAS_TX_MSGS[] = { + HYUNDAI_CANFD_CRUISE_BUTTON_TX_MSGS(2) + HYUNDAI_CANFD_LFA_STEERING_COMMON_TX_MSGS(0) + HYUNDAI_CANFD_SCC_CONTROL_COMMON_TX_MSGS(0, false) + {0xCB, 0, 24, .check_relay = true}, + {0x161, 0, 32, .check_relay = true}, + {0x162, 0, 32, .check_relay = true}, + }; + static const CanMsg HYUNDAI_CANFD_ANGLE_CCNC_LFA_ALT_BUTTONS_TX_MSGS[] = { + HYUNDAI_CANFD_LFA_STEERING_COMMON_TX_MSGS(0) + HYUNDAI_CANFD_SCC_CONTROL_COMMON_TX_MSGS(0, false) + {0x161, 0, 32, .check_relay = true}, + {0x162, 0, 32, .check_relay = true}, + }; + static const CanMsg HYUNDAI_CANFD_ANGLE_CCNC_LFA_ALT_BUTTONS_ADAS_TX_MSGS[] = { + HYUNDAI_CANFD_LFA_STEERING_COMMON_TX_MSGS(0) + HYUNDAI_CANFD_SCC_CONTROL_COMMON_TX_MSGS(0, false) + {0xCB, 0, 24, .check_relay = true}, + {0x161, 0, 32, .check_relay = true}, + {0x162, 0, 32, .check_relay = true}, + }; + static const CanMsg HYUNDAI_CANFD_LKA_STEER_MSG_ALT_CARNIVAL_TX_MSGS[] = { + HYUNDAI_CANFD_LKA_STEER_MSG_ALT_COMMON_TX_MSGS(0, 1) + {0x1AA, 1, 16, .check_relay = false}, + }; static const CanMsg HYUNDAI_CANFD_LKA_STEER_MSG_LONG_TX_MSGS[] = { HYUNDAI_CANFD_LKA_STEER_MSG_COMMON_TX_MSGS(0, 1) @@ -235,6 +1310,18 @@ static safety_config hyundai_canfd_init(uint16_t param) { {0x345, 1, 8, .check_relay = false}, // ADRV_0x345 {0x1DA, 1, 32, .check_relay = false}, // ADRV_0x1da }; + static const CanMsg HYUNDAI_CANFD_LKA_STEER_MSG_ALT_LONG_TX_MSGS[] = { + HYUNDAI_CANFD_LKA_STEER_MSG_ALT_COMMON_TX_MSGS(0, 1) + HYUNDAI_CANFD_LFA_STEERING_COMMON_TX_MSGS(1) + HYUNDAI_CANFD_SCC_CONTROL_COMMON_TX_MSGS(1, true) + {0x51, 0, 32, .check_relay = false}, // ADRV_0x51 + {0x730, 1, 8, .check_relay = false}, // tester present for ADAS ECU disable + {0x160, 1, 16, .check_relay = false}, // ADRV_0x160 + {0x1EA, 1, 32, .check_relay = false}, // ADRV_0x1ea + {0x200, 1, 8, .check_relay = false}, // ADRV_0x200 + {0x345, 1, 8, .check_relay = false}, // ADRV_0x345 + {0x1DA, 1, 32, .check_relay = false}, // ADRV_0x1da + }; static const CanMsg HYUNDAI_CANFD_LFA_STEERING_TX_MSGS[] = { HYUNDAI_CANFD_CRUISE_BUTTON_TX_MSGS(2) @@ -258,13 +1345,252 @@ static safety_config hyundai_canfd_init(uint16_t param) { HYUNDAI_CANFD_SCC_CONTROL_COMMON_TX_MSGS(0, (longitudinal)) \ {0x160, 0, 16, .check_relay = (longitudinal)}, /* ADRV_0x160 */ \ - hyundai_common_init(param); +// Ordinary CCNC without an ADAS steering ECU substitutes its two stock camera +// display frames for the older 0x160 warning frame. No camera/MDPS support +// traffic or LKA/angle-steering actuation is added by this topology. +#define HYUNDAI_CANFD_CCNC_CAMERA_SCC_TX_MSGS(longitudinal) \ + HYUNDAI_CANFD_CRUISE_BUTTON_TX_MSGS(2) \ + HYUNDAI_CANFD_LFA_STEERING_COMMON_TX_MSGS(0) \ + HYUNDAI_CANFD_SCC_CONTROL_COMMON_TX_MSGS(0, (longitudinal)) \ + {0x161, 0, 32, .check_relay = true}, /* CCNC_0x161 */ \ + {0x162, 0, 32, .check_relay = true}, /* CCNC_0x162 */ \ + + hyundai_common_init(common_param); gen_crc_lookup_table_16(0x1021, hyundai_canfd_crc_lut); hyundai_canfd_alt_buttons = GET_FLAG(param, HYUNDAI_PARAM_CANFD_ALT_BUTTONS); + hyundai_canfd_carnival_alt_resume = !angle_requested && GET_FLAG(common_param, HYUNDAI_PARAM_CARNIVAL_ALT_RESUME) && + hyundai_canfd_alt_buttons && !GET_FLAG(common_param, HYUNDAI_PARAM_LONGITUDINAL) && !hyundai_longitudinal; + hyundai_canfd_carnival_source_valid = false; + hyundai_canfd_carnival_counter_consumed = false; + hyundai_canfd_carnival_source_ts = 0U; hyundai_canfd_lka_steer_msg_alt = GET_FLAG(param, HYUNDAI_PARAM_CANFD_LKA_STEER_MSG_ALT); + hyundai_canfd_angle_steering = false; + hyundai_canfd_angle_observed_adas = false; + hyundai_canfd_angle_model = 0U; +#ifdef ALLOW_DEBUG + hyundai_canfd_ioniq6_long = ioniq6_long_requested; + hyundai_canfd_torque_ev_long = (param == 0x0015U) || (param == 0x0095U) || torque_long_aol_requested; +#endif + hyundai_torque_long_aol_cancel_preserves_lateral = torque_long_aol_requested; + aol_optional_release_capable = torque_long_aol_requested; + aol_ioniq6_long = ioniq6_stock_aol_requested || torque_long_aol_requested || (param == 0x8815U) || (param == 0x8895U); + if (aol_ioniq6_long) { + static const AolSafetyPolicy aol_ioniq6_policy = { + .reset = aol_ioniq6_reset, + .host_request = aol_ioniq6_host_request, + .request_mask = aol_ioniq6_request_mask, + .permission_mask = aol_ioniq6_permission_mask, + .rx_invalid = aol_ioniq6_rx_invalid, + }; + aol_policy = &aol_ioniq6_policy; + } + const uint16_t HYUNDAI_PARAM_CCNC = 1024U; + const bool hyundai_canfd_ccnc = !angle_requested && GET_FLAG(param, HYUNDAI_PARAM_CCNC) && hyundai_camera_scc && !hyundai_canfd_lka_steer_msg; - safety_config ret; + safety_config ret = {0}; + if (angle_requested) { + // The unbanked seven models retain their exact 60 raw profiles. Angle-only + // bit 10 selects CCNC geometry 0/1; Sportage permits documented camera + // topology only. Observed 0xCB applies only to camera-LFA in either bank. + const uint16_t topology = param & HYUNDAI_PARAM_ANGLE_TOPOLOGY_MASK; + const bool topology_valid = (topology == 16U) || (topology == (16U | 128U)) || + (topology == 8U) || (topology == (8U | 32U)); + const uint16_t angle_allowed_mask = HYUNDAI_PARAM_CANFD_ANGLE | HYUNDAI_PARAM_ANGLE_CCNC_MODEL_BANK | HYUNDAI_PARAM_ANGLE_MODEL_MASK | + HYUNDAI_PARAM_ANGLE_OBSERVED_ADAS | + HYUNDAI_PARAM_ANGLE_TOPOLOGY_MASK | 1U | 2U; + const uint8_t model = (uint8_t)((param & HYUNDAI_PARAM_ANGLE_MODEL_MASK) >> 11U); + const bool ccnc_bank = GET_FLAG(param, HYUNDAI_PARAM_ANGLE_CCNC_MODEL_BANK); + const bool ev6_stock_profile = (param == 0x7809U) || (param == 0x7829U); + const bool model_ev = ordinary_angle_requested || (!ccnc_bank && ((model == 1U) || (model == 3U) || ev6_stock_profile)); + const bool model_hybrid_capable = (!ccnc_bank && (model >= 4U) && (model <= 6U)) || (ccnc_bank && (model == 0U)); + const bool ev_flag = GET_FLAG(param, 1U); + const bool hybrid_flag = GET_FLAG(param, 2U); + const bool observed_adas = GET_FLAG(param, HYUNDAI_PARAM_ANGLE_OBSERVED_ADAS); + const uint16_t invalid_angle_bits = (uint16_t)~angle_allowed_mask; + const bool invalid_ccnc_model = !ordinary_angle_requested && ((model > 1U) || ((model == 1U) && ((topology & 8U) == 0U))); + const bool invalid_model = (ccnc_bank && invalid_ccnc_model) || (!ccnc_bank && (model == 7U) && !ev6_stock_profile); + const bool valid_angle = topology_valid && ((param & invalid_angle_bits) == 0U) && !invalid_model && + (ev_flag == model_ev) && (!hybrid_flag || model_hybrid_capable) && + (!observed_adas || ((topology & 8U) != 0U)); + if (valid_angle || ev9_long_requested) { + hyundai_canfd_angle_steering = true; + hyundai_canfd_angle_observed_adas = observed_adas; + hyundai_canfd_angle_model = pe_requested ? 10U : (ev9_requested || ev9_long_requested) ? 11U : ccnc_bank ? (uint8_t)(7U + model) : (ev6_stock_profile ? 9U : model); + static RxCheck hyundai_canfd_angle_lka_rx_checks[] = { + HYUNDAI_CANFD_STD_BUTTONS_RX_CHECKS(1) + HYUNDAI_CANFD_SCC_ADDR_CHECK(1) + }; + static RxCheck hyundai_canfd_banked_lka_ice_rx_checks[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(1, 0x100) + HYUNDAI_CANFD_SCC_ADDR_CHECK(1) + }; + static RxCheck hyundai_canfd_banked_lka_hybrid_rx_checks[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(1, 0x105) + HYUNDAI_CANFD_SCC_ADDR_CHECK(1) + }; + static RxCheck hyundai_canfd_banked_lfa_ice_rx_checks[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(0, 0x100) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + }; + static RxCheck hyundai_canfd_banked_lfa_hybrid_rx_checks[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(0, 0x105) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + }; + static RxCheck hyundai_canfd_banked_lfa_alt_ice_rx_checks[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(0, 0x100) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + }; + static RxCheck hyundai_canfd_banked_lfa_alt_hybrid_rx_checks[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(0, 0x105) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + }; + if (topology == 16U) { + ret = BUILD_SAFETY_CFG(hyundai_canfd_angle_lka_rx_checks, HYUNDAI_CANFD_LKA_STEER_MSG_TX_MSGS); + } else if (topology == (16U | 128U)) { + ret = BUILD_SAFETY_CFG(hyundai_canfd_angle_lka_rx_checks, HYUNDAI_CANFD_LKA_STEER_MSG_ALT_TX_MSGS); + } else if (topology == 8U) { + static RxCheck hyundai_canfd_angle_lfa_rx_checks[] = { + HYUNDAI_CANFD_STD_BUTTONS_RX_CHECKS(0) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + }; + if (ccnc_bank) { + ret = observed_adas ? BUILD_SAFETY_CFG(hyundai_canfd_angle_lfa_rx_checks, HYUNDAI_CANFD_ANGLE_CCNC_LFA_ADAS_TX_MSGS) : + BUILD_SAFETY_CFG(hyundai_canfd_angle_lfa_rx_checks, HYUNDAI_CANFD_ANGLE_CCNC_LFA_TX_MSGS); + } else { + ret = observed_adas ? BUILD_SAFETY_CFG(hyundai_canfd_angle_lfa_rx_checks, HYUNDAI_CANFD_ANGLE_LFA_ADAS_TX_MSGS) : + BUILD_SAFETY_CFG(hyundai_canfd_angle_lfa_rx_checks, HYUNDAI_CANFD_ANGLE_LFA_TX_MSGS); + } + } else { + static RxCheck hyundai_canfd_angle_lfa_alt_rx_checks[] = { + HYUNDAI_CANFD_ALT_BUTTONS_RX_CHECKS(0) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + }; + if (ccnc_bank) { + ret = observed_adas ? BUILD_SAFETY_CFG(hyundai_canfd_angle_lfa_alt_rx_checks, HYUNDAI_CANFD_ANGLE_CCNC_LFA_ALT_BUTTONS_ADAS_TX_MSGS) : + BUILD_SAFETY_CFG(hyundai_canfd_angle_lfa_alt_rx_checks, HYUNDAI_CANFD_ANGLE_CCNC_LFA_ALT_BUTTONS_TX_MSGS); + } else { + ret = observed_adas ? BUILD_SAFETY_CFG(hyundai_canfd_angle_lfa_alt_rx_checks, HYUNDAI_CANFD_ANGLE_LFA_ALT_BUTTONS_ADAS_TX_MSGS) : + BUILD_SAFETY_CFG(hyundai_canfd_angle_lfa_alt_rx_checks, HYUNDAI_CANFD_ANGLE_LFA_ALT_BUTTONS_TX_MSGS); + } + } + if (!ordinary_angle_requested && (model_hybrid_capable || ccnc_bank)) { + if ((topology == 16U) || (topology == (16U | 128U))) { + if (hybrid_flag) { + SET_RX_CHECKS(hyundai_canfd_banked_lka_hybrid_rx_checks, ret); + } else { + SET_RX_CHECKS(hyundai_canfd_banked_lka_ice_rx_checks, ret); + } + } else if (topology == 8U) { + if (hybrid_flag) { + SET_RX_CHECKS(hyundai_canfd_banked_lfa_hybrid_rx_checks, ret); + } else { + SET_RX_CHECKS(hyundai_canfd_banked_lfa_ice_rx_checks, ret); + } + } else { + if (hybrid_flag) { + SET_RX_CHECKS(hyundai_canfd_banked_lfa_alt_hybrid_rx_checks, ret); + } else { + SET_RX_CHECKS(hyundai_canfd_banked_lfa_alt_ice_rx_checks, ret); + } + } + } + if (ev9_long_requested) { + static RxCheck ev9_long_rx_checks[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(1, 0x35) + }; + static const CanMsg ev9_long_tx_msgs[] = { + {0x110, 0, 32, .check_relay = true, .disable_static_blocking = true}, + {0x362, 0, 32, .check_relay = true, .disable_static_blocking = true}, + {0x1CF, 1, 8, .check_relay = false}, + {0xCB, 1, 24, .check_relay = false}, + {0x12A, 1, 16, .check_relay = false}, + {0x1E0, 1, 16, .check_relay = false}, + {0x1A0, 1, 32, .check_relay = true}, + {0x1BA, 1, 24, .check_relay = false}, + {0x1E5, 1, 16, .check_relay = false}, + {0x100, 0, 24, .check_relay = false}, + {0x730, 1, 8, .check_relay = false}, + {0x160, 1, 16, .check_relay = false}, + {0x161, 1, 32, .check_relay = false}, + {0x162, 1, 32, .check_relay = false}, + {0x1EA, 1, 32, .check_relay = false}, + {0x200, 1, 8, .check_relay = false}, + {0x345, 1, 8, .check_relay = false}, + {0x38C, 1, 32, .check_relay = false}, + {0x1DA, 1, 32, .check_relay = false}, + }; + ret = BUILD_SAFETY_CFG(ev9_long_rx_checks, ev9_long_tx_msgs); + hyundai_canfd_ev9_long = true; + } + if (ordinary_angle_requested) { + static RxCheck ordinary_angle_rx_checks[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(1, 0x35) + HYUNDAI_CANFD_SCC_ADDR_CHECK(1) + }; + static const CanMsg ordinary_angle_tx_msgs[] = { + {0x110, 0, 32, .check_relay = true, .disable_static_blocking = true}, + {0x362, 0, 32, .check_relay = true, .disable_static_blocking = true}, + {0x1CF, 1, 8, .check_relay = false}, + }; + ret = BUILD_SAFETY_CFG(ordinary_angle_rx_checks, ordinary_angle_tx_msgs); + hyundai_ordinary_angle_stock = true; + static const AolSafetyPolicy ordinary_angle_policy = { + .reset = hyundai_ordinary_angle_reset, + .host_request = hyundai_ordinary_angle_host_request, + .request_mask = hyundai_ordinary_angle_request_mask, + .permission_mask = hyundai_ordinary_angle_permission_mask, + .rx_invalid = hyundai_ordinary_angle_release, + }; + aol_policy = &ordinary_angle_policy; + } + } + } else +#ifdef ALLOW_DEBUG + if (hyundai_canfd_ioniq6_long) { + // Exact HDA-II EV source and standard-button topology. Stock SCC 0x1A0 + // must already be silent from the prepublication UDS transaction. + static RxCheck ioniq6_long_rx_checks[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(1, 0x35) + }; + static const CanMsg ioniq6_long_lkas_tx_msgs[] = { + {0x50, 0, 16, .check_relay = true}, + {0x2A4, 0, 24, .check_relay = true}, + {0x12A, 1, 16, .check_relay = false}, + {0x1E0, 1, 16, .check_relay = false}, + {0x1A0, 1, 32, .check_relay = true, .check_relay_immediately = true}, + {0x100, 0, 24, .check_relay = true, .check_relay_immediately = true}, + {0x1BA, 1, 24, .check_relay = true, .check_relay_immediately = true}, + {0x1E5, 1, 16, .check_relay = true, .check_relay_immediately = true}, + {0x51, 0, 32, .check_relay = false}, + {0x730, 1, 8, .check_relay = false}, + {0x160, 1, 16, .check_relay = false}, + {0x1EA, 1, 32, .check_relay = false}, + {0x200, 1, 8, .check_relay = false}, + {0x345, 1, 8, .check_relay = false}, + {0x1DA, 1, 32, .check_relay = false}, + }; + static const CanMsg ioniq6_long_lkas_alt_tx_msgs[] = { + {0x110, 0, 32, .check_relay = true, .disable_static_blocking = true}, + {0x362, 0, 32, .check_relay = true, .disable_static_blocking = true}, + {0x12A, 1, 16, .check_relay = false}, + {0x1E0, 1, 16, .check_relay = false}, + {0x1A0, 1, 32, .check_relay = true, .check_relay_immediately = true}, + {0x100, 0, 24, .check_relay = true, .check_relay_immediately = true}, + {0x1BA, 1, 24, .check_relay = true, .check_relay_immediately = true}, + {0x1E5, 1, 16, .check_relay = true, .check_relay_immediately = true}, + {0x51, 0, 32, .check_relay = false}, + {0x730, 1, 8, .check_relay = false}, + {0x160, 1, 16, .check_relay = false}, + {0x1EA, 1, 32, .check_relay = false}, + {0x200, 1, 8, .check_relay = false}, + {0x345, 1, 8, .check_relay = false}, + {0x1DA, 1, 32, .check_relay = false}, + }; + ret = ((param == 0x8095U) || (param == 0x8895U)) ? BUILD_SAFETY_CFG(ioniq6_long_rx_checks, ioniq6_long_lkas_alt_tx_msgs) : + BUILD_SAFETY_CFG(ioniq6_long_rx_checks, ioniq6_long_lkas_tx_msgs); + } else +#endif if (hyundai_longitudinal) { if (hyundai_canfd_lka_steer_msg) { static RxCheck hyundai_canfd_lka_steer_msg_long_rx_checks[] = { @@ -272,6 +1598,9 @@ static safety_config hyundai_canfd_init(uint16_t param) { }; ret = BUILD_SAFETY_CFG(hyundai_canfd_lka_steer_msg_long_rx_checks, HYUNDAI_CANFD_LKA_STEER_MSG_LONG_TX_MSGS); + if (hyundai_canfd_lka_steer_msg_alt) { + SET_TX_MSGS(HYUNDAI_CANFD_LKA_STEER_MSG_ALT_LONG_TX_MSGS, ret); + } } else { // Longitudinal checks for LFA steering static RxCheck hyundai_canfd_long_rx_checks[] = { @@ -285,6 +1614,9 @@ static safety_config hyundai_canfd_init(uint16_t param) { static CanMsg hyundai_canfd_lfa_steering_camera_scc_tx_msgs[] = { HYUNDAI_CANFD_LFA_STEERING_CAMERA_SCC_TX_MSGS(true) }; + static CanMsg hyundai_canfd_ccnc_camera_scc_tx_msgs[] = { + HYUNDAI_CANFD_CCNC_CAMERA_SCC_TX_MSGS(true) + }; if (hyundai_canfd_alt_buttons) { SET_RX_CHECKS(hyundai_canfd_alt_buttons_long_rx_checks, ret); @@ -293,7 +1625,11 @@ static safety_config hyundai_canfd_init(uint16_t param) { } if (hyundai_camera_scc) { - SET_TX_MSGS(hyundai_canfd_lfa_steering_camera_scc_tx_msgs, ret); + if (hyundai_canfd_ccnc) { + SET_TX_MSGS(hyundai_canfd_ccnc_camera_scc_tx_msgs, ret); + } else { + SET_TX_MSGS(hyundai_canfd_lfa_steering_camera_scc_tx_msgs, ret); + } } else { SET_TX_MSGS(HYUNDAI_CANFD_LFA_STEERING_LONG_TX_MSGS, ret); } @@ -307,12 +1643,28 @@ static safety_config hyundai_canfd_init(uint16_t param) { HYUNDAI_CANFD_STD_BUTTONS_RX_CHECKS(1) HYUNDAI_CANFD_SCC_ADDR_CHECK(1) }; + static RxCheck hyundai_canfd_lka_steer_msg_carnival_rx_checks[] = { + HYUNDAI_CANFD_ALT_BUTTONS_RX_CHECKS(1) + HYUNDAI_CANFD_SCC_ADDR_CHECK(1) + }; - SET_RX_CHECKS(hyundai_canfd_lka_steer_msg_rx_checks, ret); - if (hyundai_canfd_lka_steer_msg_alt) { - SET_TX_MSGS(HYUNDAI_CANFD_LKA_STEER_MSG_ALT_TX_MSGS, ret); + if (hyundai_canfd_carnival_alt_resume) { + SET_RX_CHECKS(hyundai_canfd_lka_steer_msg_carnival_rx_checks, ret); } else { - SET_TX_MSGS(HYUNDAI_CANFD_LKA_STEER_MSG_TX_MSGS, ret); + SET_RX_CHECKS(hyundai_canfd_lka_steer_msg_rx_checks, ret); + } + if (hyundai_canfd_lka_steer_msg_alt) { + if (hyundai_canfd_carnival_alt_resume) { + SET_TX_MSGS(HYUNDAI_CANFD_LKA_STEER_MSG_ALT_CARNIVAL_TX_MSGS, ret); + } else { + SET_TX_MSGS(HYUNDAI_CANFD_LKA_STEER_MSG_ALT_TX_MSGS, ret); + } + } else { + if (hyundai_canfd_carnival_alt_resume) { + SET_TX_MSGS(HYUNDAI_CANFD_LKA_STEER_MSG_CARNIVAL_TX_MSGS, ret); + } else { + SET_TX_MSGS(HYUNDAI_CANFD_LKA_STEER_MSG_TX_MSGS, ret); + } } } else if (!hyundai_camera_scc) { // Radar sends SCC messages on these cars instead of camera @@ -350,8 +1702,23 @@ static safety_config hyundai_canfd_init(uint16_t param) { static CanMsg hyundai_canfd_lfa_steering_camera_scc_tx_msgs[] = { HYUNDAI_CANFD_LFA_STEERING_CAMERA_SCC_TX_MSGS(false) }; + static CanMsg hyundai_canfd_ccnc_camera_scc_tx_msgs[] = { + HYUNDAI_CANFD_CCNC_CAMERA_SCC_TX_MSGS(false) + }; + static CanMsg hyundai_canfd_ccnc_carnival_camera_scc_tx_msgs[] = { + HYUNDAI_CANFD_CCNC_CAMERA_SCC_TX_MSGS(false) + {0x1AA, 2, 16, .check_relay = false}, + }; - SET_TX_MSGS(hyundai_canfd_lfa_steering_camera_scc_tx_msgs, ret); + if (hyundai_canfd_ccnc) { + if (hyundai_canfd_carnival_alt_resume) { + SET_TX_MSGS(hyundai_canfd_ccnc_carnival_camera_scc_tx_msgs, ret); + } else { + SET_TX_MSGS(hyundai_canfd_ccnc_camera_scc_tx_msgs, ret); + } + } else { + SET_TX_MSGS(hyundai_canfd_lfa_steering_camera_scc_tx_msgs, ret); + } if (hyundai_canfd_alt_buttons) { SET_RX_CHECKS(hyundai_canfd_alt_buttons_rx_checks, ret); @@ -361,13 +1728,235 @@ static safety_config hyundai_canfd_init(uint16_t param) { } } + if (ioniq6_stock_aol_requested && (param != 0x0811U) && (param != 0x0891U)) { + RxCheck *selected = NULL; + if (hyundai_canfd_lka_steer_msg) { + if (hyundai_hybrid_gas_signal) { + static RxCheck torque_hda2_hybrid_alt[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(1, 0x105) + HYUNDAI_CANFD_SCC_ADDR_CHECK(1) + }; + static RxCheck torque_hda2_hybrid_std[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(1, 0x105) + HYUNDAI_CANFD_SCC_ADDR_CHECK(1) + }; + selected = hyundai_canfd_alt_buttons ? torque_hda2_hybrid_alt : torque_hda2_hybrid_std; + } else { + static RxCheck torque_hda2_ice_alt[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(1, 0x100) + HYUNDAI_CANFD_SCC_ADDR_CHECK(1) + }; + static RxCheck torque_hda2_ice_std[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(1, 0x100) + HYUNDAI_CANFD_SCC_ADDR_CHECK(1) + }; + selected = hyundai_canfd_alt_buttons ? torque_hda2_ice_alt : torque_hda2_ice_std; + } + } else if (hyundai_camera_scc) { + if (hyundai_ev_gas_signal) { + static RxCheck torque_camera_ev_alt[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(0, 0x35) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + }; + static RxCheck torque_camera_ev_std[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(0, 0x35) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + }; + selected = hyundai_canfd_alt_buttons ? torque_camera_ev_alt : torque_camera_ev_std; + } else if (hyundai_hybrid_gas_signal) { + static RxCheck torque_camera_hybrid_alt[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(0, 0x105) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + }; + static RxCheck torque_camera_hybrid_std[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(0, 0x105) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + }; + selected = hyundai_canfd_alt_buttons ? torque_camera_hybrid_alt : torque_camera_hybrid_std; + } else { + static RxCheck torque_camera_ice_alt[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(0, 0x100) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + }; + static RxCheck torque_camera_ice_std[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(0, 0x100) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + }; + selected = hyundai_canfd_alt_buttons ? torque_camera_ice_alt : torque_camera_ice_std; + } + } else { + if (hyundai_ev_gas_signal) { + static RxCheck torque_radar_ev_alt[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(0, 0x35) + HYUNDAI_CANFD_SCC_ADDR_CHECK(0) + }; + static RxCheck torque_radar_ev_std[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(0, 0x35) + HYUNDAI_CANFD_SCC_ADDR_CHECK(0) + }; + selected = hyundai_canfd_alt_buttons ? torque_radar_ev_alt : torque_radar_ev_std; + } else if (hyundai_hybrid_gas_signal) { + static RxCheck torque_radar_hybrid_alt[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(0, 0x105) + HYUNDAI_CANFD_SCC_ADDR_CHECK(0) + }; + static RxCheck torque_radar_hybrid_std[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(0, 0x105) + HYUNDAI_CANFD_SCC_ADDR_CHECK(0) + }; + selected = hyundai_canfd_alt_buttons ? torque_radar_hybrid_alt : torque_radar_hybrid_std; + } else { + static RxCheck torque_radar_ice_alt[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(0, 0x100) + HYUNDAI_CANFD_SCC_ADDR_CHECK(0) + }; + static RxCheck torque_radar_ice_std[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(0, 0x100) + HYUNDAI_CANFD_SCC_ADDR_CHECK(0) + }; + selected = hyundai_canfd_alt_buttons ? torque_radar_ice_alt : torque_radar_ice_std; + } + } + ret.rx_checks = selected; + ret.rx_checks_len = 6; + } + if (angle_aol_requested && hyundai_canfd_angle_steering && !hyundai_longitudinal) { + static CanMsg independent_tx_msgs[64]; + const bool bounded = hyundai_canfd_angle_aol_capacity(&ret); + if (bounded) { + RxCheck *selected = NULL; + if (hyundai_canfd_lka_steer_msg) { + if (hyundai_ev_gas_signal) { + static RxCheck angle_hda2_ev_alt[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(1, 0x35) + HYUNDAI_CANFD_SCC_ADDR_CHECK(1) + }; + static RxCheck angle_hda2_ev_std[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(1, 0x35) + HYUNDAI_CANFD_SCC_ADDR_CHECK(1) + }; + selected = hyundai_canfd_alt_buttons ? angle_hda2_ev_alt : angle_hda2_ev_std; + } + else if (hyundai_hybrid_gas_signal) { + static RxCheck angle_hda2_hybrid_alt[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(1, 0x105) + HYUNDAI_CANFD_SCC_ADDR_CHECK(1) + {.msg = {{0x70, 1, 32, 100U, .max_counter = 0xffU, .ignore_quality_flag = true}, {0}, {0}}}, + }; + static RxCheck angle_hda2_hybrid_std[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(1, 0x105) + HYUNDAI_CANFD_SCC_ADDR_CHECK(1) + {.msg = {{0x70, 1, 32, 100U, .max_counter = 0xffU, .ignore_quality_flag = true}, {0}, {0}}}, + }; + selected = hyundai_canfd_alt_buttons ? angle_hda2_hybrid_alt : angle_hda2_hybrid_std; + } + else { + static RxCheck angle_hda2_ice_alt[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(1, 0x100) + HYUNDAI_CANFD_SCC_ADDR_CHECK(1) + {.msg = {{0x70, 1, 32, 100U, .max_counter = 0xffU, .ignore_quality_flag = true}, {0}, {0}}}, + }; + static RxCheck angle_hda2_ice_std[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(1, 0x100) + HYUNDAI_CANFD_SCC_ADDR_CHECK(1) + {.msg = {{0x70, 1, 32, 100U, .max_counter = 0xffU, .ignore_quality_flag = true}, {0}, {0}}}, + }; + selected = hyundai_canfd_alt_buttons ? angle_hda2_ice_alt : angle_hda2_ice_std; + } + } + else { + if (hyundai_ev_gas_signal) { + static RxCheck angle_camera_ev_alt[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(0, 0x35) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + }; + static RxCheck angle_camera_ev_std[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(0, 0x35) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + }; + selected = hyundai_canfd_alt_buttons ? angle_camera_ev_alt : angle_camera_ev_std; + } + else if (hyundai_hybrid_gas_signal) { + static RxCheck angle_camera_hybrid_alt[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(0, 0x105) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + {.msg = {{0x70, 0, 32, 100U, .max_counter = 0xffU, .ignore_quality_flag = true}, {0}, {0}}}, + }; + static RxCheck angle_camera_hybrid_std[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(0, 0x105) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + {.msg = {{0x70, 0, 32, 100U, .max_counter = 0xffU, .ignore_quality_flag = true}, {0}, {0}}}, + }; + selected = hyundai_canfd_alt_buttons ? angle_camera_hybrid_alt : angle_camera_hybrid_std; + } + else { + static RxCheck angle_camera_ice_alt[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(0, 0x100) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + {.msg = {{0x70, 0, 32, 100U, .max_counter = 0xffU, .ignore_quality_flag = true}, {0}, {0}}}, + }; + static RxCheck angle_camera_ice_std[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(0, 0x100) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + {.msg = {{0x70, 0, 32, 100U, .max_counter = 0xffU, .ignore_quality_flag = true}, {0}, {0}}}, + }; + selected = hyundai_canfd_alt_buttons ? angle_camera_ice_alt : angle_camera_ice_std; + } + } + ret.rx_checks = selected; + ret.rx_checks_len = hyundai_ev_gas_signal ? 6 : 7; + for (int i = 0; i < ret.tx_msgs_len; i++) { + independent_tx_msgs[i] = ret.tx_msgs[i]; + const unsigned int addr = independent_tx_msgs[i].addr; + if ((addr == 0x50U) || (addr == 0x110U) || (addr == 0x12AU) || (addr == 0xCBU) || (addr == 0x2A4U) || (addr == 0x362U)) { + independent_tx_msgs[i].disable_static_blocking = true; + } + } + ret.tx_msgs = independent_tx_msgs; + hyundai_angle_aol_enabled = true; + hyundai_angle_aol_fault_mask = (pe_requested || ev9_requested) ? 7U : 2U; + aol_ioniq6_long = true; + static const AolSafetyPolicy independent_policy = { + .reset = aol_ioniq6_reset, + .host_request = aol_ioniq6_host_request, + .request_mask = aol_ioniq6_request_mask, + .permission_mask = hyundai_angle_aol_permission_mask, + .rx_invalid = aol_ioniq6_rx_invalid, + }; + aol_policy = &independent_policy; + } + } + return ret; +} + +static safety_config hyundai_canfd_init(uint16_t param) { + hyundai_canfd_reset_ioniq6_state(); + hyundai_ordinary_angle_reset(); +#ifdef ALLOW_DEBUG + const bool valid = !GET_FLAG(param, 32768U) || (param == 0x8015U) || (param == 0x8095U) || + (param == 0x8815U) || (param == 0x8895U); +#else + const bool valid = !GET_FLAG(param, 32768U); +#endif + safety_config ret = {0}; + const bool marker_valid = GET_FLAG(param, 16384U) || !GET_FLAG(param, 2048U) || + hyundai_canfd_stock_torque_aol_param(param) || +#ifdef ALLOW_DEBUG + (hyundai_canfd_torque_long_aol_param(param) && ((unsigned int)alternative_experience == 32U)) || +#endif + (param == 0x8815U) || (param == 0x8895U); + if (valid && marker_valid) { + ret = hyundai_canfd_init_validated(param); + } return ret; } const safety_hooks hyundai_canfd_hooks = { .init = hyundai_canfd_init, .rx = hyundai_canfd_rx_hook, + .optional_rx = hyundai_canfd_optional_rx_hook, .tx = hyundai_canfd_tx_hook, + .fwd = hyundai_canfd_fwd_hook, .get_counter = hyundai_canfd_get_counter, .get_checksum = hyundai_canfd_get_checksum, .compute_checksum = hyundai_common_canfd_compute_checksum, diff --git a/opendbc_repo/opendbc/safety/modes/hyundai_canfd_angle_aol.h b/opendbc_repo/opendbc/safety/modes/hyundai_canfd_angle_aol.h new file mode 100644 index 0000000000..d67dc2af4d --- /dev/null +++ b/opendbc_repo/opendbc/safety/modes/hyundai_canfd_angle_aol.h @@ -0,0 +1,92 @@ +#pragma once + +static inline bool hyundai_canfd_angle_aol_param(uint16_t param) { + bool result = false; + switch (param) { + case 0x4008U: + case 0x4010U: + case 0x4028U: + case 0x4090U: + case 0x4208U: + case 0x4228U: + case 0x4408U: + case 0x440AU: + case 0x4410U: + case 0x4412U: + case 0x4428U: + case 0x442AU: + case 0x4490U: + case 0x4492U: + case 0x4608U: + case 0x460AU: + case 0x4628U: + case 0x462AU: + case 0x4809U: + case 0x4811U: + case 0x4829U: + case 0x4891U: + case 0x4A09U: + case 0x4A29U: + case 0x4C08U: + case 0x4C28U: + case 0x4E08U: + case 0x4E28U: + case 0x5008U: + case 0x5010U: + case 0x5028U: + case 0x5090U: + case 0x5208U: + case 0x5228U: + case 0x5491U: + case 0x5809U: + case 0x5811U: + case 0x5829U: + case 0x5891U: + case 0x5A09U: + case 0x5A29U: + case 0x5C91U: + case 0x6008U: + case 0x600AU: + case 0x6010U: + case 0x6012U: + case 0x6028U: + case 0x602AU: + case 0x6090U: + case 0x6092U: + case 0x6208U: + case 0x620AU: + case 0x6228U: + case 0x622AU: + case 0x6808U: + case 0x680AU: + case 0x6810U: + case 0x6812U: + case 0x6828U: + case 0x682AU: + case 0x6890U: + case 0x6892U: + case 0x6A08U: + case 0x6A0AU: + case 0x6A28U: + case 0x6A2AU: + case 0x7008U: + case 0x700AU: + case 0x7010U: + case 0x7012U: + case 0x7028U: + case 0x702AU: + case 0x7090U: + case 0x7092U: + case 0x7208U: + case 0x720AU: + case 0x7228U: + case 0x722AU: + case 0x7809U: + case 0x7829U: + result = true; + break; + default: + break; + } + return result; +} diff --git a/opendbc_repo/opendbc/safety/modes/hyundai_canfd_aol.h b/opendbc_repo/opendbc/safety/modes/hyundai_canfd_aol.h new file mode 100644 index 0000000000..bc76a08123 --- /dev/null +++ b/opendbc_repo/opendbc/safety/modes/hyundai_canfd_aol.h @@ -0,0 +1,23 @@ +#pragma once + +// Bit 11 selects the shared axis lease only for these ordinary torque layouts. +// Angle model selectors and the separate Ioniq 6 actuator profile stay separate. +static inline bool hyundai_canfd_stock_torque_aol_param(uint16_t param) { + const uint16_t base = param & (uint16_t)~0x0800U; + const uint16_t gas = base & 3U; + const uint16_t topology = base & 0x00B8U; + const bool carnival = (base & 0x2000U) != 0U; + const bool ccnc = (base & 0x0400U) != 0U; + const bool standard = (topology == 0U) || (topology == 8U) || (topology == 16U) || (topology == 144U); + const bool alternate = (topology == 32U) || (topology == 40U) || + (carnival && ((topology == 48U) || (topology == 176U))); + return ((param & 0x0800U) != 0U) && ((base & (uint16_t)~0x24BBU) == 0U) && (gas != 3U) && + (standard || alternate) && (!ccnc || ((topology == 8U) || (topology == 40U))) && + (!carnival || (alternate && (gas != 1U))); +} + +#if defined(ALLOW_DEBUG) || defined(__cplusplus) +static inline bool hyundai_canfd_torque_long_aol_param(uint16_t param) { + return (param == 0x0815U) || (param == 0x0895U); +} +#endif diff --git a/opendbc_repo/opendbc/safety/modes/hyundai_classic_non_scc_aol.h b/opendbc_repo/opendbc/safety/modes/hyundai_classic_non_scc_aol.h new file mode 100644 index 0000000000..bb4e46c202 --- /dev/null +++ b/opendbc_repo/opendbc/safety/modes/hyundai_classic_non_scc_aol.h @@ -0,0 +1,263 @@ +#pragma once + +// Shared classic physical authorization. SCC and NON_SCC profiles remain distinct. +// Never used by CANFD mode. +#define HYUNDAI_FORTE_AOL_MARKER 0x0400U +#define HYUNDAI_FORTE_AOL_MAIN (0x1000U | HYUNDAI_FORTE_AOL_MARKER) +#define HYUNDAI_FORTE_AOL_LDA (0x1800U | HYUNDAI_FORTE_AOL_MARKER) +#define HYUNDAI_KONA_AOL_MAIN 0x1440U +#define HYUNDAI_KONA_AOL_LDA 0x1C40U +#define HYUNDAI_NON_SCC_HEV_AOL_MAIN 0x1402U +#define HYUNDAI_NON_SCC_HEV_AOL_LDA 0x1C02U +#define HYUNDAI_NON_SCC_EV_ALT_AOL_MAIN 0x1441U +#define HYUNDAI_NON_SCC_EV_ALT_AOL_LDA 0x1C41U +#define HYUNDAI_CLASSIC_SOURCE_MAX_AGE_US 300000U + +static bool classic_scc_aol_enabled = false; +static bool classic_scc_aol_camera = false; +static bool classic_long_aol_enabled = false; +static bool classic_long_main_neutral = false; +static bool classic_long_main_pressed = false; + +static bool legacy_long_param(uint16_t param) { + return (param == 0xE900U) || (param == 0xE901U) || (param == 0xE902U) || (param == 0xE903U) || + (param == 0xE910U) || (param == 0xE911U) || (param == 0xE912U) || (param == 0xE913U); +} + +static bool legacy_long_aol_param(uint16_t param) { + return (param == 0xE902U) || (param == 0xE903U) || (param == 0xE912U) || (param == 0xE913U); +} + +static bool classic_long_aol_param(uint16_t param) { + return ((param & 0x0404U) == 0x0404U) && ((param & 0xF3B8U) == 0U) && ((param & 3U) != 3U); +} + +static bool classic_scc_aol_param(uint16_t param) { + // Stock SCC gas, torque limits and camera routing only. No LONG or NON_SCC. + return (param == 0x0500U) || (param == 0x0D00U) || (param == 0x8408U) || (param == 0x840AU) || (param == 0x8C08U) || (param == 0x8C0AU) || + (((param & 0x0400U) != 0U) && ((param & 0xF1B4U) == 0U) && + ((param & 3U) != 3U) && ((param & 0x0240U) != 0x0240U)); +} + +static bool classic_non_scc_aol_enabled = false; +static bool classic_non_scc_aol_ev = false; +static bool classic_non_scc_aol_hev = false; +static bool classic_non_scc_aol_lda = false; +static bool classic_non_scc_aol_main = false; +static bool classic_non_scc_aol_main_seen = false; +static bool classic_non_scc_aol_source_seen[2] = {false, false}; +static bool classic_non_scc_aol_source_neutral[2] = {false, false}; +static bool classic_non_scc_aol_source_pressed[2] = {false, false}; +static uint32_t classic_non_scc_aol_source_ts[2] = {0U, 0U}; +static bool classic_non_scc_aol_neutral_seen = false; +static bool classic_non_scc_aol_token = false; +static bool classic_non_scc_aol_claimed = false; +static uint32_t classic_non_scc_aol_token_ts = 0U; +static bool classic_non_scc_aol_session = false; +static bool classic_non_scc_aol_request_seen = false; +static uint32_t classic_non_scc_aol_request_ts = 0U; + +static void classic_non_scc_aol_clear_authorization(void) { + classic_non_scc_aol_main = false; + classic_non_scc_aol_main_seen = false; + classic_non_scc_aol_neutral_seen = false; + classic_non_scc_aol_token = false; + classic_non_scc_aol_claimed = false; + classic_non_scc_aol_session = false; + classic_non_scc_aol_request_seen = false; + classic_non_scc_aol_request_ts = 0U; + for (unsigned int i = 0U; i < 2U; i++) { + classic_non_scc_aol_source_seen[i] = false; + classic_non_scc_aol_source_pressed[i] = false; + classic_non_scc_aol_source_neutral[i] = false; + classic_non_scc_aol_source_ts[i] = 0U; + } + classic_long_main_neutral = false; + classic_long_main_pressed = false; + aol_host_axis_mask = 0U; +} + +static void classic_non_scc_aol_reset(void) { + classic_non_scc_aol_clear_authorization(); + classic_non_scc_aol_enabled = false; + classic_scc_aol_enabled = false; + classic_scc_aol_camera = false; + classic_long_aol_enabled = false; + classic_non_scc_aol_ev = false; + classic_non_scc_aol_hev = false; + classic_non_scc_aol_lda = false; +} + +static void classic_non_scc_aol_host_request(uint8_t mask) { + const uint32_t now = microsecond_timer_get(); + if (classic_non_scc_aol_request_seen && + (safety_get_ts_elapsed(now, classic_non_scc_aol_request_ts) > AOL_HOST_REQUEST_TIMEOUT_US)) { + // A late fresh request cannot revive authorization from an expired lease. + classic_non_scc_aol_clear_authorization(); + } + classic_non_scc_aol_request_ts = now; + if (mask != 0U) { + classic_non_scc_aol_request_seen = true; + } +} + +static uint8_t classic_non_scc_aol_request_mask(void) { + uint8_t result = 0U; + const uint32_t now = microsecond_timer_get(); + const bool expired = classic_non_scc_aol_request_seen && + (safety_get_ts_elapsed(now, classic_non_scc_aol_request_ts) > AOL_HOST_REQUEST_TIMEOUT_US); + const bool token_expired = classic_non_scc_aol_token && !classic_non_scc_aol_claimed && + (safety_get_ts_elapsed(now, classic_non_scc_aol_token_ts) > AOL_HOST_REQUEST_TIMEOUT_US); + if (relay_malfunction || safety_rx_checks_invalid || expired || token_expired || + (classic_non_scc_aol_session && !heartbeat_engaged)) { + classic_non_scc_aol_clear_authorization(); + } else if (heartbeat_engaged) { + classic_non_scc_aol_session = true; + if (((classic_scc_aol_enabled && classic_non_scc_aol_lda) || classic_long_aol_enabled) && controls_allowed && + classic_non_scc_aol_main_seen && (!classic_long_aol_enabled || classic_non_scc_aol_main) && + aol_rx_healthy() && ((aol_host_axis_mask & 1U) != 0U)) { + // Actual stock ACC activation can seed the host's LKAS-on-engage latch. + classic_non_scc_aol_token = true; + classic_non_scc_aol_token_ts = now; + } + if (((aol_host_axis_mask & 1U) != 0U) && classic_non_scc_aol_token) { + classic_non_scc_aol_claimed = true; + } + result = aol_host_axis_mask & (classic_long_aol_enabled ? 3U : 1U); + } else { + // Physical evidence may precede heartbeat but grants no permission yet. + } + return result; +} + +static uint8_t classic_non_scc_aol_permission_mask(void) { + uint8_t result = 0U; + if (!aol_rx_healthy()) { + classic_non_scc_aol_clear_authorization(); + } else { + const uint8_t request = classic_non_scc_aol_request_mask(); + const bool physical_authorized = (classic_long_aol_enabled || (classic_scc_aol_enabled && classic_non_scc_aol_lda)) ? + (classic_non_scc_aol_token || controls_allowed) : + (classic_non_scc_aol_main || classic_non_scc_aol_token || controls_allowed); + result = (((request & 1U) != 0U) && classic_non_scc_aol_main_seen && physical_authorized && + (!classic_long_aol_enabled || classic_non_scc_aol_main)) ? 1U : 0U; + if (classic_long_aol_enabled && classic_non_scc_aol_main && controls_allowed && ((request & 2U) != 0U)) { + result |= 2U; + } + } + return result; +} + +static bool classic_non_scc_aol_combined_pressed(uint32_t now) { + bool pressed = false; + for (unsigned int i = 0U; i < 2U; i++) { + pressed |= classic_non_scc_aol_source_seen[i] && classic_non_scc_aol_source_pressed[i] && + (safety_get_ts_elapsed(now, classic_non_scc_aol_source_ts[i]) <= HYUNDAI_CLASSIC_SOURCE_MAX_AGE_US); + } + return pressed; +} + +static void classic_non_scc_aol_rx(const CANPacket_t *msg) { + if (classic_non_scc_aol_enabled && ((msg->bus == 0U) || (classic_scc_aol_enabled && classic_scc_aol_camera && (msg->bus == 2U)))) { + const uint32_t now = microsecond_timer_get(); + const uint8_t scc_bus = classic_scc_aol_camera ? 2U : 0U; + if (classic_long_aol_enabled && (msg->bus == 0U) && (msg->addr == 0x394U) && (GET_LEN(msg) == 8U)) { + classic_non_scc_aol_main = ((msg->data[5] >> 3U) & 3U) == 0U; + classic_non_scc_aol_main_seen = true; + if (!classic_non_scc_aol_main) { + classic_non_scc_aol_token = false; + classic_non_scc_aol_claimed = false; + } + } + if (classic_long_aol_enabled && (msg->bus == 0U) && (msg->addr == 0x4F1U) && (GET_LEN(msg) == 4U)) { + const bool main_pressed = GET_BIT(msg, 3U); + if (!main_pressed) { + classic_long_main_neutral = true; + } else if (classic_long_main_neutral && !classic_long_main_pressed && (aol_host_axis_mask == 0U)) { + classic_non_scc_aol_token = true; + classic_non_scc_aol_claimed = false; + classic_non_scc_aol_token_ts = now; + } else { + } + classic_long_main_pressed = main_pressed; + } + if (classic_scc_aol_enabled && (msg->bus == scc_bus) && (msg->addr == 0x420U) && (GET_LEN(msg) == 8U)) { + classic_non_scc_aol_main = GET_BIT(msg, 0U); + classic_non_scc_aol_main_seen = true; + } + if (!classic_scc_aol_enabled && !classic_long_aol_enabled && (GET_LEN(msg) == 8U) && + ((!classic_non_scc_aol_ev && !classic_non_scc_aol_hev && (msg->addr == 0x260U)) || + (classic_non_scc_aol_ev && (msg->addr == 0x592U)) || + (classic_non_scc_aol_hev && (msg->addr == 0x595U)))) { + classic_non_scc_aol_main = classic_non_scc_aol_ev ? GET_BIT(msg, 34U) : + (classic_non_scc_aol_hev ? GET_BIT(msg, 50U) : GET_BIT(msg, 25U)); + classic_non_scc_aol_main_seen = true; + } + if (classic_non_scc_aol_lda && (msg->bus == 0U) && (GET_LEN(msg) == 8U) && ((msg->addr == 0x391U) || (msg->addr == 0x50CU))) { + // Source loss clears neutral evidence without manufacturing a release. + for (unsigned int i = 0U; i < 2U; i++) { + if (classic_non_scc_aol_source_seen[i] && + (safety_get_ts_elapsed(now, classic_non_scc_aol_source_ts[i]) > HYUNDAI_CLASSIC_SOURCE_MAX_AGE_US)) { + classic_non_scc_aol_source_seen[i] = false; + classic_non_scc_aol_source_pressed[i] = false; + classic_non_scc_aol_source_neutral[i] = false; + classic_non_scc_aol_neutral_seen = false; + } + } + const unsigned int source = (msg->addr == 0x391U) ? 0U : 1U; + const bool previous = classic_non_scc_aol_combined_pressed(now); + const bool first = !classic_non_scc_aol_source_seen[source] || + (safety_get_ts_elapsed(now, classic_non_scc_aol_source_ts[source]) > HYUNDAI_CLASSIC_SOURCE_MAX_AGE_US); + if (first) { + classic_non_scc_aol_source_neutral[source] = false; + } + classic_non_scc_aol_source_seen[source] = true; + classic_non_scc_aol_source_pressed[source] = GET_BIT(msg, (source == 0U) ? 4U : 56U); + classic_non_scc_aol_source_ts[source] = now; + if (!classic_non_scc_aol_source_pressed[source]) { + classic_non_scc_aol_source_neutral[source] = true; + } + const bool pressed = classic_non_scc_aol_combined_pressed(now); + if (!pressed) { + classic_non_scc_aol_neutral_seen = true; + } else if (!first && classic_non_scc_aol_source_neutral[source] && !previous && classic_non_scc_aol_neutral_seen && (aol_host_axis_mask == 0U)) { + // Authorize the host's toggle; never invert a separate native latch. + classic_non_scc_aol_token = true; + classic_non_scc_aol_claimed = false; + classic_non_scc_aol_token_ts = now; + } else { + // Held-at-init and an alternative source's zero are not new gestures. + } + } + } +} + +static void classic_non_scc_aol_configure(uint16_t param, bool legacy) { + classic_non_scc_aol_reset(); + classic_scc_aol_enabled = classic_scc_aol_param(param) && (!legacy || ((param & (8U | 256U)) == 0U)) && + ((unsigned int)alternative_experience == 32U); +#ifdef ALLOW_DEBUG + classic_long_aol_enabled = !legacy && classic_long_aol_param(param) && ((unsigned int)alternative_experience == 32U); +#endif + classic_scc_aol_camera = classic_scc_aol_enabled && ((param & 8U) != 0U); + classic_non_scc_aol_enabled = ((!legacy && ((param == HYUNDAI_FORTE_AOL_MAIN) || (param == HYUNDAI_FORTE_AOL_LDA) || + (param == HYUNDAI_KONA_AOL_MAIN) || (param == HYUNDAI_KONA_AOL_LDA) || + (param == HYUNDAI_NON_SCC_HEV_AOL_MAIN) || (param == HYUNDAI_NON_SCC_HEV_AOL_LDA) || + (param == HYUNDAI_NON_SCC_EV_ALT_AOL_MAIN) || (param == HYUNDAI_NON_SCC_EV_ALT_AOL_LDA))) || classic_scc_aol_enabled || classic_long_aol_enabled) && + ((unsigned int)alternative_experience == 32U); + classic_non_scc_aol_lda = classic_non_scc_aol_enabled && (((classic_scc_aol_enabled || classic_long_aol_enabled) && ((param & 0x0800U) != 0U)) || (param == HYUNDAI_FORTE_AOL_LDA) || (param == HYUNDAI_KONA_AOL_LDA) || + (param == HYUNDAI_NON_SCC_HEV_AOL_LDA) || + (param == HYUNDAI_NON_SCC_EV_ALT_AOL_LDA)); + classic_non_scc_aol_ev = classic_non_scc_aol_enabled && ((param == HYUNDAI_NON_SCC_EV_ALT_AOL_MAIN) || (param == HYUNDAI_NON_SCC_EV_ALT_AOL_LDA)); + classic_non_scc_aol_hev = classic_non_scc_aol_enabled && ((param == HYUNDAI_NON_SCC_HEV_AOL_MAIN) || (param == HYUNDAI_NON_SCC_HEV_AOL_LDA)); + if (classic_non_scc_aol_enabled) { + static const AolSafetyPolicy policy = { + .reset = classic_non_scc_aol_reset, + .host_request = classic_non_scc_aol_host_request, + .request_mask = classic_non_scc_aol_request_mask, + .permission_mask = classic_non_scc_aol_permission_mask, + .rx_invalid = classic_non_scc_aol_clear_authorization, + }; + aol_policy = &policy; + } +} diff --git a/opendbc_repo/opendbc/safety/modes/hyundai_common.h b/opendbc_repo/opendbc/safety/modes/hyundai_common.h index 4638d0c886..1a30b7888a 100644 --- a/opendbc_repo/opendbc/safety/modes/hyundai_common.h +++ b/opendbc_repo/opendbc/safety/modes/hyundai_common.h @@ -29,6 +29,12 @@ bool hyundai_longitudinal = false; extern bool hyundai_camera_scc; bool hyundai_camera_scc = false; +extern bool hyundai_can_refresh_msgs; +bool hyundai_can_refresh_msgs = false; +extern bool hyundai_non_scc; +bool hyundai_non_scc = false; +extern bool hyundai_has_lda_button; +bool hyundai_has_lda_button = false; extern bool hyundai_canfd_lka_steer_msg; bool hyundai_canfd_lka_steer_msg = false; @@ -52,6 +58,9 @@ void hyundai_common_init(uint16_t param) { const uint16_t HYUNDAI_PARAM_ALT_LIMITS = 64; // TODO: shift this down with the rest of the common flags const uint16_t HYUNDAI_PARAM_FCEV_GAS = 256; const uint16_t HYUNDAI_PARAM_ALT_LIMITS_2 = 512; + const uint16_t HYUNDAI_PARAM_HAS_LDA_BUTTON = 2048; + const uint16_t HYUNDAI_PARAM_NON_SCC = 4096; + const uint16_t HYUNDAI_PARAM_CAN_REFRESH_MSGS = 32768U; hyundai_ev_gas_signal = GET_FLAG(param, HYUNDAI_PARAM_EV_GAS); hyundai_hybrid_gas_signal = !hyundai_ev_gas_signal && GET_FLAG(param, HYUNDAI_PARAM_HYBRID_GAS); @@ -60,12 +69,15 @@ void hyundai_common_init(uint16_t param) { hyundai_alt_limits = GET_FLAG(param, HYUNDAI_PARAM_ALT_LIMITS); hyundai_fcev_gas_signal = GET_FLAG(param, HYUNDAI_PARAM_FCEV_GAS); hyundai_alt_limits_2 = GET_FLAG(param, HYUNDAI_PARAM_ALT_LIMITS_2); + hyundai_has_lda_button = GET_FLAG(param, HYUNDAI_PARAM_HAS_LDA_BUTTON); + hyundai_non_scc = GET_FLAG(param, HYUNDAI_PARAM_NON_SCC); + hyundai_can_refresh_msgs = GET_FLAG(param, HYUNDAI_PARAM_CAN_REFRESH_MSGS); hyundai_last_button_interaction = HYUNDAI_PREV_BUTTON_SAMPLES; #ifdef ALLOW_DEBUG const uint16_t HYUNDAI_PARAM_LONGITUDINAL = 4; - hyundai_longitudinal = GET_FLAG(param, HYUNDAI_PARAM_LONGITUDINAL); + hyundai_longitudinal = GET_FLAG(param, HYUNDAI_PARAM_LONGITUDINAL) && !hyundai_non_scc; #else hyundai_longitudinal = false; #endif diff --git a/opendbc_repo/opendbc/safety/tests/libsafety/safety.c b/opendbc_repo/opendbc/safety/tests/libsafety/safety.c index d933612c2b..bbbd09c41a 100644 --- a/opendbc_repo/opendbc/safety/tests/libsafety/safety.c +++ b/opendbc_repo/opendbc/safety/tests/libsafety/safety.c @@ -80,8 +80,7 @@ bool safety_test_rx_health_fixture(unsigned int variant, bool canfd) { const safety_config original = current_safety_config; current_safety_config.rx_checks = (variant == 1U) ? NULL : ✓ current_safety_config.rx_checks_len = (variant == 0U) ? 0 : 1; - (void)canfd; - const bool healthy = aol_rx_healthy(); + const bool healthy = canfd ? hyundai_canfd_ioniq6_rx_healthy() : aol_rx_healthy(); current_safety_config = original; return healthy; } diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai.py b/opendbc_repo/opendbc/safety/tests/test_hyundai.py index c94d84887a..91014b6cb6 100755 --- a/opendbc_repo/opendbc/safety/tests/test_hyundai.py +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai.py @@ -4,6 +4,8 @@ import unittest from opendbc.car.hyundai.values import HyundaiSafetyFlags from opendbc.car.structs import CarParams +from opendbc.safety.tests.common import CANPackerSafety +from opendbc.safety.tests.libsafety import libsafety_py import opendbc.safety.tests.common as common from opendbc.safety.tests.hyundai_common import HyundaiButtonBase, HyundaiLongitudinalBase @@ -43,6 +45,232 @@ def checksum(msg): return addr, ret, bus +class TestHyundaiRefreshSafety(unittest.TestCase): + def setUp(self): + self.safety = libsafety_py.libsafety + self.packer = CANPackerSafety("hyundai_can_refresh_generated") + + def _set_mode(self, flags): + self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, flags) + self.safety.init_tests() + + def test_stock_camera_scc_refresh_length_and_ownership(self): + self._set_mode(HyundaiSafetyFlags.CAMERA_SCC | HyundaiSafetyFlags.CAN_REFRESH_MSGS) + lfa = self.packer.make_can_msg_safety("LFAHDA_MFC", 0, {"LFA_Icon_State": 2}) + self.assertTrue(self.safety.safety_tx_hook(lfa)) + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(0, 0x485, 4))) + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(1, 0x485, 8))) + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(0, 0x420, 8))) + + def test_long_camera_scc_refresh_allowlist(self): + self._set_mode(HyundaiSafetyFlags.CAMERA_SCC | HyundaiSafetyFlags.CAN_REFRESH_MSGS | HyundaiSafetyFlags.LONG) + self.assertTrue(self.safety.safety_tx_hook(common.make_msg(0, 0x485, 8))) + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(0, 0x485, 4))) + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(1, 0x485, 8))) + self.assertTrue(self.safety.safety_tx_hook(common.make_msg(0, 0x420, 8))) + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(2, 0x420, 8))) + + def test_radar_scc_refresh_length_and_longitudinal_ownership(self): + for longitudinal in (False, True): + with self.subTest(longitudinal=longitudinal): + flags = HyundaiSafetyFlags.CAN_REFRESH_MSGS | (HyundaiSafetyFlags.LONG if longitudinal else 0) + self._set_mode(flags) + self.assertTrue(self.safety.safety_tx_hook(common.make_msg(0, 0x485, 8))) + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(0, 0x485, 4))) + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(1, 0x485, 8))) + self.assertEqual(self.safety.safety_tx_hook(common.make_msg(0, 0x420, 8)), longitudinal) + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(2, 0x420, 8))) + + def test_classic_hyundai_length_unchanged(self): + self._set_mode(HyundaiSafetyFlags.CAMERA_SCC) + self.assertTrue(self.safety.safety_tx_hook(common.make_msg(0, 0x485, 4))) + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(0, 0x485, 8))) + + def test_hook_reinitialization_does_not_leak_refresh_length(self): + modes = ( + (CarParams.SafetyModel.hyundai, HyundaiSafetyFlags.CAMERA_SCC | HyundaiSafetyFlags.CAN_REFRESH_MSGS, 8), + (CarParams.SafetyModel.hyundai, HyundaiSafetyFlags.CAMERA_SCC, 4), + (CarParams.SafetyModel.hyundaiLegacy, 0, 4), + (CarParams.SafetyModel.hyundaiCanfd, 0, None), + (CarParams.SafetyModel.hyundai, HyundaiSafetyFlags.CAMERA_SCC | HyundaiSafetyFlags.CAN_REFRESH_MSGS, 8), + ) + for model, flags, permitted_length in modes: + with self.subTest(model=model, flags=flags): + self.safety.set_safety_hooks(model, flags) + self.safety.init_tests() + for length in (4, 8): + self.assertEqual(self.safety.safety_tx_hook(common.make_msg(0, 0x485, length)), length == permitted_length) + for bus in (1, 2): + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(bus, 0x485, 8))) + + def test_refresh_flag_fits_unique_safety_param_bit(self): + values = [int(flag) for flag in HyundaiSafetyFlags] + self.assertEqual(len(values), len(set(values))) + self.assertTrue(all(0 < value <= 0xFFFF and value.bit_count() == 1 for value in values)) + self.assertEqual(int(HyundaiSafetyFlags.CAN_REFRESH_MSGS), 32768) + + +class TestHyundaiNonSccSafety(unittest.TestCase): + def setUp(self): + self.safety = libsafety_py.libsafety + self.packer = CANPackerSafety("hyundai_can_generated") + + def _set_mode(self, flags): + self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, flags | HyundaiSafetyFlags.NON_SCC) + self.safety.init_tests() + + def _rx(self, name, values, bus=0, fix_checksum=None): + return self.safety.safety_rx_hook(self.packer.make_can_msg_safety(name, bus, values, fix_checksum=fix_checksum)) + + def test_stock_cruise_source_and_no_longitudinal_transmit(self): + sources = ( + (0, "EMS16", "CRUISE_LAMP_S", "AliveCounter", 4, checksum), + (HyundaiSafetyFlags.HYBRID_GAS, "E_CRUISE_CONTROL", "CRUISE_LAMP_S", None, 0, None), + (HyundaiSafetyFlags.EV_GAS, "EMS12", "ACC_ACT", None, 0, None), + ) + for flags, name, signal, counter, modulus, fix_checksum in sources: + for lda in (False, True): + with self.subTest(flags=flags, lda=lda): + self._set_mode(flags | (HyundaiSafetyFlags.HAS_LDA_BUTTON if lda else 0) | HyundaiSafetyFlags.LONG) + self.assertTrue(self.safety.safety_tx_hook(common.make_msg(0, 0x485, 4))) + for addr in (0x420, 0x421, 0x50A, 0x389, 0x38D, 0x483, 0x7D0): + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(0, addr, 8)), hex(addr)) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x420), 0) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x340), -1) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertTrue(self._rx("CLU11", {"CF_Clu_CruiseSwState": 2, "CF_Clu_AliveCnt1": 0})) + for engaged in (0, 1, 0): + values = {signal: engaged} + if counter is not None: + values[counter] = engaged % modulus + self.assertTrue(self._rx(name, values, fix_checksum=fix_checksum)) + self.assertEqual(self.safety.get_controls_allowed(), bool(engaged)) + # An SCC12 heartbeat is neither required nor a cruise authority on this family. + self.assertTrue(self._rx("SCC12", {"ACCMode": 1, "CR_VSM_Alive": 0}, fix_checksum=checksum)) + self.assertFalse(self.safety.get_controls_allowed()) + + def test_lda_receive_requirement_is_configuration_specific(self): + for lda in (False, True): + with self.subTest(lda=lda): + self._set_mode(HyundaiSafetyFlags.HAS_LDA_BUTTON if lda else 0) + self.assertTrue(self._rx("BCM_PO_11", {"LDA_BTN": 1})) + self.assertFalse(self.safety.get_controls_allowed()) + + def test_brake_and_gas_disengage_for_each_fuel_architecture(self): + for flags, gas_name, gas_signal in ( + (0, "EMS16", "CF_Ems_AclAct"), + (HyundaiSafetyFlags.HYBRID_GAS, "E_EMS11", "CR_Vcu_AccPedDep_Pos"), + (HyundaiSafetyFlags.EV_GAS, "E_EMS11", "Accel_Pedal_Pos"), + ): + with self.subTest(flags=flags): + self._set_mode(flags) + self.safety.set_controls_allowed(True) + self.assertTrue(self._rx("TCS13", {"DriverOverride": 2, "AliveCounterTCS": 0}, fix_checksum=checksum)) + self.assertFalse(self.safety.get_controls_allowed()) + self.safety.set_controls_allowed(True) + values = {gas_signal: 10} + if gas_name == "EMS16": + values["AliveCounter"] = 0 + values["CRUISE_LAMP_S"] = 1 + self.assertTrue(self._rx(gas_name, values, fix_checksum=checksum)) + self.assertTrue(self.safety.get_gas_pressed_prev()) + self.assertFalse(self.safety.get_longitudinal_allowed()) + + def test_receive_health_requires_real_cruise_source_not_scc12(self): + for flags, cruise_name, cruise_values in ( + (0, "EMS16", {"CRUISE_LAMP_S": 0, "AliveCounter": 0}), + (HyundaiSafetyFlags.HYBRID_GAS, "E_CRUISE_CONTROL", {"CRUISE_LAMP_S": 0}), + (HyundaiSafetyFlags.EV_GAS, "EMS12", {"ACC_ACT": 0}), + ): + for lda in (False, True): + with self.subTest(flags=flags, lda=lda): + self._set_mode(flags | (HyundaiSafetyFlags.HAS_LDA_BUTTON if lda else 0)) + frames = [ + ("WHL_SPD11", {"WHL_SPD_AliveCounter_LSB": 0, "WHL_SPD_AliveCounter_MSB": 0}, checksum), + ("TCS13", {"AliveCounterTCS": 0}, checksum), + ("MDPS12", {}, None), + ("CLU11", {"CF_Clu_AliveCnt1": 0}, None), + ] + if flags: + frames.append(("E_EMS11", {"Accel_Pedal_Pos": 0}, None)) + frames.append((cruise_name, cruise_values, checksum if not flags else None)) + if lda: + frames.append(("BCM_PO_11", {"LDA_BTN": 0}, None)) + for name, values, fix in frames: + self.assertTrue(self._rx(name, values, fix_checksum=fix), name) + self.safety.set_timer(1000) + self.safety.safety_tick() + self.assertTrue(self.safety.safety_config_valid()) + + def test_missing_or_wrong_bus_cruise_source_invalidates_health(self): + for flags, cruise_name, cruise_values in ( + (0, "EMS16", {"CRUISE_LAMP_S": 1, "AliveCounter": 0}), + (HyundaiSafetyFlags.HYBRID_GAS, "E_CRUISE_CONTROL", {"CRUISE_LAMP_S": 1}), + (HyundaiSafetyFlags.EV_GAS, "EMS12", {"ACC_ACT": 1}), + ): + for lda in (False, True): + with self.subTest(flags=flags, lda=lda): + self._set_mode(flags | (HyundaiSafetyFlags.HAS_LDA_BUTTON if lda else 0)) + self.safety.set_timer(2_000_000) + for name, values, fix in ( + ("E_EMS11", {"Accel_Pedal_Pos": 0}, None), + ("WHL_SPD11", {"WHL_SPD_AliveCounter_LSB": 0, "WHL_SPD_AliveCounter_MSB": 0}, checksum), + ("TCS13", {"AliveCounterTCS": 0}, checksum), + ("MDPS12", {}, None), + ("CLU11", {"CF_Clu_AliveCnt1": 0}, None), + ("SCC12", {"ACCMode": 1, "CR_VSM_Alive": 0}, checksum), + ): + self.assertTrue(self._rx(name, values, fix_checksum=fix), name) + if lda: + self.assertTrue(self._rx("BCM_PO_11", {"LDA_BTN": 0})) + # This address on a different bus is not evidence for the PT cruise source. + self.assertTrue(self._rx(cruise_name, cruise_values, bus=1, + fix_checksum=checksum if not flags else None)) + self.safety.set_controls_allowed(True) + self.safety.safety_tick() + self.assertFalse(self.safety.safety_config_valid()) + self.assertFalse(self.safety.get_controls_allowed()) + steer = self.packer.make_can_msg_safety("LKAS11", 0, {"CR_Lkas_StrToqReq": 1, "CF_Lkas_ActToi": 1}) + self.assertFalse(self.safety.safety_tx_hook(steer)) + + def test_expired_hybrid_cruise_source_invalidates_health(self): + self._set_mode(HyundaiSafetyFlags.HYBRID_GAS) + self.safety.set_timer(1000) + self.assertTrue(self._rx("E_CRUISE_CONTROL", {"CRUISE_LAMP_S": 0})) + self.safety.set_timer(2_000_000) + for name, values, fix in ( + ("E_EMS11", {"CR_Vcu_AccPedDep_Pos": 0}, None), + ("WHL_SPD11", {"WHL_SPD_AliveCounter_LSB": 0, "WHL_SPD_AliveCounter_MSB": 0}, checksum), + ("TCS13", {"AliveCounterTCS": 0}, checksum), + ("MDPS12", {}, None), + ("CLU11", {"CF_Clu_AliveCnt1": 0}, None), + ("SCC12", {"ACCMode": 1, "CR_VSM_Alive": 0}, checksum), + ): + self.assertTrue(self._rx(name, values, fix_checksum=fix), name) + self.safety.set_controls_allowed(True) + self.safety.safety_tick() + self.assertFalse(self.safety.safety_config_valid()) + self.assertFalse(self.safety.get_controls_allowed()) + + def test_non_scc_steering_request_and_relay_limits(self): + for flags in (0, HyundaiSafetyFlags.HYBRID_GAS, HyundaiSafetyFlags.EV_GAS, + HyundaiSafetyFlags.ALT_LIMITS, HyundaiSafetyFlags.HAS_LDA_BUTTON): + with self.subTest(flags=flags): + self._set_mode(flags) + self.safety.set_controls_allowed(True) + + def steer(torque, request): + return self.packer.make_can_msg_safety("LKAS11", 0, {"CR_Lkas_StrToqReq": torque, "CF_Lkas_ActToi": request}) + self.assertTrue(self.safety.safety_tx_hook(steer(1, 1))) + self.assertFalse(self.safety.safety_tx_hook(steer(400, 1))) + self.assertFalse(self.safety.safety_tx_hook(steer(1, 0))) + self.safety.init_tests() + self.assertTrue(self.safety.safety_rx_hook(common.make_msg(0, 0x340, 8))) + self.assertTrue(self.safety.get_relay_malfunction()) + self.assertFalse(self.safety.safety_tx_hook(steer(0, 1))) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x420), -1) + + class TestHyundaiSafety(HyundaiButtonBase, common.CarSafetyTest, common.DriverTorqueSteeringSafetyTest, common.SteerRequestCutSafetyTest): DBC = "hyundai_can_generated" SAFETY_MODEL = CarParams.SafetyModel.hyundai @@ -237,5 +465,58 @@ class TestHyundaiSafetyFCEVLong(TestHyundaiLongitudinalSafety, TestHyundaiSafety SAFETY_PARAM = HyundaiSafetyFlags.FCEV_GAS | HyundaiSafetyFlags.LONG +class TestHyundaiSafetyCameraSCCRefresh(TestHyundaiSafetyCameraSCC): + def setUp(self): + self.packer = CANPackerSafety("hyundai_can_refresh_generated") + self.safety = libsafety_py.libsafety + self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, HyundaiSafetyFlags.CAMERA_SCC | HyundaiSafetyFlags.CAN_REFRESH_MSGS) + self.safety.init_tests() + + +class TestHyundaiLongitudinalSafetyCameraSCCRefresh(TestHyundaiLongitudinalSafetyCameraSCC): + def setUp(self): + self.packer = CANPackerSafety("hyundai_can_refresh_generated") + self.safety = libsafety_py.libsafety + self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, + HyundaiSafetyFlags.CAMERA_SCC | HyundaiSafetyFlags.CAN_REFRESH_MSGS | HyundaiSafetyFlags.LONG) + self.safety.init_tests() + + +class HyundaiRefreshHybridGasMixin: + def _user_gas_msg(self, gas): + return self.packer.make_can_msg_safety("E_EMS11", 0, {"CR_Vcu_AccPedDep_Pos": gas}, fix_checksum=checksum) + + +class TestHyundaiSafetyCameraSCCRefreshHybrid(HyundaiRefreshHybridGasMixin, TestHyundaiSafetyCameraSCCRefresh): + def setUp(self): + self.packer = CANPackerSafety("hyundai_can_refresh_generated") + self.safety = libsafety_py.libsafety + flags = HyundaiSafetyFlags.CAMERA_SCC | HyundaiSafetyFlags.CAN_REFRESH_MSGS | HyundaiSafetyFlags.HYBRID_GAS + self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, flags) + self.safety.init_tests() + + def test_hybrid_stock_refresh_length_and_acc_ownership(self): + self.assertTrue(self._tx(common.make_msg(0, 0x485, 8))) + self.assertFalse(self._tx(common.make_msg(0, 0x485, 4))) + self.assertFalse(self._tx(common.make_msg(1, 0x485, 8))) + self.assertFalse(self._tx(common.make_msg(0, 0x420, 8))) + + +class TestHyundaiLongitudinalSafetyCameraSCCRefreshHybrid(HyundaiRefreshHybridGasMixin, TestHyundaiLongitudinalSafetyCameraSCCRefresh): + def setUp(self): + self.packer = CANPackerSafety("hyundai_can_refresh_generated") + self.safety = libsafety_py.libsafety + flags = HyundaiSafetyFlags.CAMERA_SCC | HyundaiSafetyFlags.CAN_REFRESH_MSGS | HyundaiSafetyFlags.HYBRID_GAS | HyundaiSafetyFlags.LONG + self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, flags) + self.safety.init_tests() + + def test_hybrid_long_refresh_length_and_acc_allowlist(self): + self.assertTrue(self._tx(common.make_msg(0, 0x485, 8))) + self.assertFalse(self._tx(common.make_msg(0, 0x485, 4))) + self.assertFalse(self._tx(common.make_msg(1, 0x485, 8))) + self.assertTrue(self._tx(common.make_msg(0, 0x420, 8))) + self.assertFalse(self._tx(common.make_msg(2, 0x420, 8))) + + if __name__ == "__main__": unittest.main() diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_angle_hybrids_three.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_angle_hybrids_three.py new file mode 100644 index 0000000000..b2f5071341 --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_angle_hybrids_three.py @@ -0,0 +1,313 @@ +import unittest + +from opendbc.can import CANPacker +from opendbc.car import Bus, gen_empty_fingerprint, structs +from opendbc.car.fingerprints import all_legacy_fingerprint_cars +from opendbc.car.fw_versions import match_fw_to_car +from opendbc.car.hyundai.carcontroller import CarController +from opendbc.car.hyundai.carstate import CarState +from opendbc.car.hyundai.fingerprints import FW_VERSIONS +from opendbc.car.hyundai.hyundaicanfd import CanBus, hkg_can_fd_checksum +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.values import (CANFD_ANGLE_MODEL_BITS, CANFD_ANGLE_MODEL_BANK_BIT, + CANFD_ANGLE_OBSERVED_ADAS_BIT, CAR, DBC, HyundaiFlags, + HyundaiSafetyFlags) +from opendbc.car.lateral import get_max_angle_vm +from opendbc.safety.tests.libsafety import libsafety_py + + +CARS = (CAR.HYUNDAI_AZERA_HEV_7TH_GEN, CAR.KIA_SORENTO_HEV_4TH_GEN_LFA2, CAR.KIA_SPORTAGE_HEV_2026) +TOPOLOGIES = ("lka", "lka_alt", "lfa", "lfa_alt") +STEER_IDS = (0x50, 0x110, 0x12A, 0xCB) + + +def params(car, topology="lka_alt", *, hybrid=False, adas=False, release=False, alpha=False): + fp = gen_empty_fingerprint() + ecan = 1 if topology.startswith("lka") else 0 + if topology == "lka": + fp[2][0x50] = 16 + elif topology == "lka_alt": + fp[2][0x110] = 32 + if topology == "lfa_alt": + fp[ecan][0x1AA] = 16 + else: + fp[ecan][0x1CF] = 8 + if hybrid: + fp[ecan][0xFA] = 8 + if adas: + fp[2][0xCB] = 24 + return CarInterface.get_params(car, fp, [], alpha, release, False) + + +def source_frames(cp, topology, *, speed=36.0, measured=0.0, gear=5, brake=False, gas=False, + cruise=True, wrong_fuel=False): + can = CanBus(cp) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + hybrid = bool(cp.flags & HyundaiFlags.HYBRID) + fuel = "ACCELERATOR_ALT" if hybrid else "ACCELERATOR_BRAKE_ALT" + if wrong_fuel: + fuel = "ACCELERATOR_BRAKE_ALT" if hybrid else "ACCELERATOR_ALT" + button = "CRUISE_BUTTONS_ALT" if topology == "lfa_alt" else "CRUISE_BUTTONS" + scc_bus = can.ECAN if topology.startswith("lka") else can.CAM + gas_values = {"ACCELERATOR_PEDAL": 4.0} if gas and fuel == "ACCELERATOR_ALT" else \ + {"ACCELERATOR_PEDAL_PRESSED": 1} if gas and fuel == "ACCELERATOR_BRAKE_ALT" else {} + entries = [(fuel, can.ECAN, gas_values), + ("TCS", can.ECAN, {"DriverBraking": int(brake)}), + ("WHEEL_SPEEDS", can.ECAN, {key: speed for key in + ("WHL_SpdFLVal", "WHL_SpdFRVal", "WHL_SpdRLVal", "WHL_SpdRRVal")}), + ("MDPS", can.ECAN, {"MDPS_PaStrAnglVal": measured}), + ("STEERING_SENSORS", can.ECAN, {"STEERING_ANGLE": measured}), + ("DOORS_SEATBELTS", can.ECAN, {"DRIVER_SEATBELT": 1}), + ("BLINKERS", can.ECAN, {}), + ("GEAR_ALT_2", can.ECAN, {"GEAR": gear}), + (button, can.ECAN, {"CRUISE_BUTTONS": 2}), + ("SCC_CONTROL", scc_bus, {"ACCMode": 0}), + ("SCC_CONTROL", scc_bus, {"ACCMode": int(cruise)})] + if topology.startswith("lka"): + entries.append(("LKAS_ALT" if topology == "lka_alt" else "LKAS", can.CAM, {})) + return packer, [packer.make_can_msg(name, bus, values) for name, bus, values in entries] + + +class TestHyundaiAngleHybridsThree(unittest.TestCase): + @staticmethod + def packet(frame): + return libsafety_py.make_CANPacket(frame[0], frame[2], frame[1]) + + def mode(self, cp): + safety = libsafety_py.libsafety + self.assertEqual(safety.set_safety_hooks(structs.CarParams.SafetyModel.hyundaiCanfd, + cp.safetyConfigs[-1].safetyParam), 0) + safety.init_tests() + safety.set_timer(1_000_000) + return safety + + def joined(self, car, topology, *, hybrid=False, adas=False, speed=36.0, measured=0.0, gear=5, + brake=False, gas=False, cruise=True, desired=1.0): + cp = params(car, topology, hybrid=hybrid, adas=adas, release=True, alpha=True) + state = CarState(cp) + parsers = state.get_can_parsers(cp) + packer, frames = source_frames(cp, topology, speed=speed, measured=measured, gear=gear, + brake=brake, gas=gas, cruise=cruise) + safety = self.mode(cp) + for frame in frames: + self.assertTrue(safety.safety_rx_hook(self.packet(frame)), (car, topology, hex(frame[0]))) + for parser in parsers.values(): + parser.update((1_000_000_000, [frame for frame in frames if frame[2] == parser.bus])) + state.out = state.update(parsers) + self.assertTrue(parsers[Bus.pt].can_valid, (car, topology, hybrid)) + self.assertEqual(state.out.cruiseState.enabled, cruise) + control = structs.CarControl() + control.enabled = True + control.latActive = True + control.actuators.steeringAngleDeg = desired + controller = CarController(DBC[car], cp) + _, sends = controller.update(control.as_reader(), state, 1_000_000_000) + steering = [frame for frame in sends if frame[0] in STEER_IDS] + return cp, state, safety, control, controller, steering, packer + + def test_all_models_fuel_sources_and_topologies_joined(self): + for car in CARS: + for topology in TOPOLOGIES: + for hybrid in (False, True): + with self.subTest(car=car, topology=topology, hybrid=hybrid): + cp, state, safety, _, _, steering, packer = self.joined(car, topology, hybrid=hybrid) + self.assertEqual(bool(cp.flags & HyundaiFlags.HYBRID), hybrid) + self.assertFalse(cp.alphaLongitudinalAvailable) + self.assertFalse(cp.openpilotLongitudinalControl) + self.assertTrue(cp.pcmCruise) + self.assertTrue(cp.steerControlType == structs.CarParams.SteerControlType.angle) + self.assertEqual(cp.safetyConfigs[-1].safetyParam & 0x3800, CANFD_ANGLE_MODEL_BITS[car.name]) + self.assertTrue(cp.safetyConfigs[-1].safetyParam & CANFD_ANGLE_MODEL_BANK_BIT) + self.assertEqual(bool(cp.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.HYBRID_GAS), hybrid) + self.assertFalse(cp.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.LONG) + self.assertEqual(state.out.gearShifter, structs.CarState.GearShifter.drive) + self.assertTrue(safety.get_controls_allowed()) + self.assertEqual(steering[0][0], 0x50 if topology == "lka" else 0x110 if topology == "lka_alt" else 0x12A) + for frame in steering: + self.assertEqual(int.from_bytes(frame[1][:2], "little"), + hkg_can_fd_checksum(frame[0], None, bytearray(frame[1]))) + self.assertTrue(safety.safety_tx_hook(self.packet(frame)), (car, topology, hybrid, hex(frame[0]))) + self.assertNotEqual((steering[0][1][11] << 6) | (steering[0][1][10] >> 2), 0) + self.assertFalse(safety.safety_tx_hook(self.packet((steering[0][0], steering[0][1], 1 - steering[0][2])))) + self.assertFalse(safety.safety_tx_hook(self.packet((steering[0][0], steering[0][1][:-8], steering[0][2])))) + self.assertFalse(any(frame[0] == 0x1A0 for frame in steering)) + self.assertEqual(packer.dbc.name_to_msg["LFA"].address, 0x12A) + + def test_parsed_non_drive_brake_and_cruise_neutral(self): + for car in CARS: + for topology in TOPOLOGIES: + for gear in (0, 6, 7, 1, 5): # P, N, R, unknown, D from GEAR_ALT_2 + for direction in (-1, 1): + cp, state, safety, _, _, steering, _ = self.joined(car, topology, hybrid=True, + gear=gear, desired=direction * 8.0) + with self.subTest(car=car, topology=topology, gear=gear, direction=direction): + self.assertEqual((steering[0][1][9] >> 4) & 3, 2 if gear == 5 else 1) + self.assertTrue(safety.safety_tx_hook(self.packet(steering[0]))) + if gear != 5: + self.assertNotEqual(state.out.gearShifter, structs.CarState.GearShifter.drive) + self.assertTrue(safety.get_controls_allowed()) + for brake, gas, cruise in ((True, False, True), (False, True, True), (False, False, False)): + cp, state, safety, _, _, steering, _ = self.joined(car, "lfa_alt", hybrid=True, + brake=brake, gas=gas, cruise=cruise) + self.assertEqual((steering[0][1][9] >> 4) & 3, 1) + self.assertTrue(safety.safety_tx_hook(self.packet(steering[0]))) + self.assertEqual(state.out.gasPressed, gas) + if brake or not cruise: + self.assertFalse(safety.get_controls_allowed()) + + def test_native_exact_fuel_source_and_host_parser_health(self): + for car in CARS: + for hybrid in (False, True): + for topology in ("lka_alt", "lfa_alt"): + cp = params(car, topology, hybrid=hybrid) + _, wrong = source_frames(cp, topology, wrong_fuel=True) + safety = self.mode(cp) + for frame in wrong: + safety.safety_rx_hook(self.packet(frame)) + safety.safety_tick() + self.assertFalse(safety.safety_config_valid(), (car, hybrid, topology)) + self.assertFalse(safety.get_controls_allowed()) + state = CarState(cp) + parsers = state.get_can_parsers(cp) + for parser in parsers.values(): + parser.update((1_000_000_000, [frame for frame in wrong if frame[2] == parser.bus])) + self.assertFalse(parsers[Bus.pt].can_valid) + + def test_native_required_bus_crc_counter_and_staleness(self): + for car in CARS: + for hybrid in (False, True): + for topology in ("lka_alt", "lfa_alt"): + cp = params(car, topology, hybrid=hybrid) + packer, frames = source_frames(cp, topology) + scc_addr = packer.dbc.name_to_msg["SCC_CONTROL"].address + active_scc = [frame for frame in frames if frame[0] == scc_addr][-1] + with self.subTest(car=car, hybrid=hybrid, topology=topology): + safety = self.mode(cp) + for frame in frames: + if frame[0] == scc_addr: + frame = (frame[0], frame[1], 1 - frame[2] if frame[2] in (0, 1) else 0) + self.assertTrue(safety.safety_rx_hook(self.packet(frame))) + safety.safety_tick() + self.assertFalse(safety.get_controls_allowed()) + + _, _, safety, _, _, _, _ = self.joined(car, topology, hybrid=hybrid) + corrupt = bytearray(active_scc[1]) + corrupt[0] ^= 1 + self.assertFalse(safety.safety_rx_hook(self.packet((scc_addr, bytes(corrupt), active_scc[2])))) + self.assertFalse(safety.get_controls_allowed()) + + _, _, safety, _, _, _, _ = self.joined(car, topology, hybrid=hybrid) + for _ in range(5): + safety.safety_rx_hook(self.packet(active_scc)) + self.assertFalse(safety.get_controls_allowed()) + + _, _, safety, _, _, _, _ = self.joined(car, topology, hybrid=hybrid) + safety.set_timer(2_100_000) + safety.safety_tick() + self.assertFalse(safety.safety_config_valid()) + + def test_observed_adas_sustained_two_frame_history(self): + for car in CARS: + for topology in ("lfa", "lfa_alt"): + for speed_mps in (2.0, 10.0, 25.0, 35.0): + for direction in (-1, 1): + with self.subTest(car=car, topology=topology, speed=speed_mps, direction=direction): + cp, state, safety, control, controller, _, _ = self.joined( + car, topology, hybrid=True, adas=True, speed=speed_mps * 3.6, + measured=direction * 10.0, desired=direction * 360.0) + self.assertTrue(cp.safetyConfigs[-1].safetyParam & CANFD_ANGLE_OBSERVED_ADAS_BIT) + for step in range(90): + safety.set_timer(1_010_000 + step * 10_000) + _, sends = controller.update(control.as_reader(), state, 1_010_000_000 + step * 10_000_000) + steering = [frame for frame in sends if frame[0] in STEER_IDS] + self.assertEqual([frame[0] for frame in steering], [0x12A, 0xCB]) + self.assertEqual((steering[0][1][9] >> 4) & 3, 1) + for frame in steering: + self.assertTrue(safety.safety_tx_hook(self.packet(frame)), + (car, topology, speed_mps, direction, step, hex(frame[0]))) + bound = get_max_angle_vm(max(state.out.vEgoRaw - 1.0, 1.0), controller.angle_vm, controller.params) + self.assertGreater(abs(controller.apply_angle_last), min(5.0, bound * 0.7)) + self.assertLessEqual(abs(controller.apply_angle_last), min(360.0, bound) + 0.2) + + def test_direct_host_native_limit_matrix(self): + for car in CARS: + for topology in TOPOLOGIES: + for speed_mps in (2.0, 10.0, 25.0, 35.0): + for direction in (-1, 1): + with self.subTest(car=car, topology=topology, speed=speed_mps, direction=direction): + _, state, safety, control, controller, _, _ = self.joined( + car, topology, hybrid=True, speed=speed_mps * 3.6, + measured=direction * 10.0, desired=direction * 360.0) + for step in range(90): + safety.set_timer(1_010_000 + step * 10_000) + _, sends = controller.update(control.as_reader(), state, 1_010_000_000 + step * 10_000_000) + steering = [frame for frame in sends if frame[0] in STEER_IDS] + self.assertEqual(len(steering), 1) + self.assertTrue(safety.safety_tx_hook(self.packet(steering[0])), + (car, topology, speed_mps, direction, step)) + bound = get_max_angle_vm(max(state.out.vEgoRaw - 1.0, 1.0), controller.angle_vm, controller.params) + self.assertGreater(abs(controller.apply_angle_last), min(5.0, bound * 0.7)) + self.assertLessEqual(abs(controller.apply_angle_last), min(360.0, bound) + 0.2) + + def test_other_actuation_fields_and_relay(self): + for car in CARS: + for topology in TOPOLOGIES: + observed = topology.startswith("lfa") + _, _, safety, _, _, steering, _ = self.joined(car, topology, hybrid=True, adas=observed) + primary = steering[-1] + if observed: + self.assertEqual([f[0] for f in steering], [0x12A, 0xCB]) + self.assertTrue(safety.safety_tx_hook(self.packet(steering[0]))) + angle_frame = steering[0] if observed else primary + for byte_index, bit in ((5, 0x2), (6, 0x10)): + data = bytearray(angle_frame[1]) + data[byte_index] ^= bit + data[:2] = hkg_can_fd_checksum(angle_frame[0], None, data).to_bytes(2, "little") + self.assertFalse(safety.safety_tx_hook(self.packet((angle_frame[0], bytes(data), angle_frame[2])))) + if angle_frame[0] == 0x110: + data = bytearray(angle_frame[1]) + data[13] ^= 0x8 + data[:2] = hkg_can_fd_checksum(angle_frame[0], None, data).to_bytes(2, "little") + self.assertFalse(safety.safety_tx_hook(self.packet((angle_frame[0], bytes(data), angle_frame[2])))) + if observed: + for byte_index, bit in ((3, 0x1), (7, 0x1), (8, 0x1)): + data = bytearray(primary[1]) + data[byte_index] ^= bit + data[:2] = hkg_can_fd_checksum(primary[0], None, data).to_bytes(2, "little") + self.assertFalse(safety.safety_tx_hook(self.packet((primary[0], bytes(data), primary[2])))) + self.assertTrue(safety.safety_tx_hook(self.packet(primary))) + self.assertEqual(safety.safety_fwd_hook(0, primary[0]), 2) + self.assertEqual(safety.safety_fwd_hook(2, primary[0]), -1) + safety.set_relay_malfunction(True) + self.assertFalse(safety.safety_tx_hook(self.packet(primary))) + + def test_joint_firmware_discovery_rejects_partial_and_mixed_cohorts(self): + self.assertTrue(set(CARS).issubset(FW_VERSIONS)) + self.assertFalse(set(CARS) & set(all_legacy_fingerprint_cars())) + frozen = { + CAR.HYUNDAI_AZERA_HEV_7TH_GEN: ( + b'\xf1\x00GN7HMFC AT KOR LHD 1.00 1.01 99211-N1110 240423', + b'\xf1\x00GN7_ RDR ----- 1.00 1.00 99110-N1100 ', + ), + CAR.KIA_SORENTO_HEV_4TH_GEN_LFA2: ( + b'\xf1\x00MQ4HMFC AT USA LHD 1.00 1.00 99210-P2600 250617', + b'\xf1\x00MQ4_ RDR ----- 1.00 1.01 99110-P2500 ', + ), + CAR.KIA_SPORTAGE_HEV_2026: ( + b'\xf1\x00NQ51.011.021.012551000HKP_NQ524_50509099211P1110', + b'\xf1\x00NQ5__ 1.00 1.04 99110CH100 ', + ), + } + for car, (camera, radar) in frozen.items(): + fw = [structs.CarParams.CarFw(ecu=ecu, address=addr, subAddress=0, fwVersion=version, brand="hyundai") + for ecu, addr, version in ((structs.CarParams.Ecu.fwdCamera, 0x7C4, camera), + (structs.CarParams.Ecu.fwdRadar, 0x7D0, radar))] + for observed in (fw, fw[:1], fw[1:], [fw[0], structs.CarParams.CarFw( + ecu=structs.CarParams.Ecu.fwdRadar, address=0x7D0, subAddress=0, + fwVersion=frozen[CAR.KIA_SPORTAGE_HEV_2026 if car != CAR.KIA_SPORTAGE_HEV_2026 else + CAR.HYUNDAI_AZERA_HEV_7TH_GEN][1], brand="hyundai")]): + _, matched = match_fw_to_car(observed, "", allow_fuzzy=False, log=False) + expected = {car} if observed == fw else set() + self.assertEqual(set(CARS) & matched, expected, (car, matched)) + _, production_matches = match_fw_to_car(observed, "", log=False) + self.assertEqual(set(CARS) & production_matches, expected, (car, production_matches)) diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_blended.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_blended.py new file mode 100644 index 0000000000..d655b1217b --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_blended.py @@ -0,0 +1,189 @@ +"""Stock mixed-bus native envelopes; stock SCC12 RX CRC remains unverified.""" +import unittest +from opendbc.car.structs import CarParams +from opendbc.safety.tests import common +from opendbc.safety.tests.libsafety import libsafety_py +from opendbc.safety.tests.test_hyundai import checksum + + +class TestHyundaiBlendedStock(unittest.TestCase): + def setUp(self): + self.safety = libsafety_py.libsafety + self.packer = common.CANPackerSafety('hyundai_can_generated') + + def select(self, hda2): + self.bus = int(hda2) + self.steer_counter = 0 + self.assertEqual(self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0x2010 if hda2 else 0x2000), 0) + self.safety.init_tests() + + def scc(self, counter, active=False, crc=0, bus=None, length=8): + data = bytearray(length) + data[0], data[1], data[3] = crc, counter << 4, int(active) << 4 + return common.make_msg(self.bus if bus is None else bus, 0x421, dat=bytes(data)) + + def steer(self, torque, request=True): + if self.bus == 0: + return self.packer.make_can_msg_safety('LKAS11', 0, {'CR_Lkas_StrToqReq': torque, 'CF_Lkas_ActToi': request}) + data = bytearray(16) + raw = torque + 1024 + data[5], data[6] = (raw & 0x7F) << 1, ((raw >> 7) & 0xF) | (int(request) << 4) + from opendbc.car.hyundai.hyundaicanfd import hkg_can_fd_checksum + data[2] = self.steer_counter + self.steer_counter = (self.steer_counter + 1) & 255 + data[:2] = hkg_can_fd_checksum(0x50, None, data).to_bytes(2, 'little') + return common.make_msg(0, 0x50, dat=bytes(data)) + + def test_scc_source_counter_and_explicit_crc_omission(self): + for hda2 in (False, True): + self.select(hda2) + for counter in range(32): + self.assertTrue(self.safety.safety_rx_hook(self.scc(counter % 16, crc=counter * 7 % 256))) + for _ in range(6): + valid = self.safety.safety_rx_hook(self.scc(15)) + self.assertFalse(valid) + self.assertFalse(self.safety.get_controls_allowed()) + self.select(hda2) + self.safety.safety_rx_hook(common.make_msg(self.bus, 0x4F1, dat=bytes([1, 0, 0, 0]))) + for message in (self.scc(0, True, bus=2), self.scc(0, True, length=7)): + self.safety.safety_rx_hook(message) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertTrue(self.safety.safety_rx_hook(self.scc(0, True))) + self.assertTrue(self.safety.get_controls_allowed()) + self.safety.safety_rx_hook(self.scc(1, False)) + self.assertFalse(self.safety.get_controls_allowed()) + + def test_set_resume_cancel_and_no_longitudinal_ownership(self): + for hda2 in (False, True): + self.select(hda2) + for controls in (False, True): + for cruise in (False, True): + self.safety.set_controls_allowed(controls) + self.safety.set_cruise_engaged_prev(cruise) + for button in (0, 1, 2, 3, 4, 5): + expected = (button in (1, 2) and controls) or (button == 4 and cruise) + self.assertEqual(self.safety.safety_tx_hook(common.make_msg(self.bus, 0x4F1, + dat=bytes([button, 0, 0, 0]))), expected) + # Stock SCC remains receiver/forwarding owned even while enabled. + for address in (0x420, 0x421): + for bus in (0, 1, 2): + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(bus, address))) + + def test_hda2_status_actual_payloads_and_reserved_bits(self): + self.select(True) + # Lossless unique coverage of the retained 92 stateless original packets. + payloads = [bytes.fromhex(value) for value in ( + 'bf00000400000000', + 'd850000400000000', + '71a0000400000000', + '16f0000400000000', + '6140000400000000', + 'a790000400000000', + 'afe0000400000000', + '6930000400000000', + '1e80000400000000', + '79d0000400000000', + 'd020000400000000', + '6370000600000000', + 'c0c0000400000000', + '0610000400000000', + '0e60000400000000', + 'c8b0000400000000', + 'b770000400000000', + '7be0000600000000', + )] + for controls in (False, True): + self.safety.set_controls_allowed(controls) + for payload in payloads: + self.assertTrue(self.safety.safety_tx_hook(common.make_msg(1, 0x485, dat=payload))) + status_packer = common.CANPackerSafety('hyundai_palisade_2023_generated') + for icon in range(4): + self.assertTrue(self.safety.safety_tx_hook(status_packer.make_can_msg_safety( + 'LFAHDA_MFC', 1, {'LFA_Icon_State': icon, 'COUNTER': 7, 'CHECKSUM': 0xA5}))) + for bus in (0, 2, 3): + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(bus, 0x485, dat=payloads[0]))) + for length in (0, 1, 2, 3, 4, 5, 6, 7, 12, 16, 20, 24, 32, 48, 64): + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(1, 0x485, length))) + mask = (0xff, 0xf0, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00) + for index, allowed in enumerate(mask): + for bit in range(8): + if not allowed & (1 << bit): + data = bytearray(payloads[0]) + data[index] |= 1 << bit + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(1, 0x485, dat=bytes(data)))) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x485), 0) + self.assertEqual(self.safety.safety_fwd_hook(0, 0x485), 2) + self.safety.set_timer(2_000_000) + self.safety.safety_rx_hook(common.make_msg(1, 0x485, dat=payloads[0])) + self.assertTrue(self.safety.get_relay_malfunction()) + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(1, 0x485, dat=payloads[0]))) + + def test_required_sources_and_timeout(self): + for hda2 in (False, True): + sources = (None, 'EMS16', 'WHL_SPD11', 'TCS13', 'MDPS12', 'CLU11', 'SCC11', 'SCC12') + for omitted in sources + (('CAM_0x2a4',) if hda2 else ()): + self.select(hda2) + for name, values, fix in ( + ('EMS16', {'AliveCounter': 0}, checksum), ('WHL_SPD11', {}, checksum), + ('TCS13', {'AliveCounterTCS': 0}, checksum), ('MDPS12', {}, None), + ('CLU11', {'CF_Clu_AliveCnt1': 0}, None), + ): + if name != omitted: + self.assertTrue(self.safety.safety_rx_hook(self.packer.make_can_msg_safety(name, self.bus, values, fix_checksum=fix))) + if omitted != 'SCC11': + self.assertTrue(self.safety.safety_rx_hook(common.make_msg(self.bus, 0x420))) + if omitted != 'SCC12': + self.assertTrue(self.safety.safety_rx_hook(self.scc(0))) + if hda2 and omitted != 'CAM_0x2a4': + camera = common.CANPackerSafety('hyundai_palisade_2023_generated') + self.assertTrue(self.safety.safety_rx_hook(camera.make_can_msg_safety('CAM_0x2a4', 2, {}))) + self.safety.set_timer(1000) + self.safety.safety_tick() + self.assertEqual(self.safety.safety_config_valid(), omitted is None) + self.safety.set_controls_allowed(True) + self.safety.set_timer(2_000_000) + self.safety.safety_tick() + self.assertFalse(self.safety.safety_config_valid()) + self.assertFalse(self.safety.get_controls_allowed()) + + def test_torque_driver_rate_off_and_cancel(self): + for hda2 in (False, True): + self.select(hda2) + maximum, rate = (384, 3) if hda2 else (404, 2) + self.safety.set_torque_driver(0, 0) + for allowed, torque, last, expected in ( + (False, 0, 0, True), (False, 1, 0, False), (True, rate, 0, True), + (True, rate + 1, 0, False), (True, maximum, maximum, True), (True, maximum + 1, maximum, False), + ): + self.safety.set_controls_allowed(allowed) + self.safety.set_desired_torque_last(last) + self.safety.set_rt_torque_last(last) + self.assertEqual(self.safety.safety_tx_hook(self.steer(torque, torque != 0)), expected) + self.safety.set_controls_allowed(True) + self.safety.set_desired_torque_last(100) + self.safety.set_rt_torque_last(100) + self.safety.set_torque_driver(-1000, -1000) + self.assertFalse(self.safety.safety_tx_hook(self.steer(100))) + self.safety.set_controls_allowed(False) + self.safety.set_cruise_engaged_prev(False) + for button in (1, 2, 4): + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(self.bus, 0x4F1, dat=bytes([button, 0, 0, 0])))) + self.safety.set_cruise_engaged_prev(True) + self.assertTrue(self.safety.safety_tx_hook(common.make_msg(self.bus, 0x4F1, dat=bytes([4, 0, 0, 0])))) + + def test_relay_exact_tx_and_stock_forwarding(self): + for hda2 in (False, True): + self.select(hda2) + address, length = (0x50, 16) if hda2 else (0x340, 8) + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(0, address, 12 if hda2 else 7))) + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(1, address, length))) + for denied in (0x110, 0x12A, 0x420, 0x421): + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(0, denied))) + for stock in (0x420, 0x421): + self.assertEqual(self.safety.safety_fwd_hook(2, stock), 0) + self.assertEqual(self.safety.safety_fwd_hook(0, stock), 2) + self.assertEqual(self.safety.safety_fwd_hook(2, address), -1) + self.safety.set_timer(2_000_000) + self.safety.safety_rx_hook(common.make_msg(0, address, length)) + self.assertTrue(self.safety.get_relay_malfunction()) + self.assertFalse(self.safety.safety_tx_hook(self.steer(0, False))) diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_blended_alpha.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_blended_alpha.py new file mode 100644 index 0000000000..02a7270d9a --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_blended_alpha.py @@ -0,0 +1,612 @@ +"""Experimental mixed longitudinal safety contract regressions.""" +import unittest +from opendbc.car.structs import CarParams +from opendbc.safety.tests import common +from opendbc.safety.tests.libsafety import libsafety_py +from opendbc.safety.tests.test_hyundai import checksum + + +class TestHyundaiBlendedAlpha(unittest.TestCase): + def setUp(self): + self.safety = libsafety_py.libsafety + self.packer = common.CANPackerSafety('hyundai_palisade_2023_generated') + + def select(self, hda2): + self.bus = int(hda2) + self.primary_counter = 0 + self.assertEqual(self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, + 0x2014 if hda2 else 0x2004), 0) + self.safety.init_tests() + + def primary(self, torque, request): + data = bytearray(16) + raw = torque + 1024 + data[5] = (raw & 0x7F) << 1 + data[6] = ((raw >> 7) & 0xF) | (int(request) << 4) + from opendbc.car.hyundai.hyundaicanfd import hkg_can_fd_checksum + data[2] = self.primary_counter + self.primary_counter = (self.primary_counter + 1) & 255 + data[:2] = hkg_can_fd_checksum(0x50, None, data).to_bytes(2, 'little') + return common.make_msg(0, 0x50, dat=bytes(data)) + + def mirror(self, torque, request): + data = bytearray(8) + raw = torque + 1024 + data[2] = raw & 0xFF + data[3] = ((raw >> 8) & 7) | (int(request) << 3) + return common.make_msg(1, 0x340, dat=bytes(data)) + + def test_secondary_torque_requires_accepted_one_use_primary(self): + self.select(True) + self.safety.set_controls_allowed(True) + self.safety.set_torque_driver(0, 0) + self.assertFalse(self.safety.safety_tx_hook(self.mirror(3, True))) + self.assertTrue(self.safety.safety_tx_hook(self.primary(3, True))) + self.assertTrue(self.safety.safety_tx_hook(self.mirror(3, True))) + self.assertFalse(self.safety.safety_tx_hook(self.mirror(3, True))) + self.assertFalse(self.safety.safety_tx_hook(self.primary(30, True))) + self.assertFalse(self.safety.safety_tx_hook(self.mirror(30, True))) + self.select(True) + self.safety.set_controls_allowed(True) + self.assertTrue(self.safety.safety_tx_hook(self.primary(3, True))) + self.safety.set_timer(10001) + self.assertFalse(self.safety.safety_tx_hook(self.mirror(3, True))) + + def test_acceleration_layout_limits_and_off_zero(self): + for hda2 in (False, True): + self.select(hda2) + for allowed, accel, expected in ((False, 0, True), (False, .01, False), + (True, -3.5, True), (True, 3.5, True), + (True, -3.51, False), (True, 3.51, False)): + self.safety.set_controls_allowed(allowed) + for raw, value in ((accel, 0), (0, accel)): + packet = self.packer.make_can_msg_safety('SCC11', self.bus, + {'aReqRaw': raw, 'aReqValue': value}) + self.assertEqual(self.safety.safety_tx_hook(packet), expected) + + def test_unresolved_payload_owners_and_non_tester_uds_stay_closed(self): + for hda2 in (False, True): + self.select(hda2) + self.safety.set_controls_allowed(True) + for address in (0x38D, 0x4F1): + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(self.bus, address))) + packet = self.packer.make_can_msg_safety('ADRV_0x51', 0, {}) + self.assertEqual(self.safety.safety_tx_hook(packet), hda2) + modified = bytearray(32) + modified[3] = 1 + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(0, 0x51, dat=bytes(modified)))) + address = 0x730 if hda2 else 0x7D0 + self.assertTrue(self.safety.safety_tx_hook(common.make_msg(self.bus, address, + dat=b'\x02\x3e\x80\x00\x00\x00\x00\x00'))) + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(self.bus, address, + dat=b'\x03\x28\x03\x01\x00\x00\x00\x00'))) + + def test_hda2_fca_exact_original_source_pattern_and_request_guards(self): + self.select(True) + for counter in range(15): + packet = self.packer.make_can_msg_safety('FCA11', 1, + {'cr_vsm_deccmd': 255, 'cf_vsm_deccmdact': 0, 'COUNTER': counter, 'CHECKSUM': 0xA5}) + self.assertEqual(bytes(packet[0].data[0:8]), bytes([0xA5, counter << 4, 0, 0, 0xC0, 0x3F, 0x7F, 0])) + self.assertTrue(self.safety.safety_tx_hook(packet)) + for index in range(2, 8): + for bit in range(8): + changed = bytearray([0xA5, counter << 4, 0, 0, 0xC0, 0x3F, 0x7F, 0]) + changed[index] ^= 1 << bit + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(1, 0x38D, dat=bytes(changed)))) + for bit in range(4): + changed = bytearray([0xA5, (counter << 4) | (1 << bit), 0, 0, 0xC0, 0x3F, 0x7F, 0]) + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(1, 0x38D, dat=bytes(changed)))) + valid = bytes([0xA5, 0, 0, 0, 0xC0, 0x3F, 0x7F, 0]) + for allowed in (False, True): + self.safety.set_controls_allowed(allowed) + self.assertTrue(self.safety.safety_tx_hook(common.make_msg(1, 0x38D, dat=valid))) + for length in (0, 1, 4, 7, 12, 16): + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(1, 0x38D, dat=valid[:length].ljust(length, b'\x00')))) + for bus in (0, 2, 3): + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(bus, 0x38D, dat=valid))) + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(1, 0x38D, + dat=bytes([0xA5, 0xF0, 0, 0, 0xC0, 0x3F, 0x7F, 0])))) + self.select(False) + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(0, 0x38D, dat=valid))) + + def test_fixed_radar_aux_source_bytes_and_mutations(self): + for hda2 in (False, True): + self.select(hda2) + specs = [('RADAR_0x363', {'FCA_ESA': 1}), + ('RADAR_0x398', {'BYTE4': 0x80, 'BYTE5': 0x5D if hda2 else 0x10})] + if hda2: + specs += [('RADAR_0x399', {'BYTE2': 2}), ('RADAR_0x39a', {'BYTE7': 0xFF}), + ('RADAR_0x39b', {}), ('RADAR_0x39c', {'BYTE5': 0xE0, 'BYTE6': 0x79}), + ('RADAR_0x43a', {'BYTE2': 7})] + for name, fields in specs: + for counter in range(16): + packet = self.packer.make_can_msg_safety(name, self.bus, + fields | {'COUNTER': counter, 'CHECKSUM': 0xA5}) + self.assertTrue(self.safety.safety_tx_hook(packet)) + for index in range(2, 8): + for bit in range(8): + modified = self.packer.make_can_msg_safety(name, self.bus, + fields | {'COUNTER': counter, 'CHECKSUM': 0xA5}) + modified[0].data[index] ^= 1 << bit + self.assertFalse(self.safety.safety_tx_hook(modified)) + + def test_auxiliary_low_nibble_and_alpha_button_are_denied(self): + for hda2 in (False, True): + self.select(hda2) + packet = self.packer.make_can_msg_safety('RADAR_0x363', self.bus, {'FCA_ESA': 1}) + self.assertTrue(self.safety.safety_tx_hook(packet)) + packet[0].data[1] ^= 1 + self.assertFalse(self.safety.safety_tx_hook(packet)) + button = self.button(0, 0) + self.assertFalse(self.safety.safety_tx_hook(button)) + self.assertFalse(self.safety.safety_test_selected_tx(button)) + + def tcs(self, active, counter=0): + values = {'ACC_REQ': int(active), 'AliveCounterTCS': counter} + return self.packer.make_can_msg_safety('TCS13', self.bus, values, fix_checksum=checksum) + + def button(self, value, counter): + return self.packer.make_can_msg_safety('CLU11', self.bus, + {'CF_Clu_CruiseSwState': value, 'CF_Clu_AliveCnt1': counter}) + + def test_cancel_enable_pair_and_active_stop_do_not_use_stale_stock_flag(self): + for hda2 in (False, True): + self.select(hda2) + self.assertTrue(self.safety.safety_rx_hook(self.tcs(False))) + self.safety.safety_rx_hook(self.button(4, 0)) + self.assertFalse(self.safety.get_controls_allowed()) + self.safety.safety_rx_hook(self.button(0, 1)) + self.assertTrue(self.safety.get_controls_allowed()) + self.safety.safety_rx_hook(self.button(4, 2)) + self.assertFalse(self.safety.get_controls_allowed()) + self.safety.safety_rx_hook(self.button(0, 3)) + self.assertFalse(self.safety.get_controls_allowed()) + self.select(hda2) + self.safety.safety_rx_hook(self.tcs(True)) + self.safety.safety_rx_hook(self.button(4, 0)) + self.safety.safety_rx_hook(self.button(0, 1)) + self.assertFalse(self.safety.get_controls_allowed()) + + def test_cancel_latch_cannot_survive_intervening_inhibits(self): + for hda2 in (False, True): + for inhibit in ('tcs', 'brake', 'gas', 'stale'): + with self.subTest(hda2=hda2, inhibit=inhibit): + self.select(hda2) + self.assertTrue(self.safety.safety_rx_hook(self.tcs(False, 0))) + self.assertTrue(self.safety.safety_rx_hook(self.button(4, 0))) + if inhibit == 'tcs': + self.assertTrue(self.safety.safety_rx_hook(self.tcs(True, 1))) + self.assertTrue(self.safety.safety_rx_hook(self.tcs(False, 2))) + elif inhibit == 'brake': + for counter, pressed in ((1, True), (2, False)): + packet = self.packer.make_can_msg_safety('TCS13', self.bus, + {'DriverOverride': 2 if pressed else 0, 'AliveCounterTCS': counter}, fix_checksum=checksum) + self.assertTrue(self.safety.safety_rx_hook(packet)) + elif inhibit == 'gas': + for counter, pressed in ((0, True), (1, False)): + packet = self.packer.make_can_msg_safety('EMS16', self.bus, + {'CF_Ems_AclAct': int(pressed), 'AliveCounter': counter}, fix_checksum=checksum) + self.assertTrue(self.safety.safety_rx_hook(packet)) + else: + self.safety.set_timer(100001) + self.assertTrue(self.safety.safety_rx_hook(self.tcs(False, 1))) + self.assertTrue(self.safety.safety_rx_hook(self.button(4, 1))) + self.assertTrue(self.safety.safety_rx_hook(self.button(0, 2))) + self.assertFalse(self.safety.get_controls_allowed()) + # A new inactive pair may enable after the inhibit has cleared. + self.assertTrue(self.safety.safety_rx_hook(self.button(4, 3))) + self.assertTrue(self.safety.safety_rx_hook(self.button(0, 4))) + self.assertTrue(self.safety.get_controls_allowed()) + + +class TestHyundaiBlendedAlphaAol(unittest.TestCase): + def setUp(self): + self.safety = libsafety_py.libsafety + self.counter = 0 + self.now = 1_000_000 + self.packer = common.CANPackerSafety('hyundai_palisade_2023_generated') + self.release = self.safety.set_safety_hooks(CarParams.SafetyModel.allOutput, 0) != 0 + + def tearDown(self): + self.safety.set_alternative_experience(0) + + def reset(self, word=0x2004, experience=32): + self.bus = int(word == 0x2014) + self.safety.init_tests() + self.safety.set_alternative_experience(experience) + self.assertEqual(self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, word), 0) + self.safety.set_timer(self.now) + self.safety.set_aol_test_heartbeat(True) + + def request(self, mask): + self.safety.aol_set_host_request(mask) + return self.safety.aol_get_permission_mask() + + def feed(self, *, button=0, lda=False, gas=False, brake=False, available=True, omit=None): + self.safety.set_timer(self.now) + c = self.counter + self.counter += 1 + messages = ( + ('EMS16', {'AliveCounter': c % 4, 'CF_Ems_AclAct': int(gas)}, True), + ('WHL_SPD11', {'WHL_SPD_AliveCounter_LSB': c % 4, 'WHL_SPD_AliveCounter_MSB': (c // 4) % 4}, True), + ('TCS13', {'AliveCounterTCS': c % 8, 'ACCEnable': 0 if available else 1, + 'DriverOverride': 2 if brake else 0}, True), + ('MDPS12', {'CR_Mdps_StrColTq': 0}, False), + ('CLU11', {'CF_Clu_AliveCnt1': c % 16, 'CF_Clu_CruiseSwState': button}, False), + ('BCM_PO_11', {'LDA_BTN': int(lda)}, False), + ) + for name, values, checked in messages: + if name != omit: + self.assertTrue(self.safety.safety_rx_hook(self.packer.make_can_msg_safety( + name, self.bus, values, fix_checksum=checksum if checked else None))) + if self.bus == 1: + self.assertTrue(self.safety.safety_rx_hook(self.packer.make_can_msg_safety( + 'CAM_0x2a4', 2, {'COUNTER': c % 256}))) + self.now += 10_000 + + def warm(self): + for _ in range(8): + self.feed() + + def test_hdai_withdrawal_consumes_valid_counter_without_output_credit(self): + from opendbc.car.hyundai.hyundaican import hyundai_checksum + + def lkas_checksum(msg): + address, data, bus = msg + return address, bytes([hyundai_checksum(data[1:8])]) + data[1:8], bus + + def primary(counter, torque=0): + return self.packer.make_can_msg_safety('LKAS11', 0, { + 'CF_Lkas_MsgCount': counter, 'CR_Lkas_StrToqReq': torque, + 'CF_Lkas_ActToi': int(torque != 0), + }, fix_checksum=lkas_checksum) + + for word in (0x2000, 0x2004): + with self.subTest(word=word): + self.counter, self.now = 0, 1_000_000 + self.reset(word) + if word == 0x2004 and self.release: + self.assertFalse(self.safety.safety_tx_hook(primary(0, 2))) + self.assertEqual(self.request(1), 0) + self.assertFalse(self.safety.get_controls_allowed()) + continue + + # Stock SCC remains a real receiver owner; alpha owns its SCC output. + def physical(lda=False, owner_word=word): + if owner_word == 0x2000: + self.assertTrue(self.safety.safety_rx_hook(self.packer.make_can_msg_safety('SCC11', 0, {}))) + self.assertTrue(self.safety.safety_rx_hook(self.packer.make_can_msg_safety( + 'SCC12', 0, {'COUNTER': self.counter % 16, 'MainMode_ACC': 1, 'ACCMode': 0}))) + self.feed(lda=lda) + + for _ in range(8): + physical() + physical(lda=True) + self.assertEqual(self.request(1), 1, 'Fresh physical LDA edge and checked RX graph own lateral only') + self.assertFalse(self.safety.get_controls_allowed()) + self.assertTrue(self.safety.safety_tx_hook(primary(0))) + self.assertTrue(self.safety.safety_tx_hook(primary(1, 2))) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x340), -1) + + self.assertEqual(self.request(0), 0) + self.assertFalse(self.safety.safety_tx_hook(primary(2, 4)), 'Queued active command must deny after host withdrawal') + self.assertTrue(self.safety.safety_tx_hook(primary(3)), 'First following neutral consumes the next real sender counter') + self.assertFalse(self.safety.get_controls_allowed()) + self.assertEqual(self.request(1), 1) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x340), 0, 'Neutral acceptance cannot restore forwarding ownership') + + wrong_bus = primary(4) + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(1, 0x340, dat=bytes(wrong_bus[0].data[0:8])))) + self.assertTrue(self.safety.safety_tx_hook(primary(4)), 'Wrong bus cannot consume a valid primary counter') + bad_crc = bytearray(primary(5)[0].data[0:8]) + bad_crc[0] ^= 1 + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(0, 0x340, dat=bytes(bad_crc)))) + self.assertEqual(self.request(1), 0, 'CRC failure clears physical authority') + self.assertFalse(self.safety.get_controls_allowed()) + physical() + self.assertEqual(self.request(1), 0, 'Fresh neutral sensor data cannot recreate an enable token') + self.assertFalse(self.safety.get_controls_allowed()) + self.assertTrue(self.safety.safety_tx_hook(primary(5))) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x340), 0) + self.assertFalse(self.safety.safety_tx_hook(primary(5)), 'Duplicate counter remains denied') + self.assertEqual(self.request(1), 0) + self.assertFalse(self.safety.get_controls_allowed()) + + def test_main_off_cancel_and_heartbeat_loss_revoke_physical_token(self): + for cause in ('main_off', 'cancel', 'heartbeat'): + self.now = 1_000_000 + self.counter = 0 + self.reset() + self.warm() + self.feed(lda=True) + self.assertEqual(self.request(1), 0 if self.release else 1) + if self.release: + continue + if cause == 'main_off': + self.feed(available=False) + elif cause == 'cancel': + self.feed(button=4) + else: + self.safety.set_aol_test_heartbeat(False) + self.assertEqual(self.request(1), 0) + self.assertFalse(self.safety.get_controls_allowed()) + + def test_cluster_lda_source_needs_its_own_neutral_edge(self): + self.reset() + self.warm() + self.assertEqual(self.request(0), 0) + for pressed in (0, 1): + packet = self.packer.make_can_msg_safety('CLU13', 0, {'CF_Clu_LdwsLkasSW': pressed}) + self.assertTrue(self.safety.safety_rx_hook(packet)) + self.assertEqual(self.request(1), 0 if self.release else 1) + self.reset() + self.warm() + self.assertEqual(self.request(1), 0) + for pressed in (0, 1): + packet = self.packer.make_can_msg_safety('CLU13', 0, {'CF_Clu_LdwsLkasSW': pressed}) + self.assertTrue(self.safety.safety_rx_hook(packet)) + self.assertEqual(self.request(1), 0) + + def test_hda2_lateral_only_mirror_requires_final_accepted_primary(self): + if self.release: + self.reset(0x2014) + self.assertFalse(self.safety.safety_tx_hook(TestHyundaiBlendedAlpha.mirror(self, 0, False))) + return + for rejection in ('none', 'crc', 'counter', 'request', 'expired', 'authority'): + with self.subTest(rejection=rejection): + self.counter = 0 + self.reset(0x2014) + self.warm() + self.feed(lda=True) + self.assertEqual(self.request(3), 1) + self.assertFalse(self.safety.get_controls_allowed()) + self.safety.set_torque_driver(0, 0) + + def primary(counter): + return self.packer.make_can_msg_safety('LKAS', 0, + {'COUNTER': counter, 'TORQUE_REQUEST': 3, 'STEER_REQ': 1}) + first = primary(0) + self.assertTrue(self.safety.safety_tx_hook(first)) + self.assertTrue(self.safety.safety_tx_hook(TestHyundaiBlendedAlpha.mirror(self, 3, True))) + packet = primary(1) + if rejection == 'crc': + data = bytearray(packet[0].data[0:16]) + data[0] ^= 1 + packet = common.make_msg(0, 0x50, dat=bytes(data)) + elif rejection == 'counter': + packet = first + self.assertEqual(self.safety.safety_tx_hook(packet), rejection not in ('crc', 'counter')) + if rejection == 'expired': + self.safety.set_timer(self.now + 10_001) + elif rejection == 'authority': + self.request(0) + mirror = TestHyundaiBlendedAlpha.mirror(self, 3, rejection != 'request') + self.assertEqual(self.safety.safety_tx_hook(mirror), rejection == 'none') + self.assertFalse(self.safety.safety_tx_hook(mirror)) + + def test_token_is_lateral_only_and_long_enable_remains_physical(self): + self.reset() + self.warm() + self.feed(lda=True) + self.assertEqual(self.request(3), 0 if self.release else 1) + self.assertFalse(self.safety.get_controls_allowed()) + if self.release: + return + self.feed(button=2) + self.feed() + self.assertTrue(self.safety.get_controls_allowed()) + self.assertEqual(self.request(3), 3) + self.assertEqual(self.request(2), 2) + self.feed(gas=True) + self.assertTrue(self.safety.get_controls_allowed()) + self.assertEqual(self.request(3), 1) + accel = self.packer.make_can_msg_safety('SCC11', 0, {'aReqRaw': 0.1, 'aReqValue': 0.1}) + self.assertFalse(self.safety.safety_tx_hook(accel)) + self.assertTrue(self.safety.safety_tx_hook(self.packer.make_can_msg_safety('SCC11', 0, {'aReqRaw': 0, 'aReqValue': 0}))) + self.feed() + self.assertEqual(self.request(3), 3, 'Gas override pauses normal cruise without disarming it') + self.assertTrue(self.safety.safety_tx_hook(accel)) + self.feed(brake=True) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertEqual(self.request(3), 1) + self.feed() + self.assertEqual(self.request(3), 1, 'Releasing brake cannot restore normal long without an enable edge') + + def test_dead_required_source_and_held_input_cannot_rearm(self): + self.reset() + self.warm() + self.feed(lda=True) + self.request(1) + for _ in range(40): + self.feed(lda=True, omit='TCS13') + self.request(1) + self.assertEqual(self.request(3), 0) + for _ in range(8): + self.feed(lda=True) + self.assertEqual(self.request(1), 0) + self.request(0) + self.feed() + self.feed(lda=True) + self.assertEqual(self.request(1), 0 if self.release else 1) + + def test_mixed_crc_counter_and_accepted_forwarding(self): + self.reset() + self.warm() + self.feed(lda=True) + self.request(1) + if self.release: + self.assertFalse(self.safety.safety_tx_hook(self.packer.make_can_msg_safety('LKAS11', 0, {}))) + return + from opendbc.car.hyundai.hyundaican import hyundai_checksum + + def packet(counter, corrupt=False): + raw = self.packer.make_can_msg('LKAS11', 0, {'CR_Lkas_StrToqReq': 2, 'CF_Lkas_ActToi': 1, + 'CF_Lkas_MsgCount': counter}) + data = bytearray(raw[1]) + data[0] = hyundai_checksum(bytes(data[1:])) + if corrupt: + data[0] ^= 1 + return common.make_msg(0, 0x340, dat=bytes(data)) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x340), 0) + self.assertTrue(self.safety.safety_tx_hook(packet(0))) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x340), -1) + self.assertFalse(self.safety.safety_tx_hook(packet(0))) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x340), 0) + self.assertFalse(self.safety.safety_tx_hook(packet(1, corrupt=True))) + + def test_hda2_and_foreign_experience_never_get_mixed_aol(self): + for word, experience in ((0x2014, 32), (0x2004, 0), (0x2004, 33)): + self.reset(word, experience) + self.assertEqual(self.request(3), 0) + if word == 0x2004 and not self.release: + for address in (0x340, 0x364, 0x485, 0x420, 0x421): + self.assertEqual(self.safety.safety_fwd_hook(2, address), -1) + self.assertEqual(self.safety.safety_fwd_hook(0, address), 2) + + +class TestHyundaiBlendedHda2Integrity(unittest.TestCase): + def setUp(self): + self.safety = libsafety_py.libsafety + self.packer = common.CANPackerSafety('hyundai_palisade_2023_generated') + self.safety.set_alternative_experience(0) + self.assertEqual(self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0x2010), 0) + self.safety.init_tests() + + def test_adrv_successive_counter_crc_shape_and_bus_fail_closed(self): + release = self.safety.set_safety_hooks(CarParams.SafetyModel.allOutput, 0) != 0 + self.safety.set_alternative_experience(0) + self.assertEqual(self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0x2014), 0) + self.safety.init_tests() + + def adrv(counter): + return self.packer.make_can_msg_safety('ADRV_0x51', 0, {'COUNTER': counter}) + + if release: + for counter in (255, 0, 1): + self.assertFalse(self.safety.safety_tx_hook(adrv(counter))) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + return + + for counter in (255, 0, 1): + self.assertTrue(self.safety.safety_tx_hook(adrv(counter)), 'Consecutive accepted ADRV packets wrap at eight bits') + for counter in (255, 0): + self.assertTrue(self.safety.safety_tx_hook(self.packer.make_can_msg_safety('LKAS', 0, {'COUNTER': counter, 'TORQUE_REQUEST': 0})), + 'ADRV progression must not consume the separate primary steering counter') + for rejection in ('duplicate', 'jump', 'crc', 'bus', 'shape'): + with self.subTest(rejection=rejection): + self.safety.set_alternative_experience(0) + self.assertEqual(self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0x2014), 0) + self.safety.init_tests() + self.assertTrue(self.safety.safety_tx_hook(adrv(42))) + packet = adrv(42 if rejection == 'duplicate' else 44 if rejection == 'jump' else 43) + if rejection in ('crc', 'bus', 'shape'): + data = bytearray(packet[0].data[0:32]) + if rejection == 'crc': + data[0] ^= 1 + packet = common.make_msg(1 if rejection == 'bus' else 0, 0x51, + dat=bytes(data[:8] if rejection == 'shape' else data)) + self.assertFalse(self.safety.safety_tx_hook(packet)) + self.assertFalse(self.safety.get_controls_allowed(), 'Rejected or neutral ADRV traffic cannot grant vehicle control') + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + # Integrity failure clears sequence/authority. Whitelist bus/length denial + # never enters the selected hook and retains its expected next counter. + recovery = 43 if rejection in ('bus', 'shape') else 97 + self.assertTrue(self.safety.safety_tx_hook(adrv(recovery))) + self.assertFalse(self.safety.get_controls_allowed(), 'Reanchoring a neutral sequence cannot restore authority') + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.assertFalse(self.safety.safety_tx_hook(adrv(recovery))) + + def test_primary_counter_crc_and_bus_are_independent_guards(self): + packet = self.packer.make_can_msg_safety('LKAS', 0, {'COUNTER': 255, 'TORQUE_REQUEST': 0}) + self.assertTrue(self.safety.safety_tx_hook(packet)) + self.assertFalse(self.safety.safety_tx_hook(packet)) + wrapped = self.packer.make_can_msg_safety('LKAS', 0, {'COUNTER': 0, 'TORQUE_REQUEST': 0}) + self.assertTrue(self.safety.safety_tx_hook(wrapped)) + data = bytearray(wrapped[0].data[0:16]) + data[2] = 1 + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(0, 0x50, dat=bytes(data)))) + corrected = self.packer.make_can_msg_safety('LKAS', 0, {'COUNTER': 1, 'TORQUE_REQUEST': 0}) + wrong_bus = common.make_msg(1, 0x50, dat=bytes(corrected[0].data[0:16])) + self.assertFalse(self.safety.safety_tx_hook(wrong_bus)) + self.assertTrue(self.safety.safety_tx_hook(corrected)) + + def test_valid_rejected_torque_consumes_sequence_without_output_credit(self): + for word in (0x2010, 0x2014): + with self.subTest(word=word): + self.safety.set_alternative_experience(0) + self.assertEqual(self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, word), 0) + self.safety.init_tests() + if word == 0x2014 and self.safety.set_safety_hooks(CarParams.SafetyModel.allOutput, 0) != 0: + continue + self.assertEqual(self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, word), 0) + self.safety.set_torque_driver(0, 0) + + def primary(counter, torque=0): + return self.packer.make_can_msg_safety('LKAS', 0, { + 'COUNTER': counter, 'TORQUE_REQUEST': torque, 'STEER_REQ': int(torque != 0), + }) + self.assertTrue(self.safety.safety_tx_hook(primary(0))) + rejected = primary(1, 385) + self.assertFalse(self.safety.safety_tx_hook(rejected)) + self.assertFalse(self.safety.safety_tx_hook(primary(1))) + if word == 0x2014: + raw = 385 + 1024 + mirror = bytearray(8) + mirror[2], mirror[3] = raw & 255, ((raw >> 8) & 7) | 8 + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(1, 0x340, dat=bytes(mirror)))) + self.assertTrue(self.safety.safety_tx_hook(primary(2))) + bad = bytearray(primary(3)[0].data[0:16]) + bad[0] ^= 1 + self.assertFalse(self.safety.safety_tx_hook(common.make_msg(0, 0x50, dat=bytes(bad)))) + valid = primary(3) + self.assertTrue(self.safety.safety_tx_hook(valid)) + self.assertFalse(self.safety.safety_tx_hook(valid)) + self.assertFalse(self.safety.safety_tx_hook(rejected)) + + def test_camera_observation_survives_authority_clear_but_not_expiry_or_reset(self): + self.safety.set_alternative_experience(32) + self.assertEqual(self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0x2010), 0) + self.safety.init_tests() + self.safety.set_timer(1_000_000) + camera = self.packer.make_can_msg_safety('CAM_0x2a4', 2, {'COUNTER': 7, 'BYTE3': 0x5A}) + copy = self.packer.make_can_msg_safety('CAM_0x2a4', 0, {'COUNTER': 7, 'BYTE3': 0x5A}) + self.assertTrue(self.safety.safety_rx_hook(camera)) + self.assertTrue(self.safety.safety_tx_hook(copy)) + # Missing PT graph withdraws authority, not the independently validated camera observation. + self.safety.safety_tick() + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.assertTrue(self.safety.safety_tx_hook(copy)) + cancel = self.packer.make_can_msg_safety('CLU11', 1, {'CF_Clu_CruiseSwState': 4}) + self.assertTrue(self.safety.safety_rx_hook(cancel)) + self.safety.aol_set_host_request(0) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.assertTrue(self.safety.safety_tx_hook(copy)) + self.safety.set_timer(1_400_000) + bad = bytearray(camera[0].data[0:24]) + bad[0] ^= 1 + self.assertFalse(self.safety.safety_rx_hook(common.make_msg(2, 0x2A4, dat=bytes(bad)))) + self.safety.set_timer(1_500_001) + self.assertFalse(self.safety.safety_tx_hook(copy)) + self.safety.set_timer(1_600_000) + fresh = self.packer.make_can_msg_safety('CAM_0x2a4', 2, {'COUNTER': 8, 'BYTE3': 0x5A}) + fresh_copy = self.packer.make_can_msg_safety('CAM_0x2a4', 0, {'COUNTER': 8, 'BYTE3': 0x5A}) + self.assertTrue(self.safety.safety_rx_hook(fresh)) + self.assertTrue(self.safety.safety_tx_hook(fresh_copy)) + self.assertEqual(self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0x2010), 0) + self.safety.init_tests() + self.assertFalse(self.safety.safety_tx_hook(fresh_copy)) + + def test_forwarding_matches_actual_output_destination_for_both_experiences(self): + if self.safety.set_safety_hooks(CarParams.SafetyModel.allOutput, 0) != 0: + return + for experience in (0, 32): + self.safety.set_alternative_experience(experience) + self.assertEqual(self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0x2014), 0) + self.safety.init_tests() + for address in (0x340, 0x485): + self.assertEqual(self.safety.safety_fwd_hook(2, address), 0) + self.assertEqual(self.safety.safety_fwd_hook(0, address), 2) + for address in (0x50, 0x2A4): + self.assertEqual(self.safety.safety_fwd_hook(2, address), -1 if experience == 0 else 0) + self.assertEqual(self.safety.safety_fwd_hook(0, address), 2) diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_blended_alpha_closure.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_blended_alpha_closure.py new file mode 100644 index 0000000000..9b176d62bd --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_blended_alpha_closure.py @@ -0,0 +1,109 @@ +"""Experimental mixed longitudinal safety contract regressions.""" +import os +import unittest +from opendbc.safety.tests.test_hyundai import checksum +from opendbc.safety.tests import test_hyundai_blended_alpha as helpers + + +class TestHyundaiBlendedAlphaClosure(unittest.TestCase): + select = helpers.TestHyundaiBlendedAlpha.select + primary = helpers.TestHyundaiBlendedAlpha.primary + mirror = helpers.TestHyundaiBlendedAlpha.mirror + tcs = helpers.TestHyundaiBlendedAlpha.tcs + button = helpers.TestHyundaiBlendedAlpha.button + + def setUp(self): + helpers.TestHyundaiBlendedAlpha.setUp(self) + if os.environ.get('ALPHA_NATIVE_RELEASE') == '1': + self.skipTest('DEBUG behavior; RELEASE admission checked separately') + + def seed_torque(self, torque): + self.safety.set_desired_torque_last(torque) + self.safety.set_rt_torque_last(torque) + self.safety.set_torque_driver(0, 0) + + def test_cancel_release_after_required_rx_loss_stays_disabled(self): + for hda2 in (False, True): + self.select(hda2) + self.assertTrue(self.safety.safety_rx_hook(self.tcs(False, 0))) + self.assertTrue(self.safety.safety_rx_hook(self.button(4, 0))) + self.safety.set_timer(1000001) + self.safety.safety_tick() + self.assertTrue(self.safety.safety_rx_hook(self.tcs(False, 1))) + self.assertTrue(self.safety.safety_rx_hook(self.button(0, 1))) + self.assertFalse(self.safety.get_controls_allowed()) + + def test_cancel_press_with_pedal_already_pressed_never_arms(self): + for hda2 in (False, True): + for pedal in ('brake', 'gas'): + self.select(hda2) + if pedal == 'brake': + packet = self.packer.make_can_msg_safety('TCS13', self.bus, + {'DriverOverride': 2, 'AliveCounterTCS': 0}, fix_checksum=checksum) + else: + self.safety.safety_rx_hook(self.tcs(False, 0)) + packet = self.packer.make_can_msg_safety('EMS16', self.bus, + {'CF_Ems_AclAct': 1, 'AliveCounter': 0}, fix_checksum=checksum) + self.assertTrue(self.safety.safety_rx_hook(packet)) + self.assertTrue(self.safety.safety_rx_hook(self.button(4, 0))) + if pedal == 'brake': + self.assertTrue(self.safety.safety_rx_hook(self.tcs(False, 1))) + else: + packet = self.packer.make_can_msg_safety('EMS16', self.bus, + {'CF_Ems_AclAct': 0, 'AliveCounter': 1}, fix_checksum=checksum) + self.assertTrue(self.safety.safety_rx_hook(packet)) + self.assertTrue(self.safety.safety_rx_hook(self.button(0, 1))) + self.assertFalse(self.safety.get_controls_allowed()) + + def test_mirror_mismatch_consumes_only_that_primary_token(self): + for torque, request in ((2, True), (3, False)): + self.select(True) + self.safety.set_controls_allowed(True) + self.assertTrue(self.safety.safety_tx_hook(self.primary(3, True))) + self.assertFalse(self.safety.safety_tx_hook(self.mirror(torque, request))) + self.assertFalse(self.safety.safety_tx_hook(self.mirror(3, True))) + self.assertTrue(self.safety.safety_tx_hook(self.primary(3, True))) + self.assertTrue(self.safety.safety_tx_hook(self.mirror(3, True))) + + def test_mirror_does_not_double_request_history_or_change_cut_budget(self): + for valid_count in (44, 88, 89): + self.select(True) + self.safety.set_timer(810000) + self.safety.set_controls_allowed(True) + self.seed_torque(30) + for _ in range(valid_count): + self.assertTrue(self.safety.safety_tx_hook(self.primary(30, True))) + self.assertTrue(self.safety.safety_tx_hook(self.mirror(30, True))) + for invalid in range(3): + expected = valid_count >= 89 and invalid < 2 + self.assertEqual(self.safety.safety_tx_hook(self.primary(30, False)), expected) + self.assertEqual(self.safety.safety_tx_hook(self.mirror(30, False)), expected) + + def test_mirror_does_not_relax_primary_driver_rate_or_controls(self): + for torque, allowed, driver in ((4, True, 0), (3, False, 0), (3, True, -300)): + self.select(True) + self.safety.set_controls_allowed(allowed) + self.safety.set_torque_driver(driver, driver) + self.assertFalse(self.safety.safety_tx_hook(self.primary(torque, True))) + self.assertFalse(self.safety.safety_tx_hook(self.mirror(torque, True))) + + def test_mirror_zero_off_and_exact_expiry_boundary(self): + self.select(True) + self.assertTrue(self.safety.safety_tx_hook(self.primary(0, False))) + self.assertTrue(self.safety.safety_tx_hook(self.mirror(0, False))) + for elapsed, allowed in ((10000, True), (10001, False)): + self.select(True) + self.safety.set_controls_allowed(True) + self.assertTrue(self.safety.safety_tx_hook(self.primary(3, True))) + self.safety.set_timer(elapsed) + self.assertEqual(self.safety.safety_tx_hook(self.mirror(3, True)), allowed) + + def test_mirror_relay_and_profile_reinit_fail_closed(self): + self.select(True) + self.safety.set_controls_allowed(True) + self.assertTrue(self.safety.safety_tx_hook(self.primary(3, True))) + self.safety.set_relay_malfunction(True) + self.assertFalse(self.safety.safety_tx_hook(self.mirror(3, True))) + self.select(True) + self.safety.set_controls_allowed(True) + self.assertFalse(self.safety.safety_tx_hook(self.mirror(3, True))) diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_blended_alpha_paired_ack.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_blended_alpha_paired_ack.py new file mode 100644 index 0000000000..2a7debc26e --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_blended_alpha_paired_ack.py @@ -0,0 +1,349 @@ +"""Native acknowledgment and caller contract tests with synthetic transport clocks.""" +from types import SimpleNamespace +import unittest + +from opendbc.car import structs +from opendbc.car.hyundai.blended_longitudinal import BlendedStartup +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.tests.test_palisade_2023 import params +from opendbc.car.hyundai.values import Buttons +from opendbc.safety.tests.libsafety import libsafety_py as lp +from opendbc.safety.tests.test_hyundai import checksum +from openpilot.selfdrive.car.car_events import CarEvents +from openpilot.selfdrive.selfdrived.events import ET +from openpilot.selfdrive.selfdrived.state import StateMachine + + +class AcknowledgedCancelStream: + def frames(self, request, button, brake, gas, omit): + frames = [] + names = set() + drive = next(key for key, value in self.ci.CS.shifter_values.items() if value in ('D', 'drive')) + for parser in self.ci.can_parsers.values(): + for address in parser.addresses: + message = parser.dbc.addr_to_msg[address] + if message.name in ('SCC11', 'SCC12') or message.name == omit: + continue + values = {'COUNTER': self.tick % 16} if 'COUNTER' in message.sigs else {} + if message.name == 'CGW1': + values['CF_Gway_DrvSeatBeltSw'] = 1 + elif message.name == 'LVR12': + values['CF_Lvr_Gear'] = drive + elif message.name == 'CLU15': + values['CF_Clu_Gear'] = drive + elif message.name == 'TCU12': + values['CUR_GR'] = drive + elif message.name == 'TCS13': + values.update(ACCEnable=0, ACC_REQ=int(request), DriverOverride=2 if brake else 0, + AliveCounterTCS=self.tick % 8) + elif message.name == 'CLU11': + values.update(CF_Clu_CruiseSwState=button, CF_Clu_AliveCnt1=self.tick % 16) + elif message.name == 'EMS16': + values.update(CF_Ems_AclAct=int(gas), AliveCounter=self.tick % 4) + elif message.name == 'WHL_SPD11': + values.update(WHL_SPD_AliveCounter_LSB=self.tick % 4, + WHL_SPD_AliveCounter_MSB=(self.tick % 16) >> 2) + elif message.name == 'MDPS12': + values['CR_Mdps_StrColTq'] = 0 + packet = self.packer.make_can_msg(message.name, parser.bus, values) + if address in (0x260, 0x386, 0x394): + packet = checksum(packet) + frames.append(packet) + names.add((message.name, parser.bus)) + # Exact original HDAI native required LDA alternative, independently of + # the host parser's lazy address registration. + if self.pt_bus == 0 and ('BCM_PO_11', 0) not in names: + frames.append(self.packer.make_can_msg('BCM_PO_11', 0, {})) + return frames + + + def __init__(self, topology, *, request=False): + from opendbc.can import CANPacker + from opendbc.car import Bus + from opendbc.car.hyundai.values import DBC + stock = params(topology) + cp = params(topology) + cp.openpilotLongitudinalControl = True + cp.pcmCruise = False + cp.safetyConfigs[0].safetyParam = 0x2014 if topology == 'hdaii' else 0x2004 + self.ci = CarInterface(cp) + self.pt_bus = int(topology == 'hdaii') + self.packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + self.car_events = CarEvents(cp) + self.machine = StateMachine() + self.cc = structs.CarControl() + self.tick = 0 + self.trace = [] + self.snapshot = None + self.counters = {} + self.owner = BlendedStartup(stock, cp, (lambda *args: None, lambda *args: None)) + # Script only the external diagnostic reply; execute the real owner transaction. + self.owner.owner.exchange = lambda *args, **kwargs: True + result = self.owner.owner.begin(*self.owner.callbacks, unpublished=True, admission=True) + assert result.admissible and self.owner.owner.active() + self.owner.source_floor_ns = 999_999_999 # Explicit synthetic boot epoch. + self.safety = lp.libsafety + assert self.safety.set_safety_hooks(cp.safetyConfigs[0].safetyModel.raw, cp.safetyConfigs[0].safetyParam) == 0 + self.safety.init_tests() + try: + lp.ffi.cdef('extern bool safety_rx_checks_invalid;') + except Exception as error: + if 'multiple declarations' not in str(error): + raise + for _ in range(12): + state = self.step(request=request) + assert state.canValid and not state.canTimeout + assert not self.safety.safety_rx_checks_invalid + assert not self.safety.get_controls_allowed() + assert not self.trace[-1]['hostEnabled'] + + def native_snapshot(self, stamp): + cp = self.ci.CP + return (SimpleNamespace(safetyModel=cp.safetyConfigs[0].safetyModel, + safetyParam=cp.safetyConfigs[0].safetyParam, + alternativeExperience=cp.alternativeExperience, + safetyRxChecksInvalid=bool(self.safety.safety_rx_checks_invalid), + controlsAllowed=bool(self.safety.get_controls_allowed()), + faults=[], ignitionLine=True, ignitionCan=False), stamp, stamp + 1) + + def step(self, *, request=False, button=Buttons.NONE, brake=False, gas=False, + omit=None, tcs_first=True, evidence='normal', observation=None, + pulses=(), publish=False): + frames = self.frames(request, button, brake, gas, omit) + frames.sort(key=lambda item: (0 if item[0] == (0x394 if tcs_first else 0x4F1) + else 1 if item[0] == (0x4F1 if tcs_first else 0x394) else 2)) + # Optional true→false transitions within one serialized CAN batch. + for name, values in pulses: + packet = self.packer.make_can_msg(name, self.pt_bus, values) + if packet[0] in (0x260, 0x386, 0x394): + packet = checksum(packet) + # The pulse arrives before the ordinary final snapshot of this source. + position = next(i for i, item in enumerate(frames) if item[0] == packet[0]) + frames.insert(position, packet) + # Counter advances per actual source frame, including multiple samples in + # one batch. Recompute original RX checksum after changing counter bits. + counted = [] + for address, data, bus in frames: + data = bytearray(data) + key = (address, bus) + counter = self.counters.get(key, 0) + if address == 0x260: + data[7] = (data[7] & 0xC0) | ((counter % 4) << 4) + elif address == 0x386: + data[1] = (data[1] & 0x3F) | ((counter % 4) << 6) + data[3] = (data[3] & 0x3F) | (((counter % 16) >> 2) << 6) + data[5] &= 0x3F + data[7] &= 0x3F + elif address == 0x394: + data[1] = (data[1] & 0x1F) | ((counter % 8) << 5) + data[6] &= 0xF0 + elif address == 0x4F1: + data[3] = (data[3] & 0x0F) | ((counter % 16) << 4) + else: + counted.append((address, data, bus)) + continue + self.counters[key] = counter + 1 + packet = (address, data, bus) + counted.append(checksum(packet) if address in (0x260, 0x386, 0x394) else packet) + frames = counted + can_stamp = 1_000_000_000 + self.tick * 10_000_000 + now = can_stamp + 2000 + self.safety.set_timer(self.tick * 10000) + rx = [bool(self.safety.safety_rx_hook(lp.make_CANPacket(address, bus, data))) + for address, data, bus in frames] + self.safety.safety_tick() + previous = self.ci.CS.out + state = self.ci.update([(can_stamp, frames)]) + # Nominal pandaStates cadence is 10Hz. Its producer timestamp is stamped + # before reading Panda state; this synthetic sample observes prior CAN RX. + if self.snapshot is None or self.tick % 10 == 0 or publish: + self.snapshot = self.native_snapshot(can_stamp + 1000) + panda, producer, received = observation if observation is not None else self.snapshot + panda = SimpleNamespace(**vars(panda)) + if evidence == 'wrong_profile': + panda.safetyParam ^= 0x10 + elif evidence == 'future': + producer = now + 1 + elif evidence == 'stale': + producer = received = now - 300_000_001 + matched_clock = 0 < producer <= now and now - producer <= 300_000_000 and 0 < received <= now and now - received <= 300_000_000 + host_enabled = self.trace[-1]['hostEnabled'] if self.trace else False + self.owner.after_state(ci=self.ci, state=state, now_ns=now, + control_enabled=host_enabled, control_current=True, + pandas=[panda], panda_log_ns=producer, panda_recv_ns=received, + panda_current=matched_clock) + events = self.car_events.update(state, previous, self.cc) + enabled, active = self.machine.update(events) + self.cc.enabled = enabled # Result of the actual caller, not a seeded intent. + self.trace.append({'tick': self.tick, 'clockNs': now, 'nativeAllowed': bool(self.safety.get_controls_allowed()), + 'nativeRxInvalid': bool(self.safety.safety_rx_checks_invalid), 'nativeAllRxAccepted': all(rx), + 'pandaProducerNs': producer, 'pandaReceiptNs': received, 'pandaAllowed': panda.controlsAllowed, + 'hostCanValid': bool(state.canValid), 'hostButtonEnable': bool(state.buttonEnable), + 'hostEnableEvent': events.contains(ET.ENABLE), 'hostNoEntry': events.contains(ET.NO_ENTRY), + 'hostEnabled': enabled, 'hostActive': active, + 'hostButtonEvents': [(str(event.type), bool(event.pressed)) for event in state.buttonEvents], + 'gesture': self.owner.enable_gesture, 'credit': self.owner.enable_credit}) + self.tick += 1 + return state + + def wait_ack(self, *, request=False): + for _ in range(10): + state = self.step(request=request) + if self.trace[-1]['hostEnabled']: + return state + raise AssertionError('No acknowledgment within nominal 100ms publication cadence') + + +class TestBlendedPairedAcknowledgment(unittest.TestCase): + def setUp(self): + self.streams = [] + + def stream(self, topology, **kwargs): + stream = AcknowledgedCancelStream(topology, **kwargs) + self.streams.append(stream) + return stream + + + def assert_host_off(self, stream, state): + self.assertFalse(state.buttonEnable) + self.assertFalse(stream.trace[-1]['hostEnabled']) + + def test_delayed_native_ack_and_second_cancel_before_tcs_ack(self): + for topology in ('hdai', 'hdaii'): + stream = self.stream(topology) + stream.step(button=Buttons.CANCEL) + stream.step(button=Buttons.CANCEL) + self.assert_host_off(stream, stream.step()) + self.assertTrue(stream.safety.get_controls_allowed()) + self.assertTrue(stream.wait_ack().buttonEnable) + self.assertFalse(stream.trace[-1]['hostNoEntry']) + self.assert_host_off(stream, stream.step(button=Buttons.CANCEL)) + self.assertFalse(stream.safety.get_controls_allowed()) + self.assert_host_off(stream, stream.step()) + for _ in range(10): + self.assert_host_off(stream, stream.step()) + + def test_native_rejected_first_pair_then_later_physical_cycles(self): + for topology in ('hdai', 'hdaii'): + stream = self.stream(topology) + for _ in range(11): + stream.step(omit='TCS13') + stream.step(button=Buttons.CANCEL, omit='TCS13') + self.assert_host_off(stream, stream.step(omit='TCS13')) + self.assertFalse(stream.safety.get_controls_allowed()) + for _ in range(10): + self.assert_host_off(stream, stream.step()) + stream.step(button=Buttons.CANCEL) + self.assert_host_off(stream, stream.step()) + self.assertTrue(stream.wait_ack().buttonEnable) + self.assert_host_off(stream, stream.step(button=Buttons.CANCEL)) + self.assert_host_off(stream, stream.step()) + self.assertFalse(stream.safety.get_controls_allowed()) + + def test_wrong_profile_stale_and_future_ack_cannot_enable(self): + for topology in ('hdai', 'hdaii'): + for evidence in ('wrong_profile', 'stale', 'future'): + stream = self.stream(topology) + stream.step(button=Buttons.CANCEL) + self.assert_host_off(stream, stream.step()) + self.assertTrue(stream.safety.get_controls_allowed()) + self.assert_host_off(stream, stream.step(evidence=evidence, publish=True)) + for _ in range(10): + self.assert_host_off(stream, stream.step()) + + def test_native_on_cached_before_release_is_not_new_ack(self): + for topology in ('hdai', 'hdaii'): + stream = self.stream(topology) + stream.step(button=Buttons.CANCEL) + stream.step() + stream.wait_ack() + cached_on = stream.snapshot + stream.step(button=Buttons.CANCEL) + stream.step() + stream.step(publish=True) # Fresh real native OFF witness. + stream.step(button=Buttons.CANCEL) + self.assert_host_off(stream, stream.step(observation=cached_on)) + self.assertTrue(stream.safety.get_controls_allowed()) + self.assert_host_off(stream, stream.step(observation=cached_on)) + self.assertTrue(stream.wait_ack().buttonEnable) + + def test_pending_rejected_credit_expires_after_500ms(self): + for topology in ('hdai', 'hdaii'): + stream = self.stream(topology) + for _ in range(11): + stream.step(omit='TCS13') + stream.step(button=Buttons.CANCEL, omit='TCS13') + self.assert_host_off(stream, stream.step(omit='TCS13')) + credit = stream.owner.enable_credit + self.assertIsNotNone(credit) + for _ in range(50): + self.assert_host_off(stream, stream.step()) + self.assertIsNotNone(stream.owner.enable_credit) + self.assert_host_off(stream, stream.step()) + self.assertIsNone(stream.owner.enable_credit) + self.assertFalse(stream.safety.get_controls_allowed()) + + def test_set_and_resume_use_fresh_postrelease_native_ack(self): + for topology in ('hdai', 'hdaii'): + for button in (Buttons.SET_DECEL, Buttons.RES_ACCEL): + stream = self.stream(topology, request=True) + stream.step(request=True, button=button) + self.assert_host_off(stream, stream.step(request=True)) + self.assertTrue(stream.safety.get_controls_allowed()) + self.assertTrue(stream.wait_ack(request=True).buttonEnable) + + def test_tcs_pedal_and_same_batch_pulses_invalidate_cancel(self): + for topology in ('hdai', 'hdaii'): + for inhibit in ('tcs', 'brake', 'gas', 'batch_tcs', 'batch_brake', 'batch_gas'): + stream = self.stream(topology) + stream.step(button=Buttons.CANCEL) + if inhibit.startswith('batch_'): + signal = inhibit[6:] + if signal == 'gas': + pulse = ('EMS16', {'CF_Ems_AclAct': 1, 'AliveCounter': stream.tick % 4}) + else: + pulse = ('TCS13', {'ACC_REQ': int(signal == 'tcs'), + 'DriverOverride': 2 if signal == 'brake' else 0, + 'AliveCounterTCS': stream.tick % 8}) + stream.step(button=Buttons.CANCEL, pulses=(pulse,)) + else: + stream.step(button=Buttons.CANCEL, request=inhibit == 'tcs', brake=inhibit == 'brake', gas=inhibit == 'gas') + stream.step(button=Buttons.CANCEL) + self.assert_host_off(stream, stream.step()) + for _ in range(10): + self.assert_host_off(stream, stream.step()) + self.assertFalse(stream.safety.get_controls_allowed()) + + def test_active_host_same_batch_cancel_press_release_stops_both(self): + for topology in ('hdai', 'hdaii'): + stream = self.stream(topology) + stream.step(button=Buttons.CANCEL) + stream.step() + stream.wait_ack() + self.assertTrue(stream.trace[-1]['hostEnabled']) + pulse = ('CLU11', {'CF_Clu_CruiseSwState': Buttons.CANCEL}) + self.assert_host_off(stream, stream.step(pulses=(pulse,))) + self.assertFalse(stream.safety.get_controls_allowed()) + self.assertEqual(len(stream.trace[-1]['hostButtonEvents']), 2) + + def test_source_loss_recovery_and_owner_init_require_a_new_pair(self): + for topology in ('hdai', 'hdaii'): + stream = self.stream(topology) + stream.step(button=Buttons.CANCEL) + for _ in range(115): + stream.step(button=Buttons.CANCEL, omit='TCS13') + self.assertTrue(stream.trace[-1]['nativeRxInvalid']) + self.assertTrue(any(not sample['hostCanValid'] for sample in stream.trace)) + stream.step(button=Buttons.CANCEL) + self.assert_host_off(stream, stream.step()) + self.assertFalse(stream.safety.get_controls_allowed()) + for _ in range(10): + self.assert_host_off(stream, stream.step()) + stream.step(button=Buttons.CANCEL) + stream.step() + self.assertIsNotNone(stream.owner.enable_credit) + replacement = self.stream(topology) + self.assertIsNone(replacement.owner.enable_credit) + self.assertIsNone(replacement.owner.enable_gesture) + self.assert_host_off(replacement, replacement.step()) + self.assertFalse(replacement.safety.get_controls_allowed()) diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_blended_bus_isolation.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_blended_bus_isolation.py new file mode 100644 index 0000000000..9ace23f8e0 --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_blended_bus_isolation.py @@ -0,0 +1,33 @@ +"""An upstream RX API lift must preserve the blended PT bus boundary.""" +import unittest + +from opendbc.safety.tests import test_hyundai_blended +from opendbc.safety.tests.test_hyundai import checksum + + +class TestHyundaiBlendedBusIsolation(unittest.TestCase): + def test_wrong_bus_cannot_change_driver_inputs_or_vehicle_moving(self): + fixture = test_hyundai_blended.TestHyundaiBlendedStock() + fixture.setUp() + messages = ( + ('EMS16', {'CF_Ems_AclAct': 10, 'AliveCounter': 0}, checksum, + lambda safety: safety.get_gas_pressed_prev()), + ('TCS13', {'DriverOverride': 2, 'AliveCounterTCS': 0}, checksum, + lambda safety: safety.get_brake_pressed_prev()), + ('WHL_SPD11', {'WHL_SPD_FL': 10, 'WHL_SPD_RR': 10}, checksum, + lambda safety: safety.get_vehicle_moving()), + ('MDPS12', {'CR_Mdps_StrColTq': 100}, None, + lambda safety: safety.get_torque_driver_max() != 0), + ) + for name, values, fix, changed in messages: + for wrong_bus in (0, 2): + with self.subTest(name=name, wrong_bus=wrong_bus): + fixture.select(True) # HDA2 blended powertrain is bus 1. + safety = fixture.safety + self.assertFalse(changed(safety)) + packet = fixture.packer.make_can_msg_safety(name, wrong_bus, values, fix_checksum=fix) + safety.safety_rx_hook(packet) + self.assertFalse(changed(safety)) + packet = fixture.packer.make_can_msg_safety(name, fixture.bus, values, fix_checksum=fix) + self.assertTrue(safety.safety_rx_hook(packet)) + self.assertTrue(changed(safety)) diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_canfd.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_canfd.py index d22bd9ac59..b98e78d45d 100755 --- a/opendbc_repo/opendbc/safety/tests/test_hyundai_canfd.py +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_canfd.py @@ -4,6 +4,8 @@ import unittest from opendbc.car.hyundai.values import HyundaiSafetyFlags from opendbc.car.structs import CarParams +from opendbc.safety.tests.common import CANPackerSafety +from opendbc.safety.tests.libsafety import libsafety_py import opendbc.safety.tests.common as common from opendbc.safety.tests.hyundai_common import HyundaiButtonBase, HyundaiLongitudinalBase @@ -29,11 +31,11 @@ class TestHyundaiCanfdBase(HyundaiButtonBase, common.CarSafetyTest, common.Drive STANDSTILL_THRESHOLD = 12 # 0.375 kph FWD_BLACKLISTED_ADDRS = {2: [0x50, 0x2a4]} - MAX_RATE_UP = 2 + MAX_RATE_UP = 10 MAX_RATE_DOWN = 3 - MAX_TORQUE_LOOKUP = [0], [270] + MAX_TORQUE_LOOKUP = [0], [409] - MAX_RT_DELTA = 112 + MAX_RT_DELTA = 375 DRIVER_TORQUE_ALLOWANCE = 250 DRIVER_TORQUE_FACTOR = 2 @@ -181,7 +183,112 @@ class TestHyundaiCanfdLKASteeringAltEV(TestHyundaiCanfdBase): GAS_MSG = ("ACCELERATOR", "ACCELERATOR_PEDAL") -class TestHyundaiCanfdLKASteeringLongEV(HyundaiLongitudinalBase, TestHyundaiCanfdLKASteeringEV): +class HyundaiCanfdLongitudinalMixin: + def setUp(self): + self.safety = libsafety_py.libsafety + self.release = self.safety.set_safety_hooks(CarParams.SafetyModel.allOutput, 0) != 0 + super().setUp() + if self.release: + self.LONGITUDINAL = False + if self.SAFETY_PARAM & HyundaiSafetyFlags.CANFD_LKA_STEER_MSG: + alternate = self.SAFETY_PARAM & HyundaiSafetyFlags.CANFD_LKA_STEER_MSG_ALT + self.STEER_MSG, self.STEER_BUS = ("LKAS_ALT" if alternate else "LKAS"), 0 + steering, camera = (0x110, 0x362) if alternate else (0x50, 0x2A4) + self.TX_MSGS = [[steering, 0], [camera, 0], [0x1CF, 1]] + self.RELAY_MALFUNCTION_ADDRS = {0: (steering, camera)} + self.FWD_BLACKLISTED_ADDRS = {2: [steering, camera]} + else: + self.TX_MSGS = [[0x12A, 0], [0x1E0, 0], [0x1A0, 0], [0x1CF, 2]] + relay = [0x12A, 0x1E0] + if self.SAFETY_PARAM & HyundaiSafetyFlags.CAMERA_SCC: + if self.SAFETY_PARAM & HyundaiSafetyFlags.CCNC: + self.TX_MSGS += [[0x161, 0], [0x162, 0]] + relay += [0x161, 0x162] + else: + self.TX_MSGS += [[0x160, 0]] + self.RELAY_MALFUNCTION_ADDRS = {0: tuple(relay)} + self.FWD_BLACKLISTED_ADDRS = {2: relay} + + def _pcm_status_msg(self, enable): + return TestHyundaiCanfdBase._pcm_status_msg(self, enable) + + def test_enable_control_allowed_from_cruise(self): + if self.release: + HyundaiButtonBase.test_enable_control_allowed_from_cruise(self) + else: + super().test_enable_control_allowed_from_cruise() + + def test_disable_control_allowed_from_cruise(self): + if self.release: + common.CarSafetyTest.test_disable_control_allowed_from_cruise(self) + else: + super().test_disable_control_allowed_from_cruise() + + def test_sampling_cruise_buttons(self): + if self.release: + HyundaiButtonBase.test_sampling_cruise_buttons(self) + else: + super().test_sampling_cruise_buttons() + + def test_cruise_engaged_prev(self): + if self.release: + common.CarSafetyTest.test_cruise_engaged_prev(self) + else: + super().test_cruise_engaged_prev() + + def test_button_sends(self): + if self.release: + if self.SAFETY_PARAM & HyundaiSafetyFlags.CANFD_ALT_BUTTONS: + TestHyundaiCanfdLFASteeringAltButtonsBase.test_button_sends(self) + else: + HyundaiButtonBase.test_button_sends(self) + else: + super().test_button_sends() + + def test_set_resume_buttons(self): + if not self.release: + super().test_set_resume_buttons() + else: + for previous in range(8): + for current in range(8): + self.safety.set_controls_allowed(False) + for button in (0, *([previous] * 10), current): + self._rx(self._button_msg(button)) + self.assertFalse(self.safety.get_controls_allowed(), (previous, current, button)) + self.assertFalse(self._tx(self._accel_msg(1))) + + def test_cancel_button(self): + if not self.release: + super().test_cancel_button() + else: + self._rx(self._pcm_status_msg(False)) + self._rx(self._button_msg(2)) + self._rx(self._pcm_status_msg(True)) + self.assertTrue(self.safety.get_controls_allowed()) + self._rx(self._button_msg(4)) + self.assertTrue(self.safety.get_controls_allowed()) + self.assertFalse(self._tx(self._accel_msg(1))) + self._rx(self._pcm_status_msg(False)) + self.assertFalse(self.safety.get_controls_allowed()) + + def _acc_cancel_msg(self, cancel, accel=0): + values = {"ACCMode": 4 if cancel else 0, "aReqRaw": accel, "aReqValue": accel} + return self.packer.make_can_msg_safety("SCC_CONTROL", self.PT_BUS, values) + + def test_tester_present_allowed(self, ecu_disable=True): + super().test_tester_present_allowed(ecu_disable=ecu_disable and not self.release) + + def test_disabled_ecu_alive(self): + if not self.release: + super().test_disabled_ecu_alive() + else: + self.assertFalse(self.safety.get_relay_malfunction()) + self._rx(self._pcm_status_msg(False)) + self.assertFalse(self.safety.get_relay_malfunction()) + self.assertFalse(self._tx(self._accel_msg(1))) + + +class TestHyundaiCanfdLKASteeringLongEV(HyundaiCanfdLongitudinalMixin, HyundaiLongitudinalBase, TestHyundaiCanfdLKASteeringEV): SAFETY_PARAM = HyundaiSafetyFlags.CANFD_LKA_STEER_MSG | HyundaiSafetyFlags.LONG | HyundaiSafetyFlags.EV_GAS @@ -197,6 +304,13 @@ class TestHyundaiCanfdLKASteeringLongEV(HyundaiLongitudinalBase, TestHyundaiCanf GAS_MSG = ("ACCELERATOR", "ACCELERATOR_PEDAL") STEER_BUS = 1 + def _torque_cmd_msg(self, torque, steer_req=1): + # Match the reached HDA2 LONG sender's neutral status/damping fields. + values = {"LKA_OptUsmSta": 2, "LKA_SysIndReq": 2, "StrTqReqVal": torque, + "LKA_SysWrn": 0, "ActToiSta": steer_req, "LKA_UsmMod": 0, + "LKA_RcgSta": 0, "Damping_Gain": 100} + return self.packer.make_can_msg_safety(self.STEER_MSG, self.STEER_BUS, values) + def _accel_msg(self, accel, aeb_req=False, aeb_decel=0): values = { "aReqRaw": accel, @@ -205,8 +319,16 @@ class TestHyundaiCanfdLKASteeringLongEV(HyundaiLongitudinalBase, TestHyundaiCanf return self.packer.make_can_msg_safety("SCC_CONTROL", 1, values) +class TestHyundaiCanfdLKASteeringAltLongEV(TestHyundaiCanfdLKASteeringLongEV): + SAFETY_PARAM = TestHyundaiCanfdLKASteeringLongEV.SAFETY_PARAM | HyundaiSafetyFlags.CANFD_LKA_STEER_MSG_ALT + TX_MSGS = [[0x110, 0], [0x1CF, 1], [0x362, 0], [0x51, 0], [0x730, 1], [0x12a, 1], [0x160, 1], + [0x1e0, 1], [0x1a0, 1], [0x1ea, 1], [0x200, 1], [0x345, 1], [0x1da, 1]] + RELAY_MALFUNCTION_ADDRS = {0: (0x110, 0x362), 1: (0x1a0,)} + FWD_BLACKLISTED_ADDRS = {2: [0x110, 0x362]} + + # Tests longitudinal for ICE, hybrid, EV cars with LFA steering -class TestHyundaiCanfdLFASteeringLongBase(HyundaiLongitudinalBase, TestHyundaiCanfdLFASteeringBase): +class TestHyundaiCanfdLFASteeringLongBase(HyundaiCanfdLongitudinalMixin, HyundaiLongitudinalBase, TestHyundaiCanfdLFASteeringBase): FWD_BLACKLISTED_ADDRS = {2: [0x12a, 0x1e0, 0x1a0, 0x160]} @@ -253,7 +375,76 @@ class TestHyundaiCanfdLFASteeringLongAltButtons(TestHyundaiCanfdLFASteeringLongB def test_acc_cancel(self): # Alt buttons does not use SCC_CONTROL to cancel if longitudinal - pass + if self.release: + TestHyundaiCanfdLFASteeringAltButtonsBase.test_acc_cancel(self) + + +class TestHyundaiCanfdCCNC(TestHyundaiCanfdLFASteeringBase): + """Ordinary non-LKA CCNC replaces 0x160/0x1e0 with paired camera display frames.""" + + TX_MSGS = [[0x12A, 0], [0x1A0, 0], [0x1CF, 2], [0x161, 0], [0x162, 0]] + RELAY_MALFUNCTION_ADDRS = {0: (0x12A, 0x161, 0x162, 0x1E0)} + FWD_BLACKLISTED_ADDRS = {2: [0x12A, 0x161, 0x162, 0x1E0]} + STEER_MSG = "LFA" + GAS_MSG = ("ACCELERATOR_BRAKE_ALT", "ACCELERATOR_PEDAL_PRESSED") + SAFETY_PARAM = HyundaiSafetyFlags.CAMERA_SCC | HyundaiSafetyFlags.CCNC + + def test_only_paired_ccnc_display_ids_have_permission(self): + for addr, name in ((0x161, "CCNC_0x161"), (0x162, "CCNC_0x162")): + with self.subTest(addr=hex(addr)): + self.assertTrue(self._tx(self.packer.make_can_msg_safety(name, 0, {}))) + for bus in (1, 2): + self.assertFalse(self._tx(self.packer.make_can_msg_safety(name, bus, {}))) + self.assertFalse(self._tx(common.make_msg(0, addr, 8))) + for addr, size, bus in ((0x160, 16, 0), (0x7C4, 8, 2), (0xEA, 24, 2)): + self.assertFalse(self._tx(common.make_msg(bus, addr, size))) + + def test_ccnc_flag_without_non_lka_camera_topology_grants_no_display_tx(self): + for param in (HyundaiSafetyFlags.CCNC, + HyundaiSafetyFlags.CCNC | HyundaiSafetyFlags.CAMERA_SCC | HyundaiSafetyFlags.CANFD_LKA_STEER_MSG): + with self.subTest(param=param): + self.safety.set_safety_hooks(CarParams.SafetyModel.hyundaiCanfd, param) + self.safety.init_tests() + for name in ("CCNC_0x161", "CCNC_0x162"): + self.assertFalse(self._tx(self.packer.make_can_msg_safety(name, 0, {}))) + + +class TestHyundaiCanfdCCNCLong(TestHyundaiCanfdLFASteeringLongBase): + TX_MSGS = TestHyundaiCanfdCCNC.TX_MSGS + RELAY_MALFUNCTION_ADDRS = {0: (0x12A, 0x1A0, 0x161, 0x162, 0x1E0)} + FWD_BLACKLISTED_ADDRS = {2: [0x12A, 0x1A0, 0x161, 0x162, 0x1E0]} + STEER_MSG = "LFA" + GAS_MSG = ("ACCELERATOR_BRAKE_ALT", "ACCELERATOR_PEDAL_PRESSED") + EXTRA_SAFETY_PARAM = HyundaiSafetyFlags.CAMERA_SCC | HyundaiSafetyFlags.CCNC + SAFETY_PARAM = HyundaiSafetyFlags.LONG | EXTRA_SAFETY_PARAM + + def test_tester_present_allowed(self, ecu_disable=False): + super().test_tester_present_allowed(ecu_disable=False) + + def test_only_paired_ccnc_display_ids_have_permission(self): + TestHyundaiCanfdCCNC.test_only_paired_ccnc_display_ids_have_permission(self) + + +class TestHyundaiCanfdCCNCAltButtons(TestHyundaiCanfdCCNC): + def setUp(self): + self.packer = CANPackerSafety("hyundai_canfd_generated") + self.safety = libsafety_py.libsafety + self.safety.set_safety_hooks(CarParams.SafetyModel.hyundaiCanfd, self.SAFETY_PARAM | HyundaiSafetyFlags.CANFD_ALT_BUTTONS) + self.safety.init_tests() + + _button_msg = TestHyundaiCanfdLFASteeringAltButtonsBase._button_msg + _acc_cancel_msg = TestHyundaiCanfdLFASteeringAltButtonsBase._acc_cancel_msg + test_button_sends = TestHyundaiCanfdLFASteeringAltButtonsBase.test_button_sends + test_acc_cancel = TestHyundaiCanfdLFASteeringAltButtonsBase.test_acc_cancel + + +class TestHyundaiCanfdCCNCLongAltButtons(TestHyundaiCanfdCCNCLong): + def setUp(self): + self.SAFETY_PARAM |= HyundaiSafetyFlags.CANFD_ALT_BUTTONS | HyundaiSafetyFlags.LONG + super().setUp() + + _button_msg = TestHyundaiCanfdLFASteeringAltButtonsBase._button_msg + test_button_sends = TestHyundaiCanfdLFASteeringAltButtonsBase.test_button_sends if __name__ == "__main__": diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_canfd_carnival.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_canfd_carnival.py new file mode 100644 index 0000000000..047099c534 --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_canfd_carnival.py @@ -0,0 +1,96 @@ +import unittest + +from opendbc.can import CANPacker +from opendbc.car import Bus, structs +from opendbc.car.hyundai.carcontroller import CarController +from opendbc.car.hyundai.carstate import CarState +from opendbc.car.hyundai.hyundaicanfd import CanBus, hkg_can_fd_checksum +from opendbc.car.hyundai.tests.test_ccnc_six import params +from opendbc.car.hyundai.values import CAR, DBC, HyundaiFlags, HyundaiSafetyFlags +from opendbc.car.structs import CarParams +from opendbc.safety.tests.libsafety import libsafety_py + + +class TestCarnivalAltResumeNative(unittest.TestCase): + def test_raw_long_flag_never_enables_stock_resume(self): + safety = libsafety_py.libsafety + cp = params(CAR.KIA_CARNIVAL_2025, alt_buttons=True) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + raw_param = cp.safetyConfigs[-1].safetyParam | HyundaiSafetyFlags.LONG + safety.set_safety_hooks(CarParams.SafetyModel.hyundaiCanfd, raw_param) + safety.init_tests() + address, raw, _ = packer.make_can_msg("CRUISE_BUTTONS_ALT", 0, {"COUNTER": 1, "SET_ME_1": 1}) + source = bytearray(raw) + source[0:2] = hkg_can_fd_checksum(address, None, source).to_bytes(2, "little") + self.assertTrue(safety.safety_rx_hook(libsafety_py.make_CANPacket(address, 0, source))) + safety.set_controls_allowed(True) + from opendbc.car.hyundai.hyundaicanfd import create_carnival_alt_resume + candidate = create_carnival_alt_resume(packer, cp, CanBus(cp), {"COUNTER": 1, "SET_ME_1": 1}) + self.assertFalse(safety.safety_tx_hook(libsafety_py.make_CANPacket(candidate[0], candidate[2], candidate[1]))) + + def test_packed_host_resume_guard(self): + # Use the safety library selected by the vehicle runner (DEBUG or RELEASE). + from opendbc.car.hyundai.tests.test_ccnc_six import TestCcncSix + + TestCcncSix()._check_packed_carnival_resume(libsafety_py.libsafety) + + def test_controller_frame_requires_real_stock_arming(self): + safety = libsafety_py.libsafety + for car in (CAR.KIA_CARNIVAL_2025, CAR.KIA_CARNIVAL_HEV_4TH_GEN): + for hda2, alt_lka in ((False, False), (True, False), (True, True)): + with self.subTest(car=car, hda2=hda2, alt_lka=alt_lka): + self._check_controller_frame(safety, car, hda2, alt_lka) + + def _check_controller_frame(self, safety, car, hda2, alt_lka): + cp = params(car, hda2=hda2, alt_buttons=True, alt_lka=alt_lka) + can = CanBus(cp) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + state = CarState(cp) + parsers = state.get_can_parsers(cp) + safety.set_safety_hooks(CarParams.SafetyModel.hyundaiCanfd, cp.safetyConfigs[-1].safetyParam) + safety.init_tests() + + def frame(name, bus, values): + address, raw, bus = packer.make_can_msg(name, bus, values) + data = bytearray(raw) + if len(data) in (16, 24, 32): + data[0:2] = hkg_can_fd_checksum(address, None, data).to_bytes(2, "little") + return address, bytes(data), bus + + def receive(name, bus, values): + address, data, bus = frame(name, bus, values) + return safety.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, data)) + + # Populate the ordinary safety RX checks, including the selected fuel path. + fuel = "ACCELERATOR_ALT" if cp.flags & HyundaiFlags.HYBRID else "ACCELERATOR_BRAKE_ALT" + for name in (fuel, "TCS", "WHEEL_SPEEDS", "MDPS"): + self.assertTrue(receive(name, can.ECAN, {}), name) + self.assertTrue(receive("SCC_CONTROL", can.ECAN if hda2 else can.CAM, {"ACCMode": 0})) + self.assertTrue(receive("CRUISE_BUTTONS_ALT", can.ECAN, {"COUNTER": 20, "CRUISE_BUTTONS": 2})) + self.assertTrue(receive("SCC_CONTROL", can.ECAN if hda2 else can.CAM, {"ACCMode": 1, "COUNTER": 1})) + self.assertTrue(safety.get_controls_allowed()) + + stock = frame("CRUISE_BUTTONS_ALT", can.ECAN, {"COUNTER": 21, "SET_ME_1": 1, "CRUISE_BUTTONS": 0}) + self.assertTrue(safety.safety_rx_hook(libsafety_py.make_CANPacket(stock[0], stock[2], stock[1]))) + parsers[Bus.pt].update((1_000_000_000, [stock])) + state.out = state.update(parsers) + control = structs.CarControl() + control.cruiseControl.resume = True + controller = CarController(DBC[cp.carFingerprint], cp) + controller.frame = 26 + _, emitted = controller.update(control.as_reader(), state, 1_050_000_000) + resume = [message for message in emitted if message[0] == 0x1AA] + self.assertEqual(len(resume), 1) + address, data, bus = resume[0] + safety.safety_tick() + self.assertTrue(safety.get_controls_allowed()) + self.assertFalse(safety.safety_tx_hook(libsafety_py.make_CANPacket(address, can.ECAN if not hda2 else can.CAM, data))) + self.assertFalse(safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data[:12]))) + corrupt = bytearray(data) + corrupt[0] ^= 1 + self.assertFalse(safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, corrupt))) + self.assertTrue(safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data))) + self.assertFalse(safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data))) + self.assertTrue(receive("SCC_CONTROL", can.ECAN if hda2 else can.CAM, {"ACCMode": 0, "COUNTER": 2})) + self.assertFalse(safety.get_controls_allowed()) + self.assertFalse(safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data))) diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_canfd_torque_envelope.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_canfd_torque_envelope.py new file mode 100644 index 0000000000..d90a49e1d1 --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_canfd_torque_envelope.py @@ -0,0 +1,133 @@ +import unittest +from types import SimpleNamespace + +from opendbc.can import CANPacker +from opendbc.car import structs +from opendbc.car.hyundai.hyundaicanfd import create_steering_messages +from opendbc.car.hyundai.values import CAR, HyundaiFlags, CarControllerParams +from opendbc.car.lateral import apply_driver_steer_torque_limits +from opendbc.safety.tests.libsafety import libsafety_py + + +class TestHyundaiCanfdTorqueEnvelope(unittest.TestCase): + def setUp(self): + self.safety = libsafety_py.libsafety + self.release = self.safety.set_safety_hooks(structs.CarParams.SafetyModel.allOutput, 0) != 0 + self.packer = CANPacker('hyundai_canfd_generated') + + def tearDown(self): + self.safety.init_tests() + self.safety.set_alternative_experience(0) + self.safety.set_safety_hooks(structs.CarParams.SafetyModel.noOutput, 0) + + def mode(self, word): + self.safety.set_alternative_experience(0) + self.assertEqual(self.safety.set_safety_hooks(structs.CarParams.SafetyModel.hyundaiCanfd, word), 0) + self.safety.init_tests() + self.safety.set_timer(0) + self.word = word + + def tx(self, frame): + return bool(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(frame[0], frame[2], frame[1]))) + + def frames(self, torque): + flags = HyundaiFlags.CANFD | (HyundaiFlags.CANFD_LKA_STEER_MSG if self.word & 16 else 0) + if self.word & 128: + flags |= HyundaiFlags.CANFD_LKA_STEER_MSG_ALT + cp = SimpleNamespace(carFingerprint=CAR.HYUNDAI_TUCSON_4TH_GEN, flags=int(flags), + openpilotLongitudinalControl=bool(self.word & 4) and not self.release) + return create_steering_messages(self.packer, cp, SimpleNamespace(ECAN=1 if self.word & 16 else 0, ACAN=0), True, True, torque) + + def previous(self, torque, driver=0): + self.safety.set_desired_torque_last(torque) + self.safety.set_rt_torque_last(torque) + for counter in range(6): + frame = self.packer.make_can_msg('MDPS', 1 if self.word & 16 else 0, + {'COUNTER': counter, 'MDPS_StrTqSnsrVal': driver, 'MDPS_OutTqVal': 0}) + self.assertTrue(self.safety.safety_rx_hook(libsafety_py.make_CANPacket(frame[0], frame[2], frame[1]))) + self.safety.set_controls_allowed(True) + + def test_host_unwind_speed_transition_and_driver_override(self): + for word in (0x11, 0x91, 9): + for vehicle in (CAR.KIA_EV6, CAR.KIA_CARNIVAL_HEV_4TH_GEN): + for sign in (-1, 1): + for speed, expected in ((14.999, 401 if vehicle == CAR.KIA_EV6 else 406), (15., 406)): + with self.subTest(word=hex(word), vehicle=vehicle, sign=sign, speed=speed): + self.mode(word) + self.previous(sign * 409, -sign * 500) + cp = SimpleNamespace(flags=int(HyundaiFlags.CANFD), carFingerprint=vehicle) + output = apply_driver_steer_torque_limits(0, sign * 409, -sign * 500, CarControllerParams(cp, speed)) + self.assertEqual(output, sign * expected) + self.assertTrue(self.tx(self.frames(output)[0])) + if speed < 15 and vehicle == CAR.KIA_EV6: + self.safety.set_timer(10_000) + next_output = apply_driver_steer_torque_limits(0, output, -sign * 500, CarControllerParams(cp, 15.)) + self.assertEqual(next_output, sign * 398) + self.assertTrue(self.tx(self.frames(next_output)[0])) + self.previous(sign * 409, -sign * 500) + self.assertFalse(self.tx(self.frames(sign * 407)[0])) + self.previous(sign) + crossing = apply_driver_steer_torque_limits(-sign * 409, sign, 0, CarControllerParams(cp, speed)) + self.assertEqual(crossing, sign * (1 - (8 if speed < 15 and vehicle == CAR.KIA_EV6 else 3))) + self.assertTrue(self.tx(self.frames(crossing)[0])) + + def test_real_clock_ramp_and_absolute_cap(self): + for word in (0x11, 0x91, 9): + for sign in (-1, 1): + with self.subTest(word=hex(word), sign=sign): + self.mode(word) + self.previous(0) + self.assertFalse(self.tx(self.frames(sign * 409)[0])) + self.previous(0) + for tick in range(42): + self.safety.set_timer(tick * 10_000) + self.assertTrue(self.tx(self.frames(sign * min(tick * 10, 409))[0])) + self.previous(sign * 409) + self.assertFalse(self.tx(self.frames(sign * 410)[0])) + self.previous(0) + self.assertFalse(self.tx(self.frames(sign * 11)[0])) + for clock in (250_000, 250_001): + self.mode(word) + self.previous(sign * 375) + self.safety.set_rt_torque_last(0) + self.safety.set_timer(clock) + self.assertTrue(self.tx(self.frames(sign * 375)[0])) + self.assertEqual(self.tx(self.frames(sign * 376)[0]), clock > 250_000) + + def test_all_hda2_long_secondary_frames_require_single_limited_primary(self): + for word in (0x14, 0x15, 0x16, 0x34, 0x35, 0x36, 0x94, 0x95, 0x96, 0xB4, 0xB5, 0xB6): + for sign in (-1, 1): + for fault in ('unpaired', 'different', 'expired', 'replacement', 'checksum', 'body'): + with self.subTest(word=hex(word), sign=sign, fault=fault): + self.mode(word) + self.previous(sign * 409) + frames = self.frames(sign * 409) + if self.release: + self.assertEqual(len(frames), 1) + self.assertTrue(self.tx(frames[0])) + lfa = self.packer.make_can_msg('LFA', 1, {'StrTqReqVal': sign * 409, 'ActToiSta': 1}) + self.assertFalse(self.tx(lfa)) + continue + source, mirror = frames + self.assertFalse(self.tx(mirror)) + self.previous(sign * 409) + self.assertTrue(self.tx(source)) + if fault == 'unpaired': + self.assertTrue(self.tx(mirror)) + elif fault == 'different': + self.assertFalse(self.tx(self.frames(sign * 384)[1])) + elif fault == 'expired': + self.safety.set_timer(10_001) + self.assertFalse(self.tx(mirror)) + elif fault == 'replacement': + self.assertFalse(self.tx(self.frames(sign * 410)[0])) + elif fault == 'body': + name = 'LKAS_ALT' if word & 128 else 'LKAS' + forged = self.packer.make_can_msg(name, 0, {'LKA_OptUsmSta': 2, 'LKA_SysIndReq': 2, + 'StrTqReqVal': sign * 409, 'ActToiSta': 1, 'Damping_Gain': 99}) + self.assertFalse(self.tx(forged)) + else: + data = bytearray(mirror[1]) + data[0] ^= 1 + self.assertFalse(self.tx((mirror[0], bytes(data), mirror[2]))) + self.assertFalse(self.tx(mirror)) diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_ccnc_angle_two.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_ccnc_angle_two.py new file mode 100644 index 0000000000..e5fa4a4caa --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_ccnc_angle_two.py @@ -0,0 +1,407 @@ +import unittest + +from opendbc.can import CANPacker, CANParser +from opendbc.car import Bus, gen_empty_fingerprint, structs +from opendbc.car.fingerprints import all_legacy_fingerprint_cars +from opendbc.car.fw_versions import match_fw_to_car +from opendbc.car.hyundai.carcontroller import CarController +from opendbc.car.hyundai.carstate import CarState +from opendbc.car.hyundai.fingerprints import FW_VERSIONS +from opendbc.car.hyundai.hyundaicanfd import CanBus, hkg_can_fd_checksum +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.values import (CANFD_ANGLE_CCNC_MODEL_BANK_BIT, CANFD_ANGLE_MODEL_BITS, + CANFD_ANGLE_OBSERVED_ADAS_BIT, CAR, DBC, HyundaiFlags, + HyundaiSafetyFlags) +from opendbc.car.lateral import get_max_angle_delta_vm, get_max_angle_vm +from opendbc.safety.tests.libsafety import libsafety_py + + +CARS = (CAR.HYUNDAI_SANTA_FE_HEV_5TH_GEN, CAR.KIA_SPORTAGE_2026) +CAMERA_TOPOLOGIES = ("lfa", "lfa_alt") +STEER_IDS = (0x50, 0x110, 0x12A, 0xCB) +DISPLAY_IDS = (0x161, 0x162) + + +def params(car, topology="lfa", *, hybrid=False, adas=False, release=False, alpha=False): + fp = gen_empty_fingerprint() + ecan = 1 if topology.startswith("lka") else 0 + if topology == "lka": + fp[2][0x50] = 16 + elif topology == "lka_alt": + fp[2][0x110] = 32 + if topology == "lfa_alt": + fp[ecan][0x1AA] = 16 + else: + fp[ecan][0x1CF] = 8 + if hybrid: + fp[ecan][0xFA] = 8 + if adas: + fp[2][0xCB] = 24 + return CarInterface.get_params(car, fp, [], alpha, release, False) + + +def source_frames(cp, topology, *, speed=36.0, measured=0.0, gear=5, brake=False, gas=False, + cruise=True, wrong_fuel=False): + can = CanBus(cp) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + hybrid = bool(cp.flags & HyundaiFlags.HYBRID) + fuel = "ACCELERATOR_ALT" if hybrid else "ACCELERATOR_BRAKE_ALT" + if wrong_fuel: + fuel = "ACCELERATOR_BRAKE_ALT" if hybrid else "ACCELERATOR_ALT" + gas_values = {"ACCELERATOR_PEDAL": 4.0} if gas and fuel == "ACCELERATOR_ALT" else \ + {"ACCELERATOR_PEDAL_PRESSED": 1} if gas and fuel == "ACCELERATOR_BRAKE_ALT" else {} + button = "CRUISE_BUTTONS_ALT" if topology == "lfa_alt" else "CRUISE_BUTTONS" + scc_bus = can.ECAN if topology.startswith("lka") else can.CAM + entries = [(fuel, can.ECAN, gas_values), + ("TCS", can.ECAN, {"DriverBraking": int(brake)}), + ("WHEEL_SPEEDS", can.ECAN, {key: speed for key in + ("WHL_SpdFLVal", "WHL_SpdFRVal", "WHL_SpdRLVal", "WHL_SpdRRVal")}), + ("MDPS", can.ECAN, {"MDPS_PaStrAnglVal": measured}), + ("STEERING_SENSORS", can.ECAN, {"STEERING_ANGLE": measured}), + ("DOORS_SEATBELTS", can.ECAN, {"DRIVER_SEATBELT": 1}), + ("BLINKERS", can.ECAN, {}), + ("GEAR_ALT_2", can.ECAN, {"GEAR": gear}), + (button, can.ECAN, {"CRUISE_BUTTONS": 2}), + ("SCC_CONTROL", scc_bus, {"ACCMode": 0}), + ("SCC_CONTROL", scc_bus, {"ACCMode": int(cruise)})] + if topology.startswith("lka"): + entries.append(("LKAS_ALT" if topology == "lka_alt" else "LKAS", can.CAM, {})) + return packer, [packer.make_can_msg(name, bus, values) for name, bus, values in entries] + + +class TestHyundaiCcncAngleTwo(unittest.TestCase): + @staticmethod + def packet(frame): + return libsafety_py.make_CANPacket(frame[0], frame[2], frame[1]) + + def mode(self, cp): + safety = libsafety_py.libsafety + self.assertEqual(safety.set_safety_hooks(structs.CarParams.SafetyModel.hyundaiCanfd, + cp.safetyConfigs[-1].safetyParam), 0) + safety.init_tests() + safety.set_timer(1_000_000) + return safety + + def joined(self, car, topology, *, hybrid=False, adas=False, speed=36.0, measured=0.0, gear=5, + brake=False, gas=False, cruise=True, desired=1.0, display=False): + cp = params(car, topology, hybrid=hybrid, adas=adas, release=True, alpha=True) + can = CanBus(cp) + state = CarState(cp) + parsers = state.get_can_parsers(cp) + packer, frames = source_frames(cp, topology, speed=speed, measured=measured, gear=gear, + brake=brake, gas=gas, cruise=cruise) + safety = self.mode(cp) + for frame in frames: + self.assertTrue(safety.safety_rx_hook(self.packet(frame)), (car, topology, hex(frame[0]))) + for parser in parsers.values(): + parser.update((990_000_000, [frame for frame in frames if frame[2] == parser.bus])) + state.out = state.update(parsers) + self.assertTrue(parsers[Bus.pt].can_valid, (car, topology, hybrid)) + self.assertEqual(state.out.cruiseState.enabled, cruise) + control = structs.CarControl() + control.enabled = control.latActive = True + control.actuators.steeringAngleDeg = desired + controller = CarController(DBC[car], cp) + # Establish the display-counter baseline before the new camera pair. + controller.update(control.as_reader(), state, 990_000_000) + controller.frame = 5 + if display: + self.assertIn(topology, CAMERA_TOPOLOGIES) + pair = [packer.make_can_msg("CCNC_0x161", can.CAM, {"COUNTER": 7, "BACKGROUND": 3}), + packer.make_can_msg("CCNC_0x162", can.CAM, {"COUNTER": 9, "SPEEDLIMIT": 60})] + parsers[Bus.cam].update((1_000_000_000, pair)) + state.out = state.update(parsers) + _, sends = controller.update(control.as_reader(), state, 1_000_000_000) + steering = [frame for frame in sends if frame[0] in STEER_IDS] + displays = [frame for frame in sends if frame[0] in DISPLAY_IDS] + return cp, state, safety, control, controller, steering, displays, packer + + def test_sixteen_observed_profiles_from_parser_to_native(self): + cases = 0 + for car in CARS: + topologies = ("lka", "lka_alt", "lfa", "lfa_alt") if car == CAR.HYUNDAI_SANTA_FE_HEV_5TH_GEN else CAMERA_TOPOLOGIES + for topology in topologies: + for hybrid in (False, True) if car == CAR.HYUNDAI_SANTA_FE_HEV_5TH_GEN else (False,): + for adas in (False, True) if topology in CAMERA_TOPOLOGIES else (False,): + with self.subTest(car=car, topology=topology, hybrid=hybrid, adas=adas): + cp, state, safety, _, controller, steering, displays, _ = self.joined(car, topology, hybrid=hybrid, + adas=adas, display=topology in CAMERA_TOPOLOGIES) + cases += 1 + self.assertEqual(cp.safetyConfigs[-1].safetyParam & CANFD_ANGLE_CCNC_MODEL_BANK_BIT, + CANFD_ANGLE_CCNC_MODEL_BANK_BIT) + self.assertEqual(cp.safetyConfigs[-1].safetyParam & (1024 | 2048 | 4096 | 8192), CANFD_ANGLE_MODEL_BITS[car.name]) + self.assertEqual(bool(cp.safetyConfigs[-1].safetyParam & CANFD_ANGLE_OBSERVED_ADAS_BIT), adas) + self.assertEqual(bool(cp.flags & HyundaiFlags.HYBRID), hybrid) + self.assertEqual((cp.alphaLongitudinalAvailable, cp.openpilotLongitudinalControl, cp.pcmCruise), (False, False, True)) + self.assertEqual(state.out.gearShifter, structs.CarState.GearShifter.drive) + self.assertTrue(safety.get_controls_allowed()) + self.assertEqual(steering[0][0], 0x50 if topology == "lka" else 0x110 if topology == "lka_alt" else 0x12A) + self.assertGreater(abs(controller.apply_angle_last), 0) + if adas: + self.assertEqual([frame[0] for frame in steering], [0x12A, 0xCB]) + self.assertEqual((steering[0][1][9] >> 4) & 3, 1) + else: + self.assertNotEqual((steering[0][1][11] << 6) | (steering[0][1][10] >> 2), 0) + for frame in steering + displays: + self.assertEqual(int.from_bytes(frame[1][:2], "little"), + hkg_can_fd_checksum(frame[0], None, bytearray(frame[1]))) + self.assertTrue(safety.safety_tx_hook(self.packet(frame)), hex(frame[0])) + self.assertEqual([f[0] for f in displays], list(DISPLAY_IDS) if topology in CAMERA_TOPOLOGIES else []) + self.assertFalse(any(f[0] in (0x160, 0x7C4, 0xEA, 0x730) for f in steering + displays)) + self.assertEqual(cases, 16) + + def test_stock_ownership_topology_and_joint_firmware_discovery(self): + for car in CARS: + for release in (False, True): + cp = params(car, "lfa", release=release, alpha=True) + self.assertFalse(cp.alphaLongitudinalAvailable) + self.assertFalse(cp.openpilotLongitudinalControl) + self.assertTrue(cp.pcmCruise) + self.assertFalse(cp.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.LONG) + for topology in ("lka", "lka_alt"): + cp = params(CAR.KIA_SPORTAGE_2026, topology) + self.assertTrue(cp.dashcamOnly) + self.assertEqual(cp.safetyConfigs[-1].safetyParam & CANFD_ANGLE_CCNC_MODEL_BANK_BIT, CANFD_ANGLE_CCNC_MODEL_BANK_BIT) + self.assertTrue(params(CAR.KIA_SPORTAGE_2026, "lfa", hybrid=True).dashcamOnly) + self.assertTrue(set(CARS).issubset(FW_VERSIONS)) + self.assertTrue(set(CARS).isdisjoint(set(all_legacy_fingerprint_cars()))) + frozen = { + CAR.HYUNDAI_SANTA_FE_HEV_5TH_GEN: ( + b'\xf1\x00MX5HMFC AT CAN LHD 1.00 1.07 99211-P6000 231218', + b'\xf1\x00MX5_ RDR ----- 1.00 1.01 99110-P6000 ', + ), + CAR.KIA_SPORTAGE_2026: ( + b'\xf1\x00NQ51.011.021.012551000HKP_NQ524_50509099211P1110', + b'\xf1\x00NQ5__ 1.00 1.04 99110P1100 ', + ), + } + sibling_radar = b'\xf1\x00NQ5__ 1.00 1.04 99110CH100 ' + for car, (camera, radar) in frozen.items(): + for observations in ((camera, radar), (camera,), (radar,), (camera, sibling_radar)): + fw = [structs.CarParams.CarFw(ecu=structs.CarParams.Ecu.fwdCamera if version == camera else structs.CarParams.Ecu.fwdRadar, + address=0x7C4 if version == camera else 0x7D0, subAddress=0, + fwVersion=version, brand="hyundai") for version in observations] + _, matches = match_fw_to_car(fw, "", allow_fuzzy=False, log=False) + expected = {car} if observations == (camera, radar) else set() + self.assertEqual(set(CARS) & matches, expected, (car, observations, matches)) + _, production_matches = match_fw_to_car(fw, "", log=False) + self.assertEqual(set(CARS) & production_matches, expected, (car, observations, production_matches)) + + def test_gear_brake_gas_and_missing_source_neutralize(self): + for car in CARS: + for gear in (0, 6, 7, 1, 5): + for direction in (-1, 1): + cp, state, safety, _, _, steering, _, _ = self.joined(car, "lfa", gear=gear, desired=direction * 8.0) + self.assertEqual((steering[0][1][9] >> 4) & 3, 2 if gear == 5 else 1) + self.assertTrue(safety.safety_tx_hook(self.packet(steering[0]))) + if gear != 5: + self.assertNotEqual(state.out.gearShifter, structs.CarState.GearShifter.drive) + for brake, gas, cruise in ((True, False, True), (False, True, True), (False, False, False)): + _, _, safety, _, _, steering, _, _ = self.joined(car, "lfa_alt", brake=brake, gas=gas, cruise=cruise) + self.assertEqual((steering[0][1][9] >> 4) & 3, 1) + self.assertTrue(safety.safety_tx_hook(self.packet(steering[0]))) + for hybrid in (False, True) if car == CAR.HYUNDAI_SANTA_FE_HEV_5TH_GEN else (False,): + cp = params(car, "lfa", hybrid=hybrid) + safety = self.mode(cp) + _, frames = source_frames(cp, "lfa", wrong_fuel=True) + for frame in frames: + self.assertTrue(safety.safety_rx_hook(self.packet(frame))) + state = CarState(cp) + parsers = state.get_can_parsers(cp) + for parser in parsers.values(): + parser.update((1_000_000_000, [frame for frame in frames if frame[2] == parser.bus])) + self.assertFalse(parsers[Bus.pt].can_valid) + safety.safety_tick() + self.assertFalse(safety.safety_config_valid()) + self.assertFalse(safety.get_controls_allowed()) + + def test_source_bus_crc_counter_and_health(self): + for car in CARS: + cp = params(car, "lfa") + _, frames = source_frames(cp, "lfa") + scc = [frame for frame in frames if frame[0] == 0x1A0][-1] + for variant in ("wrong_bus", "bad_crc", "replay", "stale"): + with self.subTest(car=car, variant=variant): + safety = self.mode(cp) + for frame in frames: + if variant == "wrong_bus" and frame[0] == 0x1A0: + frame = (frame[0], frame[1], 0 if frame[2] == 2 else 2) + self.assertTrue(safety.safety_rx_hook(self.packet(frame))) + if variant == "bad_crc": + corrupt = bytearray(scc[1]) + corrupt[0] ^= 1 + self.assertFalse(safety.safety_rx_hook(self.packet((scc[0], bytes(corrupt), scc[2])))) + elif variant == "replay": + for _ in range(5): + safety.safety_rx_hook(self.packet(scc)) + elif variant == "stale": + safety.set_timer(2_100_000) + safety.safety_tick() + self.assertFalse(safety.get_controls_allowed() and safety.safety_config_valid()) + + def test_display_pair_freshness_and_native_source_bus_length(self): + for car in CARS: + cp, state, safety, control, controller, steering, displays, packer = self.joined(car, "lfa", display=True) + can = CanBus(cp) + self.assertEqual([f[0] for f in displays], list(DISPLAY_IDS)) + for frame in displays: + self.assertTrue(safety.safety_tx_hook(self.packet(frame))) + self.assertFalse(safety.safety_tx_hook(self.packet((frame[0], frame[1], can.CAM)))) + self.assertFalse(safety.safety_tx_hook(self.packet((frame[0], frame[1][:-8], frame[2])))) + controller.frame = 10 + _, repeated = controller.update(control.as_reader(), state, 1_050_000_000) + self.assertFalse(any(f[0] in DISPLAY_IDS for f in repeated)) + parser = CANParser(DBC[car][Bus.pt], [("CCNC_0x161", 0), ("CCNC_0x162", 0)], can.CAM) + parser.update((1_060_000_000, [packer.make_can_msg("CCNC_0x161", can.CAM, {"COUNTER": 8})])) + state.ccnc_161_ts_ns = 1_060_000_000 + controller.frame = 15 + _, missing_pair = controller.update(control.as_reader(), state, 1_060_000_000) + self.assertFalse(any(f[0] in DISPLAY_IDS for f in missing_pair)) + + def test_sustained_host_native_geometry_and_dual_frame_history(self): + cases = 0 + for car in CARS: + topologies = ("lka", "lka_alt", "lfa", "lfa_alt") if car == CAR.HYUNDAI_SANTA_FE_HEV_5TH_GEN else CAMERA_TOPOLOGIES + for topology in topologies: + for hybrid in (False, True) if car == CAR.HYUNDAI_SANTA_FE_HEV_5TH_GEN else (False,): + for adas in (False, True) if topology in CAMERA_TOPOLOGIES else (False,): + for speed_mps in (2.0, 10.0, 25.0, 35.0): + for direction in (-1, 1): + with self.subTest(car=car, topology=topology, hybrid=hybrid, adas=adas, + speed=speed_mps, direction=direction): + _, state, safety, control, controller, _, _, _ = self.joined( + car, topology, hybrid=hybrid, adas=adas, speed=speed_mps * 3.6, + measured=direction * 10.0, desired=direction * 360.0) + cases += 1 + for step in range(90): + safety.set_timer(1_010_000 + step * 10_000) + _, sends = controller.update(control.as_reader(), state, 1_010_000_000 + step * 10_000_000) + steering = [frame for frame in sends if frame[0] in STEER_IDS] + self.assertEqual([frame[0] for frame in steering], + [0x12A, 0xCB] if adas else + [0x50 if topology == "lka" else 0x110 if topology == "lka_alt" else 0x12A]) + if adas: + self.assertEqual((steering[0][1][9] >> 4) & 3, 1) + for frame in steering: + self.assertTrue(safety.safety_tx_hook(self.packet(frame)), + (car, topology, hybrid, adas, speed_mps, direction, step, hex(frame[0]))) + bound = get_max_angle_vm(max(state.out.vEgoRaw - 1.0, 1.0), controller.angle_vm, controller.params) + self.assertGreater(abs(controller.apply_angle_last), min(5.0, bound * 0.7)) + self.assertLessEqual(abs(controller.apply_angle_last), min(360.0, bound) + 0.2) + self.assertEqual(cases, 128) + + def test_measured_physical_speed_and_adjacent_angle_ticks(self): + for car in CARS: + for topology in ("lfa", "lfa_alt"): + for speed_mps in (2.0, 10.0, 25.0, 41.0): + _, state, safety, _, controller, steering, _, _ = self.joined( + car, topology, speed=speed_mps * 3.6, measured=0.0, desired=0.0) + packer = CANPacker(DBC[car][Bus.pt]) + for counter in range(1, 7): + wheel_frame = packer.make_can_msg("WHEEL_SPEEDS", CanBus(controller.CP).ECAN, + {"COUNTER": counter, **dict.fromkeys( + ("WHL_SpdFLVal", "WHL_SpdFRVal", "WHL_SpdRLVal", "WHL_SpdRRVal"), speed_mps * 3.6)}) + self.assertTrue(safety.safety_rx_hook(self.packet(wheel_frame))) + measured_speed = safety.get_vehicle_speed_min() + self.assertAlmostEqual(measured_speed, speed_mps, delta=0.02) + self.assertAlmostEqual(state.out.vEgoRaw, measured_speed, delta=0.02) + effective_speed = max(measured_speed - 1.0, 1.0) + max_ticks = int(get_max_angle_vm(effective_speed, controller.angle_vm, controller.params) * 10 + 1) + delta_ticks = int(get_max_angle_delta_vm(effective_speed, controller.angle_vm, controller.params) * 10 + 1) + frame = steering[0] + + def angle_packet(ticks, frame=frame): + data = bytearray(frame[1]) + raw = ticks & 0x3FFF + data[10] = (data[10] & 3) | ((raw & 63) << 2) + data[11] = raw >> 6 + data[:2] = hkg_can_fd_checksum(frame[0], None, data).to_bytes(2, "little") + return self.packet((frame[0], bytes(data), frame[2])) + + for sign in (-1, 1): + with self.subTest(car=car, topology=topology, speed=speed_mps, sign=sign): + allowed = min(max_ticks, 3600) * sign + safety.set_controls_allowed(True) + safety.set_desired_angle_last(allowed) + self.assertTrue(safety.safety_tx_hook(angle_packet(allowed))) + if max_ticks < 3600: + rejected = (max_ticks + 1) * sign + safety.set_controls_allowed(True) + safety.set_desired_angle_last(rejected) + self.assertFalse(safety.safety_tx_hook(angle_packet(rejected))) + safety.set_controls_allowed(True) + safety.set_desired_angle_last(0) + self.assertTrue(safety.safety_tx_hook(angle_packet(delta_ticks * sign))) + safety.set_controls_allowed(True) + safety.set_desired_angle_last(0) + self.assertFalse(safety.safety_tx_hook(angle_packet((delta_ticks + 1) * sign))) + + def test_actuation_fields_forwarding_and_relay(self): + for car in CARS: + topologies = ("lka", "lka_alt", "lfa", "lfa_alt") if car == CAR.HYUNDAI_SANTA_FE_HEV_5TH_GEN else CAMERA_TOPOLOGIES + for topology in topologies: + adas = topology in CAMERA_TOPOLOGIES + _, _, safety, _, _, steering, displays, _ = self.joined(car, topology, adas=adas, + display=topology in CAMERA_TOPOLOGIES) + primary = steering[-1] + angle_frame = steering[0] if adas else primary + for byte_index, bit in ((5, 0x2), (6, 0x10)): + data = bytearray(angle_frame[1]) + data[byte_index] ^= bit + data[:2] = hkg_can_fd_checksum(angle_frame[0], None, data).to_bytes(2, "little") + self.assertFalse(safety.safety_tx_hook(self.packet((angle_frame[0], bytes(data), angle_frame[2])))) + if angle_frame[0] == 0x110: + data = bytearray(angle_frame[1]) + data[13] ^= 0x8 + data[:2] = hkg_can_fd_checksum(angle_frame[0], None, data).to_bytes(2, "little") + self.assertFalse(safety.safety_tx_hook(self.packet((angle_frame[0], bytes(data), angle_frame[2])))) + if adas: + for byte_index, bit in ((3, 0x1), (7, 0x1), (8, 0x1)): + data = bytearray(primary[1]) + data[byte_index] ^= bit + data[:2] = hkg_can_fd_checksum(primary[0], None, data).to_bytes(2, "little") + self.assertFalse(safety.safety_tx_hook(self.packet((primary[0], bytes(data), primary[2])))) + self.assertTrue(safety.safety_tx_hook(self.packet(primary))) + self.assertEqual(safety.safety_fwd_hook(0, primary[0]), 2) + self.assertEqual(safety.safety_fwd_hook(2, primary[0]), -1) + for frame in displays: + self.assertTrue(safety.safety_tx_hook(self.packet(frame))) + self.assertEqual(safety.safety_fwd_hook(0, frame[0]), 2) + self.assertEqual(safety.safety_fwd_hook(2, frame[0]), -1) + safety.set_relay_malfunction(True) + self.assertFalse(safety.safety_tx_hook(self.packet(primary))) + + def test_raw_conflicts_and_native_actuation_boundary(self): + for car in CARS: + cp, _, safety, _, _, steering, displays, _ = self.joined(car, "lfa", display=True) + active = steering[0] + for raw_or in (HyundaiSafetyFlags.LONG, HyundaiSafetyFlags.EV_GAS, HyundaiSafetyFlags.CARNIVAL_ALT_RESUME, + HyundaiSafetyFlags.CAN_REFRESH_MSGS): + bad = cp.safetyConfigs[-1].safetyParam | int(raw_or) + self.assertEqual(safety.set_safety_hooks(structs.CarParams.SafetyModel.hyundaiCanfd, bad), 0) + safety.init_tests() + self.assertFalse(safety.safety_tx_hook(self.packet(active)), hex(bad)) + _, _, safety, _, _, steering, _, _ = self.joined(car, "lfa", display=True) + active = steering[0] + self.assertTrue(safety.safety_tx_hook(self.packet(active))) + self.assertFalse(safety.safety_tx_hook(self.packet((active[0], active[1], 1 - active[2])))) + self.assertFalse(safety.safety_tx_hook(self.packet((active[0], active[1][:-8], active[2])))) + for addr in (0x7C4, 0xEA, 0x730): + self.assertFalse(safety.safety_tx_hook(self.packet((addr, bytes(8), 0)))) + self.assertFalse(safety.safety_tx_hook(self.packet((0x1A0, bytes(32), 0)))) + self.assertTrue(all(f[0] in DISPLAY_IDS for f in displays)) + + def test_actual_cp_geometry_matches_host_vm(self): + expected = {CAR.HYUNDAI_SANTA_FE_HEV_5TH_GEN: (2171.0, 2.81, 13.72, -0.0005968975988), + CAR.KIA_SPORTAGE_2026: (1871.0, 2.756, 13.7, -0.0006085929296)} + for car, (mass, wheelbase, ratio, slip) in expected.items(): + cp = params(car) + self.assertAlmostEqual(cp.mass, mass, places=3) + self.assertAlmostEqual(cp.wheelbase, wheelbase, places=6) + self.assertAlmostEqual(cp.steerRatio, ratio, places=6) + from opendbc.car.vehicle_model import VehicleModel, calc_slip_factor + self.assertAlmostEqual(calc_slip_factor(VehicleModel(cp)), slip, places=9) + + +if __name__ == "__main__": + unittest.main() diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_classic_long_aol.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_classic_long_aol.py new file mode 100644 index 0000000000..f4e117194e --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_classic_long_aol.py @@ -0,0 +1,189 @@ +from opendbc.car.structs import CarParams +import unittest +from opendbc.safety.tests import test_hyundai_kona_lda_aol as fixture + + +class TestHyundaiClassicLongAol(unittest.TestCase): + def setUp(self): + fixture.TestHyundaiKonaLdaAol.setUp(self) + self.release = self.safety.set_safety_hooks(CarParams.SafetyModel.allOutput, 0) != 0 + + tearDown = fixture.TestHyundaiKonaLdaAol.tearDown + reset = fixture.TestHyundaiKonaLdaAol.reset + rx = fixture.TestHyundaiKonaLdaAol.rx + request = fixture.TestHyundaiKonaLdaAol.request + + def feed(self, word=0x404, button=0, main=False, available=True, tcs=True, clu=True, wrong_bus=None, corrupt_tcs=False): + self.safety.set_timer(self.now) + c = self.counter + self.counter += 1 + if word & 3: + self.rx('E_EMS11', {'Accel_Pedal_Pos': 0, 'CR_Vcu_AccPedDep_Pos': 0}) + else: + self.rx('EMS16', {'AliveCounter': c % 4}, integrity=True) + self.rx('WHL_SPD11', {'WHL_SPD_AliveCounter_LSB': c % 4, 'WHL_SPD_AliveCounter_MSB': (c // 4) % 4}, integrity=True) + if tcs: + self.rx('TCS13', {'AliveCounterTCS': c % 8, 'ACCEnable': 0 if available else 1}, integrity=not corrupt_tcs, bus=0 if wrong_bus is None else wrong_bus) + self.rx('MDPS12', {'CR_Mdps_StrColTq': 0}) + if clu: + self.rx('CLU11', {'CF_Clu_AliveCnt1': c % 16, 'CF_Clu_CruiseSwState': button, 'CF_Clu_CruiseSwMain': int(main)}) + self.now += 10000 + + def test_marked_long_with_foreign_experience_has_no_rx_or_tx_admission(self): + for experience in (0, 1, 33): + self.safety.set_alternative_experience(experience) + self.assertEqual(self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0x404), 0) + self.safety.init_tests() + self.assertFalse(self.safety.safety_config_valid()) + self.assertFalse(self.safety.safety_tx_hook(self.packer.make_can_msg_safety('LKAS11', 0, {}))) + self.assertEqual(self.request(3), 0) + + def test_availability_and_held_main_cannot_authorize(self): + self.reset(0x404) + for _ in range(6): + self.feed(main=True) + self.assertEqual(self.request(3), 0) + self.assertFalse(self.safety.get_controls_allowed()) + + def test_physical_main_grants_lateral_only_without_stock_scc(self): + self.reset(0x404) + for _ in range(6): + self.feed() + self.feed(main=True) + expected = 0 if self.release else 1 + self.assertEqual(self.request(3), expected) + self.assertFalse(self.safety.get_controls_allowed()) + + def test_set_resume_capture_and_cancel_keep_longitudinal_physical(self): + if self.release: + self.skipTest('Current classic OP-long is DEBUG-only') + for button in (1, 2): + self.reset(0xC04) + for _ in range(6): + self.feed(0xC04) + self.feed(0xC04, button=button) + self.feed(0xC04) + self.assertTrue(self.safety.get_controls_allowed()) + self.assertEqual(self.request(3), 3) + self.feed(0xC04, button=4) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertEqual(self.request(3), 1) + self.safety.set_aol_test_heartbeat(False) + self.assertEqual(self.request(3), 0) + + def test_tcs_fault_and_expiry_revoke_without_disabled_scc_requirement(self): + if self.release: + self.skipTest('Current classic OP-long is DEBUG-only') + for cause in ('fault', 'expiry'): + self.reset(0x404) + for _ in range(6): + self.feed() + self.feed(main=True) + self.assertEqual(self.request(1), 1) + if cause == 'fault': + self.feed(available=False) + else: + for _ in range(120): + self.feed(tcs=False) + self.request(1) + self.assertEqual(self.request(3), 0) + + def test_release_marked_long_has_zero_tx_and_existing_unmarked_is_unchanged(self): + if not self.release: + self.skipTest('Explicit release marked-owner boundary') + for word in (0x404, 0xC04, 0x405, 0xC05, 0x406, 0xC06, 0x444, 0xC44, 0x445, 0xC45, 0x446, 0xC46): + self.reset(word) + self.assertEqual(self.request(3), 0) + for name, values in (('LKAS11', {'CR_Lkas_StrToqReq': 0, 'CF_Lkas_ActToi': 0}), ('SCC12', {'ACCMode': 0, 'aReqRaw': 0, 'aReqValue': 0})): + self.assertFalse(self.safety.safety_tx_hook(self.packer.make_can_msg_safety(name, 0, values))) + self.reset(4) + self.assertTrue(self.safety.safety_tx_hook(self.packer.make_can_msg_safety('LKAS11', 0, {'CR_Lkas_StrToqReq': 0, 'CF_Lkas_ActToi': 0}))) + self.assertFalse(self.safety.safety_tx_hook(self.packer.make_can_msg_safety('SCC12', 0, {'ACCMode': 1, 'aReqRaw': 1, 'aReqValue': 1}))) + + def test_required_clu_expiry_clears_prior_main_and_requires_fresh_gesture(self): + if self.release: + self.skipTest('Current classic OP-long is DEBUG-only') + self.reset(0x404) + for _ in range(6): + self.feed() + self.feed(main=True) + self.assertEqual(self.request(1), 1) + for _ in range(120): + self.feed(clu=False) + self.request(1) + self.assertEqual(self.request(3), 0) + for _ in range(6): + self.feed(main=True) + self.assertEqual(self.request(1), 0) + self.assertEqual(self.request(0), 0) + self.feed() + self.feed(main=True) + self.assertEqual(self.request(1), 1) + + def test_wrong_bus_tcs_does_not_renew_ready_health(self): + if self.release: + self.skipTest('Current classic OP-long is DEBUG-only') + self.reset(0x404) + for _ in range(6): + self.feed() + self.feed(main=True) + self.assertEqual(self.request(1), 1) + for _ in range(120): + self.feed(wrong_bus=1) + self.request(1) + self.assertEqual(self.request(3), 0) + + +class TestHyundaiLegacyLongAol(unittest.TestCase): + setUp = TestHyundaiClassicLongAol.setUp + tearDown = TestHyundaiClassicLongAol.tearDown + reset = TestHyundaiClassicLongAol.reset + rx = TestHyundaiClassicLongAol.rx + request = TestHyundaiClassicLongAol.request + feed = TestHyundaiClassicLongAol.feed + + def test_legacy_lda_bit_selects_real_physical_source_without_longitudinal_grant(self): + for word in (0xE902, 0xE903, 0xE912, 0xE913): + for source, signal in (('BCM_PO_11', 'LDA_BTN'), ('CLU13', 'CF_Clu_LdwsLkasSW')): + with self.subTest(word=word, source=source): + self.reset(word, mode=CarParams.SafetyModel.hyundaiLegacy) + gas_word = 2 if word & 1 else 0 + for _ in range(6): + self.feed(gas_word) + self.assertEqual(self.request(0), 0) + self.rx(source, {signal: 1}) + self.assertEqual(self.request(3), 0, 'Held at source initialization is not a physical enable edge') + self.request(0) + self.rx(source, {signal: 0}) + self.rx(source, {signal: 1}, bus=1) + self.assertEqual(self.request(3), 0, 'A wrong-bus LDA press cannot authorize either axis') + self.request(0) + self.rx(source, {signal: 1}) + expected = 1 if not self.release and word & 0x10 else 0 + self.assertEqual(self.request(3), expected) + self.assertFalse(self.safety.get_controls_allowed(), 'LDA permission is independent of normal longitudinal engagement') + self.assertEqual(self.request(2), 0, 'The physical LDA gesture cannot grant longitudinal control') + self.assertEqual(self.request(1), expected) + self.reset(word, mode=CarParams.SafetyModel.hyundaiLegacy) + self.assertEqual(self.request(3), 0, 'Reinitializing the registered mode clears the physical token') + + def test_exact_legacy_graph_without_disabled_scc(self): + for word in (0xE902, 0xE903): + self.reset(word, mode=CarParams.SafetyModel.hyundaiLegacy) + if self.release: + self.assertEqual(self.request(3), 0) + self.assertFalse(self.safety.get_controls_allowed()) + continue + gas_word = 2 if word == 0xE903 else 0 + for _ in range(6): + self.feed(gas_word) + self.feed(gas_word, main=True) + self.assertEqual(self.request(3), 1) + self.feed(gas_word, main=True, button=2) + self.feed(gas_word, main=True) + self.assertTrue(self.safety.get_controls_allowed()) + self.assertEqual(self.request(3), 3) + self.now += 400000 + self.safety.set_timer(self.now) + self.safety.safety_tick() + self.assertEqual(self.request(3), 0) diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_classic_scc_aol.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_classic_scc_aol.py new file mode 100644 index 0000000000..3691c0325e --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_classic_scc_aol.py @@ -0,0 +1,152 @@ +"""Classic SCC physical ownership remains separate from stock longitudinal control.""" + +import unittest +from opendbc.car.structs import CarParams +from opendbc.safety.tests import test_hyundai_grouped_non_scc_aol as grouped +from opendbc.safety.tests import test_hyundai_kona_lda_aol as fixture + + +class TestHyundaiClassicSccAol(unittest.TestCase): + setUp = fixture.TestHyundaiKonaLdaAol.setUp + tearDown = fixture.TestHyundaiKonaLdaAol.tearDown + reset = fixture.TestHyundaiKonaLdaAol.reset + rx = fixture.TestHyundaiKonaLdaAol.rx + request = fixture.TestHyundaiKonaLdaAol.request + + def feed(self, word, main=False, source=None, pressed=False, scc11=True, main_bus=None): + self.safety.set_timer(self.now) + count = self.counter + self.counter += 1 + if word & 3: + self.rx("E_EMS11", {"Accel_Pedal_Pos": 0, "CR_Vcu_AccPedDep_Pos": 0}) + else: + self.rx("EMS16", {"AliveCounter": count % 4}, integrity=True) + self.rx("WHL_SPD11", {"WHL_SPD_AliveCounter_LSB": count % 4, "WHL_SPD_AliveCounter_MSB": (count // 4) % 4}, integrity=True) + self.rx("TCS13", {"AliveCounterTCS": count % 8}, integrity=True) + self.rx("MDPS12", {"CR_Mdps_StrColTq": 0}) + self.rx("CLU11", {"CF_Clu_AliveCnt1": count % 16}) + bus = 2 if word & 8 else 0 + self.rx("SCC12", {"ACCMode": 0, "CR_VSM_Alive": count % 16}, bus=bus, integrity=True) + if scc11: + self.rx("SCC11", {"MainMode_ACC": int(main)}, bus=bus if main_bus is None else main_bus) + if source: + signal = "LDA_BTN" if source == "BCM_PO_11" else "CF_Clu_LdwsLkasSW" + self.rx(source, {signal: int(pressed)}) + self.now += 10_000 + + def test_main_namespace_gas_limits_bus_and_no_longitudinal(self): + for mode in (CarParams.SafetyModel.hyundai, CarParams.SafetyModel.hyundaiLegacy): + for gas in (0, 1, 2): + for limits in (0, 64, 512): + for bus in (0, 8) if mode == CarParams.SafetyModel.hyundai else (0,): + word = 0x400 | gas | limits | bus + self.reset(word, mode=mode) + for _ in range(6): + self.feed(word) + self.assertEqual(self.request(1), 0) + for _ in range(6): + self.feed(word, main=True) + self.assertEqual(self.request(1), 1) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertEqual(self.request(2), 0) + self.assertFalse( + self.safety.safety_tx_hook(self.packer.make_can_msg_safety("SCC12", 2 if bus else 0, {"ACCMode": 1, "aReqRaw": 1, "aReqValue": 1})) + ) + self.feed(word) + self.assertEqual(self.request(1), 0) + + def test_lda_sources_and_required_main_expiry(self): + for mode in (CarParams.SafetyModel.hyundai, CarParams.SafetyModel.hyundaiLegacy): + for word in (0xC00, 0xC41, 0xE02): + for source in ("BCM_PO_11", "CLU13"): + self.reset(word, mode=mode) + for _ in range(6): + self.feed(word, source=source) + self.feed(word, source=source, pressed=True) + self.assertEqual(self.request(1), 1) + for _ in range(120): + self.feed(word, source=source, scc11=False) + self.request(1) + self.assertEqual(self.request(1), 0) + + def test_wrong_bus_unknown_namespace_and_legacy_camera_rejected(self): + self.reset(0x408) + for _ in range(6): + self.feed(0x408, main=True, main_bus=0) + self.assertEqual(self.request(1), 0) + for word in (0x403, 0x640, 0x404, 0x1408, 0x2400, 0x8400): + self.reset(word) + for _ in range(6): + self.feed(word, main=True) + self.assertEqual(self.request(1), 0) + self.reset(0x408, mode=CarParams.SafetyModel.hyundaiLegacy) + for _ in range(6): + self.feed(0x408, main=True) + self.assertEqual(self.request(1), 0) + for experience in (0, 1, 33): + self.reset(0x400, experience=experience) + for _ in range(6): + self.feed(0x400, main=True) + self.assertEqual(self.request(1), 0) + + def test_swl_only_and_held_lda_do_not_authorize_independent_request(self): + self.reset(0xC02) + for _ in range(6): + self.feed(0xC02, main=True, source="CLU13") + self.rx("CLU13", {"CF_Clu_SWL_Stat": 4}) + self.assertEqual(self.request(1), 0) + self.assertFalse(self.safety.get_controls_allowed()) + self.reset(0xC02) + for _ in range(6): + self.feed(0xC02, main=True, source="BCM_PO_11", pressed=True) + self.assertEqual(self.request(1), 0) + self.assertEqual(self.request(0), 0) + self.feed(0xC02, main=True, source="BCM_PO_11") + self.feed(0xC02, main=True, source="BCM_PO_11", pressed=True) + self.assertEqual(self.request(1), 1) + + def test_no_lda_traffic_preserves_ordinary_stock_steering_with_aol_off(self): + for mode in (CarParams.SafetyModel.hyundai, CarParams.SafetyModel.hyundaiLegacy): + self.reset(0xC00, mode=mode) + for _ in range(120): + self.feed(0xC00, main=True) + self.assertEqual(self.request(1), 0) + self.rx("CLU11", {"CF_Clu_CruiseSwState": 2, "CF_Clu_AliveCnt1": self.counter % 16}) + self.rx("SCC12", {"ACCMode": 1, "CR_VSM_Alive": self.counter % 16}, integrity=True) + self.counter += 1 + self.assertTrue(self.safety.get_controls_allowed()) + self.assertEqual(self.request(0), 0) + active = self.packer.make_can_msg_safety("LKAS11", 0, {"CR_Lkas_StrToqReq": 1, "CF_Lkas_ActToi": 1}) + self.assertFalse(self.safety.safety_tx_hook(active)) + # Ordinary enabled steering requests the lateral axis even with AOL preference off. + self.assertEqual(self.request(1), 1) + self.assertTrue(self.safety.safety_tx_hook(active)) + + def test_actual_stock_engagement_seeds_lateral_then_heartbeat_revokes(self): + for mode in (CarParams.SafetyModel.hyundai, CarParams.SafetyModel.hyundaiLegacy): + self.reset(0xC00, mode=mode) + for _ in range(6): + self.feed(0xC00, main=True) + self.assertEqual(self.request(1), 0) + self.rx("CLU11", {"CF_Clu_CruiseSwState": 2, "CF_Clu_AliveCnt1": self.counter % 16}) + self.rx("SCC12", {"ACCMode": 1, "CR_VSM_Alive": self.counter % 16}, integrity=True) + self.counter += 1 + self.assertTrue(self.safety.get_controls_allowed()) + self.assertEqual(self.request(1), 1) + self.rx("SCC12", {"ACCMode": 0, "CR_VSM_Alive": self.counter % 16}, integrity=True) + self.counter += 1 + self.assertFalse(self.safety.get_controls_allowed()) + self.assertEqual(self.request(1), 1) + self.assertEqual(self.request(2), 0) + self.safety.set_aol_test_heartbeat(False) + self.assertEqual(self.request(1), 0) + + def test_non_scc_owner_survives_mode_transition(self): + self.reset(0x400) + for _ in range(6): + self.feed(0x400, main=True) + self.assertEqual(self.request(1), 1) + self.reset(0x1400) + for _ in range(6): + grouped.TestHyundaiGroupedNonSccAol.grouped_feed(self, 0x1400, main=True) + self.assertEqual(self.request(1), 1) diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_ev9_long.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_ev9_long.py new file mode 100644 index 0000000000..c3a829a993 --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_ev9_long.py @@ -0,0 +1,211 @@ +"""Exact EV9 LONG direct-angle contract; select classes by compiled variant.""" +import unittest + +from opendbc.can import CANPacker +from opendbc.car.structs import CarParams +from opendbc.car.hyundai.hyundaicanfd import hkg_can_fd_checksum +from opendbc.safety.tests.libsafety import libsafety_py + + +class Ev9LongFixture: + def setUp(self): + self.safety = libsafety_py.libsafety + self.packer = CANPacker('hyundai_canfd_generated') + self.safety.init_tests() + self.safety.set_alternative_experience(0) + self.assertEqual(self.safety.set_safety_hooks(CarParams.SafetyModel.hyundaiCanfd, 0x5C95), 0) + self.now = 1_000_000 + self.safety.set_timer(self.now) + + def packet(self, name, bus, values): + addr, data, _ = self.packer.make_can_msg(name, bus, values) + return libsafety_py.make_CANPacket(addr, bus, data) + + def feed(self, *, measured=0.0, fault=0, gear=5, buttons=True): + self.safety.set_timer(self.now) + entries = [('ACCELERATOR', {'GEAR': gear}), ('TCS', {'DriverBraking': 0}), + ('WHEEL_SPEEDS', dict.fromkeys(('WHL_SpdFLVal', 'WHL_SpdFRVal', 'WHL_SpdRLVal', 'WHL_SpdRRVal'), 40.0)), + ('MDPS', {'MDPS_EstStrAnglVal': measured, 'MDPS_PaStrAnglVal': -20.0, + 'MDPS_ADAS_AciFltSig_Lv2': fault})] + if buttons: + entries += [('CRUISE_BUTTONS', {'CRUISE_BUTTONS': 2}), ('CRUISE_BUTTONS', {'CRUISE_BUTTONS': 0})] + for name, values in entries: + self.assertTrue(self.safety.safety_rx_hook(self.packet(name, 1, values)), name) + self.now += 10_000 + + def ready(self): + for _ in range(6): + self.feed() + self.assertAlmostEqual(self.safety.get_vehicle_speed_min(), 40.0 / 3.6, places=3) + self.assertTrue(self.safety.get_controls_allowed()) + + def angle(self, *, value=0.0, active=True, bus=1, bad_crc=False): + msg = self.packet('LFA_ALT', bus, {'ADAS_ActvACISta': 0, 'ADAS_ActvACILvl2Sta': 2 if active else 1, + 'ADAS_StrAnglReqVal': value, 'ADAS_ACIAnglTqRedcGainVal': 0.0, + 'FCA_ESA_ActvSta': 0, 'FCA_ESA_TqBstGainVal': 0.0}) + if bad_crc: + msg[0].data[0] ^= 1 + return msg + + +class TestEv9LongDebugContract(Ev9LongFixture, unittest.TestCase): + def test_active_camera_forwarding_requires_current_physical_readiness(self): + self.ready() + for address in (0x110, 0x362): + self.assertEqual(self.safety.safety_test_selected_fwd(2, address), True) + self.assertTrue(self.safety.safety_rx_hook(self.packet('TCS', 1, {'DriverBraking': 1}))) + for address in (0x110, 0x362): + self.assertEqual(self.safety.safety_test_selected_fwd(2, address), False) + self.assertFalse(self.safety.safety_tx_hook(self.angle())) + + def test_direct_cb_is_bus_bound_crc_checked_and_no_110_owner(self): + self.ready() + self.assertTrue(self.safety.safety_tx_hook(self.angle(value=1.1))) + self.assertFalse(self.safety.safety_tx_hook(self.angle(bus=0))) + self.assertFalse(self.safety.safety_tx_hook(self.angle(bad_crc=True))) + self.assertFalse(self.safety.safety_tx_hook(self.packet('LKAS_ALT', 0, {}))) + self.assertFalse(self.safety.safety_tx_hook(self.angle(value=2.3))) + + def test_exact_selector_rejects_neighbor_words_and_aol_experience(self): + for word in (0x5C94, 0x5C97, 0x5D95, 0x5495, 0x5C9D, 0x5CB5, 0x5C91, 0x5491): + with self.subTest(word=hex(word)): + self.safety.set_safety_hooks(CarParams.SafetyModel.hyundaiCanfd, word) + self.safety.set_controls_allowed(True) + self.assertFalse(self.safety.safety_tx_hook(self.angle())) + self.safety.set_alternative_experience(1) + self.safety.set_safety_hooks(CarParams.SafetyModel.hyundaiCanfd, 0x5C95) + self.safety.set_controls_allowed(True) + self.assertFalse(self.safety.safety_tx_hook(self.angle())) + + def test_actual_controller_target_cap_inactive_measurement_and_reentry(self): + from opendbc.car import structs + from opendbc.car.hyundai.interface import CarInterface + from opendbc.car.hyundai.ev9_longitudinal import candidate + from opendbc.car.hyundai.tests.test_ioniq5pe_stock import params + from opendbc.car.hyundai.tests.test_ev9_long_controller import packets + from opendbc.car.hyundai.values import CAR + + for sign in (-1, 1): + self.setUp() + cp = candidate(params(candidate=CAR.KIA_EV9), enabled=True, is_release=False) + ci = CarInterface(cp) + cc = structs.CarControl(enabled=True, latActive=True, longActive=True) + cc.actuators.steeringAngleDeg = sign * 360.0 + ci.update([]) + for tick in range(1, 701): + self.now = tick * 10_000 + self.safety.set_timer(self.now) + frames = packets(self.packer, tick, moving=False, advance_counter=True, cruise_button=2 if tick == 12 else 0) + # Low physical speed permits the target cap to be observed directly. + wheel_addr = self.packer.make_can_msg('WHEEL_SPEEDS', 1, {})[0] + frames = [f for f in frames if f[0] != wheel_addr] + frames.append(self.packer.make_can_msg('WHEEL_SPEEDS', 1, + {'COUNTER': tick, **dict.fromkeys(('WHL_SpdFLVal', 'WHL_SpdFRVal', 'WHL_SpdRLVal', 'WHL_SpdRRVal'), 7.2)})) + measured = sign * 170.0 if 550 <= tick < 570 else 0.0 + mdps_addr = self.packer.make_can_msg('MDPS', 1, {})[0] + frames = [f for f in frames if f[0] != mdps_addr] + frames.append(self.packer.make_can_msg('MDPS', 1, {'COUNTER': tick, 'MDPS_EstStrAnglVal': measured})) + for address, data, bus in frames: + self.assertTrue(self.safety.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, data)), (tick, hex(address), bus)) + ci.update([(tick * 10_000_000, frames)]) + cc.enabled = tick >= 14 + cc.latActive = tick >= 14 and not 550 <= tick < 570 + _, sends = ci.apply(cc.as_reader(), tick * 10_000_000) + angle = next((f for f in sends if f[0] == 0xcb), None) + if angle is None or tick < 14: + continue + for address, data, bus in sends: + self.assertTrue(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data)), + (sign, tick, hex(address), bus)) + if tick == 549: + self.assertAlmostEqual(ci.CC.ev9_angle_filter.x, sign * 140.0, delta=0.1) + self.assertLessEqual(abs(ci.CC.apply_angle_last), 140.1) + if 550 <= tick < 570: + self.assertAlmostEqual(ci.CC.apply_angle_last, measured, delta=0.1) + self.assertEqual((angle[1][3] >> 4) & 0xf, 1) + if tick == 700: + self.assertEqual((angle[1][3] >> 4) & 0xf, 2) + self.assertLessEqual(abs(ci.CC.ev9_angle_filter.x), 140.1) + + def test_mdps_uses_byte16_and_exact_long_fault_bit(self): + self.ready() + self.safety.set_controls_allowed(False) + self.feed(measured=3.0) + self.safety.set_controls_allowed(False) + self.assertTrue(self.safety.safety_tx_hook(self.angle(value=3.0, active=False))) + self.assertFalse(self.safety.safety_tx_hook(self.angle(value=-20.0, active=False))) + for _ in range(6): + self.feed(measured=3.0, fault=1) + self.assertTrue(self.safety.safety_tx_hook(self.angle(value=3.0))) + self.feed(measured=3.0, fault=2) + self.assertFalse(self.safety.safety_tx_hook(self.angle(value=3.0))) + + def test_cancel_missing_buttons_and_rx_expiry_deny_active_angle(self): + self.ready() + self.assertTrue(self.safety.safety_rx_hook(self.packet('CRUISE_BUTTONS', 1, {'CRUISE_BUTTONS': 4}))) + self.assertFalse(self.safety.safety_tx_hook(self.angle())) + self.setUp() + for _ in range(6): + self.feed(buttons=False) + self.assertFalse(self.safety.safety_tx_hook(self.angle())) + self.ready() + self.now += 1_000_000 + self.safety.set_timer(self.now) + self.safety.safety_tick() + self.assertFalse(self.safety.safety_tx_hook(self.angle())) + + def test_ev9_effective_accel_limit_and_ten_accepted_inactive_frames(self): + self.ready() + + def scc(mode, accel): + return self.packet('SCC_CONTROL', 1, {'ACCMode': mode, 'aReqRaw': accel, 'aReqValue': accel}) + self.assertTrue(self.safety.safety_tx_hook(scc(1, 2.0))) + self.assertTrue(self.safety.safety_tx_hook(scc(1, 2.2))) + self.assertFalse(self.safety.safety_tx_hook(scc(1, 2.21))) + self.assertFalse(self.safety.safety_tx_hook(self.packet('SCC_CONTROL', 1, {'ACCMode': 1, 'aReqRaw': 2.21, 'aReqValue': 0.0}))) + self.assertFalse(self.safety.safety_tx_hook(self.packet('SCC_CONTROL', 1, {'ACCMode': 1, 'aReqRaw': 0.0, 'aReqValue': 2.21}))) + self.assertTrue(self.safety.safety_tx_hook(scc(1, -3.5))) + self.assertFalse(self.safety.safety_tx_hook(scc(1, -3.51))) + bad = scc(0, 0.0) + bad[0].data[0] ^= 1 + for _ in range(12): + self.assertFalse(self.safety.safety_tx_hook(bad)) + self.assertTrue(self.safety.get_controls_allowed()) + for _ in range(9): + self.assertTrue(self.safety.safety_tx_hook(scc(0, 0.0))) + self.assertTrue(self.safety.get_controls_allowed()) + self.assertTrue(self.safety.safety_tx_hook(scc(0, 0.0))) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertFalse(self.safety.safety_tx_hook(self.angle())) + self.assertFalse(self.safety.safety_tx_hook(scc(1, 0.01))) + self.assertFalse(self.safety.safety_tx_hook(scc(1, -0.01))) + + def test_neutral_lfa_and_literal_radar_host_heartbeat(self): + self.ready() + lfa = self.packet('LFA', 1, {'LKA_OptUsmSta': 2, 'LKA_SysIndReq': 1, + 'StrTqReqVal': 0, 'ActToiSta': 0, 'LKA_UsmMod': 0, + 'Damping_Gain': 100}) + self.assertTrue(self.safety.safety_tx_hook(lfa)) + data = bytearray.fromhex('00000000ff006f00e80400001201030055ffff0000000000') + data[4] &= 0xFE # actual physical brake is released + data[0:2] = hkg_can_fd_checksum(0x100, None, data).to_bytes(2, 'little') + self.assertTrue(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(0x100, 0, data))) + data[4] |= 1 # cannot spoof physical brake + data[0:2] = hkg_can_fd_checksum(0x100, None, data).to_bytes(2, 'little') + self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(0x100, 0, data))) + + def test_radar_track_forwarding_is_exact_bus_range(self): + self.ready() + for addr in (0x3A5, 0x3C4): + self.assertEqual(self.safety.safety_fwd_hook(0, addr), -1) + self.assertEqual(self.safety.safety_fwd_hook(2, addr), 0) + self.assertEqual(self.safety.safety_fwd_hook(0, 0x3A4), 2) + self.assertEqual(self.safety.safety_fwd_hook(0, 0x3C5), 2) + + +class TestEv9LongReleaseDenied(Ev9LongFixture, unittest.TestCase): + def test_long_profile_has_no_transmit_authority_even_with_controls_forced(self): + self.safety.set_controls_allowed(True) + self.assertFalse(self.safety.safety_tx_hook(self.angle())) + self.assertFalse(self.safety.safety_tx_hook(self.packet('SCC_CONTROL', 1, {'ACCMode': 1}))) + self.assertFalse(self.safety.safety_tx_hook(self.packet('CAM_0x362', 0, {}))) diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_ev9_stock.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_ev9_stock.py new file mode 100644 index 0000000000..4c83bf7ce9 --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_ev9_stock.py @@ -0,0 +1,29 @@ +"""EV9's distinct profile runs the shared stock ownership contract.""" +import unittest + +from opendbc.safety.tests.test_hyundai_ioniq5_pe_stock import HyundaiOrdinaryAngleOwnershipChecks + + +class TestEv9StockOwnership(HyundaiOrdinaryAngleOwnershipChecks, unittest.TestCase): + DEFAULT_PARAM = 0x5C91 + + def test_ev9_geometry_has_its_own_40kph_rate_boundary(self): + # At 40 km/h the measured EV9 geometry permits eleven CAN angle units/frame; + # the PE bank permits only ten. This catches an accidental bank10 alias. + for _ in range(6): + self.feed(speed=40.0) + self.assertAlmostEqual(self.safety.get_vehicle_speed_min(), 40.0 / 3.6, places=3) + self.safety.aol_set_host_request(1) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + self.assertTrue(self.safety.safety_tx_hook(self.frame(angle=1.1))) + self.assertFalse(self.safety.safety_tx_hook(self.frame(1, angle=2.3))) + + # Fresh PE acquisition at the same physical speed must reject the EV9 + # boundary, while accepting its own. Geometry identity is not interchangeable. + self.reset(0x5491) + for _ in range(6): + self.feed(speed=40.0) + self.safety.aol_set_host_request(1) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + self.assertFalse(self.safety.safety_tx_hook(self.frame(angle=1.1))) + self.assertTrue(self.safety.safety_tx_hook(self.frame(angle=1.0))) diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_forte_aol.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_forte_aol.py new file mode 100644 index 0000000000..df35e9cb94 --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_forte_aol.py @@ -0,0 +1,245 @@ +"""Classic Forte axis authority, physical source fusion, and reset boundaries.""" +import unittest + +from opendbc.car.structs import CarParams +from opendbc.safety.tests.common import CANPackerSafety +from opendbc.safety.tests.libsafety import libsafety_py +from opendbc.safety.tests.test_hyundai import checksum + + +class TestHyundaiForteAol(unittest.TestCase): + def setUp(self): + self.safety = libsafety_py.libsafety + self.packer = CANPackerSafety('hyundai_can_generated') + self.counter = 0 + self.now = 1_000_000 + + def tearDown(self): + self.safety.set_alternative_experience(0) + + def reset(self, word=0x1c00, experience=32, mode=CarParams.SafetyModel.hyundai): + self.safety.init_tests() + self.safety.set_alternative_experience(experience) + self.assertEqual(self.safety.set_safety_hooks(mode, word), 0) + self.safety.set_timer(self.now) + self.safety.set_aol_test_heartbeat(True) + + def rx(self, name, values, *, integrity=False, bus=0): + return self.safety.safety_rx_hook(self.packer.make_can_msg_safety( + name, bus, values, fix_checksum=checksum if integrity else None)) + + def feed(self, *, main=False, cruise=False, source='BCM_PO_11', pressed=False): + self.safety.set_timer(self.now) + count = self.counter + self.counter += 1 + self.rx('EMS16', {'CRUISE_LAMP_M': int(main), 'CRUISE_LAMP_S': int(cruise), + 'AliveCounter': count % 4}, integrity=True) + self.rx('WHL_SPD11', {'WHL_SPD_AliveCounter_LSB': count % 4, + 'WHL_SPD_AliveCounter_MSB': (count // 4) % 4}, integrity=True) + self.rx('TCS13', {'AliveCounterTCS': count % 8}, integrity=True) + self.rx('MDPS12', {}) + self.rx('CLU11', {'CF_Clu_AliveCnt1': count % 16}) + if source: + signal = 'LDA_BTN' if source == 'BCM_PO_11' else 'CF_Clu_LdwsLkasSW' + self.rx(source, {signal: int(pressed)}) + self.now += 10_000 + + def request(self, mask): + self.safety.aol_set_host_request(mask) + return self.safety.aol_get_permission_mask() + + def test_main_before_cruise_and_exact_mode_word_experience(self): + for word, source in ((0x1400, None), (0x1c00, 'BCM_PO_11'), (0x1c00, 'CLU13')): + self.reset(word) + for _ in range(3): + self.feed(main=True, source=source) + self.assertEqual(self.request(1), 1) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertEqual(self.request(2), 0) + self.feed(main=False, source=source) + self.assertEqual(self.request(1), 0) + for word, experience in ((0x1000, 32), (0x1800, 32), (0x1400, 0), (0x1400, 33), + (0x1401, 32), (0x1403, 32), (0x1404, 32), (0x1410, 32), (0x3400, 32)): + self.reset(word, experience) + self.feed(main=True) + self.assertEqual(self.request(1), 0) + self.reset(0x1400, 32, CarParams.SafetyModel.hyundaiCanfd) + self.assertEqual(self.request(1), 0) + + def test_combined_sources_do_not_double_toggle_and_neutral_retains_token(self): + self.reset() + for _ in range(3): + self.feed() + self.request(0) + self.feed(pressed=True) + self.assertEqual(self.request(1), 1) + # Another source's zero cannot erase the held combined physical state. + self.rx('CLU13', {'CF_Clu_LdwsLkasSW': 0}) + self.assertEqual(self.request(1), 1) + self.feed(pressed=False) + self.assertEqual(self.request(0), 0) + self.assertEqual(self.request(1), 1) + + def test_safety_reset_held_input_expiry_and_new_gesture(self): + self.reset() + for _ in range(3): + self.feed() + self.request(0) + self.feed(pressed=True) + self.assertEqual(self.request(1), 1) + self.reset() + for _ in range(3): + self.feed(pressed=True) + self.assertEqual(self.request(1), 0) + self.request(0) + self.feed(pressed=False) + self.feed(pressed=True) + self.assertEqual(self.request(1), 1) + self.safety.set_aol_test_heartbeat(False) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.safety.set_aol_test_heartbeat(True) + for _ in range(3): + self.feed(pressed=True) + self.assertEqual(self.request(1), 0) + + def test_unclaimed_token_and_missing_required_sources_expire(self): + self.reset() + for _ in range(3): + self.feed() + self.request(0) + self.feed(pressed=True) + # Required RX stays current; only the unclaimed gesture expires. + for _ in range(40): + self.feed(pressed=True) + self.request(0) + self.assertEqual(self.request(1), 0) + self.request(0) + self.feed(pressed=False) + self.feed(pressed=True) + self.assertEqual(self.request(1), 1) + # No permission read or host refresh during this interval: the fresh late + # request itself must reject the expired claimed authorization. + for _ in range(40): + self.feed(pressed=True) + self.assertEqual(self.request(1), 0) + for _ in range(3): + self.feed(main=True) + self.assertEqual(self.request(1), 1) + self.safety.set_timer(self.now + 300_001) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + + def test_lateral_permission_keeps_neutral_and_existing_torque_guards(self): + self.reset(0x1400) + for _ in range(3): + self.feed(main=True, source=None) + def steer(torque, request): + return self.packer.make_can_msg_safety('LKAS11', 0, { + 'CR_Lkas_StrToqReq': torque, 'CF_Lkas_ActToi': request}) + self.assertEqual(self.request(0), 0) + self.assertTrue(self.safety.safety_tx_hook(steer(0, 0))) + self.assertFalse(self.safety.safety_tx_hook(steer(1, 1))) + self.assertEqual(self.request(1), 1) + self.assertTrue(self.safety.safety_tx_hook(steer(1, 1))) + self.assertFalse(self.safety.safety_tx_hook(steer(400, 1))) + self.assertFalse(self.safety.safety_tx_hook(steer(1, 0))) + + def test_new_or_expired_held_source_requires_its_own_neutral(self): + # CLU alone supplies physical authorization; BCM is not a second required + # source. A wrong-length alternative cannot supply a gesture. + self.reset() + for _ in range(3): + self.feed(source='CLU13') + self.request(0) + malformed = libsafety_py.make_CANPacket(0x391, 0, b'\x10' + b'\x00' * 6) + self.safety.safety_rx_hook(malformed) + self.assertEqual(self.request(1), 0) + self.request(0) + self.feed(source='CLU13', pressed=True) + self.assertEqual(self.request(1), 1) + # Reverse the selected required alternative: BCM remains observable after + # CLU selects the mandatory health entry, without becoming required itself. + self.reset() + for _ in range(3): + self.feed(source='CLU13') + self.request(0) + self.rx('BCM_PO_11', {'LDA_BTN': 1}) + self.assertEqual(self.request(1), 0) + self.request(0) + self.rx('BCM_PO_11', {'LDA_BTN': 0}) + self.rx('BCM_PO_11', {'LDA_BTN': 1}) + self.assertEqual(self.request(1), 1) + self.reset() + for _ in range(3): + self.feed() + self.request(0) + self.rx('CLU13', {'CF_Clu_LdwsLkasSW': 1}) + self.assertEqual(self.request(1), 0) + self.request(0) + self.rx('CLU13', {'CF_Clu_LdwsLkasSW': 0}) + self.rx('CLU13', {'CF_Clu_LdwsLkasSW': 1}) + self.assertEqual(self.request(1), 1) + self.request(0) + # Keep required sources current while CLU drops its release and expires. + for _ in range(40): + self.feed(source='BCM_PO_11', pressed=False) + self.request(0) + self.rx('CLU13', {'CF_Clu_LdwsLkasSW': 1}) + self.assertEqual(self.request(1), 1) # Previously claimed current-epoch token remains. + self.reset() + for _ in range(3): + self.feed() + self.request(0) + self.rx('CLU13', {'CF_Clu_LdwsLkasSW': 0}) + for _ in range(40): + self.feed() + self.request(0) + self.rx('CLU13', {'CF_Clu_LdwsLkasSW': 1}) + self.assertEqual(self.request(1), 0) + self.request(0) + self.rx('CLU13', {'CF_Clu_LdwsLkasSW': 0}) + self.rx('CLU13', {'CF_Clu_LdwsLkasSW': 1}) + self.assertEqual(self.request(1), 1) + + def test_transmitted_authority_packets_never_authorize(self): + self.reset() + for _ in range(3): + self.feed() + self.request(0) + for name, values in (('EMS16', {'CRUISE_LAMP_M': 1}), + ('BCM_PO_11', {'LDA_BTN': 1}), + ('CLU13', {'CF_Clu_LdwsLkasSW': 1})): + self.assertFalse(self.safety.safety_tx_hook(self.packer.make_can_msg_safety(name, 0, values))) + self.assertEqual(self.request(1), 0) + self.request(0) + + def test_wrong_bus_integrity_counter_driver_and_relay_guards(self): + self.reset() + for _ in range(3): + self.feed() + self.request(0) + self.rx('EMS16', {'CRUISE_LAMP_M': 1, 'AliveCounter': self.counter % 4}, integrity=True, bus=1) + self.rx('BCM_PO_11', {'LDA_BTN': 1}, bus=1) + self.assertEqual(self.request(1), 0) + for _ in range(3): + self.feed(main=True) + self.assertEqual(self.request(1), 1) + self.assertFalse(self.rx('EMS16', {'CRUISE_LAMP_M': 1, 'AliveCounter': self.counter % 4, + 'Checksum': 15}, integrity=False)) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.reset(0x1400) + for _ in range(3): + self.feed(main=True, source=None) + repeated = (self.counter - 1) % 4 + for _ in range(6): + self.rx('EMS16', {'CRUISE_LAMP_M': 1, 'AliveCounter': repeated}, integrity=True) + self.assertEqual(self.request(1), 0) + self.reset(0x1400) + for _ in range(3): + self.feed(main=True, source=None) + self.assertEqual(self.request(1), 1) + steer = self.packer.make_can_msg_safety('LKAS11', 0, {'CR_Lkas_StrToqReq': 1, 'CF_Lkas_ActToi': 1}) + self.safety.set_torque_driver(-1000, -1000) + self.assertFalse(self.safety.safety_tx_hook(steer)) + self.safety.set_relay_malfunction(True) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.assertFalse(self.safety.safety_tx_hook(steer)) diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_grouped_non_scc_aol.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_grouped_non_scc_aol.py new file mode 100644 index 0000000000..e0775a60e8 --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_grouped_non_scc_aol.py @@ -0,0 +1,71 @@ +"""Grouped physical main/gas/LDA owners, retaining native common health.""" +import unittest +from opendbc.safety.tests import test_hyundai_kona_lda_aol as kona + + +class TestHyundaiGroupedNonSccAol(unittest.TestCase): + setUp = kona.TestHyundaiKonaLdaAol.setUp + tearDown = kona.TestHyundaiKonaLdaAol.tearDown + reset = kona.TestHyundaiKonaLdaAol.reset + rx = kona.TestHyundaiKonaLdaAol.rx + request = kona.TestHyundaiKonaLdaAol.request + def grouped_feed(self, word, main=False, source=None, pressed=False, engagement_source=True): + self.safety.set_timer(self.now) + count = self.counter + self.counter += 1 + if word & 1: + self.rx('E_EMS11', {'Accel_Pedal_Pos': 0}) + if engagement_source: + self.rx('EMS12', {'ACC_ACT': 0}) + self.rx('LABEL11', {'CC_React': int(main)}) + elif word & 2: + self.rx('E_EMS11', {'CR_Vcu_AccPedDep_Pos': 0}) + self.rx('E_CRUISE_CONTROL', {'CRUISE_LAMP_M': int(main), 'CRUISE_LAMP_S': 0}) + else: + self.rx('EMS16', {'CRUISE_LAMP_M': int(main), 'CRUISE_LAMP_S': 0, + 'AliveCounter': count % 4}, integrity=True) + self.rx('WHL_SPD11', {'WHL_SPD_AliveCounter_LSB': count % 4, + 'WHL_SPD_AliveCounter_MSB': (count // 4) % 4}, integrity=True) + self.rx('TCS13', {'AliveCounterTCS': count % 8}, integrity=True) + self.rx('MDPS12', {}) + self.rx('CLU11', {'CF_Clu_AliveCnt1': count % 16}) + if source: + signal = 'LDA_BTN' if source == 'BCM_PO_11' else 'CF_Clu_LdwsLkasSW' + self.rx(source, {signal: int(pressed)}) + self.now += 10_000 + + def test_grouped_main_source_identity_and_no_longitudinal(self): + for word in (0x1400,0x1440,0x1402,0x1441): + self.reset(word) + for _ in range(6): self.grouped_feed(word) + self.assertEqual(self.request(1),0) + for _ in range(6): self.grouped_feed(word,main=True) + self.assertEqual(self.request(1),1) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertEqual(self.request(2),0) + self.grouped_feed(word) + self.assertEqual(self.request(1),0) + + def test_grouped_lda_alternatives_and_missing_main_denial(self): + for word in (0x1c00,0x1c40,0x1c02,0x1c41): + for source in ('BCM_PO_11','CLU13'): + self.reset(word) + for _ in range(6): self.grouped_feed(word,source=source) + self.grouped_feed(word,source=source,pressed=True) + self.assertEqual(self.request(1),1) + self.assertEqual(self.request(2),0) + self.now += 300_001 + self.safety.set_timer(self.now) + self.assertEqual(self.request(1),0) + + def test_ev_main_and_engagement_are_distinct_required_sources(self): + self.reset(0x1441) + for _ in range(6): self.grouped_feed(0x1441) + self.rx('EMS12', {'ACC_ACT':1}) + self.assertEqual(self.request(1),0) + self.rx('LABEL11', {'CC_React':1}) + self.assertEqual(self.request(1),1) + # MAIN evidence alone must not survive required engagement-source expiry. + for _ in range(120): + self.grouped_feed(0x1441, main=True, engagement_source=False) + self.assertEqual(self.request(1),0) diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_ioniq5_pe_stock.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_ioniq5_pe_stock.py new file mode 100644 index 0000000000..defb90b39f --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_ioniq5_pe_stock.py @@ -0,0 +1,244 @@ +"""Ordinary Ioniq 5 PE readiness and committed camera replacement ownership.""" +import unittest + +from opendbc.can import CANPacker +from opendbc.car.structs import CarParams +from opendbc.car.hyundai.hyundaicanfd import hkg_can_fd_checksum +from opendbc.safety.tests.libsafety import libsafety_py + + +class HyundaiOrdinaryAngleOwnershipChecks: + DEFAULT_PARAM = None + def setUp(self): + self.safety = libsafety_py.libsafety + self.packer = CANPacker('hyundai_canfd_generated') + self.now = 1_000_000 + self.reset() + + def reset(self, word=None): + word = self.DEFAULT_PARAM if word is None else word + self.safety.init_tests() + self.safety.set_alternative_experience(0) + self.assertEqual(self.safety.set_safety_hooks(CarParams.SafetyModel.hyundaiCanfd, word), 0) + self.safety.set_timer(self.now) + self.safety.set_aol_test_heartbeat(True) + + @staticmethod + def packet(frame): + return libsafety_py.make_CANPacket(frame[0], frame[2], frame[1]) + + def feed(self, *, cruise=True, gear=5, speed=36.0, brake=False, fault=0, measured=0.0, buttons=True): + self.safety.set_timer(self.now) + entries = [('ACCELERATOR', {'GEAR': gear}), + ('TCS', {'DriverBraking': int(brake)}), + ('WHEEL_SPEEDS', dict.fromkeys(('WHL_SpdFLVal', 'WHL_SpdFRVal', 'WHL_SpdRLVal', 'WHL_SpdRRVal'), speed)), + ('MDPS', {'MDPS_ADAS_AciFltSig_Lv2': fault, 'MDPS_PaStrAnglVal': measured}), + ('CRUISE_BUTTONS', {'CRUISE_BUTTONS': 2}), + ('SCC_CONTROL', {'ACCMode': 0}), ('SCC_CONTROL', {'ACCMode': int(cruise)})] + for name, values in entries: + if name == "CRUISE_BUTTONS" and not buttons: + continue + self.assertTrue(self.safety.safety_rx_hook(self.packet(self.packer.make_can_msg(name, 1, values))), name) + self.now += 10_000 + + def frame(self, counter=0, *, address=0x110, active=True, bad_crc=False, angle=0.0): + name = 'LKAS_ALT' if address == 0x110 else 'CAM_0x362' + _, packed, _ = self.packer.make_can_msg(name, 0, {'COUNTER': counter}) + data = bytearray(packed) + if address == 0x110: + data[9] = (2 if active else 1) << 4 + raw_angle = int(round(angle * 10)) & 0x3FFF + data[10] = (raw_angle & 0x3F) << 2 + data[11] = raw_angle >> 6 + data[5], data[6] = 0, 8 # zero torque request, no torque enable + data[:2] = hkg_can_fd_checksum(address, None, data).to_bytes(2, 'little') + if bad_crc: + data[0] ^= 1 + return libsafety_py.make_CANPacket(address, 0, data) + + def ready(self): + for _ in range(6): + self.feed() + self.assertAlmostEqual(self.safety.get_vehicle_speed_min(), 10.0) + self.assertTrue(self.safety.get_controls_allowed()) + self.safety.aol_set_host_request(1) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + + def test_ready_does_not_own_until_first_accepted_replacement(self): + self.ready() + self.assertEqual(self.safety.safety_fwd_hook(2, 0x110), 0) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x362), 0) + self.assertFalse(self.safety.safety_tx_hook(self.frame(address=0x362))) + self.assertTrue(self.safety.safety_tx_hook(self.frame())) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x110), -1) + self.assertTrue(self.safety.safety_tx_hook(self.frame(address=0x362))) + self.safety.aol_set_host_request(0) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x110), 0) + self.assertFalse(self.safety.safety_tx_hook(self.frame(1))) + + def test_crc_counter_and_final_relay_denials_cannot_acquire(self): + self.ready() + self.assertFalse(self.safety.safety_tx_hook(self.frame(bad_crc=True))) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x110), 0) + self.assertFalse(self.safety.safety_tx_hook(self.frame(active=False))) + self.safety.set_relay_malfunction(True) + self.assertFalse(self.safety.safety_tx_hook(self.frame())) + self.safety.set_relay_malfunction(False) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x110), 0) + self.assertTrue(self.safety.safety_tx_hook(self.frame(254))) + self.assertFalse(self.safety.safety_tx_hook(self.frame(254))) + self.assertFalse(self.safety.safety_tx_hook(self.frame(253))) + self.assertTrue(self.safety.safety_tx_hook(self.frame(0))) + + def test_missing_replacement_expires_with_fresh_requests_and_sources(self): + self.ready() + self.assertTrue(self.safety.safety_tx_hook(self.frame())) + for tick in range(31): + self.feed() + self.safety.aol_set_host_request(1) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + self.assertFalse(self.safety.safety_tx_hook(self.frame(1, bad_crc=True))) + # Suppression traffic is accepted while ownership is current, but cannot + # sustain it when replacement steering stops arriving. + self.assertEqual(self.safety.safety_tx_hook(self.frame(address=0x362)), tick < 30) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x110), 0) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x362), 0) + self.assertFalse(self.safety.safety_tx_hook(self.frame(address=0x362))) + self.assertTrue(self.safety.safety_tx_hook(self.frame(2))) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x110), -1) + + def test_gear_standstill_fault_heartbeat_and_profile_release(self): + for changed in ({'gear': 0}, {'speed': 0.0}, {'fault': 1}, {'brake': True}): + self.reset() + self.ready() + self.assertTrue(self.safety.safety_tx_hook(self.frame())) + self.feed(**changed) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x110), 0) + self.reset() + self.ready() + self.assertTrue(self.safety.safety_tx_hook(self.frame())) + self.safety.set_aol_test_heartbeat(False) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x110), 0) + for bit in (2, 4, 8, 32, 256, 512, 2048, 8192, 32768): + self.reset(self.DEFAULT_PARAM | bit) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + + def test_counter_restart_and_nonzero_measured_angle_reacquisition(self): + self.ready() + self.assertTrue(self.safety.safety_tx_hook(self.frame(100))) + # Continuous ownership accepts skipped forward counters but not reverse. + self.assertTrue(self.safety.safety_tx_hook(self.frame(103))) + self.assertFalse(self.safety.safety_tx_hook(self.frame(102))) + self.safety.aol_set_host_request(0) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x110), 0) + self.feed(measured=3.0) + self.safety.aol_set_host_request(0) + self.safety.aol_set_host_request(1) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + # A restarted controller may start at zero in a new acquisition. The + # shared limiter now starts from actual EPS angle, not the old OP request. + self.assertTrue(self.safety.safety_tx_hook(self.frame(0, angle=3.0))) + self.assertFalse(self.safety.safety_tx_hook(self.frame(1, angle=90.0))) + self.safety.aol_set_host_request(0) + self.feed(measured=3.0) + self.safety.aol_set_host_request(0) + self.safety.aol_set_host_request(1) + self.assertFalse(self.safety.safety_tx_hook(self.frame(0, angle=360.0))) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x110), 0) + self.assertTrue(self.safety.safety_tx_hook(self.frame(0, angle=3.0))) + + def test_expired_owner_reacquires_from_110_without_forward_query(self): + self.ready() + self.assertTrue(self.safety.safety_tx_hook(self.frame(100))) + # Keep readiness alive while withholding steering and forwarding callbacks. + # 110 itself must expire the old counter before a restarted sender acquires. + for _ in range(31): + self.feed() + self.safety.aol_set_host_request(1) + self.assertTrue(self.safety.safety_tx_hook(self.frame(0))) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x110), -1) + + def test_core_wrong_bus_length_and_unlisted_tx_never_acquire(self): + self.ready() + good = self.frame() + payload = bytes(good[0].data[0:32]) + for address, bus, data in ((0x110, 1, payload), (0x110, 0, payload[:24]), (0x111, 0, payload)): + rejected = libsafety_py.make_CANPacket(address, bus, data) + self.assertFalse(self.safety.safety_tx_hook(rejected)) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x110), 0) + self.assertTrue(self.safety.safety_tx_hook(good)) + + def test_angle_and_shared_stock_namespaces_are_exact_and_invalid_experience_denied(self): + # Shared torque-stock words and Ioniq6 ordinary/LONG selectors have separate owners. + supported = { + 0x0800, 0x0801, 0x0802, 0x0808, 0x0809, 0x080A, + 0x0810, 0x0811, 0x0812, 0x0820, 0x0821, 0x0822, + 0x0828, 0x0829, 0x082A, 0x0890, 0x0891, 0x0892, + 0x0C08, 0x0C09, 0x0C0A, 0x0C28, 0x0C29, 0x0C2A, + 0x2820, 0x2822, 0x2828, 0x282A, 0x2830, 0x2832, + 0x28B0, 0x28B2, 0x2C28, 0x2C2A, + 0x8815, 0x8895, 0x5491, 0x5C91, + } + for word in range(0x10000): + if word in supported: + continue + self.reset(word) + self.safety.aol_set_host_request(3) + self.assertEqual(self.safety.aol_get_request_mask(), 0, hex(word)) + self.reset() + self.safety.aol_set_host_request(3) + self.assertEqual(self.safety.aol_get_request_mask(), 1) + self.safety.init_tests() + self.safety.set_alternative_experience(1) + self.safety.set_safety_hooks(CarParams.SafetyModel.hyundaiCanfd, self.DEFAULT_PARAM) + self.safety.set_timer(self.now) + self.safety.set_aol_test_heartbeat(True) + self.safety.aol_set_host_request(1) + self.assertEqual(self.safety.aol_get_request_mask(), 0) + + def test_required_source_expiry_releases_camera_owner(self): + self.ready() + self.assertTrue(self.safety.safety_tx_hook(self.frame())) + self.now += 1_000_000 + self.safety.set_timer(self.now) + self.safety.aol_set_host_request(1) + self.safety.safety_tick() + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x110), 0) + self.assertFalse(self.safety.safety_tx_hook(self.frame(1))) + + def test_first_acquisition_uses_measured_angle_without_prior_release(self): + # No forwarding query or zero host request seeds the inactive baseline. + for _ in range(6): + self.feed(measured=3.0) + self.assertAlmostEqual(self.safety.get_vehicle_speed_min(), 10.0) + self.safety.aol_set_host_request(1) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + self.assertFalse(self.safety.safety_tx_hook(self.frame(angle=90.0))) + self.assertTrue(self.safety.safety_tx_hook(self.frame(angle=3.0))) + self.assertFalse(self.safety.safety_tx_hook(self.frame(1, angle=90.0))) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x110), -1) + + def test_missing_or_stale_required_buttons_cannot_claim_readiness(self): + for _ in range(6): + self.feed(buttons=False) + self.safety.aol_set_host_request(1) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x110), 0) + self.assertFalse(self.safety.safety_tx_hook(self.frame())) + self.reset() + self.ready() + self.assertTrue(self.safety.safety_tx_hook(self.frame())) + # 250 ms exceeds this required 50-Hz source's 200-ms freshness bound, + # while accepted-110's 300-ms lease and every other RX source remain fresh. + for _ in range(25): + self.feed(buttons=False) + self.safety.aol_set_host_request(1) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.assertEqual(self.safety.safety_fwd_hook(2, 0x110), 0) + self.assertFalse(self.safety.safety_tx_hook(self.frame(1))) + + +class TestIoniq5PeStockOwnership(HyundaiOrdinaryAngleOwnershipChecks, unittest.TestCase): + DEFAULT_PARAM = 0x5491 diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_ioniq6_long.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_ioniq6_long.py new file mode 100644 index 0000000000..fce6ba83e9 --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_ioniq6_long.py @@ -0,0 +1,1165 @@ +import unittest + +from opendbc.can import CANPacker, CANParser +from opendbc.car import Bus, gen_empty_fingerprint, structs +from opendbc.car.hyundai.carcontroller import CarController +from opendbc.car.hyundai.hyundaicanfd import (CanBus, create_adrv_messages, create_ioniq6_radar_heartbeat, + create_ioniq6_blindspot_status, create_steering_messages, create_lfahda_cluster, hkg_can_fd_checksum) +from opendbc.car.hyundai.ioniq6_bsm import BlindspotStatus +from opendbc.car.hyundai.canfd_dash_icons import CanFDDashIcons +from opendbc.car.hyundai.ioniq6_handoff import build_ioniq6_hda2_long_candidate +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.values import CAR, DBC +from opendbc.safety.tests.libsafety import libsafety_py +from opendbc.car.hyundai.tests.test_ioniq6_longitudinal import controller_fixture, LongState + + +RAW_LONG = {"lkas": 0x8015, "lkas_alt": 0x8095} +RAW_AOL = {"lkas": 0x8815, "lkas_alt": 0x8895} +TESTER_PRESENT = libsafety_py.make_CANPacket(0x730, 1, b"\x02\x3e\x80\x00\x00\x00\x00\x00") + + +def params(topology): + fingerprint = gen_empty_fingerprint() + camera_addr, camera_len = (0x50, 16) if topology == "lkas" else (0x110, 32) + support_addr, support_len = (0x2A4, 24) if topology == "lkas" else (0x362, 32) + fingerprint[2].update({camera_addr: camera_len, support_addr: support_len}) + fingerprint[1].update({0x1CF: 8, 0x35: 32, 0x175: 24, 0xA0: 24, 0xEA: 24, + 0x1BA: 24, 0x1E5: 16, 0x36A: 16}) + fingerprint[0][0x3A5] = 24 + stock = CarInterface.get_params(CAR.HYUNDAI_IONIQ_6, fingerprint, [], False, False, False) + return stock, build_ioniq6_hda2_long_candidate(stock, fingerprint) + + +class TestHyundaiIoniq6Long(unittest.TestCase): + @staticmethod + def packet(frame): + return libsafety_py.make_CANPacket(frame[0], frame[2], frame[1]) + + def setUp(self): + self.safety = libsafety_py.libsafety + self.release = self.safety.set_safety_hooks(structs.CarParams.SafetyModel.allOutput, 0) != 0 + + def mode(self, raw, *, init_tests=True, stamp=1_000_000): + self.assertEqual(self.safety.set_safety_hooks(structs.CarParams.SafetyModel.hyundaiCanfd, raw), 0) + if init_tests: + self.safety.init_tests() + self.safety.set_timer(stamp) + + def refresh_required_rx(self, packer, counter, stamp): + self.safety.set_timer(stamp) + for name, values in (("ACCELERATOR", {"GEAR": 5}), ("TCS", {}), ("WHEEL_SPEEDS", {}), + ("MDPS", {}), ("CRUISE_BUTTONS", {})): + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + name, 1, {**values, "COUNTER": counter % (16 if name == "CRUISE_BUTTONS" else 256)}))), name) + + def bsm_physical(self, packer, *, left=False, right=False, left_lamp=False, right_lamp=False, + alternate=False, counter=1, stamp=1_000_000): + self.refresh_required_rx(packer, counter, stamp) + corner = packer.make_can_msg("BLINDSPOTS_FRONT_CORNER_2", 1, { + "COUNTER": counter, "SIDE_DETECT_STATE": (0x10 if left else 0) | (0x08 if right else 0), + }) + lamp = packer.make_can_msg("BLINKERS", 1, { + "USE_ALT_LAMP": int(alternate), + ("LEFT_LAMP_ALT" if alternate else "LEFT_LAMP"): int(left_lamp), + ("RIGHT_LAMP_ALT" if alternate else "RIGHT_LAMP"): int(right_lamp), + }) + self.assertTrue(self.safety.safety_rx_hook(self.packet(corner))) + self.assertTrue(self.safety.safety_rx_hook(self.packet(lamp))) + return corner, lamp + + def test_bsm_all_sixteen_physical_states_and_exact_paired_frames(self): + if self.release: + self.skipTest("RELEASE denies all four exact Ioniq 6 LONG profiles") + for raw in (*RAW_LONG.values(), *RAW_AOL.values()): + stock, cp = params("lkas_alt" if raw & 0x80 else "lkas") + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + for left in (False, True): + for right in (False, True): + for left_lamp in (False, True): + for right_lamp in (False, True): + with self.subTest(raw=hex(raw), sources=(left, right, left_lamp, right_lamp)): + self.mode(raw) + status = BlindspotStatus((1 + int(left_lamp)) if left else 0, + (1 + int(right_lamp)) if right else 0) + rear, front = create_ioniq6_blindspot_status(7, status) + self.assertFalse(self.safety.safety_tx_hook(self.packet(rear))) + self.bsm_physical(packer, left=left, right=right, left_lamp=left_lamp, + right_lamp=right_lamp, alternate=left_lamp, counter=1) + self.assertFalse(self.safety.safety_tx_hook(self.packet(front))) + self.assertTrue(self.safety.safety_tx_hook(self.packet(rear))) + self.assertTrue(self.safety.safety_tx_hook(self.packet(front))) + self.assertFalse(self.safety.safety_tx_hook(self.packet(front))) + self.assertFalse(self.safety.safety_tx_hook(self.packet(rear))) + + def test_bsm_alternate_lamp_selected_but_off_cannot_escalate(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 LONG status profile") + self.mode(RAW_LONG["lkas"]) + _, cp = params("lkas") + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + # USE_ALT selects byte 7 even when its left lamp bit is clear. The regular + # lamp bit is not the selected source and cannot authorize a level-2 warning. + self.bsm_physical(packer, left=True, left_lamp=False, alternate=True) + rear, front = create_ioniq6_blindspot_status(1, BlindspotStatus(1, 0)) + self.assertTrue(self.safety.safety_tx_hook(self.packet(rear))) + self.assertTrue(self.safety.safety_tx_hook(self.packet(front))) + escalated, _ = create_ioniq6_blindspot_status(2, BlindspotStatus(2, 0)) + self.assertFalse(self.safety.safety_tx_hook(self.packet(escalated))) + + def test_bsm_unselected_dashboard_bits_cannot_escalate(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 LONG status profile") + # Each valid selected lamp remains OFF. Unselected source bits and reserved + # low bits in the same physical 0x413 must never create a level-2 warning. + cases = ( + (True, False, False, 2, 0x01), # left regular lamp: unrelated byte-2 bit + (False, True, False, 2, 0x01), # right regular lamp: unrelated byte-2 bit + (True, False, False, 7, 0x09), # alternate left ON, but selector OFF plus noise + (True, False, True, 7, 0x01), # alternate selected, left OFF plus noise + (False, True, True, 7, 0x01), # alternate selected, right OFF plus noise + ) + _, cp = params("lkas") + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + for left, right, alternate, byte, bitmask in cases: + with self.subTest(left=left, right=right, alternate=alternate, byte=byte, bits=bitmask): + self.mode(RAW_LONG["lkas"]) + _, lamp = self.bsm_physical(packer, left=left, right=right, alternate=alternate) + payload = bytearray(lamp[1]) + payload[byte] |= bitmask + self.assertTrue(self.safety.safety_rx_hook(libsafety_py.make_CANPacket(0x413, 1, payload))) + escalated = BlindspotStatus(2 if left else 0, 2 if right else 0) + forged_rear, _ = create_ioniq6_blindspot_status(1, escalated) + self.assertFalse(self.safety.safety_tx_hook(self.packet(forged_rear))) + expected = BlindspotStatus(1 if left else 0, 1 if right else 0) + rear, front = create_ioniq6_blindspot_status(1, expected) + self.assertTrue(self.safety.safety_tx_hook(self.packet(rear))) + self.assertTrue(self.safety.safety_tx_hook(self.packet(front))) + + def test_bsm_integrity_pairing_freshness_and_immediate_stock_recurrence(self): + if self.release: + self.skipTest("RELEASE denies all four exact Ioniq 6 LONG profiles") + _, cp = params("lkas") + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + rear, front = create_ioniq6_blindspot_status(3, BlindspotStatus(2, 0)) + + self.mode(RAW_LONG["lkas"]) + corner, lamp = self.bsm_physical(packer, left=True, left_lamp=True) + for bad in ((rear[0], rear[1], 0), (rear[0], rear[1][:16], 1)): + self.assertFalse(self.safety.safety_tx_hook(self.packet(bad))) + corrupted = bytearray(rear[1]) + corrupted[3] ^= 1 + self.assertFalse(self.safety.safety_tx_hook(self.packet((rear[0], bytes(corrupted), 1)))) + self.assertFalse(self.safety.safety_tx_hook(self.packet(front))) + self.assertTrue(self.safety.safety_tx_hook(self.packet(rear))) + self.safety.set_timer(1_010_001) + self.assertFalse(self.safety.safety_tx_hook(self.packet(front))) # paired deadline + self.bsm_physical(packer, left=True, left_lamp=True, counter=2, stamp=1_020_000) + rear, front = create_ioniq6_blindspot_status(4, BlindspotStatus(2, 0)) + self.assertTrue(self.safety.safety_tx_hook(self.packet(rear))) + self.assertTrue(self.safety.safety_tx_hook(self.packet(front))) + + self.mode(RAW_LONG["lkas"]) + self.bsm_physical(packer, left=True, left_lamp=True) + rear, front = create_ioniq6_blindspot_status(3, BlindspotStatus(2, 0)) + self.safety.set_timer(1_100_001) # corner age > 100 ms + self.refresh_required_rx(packer, 2, 1_100_001) + self.assertFalse(self.safety.safety_tx_hook(self.packet(rear))) + self.mode(RAW_LONG["lkas"]) + self.bsm_physical(packer, left=True, left_lamp=True) + self.safety.set_timer(2_500_001) # lamp age > 1.5 s + self.refresh_required_rx(packer, 2, 2_500_001) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "BLINDSPOTS_FRONT_CORNER_2", 1, {"COUNTER": 2, "SIDE_DETECT_STATE": 0x10})))) + self.assertFalse(self.safety.safety_tx_hook(self.packet(rear))) + + for addr, length in ((0x1BA, 24), (0x1E5, 16)): + for source_len in (length, 8): + with self.subTest(source=hex(addr), source_len=source_len): + self.mode(RAW_LONG["lkas"]) + self.assertTrue(self.safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, 1, bytes(source_len)))) + self.assertTrue(self.safety.get_relay_malfunction()) + self.assertFalse(self.safety.safety_tx_hook(self.packet(rear))) + + def test_bsm_valid_crc_cannot_hide_altered_status_body(self): + if self.release: + self.skipTest("RELEASE denies all four exact Ioniq 6 LONG profiles") + for raw in (*RAW_LONG.values(), *RAW_AOL.values()): + with self.subTest(raw=hex(raw)): + self.mode(raw) + _, cp = params("lkas_alt" if raw & 0x80 else "lkas") + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + self.bsm_physical(packer, left=True, left_lamp=True) + rear, front = create_ioniq6_blindspot_status(3, BlindspotStatus(2, 0)) + + altered_rear = bytearray(rear[1]) + altered_rear[3] ^= 1 + altered_rear[:2] = hkg_can_fd_checksum(0x1BA, None, altered_rear).to_bytes(2, "little") + self.assertFalse(self.safety.safety_tx_hook(self.packet((0x1BA, bytes(altered_rear), 1)))) + self.assertFalse(self.safety.safety_tx_hook(self.packet(front))) + + self.assertTrue(self.safety.safety_tx_hook(self.packet(rear))) + altered_front = bytearray(front[1]) + altered_front[11] ^= 1 + altered_front[:2] = hkg_can_fd_checksum(0x1E5, None, altered_front).to_bytes(2, "little") + self.assertFalse(self.safety.safety_tx_hook(self.packet((0x1E5, bytes(altered_front), 1)))) + + def test_bsm_boot_without_prior_lamp_cannot_claim_held_warning(self): + if self.release: + self.skipTest("RELEASE denies all four exact Ioniq 6 LONG profiles") + for raw in (*RAW_LONG.values(), *RAW_AOL.values()): + with self.subTest(raw=hex(raw)): + self.mode(raw, stamp=100_000) + _, cp = params("lkas_alt" if raw & 0x80 else "lkas") + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + # The hold timestamp is still zero during the first half-second after + # startup; a fresh blind-spot detection cannot manufacture lamp history. + self.bsm_physical(packer, left=True, left_lamp=False, stamp=100_000) + escalated, _ = create_ioniq6_blindspot_status(3, BlindspotStatus(2, 0)) + indicated, _ = create_ioniq6_blindspot_status(3, BlindspotStatus(1, 0)) + self.assertFalse(self.safety.safety_tx_hook(self.packet(escalated))) + self.assertTrue(self.safety.safety_tx_hook(self.packet(indicated))) + + def test_bsm_lamp_release_hold_and_stale_gap_rearm(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 LONG status profile") + self.mode(RAW_LONG["lkas"]) + _, cp = params("lkas") + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + + def accept_status(level, counter): + for frame in create_ioniq6_blindspot_status(counter, BlindspotStatus(level, 0)): + self.assertTrue(self.safety.safety_tx_hook(self.packet(frame)), (level, counter, hex(frame[0]))) + + self.bsm_physical(packer, left=True, left_lamp=True, counter=1, stamp=1_000_000) + accept_status(2, 1) + self.bsm_physical(packer, left=True, left_lamp=False, counter=2, stamp=1_200_000) + accept_status(2, 2) + self.bsm_physical(packer, left=True, left_lamp=False, counter=3, stamp=1_600_000) + accept_status(2, 3) # repeated false did not restart the hold + self.refresh_required_rx(packer, 4, 1_700_001) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "BLINDSPOTS_FRONT_CORNER_2", 1, {"COUNTER": 4, "SIDE_DETECT_STATE": 0x10})))) + accept_status(1, 4) + + self.bsm_physical(packer, left=True, left_lamp=True, counter=5, stamp=2_000_000) + accept_status(2, 5) + self.bsm_physical(packer, left=True, left_lamp=False, counter=6, stamp=3_500_001) + accept_status(1, 6) # a false after source expiry cannot hold old true + + def test_bsm_right_lamp_release_holds_only_for_bounded_interval(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 LONG status profile") + self.mode(RAW_LONG["lkas"]) + _, cp = params("lkas") + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + + def accept_right(level, counter): + for frame in create_ioniq6_blindspot_status(counter, BlindspotStatus(0, level)): + self.assertTrue(self.safety.safety_tx_hook(self.packet(frame)), (level, counter, hex(frame[0]))) + + self.bsm_physical(packer, right=True, right_lamp=True, counter=1, stamp=1_000_000) + accept_right(2, 1) + self.bsm_physical(packer, right=True, right_lamp=False, counter=2, stamp=1_200_000) + accept_right(2, 2) # falling right lamp starts a 500 ms hold + self.bsm_physical(packer, right=True, right_lamp=False, counter=3, stamp=1_700_001) + accept_right(1, 3) # repeated off frames cannot extend that hold + + def test_bsm_wrong_length_lamp_invalidates_prior_source_until_rearmed(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 LONG status profile") + self.mode(RAW_LONG["lkas"]) + _, cp = params("lkas") + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + _, lamp = self.bsm_physical(packer, right=True, right_lamp=True) + rear, front = create_ioniq6_blindspot_status(1, BlindspotStatus(0, 2)) + self.assertTrue(self.safety.safety_rx_hook(self.packet((lamp[0], lamp[1][:7], lamp[2])))) + self.assertFalse(self.safety.safety_tx_hook(self.packet(rear))) + self.bsm_physical(packer, right=True, right_lamp=True, counter=2, stamp=1_010_000) + self.assertTrue(self.safety.safety_tx_hook(self.packet(rear))) + self.assertTrue(self.safety.safety_tx_hook(self.packet(front))) + + def test_bsm_bad_corner_checksum_or_replay_invalidates_prior_detection(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 LONG status profile") + _, cp = params("lkas") + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + for corruption in ("checksum", "replay"): + with self.subTest(corruption=corruption): + self.mode(RAW_LONG["lkas"]) + corner, _ = self.bsm_physical(packer, right=True, right_lamp=True) + rear, front = create_ioniq6_blindspot_status(1, BlindspotStatus(0, 2)) + invalid = bytearray(corner[1]) + if corruption == "checksum": + invalid[3] ^= 0x08 # preserve the old checksum while changing detection + self.assertTrue(self.safety.safety_rx_hook(self.packet((corner[0], bytes(invalid), corner[2])))) + self.assertFalse(self.safety.safety_tx_hook(self.packet(rear))) + self.bsm_physical(packer, right=True, right_lamp=True, counter=2, stamp=1_010_000) + self.assertTrue(self.safety.safety_tx_hook(self.packet(rear))) + self.assertTrue(self.safety.safety_tx_hook(self.packet(front))) + + def test_bsm_stale_required_rx_invalidates_fresh_optional_sources(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 LONG status profile") + self.mode(RAW_LONG["lkas"]) + _, cp = params("lkas") + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + self.bsm_physical(packer, right=True, right_lamp=True, counter=1, stamp=1_000_000) + # Keep optional evidence and every required stream except MDPS fresh. + self.safety.set_timer(2_200_001) + for name, values in (("ACCELERATOR", {"GEAR": 5}), ("TCS", {}), + ("WHEEL_SPEEDS", {}), ("CRUISE_BUTTONS", {}), + ("BLINDSPOTS_FRONT_CORNER_2", {"SIDE_DETECT_STATE": 0x08}), + ("BLINKERS", {"RIGHT_LAMP": 1})): + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + name, 1, {**values, "COUNTER": 2} if name != "BLINKERS" else values))), name) + rear, _ = create_ioniq6_blindspot_status(1, BlindspotStatus(0, 2)) + self.assertFalse(self.safety.safety_tx_hook(self.packet(rear))) + self.safety.safety_tick() + self.assertFalse(self.safety.safety_tx_hook(TESTER_PRESENT)) # stale required RX blocks even diagnostic TX + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg("MDPS", 1, {"COUNTER": 2})))) + self.safety.safety_tick() + self.assertTrue(self.safety.safety_tx_hook(TESTER_PRESENT)) + self.assertTrue(self.safety.safety_tx_hook(self.packet(rear))) + + def test_actual_parser_controller_bsm_pair_passes_native_all_four_profiles(self): + if self.release: + self.skipTest("RELEASE denies all four Ioniq 6 LONG profiles") + for topology in ("lkas", "lkas_alt"): + for aol in (False, True): + with self.subTest(topology=topology, aol=aol): + _, cp = params(topology) + raw = (RAW_AOL if aol else RAW_LONG)[topology] + cp.safetyConfigs[-1].safetyParam = raw + ci = CarInterface(cp) + controller = CarController(DBC[cp.carFingerprint], cp) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + bus = CanBus(cp) + inputs = [packer.make_can_msg(name, bus.ECAN, values) for name, values in ( + ("ACCELERATOR", {"GEAR": 5, "COUNTER": 1}), ("TCS", {"COUNTER": 1}), + ("WHEEL_SPEEDS", {"COUNTER": 1}), ("MDPS", {"COUNTER": 1}), + ("CRUISE_BUTTONS", {"COUNTER": 1}), + ("BLINDSPOTS_FRONT_CORNER_2", {"COUNTER": 1, "SIDE_DETECT_STATE": 0x18}), + ("BLINKERS", {"LEFT_LAMP": 1}), + )] + inputs.append(packer.make_can_msg("CAM_0x362" if topology == "lkas_alt" else "CAM_0x2a4", bus.CAM, {})) + ci.update([(1_000_000_000, inputs)]) + self.mode(raw) + for frame in inputs: + if frame[2] == bus.ECAN: + self.assertTrue(self.safety.safety_rx_hook(self.packet(frame)), hex(frame[0])) + control = structs.CarControl() + _, sent = controller.update(control.as_reader(), ci.CS, 1_000_000_000) + pair = [frame for frame in sent if frame[0] in (0x1BA, 0x1E5)] + self.assertEqual(pair, create_ioniq6_blindspot_status(0, BlindspotStatus(2, 1))) + for frame in pair: + self.assertTrue(self.safety.safety_tx_hook(self.packet(frame)), (topology, aol, hex(frame[0]))) + + def test_exhaustive_angle_bit_namespace_and_release_denial(self): + for low in range(0x8000): + raw = 0x8000 | low + self.mode(raw) + allowed = self.safety.safety_tx_hook(TESTER_PRESENT) + self.assertEqual(allowed, not self.release and raw in (*RAW_LONG.values(), *RAW_AOL.values()), f"raw={raw:#06x}") + + def test_stock_profiles_and_untagged_siblings_keep_their_prior_tx(self): + for topology, raw in (("lkas", 17), ("lkas_alt", 145)): + self.mode(raw) + self.assertFalse(self.safety.safety_tx_hook(TESTER_PRESENT)) + stock, _ = params(topology) + packer = CANPacker(DBC[stock.carFingerprint][Bus.pt]) + steering = create_steering_messages(packer, stock, CanBus(stock), False, False, 0)[0] + self.assertTrue(self.safety.safety_tx_hook(self.packet(steering))) + + def test_fingerprint_elm327_diagnostics_then_long_tester_authority(self): + # pandad holds ELM327 param 1 through fingerprinting until ControlsReady. + # The early UDS transaction uses diagnostic 0x730/8 only; non-diagnostic + # replacement 0x100 must wait for exact Ioniq LONG safety arming. + self.assertEqual(self.safety.set_safety_hooks(structs.CarParams.SafetyModel.elm327, 1), 0) + for payload in (b"\x02\x10\x03\x00\x00\x00\x00\x00", + b"\x03\x28\x01\x01\x00\x00\x00\x00", + b"\x02\x3e\x80\x00\x00\x00\x00\x00"): + self.assertTrue(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(0x730, 1, payload))) + self.assertFalse(self.safety.safety_tx_hook(self.packet(create_ioniq6_radar_heartbeat(0, False, False)))) + self.mode(0x11) + self.assertFalse(self.safety.safety_tx_hook(TESTER_PRESENT)) + if not self.release: + self.mode(RAW_LONG["lkas"]) + self.assertTrue(self.safety.safety_tx_hook(TESTER_PRESENT)) + self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket( + 0x730, 1, b"\x02\x10\x03\x00\x00\x00\x00\x00"))) + + def test_joined_packed_controller_frames_and_late_stock_source_fence(self): + if self.release: + self.skipTest("RELEASE forbids raw Ioniq 6 LONG; denial is covered above") + for topology, raw in RAW_LONG.items(): + with self.subTest(topology=topology): + stock, long_cp = params(topology) + self.assertFalse(stock.openpilotLongitudinalControl) + self.assertTrue(long_cp.openpilotLongitudinalControl) + self.assertEqual(long_cp.safetyConfigs[-1].safetyParam, raw) + ci = CarInterface(long_cp) + controller = CarController(DBC[long_cp.carFingerprint], long_cp) + can_bus = CanBus(long_cp) + packer = CANPacker(DBC[long_cp.carFingerprint][Bus.pt]) + cam_name = "CAM_0x362" if topology == "lkas_alt" else "CAM_0x2a4" + inputs = [ + packer.make_can_msg("ACCELERATOR", can_bus.ECAN, {"GEAR": 5}), + packer.make_can_msg("TCS", can_bus.ECAN, {"ACCEnable": 0, "ACC_REQ": 1}), + packer.make_can_msg("WHEEL_SPEEDS", can_bus.ECAN, {key: 45 for key in + ("WHL_SpdFLVal", "WHL_SpdFRVal", "WHL_SpdRLVal", "WHL_SpdRRVal")}), + packer.make_can_msg("MDPS", can_bus.ECAN, {}), + packer.make_can_msg("STEERING_SENSORS", can_bus.ECAN, {}), + packer.make_can_msg("DOORS_SEATBELTS", can_bus.ECAN, {"DRIVER_SEATBELT": 1}), + packer.make_can_msg("CRUISE_BUTTONS", can_bus.ECAN, {"COUNTER": 1, "CRUISE_BUTTONS": 2}), + packer.make_can_msg(cam_name, can_bus.CAM, {}), + ] + ci.update([(1_000_000_000, inputs)]) + self.assertTrue(ci.CS.out.canValid) + self.mode(raw) + for frame in inputs: + if frame[2] == can_bus.ECAN and frame[0] in (0x35, 0x175, 0xA0, 0xEA, 0x1CF): + self.assertTrue(self.safety.safety_rx_hook(self.packet(frame)), hex(frame[0])) + self.assertTrue(self.safety.safety_config_valid()) + self.safety.set_controls_allowed(True) + control = structs.CarControl() + control.enabled = control.latActive = control.longActive = True + control.actuators.torque = 0.02 + control.actuators.accel = 0.1 + _, sent = controller.update(control.as_reader(), ci.CS, 1_000_000_000) + expected = { + (0x110 if topology == "lkas_alt" else 0x50, 0, 32 if topology == "lkas_alt" else 16), + (0x362 if topology == "lkas_alt" else 0x2A4, 0, 32 if topology == "lkas_alt" else 24), + (0x12A, 1, 16), (0x1E0, 1, 16), (0x1A0, 1, 32), (0x51, 0, 32), + (0x730, 1, 8), (0x160, 1, 16), (0x1EA, 1, 32), (0x200, 1, 8), + (0x345, 1, 8), (0x1DA, 1, 32), (0x100, 0, 24), + } + self.assertEqual({(address, bus, len(payload)) for address, payload, bus in sent}, expected) + for frame in sent: + self.assertTrue(self.safety.safety_tx_hook(self.packet(frame)), f"{topology} {hex(frame[0])} bus={frame[2]}") + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "SCC_CONTROL", can_bus.ECAN, {"COUNTER": 4, "ACCMode": 1})))) + self.assertTrue(self.safety.get_relay_malfunction()) + self.assertFalse(self.safety.safety_tx_hook(TESTER_PRESENT)) + for frame in sent: + self.assertFalse(self.safety.safety_tx_hook(self.packet(frame))) + + def test_dash_icons_keep_exact_native_profile_and_fresh_health_contract(self): + for topology in RAW_LONG: + _, cp = params(topology) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + bus = CanBus(cp) + for raw in (RAW_LONG[topology], RAW_AOL[topology]): + with self.subTest(topology=topology, raw=raw): + self.mode(raw) + admitted = not self.release + icons = CanFDDashIcons() + frames = [] + for tick, enabled, lateral_active, expected in ((0, False, False, 0), (1, False, True, 2), + (2, False, False, 3), (102, False, False, 0)): + icon = icons.update(tick, enabled, lateral_active) + self.assertEqual(icon, expected) + frame = create_lfahda_cluster(packer, bus, enabled, lfa_icon=icon) + self.assertEqual((frame[0], frame[2], len(frame[1])), (0x1E0, 1, 16)) + frames.append(frame) + if admitted: + self.refresh_required_rx(packer, 1, 1_000_000) + for frame in frames: + self.assertEqual(bool(self.safety.safety_tx_hook(self.packet(frame))), admitted) + self.assertFalse(self.safety.safety_tx_hook(self.packet((frame[0], frame[1], 0)))) + self.assertFalse(self.safety.safety_tx_hook(self.packet((frame[0], frame[1][:8], frame[2])))) + if admitted: + self.safety.set_timer(2_200_001) + self.safety.safety_tick() + for frame in frames: + self.assertFalse(self.safety.safety_tx_hook(self.packet(frame))) + self.refresh_required_rx(packer, 2, 2_200_001) + self.safety.safety_tick() + self.assertTrue(self.safety.safety_tx_hook(self.packet(frames[2]))) + stock_scc = packer.make_can_msg("SCC_CONTROL", 1, {"COUNTER": 4, "ACCMode": 1}) + self.assertTrue(self.safety.safety_rx_hook(self.packet(stock_scc))) + self.assertTrue(self.safety.get_relay_malfunction()) + for frame in frames: + self.assertFalse(self.safety.safety_tx_hook(self.packet(frame))) + + def test_wrong_bus_length_and_withheld_status_frames(self): + if self.release: + self.skipTest("RELEASE denies the profile before TX allowlisting") + for raw in RAW_LONG.values(): + self.mode(raw) + self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(0x1BA, 1, bytes(24)))) + self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(0x1E5, 1, bytes(16)))) + self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(0xCB, 1, bytes(24)))) + self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(0x1A0, 0, bytes(32)))) + self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(0x1A0, 1, bytes(24)))) + + def test_recorded_radar_heartbeat_exact_body_pedals_freshness_and_counter(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 LONG profiles") + for raw in (*RAW_LONG.values(), *RAW_AOL.values()): + with self.subTest(raw=hex(raw)): + self.mode(raw) + _, cp = params("lkas_alt" if raw & 0x80 else "lkas") + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + for name, values in (("ACCELERATOR", {"GEAR": 5, "COUNTER": 0}), + ("TCS", {"DriverBraking": 0, "COUNTER": 0}), + ("WHEEL_SPEEDS", {"COUNTER": 0}), ("MDPS", {"COUNTER": 0}), + ("CRUISE_BUTTONS", {"COUNTER": 0})): + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg(name, 1, values)))) + neutral = create_ioniq6_radar_heartbeat(0, False, False) + self.assertTrue(self.safety.safety_tx_hook(self.packet(neutral))) + self.assertFalse(self.safety.safety_tx_hook(self.packet(neutral))) # exact replay + self.assertTrue(self.safety.safety_tx_hook(self.packet(create_ioniq6_radar_heartbeat(1, False, False)))) + forged = bytearray(create_ioniq6_radar_heartbeat(2, False, False)[1]) + forged[13] ^= 1 + forged[:2] = hkg_can_fd_checksum(0x100, None, forged).to_bytes(2, "little") + self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(0x100, 0, forged))) + bad_crc = bytearray(create_ioniq6_radar_heartbeat(2, False, False)[1]) + bad_crc[0] ^= 1 + self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(0x100, 0, bad_crc))) + self.assertFalse(self.safety.safety_tx_hook(self.packet(create_ioniq6_radar_heartbeat(2, True, False)))) + self.assertFalse(self.safety.safety_tx_hook(self.packet(create_ioniq6_radar_heartbeat(2, False, True)))) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "TCS", 1, {"DriverBraking": 1, "COUNTER": 1})))) + self.assertTrue(self.safety.safety_tx_hook(self.packet(create_ioniq6_radar_heartbeat(2, True, False)))) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "ACCELERATOR", 1, {"GEAR": 5, "ACCELERATOR_PEDAL": 1, "COUNTER": 1})))) + self.assertTrue(self.safety.safety_tx_hook(self.packet(create_ioniq6_radar_heartbeat(3, True, True)))) + self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(0x100, 1, neutral[1]))) + self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(0x100, 0, neutral[1][:16]))) + self.safety.set_timer(1_200_000) + self.assertFalse(self.safety.safety_tx_hook(self.packet(create_ioniq6_radar_heartbeat(4, True, True)))) + + def test_ioniq6_50hz_required_sources_remain_healthy_at_150ms(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 LONG profiles") + for raw in (*RAW_LONG.values(), *RAW_AOL.values()): + with self.subTest(raw=hex(raw)): + self.mode(raw) + _, cp = params("lkas_alt" if raw & 0x80 else "lkas") + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + for name, values in (("ACCELERATOR", {"GEAR": 5, "COUNTER": 0}), + ("TCS", {"COUNTER": 0}), ("WHEEL_SPEEDS", {"COUNTER": 0}), + ("MDPS", {"COUNTER": 0}), ("CRUISE_BUTTONS", {"COUNTER": 0})): + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg(name, 1, values)))) + self.safety.set_timer(1_150_000) + for name, values in (("ACCELERATOR", {"GEAR": 5, "COUNTER": 1}), + ("WHEEL_SPEEDS", {"COUNTER": 1}), ("MDPS", {"COUNTER": 1})): + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg(name, 1, values)))) + # TCS and CRUISE_BUTTONS are the 50 Hz required sources; the 100 Hz + # sources above are fresh. The native per-source bound is 200 ms. + self.assertTrue(self.safety.safety_tx_hook(self.packet(create_ioniq6_radar_heartbeat(0, False, False)))) + self.safety.set_timer(1_201_000) + self.assertFalse(self.safety.safety_tx_hook(self.packet(create_ioniq6_radar_heartbeat(1, False, False)))) + + def test_aol_50hz_source_age_does_not_shortchange_healthy_long_permission(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 AOL profiles") + for topology, raw in RAW_AOL.items(): + with self.subTest(topology=topology): + _, cp = params(topology) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + self.mode(raw) + self.safety.set_aol_test_heartbeat(True) + self.refresh_required_rx(packer, 0, 1_000_000) + for counter, button in ((1, 2), (2, 0)): + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": counter, "CRUISE_BUTTONS": button})))) + self.assertTrue(self.safety.get_controls_allowed()) + self.safety.set_timer(1_150_000) + for name, values in (("ACCELERATOR", {"GEAR": 5}), ("WHEEL_SPEEDS", {}), ("MDPS", {})): + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + name, 1, {**values, "COUNTER": 1})))) + self.safety.aol_set_host_request(2) + self.assertEqual(self.safety.aol_get_permission_mask(), 2) + self.safety.set_timer(1_201_000) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + + def test_ioniq6_accel_raw_and_value_each_veto_out_of_range_tx(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 LONG profiles") + for topology, raw in (*RAW_LONG.items(), *RAW_AOL.items()): + with self.subTest(topology=topology, raw=hex(raw)): + _, cp = params(topology) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + self.mode(raw) + self.refresh_required_rx(packer, 0, 1_000_000) + self.safety.set_controls_allowed(True) + if raw in RAW_AOL.values(): + self.safety.set_aol_test_heartbeat(True) + self.safety.aol_set_host_request(2) + + def accel(raw_value, selected_value, selected_packer=packer): + return self.packet(selected_packer.make_can_msg("SCC_CONTROL", 1, { + "aReqRaw": raw_value, "aReqValue": selected_value, "ACCMode": 1, + })) + self.assertTrue(self.safety.safety_tx_hook(accel(0, 0))) + for raw_value, selected_value in ((2.5, 0), (0, 2.5), (-4.0, 0), (0, -4.0)): + self.assertFalse(self.safety.safety_tx_hook(accel(raw_value, selected_value)), + (raw_value, selected_value)) + + def test_stock_radar_heartbeat_recurrence_is_immediate_only_in_exact_long_profile(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 LONG profiles") + for raw in (*RAW_LONG.values(), *RAW_AOL.values()): + self.mode(raw) + self.assertTrue(self.safety.safety_rx_hook(libsafety_py.make_CANPacket(0x100, 1, bytes(24)))) + self.assertFalse(self.safety.get_relay_malfunction()) + self.assertTrue(self.safety.safety_rx_hook(libsafety_py.make_CANPacket(0x100, 0, bytes(8)))) + self.assertTrue(self.safety.get_relay_malfunction()) + self.assertFalse(self.safety.safety_tx_hook(self.packet(create_ioniq6_radar_heartbeat(0, False, False)))) + self.mode(17) + self.assertTrue(self.safety.safety_rx_hook(libsafety_py.make_CANPacket(0x100, 0, bytes(8)))) + self.assertFalse(self.safety.get_relay_malfunction()) + + def test_stock_scc_recurrence_is_immediate_even_with_bad_length_or_crc(self): + if self.release: + self.skipTest("RELEASE denies the profile before relay checks") + for raw in RAW_LONG.values(): + for payload in (bytes(32), bytes(8), b"\xff" * 32): + with self.subTest(raw=raw, length=len(payload), first=payload[0]): + self.mode(raw, init_tests=False) # safety_mode_cnt remains zero + self.assertTrue(self.safety.safety_rx_hook(libsafety_py.make_CANPacket(0x1A0, 1, payload))) + self.assertTrue(self.safety.get_relay_malfunction()) + self.assertFalse(self.safety.safety_tx_hook(TESTER_PRESENT)) + self.mode(raw, init_tests=False) + self.assertTrue(self.safety.safety_rx_hook(libsafety_py.make_CANPacket(0x1A0, 0, bytes(32)))) + self.assertFalse(self.safety.get_relay_malfunction()) + self.assertTrue(self.safety.safety_tx_hook(TESTER_PRESENT)) + + # Ordinary generic LONG retains its existing transition grace. + self.mode(21, init_tests=False) + self.assertTrue(self.safety.safety_rx_hook(libsafety_py.make_CANPacket(0x1A0, 1, bytes(32)))) + self.assertFalse(self.safety.get_relay_malfunction()) + + def test_second_steering_channel_is_one_fresh_mirror(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 LONG profile") + for topology, raw in RAW_LONG.items(): + _, cp = params(topology) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + for torque, active in ((2, True), (0, False), (-2, True)): + with self.subTest(topology=topology, torque=torque, active=active): + self.mode(raw) + for address, name, values in ((0x35, "ACCELERATOR", {"GEAR": 5}), + (0x175, "TCS", {"ACCEnable": 0}), + (0xA0, "WHEEL_SPEEDS", {}), + (0xEA, "MDPS", {}), + (0x1CF, "CRUISE_BUTTONS", {"COUNTER": 1})): + frame = packer.make_can_msg(name, 1, values) + self.assertEqual(frame[0], address) + self.assertTrue(self.safety.safety_rx_hook(self.packet(frame))) + self.safety.set_controls_allowed(True) + lfa, lkas = create_steering_messages(packer, cp, CanBus(cp), True, active, torque) + self.assertFalse(self.safety.safety_tx_hook(self.packet(lkas))) # no independent A-CAN steering + extra = bytearray(lfa[1]) + extra[9] ^= 0x01 # forbidden angle/enable side channel on limited LFA + extra[:2] = hkg_can_fd_checksum(lfa[0], None, extra).to_bytes(2, "little") + self.assertFalse(self.safety.safety_tx_hook(self.packet((lfa[0], bytes(extra), lfa[2])))) + self.assertTrue(self.safety.safety_tx_hook(self.packet(lfa))) + bad = bytearray(lkas[1]) + bad[5] ^= 0x04 + bad[:2] = hkg_can_fd_checksum(lkas[0], None, bad).to_bytes(2, "little") + self.assertFalse(self.safety.safety_tx_hook(self.packet((lkas[0], bytes(bad), lkas[2])))) + self.assertFalse(self.safety.safety_tx_hook(self.packet(lkas))) # mismatch consumes the pair + self.assertTrue(self.safety.safety_tx_hook(self.packet(lfa))) + extra_lkas = bytearray(lkas[1]) + extra_lkas[14] ^= 0x01 + extra_lkas[:2] = hkg_can_fd_checksum(lkas[0], None, extra_lkas).to_bytes(2, "little") + self.assertFalse(self.safety.safety_tx_hook(self.packet((lkas[0], bytes(extra_lkas), lkas[2])))) + self.assertTrue(self.safety.safety_tx_hook(self.packet(lfa))) + wrong_damping = bytearray(lkas[1]) + self.assertEqual(wrong_damping[8], 0) + self.assertEqual(lfa[1][13], 100) + wrong_damping[8] = 100 + wrong_damping[:2] = hkg_can_fd_checksum(lkas[0], None, wrong_damping).to_bytes(2, "little") + self.assertFalse(self.safety.safety_tx_hook(self.packet((lkas[0], bytes(wrong_damping), lkas[2])))) + self.assertFalse(self.safety.safety_tx_hook(self.packet(lkas))) # rejected damping consumes the pair + self.assertTrue(self.safety.safety_tx_hook(self.packet(lfa))) + self.assertTrue(self.safety.safety_tx_hook(self.packet(lkas))) + self.assertFalse(self.safety.safety_tx_hook(self.packet(lkas))) # replay denied + self.assertTrue(self.safety.safety_tx_hook(self.packet(lfa))) + self.safety.set_timer(1_011_000) + self.assertFalse(self.safety.safety_tx_hook(self.packet(lkas))) # stale pair + if topology == "lkas_alt": + self.safety.set_timer(1_012_000) + self.assertTrue(self.safety.safety_tx_hook(self.packet(lfa))) + fca = bytearray(lkas[1]) + fca[13] |= 0x08 + fca[:2] = hkg_can_fd_checksum(lkas[0], None, fca).to_bytes(2, "little") + self.assertFalse(self.safety.safety_tx_hook(self.packet((lkas[0], bytes(fca), lkas[2])))) + + def test_auxiliary_status_frames_reject_other_actuation_payloads(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 LONG profile") + _, cp = params("lkas") + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + for frame in create_adrv_messages(packer, CanBus(cp), 0): + with self.subTest(address=hex(frame[0])): + self.mode(RAW_LONG["lkas"]) + self.assertTrue(self.safety.safety_tx_hook(self.packet(frame))) + altered = bytearray(frame[1]) + altered[7] ^= 0x01 + altered[:2] = hkg_can_fd_checksum(frame[0], None, altered).to_bytes(2, "little") + self.assertFalse(self.safety.safety_tx_hook(self.packet((frame[0], bytes(altered), frame[2])))) + + def test_110_actual_controller_cycles_keep_both_steering_channels_paired(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 LONG profile") + for topology, raw in RAW_LONG.items(): + with self.subTest(topology=topology): + _, cp = params(topology) + ci = CarInterface(cp) + controller = CarController(DBC[cp.carFingerprint], cp) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + bus = CanBus(cp) + self.mode(raw) + control = structs.CarControl() + control.enabled = control.latActive = control.longActive = True + control.actuators.torque = 0.01 + control.actuators.accel = 0.1 + steering_pairs = 0 + for tick in range(110): + self.safety.set_timer(1_000_000 + tick * 10_000) + inputs = [ + packer.make_can_msg("ACCELERATOR", bus.ECAN, {"GEAR": 5}), + packer.make_can_msg("TCS", bus.ECAN, {"ACCEnable": 0, "ACC_REQ": 1}), + packer.make_can_msg("WHEEL_SPEEDS", bus.ECAN, {key: 45 for key in + ("WHL_SpdFLVal", "WHL_SpdFRVal", "WHL_SpdRLVal", "WHL_SpdRRVal")}), + packer.make_can_msg("MDPS", bus.ECAN, {}), + packer.make_can_msg("STEERING_SENSORS", bus.ECAN, {}), + packer.make_can_msg("DOORS_SEATBELTS", bus.ECAN, {"DRIVER_SEATBELT": 1}), + packer.make_can_msg("CRUISE_BUTTONS", bus.ECAN, {"CRUISE_BUTTONS": 2}), + packer.make_can_msg("CAM_0x362" if topology == "lkas_alt" else "CAM_0x2a4", bus.CAM, {}), + ] + ci.update([(1_000_000_000 + tick * 10_000_000, inputs)]) + self.assertTrue(ci.CS.out.canValid) + for frame in inputs: + if frame[2] == bus.ECAN and frame[0] in (0x35, 0x175, 0xA0, 0xEA, 0x1CF): + self.assertTrue(self.safety.safety_rx_hook(self.packet(frame)), (tick, hex(frame[0]))) + self.safety.set_controls_allowed(True) + _, sent = controller.update(control.as_reader(), ci.CS, 1_000_000_000 + tick * 10_000_000) + for frame in sent: + self.assertTrue(self.safety.safety_tx_hook(self.packet(frame)), (tick, hex(frame[0]), frame[2])) + steering_pairs += int({0x12A, 0x110 if topology == "lkas_alt" else 0x50}.issubset({f[0] for f in sent})) + self.assertEqual(steering_pairs, 110) + + def test_brake_cancel_and_rejected_lfa_revoke_pending_mirror(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 LONG profile") + for topology, raw in RAW_LONG.items(): + _, cp = params(topology) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + bus = CanBus(cp) + for event in ("brake", "cancel", "rejected_lfa"): + with self.subTest(topology=topology, event=event): + self.mode(raw) + for name, values in (("ACCELERATOR", {"GEAR": 5}), ("TCS", {"DriverBraking": 0}), + ("WHEEL_SPEEDS", {key: 45 for key in + ("WHL_SpdFLVal", "WHL_SpdFRVal", "WHL_SpdRLVal", "WHL_SpdRRVal")}), + ("MDPS", {}), ("CRUISE_BUTTONS", {"COUNTER": 1})): + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg(name, bus.ECAN, values)))) + self.safety.set_controls_allowed(True) + lfa, lkas = create_steering_messages(packer, cp, bus, True, True, 2) + self.assertTrue(self.safety.safety_tx_hook(self.packet(lfa))) + if event == "brake": + intervening = packer.make_can_msg("TCS", bus.ECAN, {"DriverBraking": 1}) + elif event == "cancel": + intervening = packer.make_can_msg("CRUISE_BUTTONS", bus.ECAN, {"COUNTER": 2, "CRUISE_BUTTONS": 4}) + else: + payload = bytearray(lfa[1]) + payload[9] ^= 0x01 + payload[:2] = hkg_can_fd_checksum(lfa[0], None, payload).to_bytes(2, "little") + self.assertFalse(self.safety.safety_tx_hook(self.packet((lfa[0], bytes(payload), lfa[2])))) + self.assertFalse(self.safety.safety_tx_hook(self.packet(lkas))) + continue + self.assertTrue(self.safety.safety_rx_hook(self.packet(intervening))) + self.assertFalse(self.safety.lateral_controls_allowed()) + self.assertFalse(self.safety.safety_tx_hook(self.packet(lkas))) + + def test_aol_exact_tagged_profiles_require_physical_gesture_and_set_release(self): + if self.release: + for raw in RAW_AOL.values(): + self.mode(raw) + self.safety.set_aol_test_heartbeat(True) + self.safety.aol_set_host_request(3) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + return + for topology, raw in RAW_AOL.items(): + for gesture in ("LDA_BTN", "ADAPTIVE_CRUISE_MAIN_BTN"): + with self.subTest(topology=topology, gesture=gesture): + _, cp = params(topology) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + bus = CanBus(cp) + self.mode(raw) + self.safety.set_aol_test_heartbeat(True) + for name, values in (("ACCELERATOR", {"GEAR": 5}), ("TCS", {"ACCEnable": 0}), + ("WHEEL_SPEEDS", {key: 45 for key in + ("WHL_SpdFLVal", "WHL_SpdFRVal", "WHL_SpdRLVal", "WHL_SpdRRVal")}), + ("MDPS", {}), ("CRUISE_BUTTONS", {"COUNTER": 1})): + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg(name, bus.ECAN, values)))) + self.safety.aol_set_host_request(3) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) # ACCEnable availability is not main + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", bus.ECAN, {"COUNTER": 2, gesture: 1})))) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) # fresh press authorizes; host request toggles + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", bus.ECAN, {"COUNTER": 3, gesture: 0})))) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", bus.ECAN, {"COUNTER": 4, "CRUISE_BUTTONS": 2})))) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", bus.ECAN, {"COUNTER": 5, "CRUISE_BUTTONS": 0})))) + self.safety.aol_set_host_request(3) + self.assertTrue(self.safety.get_controls_allowed()) + self.assertEqual(self.safety.aol_get_permission_mask(), 3) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "TCS", bus.ECAN, {"DriverBraking": 1})))) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) # independent lateral persists through brake + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", bus.ECAN, {"COUNTER": 6, "CRUISE_BUTTONS": 4})))) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.safety.aol_set_host_request(1) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) # no stale latch after cancel + + def test_full_engagement_without_lkas_retains_only_lateral_after_brake(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 AOL profiles") + for topology, raw in RAW_AOL.items(): + with self.subTest(topology=topology): + _, cp = params(topology) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + self.mode(raw) + self.refresh_required_rx(packer, 1, 1_000_000) + self.safety.set_aol_test_heartbeat(True) + self.safety.set_timer(1_005_000) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": 2, "CRUISE_BUTTONS": 2})))) + self.safety.set_timer(1_010_000) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": 3, "CRUISE_BUTTONS": 0})))) + self.assertTrue(self.safety.get_controls_allowed()) + self.safety.aol_set_host_request(3) + self.assertEqual(self.safety.aol_get_permission_mask(), 3) + self.safety.set_timer(1_020_000) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "TCS", 1, {"DriverBraking": 1, "COUNTER": 2})))) + self.assertFalse(self.safety.get_controls_allowed()) + self.safety.aol_set_host_request(1) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + for frame in create_steering_messages(packer, cp, CanBus(cp), True, True, 2): + self.assertTrue(self.safety.safety_tx_hook(self.packet(frame))) + _, state, controller = controller_fixture(topology == "lkas_alt", aol=True) + command = structs.CarControl(enabled=True, longActive=True) + command.actuators.accel = .1 + command.actuators.longControlState = LongState.starting + _, frames = controller.update(command.as_reader(), state, 1_020_000_000) + active_scc = next(frame for frame in frames if frame[0] == 0x1A0) + self.assertFalse(self.safety.safety_tx_hook(self.packet(active_scc))) + for counter in range(4, 35): + self.refresh_required_rx(packer, counter, 1_020_000 + counter * 10_000) + self.safety.aol_set_host_request(0) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.safety.aol_set_host_request(1) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + + def test_full_engagement_release_credit_cannot_survive_native_revocation(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 AOL profiles") + for topology, raw in RAW_AOL.items(): + for reset in ("heartbeat", "timeout", "unclaimed", "long_only", "zero_only"): + with self.subTest(topology=topology, reset=reset): + _, cp = params(topology) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + self.mode(raw) + self.refresh_required_rx(packer, 1, 1_000_000) + self.safety.set_aol_test_heartbeat(True) + for counter, button in ((2, 2), (3, 0)): + self.safety.set_timer(1_000_000 + counter * 5_000) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": counter, "CRUISE_BUTTONS": button})))) + self.safety.aol_set_host_request(0 if reset in ("zero_only", "unclaimed") else 2 if reset == "long_only" else 3) + self.assertEqual(self.safety.aol_get_permission_mask(), 0 if reset in ("zero_only", "unclaimed") else 2 if reset == "long_only" else 3) + if reset == "heartbeat": + self.safety.set_aol_test_heartbeat(False) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.safety.set_aol_test_heartbeat(True) + elif reset in ("timeout", "unclaimed"): + self.safety.set_timer(1_400_000) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + stamp = 1_410_000 if reset in ("timeout", "unclaimed") else 1_020_000 + self.refresh_required_rx(packer, 4, stamp) + if reset in ("heartbeat", "timeout", "unclaimed"): + self.safety.aol_set_host_request(3) + self.assertEqual(self.safety.aol_get_permission_mask(), 3) + self.safety.set_timer(stamp + 5_000) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "TCS", 1, {"DriverBraking": 1, "COUNTER": 5})))) + self.safety.aol_set_host_request(1) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + + def test_new_physical_enable_release_restarts_engaged_heartbeat_grace(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 AOL profiles") + for topology, raw in RAW_AOL.items(): + for button in (1, 2): + with self.subTest(topology=topology, button=button): + _, cp = params(topology) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + self.mode(raw) + self.refresh_required_rx(packer, 1, 1_000_000) + self.safety.set_controls_allowed(True) + self.safety.set_aol_test_heartbeat(False) + self.safety.set_heartbeat_engaged_mismatches(2) + self.safety.set_timer(1_005_000) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": 2, "CRUISE_BUTTONS": button})))) + self.assertEqual(self.safety.get_heartbeat_engaged_mismatches(), 2) + self.safety.set_timer(1_010_000) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": 3, "CRUISE_BUTTONS": 0})))) + self.assertTrue(self.safety.get_controls_allowed()) + self.assertEqual(self.safety.get_heartbeat_engaged_mismatches(), 0) + + def test_aol_long_permission_requires_explicit_host_long_request(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 AOL profiles") + for topology, raw in RAW_AOL.items(): + with self.subTest(topology=topology): + _, cp = params(topology) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + self.mode(raw) + self.safety.set_aol_test_heartbeat(True) + for name, values in (("ACCELERATOR", {"GEAR": 5}), ("TCS", {"ACCEnable": 0}), + ("WHEEL_SPEEDS", {key: 45 for key in + ("WHL_SpdFLVal", "WHL_SpdFRVal", "WHL_SpdRLVal", "WHL_SpdRRVal")}), + ("MDPS", {}), ("CRUISE_BUTTONS", {"COUNTER": 1})): + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg(name, 1, values)))) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": 2, "LDA_BTN": 1})))) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": 3})))) + for counter, button in ((4, 2), (5, 0)): + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": counter, "CRUISE_BUTTONS": button})))) + self.assertTrue(self.safety.get_controls_allowed()) + self.safety.aol_set_host_request(1) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + self.safety.aol_set_host_request(0) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + + def test_aol_idle_bootstrap_and_rebootstrap_after_real_heartbeat_loss(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 AOL profiles") + _, cp = params("lkas") + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + bus = CanBus(cp) + self.mode(RAW_AOL["lkas"]) + for name, values in (("ACCELERATOR", {}), ("TCS", {}), ("WHEEL_SPEEDS", {}), + ("MDPS", {}), ("CRUISE_BUTTONS", {"COUNTER": 1})): + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg(name, 1, values)))) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": 2, "LDA_BTN": 1, "ADAPTIVE_CRUISE_MAIN_BTN": 1})))) + # Actual neutral controller frames arrive at 100 Hz before first 10 Hz USB + # request. They must not erase a fresh gesture merely because idle heartbeat + # is still false. Simultaneous main/LDA is one physical edge. + for tick in range(3, 9): + self.safety.set_timer(1_000_000 + tick * 10_000) + lfa, lkas = create_steering_messages(packer, cp, bus, False, False, 0) + self.assertTrue(self.safety.safety_tx_hook(self.packet(lfa))) + self.assertTrue(self.safety.safety_tx_hook(self.packet(lkas))) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.safety.set_aol_test_heartbeat(True) + self.safety.aol_set_host_request(1) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": 3})))) + self.safety.set_aol_test_heartbeat(False) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + # A lost heartbeat revokes the old token. Healthy idle can then accept a + # later fresh gesture through the same bounded bootstrap path. + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": 4})))) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": 5, "LDA_BTN": 1})))) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.safety.set_aol_test_heartbeat(True) + self.safety.aol_set_host_request(1) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + + def test_aol_armed_zero_request_preserves_token_but_never_steers(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 AOL profiles") + for topology, raw in RAW_AOL.items(): + with self.subTest(topology=topology): + _, cp = params(topology) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + bus = CanBus(cp) + self.mode(raw) + self.refresh_required_rx(packer, 1, 1_000_000) + self.safety.set_aol_test_heartbeat(True) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": 2, "LDA_BTN": 1})))) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": 3})))) + self.safety.aol_set_host_request(1) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + for tick in range(4, 104): + self.refresh_required_rx(packer, tick, 1_000_000 + tick * 10_000) + self.safety.aol_set_host_request(0) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + inactive = create_steering_messages(packer, cp, bus, False, False, 0) + active = create_steering_messages(packer, cp, bus, False, True, 1) + for frame in inactive: + self.assertTrue(self.safety.safety_tx_hook(self.packet(frame))) + self.assertFalse(self.safety.safety_tx_hook(self.packet(active[1]))) + self.safety.aol_set_host_request(1) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + self.safety.set_aol_test_heartbeat(False) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.safety.set_aol_test_heartbeat(True) + self.safety.aol_set_host_request(1) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + + def test_aol_startup_held_cancel_timeout_and_bad_counter(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 AOL profiles") + _, cp = params("lkas") + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + self.mode(RAW_AOL["lkas"]) + self.safety.set_aol_test_heartbeat(True) + for name, values in (("ACCELERATOR", {}), ("TCS", {}), ("WHEEL_SPEEDS", {}), ("MDPS", {}), + ("CRUISE_BUTTONS", {"COUNTER": 1, "LDA_BTN": 1})): + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg(name, 1, values)))) + self.safety.aol_set_host_request(1) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) # held at safety init is ignored + for counter, lda in ((2, 0), (3, 1)): + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": counter, "LDA_BTN": lda})))) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": 4, "CRUISE_BUTTONS": 4})))) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + for counter, lda in ((5, 0), (6, 1)): + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": counter, "LDA_BTN": lda})))) + self.safety.aol_set_host_request(1) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + for _ in range(5): + self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": 6, "LDA_BTN": 1}))) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) # wrong counter cannot authorize + self.mode(RAW_AOL["lkas"]) + self.safety.set_aol_test_heartbeat(True) + for name, values in (("ACCELERATOR", {}), ("TCS", {}), ("WHEEL_SPEEDS", {}), ("MDPS", {}), + ("CRUISE_BUTTONS", {"COUNTER": 1})): + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg(name, 1, values)))) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": 2, "LDA_BTN": 1})))) + self.safety.aol_set_host_request(1) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + for counter in range(1, 17): + self.refresh_required_rx(packer, counter, 1_000_000 + counter * 20_000) + self.assertTrue(self.safety.safety_config_valid()) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) # request timeout clears token + + def test_aol_unclaimed_gesture_expires_before_first_request(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 AOL profiles") + self.mode(RAW_AOL["lkas"]) + _, cp = params("lkas") + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + for name, values in (("ACCELERATOR", {}), ("TCS", {}), ("WHEEL_SPEEDS", {}), ("MDPS", {}), + ("CRUISE_BUTTONS", {"COUNTER": 1})): + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg(name, 1, values)))) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": 2, "LDA_BTN": 1})))) + for counter in range(1, 17): + self.refresh_required_rx(packer, counter + 2, 1_000_000 + counter * 20_000) + self.assertTrue(self.safety.safety_config_valid()) + # No status/TX getter has run since the gesture. A late first request + # cannot claim an already expired physical token. + self.safety.set_aol_test_heartbeat(True) + self.safety.aol_set_host_request(1) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + + def test_aol_long_only_request_cannot_claim_old_lateral_gesture(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 AOL profiles") + for topology, raw in RAW_AOL.items(): + with self.subTest(topology=topology): + self.mode(raw) + _, cp = params(topology) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + for name, values in (("ACCELERATOR", {}), ("TCS", {}), ("WHEEL_SPEEDS", {}), + ("MDPS", {}), ("CRUISE_BUTTONS", {"COUNTER": 1})): + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg(name, 1, values)))) + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": 2, "LDA_BTN": 1})))) + self.safety.set_aol_test_heartbeat(True) + self.safety.aol_set_host_request(2) # longitudinal only, never claims the lateral token + self.assertEqual(self.safety.aol_get_permission_mask() & 1, 0) + for counter in range(1, 17): + self.refresh_required_rx(packer, counter + 2, 1_000_000 + counter * 20_000) + self.safety.aol_set_host_request(2) + self.assertEqual(self.safety.aol_get_permission_mask() & 1, 0) + self.assertTrue(self.safety.safety_config_valid()) + self.safety.aol_set_host_request(1) # no new physical gesture after the deadline + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + + def test_aol_four_actual_controller_axis_modes(self): + if self.release: + self.skipTest("RELEASE denies the Ioniq 6 AOL profiles") + for topology, raw in RAW_AOL.items(): + for lateral, longitudinal in ((False, False), (True, False), (False, True), (True, True)): + with self.subTest(topology=topology, lateral=lateral, longitudinal=longitudinal): + cp, cs, controller = controller_fixture(topology == "lkas_alt", aol=True) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + self.mode(raw) + self.safety.set_aol_test_heartbeat(True) + for name, values in (("ACCELERATOR", {"GEAR": 5}), ("TCS", {"ACCEnable": 0}), + ("WHEEL_SPEEDS", {key: 45 for key in + ("WHL_SpdFLVal", "WHL_SpdFRVal", "WHL_SpdRLVal", "WHL_SpdRRVal")}), + ("MDPS", {}), ("CRUISE_BUTTONS", {"COUNTER": 1})): + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg(name, 1, values)))) + if lateral: + for counter, lda in ((2, 1), (3, 0)): + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": counter, "LDA_BTN": lda})))) + if longitudinal: + for counter, button in ((4, 2), (5, 0)): + self.assertTrue(self.safety.safety_rx_hook(self.packet(packer.make_can_msg( + "CRUISE_BUTTONS", 1, {"COUNTER": counter, "CRUISE_BUTTONS": button})))) + mask = int(lateral) | (int(longitudinal) << 1) + self.safety.aol_set_host_request(mask) + self.assertEqual(self.safety.aol_get_permission_mask(), mask) + cc = structs.CarControl() + cc.enabled = longitudinal # actual AOL lateral-only keeps standard enable off + cc.latActive, cc.longActive = lateral, longitudinal + cc.actuators.torque = 0.01 + cc.actuators.accel = 0.1 + cc.actuators.longControlState = LongState.starting + _, sent = controller.update(cc.as_reader(), cs, 1_000_000_000) + lfa = next(f for f in sent if f[0] == 0x12A) + parsed = CANParser(DBC[cp.carFingerprint][Bus.pt], [("LFA", 0)], 1) + parsed.update((1_000_000_000, [lfa])) + self.assertEqual(parsed.vl["LFA"]["LKA_SysIndReq"], 2 if lateral or longitudinal else 1) + camera_name = "LKAS_ALT" if topology == "lkas_alt" else "LKAS" + camera_addr = 0x110 if topology == "lkas_alt" else 0x50 + camera = CANParser(DBC[cp.carFingerprint][Bus.pt], [(camera_name, 0)], 0) + camera.update((1_000_000_000, [next(f for f in sent if f[0] == camera_addr)])) + self.assertEqual(camera.vl[camera_name]["LKA_SysIndReq"], 2 if lateral or longitudinal else 1) + for frame in sent: + self.assertTrue(self.safety.safety_tx_hook(self.packet(frame)), + (topology, lateral, longitudinal, hex(frame[0]))) + if not longitudinal: + active = structs.CarControl() + active.enabled = active.longActive = True + active.actuators.accel = 0.1 + active.actuators.longControlState = LongState.starting + _, active_frames = CarController(DBC[cp.carFingerprint], cp).update(active.as_reader(), cs, 1_000_000_000) + self.assertFalse(self.safety.safety_tx_hook(self.packet(next(f for f in active_frames if f[0] == 0x1A0)))) + if not lateral: + active_lfa = create_steering_messages(packer, cp, CanBus(cp), True, True, 2)[0] + self.assertFalse(self.safety.safety_tx_hook(self.packet(active_lfa))) diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_ioniq6_stock_aol.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_ioniq6_stock_aol.py new file mode 100644 index 0000000000..66c17515ba --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_ioniq6_stock_aol.py @@ -0,0 +1,105 @@ +import unittest + +from opendbc.can import CANPacker +from opendbc.car import Bus, structs +from opendbc.car.hyundai.values import DBC +from opendbc.safety.tests.libsafety import libsafety_py +from opendbc.safety.tests.test_hyundai_ioniq6_long import params + + +class TestHyundaiIoniq6StockAol(unittest.TestCase): + def mode(self, raw): + self.safety = libsafety_py.libsafety + self.assertEqual(self.safety.set_safety_hooks(structs.CarParams.SafetyModel.hyundaiCanfd, raw), 0) + self.safety.init_tests() + self.safety.set_timer(1_000_000) + self.packer = CANPacker(DBC[params("lkas_alt" if raw & 0x80 else "lkas")[0].carFingerprint][Bus.pt]) + self.steer = "LKAS_ALT" if raw & 0x80 else "LKAS" + self.counters = {} + + def packet(self, name, bus, values): + frame = self.packer.make_can_msg(name, bus, values) + return libsafety_py.make_CANPacket(frame[0], frame[2], frame[1]) + + def rx(self, name, values=None): + self.counters[name] = self.counters.get(name, 0) + 1 + self.assertTrue(self.safety.safety_rx_hook(self.packet(name, 1, { + "COUNTER": self.counters[name] % (16 if name == "CRUISE_BUTTONS" else 256), **(values or {})})), name) + + def healthy(self, acc=False): + for name, values in (("ACCELERATOR", {"GEAR": 5}), ("TCS", {}), ("WHEEL_SPEEDS", {}), + ("MDPS", {}), ("CRUISE_BUTTONS", {}), ("SCC_CONTROL", {"ACCMode": int(acc)})): + self.rx(name, values) + + def tx(self, torque, request): + frame = self.packer.make_can_msg(self.steer, 0, {"StrTqReqVal": torque, "ActToiSta": request}) + data = frame[1] + self.assertEqual((((data[6] & 0xF) << 7) | (data[5] >> 1)) - 1024, torque) + self.assertEqual((data[6] >> 4) & 1, request) + return self.safety.safety_tx_hook(libsafety_py.make_CANPacket(frame[0], frame[2], data)) + + def arm(self): + self.rx("CRUISE_BUTTONS", {"LDA_BTN": 1}) + self.rx("CRUISE_BUTTONS", {}) + self.safety.set_aol_test_heartbeat(True) + self.safety.aol_set_host_request(3) + + def test_stock_scc_and_physical_gesture_share_request_gate(self): + for raw in (0x811, 0x891): + with self.subTest(raw=raw): + self.mode(raw) + self.healthy() + self.safety.set_aol_test_heartbeat(True) + self.safety.aol_set_host_request(3) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.assertFalse(self.tx(1, 1)) + self.arm() + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + self.assertTrue(self.tx(1, 1)) + self.safety.aol_set_host_request(0) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.assertFalse(self.tx(1, 1)) + self.assertTrue(self.tx(0, 0)) + self.safety.aol_set_host_request(3) + # LDA authorizes lateral AOL, but is deliberately not a stock ACC + # enable-button interaction in hyundai_common_cruise_buttons_check. + self.rx("SCC_CONTROL", {"ACCMode": 1}) + self.assertFalse(self.safety.get_controls_allowed()) + self.rx("SCC_CONTROL", {"ACCMode": 0}) + self.rx("CRUISE_BUTTONS", {"CRUISE_BUTTONS": 2}) # physical SET + self.rx("SCC_CONTROL", {"ACCMode": 1}) + self.assertTrue(self.safety.get_controls_allowed()) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + self.rx("SCC_CONTROL", {"ACCMode": 0}) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + for name, bus, values in (("SCC_CONTROL", 1, {"ACCMode": 1, "aReqRaw": 1, "aReqValue": 1}), + ("LFA", 1, {"StrTqReqVal": 1, "ActToiSta": 1})): + self.assertFalse(self.safety.safety_tx_hook(self.packet(name, bus, values))) + self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(0x730, 1, b"\x02\x3e\x80\0\0\0\0\0"))) + + def test_cancel_heartbeat_expiry_and_reset_revoke_latch(self): + for raw in (0x811, 0x891): + for cause in ("cancel", "heartbeat", "expiry", "reset", "relay"): + with self.subTest(raw=raw, cause=cause): + self.mode(raw) + self.healthy() + self.arm() + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + if cause == "cancel": + self.rx("CRUISE_BUTTONS", {"CRUISE_BUTTONS": 4}) + elif cause == "heartbeat": + self.safety.set_aol_test_heartbeat(False) + elif cause == "expiry": + self.safety.set_timer(1_600_001) + elif cause == "relay": + self.safety.set_relay_malfunction(True) + else: + self.mode(raw) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.assertFalse(self.tx(1, 1)) + + +if __name__ == "__main__": + unittest.main() diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_kona_aol.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_kona_aol.py new file mode 100644 index 0000000000..4e4a7cdad9 --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_kona_aol.py @@ -0,0 +1,69 @@ +"""Main-only Kona and unchanged Forte shared independent authority boundaries.""" +import unittest +from opendbc.car.structs import CarParams +from opendbc.safety.tests import test_hyundai_forte_aol as fixture + + +class TestHyundaiKonaAol(unittest.TestCase): + setUp = fixture.TestHyundaiForteAol.setUp + tearDown = fixture.TestHyundaiForteAol.tearDown + reset = fixture.TestHyundaiForteAol.reset + rx = fixture.TestHyundaiForteAol.rx + feed = fixture.TestHyundaiForteAol.feed + request = fixture.TestHyundaiForteAol.request + + def test_exact_main_kona_and_unchanged_forte_profiles(self): + for word, source in ((0x1440, None), (0x1400, None), (0x1c00, 'BCM_PO_11')): + self.reset(word) + for _ in range(6): + self.feed(main=True, source=source) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertEqual(self.request(1), 1) + self.assertEqual(self.request(2), 0) + self.feed(main=False, source=source) + self.assertEqual(self.request(1), 0) + + def test_kona_namespace_experience_and_mode_denials(self): + for word in (0x1040, 0x1840, 0x1c43, 0x1c42, 0x1c44, 0x1443, 0x1442, 0x1444, 0x9440, 0x3440): + self.reset(word) + for _ in range(6): + self.feed(main=True, source=None) + self.assertEqual(self.request(1), 0, hex(word)) + for experience in (0, 1, 33): + self.reset(0x1440, experience=experience) + for _ in range(6): + self.feed(main=True, source=None) + self.assertEqual(self.request(1), 0) + self.reset(0x1440, mode=CarParams.SafetyModel.hyundaiCanfd) + self.assertEqual(self.request(1), 0) + + def test_kona_no_lda_authorization_and_no_longitudinal_permission(self): + self.reset(0x1440) + for source in ('BCM_PO_11', 'CLU13'): + for pressed in (False, True, False): + self.feed(main=False, source=source, pressed=pressed) + self.assertEqual(self.request(1), 0) + for _ in range(6): + self.feed(main=True, source=None) + self.assertEqual(self.request(1), 1) + self.assertEqual(self.request(3), 1) + self.assertEqual(self.request(2), 0) + + def test_kona_lease_and_heartbeat_loss_require_fresh_physical_main(self): + self.reset(0x1440) + for _ in range(6): + self.feed(main=True, source=None) + self.assertEqual(self.request(1), 1) + self.safety.set_aol_test_heartbeat(False) + self.assertEqual(self.request(1), 0) + self.safety.set_aol_test_heartbeat(True) + self.assertEqual(self.request(1), 0) + self.feed(main=True, source=None) + self.assertEqual(self.request(1), 1) + self.now += 300_001 + self.safety.set_timer(self.now) + self.assertEqual(self.request(1), 0) + self.assertEqual(self.request(1), 0) + for _ in range(6): + self.feed(main=True, source=None) + self.assertEqual(self.request(1), 1) diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_kona_lda_aol.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_kona_lda_aol.py new file mode 100644 index 0000000000..d9387b3e57 --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_kona_lda_aol.py @@ -0,0 +1,37 @@ +"""Exact Kona LDA uses the already-qualified classic physical token owner.""" +import unittest +from opendbc.car.structs import CarParams +from opendbc.safety.tests import test_hyundai_forte_aol as fixture + + +class TestHyundaiKonaLdaAol(unittest.TestCase): + setUp = fixture.TestHyundaiForteAol.setUp + tearDown = fixture.TestHyundaiForteAol.tearDown + rx = fixture.TestHyundaiForteAol.rx + feed = fixture.TestHyundaiForteAol.feed + request = fixture.TestHyundaiForteAol.request + + def reset(self, word=0x1c40, experience=32, mode=CarParams.SafetyModel.hyundai): + return fixture.TestHyundaiForteAol.reset(self, word, experience, mode) + + test_combined_sources_do_not_double_toggle_and_neutral_retains_token = fixture.TestHyundaiForteAol.test_combined_sources_do_not_double_toggle_and_neutral_retains_token + test_unclaimed_token_and_missing_required_sources_expire = fixture.TestHyundaiForteAol.test_unclaimed_token_and_missing_required_sources_expire + test_new_or_expired_held_source_requires_its_own_neutral = fixture.TestHyundaiForteAol.test_new_or_expired_held_source_requires_its_own_neutral + + def test_exact_kona_lda_namespace_and_no_independent_longitudinal(self): + for source in ('BCM_PO_11','CLU13'): + self.reset() + for _ in range(6): self.feed(main=False, source=source) + self.feed(main=False,source=source,pressed=True) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertEqual(self.request(1),1) + self.assertEqual(self.request(3),1) + self.assertEqual(self.request(2),0) + for word in (0x1840,0x1c43,0x1c42,0x1c44,0x9c40,0x3c40): + self.reset(word) + for _ in range(6): self.feed(main=True) + self.assertEqual(self.request(1),0) + for experience in (0,1,33): + self.reset(experience=experience) + for _ in range(6): self.feed(main=True) + self.assertEqual(self.request(1),0) diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_mrr35_angle_four.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_mrr35_angle_four.py new file mode 100644 index 0000000000..eb0af01dda --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_mrr35_angle_four.py @@ -0,0 +1,491 @@ +import math +import unittest + +from opendbc.can import CANPacker, CANParser +from opendbc.car import Bus, gen_empty_fingerprint, structs +from opendbc.car.fw_versions import match_fw_to_car +from opendbc.car.fingerprints import all_legacy_fingerprint_cars +from opendbc.car.hyundai.carcontroller import CarController +from opendbc.car.hyundai.carstate import CarState +from opendbc.car.hyundai.hyundaicanfd import CanBus, hkg_can_fd_checksum, create_angle_steering_messages +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.radar_interface import RadarInterface, MRR35_RADAR_START_ADDR +from opendbc.car.hyundai.values import CAR, DBC, HyundaiFlags, HyundaiSafetyFlags, CANFD_ANGLE_MODEL_BITS, CANFD_ANGLE_OBSERVED_ADAS_BIT +from opendbc.car.hyundai.fingerprints import FW_VERSIONS +from opendbc.car.lateral import get_max_angle_vm +from opendbc.safety.tests.libsafety import libsafety_py + + +ANGLE_CARS = (CAR.GENESIS_GV70_2026, CAR.GENESIS_GV70_ELECTRIFIED_2ND_GEN, + CAR.GENESIS_GV80_2025, CAR.HYUNDAI_IONIQ_9) +TOPOLOGIES = ("lka", "lka_alt", "lfa", "lfa_alt") + + +def params(car, topology="lka_alt", *, alpha=False, release=False, adas_cmd=False, radar=False, hybrid=False): + fp = gen_empty_fingerprint() + if topology == "lka": + fp[2][0x50] = 16 + elif topology == "lka_alt": + fp[2][0x110] = 32 + if topology.startswith("lka"): + fp[1][0x1CF] = 8 + elif topology == "lfa": + fp[0][0x1CF] = 8 + else: + fp[0][0x1AA] = 16 + if adas_cmd: + fp[2][0xCB] = 24 + if radar: + fp[0][MRR35_RADAR_START_ADDR] = 24 + if hybrid: + fp[1 if topology.startswith("lka") else 0][0xFA] = 8 + return CarInterface.get_params(car, fp, [], alpha, release, False) + + +class TestHyundaiMRR35AngleFour(unittest.TestCase): + @staticmethod + def packet(frame): + return libsafety_py.make_CANPacket(frame[0], frame[2], frame[1]) + + def mode(self, cp, extra=0): + safety = libsafety_py.libsafety + self.assertEqual(safety.set_safety_hooks(structs.CarParams.SafetyModel.hyundaiCanfd, + cp.safetyConfigs[-1].safetyParam | extra), 0) + safety.init_tests() + safety.set_timer(1_000_000) + return safety + + @staticmethod + def source_frames(cp, topology, *, brake=False, speed=30.0, cruise=True, measured_angle=0.0, gear_value=5): + can = CanBus(cp) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + ev = bool(cp.flags & HyundaiFlags.EV) + fuel = "ACCELERATOR" if ev else "ACCELERATOR_BRAKE_ALT" + gear = "ACCELERATOR" if ev else "GEAR_ALT_2" + button = "CRUISE_BUTTONS_ALT" if topology == "lfa_alt" else "CRUISE_BUTTONS" + scc_bus = can.CAM if topology.startswith("lfa") else can.ECAN + inputs = [(fuel, can.ECAN, {"GEAR": gear_value} if ev else {}), + ("TCS", can.ECAN, {"DriverBraking": int(brake)}), + ("WHEEL_SPEEDS", can.ECAN, {key: speed for key in + ("WHL_SpdFLVal", "WHL_SpdFRVal", "WHL_SpdRLVal", "WHL_SpdRRVal")}), + ("MDPS", can.ECAN, {"MDPS_PaStrAnglVal": measured_angle}), + ("STEERING_SENSORS", can.ECAN, {"STEERING_ANGLE": measured_angle}), + ("DOORS_SEATBELTS", can.ECAN, {"DRIVER_SEATBELT": 1}), + ("BLINKERS", can.ECAN, {})] + if gear != fuel: + inputs.append((gear, can.ECAN, {"GEAR": gear_value})) + inputs += [(button, can.ECAN, {"CRUISE_BUTTONS": 2}), + ("SCC_CONTROL", scc_bus, {"ACCMode": 0}), + ("SCC_CONTROL", scc_bus, {"ACCMode": int(cruise)})] + if topology.startswith("lka"): + inputs.append(("LKAS_ALT" if topology == "lka_alt" else "LKAS", can.CAM, {})) + return packer, [packer.make_can_msg(name, bus, values) for name, bus, values in inputs] + + def joined(self, car, topology, *, brake=False, speed=30.0, active=True, adas_cmd=False, + desired=1.0, measured=0.0): + cp = params(car, topology, alpha=True, adas_cmd=adas_cmd) + state = CarState(cp) + parsers = state.get_can_parsers(cp) + packer, frames = self.source_frames(cp, topology, brake=brake, speed=speed, + measured_angle=measured) + safety = self.mode(cp) + for frame in frames: + self.assertTrue(safety.safety_rx_hook(self.packet(frame)), hex(frame[0])) + for parser in parsers.values(): + parser.update((1_000_000_000, [frame for frame in frames if frame[2] == parser.bus])) + state.out = state.update(parsers) + self.assertTrue(parsers[Bus.pt].can_valid) + self.assertEqual(state.out.cruiseState.enabled, True) + self.assertEqual(state.out.gearShifter, structs.CarState.GearShifter.drive) + self.assertEqual(state.out.brakePressed, brake) + self.assertTrue(safety.get_controls_allowed()) + control = structs.CarControl() + control.enabled = True + control.latActive = active + control.actuators.steeringAngleDeg = desired + controller = CarController(DBC[car], cp) + _, sends = controller.update(control.as_reader(), state, 1_000_000_000) + steering = [frame for frame in sends if frame[0] in (0x50, 0x110, 0x12A, 0xCB)] + self.assertTrue(steering) + return cp, state, safety, control, controller, steering, packer + + def test_16_profiles_parsed_controller_to_native(self): + for car in ANGLE_CARS: + for topology in TOPOLOGIES: + with self.subTest(car=car, topology=topology): + cp, state, safety, _, _, steering, packer = self.joined(car, topology) + self.assertFalse(cp.alphaLongitudinalAvailable) + self.assertFalse(cp.openpilotLongitudinalControl) + self.assertTrue(cp.pcmCruise) + self.assertEqual(cp.steerControlType, structs.CarParams.SteerControlType.angle) + self.assertEqual(cp.safetyConfigs[-1].safetyParam & 6144, CANFD_ANGLE_MODEL_BITS[car.name]) + self.assertFalse(cp.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.LONG) + expected = 0x50 if topology == "lka" else 0x110 if topology == "lka_alt" else 0x12A + self.assertEqual(steering[0][0], expected) + self.assertNotEqual((steering[0][1][11] << 6) | (steering[0][1][10] >> 2), 0) + for frame in steering: + self.assertEqual(int.from_bytes(frame[1][:2], "little"), + hkg_can_fd_checksum(frame[0], None, bytearray(frame[1]))) + self.assertTrue(safety.safety_tx_hook(self.packet(frame)), hex(frame[0])) + if frame[0] in (0x110, 0xCB): + name = "LKAS_ALT" if frame[0] == 0x110 else "LFA_ALT" + decoded = CANParser(DBC[car][Bus.pt], [(name, math.nan)], frame[2]) + decoded.update((1_000_000_000, [frame])) + self.assertAlmostEqual(decoded.vl[name]["ADAS_StrAnglReqVal"], 1.0, delta=0.11) + self.assertEqual(decoded.vl[name]["ADAS_ActvACILvl2Sta"] if frame[0] == 0xCB else + (frame[1][9] >> 4) & 3, 2) + wrong_bus = (steering[0][0], steering[0][1], 1 - steering[0][2]) + self.assertFalse(safety.safety_tx_hook(self.packet(wrong_bus))) + self.assertFalse(safety.safety_tx_hook(self.packet((steering[0][0], steering[0][1][:-8], steering[0][2])))) + self.assertEqual(packer.dbc.name_to_msg["LFA"].address, 0x12A) + + def test_observed_adas_cmd_and_brake_neutral(self): + for car in ANGLE_CARS: + cp, state, safety, control, controller, steering, _ = self.joined(car, "lfa", adas_cmd=True) + self.assertTrue(cp.flags & HyundaiFlags.SEND_LFA) + self.assertTrue(cp.safetyConfigs[-1].safetyParam & CANFD_ANGLE_OBSERVED_ADAS_BIT) + self.assertEqual([frame[0] for frame in steering], [0x12A, 0xCB]) + decoded = CANParser(DBC[car][Bus.pt], [("LFA_ALT", math.nan)], steering[1][2]) + decoded.update((1_000_000_000, [steering[1]])) + self.assertEqual(decoded.vl["LFA_ALT"]["ADAS_ActvACILvl2Sta"], 2) + self.assertAlmostEqual(decoded.vl["LFA_ALT"]["ADAS_StrAnglReqVal"], 1.0, delta=0.11) + self.assertEqual(int.from_bytes(steering[1][1][:2], "little"), + hkg_can_fd_checksum(steering[1][0], None, bytearray(steering[1][1]))) + for frame in steering: + self.assertTrue(safety.safety_tx_hook(self.packet(frame)), hex(frame[0])) + state.out.brakePressed = True + _, sends = controller.update(control.as_reader(), state, 1_010_000_000) + neutral = [frame for frame in sends if frame[0] in (0x12A, 0xCB)] + self.assertEqual((neutral[0][1][9] >> 4) & 3, 1) + self.assertEqual((neutral[1][1][3] >> 4) & 15, 1) + for frame in neutral: + self.assertTrue(safety.safety_tx_hook(self.packet(frame))) + + def test_host_native_speed_envelopes_and_rate(self): + for car in ANGLE_CARS: + for topology in TOPOLOGIES: + for speed_mps in (2.0, 10.0, 25.0, 35.0): + with self.subTest(car=car, topology=topology, speed=speed_mps): + cp, state, safety, control, controller, _, _ = self.joined( + car, topology, speed=speed_mps * 3.6, desired=360.0) + last_angle = 0.0 + for step in range(90): + safety.set_timer(1_010_000 + step * 10_000) + _, sends = controller.update(control.as_reader(), state, 1_010_000_000 + step * 10_000_000) + steering = [frame for frame in sends if frame[0] in (0x50, 0x110, 0x12A, 0xCB)] + for frame in steering: + self.assertTrue(safety.safety_tx_hook(self.packet(frame)), (car, topology, speed_mps, step, hex(frame[0]))) + primary = steering[-1] + last_angle = ((primary[1][11] << 6) | (primary[1][10] >> 2)) / 10.0 if primary[0] != 0xCB else controller.apply_angle_last + max_angle = get_max_angle_vm(max(state.out.vEgoRaw - 1.0, 1.0), controller.angle_vm, controller.params) + self.assertGreater(last_angle, min(5.0, max_angle * 0.7)) + self.assertLessEqual(last_angle, min(360.0, max_angle) + 0.15) + data = bytearray(primary[1]) + if primary[0] != 0xCB: + reversed_angle = (-3600) & 0x3FFF + data[10] = (data[10] & 3) | ((reversed_angle & 63) << 2) + data[11] = reversed_angle >> 6 + data[:2] = hkg_can_fd_checksum(primary[0], None, data).to_bytes(2, "little") + self.assertFalse(safety.safety_tx_hook(self.packet((primary[0], bytes(data), primary[2])))) + + def test_observed_adas_multitick_actuation_history(self): + for car in ANGLE_CARS: + for topology in ("lfa", "lfa_alt"): + for speed_mps in (2.0, 10.0, 25.0, 35.0): + for direction in (-1, 1): + with self.subTest(car=car, topology=topology, speed=speed_mps, direction=direction): + _, state, safety, control, controller, _, _ = self.joined( + car, topology, speed=speed_mps * 3.6, desired=direction * 360.0, + measured=direction * 10.0, adas_cmd=True) + for step in range(90): + safety.set_timer(1_010_000 + step * 10_000) + _, sends = controller.update(control.as_reader(), state, 1_010_000_000 + step * 10_000_000) + steering = [frame for frame in sends if frame[0] in (0x12A, 0xCB)] + self.assertEqual([frame[0] for frame in steering], [0x12A, 0xCB]) + for frame in steering: + self.assertTrue(safety.safety_tx_hook(self.packet(frame)), + (car, topology, speed_mps, direction, step, hex(frame[0]))) + + def test_angle_frames_reject_other_actuation_channels(self): + for car in ANGLE_CARS: + for topology in TOPOLOGIES: + with self.subTest(car=car, topology=topology): + observed = topology.startswith("lfa") + cp, _, safety, _, _, steering, _ = self.joined(car, topology, adas_cmd=observed) + primary = steering[-1] + if observed: + self.assertTrue(safety.safety_tx_hook(self.packet(steering[0]))) + else: + self.assertFalse(cp.safetyConfigs[-1].safetyParam & CANFD_ANGLE_OBSERVED_ADAS_BIT) + + def deny(frame, byte_index, xor, safety=safety, car=car, topology=topology): + data = bytearray(frame[1]) + data[byte_index] ^= xor + data[:2] = hkg_can_fd_checksum(frame[0], None, data).to_bytes(2, "little") + self.assertFalse(safety.safety_tx_hook(self.packet((frame[0], bytes(data), frame[2]))), + (car, topology, byte_index, xor)) + + angle_frame = steering[0] if observed else primary + deny(angle_frame, 5, 0x2) # legacy torque request + deny(angle_frame, 6, 0x10) # legacy torque enable + if angle_frame[0] == 0x110: + deny(angle_frame, 13, 0x8) # independent FCA_ESA_CtrlSta + if observed: + deny(primary, 3, 0x1) # ADAS_ActvACISta + gain_overflow = bytearray(primary[1]) + gain_overflow[6] = 251 + gain_overflow[:2] = hkg_can_fd_checksum(primary[0], None, gain_overflow).to_bytes(2, "little") + self.assertFalse(safety.safety_tx_hook(self.packet((primary[0], bytes(gain_overflow), primary[2])))) + deny(primary, 7, 0x1) # FCA_ESA_ActvSta + deny(primary, 8, 0x1) # FCA_ESA_TqBstGainVal + self.assertTrue(safety.safety_tx_hook(self.packet(primary))) + else: + self.assertTrue(safety.safety_tx_hook(self.packet(primary))) + + def test_parsed_brake_cruise_and_required_source_handoff(self): + for car in ANGLE_CARS: + for topology in ("lka_alt", "lfa_alt"): + with self.subTest(car=car, topology=topology): + cp, state, safety, control, controller, steering, packer = self.joined(car, topology) + self.assertTrue(safety.safety_tx_hook(self.packet(steering[0]))) + parsers = state.get_can_parsers(cp) + can = CanBus(cp) + _, frames = self.source_frames(cp, topology, brake=True) + for frame in frames: + self.assertTrue(safety.safety_rx_hook(self.packet(frame))) + for parser in parsers.values(): + parser.update((1_010_000_000, [frame for frame in frames if frame[2] == parser.bus])) + state.out = state.update(parsers) + self.assertTrue(state.out.brakePressed) + _, sends = controller.update(control.as_reader(), state, 1_010_000_000) + for frame in sends: + if frame[0] in (0x50, 0x110, 0x12A, 0xCB): + self.assertEqual((frame[1][9] >> 4) & 3, 1) + self.assertTrue(safety.safety_tx_hook(self.packet(frame))) + self.assertFalse(safety.get_controls_allowed()) + + # A high ACCMode alone cannot re-arm after stock cruise goes inactive. + scc_bus = can.CAM if topology.startswith("lfa") else can.ECAN + off = packer.make_can_msg("SCC_CONTROL", scc_bus, {"ACCMode": 0}) + self.assertTrue(safety.safety_rx_hook(self.packet(off))) + parsers[Bus.cam if scc_bus == can.CAM else Bus.pt].update((1_020_000_000, [off])) + state.out = state.update(parsers) + self.assertFalse(state.out.cruiseState.enabled) + _, sends = controller.update(control.as_reader(), state, 1_020_000_000) + for frame in sends: + if frame[0] in (0x50, 0x110, 0x12A, 0xCB): + self.assertEqual((frame[1][9] >> 4) & 3, 1) + + missing = state.get_can_parsers(cp) + _, fresh = self.source_frames(cp, topology) + fresh = [frame for frame in fresh if frame[0] != packer.dbc.name_to_msg["TCS"].address] + for parser in missing.values(): + parser.update((1_000_000_000, [frame for frame in fresh if frame[2] == parser.bus])) + self.assertFalse(missing[Bus.pt].can_valid) + + def test_parsed_gas_and_stock_cruise_off_neutralize_controller(self): + for car in ANGLE_CARS: + for topology in ("lka", "lfa"): + with self.subTest(car=car, topology=topology): + cp, state, safety, control, controller, _, packer = self.joined(car, topology) + can = CanBus(cp) + parsers = state.get_can_parsers(cp) + _, frames = self.source_frames(cp, topology) + for parser in parsers.values(): + parser.update((1_000_000_000, [frame for frame in frames if frame[2] == parser.bus])) + fuel = "ACCELERATOR" if cp.flags & HyundaiFlags.EV else "ACCELERATOR_BRAKE_ALT" + signal = {"ACCELERATOR_PEDAL": 10} if cp.flags & HyundaiFlags.EV else {"ACCELERATOR_PEDAL_PRESSED": 1} + gas = packer.make_can_msg(fuel, can.ECAN, signal) + self.assertTrue(safety.safety_rx_hook(self.packet(gas))) + parsers[Bus.pt].update((1_010_000_000, [gas])) + state.out = state.update(parsers) + self.assertTrue(state.out.gasPressed) + _, sends = controller.update(control.as_reader(), state, 1_010_000_000) + steer = next(frame for frame in sends if frame[0] in (0x50, 0x110, 0x12A)) + self.assertEqual((steer[1][9] >> 4) & 3, 1) + self.assertTrue(safety.safety_tx_hook(self.packet(steer))) + + # With gas released, the stock SCC source still owns enable state. + cruise_bus = can.CAM if topology.startswith("lfa") else can.ECAN + off = packer.make_can_msg("SCC_CONTROL", cruise_bus, {"ACCMode": 0}) + self.assertTrue(safety.safety_rx_hook(self.packet(off))) + parsers[Bus.cam if cruise_bus == can.CAM else Bus.pt].update((1_020_000_000, [off])) + state.out = state.update(parsers) + self.assertFalse(state.out.cruiseState.enabled) + _, sends = controller.update(control.as_reader(), state, 1_020_000_000) + steer = next(frame for frame in sends if frame[0] in (0x50, 0x110, 0x12A)) + self.assertEqual((steer[1][9] >> 4) & 3, 1) + + def test_alt_lkas_parsed_gear_handoff(self): + # Frozen LKAS_ALT status is drive-gated except for the not-yet-ported + # Sportage HEV 2026. All four current identities use the drive gate. + expected_gears = {0: structs.CarState.GearShifter.park, + 1: structs.CarState.GearShifter.unknown, + 5: structs.CarState.GearShifter.drive, + 6: structs.CarState.GearShifter.neutral, + 7: structs.CarState.GearShifter.reverse} + for car in ANGLE_CARS: + for gear_value, expected_gear in expected_gears.items(): + for direction in (-1, 1): + with self.subTest(car=car, gear=gear_value, direction=direction): + cp = params(car, "lka_alt") + state = CarState(cp) + parsers = state.get_can_parsers(cp) + _, frames = self.source_frames(cp, "lka_alt", gear_value=gear_value) + safety = self.mode(cp) + for frame in frames: + self.assertTrue(safety.safety_rx_hook(self.packet(frame))) + for parser in parsers.values(): + parser.update((1_000_000_000, [frame for frame in frames if frame[2] == parser.bus])) + state.out = state.update(parsers) + self.assertEqual(state.out.gearShifter, expected_gear) + self.assertTrue(state.out.cruiseState.enabled) + self.assertTrue(safety.get_controls_allowed()) + control = structs.CarControl() + control.enabled = True + control.latActive = True + control.actuators.steeringAngleDeg = direction * 45.0 + controller = CarController(DBC[car], cp) + _, sends = controller.update(control.as_reader(), state, 1_000_000_000) + steer = next(frame for frame in sends if frame[0] == 0x110) + self.assertEqual((steer[1][9] >> 4) & 3, 2 if gear_value == 5 else 1) + self.assertTrue(safety.safety_tx_hook(self.packet(steer))) + + def test_native_required_source_bus_crc_counter_and_staleness(self): + for car in ANGLE_CARS: + for topology in TOPOLOGIES: + with self.subTest(car=car, topology=topology): + cp = params(car, topology) + packer, frames = self.source_frames(cp, topology) + scc_addr = packer.dbc.name_to_msg["SCC_CONTROL"].address + safety = self.mode(cp) + for frame in frames: + if frame[0] == scc_addr: + frame = (frame[0], frame[1], 0 if frame[2] != 0 else 1) + self.assertTrue(safety.safety_rx_hook(self.packet(frame))) + safety.safety_tick() + self.assertFalse(safety.get_controls_allowed()) + + _, _, safety, _, _, steering, _ = self.joined(car, topology) + active_scc = [frame for frame in frames if frame[0] == scc_addr][-1] + corrupt = bytearray(active_scc[1]) + corrupt[0] ^= 1 + self.assertFalse(safety.safety_rx_hook(self.packet((scc_addr, bytes(corrupt), active_scc[2])))) + self.assertFalse(safety.get_controls_allowed()) + + _, _, safety, _, _, steering, _ = self.joined(car, topology) + for _ in range(5): + safety.safety_rx_hook(self.packet(active_scc)) + self.assertFalse(safety.get_controls_allowed()) + + _, _, safety, _, _, steering, _ = self.joined(car, topology) + safety.set_timer(2_100_000) + safety.safety_tick() + self.assertFalse(safety.safety_config_valid()) + + def test_raw_angle_namespace_release_stock_ownership_and_registered_firmware(self): + safety = libsafety_py.libsafety + for car in ANGLE_CARS: + for topology in TOPOLOGIES: + cp = params(car, topology, alpha=True, release=True) + self.assertFalse(cp.openpilotLongitudinalControl) + self.assertFalse(cp.alphaLongitudinalAvailable) + raw = cp.safetyConfigs[-1].safetyParam + self.assertFalse(raw & HyundaiSafetyFlags.LONG) + for bit in (2, 4, 64, 256, 1024, 8192, 32768): + self.assertEqual(safety.set_safety_hooks(structs.CarParams.SafetyModel.hyundaiCanfd, raw | bit), 0) + safety.init_tests() + frame = CANPacker(DBC[car][Bus.pt]).make_can_msg("LKAS", 0, {}) + self.assertFalse(safety.safety_tx_hook(self.packet(frame)), (car, topology, bit)) + _, matched = match_fw_to_car([], "", log=False) + self.assertFalse(set(ANGLE_CARS) & matched) + self.assertTrue(set(ANGLE_CARS).issubset(FW_VERSIONS)) + for car in ANGLE_CARS: + cohort = [structs.CarParams.CarFw(ecu=ecu, address=address, subAddress=sub or 0, + fwVersion=versions[0], brand="hyundai") + for (ecu, address, sub), versions in FW_VERSIONS[car].items()] + exact, discovered = match_fw_to_car(cohort, "", log=False) + self.assertTrue(exact) + self.assertEqual(discovered, {car}) + for observed in ([item] for item in cohort): + self.assertFalse(set(ANGLE_CARS) & match_fw_to_car(observed, "", log=False)[1]) + self.assertFalse(set(ANGLE_CARS) & set(all_legacy_fingerprint_cars())) + + def test_angle_relay_and_forwarding_block(self): + for car in ANGLE_CARS: + for topology in TOPOLOGIES: + with self.subTest(car=car, topology=topology): + _, _, safety, _, _, steering, _ = self.joined(car, topology) + frame = steering[0] + self.assertEqual(safety.safety_fwd_hook(0, frame[0]), 2) + self.assertEqual(safety.safety_fwd_hook(2, frame[0]), -1) + safety.set_relay_malfunction(True) + self.assertEqual(safety.safety_fwd_hook(0, frame[0]), -1) + self.assertFalse(safety.safety_tx_hook(self.packet(frame))) + + def test_all_angle_tagged_raw_params_have_exactly_seventy_six_profiles(self): + safety = libsafety_py.libsafety + valid = set() + neutral_frames = [] + for car in ANGLE_CARS: + for topology in TOPOLOGIES: + cp = params(car, topology, release=True) + valid.add(cp.safetyConfigs[-1].safetyParam) + if topology.startswith("lfa"): + valid.add(params(car, topology, release=True, adas_cmd=True).safetyConfigs[-1].safetyParam) + packer = CANPacker(DBC[car][Bus.pt]) + can = CanBus(cp) + neutral_frames += create_angle_steering_messages(packer, cp, can, False, False, 0.0, 0.0) + hybrid_cars = (CAR.HYUNDAI_AZERA_HEV_7TH_GEN, CAR.KIA_SORENTO_HEV_4TH_GEN_LFA2, CAR.KIA_SPORTAGE_HEV_2026) + for car in hybrid_cars: + for topology in TOPOLOGIES: + for hybrid in (False, True): + cp = params(car, topology, release=True, hybrid=hybrid) + valid.add(cp.safetyConfigs[-1].safetyParam) + if topology.startswith("lfa"): + valid.add(params(car, topology, release=True, adas_cmd=True, hybrid=hybrid).safetyConfigs[-1].safetyParam) + packer = CANPacker(DBC[car][Bus.pt]) + neutral_frames += create_angle_steering_messages(packer, cp, CanBus(cp), False, False, 0.0, 0.0) + from opendbc.safety.tests.test_hyundai_ccnc_angle_two import params as ccnc_params + for car, topologies, fuels in ((CAR.HYUNDAI_SANTA_FE_HEV_5TH_GEN, TOPOLOGIES, (False, True)), + (CAR.KIA_SPORTAGE_2026, ("lfa", "lfa_alt"), (False,))): + for topology in topologies: + for hybrid in fuels: + cp = ccnc_params(car, topology, release=True, hybrid=hybrid) + valid.add(cp.safetyConfigs[-1].safetyParam) + if topology.startswith("lfa"): + valid.add(ccnc_params(car, topology, release=True, hybrid=hybrid, adas=True).safetyConfigs[-1].safetyParam) + packer = CANPacker(DBC[car][Bus.pt]) + neutral_frames += create_angle_steering_messages(packer, cp, CanBus(cp), False, False, 0.0, 0.0) + for topology in ("lfa", "lfa_alt"): + valid.add(params(CAR.KIA_EV6_2025, topology).safetyConfigs[-1].safetyParam) + self.assertEqual(len(valid), 78) + for raw in range(0x4000, 0x10000): + if not raw & 0x4000: + continue + self.assertEqual(safety.set_safety_hooks(structs.CarParams.SafetyModel.hyundaiCanfd, raw), 0) + safety.init_tests() + accepted = any(safety.safety_tx_hook(self.packet(frame)) for frame in neutral_frames) + self.assertEqual(accepted, raw in valid, hex(raw)) + + def test_mrr35_cycle_freshness(self): + cp = params(CAR.HYUNDAI_IONIQ_9, radar=True) + self.assertFalse(cp.radarUnavailable) + radar = RadarInterface(cp) + packer = CANPacker(DBC[cp.carFingerprint][Bus.radar]) + first = packer.make_can_msg("RADAR_TRACK_3a5", 0, {"STATE": 3, "LONG_DIST": 22.0, + "LAT_DIST": 1.0, "REL_SPEED": -1.0}) + trigger = packer.make_can_msg("RADAR_TRACK_3c4", 0, {"STATE": 0}) + out = radar.update((1_000_000_000, [first, trigger])) + self.assertEqual(len(out.points), 1) + self.assertAlmostEqual(out.points[0].dRel, 22.0, places=1) + out = radar.update((1_050_000_000, [trigger])) + self.assertEqual(len(out.points), 0) + self.assertIsNone(radar.update((1_040_000_000, [trigger]))) + self.assertIsNone(radar.update((1_060_000_000, [first]))) + out = radar.update((1_100_000_000, [trigger])) + self.assertEqual(len(out.points), 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_ray_pedal.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_ray_pedal.py new file mode 100644 index 0000000000..5c1aa3732b --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_ray_pedal.py @@ -0,0 +1,172 @@ +import unittest + +from opendbc.can import CANPacker +from opendbc.car import structs +from opendbc.car.hyundai.ray_pedal import create_ray_pedal_command, pedal_checksum +from opendbc.car.hyundai.tests.test_ray_pedal import ray_controller +from opendbc.car.hyundai.values import RAY_PEDAL_SAFETY_PARAM +from opendbc.safety.tests.libsafety import libsafety_py +from opendbc.safety.tests.test_hyundai import checksum + + +class TestHyundaiRayPedal(unittest.TestCase): + def setUp(self): + self.safety = libsafety_py.libsafety + self.packer = CANPacker('hyundai_can_generated') + self.pedal = CANPacker('hyundai_kia_ray_pedal') + self.counter = 0 + + @staticmethod + def packet(frame): + return libsafety_py.make_CANPacket(frame[0], frame[2], frame[1]) + + def rx(self, name, values, bus=0): + frame = self.packer.make_can_msg(name, bus, values) + if frame[0] in (0x260, 0x386, 0x394): + frame = checksum(frame) + self.assertTrue(self.safety.safety_rx_hook(self.packet(frame))) + return frame + + def sensor(self, state=0, pressed=False): + self.counter = (self.counter + 1) & 15 + frame = self.pedal.make_can_msg('GAS_SENSOR', 0, {'STATE': state, 'COUNTER_PEDAL': self.counter, + 'INTERCEPTOR_GAS': (310-264)*.672 if pressed else 0, 'INTERCEPTOR_GAS2': (593-497)*.332 if pressed else 0}) + self.assertTrue(self.safety.safety_rx_hook(self.packet(frame))) + return frame + + def arm(self, param=RAY_PEDAL_SAFETY_PARAM): + self.safety.set_safety_hooks(structs.CarParams.SafetyModel.hyundai, param) + self.safety.init_tests() + self.safety.set_timer(1_000_000) + self.counter = 0 + self.rx('E_EMS11', {}) + self.rx('WHL_SPD11', {'WHL_SPD_FL': 0, 'WHL_SPD_RR': 0}) + self.rx('TCS13', {'DriverOverride': 0}) + self.rx('MDPS12', {}) + self.rx('CLU11', {'CF_Clu_AliveCnt1': 1}) + self.rx('LABEL11', {'CC_React': 1}) + self.rx('BCM_PO_11', {}) + self.sensor() + self.safety.safety_tick() + self.rx('CLU11', {'CF_Clu_CruiseSwState': 2, 'CF_Clu_AliveCnt1': 2}) + self.rx('CLU11', {'CF_Clu_CruiseSwState': 0, 'CF_Clu_AliveCnt1': 3}) + self.assertTrue(self.safety.get_controls_allowed()) + + def command(self, gas, bus=0): + frame = create_ray_pedal_command(self.pedal, gas, 3) + return self.packet((frame[0], frame[1], bus)) + + def test_exact_actual_cp_and_near_neighbor_native_admission(self): + cp, _, _, _ = ray_controller() + self.assertEqual(cp.safetyConfigs[0].safetyParam, RAY_PEDAL_SAFETY_PARAM) + self.arm(cp.safetyConfigs[0].safetyParam) + self.assertTrue(self.safety.safety_tx_hook(self.command(.55))) + self.assertTrue(self.safety.safety_tx_hook(self.command(.65))) + self.assertTrue(self.safety.safety_tx_hook(self.command(.70))) + self.assertTrue(self.safety.safety_tx_hook(self.command(0))) + for param in (0x9405, 0x9801, 0x1805, 0x9804, 0x9807, 0x9885, 0x9825, 0xB805, 0): + self.safety.set_safety_hooks(structs.CarParams.SafetyModel.hyundai, param) + self.safety.init_tests() + self.safety.set_controls_allowed(True) + self.assertFalse(self.safety.safety_tx_hook(self.command(0)), hex(param)) + self.assertFalse(self.safety.safety_tx_hook(self.command(.1)), hex(param)) + + def test_command_limits_channels_checksum_and_buses(self): + self.arm() + for gas in (.71, 1): + self.assertFalse(self.safety.safety_tx_hook(self.command(gas))) + for bus in (1, 2): + self.assertFalse(self.safety.safety_tx_hook(self.command(.1, bus))) + frame = create_ray_pedal_command(self.pedal, .1, 3) + for kind in ('checksum', 'track', 'reserved', 'disabled'): + data = bytearray(frame[1]) + if kind == 'checksum': + data[-1] ^= 1 + elif kind == 'track': + data[3] += 5 + data[-1] = pedal_checksum(0, None, data) + elif kind == 'reserved': + data[4] |= 0x10 + data[-1] = pedal_checksum(0, None, data) + else: + data[4] &= ~0x80 + data[-1] = pedal_checksum(0, None, data) + self.assertFalse(self.safety.safety_tx_hook(self.packet((frame[0], bytes(data), 0))), kind) + for addr in (0x420, 0x421, 0x50A, 0x389, 0x7D0): + self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, 0, bytes(8)))) + + def test_generated_channel_rounding_bound_exhaustive(self): + self.arm() + maximum = 0 + for step in range(101, 70001): + frame = create_ray_pedal_command(self.pedal, step / 100000, step & 15) + track1 = int.from_bytes(frame[1][:2], 'big') + track2 = int.from_bytes(frame[1][2:4], 'big') + error = abs(83*(track2-497)-168*(track1-264)) + maximum = max(maximum, error) + self.assertLessEqual(error, 125) + self.assertTrue(self.safety.safety_tx_hook(self.packet(frame))) + self.assertEqual(maximum, 125) + + def test_fault_driver_brake_stale_restart_and_cancel_gates(self): + self.arm() + self.assertTrue(self.safety.safety_tx_hook(self.command(.1))) + for fault in range(1, 6): + self.sensor(state=fault) + self.assertFalse(self.safety.safety_tx_hook(self.command(.1))) + self.assertTrue(self.safety.safety_tx_hook(self.command(0))) + self.sensor() + self.assertTrue(self.safety.safety_tx_hook(self.command(.1))) + self.sensor(pressed=True) + self.assertFalse(self.safety.safety_tx_hook(self.command(.1))) + cancel = self.packer.make_can_msg('CLU11', 0, {'CF_Clu_CruiseSwState': 4}) + self.assertTrue(self.safety.safety_tx_hook(self.packet(cancel))) + self.sensor() + self.rx('TCS13', {'DriverOverride': 2, 'AliveCounterTCS': 1}) + self.assertFalse(self.safety.safety_tx_hook(self.command(.1))) + self.assertTrue(self.safety.safety_tx_hook(self.command(0))) + self.arm() + self.safety.set_timer(2_000_001) + self.assertFalse(self.safety.safety_tx_hook(self.command(.1))) + self.assertTrue(self.safety.safety_tx_hook(self.command(0))) + self.arm() + self.safety.set_controls_allowed(False) + self.assertFalse(self.safety.safety_tx_hook(self.command(.1))) + self.safety.set_safety_hooks(structs.CarParams.SafetyModel.hyundai, RAY_PEDAL_SAFETY_PARAM) + self.safety.init_tests() + self.safety.set_controls_allowed(True) + self.assertFalse(self.safety.safety_tx_hook(self.command(.1))) + + def test_actual_buttons_main_off_cancel_and_no_spurious_reenable(self): + self.arm() + self.assertTrue(self.safety.safety_tx_hook(self.command(.1))) + self.rx('CLU11', {'CF_Clu_CruiseSwState': 4, 'CF_Clu_AliveCnt1': 4}) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertFalse(self.safety.safety_tx_hook(self.command(.1))) + self.assertTrue(self.safety.safety_tx_hook(self.command(0))) + self.rx('CLU11', {'CF_Clu_CruiseSwState': 1, 'CF_Clu_AliveCnt1': 5}) + self.assertFalse(self.safety.get_controls_allowed()) + self.rx('CLU11', {'CF_Clu_CruiseSwState': 0, 'CF_Clu_AliveCnt1': 6}) + self.assertTrue(self.safety.get_controls_allowed()) + self.rx('LABEL11', {'CC_React': 0, 'CC_Engaged': 0}) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertFalse(self.safety.safety_tx_hook(self.command(.1))) + self.rx('LABEL11', {'CC_React': 1}) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertFalse(self.safety.safety_tx_hook(self.command(.1))) + self.rx('CLU11', {'CF_Clu_CruiseSwState': 2, 'CF_Clu_AliveCnt1': 7}) + self.rx('CLU11', {'CF_Clu_CruiseSwState': 0, 'CF_Clu_AliveCnt1': 8}) + self.assertTrue(self.safety.safety_tx_hook(self.command(.1))) + + def test_host_to_native_and_required_health_loss(self): + cp, controller, command, state = ray_controller() + self.arm(cp.safetyConfigs[0].safetyParam) + for frame in range(40): + for message in controller.update(command.as_reader(), state, frame*10_000_000)[1]: + self.assertTrue(self.safety.safety_tx_hook(self.packet(message)), message) + sensor = self.sensor() + bad = bytearray(sensor[1]) + bad[-1] ^= 1 + self.assertFalse(self.safety.safety_rx_hook(self.packet((sensor[0], bytes(bad), 0)))) + self.assertFalse(self.safety.safety_tx_hook(self.command(.1))) + self.assertTrue(self.safety.safety_tx_hook(self.command(0))) diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_shared_canfd_stock_aol.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_shared_canfd_stock_aol.py new file mode 100644 index 0000000000..1c5b4ad943 --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_shared_canfd_stock_aol.py @@ -0,0 +1,95 @@ +import unittest + +from opendbc.can import CANPacker +from opendbc.car import structs +from opendbc.car.hyundai.canfd_stock_aol import STOCK_AOL_WORDS +from opendbc.safety.tests.libsafety import libsafety_py + + +class TestHyundaiSharedCanfdStockAol(unittest.TestCase): + def setUp(self): + self.safety = libsafety_py.libsafety + self.packer = CANPacker('hyundai_canfd_generated') + + def mode(self, word): + self.safety.set_alternative_experience(0) + self.assertEqual(self.safety.set_safety_hooks(structs.CarParams.SafetyModel.hyundaiCanfd, word), 0) + self.safety.init_tests() + self.safety.set_timer(1_000_000) + + @staticmethod + def packet(frame): + return libsafety_py.make_CANPacket(frame[0], frame[2], frame[1]) + + def healthy(self, word, *, omit=None, wrong_bus=None): + pt = 1 if word & 0x10 else 0 + fuel = {0: 'ACCELERATOR_BRAKE_ALT', 1: 'ACCELERATOR', 2: 'ACCELERATOR_ALT'}[word & 3] + buttons = 'CRUISE_BUTTONS_ALT' if word & 0x20 else 'CRUISE_BUTTONS' + for counter in range(1, 7): + for name in (fuel, 'TCS', 'WHEEL_SPEEDS', 'MDPS', buttons): + if name == omit: + continue + bus = 1 - pt if name == wrong_bus else pt + values = {'COUNTER': counter % (16 if name == 'CRUISE_BUTTONS' else 256)} + if name == 'MDPS': + values['MDPS_StrTqSnsrVal'] = 0 + frame = self.packer.make_can_msg(name, bus, values) + self.assertTrue(self.safety.safety_rx_hook(self.packet(frame)), name) + if not word & 4 and omit != 'SCC_CONTROL': + frame = self.packer.make_can_msg('SCC_CONTROL', 2 if word & 8 else pt, {'COUNTER': counter}) + self.assertTrue(self.safety.safety_rx_hook(self.packet(frame))) + + def gesture(self, word): + pt = 1 if word & 0x10 else 0 + name = 'CRUISE_BUTTONS_ALT' if word & 0x20 else 'CRUISE_BUTTONS' + for count, pressed in ((7, 1), (8, 0)): + frame = self.packer.make_can_msg(name, pt, {'COUNTER': count, 'LDA_BTN': pressed}) + self.assertTrue(self.safety.safety_rx_hook(self.packet(frame))) + self.safety.set_aol_test_heartbeat(True) + self.safety.aol_set_host_request(3) + + def test_selected_fuel_button_and_scc_sources_are_required(self): + for word in (0x800 | fuel | topology for fuel in (0, 1, 2) for topology in (0, 8, 16, 32, 40, 144)): + fuel = {0: 'ACCELERATOR_BRAKE_ALT', 1: 'ACCELERATOR', 2: 'ACCELERATOR_ALT'}[word & 3] + buttons = 'CRUISE_BUTTONS_ALT' if word & 0x20 else 'CRUISE_BUTTONS' + for omitted in (fuel, buttons, 'SCC_CONTROL'): + self.mode(word) + self.healthy(word, omit=omitted) + if omitted != buttons: + self.gesture(word) + else: + self.safety.set_aol_test_heartbeat(True) + self.safety.aol_set_host_request(3) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.mode(word) + self.healthy(word) + self.gesture(word) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + + def test_full_stock_word_inventory_and_unknown_marker_rejection(self): + for word in STOCK_AOL_WORDS: + with self.subTest(word=hex(word)): + self.mode(word) + self.healthy(word) + self.gesture(word) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + pt = 1 if word & 0x10 else 0 + self.assertFalse(self.safety.safety_tx_hook(self.packet(self.packer.make_can_msg('SCC_CONTROL', pt, {})))) + for word in (0x803, 0x804, 0x830, 0x880, 0xC00): + self.mode(word) + self.safety.set_aol_test_heartbeat(True) + self.safety.aol_set_host_request(3) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + + def test_source_expiry_and_wrong_bus_withdraw_permission(self): + for word in (0x800 | fuel | topology for fuel in (0, 1, 2) for topology in (0, 8, 16, 32, 40, 144)): + self.mode(word) + self.healthy(word) + self.gesture(word) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + self.safety.set_timer(2_000_000) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.mode(word) + self.healthy(word, wrong_bus='WHEEL_SPEEDS') + self.gesture(word) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_stock_angle_aol.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_stock_angle_aol.py new file mode 100644 index 0000000000..a6b236122c --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_stock_angle_aol.py @@ -0,0 +1,138 @@ +from opendbc.car.hyundai.hyundaicanfd import hkg_can_fd_checksum +from opendbc.safety.tests import test_hyundai_angle_hybrids_three as angle_profiles +import unittest + +from opendbc.can import CANPacker +from opendbc.car import structs +from opendbc.safety.tests.libsafety import libsafety_py + + +WORDS = (0x4010, 0x4811, 0x5010, 0x5811, 0x6010, 0x6810, 0x7010, 0x4408, 0x4C08, 0x7809, 0x5491, 0x5C91) + + +class TestHyundaiStockAngleAol(unittest.TestCase): + def setUp(self): + self.safety = libsafety_py.libsafety + self.packer = CANPacker('hyundai_canfd_generated') + + def mode(self, word): + self.safety.set_alternative_experience(32) + self.assertEqual(self.safety.set_safety_hooks(structs.CarParams.SafetyModel.hyundaiCanfd, word), 0) + self.safety.init_tests() + self.safety.set_timer(1_000_000) + + def rx(self, name, bus, values): + self.assertFalse(set(values) - set(self.packer.dbc.name_to_msg[name].sigs), name) + frame = self.packer.make_can_msg(name, bus, values) + self.assertTrue(self.safety.safety_rx_hook(libsafety_py.make_CANPacket(frame[0], frame[2], frame[1])), name) + + def sources(self, word, *, gear=5, brake=False, gas=False, fault=0, omit=None, gear_bus=None): + pt = 1 if word & 16 else 0 + fuel = 'ACCELERATOR' if word & 1 else 'ACCELERATOR_ALT' if word & 2 else 'ACCELERATOR_BRAKE_ALT' + button = 'CRUISE_BUTTONS_ALT' if word & 32 else 'CRUISE_BUTTONS' + fuel_values = {'ACCELERATOR_PEDAL': 4 if gas else 0} if word & 3 else {'ACCELERATOR_PEDAL_PRESSED': int(gas)} + if word & 1: + fuel_values['GEAR'] = gear + for count in range(1, 7): + entries = [ + (fuel, pt, fuel_values), + ('TCS', pt, {'DriverBraking': int(brake)}), + ('WHEEL_SPEEDS', pt, dict.fromkeys(('WHL_SpdFLVal', 'WHL_SpdFRVal', 'WHL_SpdRLVal', 'WHL_SpdRRVal'), 36)), + ('MDPS', pt, {'MDPS_LkaFailSta': fault}), + (button, pt, {}), + ('SCC_CONTROL', 2 if word & 8 else pt, {}), + ] + if not word & 1: + entries.append(('GEAR_ALT_2', pt if gear_bus is None else gear_bus, {'GEAR': gear})) + for name, bus, values in entries: + if name != omit: + self.rx(name, bus, dict(values, COUNTER=count % (16 if name == 'CRUISE_BUTTONS' else 256))) + + def gesture(self, word): + pt = 1 if word & 16 else 0 + button = 'CRUISE_BUTTONS_ALT' if word & 32 else 'CRUISE_BUTTONS' + self.rx(button, pt, {'COUNTER': 7, 'LDA_BTN': 1}) + self.rx(button, pt, {'COUNTER': 8, 'LDA_BTN': 0}) + self.safety.set_aol_test_heartbeat(True) + self.safety.aol_set_host_request(3) + + @staticmethod + def packet(frame): + return libsafety_py.make_CANPacket(frame[0], frame[2], frame[1]) + + def command(self, word, counter, *, active=True, bad_crc=False): + if word & 0x200 and not word & 0x10: + return self.packet(self.packer.make_can_msg('LFA_ALT', 0, {'COUNTER': counter, + 'ADAS_ActvACISta': 0, 'ADAS_ActvACILvl2Sta': 2 if active else 1, 'ADAS_StrAnglReqVal': 0, + 'ADAS_ACIAnglTqRedcGainVal': 0, 'FCA_ESA_ActvSta': 0, 'FCA_ESA_TqBstGainVal': 0})) + name = 'LKAS_ALT' if word & 0x80 else 'LKAS' if word & 0x10 else 'LFA' + address, raw, bus = self.packer.make_can_msg(name, 0, {'COUNTER': counter}) + data = bytearray(raw) + data[9] = (2 if active else 1) << 4 + data[5], data[6] = 0, 8 + data[10], data[11] = 0, 0 + data[:2] = hkg_can_fd_checksum(address, None, data).to_bytes(2, 'little') + if bad_crc: + data[0] ^= 1 + return self.packet((address, bytes(data), bus)) + + def test_real_angle_profiles_commit_only_valid_fresh_commands(self): + for topology, adas in (('lka', False), ('lka_alt', False), ('lfa', False), ('lfa_alt', True)): + cp = angle_profiles.params(angle_profiles.CARS[0], topology, hybrid=True, adas=adas) + word = cp.safetyConfigs[-1].safetyParam + self.mode(word) + self.sources(word) + self.gesture(word) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + active = self.command(word, 1) + address = active[0].addr + self.assertTrue(self.safety.safety_tx_hook(active)) + self.assertTrue(self.safety.safety_test_selected_fwd(2, address)) + self.assertFalse(self.safety.safety_tx_hook(self.command(word, 1))) + self.assertFalse(self.safety.safety_test_selected_fwd(2, address)) + self.assertTrue(self.safety.safety_tx_hook(self.command(word, 2))) + corrupt = self.command(word, 3) + corrupt[0].data[0] ^= 1 + self.assertFalse(self.safety.safety_tx_hook(corrupt)) + self.assertFalse(self.safety.safety_test_selected_fwd(2, address)) + self.assertTrue(self.safety.safety_tx_hook(self.command(word, 3))) + self.assertTrue(self.safety.safety_tx_hook(self.command(word, 4, active=False))) + self.assertFalse(self.safety.safety_test_selected_fwd(2, address)) + self.assertTrue(self.safety.safety_tx_hook(self.command(word, 5))) + self.safety.set_timer(1_300_001) + self.assertFalse(self.safety.safety_test_selected_fwd(2, address)) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.assertFalse(self.safety.safety_tx_hook(self.command(word, 6))) + + def test_non_angle_profile_rejects_cb_at_public_and_selected_hooks(self): + self.safety.set_alternative_experience(0) + self.assertEqual(self.safety.set_safety_hooks(structs.CarParams.SafetyModel.hyundaiCanfd, 0), 0) + packet = self.packet(self.packer.make_can_msg('LFA_ALT', 0, {})) + self.assertFalse(self.safety.safety_tx_hook(packet)) + self.assertFalse(self.safety.safety_test_selected_tx(packet)) + + def test_independent_angle_invalid_capacity_returns_empty_config(self): + self.assertTrue(self.safety.safety_test_hyundai_angle_invalid_capacity(0)) + self.assertTrue(self.safety.safety_test_hyundai_angle_invalid_capacity(65)) + + def test_exact_stock_words_require_physical_token_and_fresh_drive(self): + for word in WORDS: + with self.subTest(word=hex(word)): + self.mode(word) + self.sources(word) + self.safety.set_aol_test_heartbeat(True) + self.safety.aol_set_host_request(3) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.gesture(word) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + self.safety.set_timer(2_000_000) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + + def test_non_ev_missing_wrong_bus_and_reverse_gear_denied(self): + for word in (0x4010, 0x4408, 0x4C08): + for kwargs in ({'omit': 'GEAR_ALT_2'}, {'gear_bus': 2}, {'gear': 7}): + with self.subTest(word=hex(word), kwargs=kwargs): + self.mode(word) + self.sources(word, **kwargs) + self.gesture(word) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) diff --git a/opendbc_repo/opendbc/safety/tests/test_kia_ev6_2025_angle.py b/opendbc_repo/opendbc/safety/tests/test_kia_ev6_2025_angle.py new file mode 100644 index 0000000000..eb04ec752c --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_kia_ev6_2025_angle.py @@ -0,0 +1,85 @@ +import unittest +from unittest.mock import patch + +from opendbc.can import CANParser +from opendbc.car import Bus, structs +from opendbc.car.hyundai.hyundaicanfd import hkg_can_fd_checksum +from opendbc.car.hyundai.values import CAR, DBC +from opendbc.car.vehicle_model import calc_slip_factor +from opendbc.safety.tests.libsafety import libsafety_py + + +class TestKiaEv62025Angle(unittest.TestCase): + def setUp(self): + from opendbc.safety.tests.test_hyundai_mrr35_angle_four import TestHyundaiMRR35AngleFour + self.helper = TestHyundaiMRR35AngleFour() + + def test_packed_sources_controller_and_correct_native_geometry(self): + for topology in ('lfa', 'lfa_alt'): + for speed in (2.0, 10.0, 25.0, 35.0): + cp, _, safety, _, controller, frames, _ = self.helper.joined(CAR.KIA_EV6_2025, topology, speed=speed * 3.6) + self.assertAlmostEqual(calc_slip_factor(controller.angle_vm), -0.0008898049378, places=11) + self.assertAlmostEqual(controller.angle_vm.sR, 14.26, places=5) + self.assertAlmostEqual(controller.angle_vm.l, 2.9, places=6) + self.assertEqual(cp.safetyConfigs[-1].safetyParam, 0x7809 if topology == 'lfa' else 0x7829) + self.assertEqual({frame[0] for frame in frames}, {0x12A}) + for frame in frames: + self.assertTrue(safety.safety_tx_hook(self.helper.packet(frame))) + self.assertEqual(int.from_bytes(frame[1][:2], 'little'), hkg_can_fd_checksum(frame[0], None, bytearray(frame[1]))) + self.assertEqual(safety.safety_fwd_hook(0, 0x1A0), 2) + self.assertEqual(safety.safety_fwd_hook(2, 0x1A0), 0) + self.assertEqual(safety.safety_fwd_hook(2, 0x12A), -1) + self.assertEqual(safety.safety_fwd_hook(0, 0x12A), 2) + + def test_neutral_fault_timeout_and_relay(self): + for topology in ('lfa', 'lfa_alt'): + cp, state, safety, control, controller, _, _ = self.helper.joined(CAR.KIA_EV6_2025, topology) + control.enabled = control.latActive = False + _, frames = controller.update(control.as_reader(), state, 1_010_000_000) + parser = CANParser(DBC[CAR.KIA_EV6_2025][Bus.pt], [('LFA', 0)], 0) + steering = next(frame for frame in frames if frame[0] == 0x12A) + parser.update((1_010_000_000, [steering])) + self.assertEqual((steering[1][9] >> 4) & 3, 1) + self.assertTrue(safety.safety_tx_hook(self.helper.packet(steering))) + # A cleared controls permission cannot transmit an active angle request. + cp, _, safety, _, _, active_frames, _ = self.helper.joined(CAR.KIA_EV6_2025, topology) + safety.set_controls_allowed(False) + self.assertFalse(safety.safety_tx_hook(self.helper.packet(active_frames[0]))) + cp, _, safety, _, _, active_frames, _ = self.helper.joined(CAR.KIA_EV6_2025, topology) + safety.set_timer(2_100_000) + safety.safety_tick() + self.assertFalse(safety.safety_config_valid()) + self.assertFalse(safety.safety_tx_hook(self.helper.packet(active_frames[0]))) + safety.set_relay_malfunction(True) + self.assertFalse(safety.safety_tx_hook(self.helper.packet(steering))) + + def test_driver_pedals_and_temporary_eps_fault_send_neutral(self): + for topology in ('lfa', 'lfa_alt'): + for field in ('gasPressed', 'brakePressed', 'steerFaultTemporary'): + _, state, safety, control, controller, _, _ = self.helper.joined(CAR.KIA_EV6_2025, topology) + setattr(state.out, field, True) + _, sends = controller.update(control.as_reader(), state, 1_010_000_000) + self.assertEqual({frame[0] for frame in sends}, {0x12A}) + for frame in sends: + self.assertEqual((frame[1][9] >> 4) & 3, 1) + self.assertTrue(safety.safety_tx_hook(self.helper.packet(frame))) + + def test_ev6_rate_limits_and_required_source_health(self): + from opendbc.safety.tests import test_hyundai_mrr35_angle_four as suite + with patch.object(suite, 'ANGLE_CARS', (CAR.KIA_EV6_2025,)), patch.object(suite, 'TOPOLOGIES', ('lfa', 'lfa_alt')): + self.helper.test_host_native_speed_envelopes_and_rate() + self.helper.test_native_required_source_bus_crc_counter_and_staleness() + + def test_profile_bits_cannot_enable_other_topologies_or_authorities(self): + safety = libsafety_py.libsafety + for topology in ('lfa', 'lfa_alt'): + cp, _, _, _, _, frames, _ = self.helper.joined(CAR.KIA_EV6_2025, topology) + raw = cp.safetyConfigs[-1].safetyParam + for forbidden in (2, 4, 16, 64, 128, 256, 512, 1024, 32768): + self.assertEqual(safety.set_safety_hooks(structs.CarParams.SafetyModel.hyundaiCanfd, raw | forbidden), 0) + safety.init_tests() + self.assertFalse(safety.safety_tx_hook(self.helper.packet(frames[0])), hex(raw | forbidden)) + + +if __name__ == '__main__': + unittest.main() diff --git a/opendbc_repo/opendbc/safety/tests/test_nexo_stock_aol.py b/opendbc_repo/opendbc/safety/tests/test_nexo_stock_aol.py new file mode 100644 index 0000000000..cc14e8c92f --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_nexo_stock_aol.py @@ -0,0 +1,116 @@ +import unittest + +from opendbc.car.structs import CarParams +from opendbc.safety.tests.test_hyundai_classic_scc_aol import TestHyundaiClassicSccAol as Shared + + +class TestNexoStockAol(unittest.TestCase): + setUp = Shared.setUp + tearDown = Shared.tearDown + reset = Shared.reset + rx = Shared.rx + request = Shared.request + + def feed(self, word, *, gas=0, gas_bus=0, omit=False, pressed=False): + Shared.feed(self, word, main=True, source='BCM_PO_11', pressed=pressed) + if not omit: + self.rx('FCEV_ACCELERATOR', {'ACCELERATOR_PEDAL': gas}, bus=gas_bus) + + def test_checked_fcev_pedal_cannot_be_overwritten_by_ice(self): + for word in (0x0500, 0x0D00): + self.reset(word) + for _ in range(8): + self.feed(word) + self.feed(word, pressed=True) + self.assertEqual(self.request(1), 1) + self.feed(word, gas=1) + self.assertTrue(self.safety.get_gas_pressed_prev()) + self.rx('EMS16', {'AliveCounter': self.counter % 4}, integrity=True) + self.assertTrue(self.safety.get_gas_pressed_prev()) + self.feed(word, gas=0) + self.assertFalse(self.safety.get_gas_pressed_prev()) + self.assertEqual(self.request(2), 0) + self.assertFalse(self.safety.get_controls_allowed()) + + def test_pedal_missing_or_wrong_bus_revokes_and_requires_new_gesture(self): + for wrong_bus in (False, True): + self.reset(0x0D00) + for _ in range(8): + self.feed(0x0D00) + self.feed(0x0D00, pressed=True) + self.assertEqual(self.request(1), 1) + for _ in range(120): + self.feed(0x0D00, gas_bus=1 if wrong_bus else 0, omit=not wrong_bus) + self.request(1) + self.assertEqual(self.request(1), 0) + for _ in range(8): + self.feed(0x0D00) + self.assertEqual(self.request(1), 0) + self.request(0) + self.feed(0x0D00, pressed=True) + self.assertEqual(self.request(1), 1) + + def test_legacy_and_long_namespace_remain_denied(self): + for word, mode in ((0x0500, CarParams.SafetyModel.hyundaiLegacy), + (0x0D00, CarParams.SafetyModel.hyundaiLegacy), + (0x0504, CarParams.SafetyModel.hyundai), + (0x0D04, CarParams.SafetyModel.hyundai)): + self.reset(word, mode=mode) + for _ in range(8): + self.feed(word, pressed=True) + self.assertEqual(self.request(1), 0) + + def test_real_ci_and_native_agree_on_checked_pedal_and_ice_injection(self): + import time + + from opendbc.car import gen_empty_fingerprint + from opendbc.car.hyundai.interface import CarInterface + from opendbc.car.hyundai.values import CAR + from opendbc.safety.tests.libsafety.libsafety_py import make_CANPacket + from opendbc.safety.tests.test_hyundai import checksum + + cp = CarInterface.get_params(CAR.HYUNDAI_NEXO_1ST_GEN, gen_empty_fingerprint(), [], False, False, False) + assert cp.safetyConfigs[0].safetyParam == 0x0100 + cp.safetyConfigs[0].safetyParam = 0x0500 + cp.alternativeExperience = 32 + ci = CarInterface(cp) + ci.update([]) + self.reset(0x0500) + for tick in range(24): + self.safety.set_timer(self.now) + gas = 1 if 8 <= tick < 16 else 0 + packets = [] + sources = ((0, 'CGW1'), (0, 'CGW2'), (0, 'WHL_SPD11'), (0, 'CLU15'), + (0, 'SAS11'), (0, 'MDPS12'), (0, 'CLU11'), (0, 'TCS13'), + (0, 'TCS15'), (0, 'TCS11'), (0, 'FCEV_ACCELERATOR'), (0, 'EMS20'), + (0, 'SCC11'), (0, 'SCC12'), (0, 'EMS16'), (2, 'LKAS11')) + for bus, name in sources: + values = {} + if name == 'FCEV_ACCELERATOR': + values['ACCELERATOR_PEDAL'] = gas + elif name == 'EMS16': + values['AliveCounter'] = tick % 4 + elif name == 'WHL_SPD11': + values.update(WHL_SPD_AliveCounter_LSB=tick % 4, WHL_SPD_AliveCounter_MSB=(tick // 4) % 4) + elif name == 'TCS13': + values['AliveCounterTCS'] = tick % 8 + elif name == 'CLU11': + values['CF_Clu_AliveCnt1'] = tick % 16 + elif name == 'SCC12': + values['CR_VSM_Alive'] = tick % 16 + elif name == 'SCC11': + values['MainMode_ACC'] = 1 + assert set(values) <= set(self.packer.dbc.name_to_msg[name].sigs) + msg = self.packer.make_can_msg(name, bus, values) + if name in ('EMS16', 'WHL_SPD11', 'TCS13', 'SCC12'): + msg = checksum(msg) + packets.append(msg) + self.safety.safety_rx_hook(make_CANPacket(msg[0], msg[2], msg[1])) + state = ci.update([(time.clock_gettime_ns(time.CLOCK_BOOTTIME), packets)]) + assert state.gasPressed == bool(gas) + assert bool(self.safety.get_gas_pressed_prev()) == bool(gas) + ice = checksum(self.packer.make_can_msg('EMS16', 0, {'AliveCounter': tick % 4})) + assert ice[0] == 0x260 and ice[1][7] >> 6 == 0 + self.safety.safety_rx_hook(make_CANPacket(ice[0], ice[2], ice[1])) + assert bool(self.safety.get_gas_pressed_prev()) == bool(gas) + self.now += 10_000 diff --git a/openpilot/starpilot/car/hyundai/__init__.py b/openpilot/starpilot/car/hyundai/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpilot/starpilot/car/hyundai/aol.py b/openpilot/starpilot/car/hyundai/aol.py new file mode 100644 index 0000000000..4ab050e5ee --- /dev/null +++ b/openpilot/starpilot/car/hyundai/aol.py @@ -0,0 +1,291 @@ +from dataclasses import replace + +from opendbc.car.hyundai.blended_stock_aol import ( + qualified as qualified_blended_stock, qualified_alpha as qualified_blended_alpha, + WORDS as BLENDED_STOCK_WORDS, ALPHA_WORDS as BLENDED_ALPHA_WORDS, +) +from opendbc.car.hyundai.canfd_angle_aol import qualified as qualified_angle_aol, temporary_restriction, ANGLE_AOL_WORDS +from opendbc.car.hyundai.classic_long_aol import ( + qualified as qualified_classic_long, + aol_word as classic_long_word, + native_accepts as classic_long_accepts, + LONG_AOL_WORDS, +) +from opendbc.car.hyundai.classic_scc_aol import ( + qualified as qualified_classic_scc, + aol_word as classic_aol_word, + native_accepts as classic_native_accepts, + native_profile_supported as classic_native_profile_supported, +) +from opendbc.car.hyundai.canfd_owner import config_index +from opendbc.car.hyundai.canfd_stock_aol import ( + qualified as qualified_canfd_stock, qualified_long as qualified_canfd_long, + LONG_AOL_WORDS as CANFD_LONG_AOL_WORDS, STOCK_AOL_MARKER, STOCK_AOL_WORDS, +) +from opendbc.car.hyundai.non_scc_aol import qualified as qualified_non_scc, aol_word, AOL_MARKER, AOL_EXPERIENCE, AOL_WORDS +from opendbc.car.hyundai.kona_aol import allow_lateral_onset as kona_lateral_onset +from opendbc.car.structs import car +from opendbc.car.hyundai.values import CAR as HyundaiCAR, HyundaiFlags +from opendbc.car.hyundai.ccnc_ev_stock import qualified as qualified_ccnc_ev_stock, request_allowed, STOCK_SAFETY_PARAMS +from opendbc.car.hyundai.ioniq6_handoff import IONIQ6_LONG_PREARM_ENABLED +from openpilot.starpilot.aol.policy import AolVehiclePolicy +from openpilot.starpilot.longitudinal.ioniq6_start import eligible as ioniq6_long_eligible + + +IONIQ6_STOCK_AOL_SAFETY_PARAMS = frozenset((0x0811, 0x0891)) +IONIQ6_AOL_SAFETY_PARAMS = frozenset((0x0811, 0x0891, 0x8815, 0x8895)) +IONIQ6_STOCK_SAFETY_PARAMS = frozenset((0x11, 0x91)) +_REQUIRED_FLAGS = int(HyundaiFlags.CANFD | HyundaiFlags.EV | HyundaiFlags.CANFD_LKA_STEER_MSG) +_FORBIDDEN_FLAGS = int(HyundaiFlags.CANFD_ALT_BUTTONS | HyundaiFlags.CANFD_ANGLE_STEERING | HyundaiFlags.CANFD_CAMERA_SCC) + + +def _ioniq6_hda2_base(CP) -> bool: + if ( + str(CP.carFingerprint) != str(HyundaiCAR.HYUNDAI_IONIQ_6) + or CP.brand != 'hyundai' + or CP.radarUnavailable + or CP.notCar + or CP.passive + or CP.dashcamOnly + or len(CP.safetyConfigs) != 1 + or CP.safetyConfigs[0].safetyModel != car.CarParams.SafetyModel.hyundaiCanfd + ): + return False + flags = int(CP.flags) + return flags & _REQUIRED_FLAGS == _REQUIRED_FLAGS and not flags & _FORBIDDEN_FLAGS + + +def _stock_ioniq6(CP, *, marked_only=False) -> bool: + if not _ioniq6_hda2_base(CP) or CP.openpilotLongitudinalControl or not CP.pcmCruise: + return False + stock = 0x91 if int(CP.flags) & int(HyundaiFlags.CANFD_LKA_STEER_MSG_ALT) else 0x11 + raw = int(CP.safetyConfigs[0].safetyParam) + return raw == (stock | 0x0800) or (not marked_only and raw == stock) + + +def qualified_ioniq6(CP) -> bool: + if _stock_ioniq6(CP, marked_only=True): + return True + if not _ioniq6_hda2_base(CP) or not CP.openpilotLongitudinalControl or CP.pcmCruise: + return False + flags = int(CP.flags) + raw = int(CP.safetyConfigs[0].safetyParam) + return raw == (0x8895 if flags & int(HyundaiFlags.CANFD_LKA_STEER_MSG_ALT) else 0x8815) + + +def native_admission_key(CP): + """Complete mutable CP inputs to exact one-slot Ioniq native admission. + + Keep this in sync with qualified_ioniq6 and native inventory qualification. + This key is not an admission decision and never contains live authority. + """ + if CP.brand != 'hyundai' or CP.carFingerprint != HyundaiCAR.HYUNDAI_IONIQ_6 or len(CP.safetyConfigs) != 1: + return None + return (str(CP.brand), str(CP.carFingerprint), int(CP.flags), bool(CP.radarUnavailable), + bool(CP.notCar), bool(CP.passive), bool(CP.dashcamOnly), + bool(CP.openpilotLongitudinalControl), bool(CP.pcmCruise), int(CP.alternativeExperience), + tuple((int(c.safetyModel.raw), int(c.safetyParam)) for c in CP.safetyConfigs)) + + +def ioniq6_settings_capable(CP) -> bool: + # Stock ACC lateral settings are independent of the experimental LONG opt-in. + return _stock_ioniq6(CP) or (IONIQ6_LONG_PREARM_ENABLED and ioniq6_long_eligible(CP)) + + +def _policy_for(CP) -> AolVehiclePolicy: + if qualified_blended_alpha(CP): + return AolVehiclePolicy(intent_supported=True, settings_supported=True, runtime_supported=True, + normal_runtime_supported=True, ordinary_axis_ack_required=qualified_blended_alpha(CP, marked_only=True), + full_axis_runtime_required=qualified_blended_alpha(CP, marked_only=True), + explicit_latch=True, alternative_experience_addition=32) + if qualified_blended_stock(CP): + return AolVehiclePolicy(intent_supported=True, settings_supported=True, runtime_supported=True, + normal_runtime_supported=True, explicit_latch=True, alternative_experience_addition=32) + if qualified_angle_aol(CP): + return AolVehiclePolicy( + intent_supported=True, + settings_supported=True, + runtime_supported=True, + normal_runtime_supported=True, + ordinary_axis_ack_required=qualified_ccnc_ev_stock(CP), + explicit_latch=True, + alternative_experience_addition=32, + ) + if qualified_classic_long(CP): + return AolVehiclePolicy( + intent_supported=True, + settings_supported=True, + runtime_supported=True, + normal_runtime_supported=True, + explicit_latch=True, + safety_param_addition=classic_long_word(CP) ^ int(CP.safetyConfigs[0].safetyParam), + alternative_experience_addition=AOL_EXPERIENCE, + ) + if qualified_classic_scc(CP): + return AolVehiclePolicy( + intent_supported=True, + settings_supported=True, + runtime_supported=True, + normal_runtime_supported=True, + explicit_latch=True, + safety_param_addition=classic_aol_word(CP) ^ int(CP.safetyConfigs[0].safetyParam), + alternative_experience_addition=AOL_EXPERIENCE, + ) + if qualified_canfd_long(CP): + return AolVehiclePolicy(intent_supported=True, settings_supported=True, runtime_supported=True, + normal_runtime_supported=True, explicit_latch=True, safety_param_addition=0x0800, + alternative_experience_addition=32) + if qualified_canfd_stock(CP): + return AolVehiclePolicy( + intent_supported=True, + settings_supported=True, + runtime_supported=True, + normal_runtime_supported=True, + explicit_latch=True, + safety_param_addition=STOCK_AOL_MARKER, + ) + if qualified_non_scc(CP): + return AolVehiclePolicy( + intent_supported=True, + settings_supported=True, + runtime_supported=True, + normal_runtime_supported=True, + explicit_latch=True, + safety_param_addition=AOL_MARKER | (aol_word(CP) & 0x0800), + alternative_experience_addition=AOL_EXPERIENCE, + ) + if qualified_ccnc_ev_stock(CP): + return AolVehiclePolicy(ordinary_axis_ack_required=True) + stock = _stock_ioniq6(CP) + qualified = qualified_ioniq6(CP) or stock + settings = ioniq6_settings_capable(CP) or qualified + distance_personality = ioniq6_long_eligible(CP) + return AolVehiclePolicy( + intent_supported=qualified, + settings_supported=settings, + runtime_supported=qualified, + normal_runtime_supported=qualified and (stock or distance_personality), + explicit_latch=qualified, + distance_personality=distance_personality, + safety_param_addition=0x0800 if stock else 0, + fixed_cruise_buttons=settings, + distance_pause_only=settings, + paddle_pause=settings, + ) + + +def aol_safety_config(CP): + if len(CP.safetyConfigs) == 1: + return CP.safetyConfigs[0] + owner = config_index(CP) + return CP.safetyConfigs[owner] if owner is not None else None + + +def policy_for(CP) -> AolVehiclePolicy: + policy = _policy_for(CP) + marked = ( + qualified_blended_alpha(CP, marked_only=True) or qualified_blended_stock(CP, marked_only=True) + or qualified_angle_aol(CP, marked_only=True) or qualified_classic_long(CP, marked_only=True) + or qualified_classic_scc(CP, marked_only=True) or qualified_canfd_long(CP, marked_only=True) + or qualified_canfd_stock(CP, marked_only=True) or qualified_ioniq6(CP) + or (qualified_non_scc(CP) and CP.alternativeExperience == AOL_EXPERIENCE + and CP.safetyConfigs[0].safetyParam == aol_word(CP)) + ) + config = aol_safety_config(CP) + if policy.runtime_supported and marked and config is not None: + if native_accepts_cp(CP, int(config.safetyModel.raw), int(config.safetyParam)): + policy = replace(policy, ordinary_axis_ack_required=True, full_axis_runtime_required=True) + return policy + + +def native_profile_supported(model: int, param: int) -> bool: + from opendbc.car.hyundai.legacy_long_aol import LEGACY_LONG_AOL_WORDS + return ( + (model == int(car.CarParams.SafetyModel.hyundaiLegacy) and param in LEGACY_LONG_AOL_WORDS) + or (model == int(car.CarParams.SafetyModel.hyundai) and param in LONG_AOL_WORDS | BLENDED_STOCK_WORDS | BLENDED_ALPHA_WORDS) + or classic_native_profile_supported(model, param) + or ( + model == int(car.CarParams.SafetyModel.hyundaiCanfd) + and param in IONIQ6_AOL_SAFETY_PARAMS | CANFD_LONG_AOL_WORDS | ANGLE_AOL_WORDS | STOCK_AOL_WORDS | set(STOCK_SAFETY_PARAMS.values()) + ) + or (model == int(car.CarParams.SafetyModel.hyundai) and param in AOL_WORDS) + ) + + +def native_accepts_cp(CP, model: int, param: int) -> bool: + # Ioniq 6 has its own exact contract; no generic Hyundai owner accepts it. + # Revalidate the complete contract on every call, without caching live authority. + if CP.carFingerprint == HyundaiCAR.HYUNDAI_IONIQ_6: + return qualified_ioniq6(CP) + if qualified_blended_alpha(CP, marked_only=True) or qualified_blended_stock(CP, marked_only=True): + return model == int(car.CarParams.SafetyModel.hyundai) and param == CP.safetyConfigs[0].safetyParam + if qualified_angle_aol(CP, marked_only=True): + return model == int(car.CarParams.SafetyModel.hyundaiCanfd) and param == CP.safetyConfigs[0].safetyParam + if qualified_classic_long(CP, marked_only=True): + return classic_long_accepts(CP, model, param) + if qualified_classic_scc(CP, marked_only=True): + return classic_native_accepts(CP, model, param) + if qualified_canfd_long(CP, marked_only=True): + return model == int(car.CarParams.SafetyModel.hyundaiCanfd) and param == aol_safety_config(CP).safetyParam + if qualified_canfd_stock(CP, marked_only=True): + return model == int(car.CarParams.SafetyModel.hyundaiCanfd) and param == aol_safety_config(CP).safetyParam + if qualified_non_scc(CP): + return CP.alternativeExperience == AOL_EXPERIENCE and model == int(car.CarParams.SafetyModel.hyundai) and param == aol_word(CP) + return ( + qualified_ccnc_ev_stock(CP) and model == int(car.CarParams.SafetyModel.hyundaiCanfd) and param == STOCK_SAFETY_PARAMS[CP.carFingerprint] + ) or qualified_ioniq6(CP) + + +def native_latch_rejected(CP, native) -> bool: + # In this exact native policy a requested lateral axis remains permitted + # through pedal override; denial means the physical authorization was lost. + return ( + native.requestedLateral and not native.lateralAllowed and + ( + qualified_angle_aol(CP, marked_only=True) + or qualified_blended_stock(CP, marked_only=True) + or qualified_blended_alpha(CP, marked_only=True) + or qualified_classic_long(CP, marked_only=True) + or qualified_ioniq6(CP) + or qualified_non_scc(CP) + or qualified_canfd_long(CP, marked_only=True) + or qualified_canfd_stock(CP, marked_only=True) + or qualified_classic_scc(CP, marked_only=True) + ) + ) + + +def create_intent(CP, settings): + from opendbc.car.hyundai.ev6_aol import qualified as ev6_qualified + if ev6_qualified(CP): + from openpilot.starpilot.car.hyundai.ev6_intent import Ev6CardIntent + return Ev6CardIntent(settings) + if qualified_blended_alpha(CP) or qualified_classic_long(CP): + from openpilot.starpilot.car.hyundai.classic_long_intent import ClassicLongCardIntent + + return ClassicLongCardIntent(CP, settings) + if qualified_classic_scc(CP): + from openpilot.starpilot.car.hyundai.classic_scc_intent import ClassicSccCardIntent + + return ClassicSccCardIntent(CP, settings) + if qualified_non_scc(CP): + from openpilot.starpilot.car.hyundai.forte_intent import ForteCardIntent + + return ForteCardIntent(CP, settings) + from openpilot.starpilot.aol.intent import AolCardIntent + + return AolCardIntent(settings, explicit_latch=policy_for(CP).explicit_latch) + + +def ordinary_axis_request_allowed(CP, CS) -> bool: + return bool(request_allowed(CP, CS)) + + +def allow_lateral_onset(CP, *, requested, normal_enabled, steering_pressed, previous_active): + if not (qualified_non_scc(CP) and CP.carFingerprint == HyundaiCAR.HYUNDAI_KONA_NON_SCC): + return bool(requested) + return kona_lateral_onset(permitted=requested, normal_enabled=normal_enabled, steering_pressed=steering_pressed, previous_active=previous_active) + + +def retain_on_native_denial(CP, native, state): + return bool(qualified_angle_aol(CP, marked_only=True) and native.requestedLateral and not native.lateralAllowed and temporary_restriction(CP, state)) diff --git a/openpilot/starpilot/car/hyundai/aol_policy.h b/openpilot/starpilot/car/hyundai/aol_policy.h new file mode 100644 index 0000000000..cb5fe2b7c6 --- /dev/null +++ b/openpilot/starpilot/car/hyundai/aol_policy.h @@ -0,0 +1,36 @@ +#pragma once + +#include "selfdrive/pandad/aol_protocol.h" +#include "opendbc/safety/modes/hyundai_canfd_aol.h" +#include "opendbc/safety/modes/hyundai_canfd_angle_aol.h" + +inline bool hyundai_aol_param(uint16_t param) { + // Existing mode28 entry also transports exact ordinary PE/EV9 requests. + return hyundai_canfd_angle_aol_param(param) || hyundai_canfd_stock_torque_aol_param(param) || hyundai_canfd_torque_long_aol_param(param) || param == 0x8815U || param == 0x8895U || param == 0x5491U || param == 0x5C91U; +} + +inline constexpr AolSafetyProfile HYUNDAI_AOL_PROFILE{28U, hyundai_aol_param, true}; + +inline bool hyundai_classic_scc_aol_param(uint16_t param) { + return param == 0x0500U || param == 0x0D00U || param == 0x8408U || param == 0x840AU || param == 0x8C08U || param == 0x8C0AU || + (((param & 0x0400U) != 0U) && ((param & 0xF1B4U) == 0U) && + ((param & 3U) != 3U) && ((param & 0x0240U) != 0x0240U)); +} + +inline bool hyundai_legacy_aol_param(uint16_t param) { + return param == 0xE902U || param == 0xE903U || param == 0xE912U || param == 0xE913U || + (hyundai_classic_scc_aol_param(param) && ((param & (8U | 256U)) == 0U)); +} + +inline constexpr AolSafetyProfile HYUNDAI_LEGACY_AOL_PROFILE{23U, hyundai_legacy_aol_param, true}; + +inline bool hyundai_classic_long_aol_param(uint16_t param) { + return ((param & 0x0404U) == 0x0404U) && ((param & 0xF3B8U) == 0U) && ((param & 3U) != 3U); +} + +inline bool hyundai_classic_aol_param(uint16_t param) { + return param == 0x2000U || param == 0x2004U || param == 0x2010U || param == 0x2014U || hyundai_classic_long_aol_param(param) || hyundai_classic_scc_aol_param(param) || param == 0x1400U || param == 0x1C00U || param == 0x1440U || param == 0x1C40U || + param == 0x1402U || param == 0x1C02U || param == 0x1441U || param == 0x1C41U; +} + +inline constexpr AolSafetyProfile HYUNDAI_CLASSIC_AOL_PROFILE{8U, hyundai_classic_aol_param, true}; diff --git a/openpilot/starpilot/car/hyundai/button_migration.py b/openpilot/starpilot/car/hyundai/button_migration.py new file mode 100644 index 0000000000..467af098b7 --- /dev/null +++ b/openpilot/starpilot/car/hyundai/button_migration.py @@ -0,0 +1,17 @@ +"""One-time Sonata Hybrid wheel assignment migration.""" + +import os + +from opendbc.car.hyundai.values import CAR + +MIGRATION_KEY = 'SonataHybridLKASButtonControlMigrated' + + +def migrate_lkas_default(cp, params): + if (getattr(cp, 'brand', None) != 'hyundai' or + getattr(cp, 'carFingerprint', None) != CAR.HYUNDAI_SONATA_HYBRID or params.get_bool(MIGRATION_KEY)): + return False + if params.get('LKASButtonControl') is None and not os.path.lexists(params.get_param_path('LKASButtonControl')): + params.put('LKASButtonControl', 9, block=True) + params.put_bool(MIGRATION_KEY, True, block=True) + return True diff --git a/openpilot/starpilot/car/hyundai/classic_long_intent.py b/openpilot/starpilot/car/hyundai/classic_long_intent.py new file mode 100644 index 0000000000..9112fc2e49 --- /dev/null +++ b/openpilot/starpilot/car/hyundai/classic_long_intent.py @@ -0,0 +1,12 @@ +from openpilot.starpilot.aol.intent import AolCardIntent + + +class ClassicLongCardIntent(AolCardIntent): + def __init__(self, cp, settings): + super().__init__(settings, explicit_latch=True) + self.requires_fault_observation = True + self.observe_stock_engagement = True + + def _arm_from_full_engagement(self, CS, *, source_ns): + if not (CS.steerFaultTemporary or CS.gasPressed or CS.brakePressed): + super()._arm_from_full_engagement(CS, source_ns=source_ns) diff --git a/openpilot/starpilot/car/hyundai/classic_scc_intent.py b/openpilot/starpilot/car/hyundai/classic_scc_intent.py new file mode 100644 index 0000000000..f2f97e28bc --- /dev/null +++ b/openpilot/starpilot/car/hyundai/classic_scc_intent.py @@ -0,0 +1,10 @@ +"""Stock SCC engagement intent over the shared physical lateral owner.""" +from openpilot.starpilot.car.hyundai.forte_intent import ForteCardIntent + + +class ClassicSccCardIntent(ForteCardIntent): + observe_stock_engagement = True + + def _arm_from_full_engagement(self, CS, *, source_ns): + if not (CS.steerFaultTemporary or CS.gasPressed or CS.brakePressed): + super()._arm_from_full_engagement(CS, source_ns=source_ns) diff --git a/openpilot/starpilot/car/hyundai/ev6_intent.py b/openpilot/starpilot/car/hyundai/ev6_intent.py new file mode 100644 index 0000000000..39abd6a282 --- /dev/null +++ b/openpilot/starpilot/car/hyundai/ev6_intent.py @@ -0,0 +1,117 @@ +"""EV6 configured AOL desire gated by actual physical authorization.""" +from opendbc.car.hyundai.values import Buttons +from opendbc.car.hyundai.ev6_aol import Ev6PhysicalAuthorization +from openpilot.starpilot.aol.intent import AolCardIntent, AOL_TOGGLE, ButtonType + + +class Ev6CardIntent(AolCardIntent): + observe_stock_engagement = True + + def __init__(self, settings): + # Explicit identifies the native-ACK contract, not a universal button mapping. + super().__init__(settings, explicit_latch=True) + self.physical = Ev6PhysicalAuthorization(lkas_on_engage=settings.enabled and settings.lkas_action == AOL_TOGGLE) + self._samples = () + self._buttons_current = False + self._raw_neutral_seen = False + self._acc_faulted = False + self._controller_override = None + self._engagement_manual_withdrawal = False + + def observe_physical_samples(self, state, *, now_ns): + stamp = int(state.ev6_aol_sample_stamp_ns) + self._buttons_current = bool(state.ev6_aol_source_healthy and 0 < stamp <= now_ns and now_ns - stamp <= int(state.ev6_aol_timeout_ns)) + self._samples = state.ev6_aol_samples if self._buttons_current else () + + def _perform(self, action): + # Same configured desired/pause actions as canonical non-explicit intent. + if action == AOL_TOGGLE: + if self.allowed_latch: + # Withdrawal is always allowed, including the press that removes physical authority. + self.allowed_latch = False + self._withdraw_full_engagement() + elif (self.settings.enabled and self._buttons_current and self.physical.authorized + and self._last_latch_edge_ns > 0 and not self._fault_inhibit and not self._acc_faulted): + self.allowed_latch = True + else: + return + if self.settings.main_action != AOL_TOGGLE and self.settings.lkas_action != AOL_TOGGLE: + self._controller_override = self.allowed_latch + if self.allowed_latch: + self.pause_lateral = False + else: + super()._perform(action) + + def optional_set_release_policy(self): + return bool(self.settings.enabled and self.settings.lkas_action == AOL_TOGGLE + and self._buttons_current and not self._fault_inhibit and not self._acc_faulted) + + def auxiliary_supported(self): + # Canonical auxiliary proposals remain gated by this vehicle's physical owner. + return True + + def update(self, CS, *, consumed_buttons=frozenset(), fault_active=None, now_ns=0, native_rejection_ns=0, + standard_enabled=False): + self._engagement_manual_withdrawal = False + self.physical.lkas_on_engage = self.settings.enabled and self.settings.lkas_action == AOL_TOGGLE + self._acc_faulted = bool(CS.accFaulted) + ordinary = CS.as_reader().as_builder() + ordinary.buttonEvents = [event for event in CS.buttonEvents if event.type not in (ButtonType.mainCruise, ButtonType.lkas, ButtonType.cancel)] + rejected = self._native_rejection_ns < native_rejection_ns <= now_ns + super().update(ordinary, consumed_buttons=consumed_buttons, fault_active=fault_active, now_ns=now_ns, + native_rejection_ns=native_rejection_ns, standard_enabled=standard_enabled) + healthy = (self._buttons_current and CS.canValid and not CS.canTimeout and not fault_active + and not CS.steerFaultPermanent and not self._fault_inhibit and not self._acc_faulted) + if not healthy or rejected: + self.physical.revoke() + self.allowed_latch = False + self._last_latch_edge_ns = 0 + self._raw_neutral_seen = False + lkas_managed = self.settings.enabled and self.settings.lkas_action == AOL_TOGGLE + main_managed = self.settings.main_action == AOL_TOGGLE + auto_main = self.settings.enabled and not lkas_managed and not main_managed + if healthy and not rejected: + for main, lkas, cruise in self._samples: + previous_main, previous_lkas = self.physical.previous_main, self.physical.previous_lkas + previous_cruise = self.physical.previous_cruise + if not self._raw_neutral_seen: + # The filtered base phase is not physical neutral. Initial/restarted + # held packets only establish actual levels; no replayed press credit. + self.physical.previous_main, self.physical.previous_lkas = bool(main), bool(lkas) + self.physical.previous_cruise = cruise + self._raw_neutral_seen = not (main or lkas) and cruise == Buttons.NONE + continue + self.physical.observe(main, lkas, cruise, main_available=CS.cruiseState.available) + main_press = bool(main and not previous_main) + lkas_press = bool(lkas and not previous_lkas) + self._engagement_manual_withdrawal |= main_press or lkas_press + engagement_release = (self.physical.lkas_on_engage and cruise != previous_cruise + and previous_cruise in (Buttons.SET_DECEL, Buttons.RES_ACCEL)) + if main_press or lkas_press or engagement_release: + # Fresh physical authorization never forces configured desired intent. + self._last_latch_edge_ns = now_ns + if main_press and int(ButtonType.mainCruise) not in consumed_buttons: + self._perform(self.settings.main_action) + if lkas_press and int(ButtonType.lkas) not in consumed_buttons: + self._perform(self.settings.lkas_action) + if lkas_managed and (CS.cruiseState.enabled or self.pause_lateral): + self.pause_lateral = not self.allowed_latch + if not CS.cruiseState.available: + self.physical.main_on = False + if auto_main: + self.allowed_latch = bool(CS.cruiseState.available and self._controller_override is not False) + # Retain current fail-closed startup/reset contracts in addition to Dom's gate. + if not (healthy and self.physical.authorized and self._last_latch_edge_ns > 0): + self.allowed_latch = False + self._samples = () + + def _arm_from_full_engagement(self, CS, *, source_ns): + if (self._buttons_current and self.physical.authorized and self._last_latch_edge_ns > 0 + and self._controller_override is not False and not self._acc_faulted and not self.pause_lateral): + # Keep the raw physical edge credit; host engagement cannot manufacture it. + self.allowed_latch = True + + def output(self, CS): + desired, pause_lateral, pause_longitudinal = super().output(CS) + return (desired and self._buttons_current and self.physical.authorized and self._last_latch_edge_ns > 0 + and not self._fault_inhibit and not CS.accFaulted), pause_lateral, pause_longitudinal diff --git a/openpilot/starpilot/car/hyundai/forte_intent.py b/openpilot/starpilot/car/hyundai/forte_intent.py new file mode 100644 index 0000000000..5cfe068e66 --- /dev/null +++ b/openpilot/starpilot/car/hyundai/forte_intent.py @@ -0,0 +1,89 @@ +"""Forte main availability and physical LKAS intent over the shared axis lease.""" +from dataclasses import replace + +from opendbc.car.structs import car +from opendbc.car.hyundai.values import HyundaiFlags +from openpilot.starpilot.aol.intent import AolCardIntent, AOL_TOGGLE + +ButtonType = car.CarState.ButtonEvent.Type + + +class ForteCardIntent(AolCardIntent): + def __init__(self, cp, settings): + super().__init__(settings, explicit_latch=True) + self.requires_fault_observation = True + self.has_lkas = bool(cp.flags & HyundaiFlags.HAS_LDA_BUTTON) + self.physical_latch = False + self.fault_rearm = False + self.main_off_seen = False + self.neutral_seen = False + self._lkas_down = False + self._aux_original = None + self._aux_settings = settings + + def update(self, CS, *, consumed_buttons=frozenset(), fault_active=None, + now_ns=0, native_rejection_ns=0, standard_enabled=False): + # Let the common owner retain distance/axis-pause gestures, without giving + # its unified main/LKAS latch authority over this vehicle's intent. + settings, explicit = self.settings, self.explicit_latch + main_managed = settings.main_action == AOL_TOGGLE or (not self.has_lkas and + settings.main_action != AOL_TOGGLE and settings.lkas_action != AOL_TOGGLE) + if self._aux_original is not settings: + self._aux_original = settings + self._aux_settings = replace(settings, lkas_action=0, main_action=0) + self.settings = self._aux_settings + self.explicit_latch = False + try: + super().update(CS, consumed_buttons=consumed_buttons, now_ns=now_ns, standard_enabled=standard_enabled) + finally: + self.settings, self.explicit_latch = settings, explicit + invalid = not CS.canValid or CS.canTimeout + fatal = bool(fault_active or CS.steerFaultPermanent) + fresh_rejection = self._native_rejection_ns < native_rejection_ns <= now_ns + rejected = fresh_rejection and self._last_latch_edge_ns < native_rejection_ns + if fresh_rejection: + self._native_rejection_ns = native_rejection_ns + if invalid or fatal or rejected: + self.physical_latch = False + self.neutral_seen = self.neutral_seen and not self._lkas_down + if fatal: + self.fault_rearm = True + self.main_off_seen = False + if not settings.enabled: + self.physical_latch = False + if not invalid and not fatal and not CS.cruiseState.available: + self.main_off_seen = True + for event in CS.buttonEvents: + if event.type == ButtonType.mainCruise and event.pressed and settings.main_action != AOL_TOGGLE: + self._perform(settings.main_action) + if event.type != ButtonType.lkas: + continue + if not event.pressed: + self._lkas_down = False + self.neutral_seen = True + elif not self._lkas_down: + self._lkas_down = True + if self.neutral_seen and settings.enabled and not invalid and not fatal and not rejected: + if settings.lkas_action == AOL_TOGGLE: + self._last_latch_edge_ns = now_ns + if self.fault_rearm: + self.fault_rearm = False + if main_managed and CS.cruiseState.available: + # Deliberate correction: both assigned controls toggle effective + # pause symmetrically; a main overwrite cannot make it sticky. + self.pause_lateral = not self.pause_lateral + else: + self.physical_latch = not self.physical_latch + if CS.cruiseState.enabled or self.pause_lateral: + self.pause_lateral = not self.physical_latch + else: + self._perform(settings.lkas_action) + if self.fault_rearm and self.main_off_seen and CS.cruiseState.available and not invalid and not fatal: + self.fault_rearm = False + allowed = CS.cruiseState.available if main_managed else self.physical_latch + self.allowed_latch = bool(settings.enabled and allowed and not invalid and not fatal and not self.fault_rearm) + + def _arm_from_full_engagement(self, CS, *, source_ns): + if not self.fault_rearm: + self.physical_latch = True + super()._arm_from_full_engagement(CS, source_ns=source_ns) diff --git a/openpilot/starpilot/car/hyundai/lateral_fault.py b/openpilot/starpilot/car/hyundai/lateral_fault.py new file mode 100644 index 0000000000..60556d3d9d --- /dev/null +++ b/openpilot/starpilot/car/hyundai/lateral_fault.py @@ -0,0 +1,19 @@ +from opendbc.car.hyundai.values import CAR + + +class LateralFaultLatch: + def __init__(self, fingerprint): + self.enabled = fingerprint == CAR.HYUNDAI_ELANTRA_HEV_2024 + self.faulted = False + self.previous_cruise_enabled = False + + def update(self, *, requested: bool | None, temporary_fault: bool, cruise_enabled: bool) -> bool: + if not self.enabled: + return False + cruise_reenabled = cruise_enabled and not self.previous_cruise_enabled + if requested is False or requested is True and cruise_reenabled: + self.faulted = False + else: + self.faulted |= temporary_fault + self.previous_cruise_enabled = cruise_enabled + return self.faulted diff --git a/openpilot/starpilot/car/hyundai/settings.py b/openpilot/starpilot/car/hyundai/settings.py new file mode 100644 index 0000000000..ebe1cd633c --- /dev/null +++ b/openpilot/starpilot/car/hyundai/settings.py @@ -0,0 +1,32 @@ +from dataclasses import dataclass + +from opendbc.car.hyundai.values import CAR + + +@dataclass(frozen=True) +class WheelSettingsPolicy: + settings_supported: bool = True + fixed_cruise_buttons: bool = True + explicit_latch: bool = True + distance_pause_only: bool = True + paddle_pause: bool = True + runtime_supported: bool = False + lkas_button_supported: bool = False + + +def supports_ioniq6_configuration(cp) -> bool: + return bool(cp is not None and cp.brand == 'hyundai' and cp.carFingerprint == CAR.HYUNDAI_IONIQ_6 and + not cp.notCar and not cp.passive and not cp.dashcamOnly) + + +def supports_long_configuration(cp) -> bool: + return bool(cp is not None and not cp.notCar and not cp.passive and not cp.dashcamOnly and + (supports_ioniq6_configuration(cp) or cp.alphaLongitudinalAvailable or cp.openpilotLongitudinalControl)) + + +def configuration_wheel_policy(cp) -> WheelSettingsPolicy | None: + return WheelSettingsPolicy() if supports_ioniq6_configuration(cp) else None + + +def configuration_media_supported(cp) -> bool: + return supports_ioniq6_configuration(cp) diff --git a/openpilot/starpilot/car/hyundai/tests/test_button_migration.py b/openpilot/starpilot/car/hyundai/tests/test_button_migration.py new file mode 100644 index 0000000000..6ecc45e30f --- /dev/null +++ b/openpilot/starpilot/car/hyundai/tests/test_button_migration.py @@ -0,0 +1,52 @@ +from types import SimpleNamespace + +from opendbc.car.hyundai.values import CAR +from openpilot.starpilot.car.hyundai.button_migration import MIGRATION_KEY, migrate_lkas_default + + +class Saved: + def __init__(self, action): + self.values = {'LKASButtonControl': action, 'AlwaysOnLateral': False} + def get(self, key, **kwargs): + return self.values.get(key) + def get_bool(self, key): + return bool(self.values.get(key, False)) + def get_param_path(self, key): + return '/path-that-does-not-exist-for-test/' + key + def put(self, key, value, **kwargs): + self.values[key] = value + def put_bool(self, key, value, **kwargs): + self.values[key] = value + + +def test_sonata_migration_is_once_and_preserves_postmigration_choice(): + cp = SimpleNamespace(brand='hyundai', carFingerprint=CAR.HYUNDAI_SONATA_HYBRID) + saved = Saved(None) + assert migrate_lkas_default(cp, saved) + assert saved.get('LKASButtonControl') == 9 + assert not saved.get_bool('AlwaysOnLateral') + saved.put('LKASButtonControl', b'5') + assert not migrate_lkas_default(cp, saved) + assert saved.get('LKASButtonControl') == b'5' + assert saved.get_bool(MIGRATION_KEY) + + +def test_unknown_carparams_never_reads_or_writes_saved_settings(): + class Unused: + def get_bool(self, *_): + raise AssertionError('Unknown identity must not access settings') + for cp in (None, object(), SimpleNamespace(), SimpleNamespace(brand='hyundai')): + assert not migrate_lkas_default(cp, Unused()) + + +def test_custom_mapping_and_ioniq_are_preserved(): + cp = SimpleNamespace(brand='hyundai', carFingerprint=CAR.HYUNDAI_SONATA_HYBRID) + for action in (b'0', b'3', b'4', b'5', b'9', b'garbage'): + saved = Saved(action) + assert migrate_lkas_default(cp, saved) + assert saved.get('LKASButtonControl') == action + cp.carFingerprint = CAR.HYUNDAI_IONIQ_6 + saved = Saved(b'5') + assert not migrate_lkas_default(cp, saved) + assert saved.get('LKASButtonControl') == b'5' + assert not saved.get_bool(MIGRATION_KEY) diff --git a/openpilot/starpilot/longitudinal/ioniq6_start.py b/openpilot/starpilot/longitudinal/ioniq6_start.py new file mode 100644 index 0000000000..c1811a45fb --- /dev/null +++ b/openpilot/starpilot/longitudinal/ioniq6_start.py @@ -0,0 +1,107 @@ +"""Conservative start-state admission for an exact tagged Ioniq 6 LONG session. + +This does not qualify ECU ownership; the opt-in prearm still requires a verified +live handoff before longitudinal control is published. +Weak or unknown evidence keeps the ordinary longitudinal PID path. +""" + +import math +from dataclasses import dataclass + +from opendbc.car.hyundai.ioniq6_handoff import IONIQ6_DEBUG_LONG_PARAMS +from opendbc.car.hyundai.values import CAR, HyundaiFlags +from opendbc.car.structs import car + +from openpilot.common.realtime import DT_CTRL + + +LongCtrlState = car.CarControl.Actuators.LongControlState +STRONG_TARGET_MPS2 = 0.75 # Existing Ioniq controller's standstill launch floor. +START_SPEED_MPS = 0.1 +STRONG_SETTLE_FRAMES = round(0.35 / DT_CTRL) +MIN_STRONG_SPAN_NS = (STRONG_SETTLE_FRAMES - 1) * round(DT_CTRL * 1e9) +MAX_FRAME_GAP_NS = 20_000_000 + + +@dataclass(frozen=True) +class StartEvidence: + car_fresh: bool + plan_fresh: bool + lead_clear: bool | None + drive_id: int | None + observed_ns: int | None + + +def eligible(cp) -> bool: + """A stock Ioniq CP never receives the optional starting state.""" + try: + flags = int(cp.flags) + required = int(HyundaiFlags.CANFD | HyundaiFlags.EV | HyundaiFlags.CANFD_LKA_STEER_MSG) + forbidden = int(HyundaiFlags.CANFD_ALT_BUTTONS | HyundaiFlags.CANFD_ANGLE_STEERING | HyundaiFlags.CANFD_CAMERA_SCC) + expected_raw = (0x8095, 0x8895) if flags & int(HyundaiFlags.CANFD_LKA_STEER_MSG_ALT) else (0x8015, 0x8815) + return (str(cp.carFingerprint) == str(CAR.HYUNDAI_IONIQ_6) and cp.brand == 'hyundai' and + cp.openpilotLongitudinalControl is True and cp.alphaLongitudinalAvailable is True and + not cp.pcmCruise and not cp.radarUnavailable and + not cp.passive and not cp.dashcamOnly and not cp.notCar and + flags & required == required and not flags & forbidden and + len(cp.safetyConfigs) == 1 and cp.safetyConfigs[0].safetyModel == car.CarParams.SafetyModel.hyundaiCanfd and + int(cp.safetyConfigs[0].safetyParam) in expected_raw and + int(cp.safetyConfigs[0].safetyParam) in IONIQ6_DEBUG_LONG_PARAMS) + except (AttributeError, IndexError, TypeError, ValueError): + return False + + +class Ioniq6StartPolicy: + def __init__(self) -> None: + self.strong_frames = 0 + self.drive_id: int | None = None + self.last_ns: int | None = None + self.first_ns: int | None = None + + def reset(self) -> None: + self.strong_frames = 0 + self.drive_id = None + self.last_ns = None + self.first_ns = None + + def transition(self, native_state, previous_state, *, active: bool, should_stop: bool, + brake_pressed: bool, gas_pressed: bool, can_valid: bool, can_timeout: bool, + car_fresh: bool, plan_fresh: bool, lead_clear: bool | None, + drive_id: int | None, observed_ns: int | None, + speed_mps: float, target_mps2: float, accel_limits: tuple[float, float]): + """Use native transition except for a fully evidenced, settled start window.""" + try: + lower, upper = accel_limits + physical = all(type(value) in (int, float) and math.isfinite(value) + for value in (speed_mps, target_mps2, lower, upper)) + strong = (physical and active is True and should_stop is False and brake_pressed is False and + gas_pressed is False and can_valid is True and can_timeout is False and + car_fresh is True and plan_fresh is True and lead_clear is True and + type(drive_id) is int and drive_id > 0 and + type(observed_ns) is int and observed_ns > drive_id and + 0.0 <= speed_mps <= START_SPEED_MPS and lower <= 0.0 <= upper and + target_mps2 >= STRONG_TARGET_MPS2 and upper >= STRONG_TARGET_MPS2) + except (TypeError, ValueError, OverflowError): + strong = False + + if not strong or observed_ns is None or drive_id is None: + self.reset() + if previous_state == LongCtrlState.starting: + return LongCtrlState.stopping if active and should_stop else (LongCtrlState.pid if active else LongCtrlState.off) + return native_state + + if (self.drive_id != drive_id or self.last_ns is None or + not 0 < observed_ns - self.last_ns <= MAX_FRAME_GAP_NS): + self.strong_frames = 0 + self.drive_id = drive_id + self.first_ns = observed_ns + self.last_ns = observed_ns + self.strong_frames = min(STRONG_SETTLE_FRAMES, self.strong_frames + 1) + if self.strong_frames >= STRONG_SETTLE_FRAMES and self.first_ns is not None and observed_ns - self.first_ns >= MIN_STRONG_SPAN_NS: + return LongCtrlState.starting + return LongCtrlState.pid if previous_state == LongCtrlState.starting else native_state + + @staticmethod + def starting_output(target_mps2: float, accel_limits: tuple[float, float]) -> float: + """Never add a launch kick above the fresh planner target or native bound.""" + return max(0.0, min(1.0, target_mps2, accel_limits[1])) diff --git a/openpilot/starpilot/models/startup.py b/openpilot/starpilot/models/startup.py index 050af0619c..baadc72420 100644 --- a/openpilot/starpilot/models/startup.py +++ b/openpilot/starpilot/models/startup.py @@ -2,6 +2,9 @@ from dataclasses import dataclass import time +import sys +import traceback +from threading import Thread from openpilot.cereal import messaging from openpilot.common.swaglog import cloudlog @@ -88,3 +91,9 @@ def wait_for_chestnut_power(CP, timeout: float = 30.) -> None: cloudlog.info("Chestnut power stable; starting GPU load") return raise TimeoutError("Chestnut power or vehicle READY did not stabilize") + + +def report_load_timeout(loader: Thread, timeout: float) -> None: + frame = sys._current_frames().get(loader.ident) if loader.ident is not None else None + stack = "".join(traceback.format_stack(frame, limit=24)) if frame is not None else "Loader already exited" + cloudlog.event("chestnut.load_timeout", timeout_s=timeout, loader_stack=stack, error=True) diff --git a/openpilot/starpilot/models/tests/test_startup_power.py b/openpilot/starpilot/models/tests/test_startup_power.py new file mode 100644 index 0000000000..32c5d05143 --- /dev/null +++ b/openpilot/starpilot/models/tests/test_startup_power.py @@ -0,0 +1,162 @@ +from types import SimpleNamespace + +import pytest + +from opendbc.car.hyundai.hyundaicanfd import CanBus +from opendbc.car.hyundai.model_startup import model_power_ready, model_power_ready_bus +from opendbc.car.hyundai.values import CAR, HyundaiFlags +from opendbc.car.structs import CarParams +from openpilot.starpilot.models.startup import POWER_STABLE_NS, PowerReadiness, wait_for_chestnut_power + + +NS = 1_000_000_000 + + +def test_readiness_requires_ready_and_three_seconds_of_current_voltage(): + state = PowerReadiness() + for step in range(200): + now = NS + step * NS // 10 + assert state.update(now, now, 12_000, step >= 170) is False + now += NS // 10 + assert state.update(now, now, 13_200, True) + + +@pytest.mark.parametrize("bad", ["low", "missing", "stale", "future", "not-ready", "clock", "gap"]) +def test_bad_evidence_restarts_stability(bad): + state = PowerReadiness() + for step in range(29): + now = NS + step * NS // 10 + assert not state.update(now, now, 12_000, True) + now += NS // 10 + sample, voltage, ready = now, 12_000, True + if bad == "low": + voltage = 9_999 + elif bad == "missing": + sample = 0 + elif bad == "stale": + sample -= NS + elif bad == "future": + sample += 1 + elif bad == "not-ready": + ready = False + elif bad == "clock": + now -= 2 * NS + sample = now + elif bad == "gap": + now += NS + sample = now + assert not state.update(now, sample, voltage, ready) + for step in range(1, 30): + stamp = now + step * NS // 10 + assert not state.update(stamp, stamp, 12_000, True) + stamp = now + POWER_STABLE_NS + NS // 10 + assert state.update(stamp, stamp, 12_000, True) + + +@pytest.mark.parametrize("lka,offset", [(False, 0), (True, 0), (False, 4), (True, 4)]) +def test_vehicle_ready_uses_exact_vehicle_bus_and_payload(lka, offset): + cp = CarParams(carFingerprint=CAR.HYUNDAI_IONIQ_6, brand="hyundai", flags=int(HyundaiFlags.CANFD_LKA_STEER_MSG) if lka else 0) + # Bus offset is derived from the installed safety configuration. + cp.init("safetyConfigs", 2 if offset else 1) + bus = model_power_ready_bus(cp) + assert bus is not None + assert bus == CanBus(cp).ECAN + data = bytearray(32) + data[3] = 0x40 + def message(address=0x35, src=bus, dat=data): + return SimpleNamespace(address=address, src=src, dat=bytes(dat)) + assert model_power_ready([message()], bus) is True + assert model_power_ready([message(dat=bytes(32))], bus) is False + assert model_power_ready([message(src=bus + 1), message(address=0x36), message(dat=data[:4])], bus) is None + assert model_power_ready([message(), message(dat=bytes(32))], bus) is False + cp.carFingerprint = CAR.KIA_EV6 + assert model_power_ready_bus(cp) is None + + +def test_wait_is_bounded_and_never_opens_gpu_without_power(monkeypatch): + from openpilot.starpilot.models import startup + clock = SimpleNamespace(now=0.) + class Messages: + def __init__(self, services): + self.data = {"pandaStates": [], "deviceState": SimpleNamespace(started=True)} + self.seen = dict.fromkeys(services, False) + self.valid = dict.fromkeys(services, False) + self.updated = dict.fromkeys(services, False) + self.logMonoTime = dict.fromkeys(services, 0) + def __getitem__(self, key): return self.data[key] + def update(self, timeout): clock.now += .1 + monkeypatch.setattr(startup.messaging, "SubMaster", Messages) + monkeypatch.setattr(startup.HARDWARE, "get_device_type", lambda: "mici") + monkeypatch.setattr(startup.time, "monotonic", lambda: clock.now) + monkeypatch.setattr(startup.time, "monotonic_ns", lambda: int(clock.now * NS)) + with pytest.raises(TimeoutError, match="did not stabilize"): + wait_for_chestnut_power(SimpleNamespace(brand="mock"), timeout=1.) + assert clock.now < 1.2 + + +def test_current_drive_requires_positive_monotonic_evidence(): + from openpilot.starpilot.models.startup import current_drive + class SM(dict): + seen = {'deviceState': False} + valid = {'deviceState': True} + logMonoTime = {'deviceState': 20 * NS} + sm = SM(deviceState=SimpleNamespace(started=True, startedMonoTime=10 * NS)) + assert not current_drive(sm, 20 * NS) + sm.seen['deviceState'] = True + assert current_drive(sm, 20 * NS) + assert not current_drive(sm, 23 * NS) + assert not current_drive(sm, 19 * NS) + sm['deviceState'].startedMonoTime = 0 + assert not current_drive(sm, 20 * NS) + + +def test_suspend_gap_requires_new_power_stability(): + from openpilot.starpilot.models.startup import supply_voltage + state = PowerReadiness() + # Native Panda/CAN stamps use BOOTTIME, which advances while MONOTONIC is suspended. + for tick in range(31): + stamp = (20 * NS) + tick * NS // 10 + ready = state.update(stamp, stamp, 12_000, True) + assert ready + assert not state.update(stamp + 60 * NS, stamp, 12_000, True) + assert not state.update(stamp + 60 * NS + NS // 10, stamp + 60 * NS + NS // 10, 12_000, True) + low = SimpleNamespace(pandaType='tres', voltage=3_900) + high = SimpleNamespace(pandaType='tres', voltage=12_000) + assert supply_voltage([low]) == 3_900 + assert supply_voltage([low, high]) == 0 + + +def test_paired_clocks_preserve_distinct_domains(monkeypatch): + from openpilot.starpilot.models import startup + monkeypatch.setattr(startup.time, 'CLOCK_BOOTTIME', 7, raising=False) + monkeypatch.setattr(startup.time, 'monotonic_ns', lambda: 20 * NS) + monkeypatch.setattr(startup.time, 'clock_gettime_ns', lambda clock: 80 * NS) + assert startup.paired_clocks_ns() == (20 * NS, 80 * NS) + + +def test_load_timeout_reports_the_blocked_loader_without_restarting_it(monkeypatch): + import threading + from unittest.mock import Mock + from openpilot.starpilot.models import startup + + entered, release = threading.Event(), threading.Event() + def blocked_gpu_load(): + entered.set() + release.wait(5) + + report = Mock() + monkeypatch.setattr(startup.cloudlog, 'event', report) + loader = threading.Thread(target=blocked_gpu_load) + loader.start() + try: + assert entered.wait(2) + startup.report_load_timeout(loader, 60.) + assert loader.is_alive() + args, kwargs = report.call_args + assert args == ('chestnut.load_timeout',) + assert kwargs['error'] and kwargs['timeout_s'] == 60. + assert 'blocked_gpu_load' in kwargs['loader_stack'] + finally: + release.set() + loader.join(2) + assert not loader.is_alive()