mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
246 lines
11 KiB
Python
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
|