Files
StarPilot/openpilot/starpilot/tests/test_nostalgia.py
T
2026-10-20 15:49:56 -05:00

246 lines
11 KiB
Python

"""Left-paddle longitudinal pause preserves only separately qualified AOL steering."""
from pathlib import Path
from tempfile import TemporaryDirectory
from types import SimpleNamespace
from unittest.mock import patch
from opendbc.car import gen_empty_fingerprint
from opendbc.car.hyundai.interface import CarInterface as HyundaiInterface
from opendbc.car.hyundai.ioniq6_handoff import build_ioniq6_hda2_long_candidate
from opendbc.car.hyundai.values import CAR as HyundaiCar
from opendbc.car.structs import car
from openpilot.cereal import log
from openpilot.common.params import Params
from openpilot.selfdrive.selfdrived.events import Events, EventName
from openpilot.selfdrive.selfdrived.state import StateMachine
from openpilot.starpilot.aol.intent import AolCardIntent, AolSettings
from openpilot.starpilot.aol.runtime import decide_axes
from openpilot.starpilot.feature_runtime import enabled as feature_enabled, requested as feature_requested
from openpilot.starpilot.nostalgia import aol_no_entry, paddle_cancel, saved_enabled
ButtonType = car.CarState.ButtonEvent.Type
class Files:
def __init__(self, root: Path):
self.root = root
def get_param_path(self, key: str) -> str:
return str(self.root / key)
def _car(*events):
return SimpleNamespace(canValid=True, canTimeout=False, buttonEvents=events,
gearShifter=car.CarState.GearShifter.drive,
steerFaultPermanent=False, steerFaultTemporary=False,
brakePressed=False, vEgo=12.0, standstill=False, accFaulted=False,
cruiseState=SimpleNamespace(available=True))
def _button(kind, pressed):
return car.CarState.ButtonEvent(type=kind, pressed=pressed)
def test_saved_preference_is_strict_and_absent_off():
with TemporaryDirectory() as directory:
params = Files(Path(directory))
path = Path(params.get_param_path('NostalgiaMode'))
assert not saved_enabled(params)
for invalid in (b'01', b'true', b'1' * 20):
path.write_bytes(invalid)
assert not saved_enabled(params)
assert path.read_bytes() == invalid
path.write_bytes(b'1')
assert saved_enabled(params)
def test_normal_aol_owner_needs_saved_request_and_exact_tagged_ioniq_cp():
with TemporaryDirectory() as directory:
params = Params(directory)
fp = gen_empty_fingerprint()
fp[2].update({0x50: 16, 0x2A4: 24})
fp[1].update({0x1CF: 8, 0x1AA: 16, 0x35: 32, 0x175: 24, 0xA0: 24, 0xEA: 24,
0x1BA: 24, 0x1E5: 16, 0x36A: 16})
fp[0][0x3A5] = 24
stock = HyundaiInterface.get_params(HyundaiCar.HYUNDAI_IONIQ_6, fp, [], False, False, False)
assert not feature_enabled(params, stock, 'aol', {})
Path(params.get_param_path('AlwaysOnLateral')).write_bytes(b'1')
assert feature_requested(params, 'aol')
assert feature_enabled(params, stock, 'aol', {})
tagged = build_ioniq6_hda2_long_candidate(stock, fp)
assert tagged is not None
assert not feature_enabled(params, tagged, 'aol', {}) # Card has not requested native AOL bit.
tagged.safetyConfigs[0].safetyParam |= 0x800
assert feature_enabled(params, tagged, 'aol', {})
Path(params.get_param_path('AlwaysOnLateral')).write_bytes(b'01')
assert not feature_requested(params, 'aol')
from openpilot.starpilot.aol.intent import read_settings
from openpilot.starpilot.aol.vehicle import policy_for
from openpilot.starpilot.car.hyundai.aol import create_intent
assert policy_for(tagged).full_axis_runtime_required
assert feature_enabled(params, tagged, 'aol', {})
settings = read_settings(params)
assert not settings.enabled
owner = create_intent(tagged, settings)
for state in (_car(), _car(_button(ButtonType.lkas, True)), _car(_button(ButtonType.lkas, False))):
owner.update(state)
assert not owner.allowed_latch
assert not owner.output(state)[0]
def test_only_factual_ioniq_press_cancels_longitudinal():
press = _car(_button(ButtonType.altButton2, True))
held = _car()
release = _car(_button(ButtonType.altButton2, False))
with patch('openpilot.starpilot.nostalgia.ioniq6_long_eligible', side_effect=lambda cp: cp == 'tagged'):
assert paddle_cancel('tagged', press, enabled=True, saved=True)
for car_state in (held, release):
assert not paddle_cancel('tagged', car_state, enabled=True, saved=True)
assert not paddle_cancel('stock', press, enabled=True, saved=True)
assert not paddle_cancel('tagged', press, enabled=False, saved=True)
assert not paddle_cancel('tagged', press, enabled=True, saved=False)
press.canTimeout = True
assert not paddle_cancel('tagged', press, enabled=True, saved=True)
def test_paddle_user_disable_keeps_only_independently_permitted_lateral():
events = Events()
events.add(EventName.buttonCancel)
state = StateMachine()
state.state = log.SelfdriveState.OpenpilotState.enabled
enabled, active = state.update(events)
assert not enabled and not active # ordinary longitudinal state machine sees Cancel
paddle = _car(_button(ButtonType.altButton2, True))
assert not aol_no_entry(events.names, paddle, paddle_only_cancel=True)
native = SimpleNamespace(requestedLateral=True, requestedLongitudinal=False,
lateralAllowed=True, longitudinalAllowed=True)
intent = SimpleNamespace(allowedLatch=True, pauseLateral=False, pauseLongitudinal=False)
decision = decide_axes(standard_lateral=active, standard_longitudinal=enabled,
intent=intent, native=native, car_state=paddle,
initialized=True, model_ready=True, no_entry=False,
immediate_disable=False, dm_lockout=False, pause_brake_mps=0)
assert decision.lateral_active and not decision.longitudinal_active
# With AOL off, or without its saved latch, both axes stay off after the press.
assert not active and not enabled
intent.allowedLatch = False
decision = decide_axes(standard_lateral=active, standard_longitudinal=enabled,
intent=intent, native=native, car_state=paddle,
initialized=True, model_ready=True, no_entry=False,
immediate_disable=False, dm_lockout=False, pause_brake_mps=0)
assert not decision.lateral_active and not decision.longitudinal_active
# A real Cancel on the same frame retains the normal all-axis veto.
paddle.buttonEvents = (*paddle.buttonEvents, _button(ButtonType.cancel, True))
assert aol_no_entry(events.names, paddle, paddle_only_cancel=True)
assert aol_no_entry(events.names, _car(), paddle_only_cancel=False)
def test_latch_survives_press_and_next_frame_then_real_cancel_requires_rearm():
intent = AolCardIntent(AolSettings(True, 0.0, 0, 0, (0, 0, 0), (0, 0, 0)), explicit_latch=True)
neutral = _car()
intent.update(neutral)
arm = _car(_button(ButtonType.lkas, True))
intent.update(arm)
assert intent.output(arm)[0]
intent.update(_car(_button(ButtonType.lkas, False)))
state = StateMachine()
state.state = log.SelfdriveState.OpenpilotState.enabled
press = _car(_button(ButtonType.altButton2, True))
intent.update(press)
events = Events()
events.add(EventName.buttonCancel)
enabled, active = state.update(events)
assert not enabled and not active and intent.output(press)[0]
assert not aol_no_entry(events.names, press, paddle_only_cancel=True)
next_frame = _car()
intent.update(next_frame)
enabled, active = state.update(Events())
assert not enabled and not active and intent.output(next_frame)[0]
cancel = _car(_button(ButtonType.cancel, True))
intent.update(cancel)
assert not intent.output(cancel)[0]
assert aol_no_entry(events.names, cancel, paddle_only_cancel=True)
intent.update(_car(_button(ButtonType.cancel, False)))
assert not intent.output(neutral)[0]
intent.update(_car(_button(ButtonType.lkas, True)))
assert intent.output(neutral)[0] # A new deliberate host gesture rearms steering.
def _ioniq5_params(*, alternate=False, long=True):
fp = gen_empty_fingerprint()
fp[2].update({0x50: 16, 0x2A4: 24})
fp[1][0x1AA if alternate else 0x1CF] = 16 if alternate else 8
cp = HyundaiInterface.get_params(HyundaiCar.HYUNDAI_IONIQ_5, fp, [], long, False, False)
if long and not alternate:
from opendbc.car.hyundai.torque_ev_startup import candidate
cp = candidate(cp, requested=True, is_release=False)
assert cp is not None
from openpilot.starpilot.car.hyundai.aol import policy_for
policy = policy_for(cp)
cp.safetyConfigs[-1].safetyParam |= policy.safety_param_addition
cp.alternativeExperience |= policy.alternative_experience_addition
return cp
def test_ioniq5_physical_parser_emits_paddle_edges_with_existing_long_policy():
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
from opendbc.car.hyundai.values import DBC
from openpilot.starpilot.car.hyundai.aol import policy_for
cp = _ioniq5_params()
assert cp.openpilotLongitudinalControl
assert policy_for(cp).paddle_pause and policy_for(cp).runtime_supported
case = StockParserTests(methodName='runTest')
ci = case.host(cp)
case.feed(ci, count=5)
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
for counter, pressed in ((6, True), (7, False)):
ci.test_time_ns += 10_000_000
msg = packer.make_can_msg('CRUISE_BUTTONS', CanBus(cp).ECAN,
{'COUNTER': counter, 'LEFT_PADDLE': int(pressed)})
state = ci.update([(ci.test_time_ns, [msg])])
events = [e for e in state.buttonEvents if e.type == ButtonType.altButton2]
assert len(events) == 1 and events[0].pressed == pressed
assert paddle_cancel(cp, state, enabled=True, saved=True) == pressed
for cp in (_ioniq5_params(alternate=True), _ioniq5_params(long=False)):
assert not policy_for(cp).paddle_pause
assert not paddle_cancel(cp, _car(_button(ButtonType.altButton2, True)), enabled=True, saved=True)
def test_actual_ioniq5_paddle_withdraws_long_without_cancelling_aol():
from contextlib import ExitStack
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.starpilot.aol.tests.test_ioniq_availability import IoniqAvailabilityTests, car_state
case = IoniqAvailabilityTests(methodName='runTest')
with OpenpilotPrefix(), ExitStack() as cleanup:
cleanup.callback(case.doCleanups)
params = Params()
params.put_bool('NostalgiaMode', True, block=True)
cp = _ioniq5_params()
sd, _, drive = case._authority_callers(True, cp=cp, genuine_events=True)
axis, _, command = drive(0)
assert command.latActive and command.longActive
state = car_state()
state.buttonEvents = [_button(ButtonType.altButton2, True)]
axis, _, command = drive(1, observed_state=state)
assert sd.nostalgia_paddle_cancel
assert not sd.enabled and not command.longActive and not axis.longitudinalActive
assert command.latActive and axis.lateralActive
assert EventName.buttonCancel in sd.events.names
axis, _, command = drive(2, native_request_long=False, long_allowed=False)
assert axis.lateralActive and command.latActive and not command.longActive
state.buttonEvents = [_button(ButtonType.cancel, True)]
axis, _, command = drive(3, observed_state=state, native_request_long=False, long_allowed=False)
assert not command.latActive and not command.longActive