Files
2026-10-22 12:51:07 -05:00

185 lines
8.3 KiB
Python

"""Physical Ioniq paddle edges use the existing authenticated wheel Bookmark path."""
from collections import deque
from pathlib import Path
from types import SimpleNamespace as NS
import pytest
from opendbc.car.hyundai.values import CAR, HyundaiFlags
from openpilot.cereal import messaging
from openpilot.common.params import Params
from openpilot.starpilot.controllers.tests.test_wheel_actions import DRIVE, NOW, Publisher, candidate, services, state, TYPE
from openpilot.starpilot.controllers.tests.test_native_wheel_callbacks import native_session # noqa: F401
from openpilot.starpilot.controllers.wheel_actions import WheelConsumer, WheelPublisher
from openpilot.starpilot.tests.test_nostalgia import _ioniq5_params
from openpilot.starpilot.ui.feature_settings_state import FeatureSettingsRequest
from openpilot.starpilot.ui.wheel_feature import WheelFeature
KEY = 'LeftPaddleButtonControl'
def save(params, value=b'8'):
# Also permits the unchanged baseline to reproduce a behavioral failure.
Path(params.get_param_path(KEY)).write_bytes(value)
def tick(owner, params, cp, pressed=None, frame=0):
cs = state() if pressed is None else state(pressed, button=TYPE.altButton2)
before = cs.to_bytes()
commands = owner.observe(params, cp, cs, now_ns=NOW + frame * 10_000_000, drive_id=DRIVE)
assert cs.to_bytes() == before # The physical event still reaches longitudinal/AOL owners.
return commands
@pytest.mark.parametrize('fingerprint', (CAR.HYUNDAI_IONIQ_5, CAR.HYUNDAI_IONIQ_5_N,
CAR.HYUNDAI_IONIQ_5_PE, CAR.HYUNDAI_IONIQ_6))
@pytest.mark.parametrize('longitudinal', (False, True))
def test_supported_paddle_bookmarks_on_release_once(tmp_path, fingerprint, longitudinal):
params = Params(str(tmp_path))
cp = _ioniq5_params(long=longitudinal) if fingerprint != CAR.HYUNDAI_IONIQ_6 else candidate(False)[1]
cp.carFingerprint = fingerprint
cp.openpilotLongitudinalControl = longitudinal
save(params)
owner = WheelPublisher(params)
tick(owner, params, cp)
assert not any(action for _, action in tick(owner, params, cp, True, 1))
assert not any(action for _, action in tick(owner, params, cp, True, 2))
assert tick(owner, params, cp, False, 3) == ((KEY, 8),)
assert not owner.suppress_distance_release
assert not any(action for _, action in tick(owner, params, cp, False, 4))
@pytest.mark.parametrize('initial', (None, b'0', b'1', b'8garbage'))
def test_default_off_and_invalid_assignment_have_no_action(tmp_path, initial):
params = Params(str(tmp_path))
if initial is not None:
save(params, initial)
cp = _ioniq5_params()
owner = WheelPublisher(params)
tick(owner, params, cp, True, 1)
assert not any(action for _, action in tick(owner, params, cp, False, 2))
@pytest.mark.parametrize('reason', ('pause', 'invalid_pause', 'alternate', 'other_vehicle', 'invalid_can', 'new_drive', 'mapping'))
def test_pause_and_source_changes_revoke_held_paddle(tmp_path, reason):
params = Params(str(tmp_path))
cp = _ioniq5_params()
save(params)
owner = WheelPublisher(params)
tick(owner, params, cp, True, 1)
if reason in ('pause', 'invalid_pause'):
Path(params.get_param_path('NostalgiaMode')).write_bytes(b'1' if reason == 'pause' else b'bad')
tick(owner, params, cp, frame=2)
params.put_bool('NostalgiaMode', False, block=True)
elif reason == 'alternate':
cp.flags = int(cp.flags | HyundaiFlags.CANFD_ALT_BUTTONS)
elif reason == 'other_vehicle':
cp.carFingerprint = CAR.KIA_EV6
elif reason == 'invalid_can':
invalid = state()
invalid.canValid = False
owner.observe(params, cp, invalid, now_ns=NOW + 20_000_000, drive_id=DRIVE)
elif reason == 'new_drive':
owner.observe(params, cp, state(), now_ns=NOW + 20_000_000, drive_id=DRIVE + 1)
else:
save(params, b'0')
tick(owner, params, cp, frame=2)
save(params)
assert not any(action for _, action in tick(owner, params, cp, False, 3))
def test_pause_on_never_emits_and_consumer_rechecks_priority(tmp_path):
params = Params(str(tmp_path))
cp = _ioniq5_params()
save(params)
owner = WheelPublisher(params)
sender = Publisher()
owner.publish(((KEY, 8),), cp, sender, now_ns=NOW, drive_id=DRIVE, source_car_ns=NOW, source_control_ns=NOW)
event = messaging.log_from_bytes(sender.events[0][1])
assert WheelConsumer().accept(event, params, cp, services(cp), now_ns=NOW).action == 8
params.put_bool('NostalgiaMode', True, block=True)
assert WheelConsumer().accept(event, params, cp, services(cp), now_ns=NOW) is None
tick(owner, params, cp, True, 1)
assert not any(action for _, action in tick(owner, params, cp, False, 2))
def test_actual_can_parser_paddle_to_authenticated_bookmark(tmp_path):
from opendbc.can import CANPacker
from opendbc.car import Bus
from opendbc.car.hyundai.hyundaicanfd import CanBus
from opendbc.car.hyundai.tests.test_ioniq6_stock_parser import StockParserTests, params as i6_params
from opendbc.car.hyundai.values import DBC
params = Params(str(tmp_path))
save(params)
for cp in (_ioniq5_params(), i6_params(False)[0]):
case = StockParserTests(methodName='runTest')
ci = case.host(cp)
cs = case.feed(ci, count=5)
owner = WheelPublisher(params)
owner.observe(params, cp, cs, now_ns=NOW, drive_id=DRIVE)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
for index, pressed in enumerate((True, False), 6):
ci.test_time_ns += 10_000_000
frame = packer.make_can_msg('CRUISE_BUTTONS', CanBus(cp).ECAN,
{'COUNTER': index, 'LEFT_PADDLE': int(pressed)})
cs = ci.update([(ci.test_time_ns, [frame])])
assert cs.canValid
before = [(e.type, e.pressed) for e in cs.buttonEvents]
commands = owner.observe(params, cp, cs, now_ns=NOW + index * 10_000_000, drive_id=DRIVE)
assert [(e.type, e.pressed) for e in cs.buttonEvents] == before
assert tuple((key, action) for key, action in commands if action) == (() if pressed else ((KEY, 8),))
def test_ui_exposes_off_bookmark_only_when_pause_off_and_rejects_stale_request(tmp_path):
params = Params(str(tmp_path))
cp = _ioniq5_params()
owner = NS(params=params, vehicle_params=lambda: cp, configuration_vehicle=lambda: False,
configuration_longitudinal=lambda: False, longitudinal_available=lambda: True, authority=lambda _: True)
feature = WheelFeature(owner)
row = next(row for row in feature.rows() if row.key == 'wheel:' + KEY)
assert row.choices == ('Off', 'Bookmark') and row.value == 'Off'
request = FeatureSettingsRequest(row.key, row.source, 'Bookmark', capability=row.capability)
params.put_bool('NostalgiaMode', True, block=True)
assert not feature.apply(request)
assert feature.choices(KEY) == ()
params.put_bool('NostalgiaMode', False, block=True)
assert feature.apply(request)
assert Path(params.get_param_path(KEY)).read_bytes() == b'8'
cp.flags = int(cp.flags | HyundaiFlags.CANFD_ALT_BUTTONS)
assert feature.choices(KEY) == ()
def test_paddle_receipt_reaches_native_bookmark_once(native_session, monkeypatch): # noqa: F811
runtime, session, ui, _, calls = native_session
save(ui.params)
owner = WheelPublisher(ui.params)
tick(owner, ui.params, ui.CP, True, 1)
commands = tick(owner, ui.params, ui.CP, False, 2)
assert commands == ((KEY, 8),)
sender = Publisher()
owner.publish(commands, ui.CP, sender, now_ns=NOW, drive_id=DRIVE, source_car_ns=NOW, source_control_ns=NOW)
event = messaging.log_from_bytes(sender.events[0][1])
queue = deque((event, event))
monkeypatch.setattr(runtime.messaging, 'recv_one_or_none', lambda _: queue.popleft() if queue else None)
session._poll_wheel(NOW)
assert calls == ['bookmark']
def test_neutral_and_unsupported_paddles_do_not_read_pause_setting(tmp_path, monkeypatch):
from openpilot.starpilot.controllers import wheel_actions
params = Params(str(tmp_path))
cp = _ioniq5_params()
save(params)
owner = WheelPublisher(params)
tick(owner, params, cp)
reads = []
read_saved = wheel_actions.read_saved
monkeypatch.setattr(wheel_actions, 'read_saved', lambda params, key, size: (reads.append(key), read_saved(params, key, size))[1])
for frame in range(1, 50):
tick(owner, params, cp, frame=frame)
assert reads == []
cp.flags = int(cp.flags | HyundaiFlags.CANFD_ALT_BUTTONS)
tick(owner, params, cp, True, 50)
tick(owner, params, cp, False, 51)
assert 'NostalgiaMode' not in reads