From dc7ff90070923bf48e12fe7fbadc7a1f3d5e96cc Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Thu, 29 Oct 2026 22:46:29 -0500 Subject: [PATCH] Restore Sportage AOL and show effective wheel controls --- .../opendbc/car/hyundai/canfd_stock_aol.py | 10 +- .../opendbc/car/hyundai/carcontroller.py | 6 +- .../opendbc/safety/modes/hyundai_canfd.h | 15 +-- .../opendbc/safety/modes/hyundai_canfd_aol.h | 9 +- .../tests/test_hyundai_sportage_long_aol.py | 113 ++++++++++++++++++ .../pandad/tests/test_aol_protocol.cc | 5 + .../aol/tests/test_sportage_alpha_aol.py | 102 ++++++++++++++++ openpilot/starpilot/car/hyundai/aol.py | 1 - openpilot/starpilot/car/hyundai/settings.py | 4 +- .../tests/test_offroad_configuration_pages.py | 10 +- .../ui/tests/test_feature_settings_lateral.py | 14 +-- .../test_hyundai_configuration_settings.py | 2 +- .../tests/test_wheel_effective_aol_roles.py | 83 +++++++++++++ openpilot/starpilot/ui/wheel_feature.py | 47 +++++++- prebuilt | 0 prebuilt.json | 1 - 16 files changed, 388 insertions(+), 34 deletions(-) create mode 100644 opendbc_repo/opendbc/safety/tests/test_hyundai_sportage_long_aol.py create mode 100644 openpilot/starpilot/aol/tests/test_sportage_alpha_aol.py create mode 100644 openpilot/starpilot/ui/tests/test_wheel_effective_aol_roles.py delete mode 100644 prebuilt delete mode 100644 prebuilt.json diff --git a/opendbc_repo/opendbc/car/hyundai/canfd_stock_aol.py b/opendbc_repo/opendbc/car/hyundai/canfd_stock_aol.py index f152fdfc60..b1e5a21fd3 100644 --- a/opendbc_repo/opendbc/car/hyundai/canfd_stock_aol.py +++ b/opendbc_repo/opendbc/car/hyundai/canfd_stock_aol.py @@ -95,9 +95,10 @@ def qualified(cp, *, marked_only=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, 0x2815, 0x2895)) +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((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} +INDEPENDENT_LONG_BUTTON_CARS = LONG_EV_CARS | {CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, CAR.KIA_SPORTAGE_5TH_GEN} def qualified_long(cp, *, marked_only=False): @@ -111,7 +112,10 @@ def qualified_long(cp, *, marked_only=False): if not eligible(cp): return False word = _base_word(cp, experiences=(0, 32)) - if word not in (0x11, 0x91): + if cp.carFingerprint == CAR.KIA_SPORTAGE_5TH_GEN: + if word is None or word | 0x804 not in SPORTAGE_LONG_AOL_WORDS: + return False + elif word not in (0x11, 0x91): return False raw = int(cp.safetyConfigs[config_index(cp)].safetyParam) if cp.carFingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN: diff --git a/opendbc_repo/opendbc/car/hyundai/carcontroller.py b/opendbc_repo/opendbc/car/hyundai/carcontroller.py index a65a5f79d3..eef359b070 100644 --- a/opendbc_repo/opendbc/car/hyundai/carcontroller.py +++ b/opendbc_repo/opendbc/car/hyundai/carcontroller.py @@ -27,7 +27,7 @@ from opendbc.car.hyundai.ioniq6_longitudinal import Ioniq6LongitudinalPolicy from opendbc.car.hyundai.canfd_dash_icons import CanFDDashIcons from opendbc.car.hyundai.classic_scc_aol import qualified as qualified_classic_stock_aol from opendbc.car.hyundai.classic_long_aol import independent_buttons_qualified as qualified_classic_long_buttons -from opendbc.car.hyundai.canfd_stock_aol import qualified as qualified_canfd_stock_aol +from opendbc.car.hyundai.canfd_stock_aol import qualified as qualified_canfd_stock_aol, qualified_long as qualified_canfd_long_aol from opendbc.car.hyundai.ev6_aol import qualified as qualified_ev6_aol from opendbc.car.hyundai.g90_longitudinal import G90LongitudinalPolicy from opendbc.car.hyundai.gv70_longitudinal import is_gv70, scc_request, suppress_stock_cancel @@ -157,7 +157,9 @@ class CarController(CarControllerBase): 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.ev6_aol_status = CP.carFingerprint == CAR.KIA_EV6 and (qualified_canfd_stock_aol(CP) or qualified_ev6_aol(CP)) + self.ev6_aol_status = ((CP.carFingerprint == CAR.KIA_EV6 and (qualified_canfd_stock_aol(CP) or qualified_ev6_aol(CP))) or + (CP.carFingerprint == CAR.KIA_SPORTAGE_5TH_GEN and + (qualified_canfd_stock_aol(CP) or qualified_canfd_long_aol(CP)))) self.canfd_dash_icons = CanFDDashIcons() if ioniq6_status or self.torque_ev_scc_enabled or self.ev6_aol_status else None self.classic_dash_icons = CanFDDashIcons() if classic_aol_dash_enabled(CP) else None self.classic_lfa_icon = None diff --git a/opendbc_repo/opendbc/safety/modes/hyundai_canfd.h b/opendbc_repo/opendbc/safety/modes/hyundai_canfd.h index 51aabe7b80..997952de2b 100644 --- a/opendbc_repo/opendbc/safety/modes/hyundai_canfd.h +++ b/opendbc_repo/opendbc/safety/modes/hyundai_canfd.h @@ -330,7 +330,7 @@ static uint8_t aol_ioniq6_request_mask(void) { } else if (heartbeat_engaged) { aol_ioniq6_session_started = true; if ((hyundai_canfd_ioniq6_long || hyundai_ev9_long_aol || - (hyundai_canfd_torque_ev_long && !aol_optional_release_policy)) && controls_allowed && aol_ioniq6_enable_release && + (aol_optional_release_capable && !aol_optional_release_policy)) && controls_allowed && aol_ioniq6_enable_release && ((aol_host_axis_mask & 0x3U) == 0x3U) && (safety_get_ts_elapsed(now, aol_ioniq6_enable_release_ts) <= AOL_HOST_REQUEST_TIMEOUT_US)) { aol_ioniq6_lateral_latch = true; @@ -583,21 +583,22 @@ static void hyundai_canfd_rx_hook(const CANPacket_t *msg) { cruise_button = (msg->data[4] >> 4) & 0x7U; main_button = GET_BIT(msg, 34U); } + const bool lkas_button = hyundai_canfd_alt_buttons ? GET_BIT(msg, 39U) : GET_BIT(msg, 23U); const bool previous_controls_allowed = controls_allowed; const int previous_cruise_button = cruise_button_prev; const int main_policy = hyundai_canfd_ccnc_ev_long ? (hyundai_canfd_pe_long ? HYUNDAI_MAIN_WITHDRAW : HYUNDAI_MAIN_TRACK) : HYUNDAI_MAIN_NONE; const bool normal_main_button = hyundai_canfd_ccnc_ev_long && GET_BIT(msg, 21U); hyundai_common_cruise_buttons_check(cruise_button, main_button, normal_main_button, main_policy); if ((hyundai_canfd_ioniq6_long || hyundai_ev9_long_aol || - (hyundai_canfd_torque_ev_long && !aol_optional_release_policy)) && aol_ioniq6_long) { + (aol_optional_release_capable && !aol_optional_release_policy)) && aol_ioniq6_long) { static uint8_t aol_ioniq6_enable_counter = 0U; static uint32_t aol_ioniq6_enable_ts = 0U; const uint8_t counter = hyundai_canfd_get_counter(msg); const uint32_t now = microsecond_timer_get(); const uint32_t elapsed = safety_get_ts_elapsed(now, aol_ioniq6_enable_ts); const bool fresh = aol_ioniq6_enable_counter_seen && - hyundai_canfd_button_counter_follows(aol_ioniq6_enable_counter, counter, 0xFU) && (elapsed > 0U) && (elapsed <= 100000U); - if (!fresh || main_button || GET_BIT(msg, 23U)) { + hyundai_canfd_button_counter_follows(aol_ioniq6_enable_counter, counter, hyundai_canfd_alt_buttons ? 0xFFU : 0xFU) && (elapsed > 0U) && (elapsed <= 100000U); + if (!fresh || main_button || lkas_button) { aol_ioniq6_enable_pending = false; aol_ioniq6_enable_release = false; } else if (aol_ioniq6_enable_neutral && (previous_cruise_button == HYUNDAI_BTN_NONE) && @@ -616,7 +617,7 @@ static void hyundai_canfd_rx_hook(const CANPacket_t *msg) { } else { // A held enable button does not renew the heartbeat grace. } - aol_ioniq6_enable_neutral = !main_button && !GET_BIT(msg, 23U) && (cruise_button == HYUNDAI_BTN_NONE); + aol_ioniq6_enable_neutral = !main_button && !lkas_button && (cruise_button == HYUNDAI_BTN_NONE); aol_ioniq6_enable_counter_seen = true; aol_ioniq6_enable_counter = counter; aol_ioniq6_enable_ts = now; @@ -657,7 +658,7 @@ static void hyundai_canfd_rx_hook(const CANPacket_t *msg) { } aol_optional_release_previous = (uint8_t)cruise_button; } - const bool gesture = main_button || (hyundai_canfd_alt_buttons ? GET_BIT(msg, 39U) : GET_BIT(msg, 23U)); // LDA/LKAS button + const bool gesture = main_button || lkas_button; if (cruise_button == HYUNDAI_BTN_CANCEL) { aol_ioniq6_enable_reset(); aol_optional_release_pending = false; @@ -1495,7 +1496,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 || gv60_long_requested; + hyundai_canfd_torque_ev_long = (param == 0x0015U) || (param == 0x0095U) || (torque_long_aol_requested && GET_FLAG(param, 16U)) || gv60_long_requested; #endif hyundai_torque_aol_cancel_preserves_lateral = ioniq6_stock_aol_requested || torque_long_aol_requested || (param == 0x8815U) || (param == 0x8895U); diff --git a/opendbc_repo/opendbc/safety/modes/hyundai_canfd_aol.h b/opendbc_repo/opendbc/safety/modes/hyundai_canfd_aol.h index 3802a5111a..83626206f4 100644 --- a/opendbc_repo/opendbc/safety/modes/hyundai_canfd_aol.h +++ b/opendbc_repo/opendbc/safety/modes/hyundai_canfd_aol.h @@ -18,6 +18,13 @@ static inline bool hyundai_canfd_stock_torque_aol_param(uint16_t param) { #if defined(ALLOW_DEBUG) || defined(__cplusplus) static inline bool hyundai_canfd_torque_long_aol_param(uint16_t param) { - return (param == 0x0815U) || (param == 0x0895U) || (param == 0x2815U) || (param == 0x2895U); + const uint16_t base = param & (uint16_t)~0x0800U; + const uint16_t fuel = base & 3U; + const uint16_t topology = base & 0x00B8U; + const bool sportage = ((param & 0x0800U) != 0U) && ((base & (uint16_t)~0x00BEU) == 0U) && + ((base & 4U) != 0U) && ((fuel == 0U) || (fuel == 2U)) && + ((topology == 0U) || (topology == 8U) || (topology == 16U) || + (topology == 144U) || (topology == 32U) || (topology == 40U)); + return sportage || (param == 0x0815U) || (param == 0x0895U) || (param == 0x2815U) || (param == 0x2895U); } #endif diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_sportage_long_aol.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_sportage_long_aol.py new file mode 100644 index 0000000000..54b13f76d6 --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_sportage_long_aol.py @@ -0,0 +1,113 @@ +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 SPORTAGE_LONG_AOL_WORDS +from opendbc.safety.tests.libsafety import libsafety_py +from opendbc.safety.tests import test_hyundai_shared_canfd_stock_aol as fixtures + + +class TestSportageLongAol(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_SPORTAGE_5TH_GEN, 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 diff --git a/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc b/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc index ac75feed08..5be4e071de 100644 --- a/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc +++ b/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc @@ -512,6 +512,8 @@ int main() { 0x6A28U, 0x6A2AU, 0x7008U, 0x700AU, 0x7010U, 0x7012U, 0x7028U, 0x702AU, 0x7090U, 0x7092U, 0x7208U, 0x720AU, 0x7228U, 0x722AU, 0x7809U, 0x7829U, }; + 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 == 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) { @@ -520,6 +522,9 @@ int main() { for (uint16_t stock_word : stock_angle_words) { expected = expected || word == stock_word; } + for (uint16_t sportage_word : sportage_long_words) { + expected = expected || word == sportage_word; + } assert(hyundai_aol_param(static_cast(word)) == expected); } diff --git a/openpilot/starpilot/aol/tests/test_sportage_alpha_aol.py b/openpilot/starpilot/aol/tests/test_sportage_alpha_aol.py new file mode 100644 index 0000000000..fbdedf9893 --- /dev/null +++ b/openpilot/starpilot/aol/tests/test_sportage_alpha_aol.py @@ -0,0 +1,102 @@ +from types import SimpleNamespace + +import pytest + +from opendbc.car import gen_empty_fingerprint, structs +from opendbc.car.hyundai.canfd_stock_aol import qualified_long +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.values import CAR +from openpilot.starpilot.aol.intent import AolSettings +from openpilot.starpilot.car.hyundai.aol import create_intent, native_accepts_cp, policy_for +from openpilot.starpilot.ui.wheel_feature import WheelFeature + + +def sportage(*, hybrid=False, alternate=False, lka=0, alpha=True): + fp = gen_empty_fingerprint() + bus = 1 if lka else 0 + if lka: + fp[2][0x110 if lka == 2 else 0x50] = 32 if lka == 2 else 16 + fp[bus].update({0x1aa if alternate else 0x1cf: 16 if alternate else 8, 0x130: 32, 0x35: 32}) + if hybrid: + fp[bus][0xFA] = 8 + fw = [structs.CarParams.CarFw(ecu='adas')] if lka else [] + return CarInterface.get_params(CAR.KIA_SPORTAGE_5TH_GEN, fp, fw, alpha, False, False) + + +@pytest.mark.parametrize('hybrid', [False, True]) +@pytest.mark.parametrize('layout', [(0, False), (0, True), (1, False), (2, False)]) +@pytest.mark.parametrize('alpha', [False, True]) +def test_sportage_factory_wheel_choices_and_physical_latch(hybrid, layout, alpha): + lka, alternate = layout + cp = sportage(hybrid=hybrid, alternate=alternate, lka=lka, alpha=alpha) + assert cp.openpilotLongitudinalControl == alpha + policy = policy_for(cp) + assert policy.settings_supported and policy.runtime_supported + owner = SimpleNamespace(vehicle_params=lambda: cp, configuration_vehicle=lambda: False, + configuration_longitudinal=lambda: False, longitudinal_available=lambda: alpha) + for key in ('LKASButtonControl', 'MainCruiseButtonControl'): + assert 'Toggle AOL' in WheelFeature(owner).choices(key) + cp.safetyConfigs[-1].safetyParam |= policy.safety_param_addition + cp.alternativeExperience |= policy.alternative_experience_addition + assert native_accepts_cp(cp, int(cp.safetyConfigs[-1].safetyModel.raw), cp.safetyConfigs[-1].safetyParam) + if alpha: + assert qualified_long(cp, marked_only=True) + for button in ('lkas', 'mainCruise'): + intent = create_intent(cp, AolSettings(True, 0., 9, 9, (0, 0, 0), (0, 0, 0))) + cs = structs.CarState(canValid=True, gearShifter='drive') + cs.cruiseState.available = True + intent.update(cs, now_ns=1, fault_active=False) + cs.buttonEvents = [structs.CarState.ButtonEvent(type=button, pressed=True)] + intent.update(cs, now_ns=2, fault_active=False) + assert intent.allowed_latch + cs.buttonEvents = [structs.CarState.ButtonEvent(type='cancel', pressed=True)] + intent.update(cs, now_ns=3, fault_active=False) + assert intent.allowed_latch + cs.buttonEvents = [] + cs.brakePressed = True + intent.update(cs, now_ns=4, fault_active=False) + assert intent.allowed_latch + cs.canValid = False + intent.update(cs, now_ns=5, fault_active=False) + assert not intent.output(cs)[0] + + +def test_sportage_alpha_rejects_wrong_identity_word_and_experience(): + cp = sportage() + assert qualified_long(cp) + for field, value in (('carFingerprint', CAR.KIA_SELTOS), ('passive', True), ('dashcamOnly', True), + ('notCar', True), ('pcmCruise', True), ('alternativeExperience', 32)): + bad = cp.as_reader().as_builder() + setattr(bad, field, value) + assert not qualified_long(bad) + for word in (0x80d, 0x80f, 0x830, 0x180c, 0x480c): + bad = cp.as_reader().as_builder() + bad.safetyConfigs[-1].safetyParam = word + bad.alternativeExperience = 32 + assert not qualified_long(bad) + + +@pytest.mark.parametrize('hybrid', [False, True]) +@pytest.mark.parametrize('alternate', [False, True]) +@pytest.mark.parametrize('alpha', [False, True]) +def test_sportage_lateral_only_uses_active_status_without_changing_normal_commands(hybrid, alternate, alpha): + from opendbc.can import CANParser + from opendbc.car import Bus + from opendbc.car.hyundai.values import DBC + + cp = sportage(hybrid=hybrid, alternate=alternate, alpha=alpha) + policy = policy_for(cp) + cp.safetyConfigs[-1].safetyParam |= policy.safety_param_addition + cp.alternativeExperience |= policy.alternative_experience_addition + ci = CarInterface(cp) + ci.update([]) + parser = CANParser(DBC[CAR.KIA_SPORTAGE_5TH_GEN][Bus.pt], [('LFA', 0)], 0) + before = cp.to_dict() + for enabled, lateral, expected in ((False, False, 1), (False, True, 2), (True, True, 2), (True, False, 2)): + cc = structs.CarControl(enabled=enabled, latActive=lateral) + stamp = 10_000_000_000 + ci.CC.frame * 10_000_000 + _, frames = ci.CC.update(cc.as_reader(), ci.CS, stamp) + parser.update((stamp, frames)) + assert parser.vl['LFA']['LKA_SysIndReq'] == expected + assert parser.vl['LFA']['Damping_Gain'] == 100 + assert cp.to_dict() == before diff --git a/openpilot/starpilot/car/hyundai/aol.py b/openpilot/starpilot/car/hyundai/aol.py index 8170c3d681..f88995e5a3 100644 --- a/openpilot/starpilot/car/hyundai/aol.py +++ b/openpilot/starpilot/car/hyundai/aol.py @@ -183,7 +183,6 @@ def _policy_for(CP) -> AolVehiclePolicy: explicit_latch=qualified, distance_personality=distance_personality, safety_param_addition=0x0800 if stock else 0, - fixed_cruise_buttons=settings, distance_pause_only=settings, paddle_pause=settings, ) diff --git a/openpilot/starpilot/car/hyundai/settings.py b/openpilot/starpilot/car/hyundai/settings.py index 221e4f1ce1..b2158a9b14 100644 --- a/openpilot/starpilot/car/hyundai/settings.py +++ b/openpilot/starpilot/car/hyundai/settings.py @@ -6,12 +6,12 @@ from opendbc.car.hyundai.values import CAR @dataclass(frozen=True) class WheelSettingsPolicy: settings_supported: bool = True - fixed_cruise_buttons: bool = True + fixed_cruise_buttons: bool = False explicit_latch: bool = True distance_pause_only: bool = True paddle_pause: bool = True runtime_supported: bool = False - lkas_button_supported: bool = False + lkas_button_supported: bool = True auxiliary_lkas_supported: bool = False diff --git a/openpilot/starpilot/galaxy/tests/test_offroad_configuration_pages.py b/openpilot/starpilot/galaxy/tests/test_offroad_configuration_pages.py index 87f7004707..f84f4cbd2e 100644 --- a/openpilot/starpilot/galaxy/tests/test_offroad_configuration_pages.py +++ b/openpilot/starpilot/galaxy/tests/test_offroad_configuration_pages.py @@ -114,18 +114,18 @@ class TestOffroadConfigurationPages(unittest.TestCase): self.assertTrue(rows[label]['choices'] or rows[label]['step'], (name, label)) self.assert_no_car_cache() - def test_fixed_ioniq6_cruise_buttons_remain_outside_remapping_catalog(self): + def test_ioniq6_cruise_buttons_are_in_remapping_catalog(self): from openpilot.starpilot.car.hyundai.settings import configuration_wheel_policy cp = self.context.sample().cp policy = configuration_wheel_policy(cp) assert policy is not None - self.assertTrue(policy.fixed_cruise_buttons) - self.assertFalse(policy.lkas_button_supported) + self.assertFalse(policy.fixed_cruise_buttons) + self.assertTrue(policy.lkas_button_supported) self.params.put('LKASButtonControl', 9, block=True) self.params.put('MainCruiseButtonControl', 10, block=True) labels = {row['label'] for row in self.page('wheel')['rows']} - self.assertNotIn('LKAS press', labels) - self.assertNotIn('Main cruise press', labels) + self.assertIn('LKAS press', labels) + self.assertIn('Main cruise press', labels) self.assertIn('Distance long press', labels) self.assertEqual(self.params.get('LKASButtonControl'), 9) self.assertEqual(self.params.get('MainCruiseButtonControl'), 10) diff --git a/openpilot/starpilot/ui/tests/test_feature_settings_lateral.py b/openpilot/starpilot/ui/tests/test_feature_settings_lateral.py index be256fdb93..a9a44ad68a 100644 --- a/openpilot/starpilot/ui/tests/test_feature_settings_lateral.py +++ b/openpilot/starpilot/ui/tests/test_feature_settings_lateral.py @@ -300,8 +300,7 @@ class LateralFeatureSettingsTests(unittest.TestCase): master = self.row("aol", "AlwaysOnLateral") self.assertTrue(master.available) self.assertIn("independently of cruise control", master.reason) - self.assertFalse(any(row.key == "wheel:LKASButtonControl" for row in self.owner.snapshot( - "wheel", parked=True, system_long=True, lateral_context=True, metric=False).rows)) + self.assertIn("Toggle AOL", self.row("wheel", "wheel:LKASButtonControl").choices) nostalgia = self.row("vehicle", "NostalgiaMode") self.assertTrue(nostalgia.available) self.assertTrue(self.owner.apply(required_change(nostalgia))) @@ -316,8 +315,7 @@ class LateralFeatureSettingsTests(unittest.TestCase): tagged.safetyConfigs[0].safetyParam |= 0x800 self.cp = tagged self.assertTrue(self.row("aol", "AlwaysOnLateral").available) - self.assertFalse(any(row.key == "wheel:LKASButtonControl" for row in self.owner.snapshot( - "wheel", parked=True, system_long=True, lateral_context=True, metric=False).rows)) + self.assertIn("Toggle AOL", self.row("wheel", "wheel:LKASButtonControl").choices) def test_ioniq5_left_paddle_setting_uses_existing_long_configuration(self): from openpilot.starpilot.tests.test_nostalgia import _ioniq5_params @@ -429,10 +427,8 @@ class LateralFeatureSettingsTests(unittest.TestCase): stock, long_cp = ioniq6_candidate(False) self.cp = stock self.params.put_bool("AlwaysOnLateral", True, block=True) - self.assertFalse(any(row.key == "wheel:LKASButtonControl" for row in self.owner.snapshot( - "wheel", parked=True, system_long=True, lateral_context=True, metric=False).rows)) - self.assertFalse(any(row.key == "wheel:MainCruiseButtonControl" for row in self.owner.snapshot( - "wheel", parked=True, system_long=True, lateral_context=True, metric=False).rows)) + self.assertIn("Toggle AOL", self.row("wheel", "wheel:LKASButtonControl").choices) + self.assertIn("Toggle AOL", self.row("wheel", "wheel:MainCruiseButtonControl").choices) cases = (("DistanceButtonControl", "Pause steering", 1, (False, True), True), ("LongDistanceButtonControl", "Pause longitudinal", CRUISE_LONG_PRESS, (True, False), True), ("VeryLongDistanceButtonControl", "Pause steering", CRUISE_LONG_PRESS * 5, (False, True), True), @@ -491,7 +487,7 @@ class LateralFeatureSettingsTests(unittest.TestCase): def test_ioniq_distance_canonical_repair_and_fixed_gestures(self): self.cp, _ = ioniq6_candidate(False) state = self.owner.snapshot("wheel", parked=True, system_long=True, lateral_context=True, metric=False) - self.assertFalse(any(row.key in ("wheel:LKASButtonControl", "wheel:MainCruiseButtonControl") for row in state.rows)) + self.assertTrue({"wheel:LKASButtonControl", "wheel:MainCruiseButtonControl"} <= {row.key for row in state.rows}) key = "DistanceButtonControl" path = Path(self.params.get_param_path(key)) for raw in (b"03", b"99", b"bad"): diff --git a/openpilot/starpilot/ui/tests/test_hyundai_configuration_settings.py b/openpilot/starpilot/ui/tests/test_hyundai_configuration_settings.py index 4c3e64d83c..233e7dff88 100644 --- a/openpilot/starpilot/ui/tests/test_hyundai_configuration_settings.py +++ b/openpilot/starpilot/ui/tests/test_hyundai_configuration_settings.py @@ -17,7 +17,7 @@ class HyundaiConfigurationSettingsTests(unittest.TestCase): self.assertTrue(configuration_media_supported(cp)) policy = configuration_wheel_policy(cp) assert policy is not None - self.assertTrue(policy.fixed_cruise_buttons) + self.assertFalse(policy.fixed_cruise_buttons) self.assertTrue(policy.distance_pause_only) self.assertFalse(policy.runtime_supported) self.assertEqual(vars(cp), before) diff --git a/openpilot/starpilot/ui/tests/test_wheel_effective_aol_roles.py b/openpilot/starpilot/ui/tests/test_wheel_effective_aol_roles.py new file mode 100644 index 0000000000..14de75573d --- /dev/null +++ b/openpilot/starpilot/ui/tests/test_wheel_effective_aol_roles.py @@ -0,0 +1,83 @@ +from types import SimpleNamespace + +import pytest + +from opendbc.car import structs +from opendbc.car.car_helpers import interfaces +from opendbc.car.ford.values import CAR as FORD +from openpilot.common.params import Params +from openpilot.selfdrive.car.tests.test_hyundai_aol import candidate +from openpilot.starpilot.aol.intent import AolSettings +from openpilot.starpilot.aol.tests.test_sportage_alpha_aol import sportage +from openpilot.starpilot.car.ford.aol import create_intent as ford_intent +from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner +from openpilot.starpilot.ui.wheel_feature import WheelFeature + + +def owner(params, cp, *, configuration=False): + return SimpleNamespace(params=params, vehicle_params=lambda: cp, configuration_vehicle=lambda: configuration, + configuration_longitudinal=lambda: False, longitudinal_available=lambda: cp.openpilotLongitudinalControl, + authority=lambda _: True) + + +@pytest.mark.parametrize('alpha', [False, True]) +@pytest.mark.parametrize('configuration', [False, True]) +def test_ioniq6_lkas_and_main_are_visible_and_configurable_without_changing_saved_settings(tmp_path, alpha, configuration): + params = Params(str(tmp_path)) + stock, long_cp = candidate(False) + cp = long_cp if alpha else stock + wheel = WheelFeature(owner(params, cp, configuration=configuration)) + for key in ('LKASButtonControl', 'MainCruiseButtonControl'): + assert 'Toggle AOL' in wheel.choices(key) + assert params.get(key) is None + assert any(row.key == 'wheel:' + key for row in wheel.rows()) + + +@pytest.mark.parametrize('alpha', [False, True]) +def test_hkg_default_toggle_and_explicit_mapping_are_displayed_truthfully(tmp_path, alpha): + params = Params(str(tmp_path)) + cp = sportage(alpha=alpha) + wheel = WheelFeature(owner(params, cp)) + rows = {row.key: row for row in wheel.rows()} + for key in ('LKASButtonControl', 'MainCruiseButtonControl'): + assert rows['wheel:' + key].value == 'Toggle AOL' + assert params.get(key) is None + params.put('LKASButtonControl', 9, block=True) + params.put('MainCruiseButtonControl', 0, block=True) + rows = {row.key: row for row in wheel.rows()} + assert rows['wheel:LKASButtonControl'].value == 'Toggle AOL' + assert rows['wheel:MainCruiseButtonControl'].value == 'Off' + assert params.get('LKASButtonControl') == 9 + assert params.get('MainCruiseButtonControl') == 0 + + +def test_mach_e_fixed_lkas_role_matches_actual_intent_and_cannot_claim_off(tmp_path): + params = Params(str(tmp_path)) + cp = interfaces[FORD.FORD_MUSTANG_MACH_E_MK1].get_non_essential_params(FORD.FORD_MUSTANG_MACH_E_MK1) + wheel = WheelFeature(owner(params, cp)) + rows = {row.key: row for row in wheel.rows()} + lkas = rows['wheel:LKASButtonControl'] + assert lkas.value == 'Toggle AOL' and 'Toggle AOL' in lkas.choices + assert lkas.available and 'also toggles AOL' in lkas.reason + main = rows['wheel:MainCruiseButtonControl'] + assert main.value == 'Factory cruise / AOL' + assert not main.available and not main.choices + for key in ('LKASButtonControl', 'MainCruiseButtonControl'): + assert params.get(key) is None + intent = ford_intent(cp, AolSettings(True, 0., 0, 0, (0, 0, 0), (0, 0, 0))) + state = structs.CarState(canValid=True, gearShifter='drive') + state.cruiseState.available = True + intent.update(state, now_ns=1, fault_active=False) + state.buttonEvents = [structs.CarState.ButtonEvent(type='lkas', pressed=True)] + intent.update(state, now_ns=2, fault_active=False) + assert intent.allowed_latch + + +def test_ioniq6_actual_settings_owner_exposes_both_rows(tmp_path): + params = Params(str(tmp_path)) + cp, _ = candidate(False) + settings = FeatureSettingsOwner(params, lambda _: True, vehicle_fingerprint=lambda: str(cp.carFingerprint), vehicle_params=lambda: cp) + page = settings.snapshot('wheel', parked=True, system_long=True, lateral_context=True, metric=False) + rows = {row.key: row for row in page.rows} + for key in ('LKASButtonControl', 'MainCruiseButtonControl'): + assert 'Toggle AOL' in rows['wheel:' + key].choices diff --git a/openpilot/starpilot/ui/wheel_feature.py b/openpilot/starpilot/ui/wheel_feature.py index 7261ccfb40..98dee9e47a 100644 --- a/openpilot/starpilot/ui/wheel_feature.py +++ b/openpilot/starpilot/ui/wheel_feature.py @@ -29,13 +29,35 @@ class WheelFeature: return base + (self.owner.longitudinal_available(), "configuration") return base + (True,) if self.owner.configuration_longitudinal() else base + def _policy(self, cp): + policy = policy_for(cp) + if self.owner.configuration_vehicle(): + from openpilot.starpilot.car.hyundai.settings import configuration_wheel_policy + policy = configuration_wheel_policy(cp) or policy + return policy + + def fixed_role(self, cp, key, policy): + if key not in KEYS[:2] or not policy.settings_supported: + return None + if key == KEYS[0] and (not button_reported(cp, key) or + not (policy.lkas_button_supported or policy.auxiliary_lkas_supported)): + return None + if policy.fixed_cruise_buttons and not (key == KEYS[0] and policy.auxiliary_lkas_supported): + return "Toggle AOL" if cp.brand == 'hyundai' else "Factory cruise / AOL" + if cp.brand == 'ford' and policy.explicit_latch and policy.runtime_supported and key == KEYS[1]: + sources = [read_saved(self.owner.params, name, 8) for name in KEYS[:2]] + return "Factory cruise / AOL" if all(ok and value != b'9' for value, ok in sources) else "Factory cruise" + return None + def choices(self, key): cp = self.owner.vehicle_params() if self.capability() is None: return () if key == PADDLE_KEY: return ("Off", "Bookmark") if paddle_bookmark_available(self.owner.params, cp) else () - policy = policy_for(cp) + policy = self._policy(cp) + if self.fixed_role(cp, key, policy) is not None: + return () media = ioniq6_media_eligible(cp) if self.owner.configuration_vehicle(): from openpilot.starpilot.car.hyundai.settings import configuration_wheel_policy, configuration_media_supported @@ -76,18 +98,39 @@ class WheelFeature: if capability is None: return [] rows = [] + cp = self.owner.vehicle_params() + policy = self._policy(cp) for key, label in zip(KEYS, LABELS, strict=True): + fixed = self.fixed_role(cp, key, policy) + if fixed is not None: + raw, _ = read_saved(self.owner.params, key, 8) + rows.append(FeatureRow(PREFIX + key, label, fixed, raw, (), available=False, + reason="Vehicle-controlled AOL role; this button cannot be reassigned. Saved preferences are unchanged.", + capability=capability)) + continue choices = self.choices(key) if not choices: continue raw, readable = read_saved(self.owner.params, key, 8) number = DEFAULTS.get(key, 0) if raw is None else int(raw) if raw in tuple(str(i).encode() for i in range(15)) else -1 selected = next((name for name, code in ACTIONS.items() if code == number), None) + implicit_toggle = False + if cp.brand == 'hyundai' and policy.explicit_latch and key in KEYS[:2]: + from openpilot.starpilot.car.hyundai.aol import independent_cruise_buttons_supported + sources = [read_saved(self.owner.params, name, 8) for name in KEYS[:2]] + ordinary_actions = (None, *(str(code).encode() for code in ACTIONS.values() if code != 9)) + implicit_toggle = independent_cruise_buttons_supported(cp) and all(ok and value in ordinary_actions + for value, ok in sources) + ford_role = cp.brand == 'ford' and policy.explicit_latch and policy.runtime_supported and key == KEYS[0] + if implicit_toggle or ford_role and selected == "Off": + selected = "Toggle AOL" default = next(label for label, code in ACTIONS.items() if code == DEFAULTS.get(key, 0)) rows.append(FeatureRow(PREFIX + key, label, selected or "Invalid saved action", raw, choices, default_value=default if default in choices else None, available=readable and self.owner.authority("preferences"), - reason=("Bookmark on release. Available while Left Paddle Longitudinal Pause is Off." if key == PADDLE_KEY else + reason=("LKAS also toggles AOL independently of its saved extra action." if ford_role else + "Neither LKAS nor MAIN is assigned Toggle AOL, so both retain the default AOL toggle." if implicit_toggle else + "Bookmark on release. Available while Left Paddle Longitudinal Pause is Off." if key == PADDLE_KEY else "Axis pause and AOL assignments apply after restarting. Other actions can apply this drive. " + "Long actions fire while held."), capability=capability, repair_value="Off" if selected is None else "")) diff --git a/prebuilt b/prebuilt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/prebuilt.json b/prebuilt.json deleted file mode 100644 index 2c1952d15c..0000000000 --- a/prebuilt.json +++ /dev/null @@ -1 +0,0 @@ -{"protected_inventory_sha256": "7a8c76528995c0eadbec1aa432c4c324fad71b1048b8b0e39182d47e6dc99a65", "schema_version": 1}