mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
Hyundai
Integrate vehicle-specific steering, longitudinal control, startup, and handoff policies, including Ioniq, EV9, GV70, and non-SCC configurations. Preserve the corresponding native safety and CAN routing contracts. Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com> Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Co-authored-by: Intelli <contact@intelli.software>
This commit is contained in:
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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, {})]
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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')
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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')
|
||||
@@ -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))
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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])))
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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())
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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])
|
||||
@@ -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))
|
||||
@@ -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()
|
||||
@@ -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.)
|
||||
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
+102
@@ -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]
|
||||
]}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user