mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-06 20:43:46 +08:00
Restore Carnival ICE independent lateral and longitudinal startup
This commit is contained in:
@@ -96,9 +96,12 @@ def qualified(cp, *, marked_only=False):
|
||||
|
||||
|
||||
SPORTAGE_LONG_AOL_WORDS = frozenset(0x0804 | fuel | topology for fuel in (0, 2) for topology in (0, 8, 16, 144, 32, 40))
|
||||
LONG_AOL_WORDS = frozenset((0x080D, 0x0815, 0x0895, 0x2815, 0x2895)) | SPORTAGE_LONG_AOL_WORDS
|
||||
CARNIVAL_LONG_AOL_WORD = 0x28B4
|
||||
LONG_AOL_WORDS = frozenset((CARNIVAL_LONG_AOL_WORD, 0x080D, 0x0815, 0x0895, 0x2815, 0x2895)) | SPORTAGE_LONG_AOL_WORDS
|
||||
LONG_EV_CARS = frozenset((CAR.HYUNDAI_IONIQ_5, CAR.HYUNDAI_KONA_EV_2ND_GEN))
|
||||
INDEPENDENT_LONG_BUTTON_CARS = LONG_EV_CARS | {CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, CAR.GENESIS_GV60_EV_1ST_GEN, CAR.KIA_SPORTAGE_5TH_GEN}
|
||||
INDEPENDENT_LONG_BUTTON_CARS = LONG_EV_CARS | {
|
||||
CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, CAR.GENESIS_GV60_EV_1ST_GEN, CAR.KIA_SPORTAGE_5TH_GEN, CAR.KIA_CARNIVAL_2025,
|
||||
}
|
||||
|
||||
|
||||
def qualified_long(cp, *, marked_only=False):
|
||||
@@ -118,6 +121,9 @@ def qualified_long(cp, *, marked_only=False):
|
||||
elif cp.carFingerprint == CAR.KIA_SPORTAGE_5TH_GEN:
|
||||
if word is None or word | 0x804 not in SPORTAGE_LONG_AOL_WORDS:
|
||||
return False
|
||||
elif cp.carFingerprint == CAR.KIA_CARNIVAL_2025:
|
||||
if word != 0x20B0:
|
||||
return False
|
||||
elif word not in (0x11, 0x91):
|
||||
return False
|
||||
raw = int(cp.safetyConfigs[config_index(cp)].safetyParam)
|
||||
|
||||
@@ -512,8 +512,9 @@ class CarController(CarControllerBase):
|
||||
)
|
||||
else:
|
||||
steering_status_active = CC.enabled
|
||||
if self.ev6_aol_status or (self.ioniq6_longitudinal is not None and
|
||||
self.CP.safetyConfigs[-1].safetyParam in (0x8815, 0x8895)):
|
||||
if ((self.CP.carFingerprint == CAR.KIA_CARNIVAL_2025 and self.CP.safetyConfigs[-1].safetyParam == 0x28B4) or
|
||||
self.ev6_aol_status or (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,
|
||||
|
||||
@@ -669,6 +669,12 @@ class CarState(CarStateBase):
|
||||
msgs.append(("CRUISE_BUTTONS_ALT", math.nan))
|
||||
if not (ev9_long_qualified(CP) or pe_long_qualified(CP)):
|
||||
(cam_msgs if CP.flags & HyundaiFlags.CANFD_CAMERA_SCC else msgs).append(("SCC_CONTROL", 50))
|
||||
elif CP.carFingerprint == CAR.KIA_CARNIVAL_2025 and CP.openpilotLongitudinalControl and CP.safetyConfigs[-1].safetyParam in (0x20B4, 0x28B4):
|
||||
msgs = [(name, frequency) for name, frequency in msgs if name != "CRUISE_BUTTONS_ALT"]
|
||||
msgs.extend((("CRUISE_BUTTONS_ALT", 50), ("ACCELERATOR_BRAKE_ALT", 100), ("TCS", 50),
|
||||
("WHEEL_SPEEDS", 100), ("MDPS", 100), (self.gear_msg_canfd, 100),
|
||||
("STEERING_SENSORS", 100), ("DOORS_SEATBELTS", 10), ("BLINKERS", 10), ("SCC_CONTROL", math.nan)))
|
||||
cam_msgs.append(("CAM_0x362", 20))
|
||||
elif torque_ev_scc_eligible(CP):
|
||||
msgs.extend(((self.accelerator_msg_canfd, 100), ("TCS", 50), ("WHEEL_SPEEDS", 100), ("MDPS", 100)))
|
||||
msgs.extend((("CRUISE_BUTTONS_ALT", math.nan), ("DOORS_SEATBELTS", math.nan),
|
||||
|
||||
@@ -49,6 +49,11 @@ EXISTING_OWNERS_OR_DISTINCT_RECEIVERS = frozenset((
|
||||
CAR.GENESIS_GV80,
|
||||
))
|
||||
|
||||
def carnival_long(cp):
|
||||
return (cp.carFingerprint == CAR.KIA_CARNIVAL_2025 and qualified_long(cp) and
|
||||
cp.safetyConfigs[0].safetyParam in (0x20B4, 0x28B4))
|
||||
|
||||
|
||||
STOCK_WORDS = (0x10, 0x11, 0x12, 0x90, 0x91, 0x92)
|
||||
|
||||
|
||||
@@ -65,6 +70,8 @@ def qualified(cp):
|
||||
HyundaiFlags.CANFD_ALT_GEARS | HyundaiFlags.CANFD_ALT_GEARS_2)
|
||||
if not required(cp) or cp.carFingerprint not in CARS:
|
||||
return False
|
||||
if carnival_long(cp):
|
||||
return cp.alphaLongitudinalAvailable and cp.alternativeExperience == 0
|
||||
factory = CAR(cp.carFingerprint)
|
||||
stock = stock_copy(cp)
|
||||
return (cp.alphaLongitudinalAvailable and cp.alternativeExperience == 0 and not cp.pcmCruise and
|
||||
@@ -91,13 +98,13 @@ class GenericHDA2Startup(RadarHDA2Startup):
|
||||
raise RuntimeError('Ordinary HDA-II finalization requires configured startup')
|
||||
if self.prepared_for(ci.CP):
|
||||
return
|
||||
long_aol = self.outcome is Outcome.SENT_UNCONFIRMED and qualified_long(ci.CP, marked_only=True)
|
||||
long_aol = self.outcome in (Outcome.PENDING, Outcome.SENT_UNCONFIRMED) and qualified_long(ci.CP, marked_only=True)
|
||||
if not long_aol and (self.outcome not in (Outcome.STOCK_UNTOUCHED, Outcome.STOCK_RESTORED) or
|
||||
not stock_qualified(ci.CP, marked_only=True)):
|
||||
raise RuntimeError('Ordinary HDA-II startup does not admit this AOL configuration')
|
||||
expected: Any = deepcopy(self.prepared_cp)
|
||||
configs = expected.get('safetyConfigs', [])
|
||||
allowed_words = tuple(word | 4 for word in STOCK_WORDS) if long_aol else STOCK_WORDS
|
||||
allowed_words = (*tuple(word | 4 for word in STOCK_WORDS), 0x20B4) if long_aol else STOCK_WORDS
|
||||
if (len(configs) != 1 or configs[0]['safetyModel'] != 'hyundaiCanfd' or
|
||||
configs[0]['safetyParam'] not in allowed_words or expected['alternativeExperience'] != 0):
|
||||
raise RuntimeError('Ordinary HDA-II prepared stock profile cannot be finalized')
|
||||
@@ -107,3 +114,50 @@ class GenericHDA2Startup(RadarHDA2Startup):
|
||||
if ci.CP.to_dict() != expected:
|
||||
raise RuntimeError('Ordinary HDA-II finalization changed unrelated CarParams')
|
||||
self.prepared_cp = expected
|
||||
|
||||
def prepare_when_controls_ready(self, *, admission):
|
||||
if not carnival_long(self.cp):
|
||||
return self.prepare(admission=admission)
|
||||
if self.outcome is not None or self.closed:
|
||||
raise RuntimeError('Carnival startup preparation is single-use')
|
||||
if admission() is not True:
|
||||
return self.prepare(admission=lambda: False)
|
||||
self.admission = admission
|
||||
self.floor_ns = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
|
||||
self.outcome = Outcome.PENDING
|
||||
self.prepared_cp = deepcopy(self.cp.to_dict())
|
||||
return self.cp
|
||||
|
||||
def commit_pending(self, ci, *, sources_current, control_current):
|
||||
if (not carnival_long(ci.CP) or self.outcome is not Outcome.PENDING or self.closed or not self.published or
|
||||
ci is not self.ci or not self.prepared_for(ci.CP) or not sources_current or not control_current or
|
||||
self.admission is None or self.admission() is not True):
|
||||
raise RuntimeError('Carnival takeover requires current diagnostic, source and inactive control admission')
|
||||
try:
|
||||
success = self._disable(self.callbacks[0], self._send, bus=self.bus, addr=self.address,
|
||||
com_cont_req=self.communication_control_request())
|
||||
if not success or not self.disable_attempted:
|
||||
raise RuntimeError('Carnival pending ECU disable was not sent successfully')
|
||||
self.outcome = Outcome.SENT_UNCONFIRMED
|
||||
self.floor_ns = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
|
||||
from opendbc.car import make_tester_present_msg
|
||||
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()
|
||||
except BaseException:
|
||||
if self.keeper is not None:
|
||||
self.keeper.stop()
|
||||
try:
|
||||
restored = not (self.session_attempted or self.disable_attempted) or self._restore()
|
||||
except Exception:
|
||||
restored = False
|
||||
self.outcome = Outcome.ABORT_UNCERTAIN
|
||||
if not restored:
|
||||
raise RuntimeError('Carnival pending takeover failed; stock restoration unverified') from None
|
||||
raise
|
||||
|
||||
def before_control(self, **context):
|
||||
if self.outcome is Outcome.PENDING:
|
||||
self.check()
|
||||
return False
|
||||
return super().before_control(**context)
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
"""Keep factory SCC until an inactive producer is current; never bypass native RX."""
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.car import Bus, structs
|
||||
from opendbc.car.hyundai.ecu_startup import Outcome
|
||||
from opendbc.car.hyundai.interface import CarInterface
|
||||
from opendbc.car.hyundai.radar_interface import RadarInterface
|
||||
from opendbc.car.hyundai.tests.test_torque_ev_startup import Keeper
|
||||
from opendbc.car.hyundai.tests.test_generic_hda2_startup_lifecycle import physical_frames as source_frames
|
||||
from opendbc.car.hyundai.tests.test_ccnc_six import params
|
||||
from opendbc.car.hyundai.values import CAR
|
||||
from opendbc.car.hyundai.canfd_stock_aol import qualified_long
|
||||
from opendbc.car.hyundai.torque_ev_scc import eligible as ev_scc_eligible
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
from openpilot.cereal import messaging
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.selfdrive.car.card import Car
|
||||
from openpilot.starpilot.vehicle_startup import VehicleStartupOwner
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scenario", (
|
||||
"healthy_suppressed", "healthy_positive", "stale_control", "active_control", "missing_source",
|
||||
"native_mode", "cruise_fault", "stock_engaged", "disable_negative", "disable_malformed", "restore_failure", "publisher_failure"))
|
||||
def test_actual_card_defers_carnival_until_initialized_and_then_native_rx(scenario):
|
||||
stock = params(CAR.KIA_CARNIVAL_2025, hda2=True, alt_buttons=True, alpha=True, adas=True, alt_lka=True)
|
||||
assert stock.safetyConfigs[0].safetyParam == 0x20B4 and qualified_long(stock) and not ev_scc_eligible(stock)
|
||||
native = libsafety_py.libsafety
|
||||
assert native.set_safety_hooks(int(structs.CarParams.SafetyModel.elm327), 1) == 0
|
||||
native.init_tests()
|
||||
now = [100_000_000_000]
|
||||
requests, emitted = [], []
|
||||
muted = [False]
|
||||
|
||||
def send(frames):
|
||||
for address, data, bus in frames:
|
||||
assert native.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data))
|
||||
emitted.extend(frames)
|
||||
if scenario == "publisher_failure" and any(data[:4] == b"\x03\x28\x83\x01" for _, data, _ in frames):
|
||||
muted[0] = True
|
||||
raise OSError("diagnostic publisher failed after possible ECU delivery")
|
||||
|
||||
def query(sender, receiver, bus, addresses, commands, responses):
|
||||
command = commands[0]
|
||||
requests.append((now[0], command))
|
||||
|
||||
class Query:
|
||||
def get_data(self, timeout):
|
||||
sender([(0x730, bytes([len(command)]) + command + bytes(7 - len(command)), bus)])
|
||||
if command == b"\x28\x83\x01":
|
||||
muted[0] = True
|
||||
if scenario in ("disable_negative", "restore_failure"):
|
||||
return {(0x730, None): b"\x7f\x28\x22"}
|
||||
if scenario == "disable_malformed":
|
||||
return {(0x730, None): b"\x68"}
|
||||
return {(0x730, None): b"\x68\x03"} if scenario == "healthy_positive" else {}
|
||||
if command == b"\x28\x00\x01":
|
||||
if scenario == "restore_failure":
|
||||
return {}
|
||||
muted[0] = False
|
||||
return {(0x730, None): b""}
|
||||
return Query()
|
||||
|
||||
def sleep(seconds):
|
||||
now[0] += round(seconds * 1e9)
|
||||
|
||||
def update_sm(host, command, stale=False):
|
||||
message = messaging.new_message("carControl", valid=True, logMonoTime=now[0] - (151_000_000 if stale else 0))
|
||||
message.carControl = command
|
||||
panda = messaging.new_message("pandaStates", 1, valid=True, logMonoTime=now[0])
|
||||
state = panda.pandaStates[0]
|
||||
state.safetyModel, state.safetyParam = native.get_current_safety_mode(), native.get_current_safety_param()
|
||||
state.alternativeExperience = host.CP.alternativeExperience
|
||||
state.ignitionLine = True
|
||||
state.safetyRxChecksInvalid = native.safety_test_toyota_rx_summary_invalid()
|
||||
host.sm.update_msgs(now[0] / 1e9, [message.as_reader(), panda.as_reader()])
|
||||
|
||||
with OpenpilotPrefix(), patch.dict(os.environ, {"SIMULATION": "0", "AOL_REPLAY_RUNTIME": "1"}), \
|
||||
patch("time.clock_gettime_ns", side_effect=lambda clock: now[0]), \
|
||||
patch("time.monotonic_ns", side_effect=lambda: now[0]), \
|
||||
patch("time.monotonic", side_effect=lambda: now[0] / 1e9), \
|
||||
patch("time.sleep", side_effect=sleep):
|
||||
saved = Params()
|
||||
for key in ("OpenpilotEnabledToggle", "AlwaysOnLateral", "AlphaLongitudinalEnabled"):
|
||||
saved.put_bool(key, True, block=True)
|
||||
holder = VehicleStartupOwner()
|
||||
prepared = holder.prepare(stock, CarInterface, (list, send), requested=True,
|
||||
admission=lambda: True,
|
||||
defer_until_controls=True)
|
||||
startup = holder.owner
|
||||
assert startup is not None
|
||||
startup.clock, startup.keeper_factory, startup._query = lambda: now[0] / 1e9, Keeper, query
|
||||
try:
|
||||
assert startup.outcome is Outcome.PENDING and not requests and not emitted
|
||||
assert not startup.before_control(configured=True, sources_current=True, control_current=True)
|
||||
card = Car(CI=CarInterface(prepared), RI=RadarInterface(prepared), startup_owner=startup)
|
||||
startup.admission = card.startup_diagnostic_admission
|
||||
card.params.put_bool("IsOffroad", False, block=True)
|
||||
original_cp = card.CP.to_dict()
|
||||
assert card.CP.safetyConfigs[0].safetyParam == 0x28B4 and card.CP.alternativeExperience == 32
|
||||
assert not ev_scc_eligible(card.CP)
|
||||
# Explicit synthetic delayed producer: stock has not been told to stop.
|
||||
for tick in range(1, 701):
|
||||
now[0] += 10_000_000
|
||||
state = card.CI.update([(now[0], [f for f in source_frames(card.CI, tick) if f[0] not in (0x1A0, 0x1CF, 0x160)])])
|
||||
assert state.canValid and not state.canTimeout and not state.accFaulted
|
||||
assert not requests and not muted[0] and startup.keeper is None
|
||||
command = structs.CarControl(enabled=scenario == "active_control", latActive=scenario == "active_control").as_reader()
|
||||
if scenario == "missing_source":
|
||||
parser = card.CI.can_parsers[Bus.pt]
|
||||
parser.message_states[parser.dbc.name_to_msg["TCS"].address].timestamps.clear()
|
||||
if scenario == "cruise_fault":
|
||||
state.accFaulted = True
|
||||
if scenario == "stock_engaged":
|
||||
state.cruiseState.enabled = True
|
||||
config = card.CP.safetyConfigs[-1]
|
||||
if scenario == "native_mode":
|
||||
native.set_alternative_experience(card.CP.alternativeExperience)
|
||||
assert native.set_safety_hooks(int(config.safetyModel.raw), int(config.safetyParam)) == 0
|
||||
update_sm(card, command, stale=scenario == "stale_control")
|
||||
card.controls_update(state, command, initialize_only=True)
|
||||
assert not requests and not muted[0] and not card.ci_initialized
|
||||
if scenario in ("disable_negative", "disable_malformed", "restore_failure", "publisher_failure"):
|
||||
with pytest.raises(RuntimeError, match="pending"):
|
||||
card.controls_update(state, command)
|
||||
assert startup.outcome is Outcome.ABORT_UNCERTAIN and not card.ci_initialized
|
||||
assert card.CP.to_dict() == original_cp and not startup.handed_off
|
||||
assert muted[0] == (scenario == "restore_failure")
|
||||
assert startup.restored == (scenario != "restore_failure")
|
||||
return
|
||||
card.controls_update(state, command)
|
||||
if not scenario.startswith("healthy"):
|
||||
assert not requests and not muted[0] and not card.ci_initialized
|
||||
assert startup.outcome is Outcome.PENDING and card.CP.to_dict() == original_cp
|
||||
return
|
||||
assert [command for _, command in requests] == [b"\x10\x03", b"\x28\x83\x01"]
|
||||
assert muted[0] and card.ci_initialized and startup.outcome is Outcome.SENT_UNCONFIRMED
|
||||
assert startup.keeper.started and not startup.handed_off
|
||||
# No SCC while native is diagnostic, mismatched, or awaiting RX health.
|
||||
emitted.clear()
|
||||
card.controls_update(state, command)
|
||||
assert not emitted and not startup.handed_off
|
||||
native.set_alternative_experience(card.CP.alternativeExperience)
|
||||
assert native.set_safety_hooks(int(config.safetyModel.raw), int(config.safetyParam)) == 0
|
||||
native.init_tests()
|
||||
update_sm(card, command)
|
||||
card.controls_update(state, command)
|
||||
assert not emitted and not startup.handed_off
|
||||
for tick in range(701, 811):
|
||||
now[0] += 10_000_000
|
||||
frames = [f for f in source_frames(card.CI, tick) if f[0] not in (0x1A0, 0x1CF, 0x160)]
|
||||
state = card.CI.update([(now[0], frames)])
|
||||
native.set_timer(tick * 10_000)
|
||||
for address, data, bus in frames:
|
||||
native.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, data))
|
||||
if tick % 100 == 0:
|
||||
native.safety_tick()
|
||||
assert native.safety_config_valid()
|
||||
update_sm(card, command)
|
||||
captured = []
|
||||
|
||||
def publish(frames, valid=True):
|
||||
if valid:
|
||||
send(frames)
|
||||
card.vehicle_startup.sent(frames, valid=valid)
|
||||
captured.extend(frames)
|
||||
|
||||
with patch.object(card, "publish_sendcan", side_effect=publish):
|
||||
card.controls_update(state, command)
|
||||
assert any(address == 0x1A0 and bus == 1 for address, _, bus in captured)
|
||||
decoder = card.CI.can_parsers[Bus.pt]
|
||||
decoder.update((now[0], captured))
|
||||
scc = decoder.vl['SCC_CONTROL']
|
||||
assert scc['ACCMode'] == 0 and scc['aReqRaw'] == 0 and scc['aReqValue'] == 0
|
||||
assert startup.handed_off and not startup.keeper.started
|
||||
assert not native.get_controls_allowed() and native.aol_get_permission_mask() == 0
|
||||
assert card.CP.to_dict() == original_cp
|
||||
finally:
|
||||
holder.close()
|
||||
@@ -0,0 +1,51 @@
|
||||
import pytest
|
||||
from opendbc.car import Bus
|
||||
from opendbc.car.hyundai.tests.test_ccnc_six import params
|
||||
from opendbc.car.hyundai.values import CAR, HyundaiFlags
|
||||
from opendbc.car.hyundai.canfd_stock_aol import qualified_long
|
||||
from opendbc.car.hyundai.generic_hda2_startup import qualified
|
||||
from opendbc.car.hyundai.carstate import CarState
|
||||
|
||||
|
||||
def carnival():
|
||||
return params(CAR.KIA_CARNIVAL_2025, hda2=True, alt_buttons=True, alpha=True, adas=True, alt_lka=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('word,experience,expected', [
|
||||
(0x20B4,0,True), (0x28B4,32,True), (0x28B4,0,False), (0x20B4,32,False),
|
||||
(0x28B6,32,False), (0x28B0,32,False), (0x8B4,32,False), (0x28B5,32,False),
|
||||
])
|
||||
def test_exact_profile(word, experience, expected):
|
||||
cp = carnival()
|
||||
cp.safetyConfigs[0].safetyParam, cp.alternativeExperience = word, experience
|
||||
assert qualified_long(cp) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('mutation', ['hybrid','other_car','passive','dashcam','wrong_model','angle','other_bus'])
|
||||
def test_reject_other_configuration(mutation):
|
||||
cp = carnival()
|
||||
if mutation == 'hybrid':
|
||||
cp.flags = int(cp.flags | HyundaiFlags.HYBRID)
|
||||
elif mutation == 'other_car':
|
||||
cp.carFingerprint = CAR.KIA_CARNIVAL_HEV_4TH_GEN
|
||||
elif mutation == 'passive':
|
||||
cp.passive = True
|
||||
elif mutation == 'dashcam':
|
||||
cp.dashcamOnly = True
|
||||
elif mutation == 'angle':
|
||||
cp.flags = int(cp.flags | HyundaiFlags.CANFD_ANGLE_STEERING)
|
||||
elif mutation == 'other_bus':
|
||||
cp.safetyConfigs = [dict(safetyModel='noOutput'),dict(safetyModel='hyundaiCanfd',safetyParam=0x20B4)]
|
||||
else:
|
||||
cp.safetyConfigs[0].safetyModel = 'noOutput'
|
||||
assert not qualified(cp)
|
||||
|
||||
|
||||
def test_takeover_health_binds_physical_sources_not_removed_scc():
|
||||
cp = carnival()
|
||||
parser = CarState(cp).get_can_parsers(cp)[Bus.pt]
|
||||
assert parser.message_states[0x1AA].frequency == 50
|
||||
assert 0x1CF not in parser.addresses
|
||||
assert not parser.message_states[0x1A0].frequency > 0
|
||||
for address in (0x100,0x175,0xA0,0xEA):
|
||||
assert parser.message_states[address].frequency > 0
|
||||
@@ -8,7 +8,7 @@ from opendbc.car.hyundai.canfd_stock_aol import qualified_long
|
||||
def eligible(cp):
|
||||
from opendbc.car.hyundai.values import CAR
|
||||
# Genesis keeps its existing longitudinal controller.
|
||||
return cp.carFingerprint not in (CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, CAR.GENESIS_GV60_EV_1ST_GEN) and qualified_long(cp)
|
||||
return cp.carFingerprint not in (CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, CAR.GENESIS_GV60_EV_1ST_GEN, CAR.KIA_CARNIVAL_2025) and qualified_long(cp)
|
||||
|
||||
|
||||
def options(available, control_state, lead):
|
||||
|
||||
@@ -1359,13 +1359,15 @@ static safety_config hyundai_canfd_init_validated(uint16_t supplied_param) {
|
||||
// under torque LKA + LONG. Other uses of the bit retain their original meaning.
|
||||
const uint16_t damping_profile_mask = HYUNDAI_PARAM_CANFD_ANGLE | 16U | HYUNDAI_PARAM_LONGITUDINAL | 8192U;
|
||||
hyundai_canfd_lkas_damping_100 = (param & damping_profile_mask) == (16U | HYUNDAI_PARAM_LONGITUDINAL | 8192U);
|
||||
const bool carnival_long_requested = (param == 0x20B4U) || (param == 0x28B4U);
|
||||
|
||||
if (hyundai_canfd_lkas_damping_100) {
|
||||
param &= (uint16_t)~8192U;
|
||||
}
|
||||
const bool angle_requested = GET_FLAG(param, HYUNDAI_PARAM_CANFD_ANGLE);
|
||||
const bool ioniq6_stock_aol_requested = hyundai_canfd_stock_torque_aol_param(param);
|
||||
#ifdef ALLOW_DEBUG
|
||||
const bool torque_long_aol_requested = hyundai_canfd_torque_long_aol_param(param) && ((unsigned int)alternative_experience == 32U);
|
||||
const bool torque_long_aol_requested = ((carnival_long_requested && ((param & 0x0800U) != 0U)) || hyundai_canfd_torque_long_aol_param(param)) && ((unsigned int)alternative_experience == 32U);
|
||||
#else
|
||||
const bool torque_long_aol_requested = false;
|
||||
#endif
|
||||
@@ -1559,7 +1561,7 @@ static safety_config hyundai_canfd_init_validated(uint16_t supplied_param) {
|
||||
hyundai_canfd_angle_model = 0U;
|
||||
#ifdef ALLOW_DEBUG
|
||||
hyundai_canfd_ioniq6_long = ioniq6_long_requested;
|
||||
hyundai_canfd_torque_ev_long = (param == 0x0015U) || (param == 0x0095U) || (torque_long_aol_requested && GET_FLAG(param, 16U)) || gv60_long_requested;
|
||||
hyundai_canfd_torque_ev_long = (param == 0x0015U) || (param == 0x0095U) || (torque_long_aol_requested && GET_FLAG(param, 16U) && !carnival_long_requested) || gv60_long_requested;
|
||||
#endif
|
||||
hyundai_torque_aol_cancel_preserves_lateral = ioniq6_stock_aol_requested || torque_long_aol_requested ||
|
||||
(param == 0x8815U) || (param == 0x8895U);
|
||||
@@ -1813,7 +1815,20 @@ static safety_config hyundai_canfd_init_validated(uint16_t supplied_param) {
|
||||
HYUNDAI_CANFD_STD_BUTTONS_RX_CHECKS(1)
|
||||
};
|
||||
|
||||
static const RxCheck hyundai_canfd_lka_alt_buttons_long_rx_checks[] = {
|
||||
HYUNDAI_CANFD_ALT_BUTTONS_RX_CHECKS(1)
|
||||
};
|
||||
static const RxCheck carnival_long_rx_checks[] = {
|
||||
HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(1, 0x100)
|
||||
};
|
||||
ret = BUILD_SAFETY_CFG(hyundai_canfd_lka_steer_msg_long_rx_checks, HYUNDAI_CANFD_LKA_STEER_MSG_LONG_TX_MSGS);
|
||||
if (carnival_long_requested) {
|
||||
SET_RX_CHECKS(carnival_long_rx_checks, ret);
|
||||
} else if (hyundai_canfd_alt_buttons) {
|
||||
SET_RX_CHECKS(hyundai_canfd_lka_alt_buttons_long_rx_checks, ret);
|
||||
} else {
|
||||
// Standard-button profiles retain their existing physical source.
|
||||
}
|
||||
if (hyundai_canfd_lka_steer_msg_alt) {
|
||||
SET_TX_MSGS(HYUNDAI_CANFD_LKA_STEER_MSG_ALT_LONG_TX_MSGS, ret);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,6 @@ static inline bool hyundai_canfd_torque_long_aol_param(uint16_t param) {
|
||||
((base & 4U) != 0U) && ((fuel == 0U) || (fuel == 2U)) &&
|
||||
((topology == 0U) || (topology == 8U) || (topology == 16U) ||
|
||||
(topology == 144U) || (topology == 32U) || (topology == 40U));
|
||||
return sportage || (param == 0x080DU) || (param == 0x0815U) || (param == 0x0895U) || (param == 0x2815U) || (param == 0x2895U);
|
||||
return (param == 0x28B4U) || sportage || (param == 0x080DU) || (param == 0x0815U) || (param == 0x0895U) || (param == 0x2815U) || (param == 0x2895U);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.hyundai.hyundaicanfd import create_steering_messages
|
||||
from opendbc.car.hyundai.values import CAR, HyundaiFlags
|
||||
from opendbc.car.hyundai.canfd_stock_aol import CARNIVAL_LONG_AOL_WORD
|
||||
|
||||
SPORTAGE_LONG_AOL_WORDS = (CARNIVAL_LONG_AOL_WORD,)
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
from opendbc.safety.tests import test_hyundai_shared_canfd_stock_aol as fixtures
|
||||
|
||||
|
||||
class TestCarnivalLongAol(unittest.TestCase):
|
||||
packet = staticmethod(fixtures.TestHyundaiSharedCanfdStockAol.packet)
|
||||
healthy = fixtures.TestHyundaiSharedCanfdStockAol.healthy
|
||||
gesture = fixtures.TestHyundaiSharedCanfdStockAol.gesture
|
||||
|
||||
def setUp(self):
|
||||
self.safety = libsafety_py.libsafety
|
||||
self.packer = CANPacker('hyundai_canfd_generated')
|
||||
self.release = self.safety.set_safety_hooks(structs.CarParams.SafetyModel.allOutput, 0) != 0
|
||||
|
||||
def tearDown(self):
|
||||
self.safety.set_alternative_experience(0)
|
||||
self.safety.set_safety_hooks(structs.CarParams.SafetyModel.noOutput, 0)
|
||||
|
||||
def mode(self, word, experience=32):
|
||||
self.safety.set_alternative_experience(experience)
|
||||
assert self.safety.set_safety_hooks(structs.CarParams.SafetyModel.hyundaiCanfd, word) == 0
|
||||
self.safety.init_tests()
|
||||
self.safety.set_timer(1_000_000)
|
||||
|
||||
def test_physical_arm_cancel_pedals_and_ungranted_longitudinal(self):
|
||||
for word in SPORTAGE_LONG_AOL_WORDS:
|
||||
with self.subTest(word=hex(word)):
|
||||
self.mode(word)
|
||||
if self.release:
|
||||
assert not self.safety.safety_config_valid()
|
||||
continue
|
||||
self.healthy(word)
|
||||
self.safety.set_aol_test_heartbeat(True)
|
||||
self.safety.aol_set_host_request(3)
|
||||
assert self.safety.aol_get_permission_mask() == 0
|
||||
self.gesture(word)
|
||||
assert self.safety.aol_get_permission_mask() == 1
|
||||
bus = 1 if word & 16 else 0
|
||||
flags = HyundaiFlags.CANFD
|
||||
if word & 16:
|
||||
flags |= HyundaiFlags.CANFD_LKA_STEER_MSG
|
||||
if word & 128:
|
||||
flags |= HyundaiFlags.CANFD_LKA_STEER_MSG_ALT
|
||||
cp = SimpleNamespace(carFingerprint=CAR.KIA_CARNIVAL_2025, flags=int(flags), openpilotLongitudinalControl=True)
|
||||
frames = create_steering_messages(self.packer, cp, SimpleNamespace(ECAN=bus, ACAN=1-bus), True, True, 1)
|
||||
for frame in frames:
|
||||
assert self.safety.safety_tx_hook(self.packet(frame))
|
||||
excessive = create_steering_messages(self.packer, cp, SimpleNamespace(ECAN=bus, ACAN=1-bus), True, True, 410)
|
||||
assert not self.safety.safety_tx_hook(self.packet(excessive[0]))
|
||||
name = 'CRUISE_BUTTONS_ALT' if word & 32 else 'CRUISE_BUTTONS'
|
||||
frame = self.packer.make_can_msg(name, bus, {'COUNTER': 9, 'CRUISE_BUTTONS': 4})
|
||||
assert self.safety.safety_rx_hook(self.packet(frame))
|
||||
assert self.safety.aol_get_permission_mask() == 1
|
||||
for counter, brake in ((7, True), (8, False)):
|
||||
frame = self.packer.make_can_msg('TCS', bus, {'COUNTER': counter, 'DriverBraking': brake})
|
||||
assert self.safety.safety_rx_hook(self.packet(frame))
|
||||
assert self.safety.aol_get_permission_mask() == 1
|
||||
accel = self.packet(self.packer.make_can_msg('SCC_CONTROL', bus, {'ACCMode': 1, 'aReqRaw': .1, 'aReqValue': .1}))
|
||||
assert not self.safety.safety_tx_hook(accel)
|
||||
self.safety.set_timer(1_500_001)
|
||||
assert self.safety.aol_get_permission_mask() == 0
|
||||
|
||||
def test_wrong_experience_missing_source_and_wrong_bus_deny(self):
|
||||
for word in SPORTAGE_LONG_AOL_WORDS:
|
||||
for experience in (0, 1, 33):
|
||||
self.mode(word, experience)
|
||||
assert not self.safety.safety_config_valid()
|
||||
if self.release:
|
||||
continue
|
||||
fuel = 'ACCELERATOR_ALT' if word & 2 else 'ACCELERATOR_BRAKE_ALT'
|
||||
buttons = 'CRUISE_BUTTONS_ALT' if word & 32 else 'CRUISE_BUTTONS'
|
||||
for omitted in (fuel, buttons, 'TCS', 'WHEEL_SPEEDS', 'MDPS'):
|
||||
self.mode(word)
|
||||
self.healthy(word, omit=omitted)
|
||||
if omitted != buttons:
|
||||
self.gesture(word)
|
||||
assert self.safety.aol_get_permission_mask() == 0
|
||||
|
||||
def test_fresh_set_release_arms_lateral_through_cancel_and_brake(self):
|
||||
for word in SPORTAGE_LONG_AOL_WORDS:
|
||||
self.mode(word)
|
||||
if self.release:
|
||||
assert not self.safety.safety_config_valid()
|
||||
continue
|
||||
name = 'CRUISE_BUTTONS_ALT' if word & 32 else 'CRUISE_BUTTONS'
|
||||
maximum = 255 if word & 32 else 14
|
||||
bus = 1 if word & 16 else 0
|
||||
self.healthy(word, omit=name)
|
||||
self.safety.set_aol_test_heartbeat(True)
|
||||
self.safety.aol_set_host_request(3)
|
||||
for tick, counter, button in ((0, maximum - 1, 0), (1, maximum, 2), (2, 0, 0)):
|
||||
self.safety.set_timer(1_000_000 + tick * 20_000)
|
||||
frame = self.packer.make_can_msg(name, bus, {'COUNTER': counter, 'CRUISE_BUTTONS': button})
|
||||
assert self.safety.safety_rx_hook(self.packet(frame))
|
||||
self.safety.aol_set_host_request(3)
|
||||
assert self.safety.get_controls_allowed()
|
||||
assert self.safety.aol_get_permission_mask() == 3
|
||||
self.safety.set_timer(1_060_000)
|
||||
frame = self.packer.make_can_msg(name, bus, {'COUNTER': 1, 'CRUISE_BUTTONS': 4})
|
||||
assert self.safety.safety_rx_hook(self.packet(frame))
|
||||
assert not self.safety.get_controls_allowed()
|
||||
assert self.safety.aol_get_permission_mask() == 1, hex(word)
|
||||
frame = self.packer.make_can_msg('TCS', bus, {'COUNTER': 7, 'DriverBraking': True})
|
||||
assert self.safety.safety_rx_hook(self.packet(frame))
|
||||
assert self.safety.aol_get_permission_mask() == 1
|
||||
@@ -1557,9 +1557,10 @@ class Car:
|
||||
from opendbc.car.hyundai.torque_ev_startup import TorqueEVStartup, required
|
||||
from opendbc.car.hyundai.ecu_startup import Outcome
|
||||
owner = getattr(getattr(self, 'vehicle_startup', None), 'owner', None)
|
||||
return (isinstance(owner, TorqueEVStartup) and owner.outcome in (Outcome.PENDING, Outcome.SENT_UNCONFIRMED) and
|
||||
from opendbc.car.hyundai.generic_hda2_startup import GenericHDA2Startup, carnival_long
|
||||
return (isinstance(owner, (TorqueEVStartup, GenericHDA2Startup)) and owner.outcome in (Outcome.PENDING, Outcome.SENT_UNCONFIRMED) and
|
||||
owner.ready and owner.published and owner.ci is self.CI and not owner.closed and
|
||||
required(self.CP))
|
||||
(required(self.CP) or (isinstance(owner, GenericHDA2Startup) and carnival_long(self.CP))))
|
||||
|
||||
def mixed_startup_keepalive(self):
|
||||
from opendbc.car.hyundai.blended_longitudinal import BlendedStartup
|
||||
@@ -1622,7 +1623,11 @@ class Car:
|
||||
if getattr(self.CI.CC, "bolt_cc_profile", False):
|
||||
self.CI.CC.bolt_cc_metric = self.is_metric
|
||||
|
||||
if (initialize_only and self.mixed_startup_keepalive() and self.vehicle_startup.owner.pending_target):
|
||||
from opendbc.car.hyundai.generic_hda2_startup import GenericHDA2Startup
|
||||
from opendbc.car.hyundai.ecu_startup import Outcome
|
||||
pending_carnival = (isinstance(self.vehicle_startup.owner, GenericHDA2Startup) and
|
||||
self.vehicle_startup.owner.outcome is Outcome.PENDING)
|
||||
if initialize_only and (pending_carnival or (self.mixed_startup_keepalive() and self.vehicle_startup.owner.pending_target)):
|
||||
# Fresh disabled controls alone are insufficient: finish the host's
|
||||
# selfdriveInitializing phase before suppressing factory ADAS output.
|
||||
return
|
||||
@@ -1639,7 +1644,7 @@ class Car:
|
||||
from opendbc.car.hyundai.ev6_startup import EV6Startup
|
||||
from opendbc.car.hyundai.ecu_startup import Outcome
|
||||
classic = self.vehicle_startup.owner
|
||||
if isinstance(classic, (G90Startup, TorqueEVStartup, EV6Startup)) and classic.outcome is Outcome.PENDING:
|
||||
if isinstance(classic, (G90Startup, TorqueEVStartup, EV6Startup, GenericHDA2Startup)) and classic.outcome is Outcome.PENDING:
|
||||
now = time.monotonic_ns()
|
||||
source = int(self.sm.logMonoTime['carControl'])
|
||||
receipt = int(self.sm.recv_time['carControl'] * 1e9)
|
||||
|
||||
@@ -505,7 +505,7 @@ int main() {
|
||||
constexpr uint16_t sportage_long_words[] = {0x0804U, 0x0806U, 0x080CU, 0x080EU, 0x0814U, 0x0816U,
|
||||
0x0894U, 0x0896U, 0x0824U, 0x0826U, 0x082CU, 0x082EU};
|
||||
for (uint32_t word = 0U; word <= 0xFFFFU; ++word) {
|
||||
bool expected = word == 0x080DU || word == 0x2815U || word == 0x2895U || word == 0x0815U || word == 0x0895U || word == 0x8815U || word == 0x8895U || word == 0x5491U || word == 0x5C91U || word == 0x5C95U || word == 0x5495U;
|
||||
bool expected = word == 0x28B4U || word == 0x080DU || word == 0x2815U || word == 0x2895U || word == 0x0815U || word == 0x0895U || word == 0x8815U || word == 0x8895U || word == 0x5491U || word == 0x5C91U || word == 0x5C95U || word == 0x5495U;
|
||||
for (uint16_t stock_word : stock_canfd_words) {
|
||||
expected = expected || word == stock_word;
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"protected_inventory_sha256": "3ec588956664c5943c0dab67dcd0817f33c89ba30d19ca6841a86a57c1edf6c8", "schema_version": 1}
|
||||
@@ -530,6 +530,7 @@ RELEASE_TARGETS = (
|
||||
"opendbc_repo/opendbc/safety/tests/test_subaru_ascent_angle.py",
|
||||
"opendbc_repo/opendbc/safety/tests/test_ford_three.py",
|
||||
"opendbc_repo/opendbc/safety/tests/test_hyundai_canfd_carnival.py",
|
||||
"opendbc_repo/opendbc/safety/tests/test_hyundai_carnival_long_aol.py",
|
||||
"opendbc_repo/opendbc/safety/tests/test_subaru_stock_aol.py",
|
||||
"opendbc_repo/opendbc/safety/tests/test_subaru_software_cruise.py",
|
||||
"opendbc_repo/opendbc/safety/tests/test_subaru_sng.py",
|
||||
|
||||
Reference in New Issue
Block a user