Files
StarPilot/openpilot/starpilot/aol/tests/test_ev6_physical_authorization.py
T

353 lines
18 KiB
Python

"""EV6 physical authorization and button intent."""
import unittest
from types import SimpleNamespace
from typing import cast
from opendbc.car import Bus, structs
from opendbc.car.hyundai.ev6_aol import Ev6PhysicalAuthorization, qualified
from opendbc.car.hyundai.interface import CarInterface
from opendbc.car.interfaces import CarInterfaceBase
from opendbc.car.hyundai.hyundaicanfd import CanBus
from opendbc.car.hyundai.values import Buttons, CAR, DBC, HyundaiFlags
from opendbc.can.packer import CANPacker
from openpilot.starpilot.aol.intent import AolSettings, AOL_TOGGLE, PAUSE_LATERAL, PAUSE_LONGITUDINAL
from openpilot.starpilot.car.hyundai.ev6_intent import Ev6CardIntent
def ev6_params():
# Factory remains stock. The actual captured/UDS prepublication owner alone promotes alpha.
from openpilot.starpilot.tests.test_ev6_startup import TestEV6Startup
ci, holder, _ = TestEV6Startup().exercise(mode='sent_restore')
try:
assert ci.CP.openpilotLongitudinalControl and ci.CP.safetyConfigs[-1].safetyParam == 0x15
return ci.CP.as_reader().as_builder()
finally:
holder.close()
class TestEv6PhysicalAuthorization(unittest.TestCase):
def test_exact_final_word_and_identity(self):
cp = ev6_params()
self.assertEqual(cp.safetyConfigs[0].safetyParam, 0x15)
self.assertTrue(qualified(cp))
cp.safetyConfigs[0].safetyParam = 0x815
cp.alternativeExperience = 32
self.assertTrue(qualified(cp, marked_only=True))
for identity in (CAR.KIA_EV6_2025, CAR.KIA_EV9, CAR.HYUNDAI_IONIQ_5):
bad = cp.as_reader().as_builder()
bad.carFingerprint = identity
self.assertFalse(qualified(bad))
for flag in (HyundaiFlags.CANFD_ALT_BUTTONS, HyundaiFlags.CANFD_LKA_STEER_MSG_ALT,
HyundaiFlags.CANFD_ANGLE_STEERING, HyundaiFlags.CCNC):
bad = cp.as_reader().as_builder()
bad.flags |= int(flag)
self.assertFalse(qualified(bad))
bad = cp.as_reader().as_builder()
bad.openpilotLongitudinalControl, bad.pcmCruise = False, True
self.assertFalse(qualified(bad))
def test_actual_parser_preserves_each_physical_sample(self):
cp = ev6_params()
interface = CarInterface(cp)
state, parsers = interface.CS, interface.can_parsers
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
warm_ns = 1_000_000
warm = [CANPacker(parser.dbc.name).make_can_msg(parser.dbc.addr_to_msg[address].name,
parser.bus, {'COUNTER': 0}) for parser in parsers.values() for address in sorted(parser.addresses)]
interface.update([(warm_ns, warm)])
self.assertEqual(parsers[Bus.pt].can_invalid_cnt, 0)
# Let required non-button bodies expire while the ordered button source remains current.
target_ns = warm_ns + int(max(message.timeout_threshold for message in parsers[Bus.pt].message_states.values()
if not message.ignore_alive)) + 1
samples = [(0, 0, Buttons.NONE), (1, 0, Buttons.NONE), (0, 0, Buttons.NONE),
(0, 1, Buttons.SET_DECEL), (0, 0, Buttons.NONE)]
messages = [packer.make_can_msg(state.cruise_btns_msg_canfd, CanBus(cp).ECAN,
{'ADAPTIVE_CRUISE_MAIN_BTN': main, 'LDA_BTN': lkas, 'CRUISE_BUTTONS': cruise, 'COUNTER': counter})
for counter, (main, lkas, cruise) in enumerate(samples, start=1)]
self.assertTrue(cp.flags & HyundaiFlags.CANFD)
interface.update([(target_ns, messages)])
self.assertEqual(parsers[Bus.pt].can_invalid_cnt, 1)
self.assertTrue(state.ev6_aol_source_healthy)
self.assertEqual(list(state.ev6_aol_samples), samples)
interface.update([(target_ns + 10_000_000, [])])
self.assertEqual(parsers[Bus.pt].can_invalid_cnt, 2)
self.assertEqual(state.ev6_aol_samples, ())
interface.update([(target_ns + state.ev6_aol_timeout_ns + 1_000_000_000, [])])
self.assertFalse(state.ev6_aol_source_healthy)
def test_authorization_is_union_and_optional_release_not_desired_intent(self):
for preference in (False, True):
for button in (Buttons.SET_DECEL, Buttons.RES_ACCEL):
owner = Ev6PhysicalAuthorization(lkas_on_engage=preference)
owner.observe(0, 0, button, main_available=True)
self.assertFalse(owner.authorized)
owner.observe(0, 0, Buttons.NONE, main_available=True)
self.assertEqual(owner.authorized, preference)
owner.observe(1, 0, Buttons.NONE, main_available=True)
self.assertTrue(owner.authorized)
owner.observe(1, 0, Buttons.NONE, main_available=True)
self.assertTrue(owner.authorized) # Held MAIN is not a second toggle.
owner.observe(0, 0, Buttons.NONE, main_available=False)
self.assertEqual(owner.authorized, preference) # MAIN loss preserves LKAS latch.
owner.revoke()
self.assertFalse(owner.authorized)
class TestEv6ConfiguredDesire(unittest.TestCase):
def owner(self, *, main=0, lkas=0, enabled=True, cancel=(0, 0, 0)):
return Ev6CardIntent(AolSettings(enabled, 0., lkas, main, (0, 0, 0), cancel))
def sample(self, owner, samples, *, available=True, active=False, events=(), valid=True, fault=False, acc_fault=False, cruise_enabled=False):
state = structs.CarState(canValid=valid, canTimeout=False, gearShifter='drive', buttonEvents=list(events))
state.cruiseState.available = available
state.cruiseState.enabled = cruise_enabled
state.accFaulted = acc_fault
now = getattr(owner, '_test_clock_ns', 90) + 10
owner._test_clock_ns = now
owner.observe_physical_samples(SimpleNamespace(ev6_aol_sample_stamp_ns=now,
ev6_aol_source_healthy=True, ev6_aol_timeout_ns=1_000_000_000, ev6_aol_samples=samples), now_ns=now)
owner.update(state, now_ns=now, fault_active=fault)
owner.observe_full_engagement(state, enabled=active, active=active, source_ns=now,
observation_valid=True, now_ns=now, fault_active=bool(fault))
return state
def test_exact_main_and_lkas_action_ownership(self):
for button in ('main', 'lkas'):
for action in (0, AOL_TOGGLE, PAUSE_LATERAL, PAUSE_LONGITUDINAL):
owner = self.owner(**{button: action})
self.sample(owner, [(0, 0, Buttons.NONE)])
pressed = (1, 0, Buttons.NONE) if button == 'main' else (0, 1, Buttons.NONE)
self.sample(owner, [pressed, (0, 0, Buttons.NONE)])
self.assertTrue(owner.physical.authorized)
self.assertTrue(owner.allowed_latch)
self.assertEqual(owner.pause_lateral, action == PAUSE_LATERAL)
self.assertEqual(owner.pause_longitudinal, action == PAUSE_LONGITUDINAL)
# The unassigned other input cannot toggle a managed desired owner.
if action == AOL_TOGGLE:
other = self.owner(**{button: action})
self.sample(other, [(0, 0, Buttons.NONE)])
opposite = (0, 1, Buttons.NONE) if button == 'main' else (1, 0, Buttons.NONE)
self.sample(other, [opposite, (0, 0, Buttons.NONE)])
self.assertTrue(other.physical.authorized)
self.assertFalse(other.allowed_latch)
def test_default_desire_depends_on_actual_main_not_only_physical_arm(self):
owner = self.owner()
self.sample(owner, [(0, 0, Buttons.NONE)], available=False)
cs = self.sample(owner, [(0, 1, Buttons.NONE), (0, 0, Buttons.NONE)], available=False)
self.assertTrue(owner.physical.authorized)
self.assertFalse(owner.output(cs)[0])
cs = self.sample(owner, [], available=True)
self.assertTrue(owner.output(cs)[0])
cs = self.sample(owner, [], available=False)
self.assertFalse(owner.output(cs)[0])
def test_set_release_authorization_and_actual_active_engagement_are_distinct(self):
for button in (Buttons.SET_DECEL, Buttons.RES_ACCEL):
for lkas_action in (0, AOL_TOGGLE):
owner = self.owner(lkas=lkas_action)
self.sample(owner, [(0, 0, Buttons.NONE)])
self.sample(owner, [(0, 0, button), (0, 0, Buttons.NONE)])
self.assertEqual(owner.physical.authorized, lkas_action == AOL_TOGGLE)
self.assertFalse(owner.allowed_latch) # Physical release alone never forces desire.
self.sample(owner, [], active=True)
self.assertTrue(owner.allowed_latch) # A successful fresh full engagement arms either configured mode.
def test_raw_manual_off_wins_over_full_engagement_without_net_button_event(self):
owner = self.owner(main=AOL_TOGGLE)
self.sample(owner, [(0, 0, Buttons.NONE)])
self.sample(owner, [(0, 1, Buttons.NONE), (0, 0, Buttons.NONE)])
self.sample(owner, [(1, 0, Buttons.NONE), (0, 0, Buttons.NONE)])
self.assertTrue(owner.allowed_latch and owner.physical.authorized)
cs = self.sample(owner, [(1, 0, Buttons.NONE), (0, 0, Buttons.NONE)], active=True)
self.assertTrue(owner.physical.authorized)
self.assertFalse(cs.buttonEvents)
self.assertFalse(owner.allowed_latch or owner.output(cs)[0])
self.sample(owner, [], active=True)
self.assertFalse(owner.allowed_latch)
def test_auxiliary_off_after_pending_activation_cannot_rearm_same_engagement(self):
owner = self.owner(lkas=AOL_TOGGLE)
self.sample(owner, [(0, 0, Buttons.NONE)])
cs = self.sample(owner, [(0, 1, Buttons.NONE), (0, 0, Buttons.NONE)])
self.assertTrue(owner.allowed_latch and owner.physical.authorized)
now = owner._test_clock_ns + 1
owner.observe_full_engagement(cs, enabled=True, active=False, source_ns=now,
observation_valid=True, now_ns=now, fault_active=False)
owner._perform(AOL_TOGGLE)
self.assertFalse(owner.allowed_latch)
self.sample(owner, [], active=True)
self.assertFalse(owner.allowed_latch)
def test_cold_held_no_source_and_unphysical_auxiliary_cannot_create_output(self):
for main_action, lkas_action in ((0, 0), (AOL_TOGGLE, 0), (0, AOL_TOGGLE),
(PAUSE_LATERAL, PAUSE_LONGITUDINAL)):
owner = self.owner(main=main_action, lkas=lkas_action, cancel=(AOL_TOGGLE, 0, 0))
cs = self.sample(owner, [(1, 0, Buttons.NONE)])
self.assertFalse(owner.output(cs)[0])
owner._perform(AOL_TOGGLE)
self.assertFalse(owner.output(cs)[0])
cs = self.sample(owner, [(0, 0, Buttons.NONE)], valid=False)
self.assertFalse(owner.output(cs)[0])
owner.update_auxiliary(cs, media=None, media_eligible=False, fault_active=False)
self.assertFalse(owner.output(cs)[0])
def test_cancel_and_custom_proposals_remain_behind_physical_gate(self):
for main, lkas in ((0, 0), (AOL_TOGGLE, 0), (0, AOL_TOGGLE)):
with self.subTest(main=main, lkas=lkas):
owner = self.owner(main=main, lkas=lkas)
self.sample(owner, [(0, 0, Buttons.NONE)])
cs = self.sample(owner, [(1, int(lkas == AOL_TOGGLE), Buttons.NONE), (0, 0, Buttons.NONE)])
self.assertTrue(owner.output(cs)[0])
for pressed, raw in ((True, Buttons.CANCEL), (False, Buttons.NONE)):
cancel = structs.CarState.ButtonEvent(type='cancel', pressed=pressed)
cs = self.sample(owner, [(0, 0, raw)], events=[cancel])
owner.update_auxiliary(cs, fault_active=False)
self.assertTrue(owner.output(cs)[0])
owner.settings = AolSettings(True, 0., lkas, main, (0, 0, 0), (AOL_TOGGLE, 0, 0))
for pressed, raw in ((True, Buttons.CANCEL), (False, Buttons.NONE)):
cancel = structs.CarState.ButtonEvent(type='cancel', pressed=pressed)
cs = self.sample(owner, [(0, 0, raw)], events=[cancel])
owner.update_auxiliary(cs, fault_active=False)
self.assertFalse(owner.output(cs)[0])
owner.physical.revoke()
owner._perform(AOL_TOGGLE)
self.assertFalse(owner.output(cs)[0])
def test_retained_fault_cannot_be_bypassed_by_default_or_fresh_mapped_press(self):
for main in (0, AOL_TOGGLE):
owner = self.owner(main=main)
self.sample(owner, [(0, 0, Buttons.NONE)])
cs = self.sample(owner, [(1, 0, Buttons.NONE), (0, 0, Buttons.NONE)])
self.assertTrue(owner.output(cs)[0])
cs = self.sample(owner, [], fault=True)
self.assertFalse(owner.output(cs)[0])
cs = self.sample(owner, [(0, 0, Buttons.NONE), (1, 0, Buttons.NONE), (0, 0, Buttons.NONE)], fault=None)
self.assertTrue(owner._fault_inhibit)
self.assertFalse(owner.allowed_latch)
self.assertFalse(owner.output(cs)[0])
owner._perform(AOL_TOGGLE)
self.assertFalse(owner.allowed_latch)
cs = self.sample(owner, [(0, 0, Buttons.NONE), (1, 0, Buttons.NONE), (0, 0, Buttons.NONE)], fault=False)
self.assertFalse(owner._fault_inhibit)
self.assertTrue(owner.output(cs)[0])
cs = self.sample(owner, [(0, 0, Buttons.NONE), (1, 0, Buttons.NONE)], acc_fault=True)
self.assertFalse(owner.allowed_latch)
self.assertFalse(owner.output(cs)[0])
def test_raw_neutral_is_required_after_cold_held_and_restart(self):
for main in (0, AOL_TOGGLE):
owner = self.owner(main=main)
cs = self.sample(owner, [(1, 0, Buttons.NONE)])
self.assertFalse(owner._raw_neutral_seen)
self.assertFalse(owner.output(cs)[0])
cs = self.sample(owner, [(1, 0, Buttons.NONE)])
self.assertFalse(owner.output(cs)[0])
cs = self.sample(owner, [(0, 0, Buttons.NONE), (1, 0, Buttons.NONE), (0, 0, Buttons.NONE)])
self.assertTrue(owner.output(cs)[0])
cs = self.sample(owner, [], valid=False)
self.assertFalse(owner.output(cs)[0])
cs = self.sample(owner, [(1, 0, Buttons.NONE)])
self.assertFalse(owner.output(cs)[0])
cs = self.sample(owner, [(0, 0, Buttons.NONE), (1, 0, Buttons.NONE), (0, 0, Buttons.NONE)])
self.assertTrue(owner.output(cs)[0])
for cruise in (Buttons.SET_DECEL, Buttons.RES_ACCEL):
with self.subTest(cruise=cruise):
owner = self.owner(lkas=AOL_TOGGLE)
for restart in (False, True):
if restart:
self.sample(owner, [], valid=False)
cs = self.sample(owner, [(0, 0, cruise)], active=False)
self.assertFalse(owner._raw_neutral_seen)
self.assertFalse(owner.physical.authorized)
cs = self.sample(owner, [(0, 0, Buttons.NONE)], active=True)
self.assertTrue(owner._raw_neutral_seen)
self.assertFalse(owner.physical.authorized)
self.assertFalse(owner.output(cs)[0])
self.sample(owner, [(0, 0, cruise)], active=False)
cs = self.sample(owner, [(0, 0, Buttons.NONE)], active=True)
self.assertTrue(owner.physical.authorized)
self.assertTrue(owner.output(cs)[0])
def test_main_derived_auxiliary_off_persists_until_explicit_on(self):
owner = self.owner()
self.sample(owner, [(0, 0, Buttons.NONE)])
cs = self.sample(owner, [(1, 0, Buttons.NONE), (0, 0, Buttons.NONE)])
self.assertTrue(owner.output(cs)[0])
owner._perform(AOL_TOGGLE)
self.assertFalse(owner._controller_override)
cs = self.sample(owner, [])
self.assertFalse(owner.allowed_latch)
self.assertFalse(owner.output(cs)[0])
owner._perform(AOL_TOGGLE)
self.assertTrue(owner._controller_override)
cs = self.sample(owner, [])
self.assertTrue(owner.output(cs)[0])
def test_second_lkas_toggle_with_ordinary_engagement_pauses_steering(self):
owner = self.owner(lkas=AOL_TOGGLE)
self.sample(owner, [(0, 0, Buttons.NONE)])
cs = self.sample(owner, [(0, 1, Buttons.NONE), (0, 0, Buttons.NONE)])
self.assertTrue(owner.output(cs)[0])
self.sample(owner, [], active=True, cruise_enabled=True)
cs = self.sample(owner, [(0, 1, Buttons.NONE), (0, 0, Buttons.NONE)], active=True, cruise_enabled=True)
self.assertFalse(owner.physical.authorized)
self.assertFalse(owner.allowed_latch)
self.assertTrue(owner.pause_lateral)
self.assertFalse(owner.output(cs)[0])
owner._perform(AOL_TOGGLE)
self.assertFalse(owner.allowed_latch) # ON remains denied without physical authorization.
self.assertTrue(owner.pause_lateral)
def test_current_params_thread_refreshes_optional_release_without_granting_authority(self):
import tempfile
import threading
from unittest.mock import patch
from openpilot.common.params import Params
from openpilot.selfdrive.car.card import Car
from openpilot.selfdrive.car.cruise import VCruiseHelper
from openpilot.starpilot.aol.intent import read_settings
with tempfile.TemporaryDirectory() as directory:
params = Params(directory)
params.put_bool('AlwaysOnLateral', True, block=True)
params.put('LKASButtonControl', 0, block=True)
params.put('MainCruiseButtonControl', 0, block=True)
card = Car.__new__(Car)
card.params, card.CP = params, ev6_params()
card.CI = cast(CarInterfaceBase, SimpleNamespace(CP=card.CP, CC=None))
card.slc_transport_available = False
card.v_cruise_helper = VCruiseHelper(card.CP)
card.aol_card_intent = Ev6CardIntent(read_settings(params))
owner = card.aol_card_intent
cp_before = card.CP.as_reader().as_builder().to_bytes()
def refresh():
stop = threading.Event()
with patch('openpilot.selfdrive.car.card.time.sleep', side_effect=lambda _: stop.set()):
card.params_thread(stop)
self.sample(owner, [(0, 0, Buttons.NONE)])
params.put('LKASButtonControl', AOL_TOGGLE, block=True)
refresh()
cs = self.sample(owner, [])
self.assertTrue(owner.physical.lkas_on_engage)
self.assertFalse(owner.physical.authorized)
self.assertFalse(owner.output(cs)[0])
cs = self.sample(owner, [(0, 0, Buttons.SET_DECEL), (0, 0, Buttons.NONE)])
self.assertTrue(owner.physical.authorized)
self.assertFalse(owner.output(cs)[0])
cs = self.sample(owner, [], active=True)
self.assertTrue(owner.output(cs)[0])
for action, enabled in ((0, True), (PAUSE_LONGITUDINAL, True), (AOL_TOGGLE, False)):
self.sample(owner, [], valid=False)
params.put('LKASButtonControl', action, block=True)
params.put_bool('AlwaysOnLateral', enabled, block=True)
refresh()
cs = self.sample(owner, [(0, 0, Buttons.NONE), (0, 0, Buttons.RES_ACCEL), (0, 0, Buttons.NONE)])
self.assertFalse(owner.physical.lkas_on_engage)
self.assertFalse(owner.physical.authorized)
self.assertFalse(owner.output(cs)[0])
self.assertEqual(card.CP.as_reader().as_builder().to_bytes(), cp_before)