Files
StarPilot/openpilot/starpilot/controllers/tests/test_wheel_actions.py
T
2026-10-22 12:51:07 -05:00

136 lines
5.7 KiB
Python

from types import SimpleNamespace as NS
import pytest
from opendbc.car.structs import car
from openpilot.cereal import messaging
from openpilot.common.params import Params
from openpilot.selfdrive.car.card import Car
from openpilot.selfdrive.car.tests.publication_fixture import initialize_publication_sources
from openpilot.selfdrive.car.cruise import CRUISE_LONG_PRESS, VCruiseHelper
from openpilot.selfdrive.car.tests.test_hyundai_aol import candidate
from openpilot.starpilot.controllers.wheel_actions import ACTIONS, KEYS, WheelConsumer, WheelPublisher, capture
TYPE = car.CarState.ButtonEvent.Type
DRIVE = 1_000_000_000
NOW = DRIVE + 2_000_000_000
def state(*pressed, button=TYPE.gapAdjustCruise):
return car.CarState(canValid=True, buttonEvents=[car.CarState.ButtonEvent(type=button, pressed=value) for value in pressed])
class Publisher:
def __init__(self):
self.events = []
def send(self, service, event):
self.events.append((service, event.to_bytes()))
def services(cp, now=NOW):
names = ('deviceState', 'carState', 'carControl')
data = {'deviceState': NS(started=True, startedMonoTime=DRIVE), 'carState': state(),
'carControl': car.CarControl(enabled=True, longActive=True)}
class Services(dict):
pass
sm = Services(data)
sm.logMonoTime = dict.fromkeys(names, now)
sm.recv_time = dict.fromkeys(names, now / 1e9)
sm.seen = sm.alive = sm.valid = dict.fromkeys(names, True)
sm.frame = 1
sm.all_checks = lambda _: True
return sm
@pytest.fixture
def context(tmp_path):
params = Params(str(tmp_path))
_, cp = candidate(False)
return params, cp
def test_exact_catalog_and_missing_vs_saved_off(context):
params, _ = context
assert sorted(ACTIONS.values()) == list(range(15))
assert len(KEYS) == 15
assert capture(params)['DistanceButtonControl'] == 1
assert capture(params)['LongDistanceButtonControl'] == 5
assert capture(params)['VeryLongDistanceButtonControl'] == 6
params.put('DistanceButtonControl', 0, block=True)
assert capture(params)['DistanceButtonControl'] == 0
@pytest.mark.parametrize('key,code', [('DistanceButtonControl', code) for code in (1, 2, 5, 6, 7, 8, 11, 12, 13, 14)])
def test_real_distance_short_delivers_once_and_claims_serialized_release(context, key, code):
params, cp = context
params.put(key, code, block=True)
owner = WheelPublisher()
owner.observe(params, cp, state(), now_ns=NOW, drive_id=DRIVE)
assert not any(action for _, action in owner.observe(params, cp, state(True), now_ns=NOW + 10_000_000, drive_id=DRIVE))
commands = owner.observe(params, cp, state(False), now_ns=NOW + 20_000_000, drive_id=DRIVE)
assert commands == ((key, code),)
assert owner.suppress_distance_release
assert not any(action for _, action in owner.observe(params, cp, state(), now_ns=NOW + 30_000_000, drive_id=DRIVE))
def test_held_long_then_very_long_exactly_once_no_short_release(context):
params, cp = context
owner = WheelPublisher()
owner.observe(params, cp, state(), now_ns=NOW, drive_id=DRIVE)
observed = []
for frame in range(1, CRUISE_LONG_PRESS * 5 + 3):
commands = owner.observe(params, cp, state(True) if frame == 1 else state(),
now_ns=NOW + frame * 10_000_000, drive_id=DRIVE)
observed.extend((frame, key, action) for key, action in commands if action)
assert observed == [(CRUISE_LONG_PRESS, 'LongDistanceButtonControl', 5),
(CRUISE_LONG_PRESS * 5, 'VeryLongDistanceButtonControl', 6)]
assert not any(action for _, action in owner.observe(params, cp, state(False),
now_ns=NOW + (CRUISE_LONG_PRESS * 5 + 4) * 10_000_000, drive_id=DRIVE))
def test_published_receipt_exact_cp_session_replay_and_delayed_source(context):
params, cp = context
params.put('DistanceButtonControl', 8, block=True)
publisher = Publisher()
owner = WheelPublisher()
owner.publish((('DistanceButtonControl', 8),), cp, publisher, now_ns=NOW, drive_id=DRIVE,
source_car_ns=NOW, source_control_ns=NOW)
event = messaging.log_from_bytes(publisher.events[0][1])
sm = services(cp)
consumer = WheelConsumer()
assert consumer.accept(event, params, cp, sm, now_ns=NOW).action == 8
assert consumer.accept(event, params, cp, sm, now_ns=NOW) is None
changed = cp.as_reader().as_builder()
changed.pcmCruise = not cp.pcmCruise
assert WheelConsumer().accept(event, params, changed.as_reader(), sm, now_ns=NOW) is None
sm.recv_time['carState'] = (NOW - 300_000_000) / 1e9
assert WheelConsumer().accept(event, params, cp, sm, now_ns=NOW) is None
def test_actual_card_published_distance_off_cannot_trigger_unconditional_personality(context):
params, cp = context
params.put('DistanceButtonControl', 0, block=True)
owner = WheelPublisher()
sm = services(cp)
owner.observe(params, cp, state(), now_ns=NOW, drive_id=DRIVE)
owner.observe(params, cp, state(True), now_ns=NOW + 10_000_000, drive_id=DRIVE)
physical = state(False)
commands = owner.observe(params, cp, physical, now_ns=NOW + 20_000_000, drive_id=DRIVE)
instance = Car.__new__(Car)
instance.CP, instance.params, instance.sm = cp, params, sm
vars(instance)['pm'] = Publisher()
instance.wheel_publisher, instance.wheel_commands = owner, commands
instance.slc_replay = instance.curve_replay = instance.conditional_replay = instance.aol_replay = False
instance.last_actuators_output = car.CarControl.Actuators()
instance.can_rcv_cum_timeout_counter = 0
vars(instance)['rk'] = NS(remaining=0.0)
instance.v_cruise_helper = VCruiseHelper(cp)
instance.slc_receipts = []
initialize_publication_sources(instance)
instance.state_publish(physical, None)
serialized = next(messaging.log_from_bytes(raw).carState for name, raw in instance.pm.events if name == 'carState')
assert len(serialized.buttonEvents) == 0
assert len(physical.buttonEvents) == 1