Restore Sportage AOL and show effective wheel controls

This commit is contained in:
firestar5683
2026-10-29 22:46:29 -05:00
parent c6ef6b138a
commit dc7ff90070
16 changed files with 388 additions and 34 deletions
@@ -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:
@@ -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
@@ -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);
@@ -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
@@ -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
@@ -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<uint16_t>(word)) == expected);
}
@@ -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
-1
View File
@@ -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,
)
+2 -2
View File
@@ -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
@@ -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)
@@ -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"):
@@ -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)
@@ -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
+45 -2
View File
@@ -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 ""))
View File
-1
View File
@@ -1 +0,0 @@
{"protected_inventory_sha256": "7a8c76528995c0eadbec1aa432c4c324fad71b1048b8b0e39182d47e6dc99a65", "schema_version": 1}