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